diff --git a/api/client/client.gen.go b/api/client/client.gen.go index e09ec674d..6905c2946 100644 --- a/api/client/client.gen.go +++ b/api/client/client.gen.go @@ -102,7 +102,7 @@ type ClientInterface interface { DeleteScanConfigsScanConfigID(ctx context.Context, scanConfigID ScanConfigID, reqEditors ...RequestEditorFn) (*http.Response, error) // GetScanConfigsScanConfigID request - GetScanConfigsScanConfigID(ctx context.Context, scanConfigID ScanConfigID, reqEditors ...RequestEditorFn) (*http.Response, error) + GetScanConfigsScanConfigID(ctx context.Context, scanConfigID ScanConfigID, params *GetScanConfigsScanConfigIDParams, reqEditors ...RequestEditorFn) (*http.Response, error) // PatchScanConfigsScanConfigID request with any body PatchScanConfigsScanConfigIDWithBody(ctx context.Context, scanConfigID ScanConfigID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -227,8 +227,8 @@ func (c *Client) DeleteScanConfigsScanConfigID(ctx context.Context, scanConfigID return c.Client.Do(req) } -func (c *Client) GetScanConfigsScanConfigID(ctx context.Context, scanConfigID ScanConfigID, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetScanConfigsScanConfigIDRequest(c.Server, scanConfigID) +func (c *Client) GetScanConfigsScanConfigID(ctx context.Context, scanConfigID ScanConfigID, params *GetScanConfigsScanConfigIDParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetScanConfigsScanConfigIDRequest(c.Server, scanConfigID, params) if err != nil { return nil, err } @@ -612,9 +612,9 @@ func NewGetScanConfigsRequest(server string, params *GetScanConfigsParams) (*htt } - if params.Page != nil { + if params.Select != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "page", runtime.ParamLocationQuery, *params.Page); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "$select", runtime.ParamLocationQuery, *params.Select); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -628,9 +628,57 @@ func NewGetScanConfigsRequest(server string, params *GetScanConfigsParams) (*htt } - if params.PageSize != nil { + if params.Count != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "pageSize", runtime.ParamLocationQuery, *params.PageSize); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "$count", runtime.ParamLocationQuery, *params.Count); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Top != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "$top", runtime.ParamLocationQuery, *params.Top); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Skip != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "$skip", runtime.ParamLocationQuery, *params.Skip); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Expand != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "$expand", runtime.ParamLocationQuery, *params.Expand); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -729,7 +777,7 @@ func NewDeleteScanConfigsScanConfigIDRequest(server string, scanConfigID ScanCon } // NewGetScanConfigsScanConfigIDRequest generates requests for GetScanConfigsScanConfigID -func NewGetScanConfigsScanConfigIDRequest(server string, scanConfigID ScanConfigID) (*http.Request, error) { +func NewGetScanConfigsScanConfigIDRequest(server string, scanConfigID ScanConfigID, params *GetScanConfigsScanConfigIDParams) (*http.Request, error) { var err error var pathParam0 string @@ -754,6 +802,42 @@ func NewGetScanConfigsScanConfigIDRequest(server string, scanConfigID ScanConfig return nil, err } + queryValues := queryURL.Query() + + if params.Select != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "$select", runtime.ParamLocationQuery, *params.Select); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Expand != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "$expand", runtime.ParamLocationQuery, *params.Expand); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err @@ -1709,7 +1793,7 @@ type ClientWithResponsesInterface interface { DeleteScanConfigsScanConfigIDWithResponse(ctx context.Context, scanConfigID ScanConfigID, reqEditors ...RequestEditorFn) (*DeleteScanConfigsScanConfigIDResponse, error) // GetScanConfigsScanConfigID request - GetScanConfigsScanConfigIDWithResponse(ctx context.Context, scanConfigID ScanConfigID, reqEditors ...RequestEditorFn) (*GetScanConfigsScanConfigIDResponse, error) + GetScanConfigsScanConfigIDWithResponse(ctx context.Context, scanConfigID ScanConfigID, params *GetScanConfigsScanConfigIDParams, reqEditors ...RequestEditorFn) (*GetScanConfigsScanConfigIDResponse, error) // PatchScanConfigsScanConfigID request with any body PatchScanConfigsScanConfigIDWithBodyWithResponse(ctx context.Context, scanConfigID ScanConfigID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PatchScanConfigsScanConfigIDResponse, error) @@ -2346,8 +2430,8 @@ func (c *ClientWithResponses) DeleteScanConfigsScanConfigIDWithResponse(ctx cont } // GetScanConfigsScanConfigIDWithResponse request returning *GetScanConfigsScanConfigIDResponse -func (c *ClientWithResponses) GetScanConfigsScanConfigIDWithResponse(ctx context.Context, scanConfigID ScanConfigID, reqEditors ...RequestEditorFn) (*GetScanConfigsScanConfigIDResponse, error) { - rsp, err := c.GetScanConfigsScanConfigID(ctx, scanConfigID, reqEditors...) +func (c *ClientWithResponses) GetScanConfigsScanConfigIDWithResponse(ctx context.Context, scanConfigID ScanConfigID, params *GetScanConfigsScanConfigIDParams, reqEditors ...RequestEditorFn) (*GetScanConfigsScanConfigIDResponse, error) { + rsp, err := c.GetScanConfigsScanConfigID(ctx, scanConfigID, params, reqEditors...) if err != nil { return nil, err } diff --git a/api/models/models.gen.go b/api/models/models.gen.go index c6eae4027..c1c352137 100644 --- a/api/models/models.gen.go +++ b/api/models/models.gen.go @@ -362,11 +362,11 @@ type ScanConfigRelationship struct { // ScanConfigs defines model for ScanConfigs. type ScanConfigs struct { + // Count Total scan config count according to the given filters + Count *int `json:"count,omitempty"` + // Items List of scan configs according to the given filters and page. List length must be lower or equal to pageSize. Items *[]ScanConfig `json:"items,omitempty"` - - // Total Total scan config count according to the given filters - Total *int `json:"total,omitempty"` } // ScanExists defines model for ScanExists. @@ -619,12 +619,24 @@ type WeeklyScheduleScanConfig struct { TimeOfDay *TimeOfDay `json:"timeOfDay,omitempty"` } +// OdataCount defines model for odataCount. +type OdataCount = bool + +// OdataExpand defines model for odataExpand. +type OdataExpand = string + // OdataFilter defines model for odataFilter. type OdataFilter = string // OdataSelect defines model for odataSelect. type OdataSelect = string +// OdataSkip defines model for odataSkip. +type OdataSkip = int + +// OdataTop defines model for odataTop. +type OdataTop = int + // Page defines model for page. type Page = int @@ -652,12 +664,17 @@ type UnknownError = ApiResponse // GetScanConfigsParams defines parameters for GetScanConfigs. type GetScanConfigsParams struct { Filter *OdataFilter `form:"$filter,omitempty" json:"$filter,omitempty"` + Select *OdataSelect `form:"$select,omitempty" json:"$select,omitempty"` + Count *OdataCount `form:"$count,omitempty" json:"$count,omitempty"` + Top *OdataTop `form:"$top,omitempty" json:"$top,omitempty"` + Skip *OdataSkip `form:"$skip,omitempty" json:"$skip,omitempty"` + Expand *OdataExpand `form:"$expand,omitempty" json:"$expand,omitempty"` +} - // Page Page number of the query - Page *Page `form:"page,omitempty" json:"page,omitempty"` - - // PageSize Maximum items to return - PageSize *PageSize `form:"pageSize,omitempty" json:"pageSize,omitempty"` +// GetScanConfigsScanConfigIDParams defines parameters for GetScanConfigsScanConfigID. +type GetScanConfigsScanConfigIDParams struct { + Select *OdataSelect `form:"$select,omitempty" json:"$select,omitempty"` + Expand *OdataExpand `form:"$expand,omitempty" json:"$expand,omitempty"` } // GetScanResultsParams defines parameters for GetScanResults. diff --git a/api/openapi.yaml b/api/openapi.yaml index 8396db0e5..27e3da437 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -362,8 +362,11 @@ paths: summary: Get all scan configs. parameters: - $ref: '#/components/parameters/odataFilter' - - $ref: '#/components/parameters/page' - - $ref: '#/components/parameters/pageSize' + - $ref: '#/components/parameters/odataSelect' + - $ref: '#/components/parameters/odataCount' + - $ref: '#/components/parameters/odataTop' + - $ref: '#/components/parameters/odataSkip' + - $ref: '#/components/parameters/odataExpand' responses: 200: description: Success @@ -404,6 +407,8 @@ paths: summary: Get the details for a scan config. parameters: - $ref: '#/components/parameters/scanConfigID' + - $ref: '#/components/parameters/odataSelect' + - $ref: '#/components/parameters/odataExpand' responses: 200: description: Success @@ -687,7 +692,7 @@ components: ScanConfigs: type: object properties: - total: + count: type: integer description: Total scan config count according to the given filters readOnly: true @@ -698,8 +703,6 @@ components: items: $ref: '#/components/schemas/ScanConfig' readOnly: true - required: - - total ScanConfigData: type: object @@ -1393,6 +1396,30 @@ components: schema: type: string + odataCount: + name: "$count" + in: query + schema: + type: boolean + + odataTop: + name: "$top" + in: query + schema: + type: integer + + odataSkip: + name: "$skip" + in: query + schema: + type: integer + + odataExpand: + name: "$expand" + in: query + schema: + type: string + scanID: name: scanID in: path diff --git a/api/server/server.gen.go b/api/server/server.gen.go index 684632740..2a19f2cd2 100644 --- a/api/server/server.gen.go +++ b/api/server/server.gen.go @@ -32,7 +32,7 @@ type ServerInterface interface { DeleteScanConfigsScanConfigID(ctx echo.Context, scanConfigID ScanConfigID) error // Get the details for a scan config. // (GET /scanConfigs/{scanConfigID}) - GetScanConfigsScanConfigID(ctx echo.Context, scanConfigID ScanConfigID) error + GetScanConfigsScanConfigID(ctx echo.Context, scanConfigID ScanConfigID, params GetScanConfigsScanConfigIDParams) error // Patch a scan config. // (PATCH /scanConfigs/{scanConfigID}) PatchScanConfigsScanConfigID(ctx echo.Context, scanConfigID ScanConfigID) error @@ -107,18 +107,39 @@ func (w *ServerInterfaceWrapper) GetScanConfigs(ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter $filter: %s", err)) } - // ------------- Optional query parameter "page" ------------- + // ------------- Optional query parameter "$select" ------------- - err = runtime.BindQueryParameter("form", true, false, "page", ctx.QueryParams(), ¶ms.Page) + err = runtime.BindQueryParameter("form", true, false, "$select", ctx.QueryParams(), ¶ms.Select) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter page: %s", err)) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter $select: %s", err)) } - // ------------- Optional query parameter "pageSize" ------------- + // ------------- Optional query parameter "$count" ------------- - err = runtime.BindQueryParameter("form", true, false, "pageSize", ctx.QueryParams(), ¶ms.PageSize) + err = runtime.BindQueryParameter("form", true, false, "$count", ctx.QueryParams(), ¶ms.Count) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter pageSize: %s", err)) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter $count: %s", err)) + } + + // ------------- Optional query parameter "$top" ------------- + + err = runtime.BindQueryParameter("form", true, false, "$top", ctx.QueryParams(), ¶ms.Top) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter $top: %s", err)) + } + + // ------------- Optional query parameter "$skip" ------------- + + err = runtime.BindQueryParameter("form", true, false, "$skip", ctx.QueryParams(), ¶ms.Skip) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter $skip: %s", err)) + } + + // ------------- Optional query parameter "$expand" ------------- + + err = runtime.BindQueryParameter("form", true, false, "$expand", ctx.QueryParams(), ¶ms.Expand) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter $expand: %s", err)) } // Invoke the callback with all the unmarshalled arguments @@ -162,8 +183,24 @@ func (w *ServerInterfaceWrapper) GetScanConfigsScanConfigID(ctx echo.Context) er return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter scanConfigID: %s", err)) } + // Parameter object where we will unmarshal all parameters from the context + var params GetScanConfigsScanConfigIDParams + // ------------- Optional query parameter "$select" ------------- + + err = runtime.BindQueryParameter("form", true, false, "$select", ctx.QueryParams(), ¶ms.Select) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter $select: %s", err)) + } + + // ------------- Optional query parameter "$expand" ------------- + + err = runtime.BindQueryParameter("form", true, false, "$expand", ctx.QueryParams(), ¶ms.Expand) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter $expand: %s", err)) + } + // Invoke the callback with all the unmarshalled arguments - err = w.Handler.GetScanConfigsScanConfigID(ctx, scanConfigID) + err = w.Handler.GetScanConfigsScanConfigID(ctx, scanConfigID, params) return err } @@ -554,74 +591,75 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "eAHPUE7g8+fn/w8AAP//xe3zmOV0AAA=", + "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", + "3zvolDyHZ/AEFQQ+fXn6/wAAAP//gJMYvdp2AAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/backend/cmd/backend/main.go b/backend/cmd/backend/main.go index 5cd44499d..3eaa530ad 100644 --- a/backend/cmd/backend/main.go +++ b/backend/cmd/backend/main.go @@ -26,7 +26,7 @@ import ( "github.com/openclarity/vmclarity/backend/pkg/backend" "github.com/openclarity/vmclarity/backend/pkg/config" - "github.com/openclarity/vmclarity/backend/pkg/database" + databaseTypes "github.com/openclarity/vmclarity/backend/pkg/database/types" "github.com/openclarity/vmclarity/backend/pkg/version" ) @@ -43,7 +43,7 @@ func versionCommand(_ *cli.Context) { func main() { viper.SetDefault(config.HealthCheckAddress, ":8081") viper.SetDefault(config.BackendRestPort, "8888") - viper.SetDefault(config.DatabaseDriver, database.DBDriverTypeLocal) + viper.SetDefault(config.DatabaseDriver, databaseTypes.DBDriverTypeLocal) viper.AutomaticEnv() app := cli.NewApp() app.Usage = "" diff --git a/backend/pkg/backend/backend.go b/backend/pkg/backend/backend.go index 82ae54cd0..9446ccd72 100644 --- a/backend/pkg/backend/backend.go +++ b/backend/pkg/backend/backend.go @@ -27,6 +27,7 @@ import ( _config "github.com/openclarity/vmclarity/backend/pkg/config" "github.com/openclarity/vmclarity/backend/pkg/database" + databaseTypes "github.com/openclarity/vmclarity/backend/pkg/database/types" "github.com/openclarity/vmclarity/backend/pkg/rest" runtime_scan_config "github.com/openclarity/vmclarity/runtime_scan/pkg/config" "github.com/openclarity/vmclarity/runtime_scan/pkg/orchestrator" @@ -35,17 +36,17 @@ import ( ) type Backend struct { - dbHandler *database.Handler + dbHandler databaseTypes.Database } -func CreateBackend(dbHandler *database.Handler) *Backend { +func CreateBackend(dbHandler databaseTypes.Database) *Backend { return &Backend{ dbHandler: dbHandler, } } -func createDatabaseConfig(config *_config.Config) *database.DBConfig { - return &database.DBConfig{ +func createDatabaseConfig(config *_config.Config) databaseTypes.DBConfig { + return databaseTypes.DBConfig{ DriverType: config.DatabaseDriver, EnableInfoLogs: config.EnableDBInfoLogs, DBPassword: config.DBPassword, @@ -78,7 +79,10 @@ func Run() { log.Info("VMClarity backend is running") dbConfig := createDatabaseConfig(config) - dbHandler := database.Init(dbConfig) + dbHandler, err := database.InitaliseDatabase(dbConfig) + if err != nil { + log.Fatalf("Failed to initialise database: %v", err) + } _ = CreateBackend(dbHandler) diff --git a/backend/pkg/database/database.go b/backend/pkg/database/database.go index fafdd7904..b143ba6c3 100644 --- a/backend/pkg/database/database.go +++ b/backend/pkg/database/database.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Cisco Systems, Inc. and its affiliates. +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. // All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,99 +16,36 @@ package database import ( - "time" + "fmt" + "sync" - uuid "github.com/satori/go.uuid" - log "github.com/sirupsen/logrus" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" + "github.com/openclarity/vmclarity/backend/pkg/database/gorm" + "github.com/openclarity/vmclarity/backend/pkg/database/types" ) -const ( - DBDriverTypeLocal = "LOCAL" -) - -type Database interface { - ScanResultsTable() ScanResultsTable - ScanConfigsTable() ScanConfigsTable - ScansTable() ScansTable - TargetsTable() TargetsTable -} - -type Handler struct { - DB *gorm.DB -} - -type DBConfig struct { - EnableInfoLogs bool `json:"enable-info-logs"` - DriverType string `json:"driver-type,omitempty"` - DBPassword string `json:"-"` - DBUser string `json:"db-user,omitempty"` - DBHost string `json:"db-host,omitempty"` - DBPort string `json:"db-port,omitempty"` - DBName string `json:"db-name,omitempty"` - - LocalDBPath string `json:"local-db-path,omitempty"` -} - -// Base contains common columns for all tables. -type Base struct { - ID uuid.UUID `gorm:"type:uuid;primaryKey"` - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt *time.Time `sql:"index"` -} - -// BeforeCreate will set a UUID rather than numeric ID. -func (base *Base) BeforeCreate(db *gorm.DB) error { - base.ID = uuid.NewV4() - return nil -} - -func Init(config *DBConfig) *Handler { - databaseHandler := Handler{} +type DBDriver func(config types.DBConfig) (types.Database, error) - databaseHandler.DB = initDataBase(config) - - return &databaseHandler -} - -func initDataBase(config *DBConfig) *gorm.DB { - dbDriver := config.DriverType - dbLogger := logger.Default - if config.EnableInfoLogs { - dbLogger = dbLogger.LogMode(logger.Info) - } - - db := initDB(config, dbDriver, dbLogger) - - // this will ensure table is created - if err := db.AutoMigrate(Target{}, ScanResult{}, ScanConfig{}, Scan{}); err != nil { - log.Fatalf("Failed to run auto migration: %v", err) - } +var ( + DBDrivers map[string]DBDriver + RegisterDriversOnce sync.Once +) - return db +func RegisterDrivers() { + RegisterDriversOnce.Do(func() { + // If DBDrivers is initialised before this function is called don't + // reset it, this is useful for testing. + if DBDrivers == nil { + DBDrivers = map[string]DBDriver{} + } + DBDrivers[types.DBDriverTypeLocal] = gorm.NewDatabase + }) } -func initDB(config *DBConfig, dbDriver string, dbLogger logger.Interface) *gorm.DB { - var db *gorm.DB - switch dbDriver { - case DBDriverTypeLocal: - db = initSqlite(config, dbLogger) - default: - log.Fatalf("DB driver is not supported: %v", dbDriver) - } - return db -} +func InitaliseDatabase(config types.DBConfig) (types.Database, error) { + RegisterDrivers() -func initSqlite(config *DBConfig, dbLogger logger.Interface) *gorm.DB { - db, err := gorm.Open(sqlite.Open(config.LocalDBPath), &gorm.Config{ - Logger: dbLogger, - }) - if err != nil { - log.Fatalf("Failed to open db: %v", err) + if driver, ok := DBDrivers[config.DriverType]; ok { + return driver(config) } - - return db + return nil, fmt.Errorf("unknown DB driver %s", config.DriverType) } diff --git a/backend/pkg/database/gorm/database.go b/backend/pkg/database/gorm/database.go new file mode 100644 index 000000000..f9596c664 --- /dev/null +++ b/backend/pkg/database/gorm/database.go @@ -0,0 +1,98 @@ +// Copyright © 2022 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 gorm + +import ( + "fmt" + "time" + + uuid "github.com/satori/go.uuid" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "github.com/openclarity/vmclarity/backend/pkg/database/types" +) + +func NewDatabase(config types.DBConfig) (types.Database, error) { + db, err := initDataBase(config) + if err != nil { + return nil, fmt.Errorf("unable to create new GORM database: %w", err) + } + return &Handler{DB: db}, nil +} + +type Handler struct { + DB *gorm.DB +} + +// Base contains common columns for all tables. +type Base struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time `sql:"index"` +} + +// BeforeCreate will set a UUID rather than numeric ID. +func (base *Base) BeforeCreate(db *gorm.DB) error { + base.ID = uuid.NewV4() + return nil +} + +func initDataBase(config types.DBConfig) (*gorm.DB, error) { + dbDriver := config.DriverType + dbLogger := logger.Default + if config.EnableInfoLogs { + dbLogger = dbLogger.LogMode(logger.Info) + } + + db, err := initDB(config, dbDriver, dbLogger) + if err != nil { + return nil, err + } + + // this will ensure table is created + if err := db.AutoMigrate( + Target{}, + ScanResult{}, + ScanConfig{}, + Scan{}, + ); err != nil { + return nil, fmt.Errorf("failed to run auto migration: %w", err) + } + + return db, nil +} + +func initDB(config types.DBConfig, dbDriver string, dbLogger logger.Interface) (*gorm.DB, error) { + switch dbDriver { + case types.DBDriverTypeLocal: + return initSqlite(config, dbLogger) + default: + return nil, fmt.Errorf("driver type %s is not supported by GORM driver", dbDriver) + } +} + +func initSqlite(config types.DBConfig, dbLogger logger.Interface) (*gorm.DB, error) { + db, err := gorm.Open(sqlite.Open(config.LocalDBPath), &gorm.Config{ + Logger: dbLogger, + }) + if err != nil { + return nil, fmt.Errorf("failed to open db: %w", err) + } + return db, nil +} diff --git a/backend/pkg/rest/convert/dbtorest/convert.go b/backend/pkg/database/gorm/dbtorest.go similarity index 51% rename from backend/pkg/rest/convert/dbtorest/convert.go rename to backend/pkg/database/gorm/dbtorest.go index 29dd7abbb..3b8e77e46 100644 --- a/backend/pkg/rest/convert/dbtorest/convert.go +++ b/backend/pkg/database/gorm/dbtorest.go @@ -13,73 +13,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dbtorest +package gorm import ( "encoding/json" "fmt" "github.com/openclarity/vmclarity/api/models" - "github.com/openclarity/vmclarity/backend/pkg/database" "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" ) -func ConvertScanConfig(config *database.ScanConfig) (*models.ScanConfig, error) { - var ret models.ScanConfig - - if config.ScanFamiliesConfig != nil { - ret.ScanFamiliesConfig = &models.ScanFamiliesConfig{} - if err := json.Unmarshal(config.ScanFamiliesConfig, ret.ScanFamiliesConfig); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) - } - } - - if config.Scope != nil { - ret.Scope = &models.ScanScopeType{} - if err := ret.Scope.UnmarshalJSON(config.Scope); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) - } - } - - if config.Scheduled != nil { - ret.Scheduled = &models.RuntimeScheduleScanConfigType{} - if err := ret.Scheduled.UnmarshalJSON(config.Scheduled); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) - } - } - - ret.Name = config.Name - - ret.Id = utils.StringPtr(config.ID.String()) - - return &ret, nil -} - -func ConvertScanConfigs(configs []*database.ScanConfig, total int64) (*models.ScanConfigs, error) { - ret := models.ScanConfigs{ - Items: &[]models.ScanConfig{}, - } - - for _, config := range configs { - sc, err := ConvertScanConfig(config) - if err != nil { - return nil, fmt.Errorf("failed to convet scan config: %w", err) - } - *ret.Items = append(*ret.Items, *sc) - } - - ret.Total = utils.IntPtr(int(total)) - - return &ret, nil -} - -func ConvertTarget(target *database.Target) (*models.Target, error) { +func ConvertToRestTarget(target Target) (models.Target, error) { ret := models.Target{ TargetInfo: &models.TargetType{}, } switch target.Type { - case "VMInfo": + case targetTypeVM: var cloudProvider *models.CloudProvider if target.InstanceProvider != nil { cp := models.CloudProvider(*target.InstanceProvider) @@ -90,130 +40,128 @@ func ConvertTarget(target *database.Target) (*models.Target, error) { InstanceProvider: cloudProvider, Location: *target.Location, }); err != nil { - return nil, fmt.Errorf("FromVMInfo failed: %w", err) + return ret, fmt.Errorf("FromVMInfo failed: %w", err) } - case "Dir": - return nil, fmt.Errorf("unsupported target type Dir") - case "Pod": - return nil, fmt.Errorf("unsupported target type Pod") + case targetTypeDir, targetTypePod: + fallthrough default: - return nil, fmt.Errorf("unknown target type: %v", target.Type) + return ret, fmt.Errorf("unknown target type: %v", target.Type) } ret.Id = utils.StringPtr(target.ID.String()) - return &ret, nil + return ret, nil } -func ConvertTargets(targets []*database.Target, total int64) (*models.Targets, error) { +func ConvertToRestTargets(targets []Target) (models.Targets, error) { ret := models.Targets{ Items: &[]models.Target{}, } for _, target := range targets { - tr, err := ConvertTarget(target) + tr, err := ConvertToRestTarget(target) if err != nil { - return nil, fmt.Errorf("failed to convert target: %w", err) + return ret, fmt.Errorf("failed to convert target: %w", err) } - *ret.Items = append(*ret.Items, *tr) + *ret.Items = append(*ret.Items, tr) } - ret.Total = utils.IntPtr(int(total)) + ret.Total = utils.IntPtr(len(targets)) - return &ret, nil + return ret, nil } // nolint:cyclop -func ConvertScanResult(scanResult *database.ScanResult) (*models.TargetScanResult, error) { +func ConvertToRestScanResult(scanResult ScanResult) (models.TargetScanResult, error) { var ret models.TargetScanResult if scanResult.Secrets != nil { ret.Secrets = &models.SecretScan{} if err := json.Unmarshal(scanResult.Secrets, ret.Secrets); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } if scanResult.Vulnerabilities != nil { ret.Vulnerabilities = &models.VulnerabilityScan{} if err := json.Unmarshal(scanResult.Vulnerabilities, ret.Vulnerabilities); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } if scanResult.Exploits != nil { ret.Exploits = &models.ExploitScan{} if err := json.Unmarshal(scanResult.Exploits, ret.Exploits); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } if scanResult.Malware != nil { ret.Malware = &models.MalwareScan{} if err := json.Unmarshal(scanResult.Malware, ret.Malware); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } if scanResult.Misconfigurations != nil { ret.Misconfigurations = &models.MisconfigurationScan{} if err := json.Unmarshal(scanResult.Misconfigurations, ret.Misconfigurations); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } if scanResult.Rootkits != nil { ret.Rootkits = &models.RootkitScan{} if err := json.Unmarshal(scanResult.Rootkits, ret.Rootkits); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } if scanResult.Sboms != nil { ret.Sboms = &models.SbomScan{} if err := json.Unmarshal(scanResult.Sboms, ret.Sboms); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } if scanResult.Status != nil { ret.Status = &models.TargetScanStatus{} if err := json.Unmarshal(scanResult.Status, ret.Status); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } ret.Id = utils.StringPtr(scanResult.ID.String()) ret.ScanId = scanResult.ScanID ret.TargetId = scanResult.TargetID - return &ret, nil + return ret, nil } -func ConvertScanResults(scanResults []*database.ScanResult, total int64) (*models.TargetScanResults, error) { +func ConvertToRestScanResults(scanResults []ScanResult) (models.TargetScanResults, error) { ret := models.TargetScanResults{ Items: &[]models.TargetScanResult{}, } for _, scanResult := range scanResults { - sr, err := ConvertScanResult(scanResult) + sr, err := ConvertToRestScanResult(scanResult) if err != nil { - return nil, fmt.Errorf("failed to convert scan result: %w", err) + return ret, fmt.Errorf("failed to convert scan result: %w", err) } - *ret.Items = append(*ret.Items, *sr) + *ret.Items = append(*ret.Items, sr) } - ret.Total = utils.IntPtr(int(total)) + ret.Total = utils.IntPtr(len(scanResults)) - return &ret, nil + return ret, nil } -func ConvertScan(scan *database.Scan) (*models.Scan, error) { +func ConvertToRestScan(scan Scan) (models.Scan, error) { var ret models.Scan if scan.ScanConfigSnapshot != nil { ret.ScanConfigSnapshot = &models.ScanConfigData{} if err := json.Unmarshal(scan.ScanConfigSnapshot, ret.ScanConfigSnapshot); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } if scan.TargetIDs != nil { ret.TargetIDs = &[]string{} if err := json.Unmarshal(scan.TargetIDs, ret.TargetIDs); err != nil { - return nil, fmt.Errorf("failed to unmarshal json: %w", err) + return ret, fmt.Errorf("failed to unmarshal json: %w", err) } } @@ -222,23 +170,23 @@ func ConvertScan(scan *database.Scan) (*models.Scan, error) { ret.EndTime = scan.ScanEndTime ret.ScanConfig = &models.ScanConfigRelationship{Id: *scan.ScanConfigID} - return &ret, nil + return ret, nil } -func ConvertScans(scans []*database.Scan, total int64) (*models.Scans, error) { +func ConvertToRestScans(scans []Scan) (models.Scans, error) { ret := models.Scans{ Items: &[]models.Scan{}, } for _, scan := range scans { - sc, err := ConvertScan(scan) + sc, err := ConvertToRestScan(scan) if err != nil { - return nil, fmt.Errorf("failed to convert scan: %w", err) + return models.Scans{}, fmt.Errorf("failed to convert scan: %w", err) } - *ret.Items = append(*ret.Items, *sc) + *ret.Items = append(*ret.Items, sc) } - ret.Total = utils.IntPtr(int(total)) + ret.Total = utils.IntPtr(len(scans)) - return &ret, nil + return ret, nil } diff --git a/backend/pkg/database/gorm/dbtorest_test.go b/backend/pkg/database/gorm/dbtorest_test.go new file mode 100644 index 000000000..e46be4c4f --- /dev/null +++ b/backend/pkg/database/gorm/dbtorest_test.go @@ -0,0 +1,171 @@ +// Copyright © 2022 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 gorm + +import ( + "encoding/json" + "reflect" + "testing" + + uuid "github.com/satori/go.uuid" + "gotest.tools/v3/assert" + + "github.com/openclarity/vmclarity/api/models" + "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" +) + +func TestConvertToRestScanResult(t *testing.T) { + state := models.DONE + status := models.TargetScanStatus{ + Vulnerabilities: &models.TargetScanState{ + Errors: nil, + State: &state, + }, + Exploits: &models.TargetScanState{ + Errors: &[]string{"err"}, + State: &state, + }, + } + + vulsScan := models.VulnerabilityScan{ + Vulnerabilities: &[]models.Vulnerability{ + { + VulnerabilityInfo: &models.VulnerabilityInfo{ + VulnerabilityName: utils.StringPtr("name"), + }, + }, + }, + } + + vulScanB, err := json.Marshal(&vulsScan) + assert.NilError(t, err) + + statusB, err := json.Marshal(&status) + assert.NilError(t, err) + + uid := uuid.NewV4() + + type args struct { + scanResult ScanResult + } + tests := []struct { + name string + args args + want models.TargetScanResult + wantErr bool + }{ + { + name: "sanity", + args: args{ + scanResult: ScanResult{ + Base: Base{ + ID: uid, + }, + ScanID: "1", + TargetID: "2", + Status: statusB, + Vulnerabilities: vulScanB, + }, + }, + want: models.TargetScanResult{ + Id: utils.StringPtr(uid.String()), + ScanId: "1", + Status: &status, + TargetId: "2", + Vulnerabilities: &vulsScan, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ConvertToRestScanResult(tt.args.scanResult) + if (err != nil) != tt.wantErr { + t.Errorf("ConvertToRestScanResult() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ConvertToRestScanResult() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestConvertToRestScan(t *testing.T) { + scanSnap := models.ScanConfigData{ + ScanFamiliesConfig: &models.ScanFamiliesConfig{ + Vulnerabilities: &models.VulnerabilitiesConfig{ + Enabled: nil, + }, + }, + } + + scanSnapB, err := json.Marshal(&scanSnap) + assert.NilError(t, err) + + targetIDs := []string{"s"} + targetIDsB, err := json.Marshal(&targetIDs) + assert.NilError(t, err) + + id := uuid.NewV4() + + type args struct { + scan Scan + } + tests := []struct { + name string + args args + want models.Scan + wantErr bool + }{ + { + name: "sanity", + args: args{ + scan: Scan{ + Base: Base{ + ID: id, + }, + ScanStartTime: nil, + ScanEndTime: nil, + ScanConfigID: utils.StringPtr("1"), + ScanConfigSnapshot: scanSnapB, + TargetIDs: targetIDsB, + }, + }, + want: models.Scan{ + Id: utils.StringPtr(id.String()), + ScanConfig: &models.ScanConfigRelationship{ + Id: "1", + }, + ScanConfigSnapshot: &scanSnap, + TargetIDs: &targetIDs, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ConvertToRestScan(tt.args.scan) + if (err != nil) != tt.wantErr { + t.Errorf("ConvertToRestScan() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ConvertToRestScan() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/backend/pkg/database/gorm/odata.go b/backend/pkg/database/gorm/odata.go new file mode 100644 index 000000000..53ba75902 --- /dev/null +++ b/backend/pkg/database/gorm/odata.go @@ -0,0 +1,236 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 gorm + +import ( + "fmt" + + "gorm.io/datatypes" + "gorm.io/gorm" + + "github.com/openclarity/vmclarity/backend/pkg/database/odatasql" + "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" +) + +type ODataObject struct { + gorm.Model + Data datatypes.JSON +} + +var schemaMetas = map[string]odatasql.SchemaMeta{ + "ScanConfig": { + Table: "scan_configs", + Fields: odatasql.Schema{ + "id": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "name": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "scanFamiliesConfig": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"ScanFamiliesConfig"}, + }, + "scheduled": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"SingleScheduleScanConfig"}, + DescriminatorProperty: "objectType", + }, + "scope": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"AwsScanScope"}, + DescriminatorProperty: "objectType", + }, + }, + }, + "ScanFamiliesConfig": { + Fields: odatasql.Schema{ + "exploits": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"ExploitsConfig"}, + }, + "malware": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"MalwareConfig"}, + }, + "misconfigurations": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"MisconfigurationsConfig"}, + }, + "rootkits": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"RootkitsConfig"}, + }, + "sbom": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"SBOMConfig"}, + }, + "secrets": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"SecretsConfig"}, + }, + "vulnerabilities": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"VulnerabilitiesConfig"}, + }, + }, + }, + "ExploitsConfig": { + Fields: odatasql.Schema{ + "enabled": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "MalwareConfig": { + Fields: odatasql.Schema{ + "enabled": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "MisconfigurationsConfig": { + Fields: odatasql.Schema{ + "enabled": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "RootkitsConfig": { + Fields: odatasql.Schema{ + "enabled": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "SBOMConfig": { + Fields: odatasql.Schema{ + "enabled": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "SecretsConfig": { + Fields: odatasql.Schema{ + "enabled": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "VulnerabilitiesConfig": { + Fields: odatasql.Schema{ + "enabled": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "SingleScheduleScanConfig": { + Fields: odatasql.Schema{ + "objectType": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "operationTime": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "AwsScanScope": { + Fields: odatasql.Schema{ + "objectType": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "all": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "shouldScanStoppedInstances": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "instanceTagExclusion": odatasql.FieldMeta{ + FieldType: odatasql.CollectionFieldType, + CollectionItemMeta: &odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"Tag"}, + }, + }, + "instanceTagSelector": odatasql.FieldMeta{ + FieldType: odatasql.CollectionFieldType, + CollectionItemMeta: &odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"Tag"}, + }, + }, + "regions": odatasql.FieldMeta{ + FieldType: odatasql.CollectionFieldType, + CollectionItemMeta: &odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"AwsRegion"}, + }, + }, + }, + }, + "Tag": { + Fields: odatasql.Schema{ + "key": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "value": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, + "AwsRegion": { + Fields: odatasql.Schema{ + "id": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "name": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "vpcs": odatasql.FieldMeta{ + FieldType: odatasql.CollectionFieldType, + CollectionItemMeta: &odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"AwsVPC"}, + }, + }, + }, + }, + "AwsVPC": { + Fields: odatasql.Schema{ + "id": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "name": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "securityGroups": odatasql.FieldMeta{ + FieldType: odatasql.CollectionFieldType, + CollectionItemMeta: &odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"AwsSecurityGroup"}, + }, + }, + }, + }, + "AwsSecurityGroup": { + Fields: odatasql.Schema{ + "id": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "name": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, +} + +func ODataQuery(db *gorm.DB, schema string, filterString, selectString, expandString *string, top, skip *int, collection bool, result interface{}) error { + // If we're not getting a collection, make sure the result is limited + // to 1 item. + if !collection { + top = utils.IntPtr(1) + skip = nil + } + + // Build the raw SQL query using the odatasql library, this will also + // parse and validate the ODATA query params. + query, err := odatasql.BuildSQLQuery(schemaMetas, schema, filterString, selectString, expandString, top, skip) + if err != nil { + return fmt.Errorf("failed to build query for DB: %w", err) + } + + // Use the query to populate "result" using the gorm finalisers so that + // the gorm error handling processes things like no results found. + if collection { + if err := db.Raw(query).Find(result).Error; err != nil { + return fmt.Errorf("failed to query DB: %w", err) + } + } else { + if err := db.Raw(query).First(result).Error; err != nil { + return fmt.Errorf("failed to query DB: %w", err) + } + } + return nil +} + +func ODataCount(db *gorm.DB, schema string, filterString *string) (int, error) { + query, err := odatasql.BuildCountQuery(schemaMetas, schema, filterString) + if err != nil { + return 0, fmt.Errorf("failed to build query to count objects: %w", err) + } + + var count int + if err := db.Raw(query).Scan(&count).Error; err != nil { + return 0, fmt.Errorf("failed to query DB: %w", err) + } + return count, nil +} diff --git a/backend/pkg/rest/convert/resttodb/convert.go b/backend/pkg/database/gorm/resttodb.go similarity index 51% rename from backend/pkg/rest/convert/resttodb/convert.go rename to backend/pkg/database/gorm/resttodb.go index bfec373e9..bbc71cbc6 100644 --- a/backend/pkg/rest/convert/resttodb/convert.go +++ b/backend/pkg/database/gorm/resttodb.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package resttodb +package gorm import ( "encoding/json" @@ -22,75 +22,34 @@ import ( uuid "github.com/satori/go.uuid" "github.com/openclarity/vmclarity/api/models" - "github.com/openclarity/vmclarity/backend/pkg/database" "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" ) -func ConvertScanConfig(config *models.ScanConfig, id string) (*database.ScanConfig, error) { - var ret database.ScanConfig - var err error - var scanConfigUUID uuid.UUID - - if id != "" { - scanConfigUUID, err = uuid.FromString(id) - if err != nil { - return nil, fmt.Errorf("failed to convert scanConfigID %v to uuid: %v", id, err) - } - } - - if config.ScanFamiliesConfig != nil { - ret.ScanFamiliesConfig, err = json.Marshal(config.ScanFamiliesConfig) - if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) - } - } - - if config.Scope != nil { - ret.Scope, err = config.Scope.MarshalJSON() - if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) - } - } - - if config.Scheduled != nil { - ret.Scheduled, err = config.Scheduled.MarshalJSON() - if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) - } - } - - ret.Name = config.Name - - ret.Base = database.Base{ID: scanConfigUUID} - - return &ret, nil -} - -func ConvertTarget(target *models.Target, id string) (*database.Target, error) { +func ConvertToDBTarget(target models.Target) (Target, error) { var targetUUID uuid.UUID var err error - if id != "" { - targetUUID, err = uuid.FromString(id) + if target.Id != nil { + targetUUID, err = uuid.FromString(*target.Id) if err != nil { - return nil, fmt.Errorf("failed to convert targetID %v to uuid: %v", id, err) + return Target{}, fmt.Errorf("failed to convert targetID %v to uuid: %v", *target.Id, err) } } disc, err := target.TargetInfo.Discriminator() if err != nil { - return nil, fmt.Errorf("failed to get discriminator: %w", err) + return Target{}, fmt.Errorf("failed to get discriminator: %w", err) } switch disc { case "VMInfo": vminfo, err := target.TargetInfo.AsVMInfo() if err != nil { - return nil, fmt.Errorf("failed to convert target to vm info: %w", err) + return Target{}, fmt.Errorf("failed to convert target to vm info: %w", err) } var provider *string if vminfo.InstanceProvider != nil { provider = utils.StringPtr(string(*vminfo.InstanceProvider)) } - return &database.Target{ - Base: database.Base{ + return Target{ + Base: Base{ ID: targetUUID, }, Type: vminfo.ObjectType, @@ -99,20 +58,20 @@ func ConvertTarget(target *models.Target, id string) (*database.Target, error) { InstanceProvider: provider, }, nil default: - return nil, fmt.Errorf("unknown target type: %v", disc) + return Target{}, fmt.Errorf("unknown target type: %v", disc) } } // nolint:cyclop -func ConvertScanResult(result *models.TargetScanResult, id string) (*database.ScanResult, error) { - var ret database.ScanResult +func ConvertToDBScanResult(result models.TargetScanResult) (ScanResult, error) { + var ret ScanResult var err error var scanResultUUID uuid.UUID - if id != "" { - scanResultUUID, err = uuid.FromString(id) + if result.Id != nil { + scanResultUUID, err = uuid.FromString(*result.Id) if err != nil { - return nil, fmt.Errorf("failed to convert scanResultID %v to uuid: %v", id, err) + return ret, fmt.Errorf("failed to convert scanResultID %v to uuid: %v", *result.Id, err) } } ret.ScanID = result.ScanId @@ -121,67 +80,67 @@ func ConvertScanResult(result *models.TargetScanResult, id string) (*database.Sc if result.Exploits != nil { ret.Exploits, err = json.Marshal(result.Exploits) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } if result.Malware != nil { ret.Malware, err = json.Marshal(result.Malware) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } if result.Misconfigurations != nil { ret.Misconfigurations, err = json.Marshal(result.Misconfigurations) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } if result.Rootkits != nil { ret.Rootkits, err = json.Marshal(result.Rootkits) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } if result.Sboms != nil { ret.Sboms, err = json.Marshal(result.Sboms) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } if result.Secrets != nil { ret.Secrets, err = json.Marshal(result.Secrets) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } if result.Status != nil { ret.Status, err = json.Marshal(result.Status) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } if result.Vulnerabilities != nil { ret.Vulnerabilities, err = json.Marshal(result.Vulnerabilities) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } - ret.Base = database.Base{ID: scanResultUUID} + ret.Base = Base{ID: scanResultUUID} - return &ret, nil + return ret, nil } -func ConvertScan(scan *models.Scan, id string) (*database.Scan, error) { - var ret database.Scan +func ConvertToDBScan(scan models.Scan) (Scan, error) { + var ret Scan var err error var scanUUID uuid.UUID - if id != "" { - scanUUID, err = uuid.FromString(id) + if scan.Id != nil { + scanUUID, err = uuid.FromString(*scan.Id) if err != nil { - return nil, fmt.Errorf("failed to convert scanID %v to uuid: %v", id, err) + return ret, fmt.Errorf("failed to convert scanID %v to uuid: %v", scan.Id, err) } } @@ -196,18 +155,18 @@ func ConvertScan(scan *models.Scan, id string) (*database.Scan, error) { if scan.ScanConfigSnapshot != nil { ret.ScanConfigSnapshot, err = json.Marshal(scan.ScanConfigSnapshot) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } if scan.TargetIDs != nil { ret.TargetIDs, err = json.Marshal(scan.TargetIDs) if err != nil { - return nil, fmt.Errorf("failed to marshal json: %w", err) + return ret, fmt.Errorf("failed to marshal json: %w", err) } } - ret.Base = database.Base{ID: scanUUID} + ret.Base = Base{ID: scanUUID} - return &ret, nil + return ret, nil } diff --git a/backend/pkg/rest/convert/resttodb/convert_test.go b/backend/pkg/database/gorm/resttodb_test.go similarity index 50% rename from backend/pkg/rest/convert/resttodb/convert_test.go rename to backend/pkg/database/gorm/resttodb_test.go index 0b1e566d5..7f718426e 100644 --- a/backend/pkg/rest/convert/resttodb/convert_test.go +++ b/backend/pkg/database/gorm/resttodb_test.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package resttodb +package gorm import ( "encoding/json" @@ -24,99 +24,12 @@ import ( "gotest.tools/v3/assert" "github.com/openclarity/vmclarity/api/models" - "github.com/openclarity/vmclarity/backend/pkg/database" "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" ) const id = "f12d1ca7-1048-4e31-899c-7a25b357bed1" -func TestConvertScanConfig(t *testing.T) { - scanFamiliesConfig := models.ScanFamiliesConfig{ - Vulnerabilities: &models.VulnerabilitiesConfig{Enabled: utils.BoolPtr(true)}, - } - - scanFamiliesConfigB, err := json.Marshal(&scanFamiliesConfig) - assert.NilError(t, err) - - awsScanScope := models.AwsScanScope{ - All: utils.BoolPtr(true), - InstanceTagExclusion: nil, - InstanceTagSelector: nil, - ObjectType: "AwsScanScope", - Regions: nil, - ShouldScanStoppedInstances: utils.BoolPtr(false), - } - - var scanScopeType models.ScanScopeType - - err = scanScopeType.FromAwsScanScope(awsScanScope) - assert.NilError(t, err) - - scanScopeTypeB, err := scanScopeType.MarshalJSON() - assert.NilError(t, err) - - byHoursScheduleScanConfig := models.ByHoursScheduleScanConfig{ - HoursInterval: utils.IntPtr(2), - ObjectType: "ByHoursScheduleScanConfig", - } - - var runtimeScheduleScanConfigType models.RuntimeScheduleScanConfigType - err = runtimeScheduleScanConfigType.FromByHoursScheduleScanConfig(byHoursScheduleScanConfig) - assert.NilError(t, err) - - runtimeScheduleScanConfigTypeB, err := runtimeScheduleScanConfigType.MarshalJSON() - assert.NilError(t, err) - - UUID, err := uuid.FromString(id) - assert.NilError(t, err) - - type args struct { - config *models.ScanConfig - id string - } - tests := []struct { - name string - args args - want *database.ScanConfig - wantErr bool - }{ - { - name: "sanity", - args: args{ - id: id, - config: &models.ScanConfig{ - Id: utils.StringPtr("1"), - Name: utils.StringPtr("scanConfigName"), - ScanFamiliesConfig: &scanFamiliesConfig, - Scheduled: &runtimeScheduleScanConfigType, - Scope: &scanScopeType, - }, - }, - want: &database.ScanConfig{ - Base: database.Base{ID: UUID}, - Name: utils.StringPtr("scanConfigName"), - ScanFamiliesConfig: scanFamiliesConfigB, - Scheduled: runtimeScheduleScanConfigTypeB, - Scope: scanScopeTypeB, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ConvertScanConfig(tt.args.config, tt.args.id) - if (err != nil) != tt.wantErr { - t.Errorf("ConvertScanConfig() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ConvertScanConfig() got = %v, want %v", got, tt.want) - } - }) - } -} - -func TestConvertScanResult(t *testing.T) { +func TestConvertToDBScanResult(t *testing.T) { vulnerabilities := []models.Vulnerability{ { VulnerabilityInfo: &models.VulnerabilityInfo{ @@ -134,29 +47,29 @@ func TestConvertScanResult(t *testing.T) { UUID, err := uuid.FromString(id) assert.NilError(t, err) + idPtr := id + type args struct { - result *models.TargetScanResult - id string + result models.TargetScanResult } tests := []struct { name string args args - want *database.ScanResult + want ScanResult wantErr bool }{ { name: "sanity", args: args{ - id: id, - result: &models.TargetScanResult{ - Id: nil, + result: models.TargetScanResult{ + Id: &idPtr, ScanId: "3", TargetId: "2", Vulnerabilities: &vulScan, }, }, - want: &database.ScanResult{ - Base: database.Base{ID: UUID}, + want: ScanResult{ + Base: Base{ID: UUID}, ScanID: "3", TargetID: "2", Vulnerabilities: vulScanB, @@ -166,19 +79,19 @@ func TestConvertScanResult(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := ConvertScanResult(tt.args.result, tt.args.id) + got, err := ConvertToDBScanResult(tt.args.result) if (err != nil) != tt.wantErr { - t.Errorf("ConvertScanResult() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("ConvertToDBScanResult() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ConvertScanResult() got = %v, want %v", got, tt.want) + t.Errorf("ConvertToDBScanResult() got = %v, want %v", got, tt.want) } }) } } -func TestConvertTarget(t *testing.T) { +func TestConvertToDBTarget(t *testing.T) { cloudProvider := models.CloudProvider("aws") vmInfo := models.VMInfo{ InstanceID: "instanceID", @@ -194,26 +107,27 @@ func TestConvertTarget(t *testing.T) { UUID, err := uuid.FromString(id) assert.NilError(t, err) + idPtr := id + type args struct { - target *models.Target - id string + target models.Target } tests := []struct { name string args args - want *database.Target + want Target wantErr bool }{ { name: "sanity", args: args{ - target: &models.Target{ + target: models.Target{ + Id: &idPtr, TargetInfo: &targetType, }, - id: id, }, - want: &database.Target{ - Base: database.Base{ID: UUID}, + want: Target{ + Base: Base{ID: UUID}, Type: "VMInfo", Location: utils.StringPtr("location"), InstanceID: utils.StringPtr("instanceID"), @@ -226,19 +140,19 @@ func TestConvertTarget(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := ConvertTarget(tt.args.target, tt.args.id) + got, err := ConvertToDBTarget(tt.args.target) if (err != nil) != tt.wantErr { - t.Errorf("ConvertTarget() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("ConvertToDBTarget() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ConvertTarget() got = %v, want %v", got, tt.want) + t.Errorf("ConvertToDBTarget() got = %v, want %v", got, tt.want) } }) } } -func TestConvertScan(t *testing.T) { +func TestConvertToDBScan(t *testing.T) { scanFamiliesConfig := models.ScanConfigData{ ScanFamiliesConfig: &models.ScanFamiliesConfig{ Exploits: &models.ExploitsConfig{ @@ -257,21 +171,22 @@ func TestConvertScan(t *testing.T) { UUID, err := uuid.FromString(id) assert.NilError(t, err) + idPtr := id + type args struct { - scan *models.Scan - id string + scan models.Scan } tests := []struct { name string args args - want *database.Scan + want Scan wantErr bool }{ { name: "sanity", args: args{ - id: id, - scan: &models.Scan{ + scan: models.Scan{ + Id: &idPtr, ScanConfig: &models.ScanConfigRelationship{ Id: "1", }, @@ -279,8 +194,8 @@ func TestConvertScan(t *testing.T) { TargetIDs: &targetIDs, }, }, - want: &database.Scan{ - Base: database.Base{ID: UUID}, + want: Scan{ + Base: Base{ID: UUID}, ScanConfigID: utils.StringPtr("1"), ScanConfigSnapshot: scanFamiliesConfigB, TargetIDs: targetIDsB, @@ -290,13 +205,13 @@ func TestConvertScan(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := ConvertScan(tt.args.scan, tt.args.id) + got, err := ConvertToDBScan(tt.args.scan) if (err != nil) != tt.wantErr { - t.Errorf("ConvertScan() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("ConvertToDBScan() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ConvertScan() got = %v, want %v", got, tt.want) + t.Errorf("ConvertToDBScan() got = %v, want %v", got, tt.want) } }) } diff --git a/backend/pkg/database/gorm/scan.go b/backend/pkg/database/gorm/scan.go new file mode 100644 index 000000000..322386677 --- /dev/null +++ b/backend/pkg/database/gorm/scan.go @@ -0,0 +1,190 @@ +// Copyright © 2022 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 gorm + +import ( + "context" + "errors" + "fmt" + "time" + + "gorm.io/gorm" + + "github.com/openclarity/vmclarity/api/models" + "github.com/openclarity/vmclarity/backend/pkg/common" + "github.com/openclarity/vmclarity/backend/pkg/database/types" +) + +const ( + scansTableName = "scans" +) + +type Scan struct { + Base + + ScanStartTime *time.Time `json:"scan_start_time,omitempty" gorm:"column:scan_start_time"` + ScanEndTime *time.Time `json:"scan_end_time,omitempty" gorm:"column:scan_end_time"` + + // ScanConfigID The ID of the config that this scan was initiated from (optionanl) + ScanConfigID *string `json:"scan_config_id,omitempty" gorm:"column:scan_config_id"` + // ScanFamiliesConfig The configuration of the scanner families within a scan config + ScanConfigSnapshot []byte `json:"scan_families_config,omitempty" gorm:"column:scan_families_config"` + + // TargetIDs List of target IDs that are targeted for scanning as part of this scan + TargetIDs []byte `json:"target_ids,omitempty" gorm:"column:target_ids"` +} + +type GetScansParams struct { + // Filter Odata filter + Filter *string + // Page Page number of the query + Page *int + // PageSize Maximum items to return + PageSize *int +} + +type ScansTableHandler struct { + scansTable *gorm.DB +} + +func (db *Handler) ScansTable() types.ScansTable { + return &ScansTableHandler{ + scansTable: db.DB.Table(scansTableName), + } +} + +func (s *ScansTableHandler) GetScans(params models.GetScansParams) (models.Scans, error) { + tx := s.scansTable + + var dbScans []Scan + if err := tx.Find(&dbScans).Error; err != nil { + return models.Scans{}, fmt.Errorf("failed to find scans: %w", err) + } + + scans, err := ConvertToRestScans(dbScans) + if err != nil { + return models.Scans{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return scans, nil +} + +func (s *ScansTableHandler) GetScan(scanID models.ScanID) (models.Scan, error) { + var dbScan Scan + if err := s.scansTable.First(&dbScan, "id = ?", scanID).Error; err != nil { + return models.Scan{}, fmt.Errorf("failed to get scan by id %q: %w", scanID, err) + } + + scan, err := ConvertToRestScan(dbScan) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return scan, nil +} + +func (s *ScansTableHandler) CreateScan(scan models.Scan) (models.Scan, error) { + // check if there is already a running scan for that scan config id. + existingSR, exist, err := s.checkExist(scan.ScanConfig.Id) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to check existing scan: %w", err) + } + if exist { + converted, err := ConvertToRestScan(existingSR) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, &common.ConflictError{ + Reason: fmt.Sprintf("There is a running scan with scanConfigID=%s", *existingSR.ScanConfigID), + } + } + + dbScan, err := ConvertToDBScan(scan) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to create scan in db: %w", err) + } + + if err := s.scansTable.Create(&dbScan).Error; err != nil { + return models.Scan{}, fmt.Errorf("failed to create scan in db: %w", err) + } + + converted, err := ConvertToRestScan(dbScan) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (s *ScansTableHandler) SaveScan(scan models.Scan) (models.Scan, error) { + dbScan, err := ConvertToDBScan(scan) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to create scan in db: %w", err) + } + + if err := s.scansTable.Save(&dbScan).Error; err != nil { + return models.Scan{}, fmt.Errorf("failed to save scan in db: %w", err) + } + + converted, err := ConvertToRestScan(dbScan) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (s *ScansTableHandler) UpdateScan(scan models.Scan) (models.Scan, error) { + dbScan, err := ConvertToDBScan(scan) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to create scan in db: %w", err) + } + + if err := s.scansTable.Model(dbScan).Updates(&dbScan).Error; err != nil { + return models.Scan{}, fmt.Errorf("failed to update scan in db: %w", err) + } + + converted, err := ConvertToRestScan(dbScan) + if err != nil { + return models.Scan{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (s *ScansTableHandler) DeleteScan(scanID models.ScanID) error { + if err := s.scansTable.Delete(&Scan{}, scanID).Error; err != nil { + return fmt.Errorf("failed to delete scan: %w", err) + } + return nil +} + +func (s *ScansTableHandler) checkExist(scanConfigID string) (Scan, bool, error) { + var scans []Scan + + tx := s.scansTable.WithContext(context.Background()) + + if err := tx.Where("scan_config_id = ?", scanConfigID).Find(&scans).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return Scan{}, false, nil + } + return Scan{}, false, fmt.Errorf("failed to query: %w", err) + } + + // check if there is a running scan (end time not set) + for i, scan := range scans { + if scan.ScanEndTime == nil { + return scans[i], true, nil + } + } + + return Scan{}, false, nil +} diff --git a/backend/pkg/database/gorm/scan_config.go b/backend/pkg/database/gorm/scan_config.go new file mode 100644 index 000000000..a8bcdfc8c --- /dev/null +++ b/backend/pkg/database/gorm/scan_config.go @@ -0,0 +1,256 @@ +// Copyright © 2022 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 gorm + +import ( + "encoding/json" + "errors" + "fmt" + + jsonpatch "github.com/evanphx/json-patch/v5" + "github.com/google/uuid" + "gorm.io/gorm" + + "github.com/openclarity/vmclarity/api/models" + "github.com/openclarity/vmclarity/backend/pkg/common" + "github.com/openclarity/vmclarity/backend/pkg/database/types" +) + +type ScanConfig struct { + ODataObject +} + +type ScanConfigsTableHandler struct { + DB *gorm.DB +} + +func (db *Handler) ScanConfigsTable() types.ScanConfigsTable { + return &ScanConfigsTableHandler{ + DB: db.DB, + } +} + +func (s *ScanConfigsTableHandler) GetScanConfigs(params models.GetScanConfigsParams) (models.ScanConfigs, error) { + var scanConfigs []ScanConfig + err := ODataQuery(s.DB, "ScanConfig", params.Filter, params.Select, params.Expand, params.Top, params.Skip, true, &scanConfigs) + if err != nil { + return models.ScanConfigs{}, err + } + + items := []models.ScanConfig{} + for _, scanConfig := range scanConfigs { + var sc models.ScanConfig + err := json.Unmarshal(scanConfig.Data, &sc) + if err != nil { + return models.ScanConfigs{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + items = append(items, sc) + } + + output := models.ScanConfigs{Items: &items} + + if params.Count != nil && *params.Count { + count, err := ODataCount(s.DB, "ScanConfig", params.Filter) + if err != nil { + return models.ScanConfigs{}, fmt.Errorf("failed to count records: %w", err) + } + output.Count = &count + } + + return output, nil +} + +func (s *ScanConfigsTableHandler) GetScanConfig(scanConfigID models.ScanConfigID, params models.GetScanConfigsScanConfigIDParams) (models.ScanConfig, error) { + var dbScanConfig ScanConfig + filter := fmt.Sprintf("id eq '%s'", scanConfigID) + err := ODataQuery(s.DB, "ScanConfig", &filter, params.Select, params.Expand, nil, nil, false, &dbScanConfig) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return models.ScanConfig{}, types.ErrNotFound + } + return models.ScanConfig{}, err + } + + var sc models.ScanConfig + err = json.Unmarshal(dbScanConfig.Data, &sc) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + + return sc, nil +} + +func (s *ScanConfigsTableHandler) CreateScanConfig(scanConfig models.ScanConfig) (models.ScanConfig, error) { + // Check the user provided the name field + if scanConfig.Name == nil || *scanConfig.Name == "" { + return models.ScanConfig{}, fmt.Errorf("name is a required field") + } + + // Check the user didn't provide an Id + if scanConfig.Id != nil { + return models.ScanConfig{}, fmt.Errorf("can not specify Id field when creating a new ScanConfig") + } + + // Generate a new UUID + newID := uuid.New().String() + scanConfig.Id = &newID + + // TODO(sambetts) Lock the table here to prevent race conditions + // checking the uniqueness. + // + // We might also be able to do this without locking the table by doing + // a single query which includes the uniqueness check like: + // + // INSERT INTO scan_configs(data) SELECT * FROM (SELECT "") AS tmp WHERE NOT EXISTS (SELECT * FROM scan_configs WHERE JSON_EXTRACT(`Data`, '$.Name') = '') LIMIT 1; + // + // This should return 0 affected fields if there is a conflicting + // record in the DB, and should be treated safely by the DB without + // locking the table. + + // Check the existing DB entries to ensure that the name field is unique + var scanConfigs []ScanConfig + filter := fmt.Sprintf("name eq '%s'", *scanConfig.Name) + err := ODataQuery(s.DB, "ScanConfig", &filter, nil, nil, nil, nil, true, &scanConfigs) + if err != nil { + return models.ScanConfig{}, err + } + + if len(scanConfigs) > 0 { + var sc models.ScanConfig + err := json.Unmarshal(scanConfigs[0].Data, &sc) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return sc, &common.ConflictError{ + Reason: fmt.Sprintf("Scan config exists with name=%s", *sc.Name), + } + } + + marshaled, err := json.Marshal(scanConfig) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to convert API model to DB model: %w", err) + } + + newScanConfig := ScanConfig{} + newScanConfig.Data = marshaled + + if err := s.DB.Create(&newScanConfig).Error; err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to create scan config in db: %w", err) + } + + // TODO(sambetts) Maybe this isn't required now because the DB isn't + // creating any of the data (like the ID) so we can just return the + // scanConfig pre-marshal above. + var sc models.ScanConfig + err = json.Unmarshal(newScanConfig.Data, &sc) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + + return sc, nil +} + +func getExistingScanConfigbyID(db *gorm.DB, scanConfig models.ScanConfig) (ScanConfig, error) { + var dbScanConfig ScanConfig + filter := fmt.Sprintf("id eq '%s'", *scanConfig.Id) + err := ODataQuery(db, "ScanConfig", &filter, nil, nil, nil, nil, false, &dbScanConfig) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ScanConfig{}, types.ErrNotFound + } + return ScanConfig{}, err + } + return dbScanConfig, nil +} + +func (s *ScanConfigsTableHandler) SaveScanConfig(scanConfig models.ScanConfig) (models.ScanConfig, error) { + dbScanConfig, err := getExistingScanConfigbyID(s.DB, scanConfig) + if err != nil { + return models.ScanConfig{}, err + } + + marshaled, err := json.Marshal(scanConfig) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to convert API model to DB model: %w", err) + } + + dbScanConfig.Data = marshaled + + if err := s.DB.Save(&dbScanConfig).Error; err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to save scan config in db: %w", err) + } + + // TODO(sambetts) Maybe this isn't required now because the DB isn't + // creating any of the data (like the ID) so we can just return the + // scanConfig pre-marshal above. + var sc models.ScanConfig + err = json.Unmarshal(dbScanConfig.Data, &sc) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return sc, nil +} + +func (s *ScanConfigsTableHandler) UpdateScanConfig(scanConfig models.ScanConfig) (models.ScanConfig, error) { + dbScanConfig, err := getExistingScanConfigbyID(s.DB, scanConfig) + if err != nil { + return models.ScanConfig{}, err + } + + marshaled, err := json.Marshal(scanConfig) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to convert API model to DB model: %w", err) + } + + // Calculate the diffs between the current doc and the user doc + patch, err := jsonpatch.CreateMergePatch(dbScanConfig.Data, marshaled) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to calculate patch changes: %w", err) + } + + // Apply the diff to the doc stored in the DB + updated, err := jsonpatch.MergePatch(dbScanConfig.Data, patch) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to apply patch: %w", err) + } + + dbScanConfig.Data = updated + + if err := s.DB.Save(&dbScanConfig).Error; err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to save scan config in db: %w", err) + } + + // TODO(sambetts) Maybe this isn't required now because the DB isn't + // creating any of the data (like the ID) so we can just return the + // scanConfig pre-marshal above. + var sc models.ScanConfig + err = json.Unmarshal(dbScanConfig.Data, &sc) + if err != nil { + return models.ScanConfig{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return sc, nil +} + +func (s *ScanConfigsTableHandler) DeleteScanConfig(scanConfigID models.ScanConfigID) error { + jsonQuotedID := fmt.Sprintf("\"%s\"", scanConfigID) + if err := s.DB.Where("`Data` -> '$.id' = ?", jsonQuotedID).Delete(&ScanConfig{}).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return types.ErrNotFound + } + return fmt.Errorf("failed to delete scan config: %w", err) + } + return nil +} diff --git a/backend/pkg/database/gorm/scan_result.go b/backend/pkg/database/gorm/scan_result.go new file mode 100644 index 000000000..0fe8fdae8 --- /dev/null +++ b/backend/pkg/database/gorm/scan_result.go @@ -0,0 +1,166 @@ +// Copyright © 2022 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 gorm + +import ( + "context" + "errors" + "fmt" + + "gorm.io/gorm" + + "github.com/openclarity/vmclarity/api/models" + "github.com/openclarity/vmclarity/backend/pkg/common" + "github.com/openclarity/vmclarity/backend/pkg/database/types" +) + +const ( + scanResultsTableName = "scan_results" +) + +type ScanResult struct { + Base + + ScanID string `json:"scan_id,omitempty" gorm:"column:scan_id"` + TargetID string `json:"target_id,omitempty" gorm:"column:target_id"` + + Exploits []byte `json:"exploits,omitempty" gorm:"column:exploits"` + Malware []byte `json:"malware,omitempty" gorm:"column:malware"` + Misconfigurations []byte `json:"misconfigurations,omitempty" gorm:"column:misconfigurations"` + Rootkits []byte `json:"rootkits,omitempty" gorm:"column:rootkits"` + Sboms []byte `json:"sboms,omitempty" gorm:"column:sboms"` + Secrets []byte `json:"secrets,omitempty" gorm:"column:secrets"` + Status []byte `json:"status,omitempty" gorm:"column:status"` + Vulnerabilities []byte `json:"vulnerabilities,omitempty" gorm:"column:vulnerabilities"` +} + +type ScanResultsTableHandler struct { + scanResultsTable *gorm.DB +} + +func (db *Handler) ScanResultsTable() types.ScanResultsTable { + return &ScanResultsTableHandler{ + scanResultsTable: db.DB.Table(scanResultsTableName), + } +} + +func (s *ScanResultsTableHandler) GetScanResults(params models.GetScanResultsParams) (models.TargetScanResults, error) { + var scanResults []ScanResult + tx := s.scanResultsTable + if err := tx.Find(&scanResults).Error; err != nil { + return models.TargetScanResults{}, fmt.Errorf("failed to find scan results: %w", err) + } + + converted, err := ConvertToRestScanResults(scanResults) + if err != nil { + return models.TargetScanResults{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (s *ScanResultsTableHandler) GetScanResult(scanResultID models.ScanResultID, params models.GetScanResultsScanResultIDParams) (models.TargetScanResult, error) { + var scanResult ScanResult + if err := s.scanResultsTable.Where("id = ?", scanResultID).First(&scanResult).Error; err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to get scan result by id %q: %w", scanResultID, err) + } + + converted, err := ConvertToRestScanResult(scanResult) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (s *ScanResultsTableHandler) CreateScanResult(scanResult models.TargetScanResult) (models.TargetScanResult, error) { + // check if there is already a scanResult for that scan id and target id. + existingSR, exist, err := s.checkExist(scanResult.ScanId, scanResult.TargetId) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to check existing scan result: %w", err) + } + if exist { + converted, err := ConvertToRestScanResult(existingSR) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, &common.ConflictError{ + Reason: fmt.Sprintf("Target scan result exists with scanID=%s, targetID=%s", existingSR.ScanID, existingSR.TargetID), + } + } + + dbScanResult, err := ConvertToDBScanResult(scanResult) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to convert API model to DB model: %w", err) + } + + if err := s.scanResultsTable.Create(&dbScanResult).Error; err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to create scan result in db: %w", err) + } + + converted, err := ConvertToRestScanResult(dbScanResult) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (s *ScanResultsTableHandler) SaveScanResult(scanResult models.TargetScanResult) (models.TargetScanResult, error) { + dbScanResult, err := ConvertToDBScanResult(scanResult) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to convert API model to DB model: %w", err) + } + + if err := s.scanResultsTable.Save(&dbScanResult).Error; err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to save scan result in db: %w", err) + } + + converted, err := ConvertToRestScanResult(dbScanResult) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (s *ScanResultsTableHandler) UpdateScanResult(scanResult models.TargetScanResult) (models.TargetScanResult, error) { + dbScanResult, err := ConvertToDBScanResult(scanResult) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to convert API model to DB model: %w", err) + } + + if err := s.scanResultsTable.Model(dbScanResult).Updates(&dbScanResult).Error; err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to update scan result in db: %w", err) + } + + converted, err := ConvertToRestScanResult(dbScanResult) + if err != nil { + return models.TargetScanResult{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (s *ScanResultsTableHandler) checkExist(scanID string, targetID string) (ScanResult, bool, error) { + var scanResult ScanResult + + tx := s.scanResultsTable.WithContext(context.Background()) + + if err := tx.Where("scan_id = ? AND target_id = ?", scanID, targetID).First(&scanResult).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ScanResult{}, false, nil + } + return ScanResult{}, false, fmt.Errorf("failed to query: %w", err) + } + + return scanResult, true, nil +} diff --git a/backend/pkg/database/gorm/target.go b/backend/pkg/database/gorm/target.go new file mode 100644 index 000000000..a88772415 --- /dev/null +++ b/backend/pkg/database/gorm/target.go @@ -0,0 +1,176 @@ +// Copyright © 2022 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 gorm + +import ( + "context" + "errors" + "fmt" + + "gorm.io/gorm" + + "github.com/openclarity/vmclarity/api/models" + "github.com/openclarity/vmclarity/backend/pkg/common" + "github.com/openclarity/vmclarity/backend/pkg/database/types" +) + +const ( + targetsTableName = "targets" + + targetTypeVM = "VMInfo" + targetTypeDir = "Dir" + targetTypePod = "Pod" +) + +type Target struct { + Base + + Type string `json:"type,omitempty" gorm:"column:type"` + Location *string `json:"location,omitempty" gorm:"column:location"` + + // VMInfo + InstanceID *string `json:"instance_id,omitempty" gorm:"column:instance_id"` + InstanceProvider *string `json:"instance_provider,omitempty" gorm:"column:instance_provider"` + + // PodInfo + PodName *string `json:"pod_name,omitempty" gorm:"column:pod_name"` + + // DirInfo + DirName *string `json:"dir_name,omitempty" gorm:"column:dir_name"` +} + +type TargetsTableHandler struct { + targetsTable *gorm.DB +} + +func (db *Handler) TargetsTable() types.TargetsTable { + return &TargetsTableHandler{ + targetsTable: db.DB.Table(targetsTableName), + } +} + +func (t *TargetsTableHandler) GetTargets(params models.GetTargetsParams) (models.Targets, error) { + var targets []Target + tx := t.targetsTable + if err := tx.Find(&targets).Error; err != nil { + return models.Targets{}, fmt.Errorf("failed to find targets: %w", err) + } + + converted, err := ConvertToRestTargets(targets) + if err != nil { + return models.Targets{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (t *TargetsTableHandler) GetTarget(targetID models.TargetID) (models.Target, error) { + var target Target + if err := t.targetsTable.Where("id = ?", targetID).First(&target).Error; err != nil { + return models.Target{}, fmt.Errorf("failed to get target by id %q: %w", targetID, err) + } + + converted, err := ConvertToRestTarget(target) + if err != nil { + return models.Target{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (t *TargetsTableHandler) CreateTarget(target models.Target) (models.Target, error) { + dbTarget, err := ConvertToDBTarget(target) + if err != nil { + return models.Target{}, fmt.Errorf("failed to convert API model to DB model: %w", err) + } + + existingTarget, exist, err := t.checkExist(dbTarget) + if err != nil { + return models.Target{}, fmt.Errorf("failed to check existing target: %w", err) + } + if exist { + converted, err := ConvertToRestTarget(existingTarget) + if err != nil { + return models.Target{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, &common.ConflictError{ + Reason: fmt.Sprintf("Target exists with the unique constraint combination: %s", getUniqueConstraintsOfTarget(existingTarget)), + } + } + + if err := t.targetsTable.Create(&dbTarget).Error; err != nil { + return models.Target{}, fmt.Errorf("failed to create target in db: %w", err) + } + + converted, err := ConvertToRestTarget(dbTarget) + if err != nil { + return models.Target{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (t *TargetsTableHandler) SaveTarget(target models.Target) (models.Target, error) { + dbTarget, err := ConvertToDBTarget(target) + if err != nil { + return models.Target{}, fmt.Errorf("failed to convert API model to DB model: %w", err) + } + + if err := t.targetsTable.Save(&dbTarget).Error; err != nil { + return models.Target{}, fmt.Errorf("failed to save target in db: %w", err) + } + + converted, err := ConvertToRestTarget(dbTarget) + if err != nil { + return models.Target{}, fmt.Errorf("failed to convert DB model to API model: %w", err) + } + return converted, nil +} + +func (t *TargetsTableHandler) DeleteTarget(targetID models.TargetID) error { + if err := t.targetsTable.Delete(&Target{}, targetID).Error; err != nil { + return fmt.Errorf("failed to delete target: %w", err) + } + return nil +} + +func (t *TargetsTableHandler) checkExist(target Target) (Target, bool, error) { + var targetFromDB Target + + tx := t.targetsTable.WithContext(context.Background()) + + switch target.Type { + case targetTypeVM: + if err := tx.Where("instance_id = ? AND location = ?", *target.InstanceID, *target.Location).First(&targetFromDB).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return Target{}, false, nil + } + return Target{}, false, fmt.Errorf("failed to query: %w", err) + } + } + + return targetFromDB, true, nil +} + +func getUniqueConstraintsOfTarget(target Target) string { + switch target.Type { + case targetTypeVM: + return fmt.Sprintf("instanceID=%s, region=%s", *target.InstanceID, *target.Location) + case targetTypeDir: + return "unsupported target type Dir" + case targetTypePod: + return "unsupported target type Pod" + default: + return fmt.Sprintf("unknown target type: %v", target.Type) + } +} diff --git a/backend/pkg/database/odatasql/query.go b/backend/pkg/database/odatasql/query.go new file mode 100644 index 000000000..7fbd3c962 --- /dev/null +++ b/backend/pkg/database/odatasql/query.go @@ -0,0 +1,408 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 odatasql + +import ( + "context" + "fmt" + "strings" + "sync" + + "github.com/CiscoM31/godata" +) + +var fixSelectToken sync.Once + +// nolint:cyclop +func BuildCountQuery(schemaMetas map[string]SchemaMeta, schema string, filterString *string) (string, error) { + // Parse top level $filter and create the top level "WHERE" + var where string + if filterString != nil && *filterString != "" { + filterQuery, err := godata.ParseFilterString(context.TODO(), *filterString) + if err != nil { + return "", fmt.Errorf("failed to parse $filter: %w", err) + } + + // Build the WHERE conditions based on the $filter tree + conditions, err := buildWhereFromFilter("Data", filterQuery.Tree) + if err != nil { + return "", fmt.Errorf("failed to build DB query from $filter: %w", err) + } + + where = fmt.Sprintf("WHERE %s", conditions) + } + + table := schemaMetas[schema].Table + if table == "" { + return "", fmt.Errorf("trying to query complex type schema %s with no source table", schema) + } + + return fmt.Sprintf("SELECT COUNT(*) FROM %s %s", table, where), nil +} + +// nolint:cyclop +func BuildSQLQuery(schemaMetas map[string]SchemaMeta, schema string, filterString, selectString, expandString *string, top, skip *int) (string, error) { + // Fix GlobalExpandTokenizer so that it allows for `-` characters in the Literal tokens + fixSelectToken.Do(func() { + godata.GlobalExpandTokenizer.Add("^[a-zA-Z0-9_\\'\\.:\\$ \\*-]+", godata.ExpandTokenLiteral) + }) + + // Parse top level $filter and create the top level "WHERE" + var where string + if filterString != nil && *filterString != "" { + filterQuery, err := godata.ParseFilterString(context.TODO(), *filterString) + if err != nil { + return "", fmt.Errorf("failed to parse $filter: %w", err) + } + + // Build the WHERE conditions based on the $filter tree + conditions, err := buildWhereFromFilter("Data", filterQuery.Tree) + if err != nil { + return "", fmt.Errorf("failed to build DB query from $filter: %w", err) + } + + where = fmt.Sprintf("WHERE %s", conditions) + } + + var selectQuery *godata.GoDataSelectQuery + if selectString != nil && *selectString != "" { + // NOTE(sambetts): + // For now we'll won't parse the data here and instead pass + // just the raw value into the selectTree. The select tree will + // parse the select query using the ExpandParser. If we can + // update the GoData select parser to handle paths properly and + // nest query params then we can switch to parsing select once + // here before passing it to the selectTree. + selectQuery = &godata.GoDataSelectQuery{RawValue: *selectString} + } + + var expandQuery *godata.GoDataExpandQuery + if expandString != nil && *expandString != "" { + var err error + expandQuery, err = godata.ParseExpandString(context.TODO(), *expandString) + if err != nil { + return "", fmt.Errorf("failed to parse $expand ") + } + } + + // Turn the select and expand query params into a tree that can be used + // to build nested select statements for the whole schema. + // + // TODO(sambetts) This should probably also validate that all the + // selected/expanded fields are part of the schema. + selectTree := newSelectTree() + err := selectTree.insert(nil, nil, selectQuery, expandQuery, false) + if err != nil { + return "", fmt.Errorf("failed to parse select and expand: %w", err) + } + + table := schemaMetas[schema].Table + + // Build query selecting fields based on the selectTree + // For now all queries must start with a root "object" so we create a + // complex field meta to represent that object + rootObject := FieldMeta{FieldType: ComplexFieldType, ComplexFieldSchemas: []string{schema}} + selectFields := buildSelectFields(schemaMetas, rootObject, schema, fmt.Sprintf("%s.Data", table), "$", selectTree) + + // Build paging statement + var limitStm string + if top != nil || skip != nil { + limitVal := -1 // Negative means no limit, if no "$top" is specified this is what we want + if top != nil { + limitVal = *top + } + limitStm = fmt.Sprintf("LIMIT %d", limitVal) + + if skip != nil { + limitStm = fmt.Sprintf("%s OFFSET %d", limitStm, *skip) + } + } + + if table == "" { + return "", fmt.Errorf("trying to query complex type schema %s with no source table", schema) + } + + return fmt.Sprintf("SELECT ID, %s AS Data FROM %s %s %s", selectFields, table, where, limitStm), nil +} + +// nolint:cyclop,gocognit,gocyclo +func buildSelectFields(schemaMetas map[string]SchemaMeta, field FieldMeta, identifier, source, path string, st *selectNode) string { + switch field.FieldType { + case PrimitiveFieldType: + // If root of source (path is just $) is primitive just return the source + if path == "$" { + return source + } + return fmt.Sprintf("%s -> '%s'", source, path) + case CollectionFieldType: + newIdentifier := fmt.Sprintf("%sOptions", identifier) + newSource := fmt.Sprintf("%s.value", identifier) + + var where string + var newSelectNode *selectNode + if st != nil { + if st.filter != nil { + conditions, _ := buildWhereFromFilter(newSource, st.filter.Tree) + where = fmt.Sprintf("WHERE %s", conditions) + } + newSelectNode = &selectNode{children: st.children, expand: st.expand} + } + + subQuery := buildSelectFields(schemaMetas, *field.CollectionItemMeta, newIdentifier, newSource, "$", newSelectNode) + return fmt.Sprintf("(SELECT JSON_GROUP_ARRAY(%s) FROM JSON_EACH(%s, '%s') AS %s %s)", subQuery, source, path, identifier, where) + case ComplexFieldType: + // If there are no children in the select tree for this complex + // type, shortcircuit and just return the data from the DB raw, + // as there is no need to build the complex query, and it'll + // ensure that null values are handled correctly. + if st == nil || len(st.children) == 0 { + return fmt.Sprintf("%s -> '%s'", source, path) + } + + objects := []string{} + for _, schemaName := range field.ComplexFieldSchemas { + schema := schemaMetas[schemaName] + + parts := []string{} + if field.DescriminatorProperty != "" { + parts = append(parts, fmt.Sprintf("'%s', '%s'", field.DescriminatorProperty, schemaName)) + } + for key, fm := range schema.Fields { + if field.DescriminatorProperty != "" && key == field.DescriminatorProperty { + continue + } + + var sel *selectNode + if st != nil { + // If there are any select children + // then we need to make sure this is + // either a select child or a expand + // child, otherwise skip this field. + if len(st.selectChildren) > 0 { + _, isSelect := st.selectChildren[key] + _, isExpand := st.expandChildren[key] + if !isSelect && !isExpand { + continue + } + } + sel = st.children[key] + } + + extract := buildSelectFields(schemaMetas, fm, fmt.Sprintf("%s%s", identifier, key), source, fmt.Sprintf("%s.%s", path, key), sel) + part := fmt.Sprintf("'%s', %s", key, extract) + parts = append(parts, part) + } + objects = append(objects, fmt.Sprintf("JSON_OBJECT(%s)", strings.Join(parts, ","))) + } + + if len(objects) == 1 { + return objects[0] + } + + // TODO(sambetts) Error, if multiple schema there must be a + // descriminator, this would be a developer error. Might be + // avoidable if we create a schema builder thing instead of + // just defining it as a variable. + // if field.DescriminatorProperty == "" { + // } + + return fmt.Sprintf("(SELECT %s.value FROM JSON_EACH(JSON_ARRAY(%s)) AS %s WHERE %s.value -> '$.%s' = %s -> '%s.%s')", identifier, strings.Join(objects, ","), identifier, identifier, field.DescriminatorProperty, source, path, field.DescriminatorProperty) + + case RelationshipFieldType: + if st == nil || !st.expand { + return fmt.Sprintf("%s -> '%s'", source, path) + } + + schemaName := field.RelationshipSchema + schema := schemaMetas[schemaName] + newsource := fmt.Sprintf("%s.Data", schema.Table) + parts := []string{} + for key, fm := range schema.Fields { + var sel *selectNode + if st != nil { + // If there are any select children + // then we need to make sure this is + // either a select child or a expand + // child, otherwise skip this field. + if len(st.selectChildren) > 0 { + _, isSelect := st.selectChildren[key] + _, isExpand := st.expandChildren[key] + if !isSelect && !isExpand { + continue + } + } + sel = st.children[key] + } + + extract := buildSelectFields(schemaMetas, fm, fmt.Sprintf("%s%s", identifier, key), newsource, fmt.Sprintf("$.%s", key), sel) + part := fmt.Sprintf("'%s', %s", key, extract) + parts = append(parts, part) + } + object := fmt.Sprintf("JSON_OBJECT(%s)", strings.Join(parts, ",")) + + return fmt.Sprintf("(SELECT %s FROM %s WHERE %s -> '$.%s' == %s -> '%s.%s')", object, schema.Table, newsource, field.RelationshipProperty, source, path, field.RelationshipProperty) + case RelationshipCollectionFieldType: + if st == nil || !st.expand { + return fmt.Sprintf("%s -> '%s'", source, path) + } + + schemaName := field.RelationshipSchema + schema := schemaMetas[schemaName] + newSource := fmt.Sprintf("%s.Data", schema.Table) + + where := fmt.Sprintf("WHERE %s -> '$.%s' = %s.value -> '$.%s'", newSource, field.RelationshipProperty, identifier, field.RelationshipProperty) + if st != nil { + if st.filter != nil { + conditions, _ := buildWhereFromFilter(newSource, st.filter.Tree) + where = fmt.Sprintf("%s and %s", where, conditions) + } + } + + parts := []string{} + for key, fm := range schema.Fields { + var sel *selectNode + if st != nil { + // If there are any select children + // then we need to make sure this is + // either a select child or a expand + // child, otherwise skip this field. + if len(st.selectChildren) > 0 { + _, isSelect := st.selectChildren[key] + _, isExpand := st.expandChildren[key] + if !isSelect && !isExpand { + continue + } + } + sel = st.children[key] + } + + extract := buildSelectFields(schemaMetas, fm, fmt.Sprintf("%s%s", identifier, key), newSource, fmt.Sprintf("$.%s", key), sel) + part := fmt.Sprintf("'%s', %s", key, extract) + parts = append(parts, part) + } + subQuery := fmt.Sprintf("JSON_OBJECT(%s)", strings.Join(parts, ",")) + + return fmt.Sprintf("(SELECT JSON_GROUP_ARRAY(%s) FROM %s,JSON_EACH(%s, '%s') AS %s %s)", subQuery, schema.Table, source, path, identifier, where) + default: + return "" + } +} + +var sqlOperators = map[string]string{ + "eq": "=", + "ne": "!=", + "gt": ">", + "ge": ">=", + "lt": "<", + "le": "<=", + "or": "or", + "contains": "%%%s%%", + "endswith": "%%%s", + "startswith": "%s%%", +} + +func singleQuote(s string) string { + return fmt.Sprintf("'%s'", s) +} + +func buildJSONPathFromParseNode(node *godata.ParseNode) (string, error) { + switch node.Token.Type { + case godata.ExpressionTokenNav: + right, err := buildJSONPathFromParseNode(node.Children[0]) + if err != nil { + return "", fmt.Errorf("unable to build right side of navigation path: %w", err) + } + + left, err := buildJSONPathFromParseNode(node.Children[1]) + if err != nil { + return "", fmt.Errorf("unable to build left side of navigation path: %w", err) + } + return fmt.Sprintf("%s.%s", right, left), nil + case godata.ExpressionTokenLiteral: + return node.Token.Value, nil + default: + return "", fmt.Errorf("unsupported token type") + } +} + +// nolint:cyclop +func buildWhereFromFilter(source string, node *godata.ParseNode) (string, error) { + operator := node.Token.Value + + var query string + switch operator { + case "eq", "ne", "gt", "ge", "lt", "le": + // Convert ODATA paths with slashes like "Thing/Name" into JSON + // path like "Thing.Name". + queryPath, err := buildJSONPathFromParseNode(node.Children[0]) + if err != nil { + return "", fmt.Errorf("unable to covert oData path to json path: %w", err) + } + + rhs := node.Children[1] + extractFunction := "->" + var value string + switch rhs.Token.Type { + case godata.ExpressionTokenString: + value = singleQuote(strings.ReplaceAll(rhs.Token.Value, "'", "\"")) + case godata.ExpressionTokenBoolean: + value = singleQuote(rhs.Token.Value) + case godata.ExpressionTokenInteger, godata.ExpressionTokenFloat: + value = rhs.Token.Value + extractFunction = "->>" + default: + return "", fmt.Errorf("unsupported token type %s", node.Children[1].Token.Type) + } + + query = fmt.Sprintf("%s %s '%s' %s %s", source, extractFunction, queryPath, sqlOperators[operator], value) + case "and": + left, err := buildWhereFromFilter(source, node.Children[0]) + if err != nil { + return query, err + } + right, err := buildWhereFromFilter(source, node.Children[1]) + if err != nil { + return query, err + } + query = fmt.Sprintf("(%s AND %s)", left, right) + case "or": + left, err := buildWhereFromFilter(source, node.Children[0]) + if err != nil { + return query, err + } + right, err := buildWhereFromFilter(source, node.Children[1]) + if err != nil { + return query, err + } + query = fmt.Sprintf("(%s OR %s)", left, right) + case "contains", "endswith", "startswith": + queryField := node.Children[0].Token.Value + queryPath := fmt.Sprintf("$.%s", queryField) + + right := node.Children[1].Token.Value + var value interface{} + switch node.Children[1].Token.Type { + case godata.ExpressionTokenString: + r := strings.ReplaceAll(right, "'", "") + value = fmt.Sprintf(sqlOperators[operator], r) + default: + return query, fmt.Errorf("unsupported token type") + } + query = fmt.Sprintf("%s -> '%s' LIKE '%s'", source, queryPath, value) + } + + return query, nil +} diff --git a/backend/pkg/database/odatasql/query_test.go b/backend/pkg/database/odatasql/query_test.go new file mode 100644 index 000000000..57b875107 --- /dev/null +++ b/backend/pkg/database/odatasql/query_test.go @@ -0,0 +1,831 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 odatasql + +import ( + "encoding/json" + "fmt" + "os" + "path" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + + "gorm.io/datatypes" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +type SubOption struct { + Name string `json:"Name"` +} + +type Options struct { + // Example field of a primitve type inside of a nested complex type + Supercharger bool `json:"Supercharger"` + + // Example field of a collection of complex type inside of a nested + // complex type + SubOptions []SubOption `json:"SubOptions"` + + // Example field of a collection of primitive type inside of a nested + // complex type + OtherThings []string `json:OtherThings` +} + +type Engine struct { + // Example field of a complex type inside of a complex type + Options *Options `json:"Options"` + + // Example field which is a relationship to a different collection + // within a complex property + Manufacturer *Manufacturer `json:"Manufacturer"` +} + +// CDPlayer StereoType. +type CDPlayer struct { + ObjectType string `json:"ObjectType"` + Brand string `json:"Brand"` + NumberOfDisks int `json:"NumberOfDisks"` +} + +// Radio StereoType. +type Radio struct { + ObjectType string `json:"ObjectType"` + Brand string `json:"Brand"` + Frequency string `json:"Frequency"` +} + +type Address struct { + City string `json:"City,omitempty"` + Country string `json:"Country,omitempty"` +} + +type StereoType struct { + union json.RawMessage +} + +func (t StereoType) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err // nolint:wrapcheck +} + +func (t *StereoType) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err // nolint:wrapcheck +} + +func (t *StereoType) FromCDPlayer(v CDPlayer) error { + v.ObjectType = "CDPlayer" + b, err := json.Marshal(v) + t.union = b + return err // nolint:wrapcheck +} + +func (t *StereoType) FromRadio(v Radio) error { + v.ObjectType = "Radio" + b, err := json.Marshal(v) + t.union = b + return err // nolint:wrapcheck +} + +type Manufacturer struct { + ID string `json:"Id"` + Name string `json:"Name,omitempty"` + Address *Address `json:"Address,omitempty"` +} + +type Car struct { + ID string `json:"Id"` + + // Example field which is a primitive type + ModelName string `json:"ModelName"` + Seats int `json:"Seats"` + + // Example field which is a complex property + Engine *Engine `json:"Engine"` + + // Example field which can have different object types + MainStereo StereoType `json:"MainStereo"` + + // Example field which is a collection of different object types + OtherStereos []StereoType `json:"OtherStereos"` + + // Example field which is a relationship to a different collection + Manufacturer *Manufacturer `json:"Manufacturer"` + + // Example field which is a collection of relationships to a + // different collection + Manufacturers []Manufacturer `json:"Manufacturers"` +} + +// Gorm Table Definitions. +type CarRow struct { + gorm.Model + Data datatypes.JSON +} + +type ManufacturerRow struct { + gorm.Model + Data datatypes.JSON +} + +var carSchemaMetas = map[string]SchemaMeta{ + "Car": { + Table: "car_rows", + Fields: map[string]FieldMeta{ + "Id": {FieldType: PrimitiveFieldType}, + "ModelName": {FieldType: PrimitiveFieldType}, + "Seats": {FieldType: PrimitiveFieldType}, + "Engine": { + FieldType: ComplexFieldType, + ComplexFieldSchemas: []string{"Engine"}, + }, + "MainStereo": { + FieldType: ComplexFieldType, + ComplexFieldSchemas: []string{"CDPlayer", "Radio"}, + DescriminatorProperty: "ObjectType", + }, + "OtherStereos": { + FieldType: CollectionFieldType, + CollectionItemMeta: &FieldMeta{ + FieldType: ComplexFieldType, + ComplexFieldSchemas: []string{"CDPlayer", "Radio"}, + DescriminatorProperty: "ObjectType", + }, + }, + "Manufacturer": { + FieldType: RelationshipFieldType, + RelationshipSchema: "Manufacturer", + RelationshipProperty: "Id", + }, + "Manufacturers": { + FieldType: RelationshipCollectionFieldType, + RelationshipSchema: "Manufacturer", + RelationshipProperty: "Id", + }, + }, + }, + "Manufacturer": { + Table: "manufacturer_rows", + Fields: map[string]FieldMeta{ + "Id": {FieldType: PrimitiveFieldType}, + "Name": {FieldType: PrimitiveFieldType}, + "Address": { + FieldType: ComplexFieldType, + ComplexFieldSchemas: []string{"Address"}, + }, + "Source": {FieldType: PrimitiveFieldType}, + }, + }, + "Engine": { + Fields: map[string]FieldMeta{ + "Options": { + FieldType: ComplexFieldType, + ComplexFieldSchemas: []string{"Options"}, + }, + "Manufacturer": { + FieldType: RelationshipFieldType, + RelationshipSchema: "Manufacturer", + RelationshipProperty: "Id", + }, + }, + }, + "Options": { + Fields: map[string]FieldMeta{ + "Supercharger": {FieldType: PrimitiveFieldType}, + "SubOptions": { + FieldType: CollectionFieldType, + CollectionItemMeta: &FieldMeta{ + FieldType: ComplexFieldType, + ComplexFieldSchemas: []string{"SubOption"}, + }, + }, + "OtherThings": { + FieldType: CollectionFieldType, + CollectionItemMeta: &FieldMeta{FieldType: PrimitiveFieldType}, + }, + }, + }, + "SubOption": { + Fields: map[string]FieldMeta{ + "Name": {FieldType: PrimitiveFieldType}, + }, + }, + "CDPlayer": { + Fields: map[string]FieldMeta{ + "ObjectType": {FieldType: PrimitiveFieldType}, + "Brand": {FieldType: PrimitiveFieldType}, + "NumberOfDisks": {FieldType: PrimitiveFieldType}, + }, + }, + "Radio": { + Fields: map[string]FieldMeta{ + "ObjectType": {FieldType: PrimitiveFieldType}, + "Brand": {FieldType: PrimitiveFieldType}, + "Frequency": {FieldType: PrimitiveFieldType}, + }, + }, + "Address": { + Fields: map[string]FieldMeta{ + "City": {FieldType: PrimitiveFieldType}, + "Country": {FieldType: PrimitiveFieldType}, + }, + }, +} + +func addManufacturer(db *gorm.DB, postfix string) (Manufacturer, error) { + id := uuid.New().String() + manufacturer := Manufacturer{ + ID: id, + Name: fmt.Sprintf("manu%s", postfix), + Address: &Address{ + City: fmt.Sprintf("city%s", postfix), + Country: "middleofnowhere", + }, + } + manuBytes, err := json.Marshal(manufacturer) + if err != nil { + return Manufacturer{}, fmt.Errorf("failed to marshal manufacturer: %w", err) + } + manu := ManufacturerRow{Data: manuBytes} + db.Create(&manu) + return manufacturer, nil +} + +func addCar(db *gorm.DB, model, manuID string, otherManus []string, seats int) (Car, error) { + id := uuid.New().String() + + otherMs := []Manufacturer{} + for _, otherManu := range otherManus { + otherMs = append(otherMs, Manufacturer{ + ID: otherManu, + }) + } + + cdPlayer1 := &StereoType{} + err := cdPlayer1.FromCDPlayer(CDPlayer{ + ObjectType: "CDPlayer", + Brand: "Sony", + NumberOfDisks: 12, + }) + if err != nil { + return Car{}, err + } + + cdPlayer2 := &StereoType{} + err = cdPlayer2.FromCDPlayer(CDPlayer{ + ObjectType: "CDPlayer", + Brand: "Unknown", + NumberOfDisks: 50, + }) + if err != nil { + return Car{}, err + } + + radio := &StereoType{} + err = radio.FromRadio(Radio{ + ObjectType: "Radio", + Brand: "Samsung", + Frequency: "500mhz", + }) + if err != nil { + return Car{}, err + } + + car := Car{ + ID: id, + ModelName: model, + Seats: seats, + Engine: &Engine{ + Options: &Options{ + Supercharger: false, + SubOptions: []SubOption{ + { + Name: "bluePaint", + }, + { + Name: "blueShoes", + }, + { + Name: "greenPaint", + }, + { + Name: "yellowPaint", + }, + }, + OtherThings: []string{ + "thing1", + "thing2", + }, + }, + Manufacturer: &Manufacturer{ + ID: manuID, + }, + }, + MainStereo: *cdPlayer1, + OtherStereos: []StereoType{ + *radio, + *cdPlayer2, + }, + Manufacturer: &Manufacturer{ + ID: manuID, + }, + Manufacturers: otherMs, + } + carBytes, err := json.Marshal(car) + if err != nil { + return Car{}, fmt.Errorf("failed to marshal car: %w", err) + } + carRow := CarRow{Data: carBytes} + db.Create(&carRow) + return car, nil +} + +// nolint:cyclop,maintidx +func Test_BuildSQLQuery(t *testing.T) { + dbLogger := logger.Default + dbLogger = dbLogger.LogMode(logger.Info) + + dir, err := os.MkdirTemp("", "") + if err != nil { + t.Errorf("Failed to create tmp dir for database: %v", err) + } + defer os.RemoveAll(dir) + + dbpath := path.Join(dir, "test.db") + db, err := gorm.Open(sqlite.Open(dbpath), &gorm.Config{ + Logger: dbLogger, + }) + if err != nil { + t.Errorf("failed to open db: %v", err) + } + + if err := db.AutoMigrate( + CarRow{}, + ManufacturerRow{}, + ); err != nil { + t.Errorf("failed to run auto migration: %v", err) + } + + manu1, err := addManufacturer(db, "1") + if err != nil { + t.Errorf("failed to add manufacturer to db %v", err) + } + + manu2, err := addManufacturer(db, "2") + if err != nil { + t.Errorf("failed to add manufacturer to db %v", err) + } + + manu3, err := addManufacturer(db, "3") + if err != nil { + t.Errorf("failed to add manufacturer to db %v", err) + } + + car1, err := addCar(db, "model1", manu1.ID, []string{}, 5) + if err != nil { + t.Errorf("failed to add car to db %v", err) + } + + car2, err := addCar(db, "model2", manu1.ID, []string{manu2.ID, manu3.ID}, 5) + if err != nil { + t.Errorf("failed to add car to db %v", err) + } + + car3, err := addCar(db, "model3", manu2.ID, []string{}, 2) + if err != nil { + t.Errorf("failed to add car to db %v", err) + } + + car4, err := addCar(db, "model4", manu3.ID, []string{}, 2) + if err != nil { + t.Errorf("failed to add car to db %v", err) + } + + tests := []struct { + name string + filterString *string + selectString *string + expandString *string + top *int + skip *int + want []Car + buildQueryError bool + }{ + { + name: "All cars, no filter, select, expand or pagination", + filterString: nil, + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{car1, car2, car3, car4}, + }, + { + name: "eq filter by primitive type ModelName", + filterString: PointerTo("ModelName eq 'model1'"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{car1}, + }, + { + name: "gt by primitive type Seats", + filterString: PointerTo("Seats gt 2"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{car1, car2}, + }, + { + name: "gt by primitive type on Seats with float", + filterString: PointerTo("Seats gt 2.0"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{car1, car2}, + }, + { + name: "gt by primitive type Seats with no results", + filterString: PointerTo("Seats gt 10"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{}, + }, + { + name: "gt by primitive type with float on Seats with no results", + filterString: PointerTo("Seats gt 10.0"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{}, + }, + { + name: "combined 'and' filter", + filterString: PointerTo("Seats gt 2 and ModelName eq 'model2'"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{car2}, + }, + { + name: "combined 'and' filter with no results", + filterString: PointerTo("Seats gt 2 and ModelName eq 'doesnotexist'"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{}, + }, + { + name: "combined 'or' filter", + filterString: PointerTo("ModelName eq 'model3' or Seats eq 5"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{car1, car2, car3}, + }, + { + name: "'contains' filter", + filterString: PointerTo("contains(ModelName, '1')"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{car1}, + }, + { + name: "filter on nested field", + filterString: PointerTo(fmt.Sprintf("Engine/Manufacturer/Id eq '%s'", manu1.ID)), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + want: []Car{car1, car2}, + }, + { + name: "mismatched brackets", + filterString: PointerTo("contains(ModelName, '1'"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + buildQueryError: true, + }, + { + name: "non-boolean filter", + filterString: PointerTo("ModelName eq"), + selectString: nil, + expandString: nil, + top: nil, + skip: nil, + buildQueryError: true, + }, + { + name: "simple select one field", + filterString: nil, + selectString: PointerTo("ModelName"), + expandString: nil, + top: nil, + skip: nil, + want: []Car{ + { + ModelName: car1.ModelName, + }, + { + ModelName: car2.ModelName, + }, + { + ModelName: car3.ModelName, + }, + { + ModelName: car4.ModelName, + }, + }, + }, + { + name: "simple select + select nested field", + filterString: nil, + selectString: PointerTo("Seats,Engine/Manufacturer"), + expandString: nil, + top: nil, + skip: nil, + want: []Car{ + { + Seats: car1.Seats, + Engine: &Engine{ + Manufacturer: car1.Manufacturer, + }, + }, + { + Seats: car2.Seats, + Engine: &Engine{ + Manufacturer: car2.Manufacturer, + }, + }, + { + Seats: car3.Seats, + Engine: &Engine{ + Manufacturer: car3.Manufacturer, + }, + }, + { + Seats: car4.Seats, + Engine: &Engine{ + Manufacturer: car4.Manufacturer, + }, + }, + }, + }, + { + name: "select with nested filter", + filterString: nil, + selectString: PointerTo("ModelName,Engine/Options/SubOptions($filter=contains(Name, 'blue'))"), + expandString: nil, + top: nil, + skip: nil, + want: []Car{ + { + ModelName: car1.ModelName, + Engine: &Engine{ + Options: &Options{ + SubOptions: []SubOption{ + { + Name: "bluePaint", + }, + { + Name: "blueShoes", + }, + }, + }, + }, + }, + { + ModelName: car2.ModelName, + Engine: &Engine{ + Options: &Options{ + SubOptions: []SubOption{ + { + Name: "bluePaint", + }, + { + Name: "blueShoes", + }, + }, + }, + }, + }, + { + ModelName: car3.ModelName, + Engine: &Engine{ + Options: &Options{ + SubOptions: []SubOption{ + { + Name: "bluePaint", + }, + { + Name: "blueShoes", + }, + }, + }, + }, + }, + { + ModelName: car4.ModelName, + Engine: &Engine{ + Options: &Options{ + SubOptions: []SubOption{ + { + Name: "bluePaint", + }, + { + Name: "blueShoes", + }, + }, + }, + }, + }, + }, + }, + { + name: "expand on relationship", + filterString: PointerTo(fmt.Sprintf("Id eq '%s'", car1.ID)), + selectString: PointerTo("ModelName,Manufacturer"), + expandString: PointerTo("Manufacturer"), + top: nil, + skip: nil, + want: []Car{ + { + ModelName: car1.ModelName, + Manufacturer: &manu1, + }, + }, + }, + { + name: "expand on relationship, relationship not in select", + filterString: PointerTo(fmt.Sprintf("Id eq '%s'", car1.ID)), + selectString: PointerTo("ModelName"), + expandString: PointerTo("Manufacturer"), + top: nil, + skip: nil, + want: []Car{ + { + ModelName: car1.ModelName, + Manufacturer: &manu1, + }, + }, + }, + { + name: "expand on relationship with nested select", + filterString: PointerTo(fmt.Sprintf("Id eq '%s'", car1.ID)), + selectString: PointerTo("ModelName"), + expandString: PointerTo("Manufacturer($select=Name)"), + top: nil, + skip: nil, + want: []Car{ + { + ModelName: car1.ModelName, + Manufacturer: &Manufacturer{ + Name: manu1.Name, + }, + }, + }, + }, + { + name: "expand on relationship collection", + filterString: PointerTo(fmt.Sprintf("Id eq '%s'", car2.ID)), + selectString: PointerTo("ModelName"), + expandString: PointerTo("Manufacturers"), + top: nil, + skip: nil, + want: []Car{ + { + ModelName: car2.ModelName, + Manufacturers: []Manufacturer{ + manu2, + manu3, + }, + }, + }, + }, + { + name: "expand on relationship collection with filter", + filterString: PointerTo(fmt.Sprintf("Id eq '%s'", car2.ID)), + selectString: PointerTo("ModelName"), + expandString: PointerTo("Manufacturers($filter=Name eq 'manu2')"), + top: nil, + skip: nil, + want: []Car{ + { + ModelName: car2.ModelName, + Manufacturers: []Manufacturer{ + manu2, + }, + }, + }, + }, + { + name: "expand on relationship within complex property", + filterString: PointerTo(fmt.Sprintf("Id eq '%s'", car2.ID)), + selectString: PointerTo("ModelName,Engine/Manufacturer"), + expandString: PointerTo("Engine/Manufacturer"), + top: nil, + skip: nil, + want: []Car{ + { + ModelName: car2.ModelName, + Engine: &Engine{ + Manufacturer: &manu1, + }, + }, + }, + }, + { + name: "expand mismatched brackets", + filterString: PointerTo(fmt.Sprintf("Id eq '%s'", car2.ID)), + selectString: PointerTo("ModelName,Engine/Manufacturer"), + expandString: PointerTo("Engine/Manufacturer($filter=Name eq 'foo'"), + top: nil, + skip: nil, + want: []Car{}, + buildQueryError: true, + }, + { + name: "expand bad nested filter", + filterString: PointerTo(fmt.Sprintf("Id eq '%s'", car2.ID)), + selectString: PointerTo("ModelName,Engine/Manufacturer"), + expandString: PointerTo("Engine/Manufacturer($filter=Name eq)"), + top: nil, + skip: nil, + want: []Car{}, + buildQueryError: true, + }, + } + + for _, test := range tests { + query, err := BuildSQLQuery(carSchemaMetas, "Car", test.filterString, test.selectString, test.expandString, test.top, test.skip) + if err != nil { + if !test.buildQueryError { + t.Errorf("[%s] failed to build query for DB: %v", test.name, err) + } + continue + } + + carResult, err := findCars(db, query) + if err != nil { + t.Errorf("[%s] failed to fetch cars: %v", test.name, err) + continue + } + + if diff := cmp.Diff(test.want, carResult, cmp.AllowUnexported(StereoType{})); diff != "" { + t.Errorf("[%s] []Car mismatch (-want +got):\n%s", test.name, diff) + } + } +} + +func findCars(db *gorm.DB, query string) ([]Car, error) { + var results []CarRow + if err := db.Raw(query).Find(&results).Error; err != nil { + return []Car{}, fmt.Errorf("failed to query DB: %w", err) + } + + carResult := []Car{} + for _, result := range results { + var car Car + err := json.Unmarshal(result.Data, &car) + if err != nil { + return []Car{}, fmt.Errorf("failed to unmarshal car data %v: %w", result.Data, err) + } + carResult = append(carResult, car) + } + + return carResult, nil +} + +func PointerTo[T any](value T) *T { + return &value +} diff --git a/backend/pkg/database/odatasql/schema.go b/backend/pkg/database/odatasql/schema.go new file mode 100644 index 000000000..38678bae8 --- /dev/null +++ b/backend/pkg/database/odatasql/schema.go @@ -0,0 +1,48 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 odatasql + +type FieldType string + +const ( + PrimitiveFieldType FieldType = "primitive" + CollectionFieldType FieldType = "collection" + ComplexFieldType FieldType = "complex" + RelationshipFieldType FieldType = "relationship" + RelationshipCollectionFieldType FieldType = "relationshipCollection" +) + +type FieldMeta struct { + FieldType FieldType + + // Field for collection field types + CollectionItemMeta *FieldMeta + + // Fields for complex field types + ComplexFieldSchemas []string + DescriminatorProperty string + + // Fields for relationship and relationship collection types + RelationshipSchema string + RelationshipProperty string +} + +type SchemaMeta struct { + Table string + Fields map[string]FieldMeta +} + +type Schema map[string]FieldMeta diff --git a/backend/pkg/database/odatasql/selectTree.go b/backend/pkg/database/odatasql/selectTree.go new file mode 100644 index 000000000..6365d35fd --- /dev/null +++ b/backend/pkg/database/odatasql/selectTree.go @@ -0,0 +1,114 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 odatasql + +import ( + "context" + "fmt" + + "github.com/CiscoM31/godata" +) + +type selectNode struct { + children map[string]*selectNode + expandChildren map[string]struct{} + selectChildren map[string]struct{} + filter *godata.GoDataFilterQuery + expand bool +} + +func newSelectTree() *selectNode { + return &selectNode{ + children: map[string]*selectNode{}, + expandChildren: map[string]struct{}{}, + selectChildren: map[string]struct{}{}, + } +} + +// nolint:gocognit,cyclop +func (st *selectNode) insert(path []*godata.Token, filter *godata.GoDataFilterQuery, sel *godata.GoDataSelectQuery, subExpand *godata.GoDataExpandQuery, expand bool) error { + // If path length == 0 then we've reach the bottom of the path and now + // we need to save the filter/select and process any sub + // selects/expands + if len(path) == 0 { + if st.filter != nil { + return fmt.Errorf("filter for field specified twice") + } + st.filter = filter + + st.expand = expand + + if sel != nil { + if len(st.children) > 0 { + return fmt.Errorf("can not specify selection for field in multiple places") + } + + // Parse $select using ParseExpandString because godata.ParseSelectString + // is a nieve implementation and doesn't handle query options properly + childSelections, err := godata.ParseExpandString(context.TODO(), sel.RawValue) + if err != nil { + return fmt.Errorf("failed to parse select: %w", err) + } + + for _, s := range childSelections.ExpandItems { + if s.Expand != nil { + return fmt.Errorf("expand can not be specified inside of select") + } + err := st.insert(s.Path, s.Filter, s.Select, nil, false) + if err != nil { + return err + } + } + } + + if subExpand != nil { + for _, s := range subExpand.ExpandItems { + err := st.insert(s.Path, s.Filter, s.Select, s.Expand, true) + if err != nil { + return err + } + } + } + + return nil + } + + // Keep adding the items in the path to the tree. First find if this + // entry is already in the tree by checking children, if it is then + // insert the next part of the path into that child, otherwise add a + // new child and insert it there. + childName := path[0].Value + child, ok := st.children[childName] + if !ok { + st.children[childName] = newSelectTree() + child = st.children[childName] + } + + // Keep track of which children where added as part of $select or + // $expand, or both. + if expand { + st.expandChildren[childName] = struct{}{} + } else { + st.selectChildren[childName] = struct{}{} + } + + err := child.insert(path[1:], filter, sel, subExpand, expand) + if err != nil { + return err + } + + return nil +} diff --git a/backend/pkg/database/scan.go b/backend/pkg/database/scan.go deleted file mode 100644 index d59632763..000000000 --- a/backend/pkg/database/scan.go +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright © 2022 Cisco Systems, Inc. and its affiliates. -// All rights reserved. -// -// 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 ( - "context" - "errors" - "fmt" - "time" - - uuid "github.com/satori/go.uuid" - "gorm.io/gorm" - - "github.com/openclarity/vmclarity/backend/pkg/common" -) - -const ( - scansTableName = "scans" -) - -type Scan struct { - Base - - ScanStartTime *time.Time `json:"scan_start_time,omitempty" gorm:"column:scan_start_time"` - ScanEndTime *time.Time `json:"scan_end_time,omitempty" gorm:"column:scan_end_time"` - - // ScanConfigID The ID of the config that this scan was initiated from (optionanl) - ScanConfigID *string `json:"scan_config_id,omitempty" gorm:"column:scan_config_id"` - // ScanFamiliesConfig The configuration of the scanner families within a scan config - ScanConfigSnapshot []byte `json:"scan_families_config,omitempty" gorm:"column:scan_families_config"` - - // TargetIDs List of target IDs that are targeted for scanning as part of this scan - TargetIDs []byte `json:"target_ids,omitempty" gorm:"column:target_ids"` -} - -type GetScansParams struct { - // Filter Odata filter - Filter *string - // Page Page number of the query - Page *int - // PageSize Maximum items to return - PageSize *int -} - -type ScansTable interface { - GetScansAndTotal(params GetScansParams) ([]*Scan, int64, error) - GetScan(scanID uuid.UUID) (*Scan, error) - UpdateScan(scan *Scan) (*Scan, error) - SaveScan(scan *Scan) (*Scan, error) - DeleteScan(scanID uuid.UUID) error - CreateScan(scan *Scan) (*Scan, error) -} - -type ScansTableHandler struct { - scansTable *gorm.DB -} - -func (db *Handler) ScansTable() ScansTable { - return &ScansTableHandler{ - scansTable: db.DB.Table(scansTableName), - } -} - -func (s *ScansTableHandler) GetScansAndTotal(params GetScansParams) ([]*Scan, int64, error) { - var count int64 - var scans []*Scan - - tx := s.scansTable - - if err := tx.Count(&count).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count total: %w", err) - } - - if err := tx.Find(&scans).Error; err != nil { - return nil, 0, fmt.Errorf("failed to find scans: %w", err) - } - - return scans, count, nil -} - -func (s *ScansTableHandler) CreateScan(scan *Scan) (*Scan, error) { - // check if there is already a running scan for that scan config id. - existingSR, exist, err := s.checkExist(*scan.ScanConfigID) - if err != nil { - return nil, fmt.Errorf("failed to check existing scan: %w", err) - } - if exist { - return existingSR, &common.ConflictError{ - Reason: fmt.Sprintf("There is a running scan with scanConfigID=%s", *existingSR.ScanConfigID), - } - } - - if err := s.scansTable.Create(scan).Error; err != nil { - return nil, fmt.Errorf("failed to create scan in db: %w", err) - } - return scan, nil -} - -func (s *ScansTableHandler) SaveScan(scan *Scan) (*Scan, error) { - if err := s.scansTable.Save(scan).Error; err != nil { - return nil, fmt.Errorf("failed to save scan in db: %w", err) - } - - return scan, nil -} - -func (s *ScansTableHandler) UpdateScan(scan *Scan) (*Scan, error) { - if err := s.scansTable.Model(scan).Updates(scan).Error; err != nil { - return nil, fmt.Errorf("failed to update scan in db: %w", err) - } - return scan, nil -} - -func (s *ScansTableHandler) GetScan(scanID uuid.UUID) (*Scan, error) { - var scan *Scan - - if err := s.scansTable.Where("id = ?", scanID).First(&scan).Error; err != nil { - return nil, fmt.Errorf("failed to get scan by id %q: %w", scanID, err) - } - - return scan, nil -} - -func (s *ScansTableHandler) DeleteScan(scanID uuid.UUID) error { - if err := s.scansTable.Delete(&Scan{}, scanID).Error; err != nil { - return fmt.Errorf("failed to delete scan: %w", err) - } - return nil -} - -func (s *ScansTableHandler) checkExist(scanConfigID string) (*Scan, bool, error) { - var scans []Scan - - tx := s.scansTable.WithContext(context.Background()) - - if err := tx.Where("scan_config_id = ?", scanConfigID).Find(&scans).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, false, nil - } - return nil, false, fmt.Errorf("failed to query: %w", err) - } - - // check if there is a running scan (end time not set) - for i, scan := range scans { - if scan.ScanEndTime == nil { - return &scans[i], true, nil - } - } - - return nil, false, nil -} diff --git a/backend/pkg/database/scan_config.go b/backend/pkg/database/scan_config.go deleted file mode 100644 index f559823e7..000000000 --- a/backend/pkg/database/scan_config.go +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright © 2022 Cisco Systems, Inc. and its affiliates. -// All rights reserved. -// -// 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 ( - "context" - "errors" - "fmt" - - uuid "github.com/satori/go.uuid" - "gorm.io/gorm" - - "github.com/openclarity/vmclarity/backend/pkg/common" -) - -const ( - scanConfigsTableName = "scan_configs" -) - -type ScanConfig struct { - Base - - Name *string `json:"name,omitempty" gorm:"column:name"` - - // ScanFamiliesConfig The configuration of the scanner families within a scan config - ScanFamiliesConfig []byte `json:"scan_families_config,omitempty" gorm:"column:scan_families_config"` - Scheduled []byte `json:"scheduled,omitempty" gorm:"column:scheduled"` - Scope []byte `json:"scope,omitempty" gorm:"column:scope"` -} - -type GetScanConfigsParams struct { - // Filter Odata filter - Filter *string - // Page Page number of the query - Page *int - // PageSize Maximum items to return - PageSize *int -} - -type ScanConfigsTable interface { - GetScanConfigsAndTotal(params GetScanConfigsParams) ([]*ScanConfig, int64, error) - GetScanConfig(scanConfigID uuid.UUID) (*ScanConfig, error) - UpdateScanConfig(scanConfig *ScanConfig) (*ScanConfig, error) - SaveScanConfig(scanConfig *ScanConfig) (*ScanConfig, error) - DeleteScanConfig(scanConfigID uuid.UUID) error - CreateScanConfig(scanConfig *ScanConfig) (*ScanConfig, error) -} - -type ScanConfigsTableHandler struct { - scanConfigsTable *gorm.DB -} - -func (db *Handler) ScanConfigsTable() ScanConfigsTable { - return &ScanConfigsTableHandler{ - scanConfigsTable: db.DB.Table(scanConfigsTableName), - } -} - -func (s *ScanConfigsTableHandler) GetScanConfigsAndTotal(params GetScanConfigsParams) ([]*ScanConfig, int64, error) { - var count int64 - var scanConfigs []*ScanConfig - - tx := s.scanConfigsTable - - if err := tx.Count(&count).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count total: %w", err) - } - - if err := tx.Find(&scanConfigs).Error; err != nil { - return nil, 0, fmt.Errorf("failed to find scan configs: %w", err) - } - - return scanConfigs, count, nil -} - -func (s *ScanConfigsTableHandler) CreateScanConfig(scanConfig *ScanConfig) (*ScanConfig, error) { - // check if there is already a scan config with that name. - existingSR, exist, err := s.checkExist(*scanConfig.Name) - if err != nil { - return nil, fmt.Errorf("failed to check existing scan config: %w", err) - } - if exist { - return existingSR, &common.ConflictError{ - Reason: fmt.Sprintf("Scan config exists with name=%s", *existingSR.Name), - } - } - - if err := s.scanConfigsTable.Create(scanConfig).Error; err != nil { - return nil, fmt.Errorf("failed to create scan config in db: %w", err) - } - return scanConfig, nil -} - -func (s *ScanConfigsTableHandler) SaveScanConfig(scanConfig *ScanConfig) (*ScanConfig, error) { - if err := s.scanConfigsTable.Save(scanConfig).Error; err != nil { - return nil, fmt.Errorf("failed to save scan config in db: %w", err) - } - - return scanConfig, nil -} - -func (s *ScanConfigsTableHandler) UpdateScanConfig(scanConfig *ScanConfig) (*ScanConfig, error) { - if err := s.scanConfigsTable.Model(scanConfig).Updates(scanConfig).Error; err != nil { - return nil, fmt.Errorf("failed to update scan config in db: %w", err) - } - - return scanConfig, nil -} - -func (s *ScanConfigsTableHandler) GetScanConfig(scanConfigID uuid.UUID) (*ScanConfig, error) { - var scanConfig *ScanConfig - - if err := s.scanConfigsTable.Where("id = ?", scanConfigID).First(&scanConfig).Error; err != nil { - return nil, fmt.Errorf("failed to get scan config by id %q: %w", scanConfigID, err) - } - - return scanConfig, nil -} - -func (s *ScanConfigsTableHandler) DeleteScanConfig(scanConfigID uuid.UUID) error { - if err := s.scanConfigsTable.Delete(&Scan{}, scanConfigID).Error; err != nil { - return fmt.Errorf("failed to delete scan config: %w", err) - } - return nil -} - -func (s *ScanConfigsTableHandler) checkExist(name string) (*ScanConfig, bool, error) { - var scanConfig *ScanConfig - - tx := s.scanConfigsTable.WithContext(context.Background()) - - if err := tx.Where("name = ?", name).First(&scanConfig).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, false, nil - } - return nil, false, fmt.Errorf("failed to query: %w", err) - } - - return scanConfig, true, nil -} diff --git a/backend/pkg/database/scan_result.go b/backend/pkg/database/scan_result.go deleted file mode 100644 index e5b5484b6..000000000 --- a/backend/pkg/database/scan_result.go +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright © 2022 Cisco Systems, Inc. and its affiliates. -// All rights reserved. -// -// 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 ( - "context" - "errors" - "fmt" - - uuid "github.com/satori/go.uuid" - "gorm.io/gorm" - - "github.com/openclarity/vmclarity/backend/pkg/common" -) - -const ( - scanResultsTableName = "scan_results" -) - -type ScanResult struct { - Base - - ScanID string `json:"scan_id,omitempty" gorm:"column:scan_id"` - TargetID string `json:"target_id,omitempty" gorm:"column:target_id"` - - Exploits []byte `json:"exploits,omitempty" gorm:"column:exploits"` - Malware []byte `json:"malware,omitempty" gorm:"column:malware"` - Misconfigurations []byte `json:"misconfigurations,omitempty" gorm:"column:misconfigurations"` - Rootkits []byte `json:"rootkits,omitempty" gorm:"column:rootkits"` - Sboms []byte `json:"sboms,omitempty" gorm:"column:sboms"` - Secrets []byte `json:"secrets,omitempty" gorm:"column:secrets"` - Status []byte `json:"status,omitempty" gorm:"column:status"` - Vulnerabilities []byte `json:"vulnerabilities,omitempty" gorm:"column:vulnerabilities"` -} - -type GetScanResultsParams struct { - // Filter Odata filter - Filter *string - // Select Odata select - Select *string - // Page Page number of the query - Page *int - // PageSize Maximum items to return - PageSize *int -} - -type GetScanResultsScanResultIDParams struct { - // Select Odata select - Select *string -} - -//nolint:interfacebloat -type ScanResultsTable interface { - CreateScanResult(scanResults *ScanResult) (*ScanResult, error) - GetScanResultsAndTotal(params GetScanResultsParams) ([]*ScanResult, int64, error) - GetScanResult(scanResultID uuid.UUID, params GetScanResultsScanResultIDParams) (*ScanResult, error) - UpdateScanResult(scanResults *ScanResult) (*ScanResult, error) - SaveScanResult(scanResults *ScanResult) (*ScanResult, error) -} - -type ScanResultsTableHandler struct { - scanResultsTable *gorm.DB -} - -func (db *Handler) ScanResultsTable() ScanResultsTable { - return &ScanResultsTableHandler{ - scanResultsTable: db.DB.Table(scanResultsTableName), - } -} - -func (s *ScanResultsTableHandler) GetScanResult(scanResultID uuid.UUID, params GetScanResultsScanResultIDParams) (*ScanResult, error) { - var scanResult *ScanResult - - if err := s.scanResultsTable.Where("id = ?", scanResultID).First(&scanResult).Error; err != nil { - return nil, fmt.Errorf("failed to get scan result by id %q: %w", scanResultID, err) - } - - return scanResult, nil -} - -func (s *ScanResultsTableHandler) CreateScanResult(scanResult *ScanResult) (*ScanResult, error) { - // check if there is already a scanResult for that scan id and target id. - existingSR, exist, err := s.checkExist(scanResult.ScanID, scanResult.TargetID) - if err != nil { - return nil, fmt.Errorf("failed to check existing scan result: %w", err) - } - if exist { - return existingSR, &common.ConflictError{ - Reason: fmt.Sprintf("Target scan result exists with scanID=%s, targetID=%s", existingSR.ScanID, existingSR.TargetID), - } - } - - if err := s.scanResultsTable.Create(scanResult).Error; err != nil { - return nil, fmt.Errorf("failed to create scan result in db: %w", err) - } - return scanResult, nil -} - -func (s *ScanResultsTableHandler) GetScanResultsAndTotal(params GetScanResultsParams) ([]*ScanResult, int64, error) { - var count int64 - var scanResults []*ScanResult - - tx := s.scanResultsTable - - if err := tx.Count(&count).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count total: %w", err) - } - - if err := tx.Find(&scanResults).Error; err != nil { - return nil, 0, fmt.Errorf("failed to find scan results: %w", err) - } - - return scanResults, count, nil -} - -func (s *ScanResultsTableHandler) SaveScanResult(scanResult *ScanResult) (*ScanResult, error) { - if err := s.scanResultsTable.Save(scanResult).Error; err != nil { - return nil, fmt.Errorf("failed to save scan result in db: %w", err) - } - - return scanResult, nil -} - -func (s *ScanResultsTableHandler) UpdateScanResult(scanResult *ScanResult) (*ScanResult, error) { - if err := s.scanResultsTable.Model(scanResult).Updates(scanResult).Error; err != nil { - return nil, fmt.Errorf("failed to update scan result in db: %w", err) - } - - return scanResult, nil -} - -func (s *ScanResultsTableHandler) checkExist(scanID string, targetID string) (*ScanResult, bool, error) { - var scanResult *ScanResult - - tx := s.scanResultsTable.WithContext(context.Background()) - - if err := tx.Where("scan_id = ? AND target_id = ?", scanID, targetID).First(&scanResult).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, false, nil - } - return nil, false, fmt.Errorf("failed to query: %w", err) - } - - return scanResult, true, nil -} diff --git a/backend/pkg/database/target.go b/backend/pkg/database/target.go deleted file mode 100644 index 1eeb68a3a..000000000 --- a/backend/pkg/database/target.go +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright © 2022 Cisco Systems, Inc. and its affiliates. -// All rights reserved. -// -// 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 ( - "context" - "errors" - "fmt" - - uuid "github.com/satori/go.uuid" - "gorm.io/gorm" - - "github.com/openclarity/vmclarity/backend/pkg/common" -) - -const ( - targetsTableName = "targets" -) - -type Target struct { - Base - - Type string `json:"type,omitempty" gorm:"column:type"` - Location *string `json:"location,omitempty" gorm:"column:location"` - - // VMInfo - InstanceID *string `json:"instance_id,omitempty" gorm:"column:instance_id"` - InstanceProvider *string `json:"instance_provider,omitempty" gorm:"column:instance_provider"` - - // PodInfo - PodName *string `json:"pod_name,omitempty" gorm:"column:pod_name"` - - // DirInfo - DirName *string `json:"dir_name,omitempty" gorm:"column:dir_name"` -} - -type GetTargetsParams struct { - // Filter Odata filter - Filter *string - // Page Page number of the query - Page *int - // PageSize Maximum items to return - PageSize *int -} - -type TargetsTable interface { - GetTargetsAndTotal(params GetTargetsParams) ([]*Target, int64, error) - GetTarget(targetID uuid.UUID) (*Target, error) - CreateTarget(target *Target) (*Target, error) - SaveTarget(target *Target) (*Target, error) - DeleteTarget(targetID uuid.UUID) error -} - -type TargetsTableHandler struct { - targetsTable *gorm.DB -} - -func (db *Handler) TargetsTable() TargetsTable { - return &TargetsTableHandler{ - targetsTable: db.DB.Table(targetsTableName), - } -} - -func (t *TargetsTableHandler) GetTargetsAndTotal(params GetTargetsParams) ([]*Target, int64, error) { - var count int64 - var targets []*Target - - tx := t.targetsTable - - if err := tx.Count(&count).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count total: %w", err) - } - - if err := tx.Find(&targets).Error; err != nil { - return nil, 0, fmt.Errorf("failed to find targets: %w", err) - } - - return targets, count, nil -} - -func (t *TargetsTableHandler) GetTarget(targetID uuid.UUID) (*Target, error) { - var target *Target - - if err := t.targetsTable.Where("id = ?", targetID).First(&target).Error; err != nil { - return nil, fmt.Errorf("failed to get target by id %q: %w", targetID, err) - } - - return target, nil -} - -func (t *TargetsTableHandler) CreateTarget(target *Target) (*Target, error) { - existingTarget, exist, err := t.checkExist(target) - if err != nil { - return nil, fmt.Errorf("failed to check existing target: %w", err) - } - if exist { - return existingTarget, &common.ConflictError{ - Reason: fmt.Sprintf("Target exists with the unique constraint combination: %s", getUniqueConstraintsOfTarget(existingTarget)), - } - } - - if err := t.targetsTable.Create(target).Error; err != nil { - return nil, fmt.Errorf("failed to create target in db: %w", err) - } - return target, nil -} - -func (t *TargetsTableHandler) SaveTarget(target *Target) (*Target, error) { - if err := t.targetsTable.Save(target).Error; err != nil { - return nil, fmt.Errorf("failed to save target in db: %w", err) - } - - return target, nil -} - -func (t *TargetsTableHandler) DeleteTarget(targetID uuid.UUID) error { - if err := t.targetsTable.Delete(&Scan{}, targetID).Error; err != nil { - return fmt.Errorf("failed to delete target: %w", err) - } - return nil -} - -func (t *TargetsTableHandler) checkExist(target *Target) (*Target, bool, error) { - var targetFromDB Target - - tx := t.targetsTable.WithContext(context.Background()) - - switch target.Type { - case "VMInfo": - if err := tx.Where("instance_id = ? AND location = ?", *target.InstanceID, *target.Location).First(&targetFromDB).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, false, nil - } - return nil, false, fmt.Errorf("failed to query: %w", err) - } - } - - return &targetFromDB, true, nil -} - -func getUniqueConstraintsOfTarget(target *Target) string { - switch target.Type { - case "VMInfo": - return fmt.Sprintf("instanceID=%s, region=%s", *target.InstanceID, *target.Location) - case "Dir": - return "unsupported target type Dir" - case "Pod": - return "unsupported target type Pod" - default: - return fmt.Sprintf("unknown target type: %v", target.Type) - } -} diff --git a/backend/pkg/database/types/database.go b/backend/pkg/database/types/database.go new file mode 100644 index 000000000..7e7b9783c --- /dev/null +++ b/backend/pkg/database/types/database.go @@ -0,0 +1,91 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// 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 types + +import ( + "errors" + + "github.com/openclarity/vmclarity/api/models" +) + +const ( + DBDriverTypeLocal = "LOCAL" +) + +var ErrNotFound = errors.New("not found") + +type DBConfig struct { + EnableInfoLogs bool `json:"enable-info-logs"` + DriverType string `json:"driver-type,omitempty"` + DBPassword string `json:"-"` + DBUser string `json:"db-user,omitempty"` + DBHost string `json:"db-host,omitempty"` + DBPort string `json:"db-port,omitempty"` + DBName string `json:"db-name,omitempty"` + + LocalDBPath string `json:"local-db-path,omitempty"` +} + +type Database interface { + ScanResultsTable() ScanResultsTable + ScanConfigsTable() ScanConfigsTable + ScansTable() ScansTable + TargetsTable() TargetsTable +} + +type ScansTable interface { + GetScans(params models.GetScansParams) (models.Scans, error) + GetScan(scanID models.ScanID) (models.Scan, error) + + CreateScan(scan models.Scan) (models.Scan, error) + UpdateScan(scan models.Scan) (models.Scan, error) + SaveScan(scan models.Scan) (models.Scan, error) + + DeleteScan(scanID models.ScanID) error +} + +type ScanResultsTable interface { + GetScanResults(params models.GetScanResultsParams) (models.TargetScanResults, error) + GetScanResult(scanResultID models.ScanResultID, params models.GetScanResultsScanResultIDParams) (models.TargetScanResult, error) + + CreateScanResult(scanResults models.TargetScanResult) (models.TargetScanResult, error) + UpdateScanResult(scanResults models.TargetScanResult) (models.TargetScanResult, error) + SaveScanResult(scanResults models.TargetScanResult) (models.TargetScanResult, error) + + // DeleteScanResult(scanResultID models.ScanResultID) error +} + +type ScanConfigsTable interface { + GetScanConfigs(params models.GetScanConfigsParams) (models.ScanConfigs, error) + GetScanConfig(scanConfigID models.ScanConfigID, params models.GetScanConfigsScanConfigIDParams) (models.ScanConfig, error) + + CreateScanConfig(scanConfig models.ScanConfig) (models.ScanConfig, error) + UpdateScanConfig(scanConfig models.ScanConfig) (models.ScanConfig, error) + SaveScanConfig(scanConfig models.ScanConfig) (models.ScanConfig, error) + + DeleteScanConfig(scanConfigID models.ScanConfigID) error +} + +type TargetsTable interface { + GetTargets(params models.GetTargetsParams) (models.Targets, error) + GetTarget(targetID models.TargetID) (models.Target, error) + + CreateTarget(target models.Target) (models.Target, error) + // UpdateTarget(target models.Target) (models.Target, error) + SaveTarget(target models.Target) (models.Target, error) + + DeleteTarget(targetID models.TargetID) error +} diff --git a/backend/pkg/rest/convert/dbtorest/convert_test.go b/backend/pkg/rest/convert/dbtorest/convert_test.go deleted file mode 100644 index 3a8df2fbe..000000000 --- a/backend/pkg/rest/convert/dbtorest/convert_test.go +++ /dev/null @@ -1,352 +0,0 @@ -// Copyright © 2022 Cisco Systems, Inc. and its affiliates. -// All rights reserved. -// -// 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 dbtorest - -import ( - "encoding/json" - "reflect" - "testing" - - uuid "github.com/satori/go.uuid" - "gotest.tools/v3/assert" - - "github.com/openclarity/vmclarity/api/models" - "github.com/openclarity/vmclarity/backend/pkg/database" - "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" -) - -func TestConvertScanConfig(t *testing.T) { - scanFamiliesConfig := models.ScanFamiliesConfig{ - Vulnerabilities: &models.VulnerabilitiesConfig{Enabled: utils.BoolPtr(true)}, - } - - scanFamiliesConfigB, err := json.Marshal(&scanFamiliesConfig) - assert.NilError(t, err) - - awsScanScope := models.AwsScanScope{ - All: utils.BoolPtr(true), - InstanceTagExclusion: nil, - InstanceTagSelector: nil, - ObjectType: "AwsScanScope", - Regions: nil, - ShouldScanStoppedInstances: utils.BoolPtr(false), - } - - var scanScopeType models.ScanScopeType - - err = scanScopeType.FromAwsScanScope(awsScanScope) - assert.NilError(t, err) - - scanScopeTypeB, err := scanScopeType.MarshalJSON() - assert.NilError(t, err) - - byHoursScheduleScanConfig := models.ByHoursScheduleScanConfig{ - HoursInterval: utils.IntPtr(2), - ObjectType: "ByHoursScheduleScanConfig", - } - - var runtimeScheduleScanConfigType models.RuntimeScheduleScanConfigType - err = runtimeScheduleScanConfigType.FromByHoursScheduleScanConfig(byHoursScheduleScanConfig) - assert.NilError(t, err) - - runtimeScheduleScanConfigTypeB, err := runtimeScheduleScanConfigType.MarshalJSON() - assert.NilError(t, err) - - uid := uuid.NewV4() - - type args struct { - config *database.ScanConfig - } - tests := []struct { - name string - args args - want *models.ScanConfig - wantErr bool - }{ - { - name: "sanity", - args: args{ - config: &database.ScanConfig{ - Base: database.Base{ - ID: uid, - }, - Name: utils.StringPtr("test"), - ScanFamiliesConfig: scanFamiliesConfigB, - Scheduled: runtimeScheduleScanConfigTypeB, - Scope: scanScopeTypeB, - }, - }, - want: &models.ScanConfig{ - Id: utils.StringPtr(uid.String()), - Name: utils.StringPtr("test"), - ScanFamiliesConfig: &scanFamiliesConfig, - Scheduled: &runtimeScheduleScanConfigType, - Scope: &scanScopeType, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ConvertScanConfig(tt.args.config) - if (err != nil) != tt.wantErr { - t.Errorf("ConvertScanConfig() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ConvertScanConfig() got = %v, want %v", got, tt.want) - } - }) - } -} - -func TestConvertScanConfigs(t *testing.T) { - scanFamiliesConfig := models.ScanFamiliesConfig{ - Vulnerabilities: &models.VulnerabilitiesConfig{Enabled: utils.BoolPtr(true)}, - } - - scanFamiliesConfigB, err := json.Marshal(&scanFamiliesConfig) - assert.NilError(t, err) - - awsScanScope := models.AwsScanScope{ - All: utils.BoolPtr(true), - InstanceTagExclusion: nil, - InstanceTagSelector: nil, - ObjectType: "AwsScanScope", - Regions: nil, - ShouldScanStoppedInstances: utils.BoolPtr(false), - } - - var scanScopeType models.ScanScopeType - - err = scanScopeType.FromAwsScanScope(awsScanScope) - assert.NilError(t, err) - - scanScopeTypeB, err := scanScopeType.MarshalJSON() - assert.NilError(t, err) - - byHoursScheduleScanConfig := models.ByHoursScheduleScanConfig{ - HoursInterval: utils.IntPtr(2), - ObjectType: "ByHoursScheduleScanConfig", - } - - var runtimeScheduleScanConfigType models.RuntimeScheduleScanConfigType - err = runtimeScheduleScanConfigType.FromByHoursScheduleScanConfig(byHoursScheduleScanConfig) - assert.NilError(t, err) - - runtimeScheduleScanConfigTypeB, err := runtimeScheduleScanConfigType.MarshalJSON() - assert.NilError(t, err) - - uid := uuid.NewV4() - total := 1 - - type args struct { - configs []*database.ScanConfig - total int64 - } - tests := []struct { - name string - args args - want *models.ScanConfigs - wantErr bool - }{ - { - name: "sanity", - args: args{ - configs: []*database.ScanConfig{ - { - Base: database.Base{ - ID: uid, - }, - Name: utils.StringPtr("test"), - ScanFamiliesConfig: scanFamiliesConfigB, - Scheduled: runtimeScheduleScanConfigTypeB, - Scope: scanScopeTypeB, - }, - }, - total: 1, - }, - want: &models.ScanConfigs{ - Items: &[]models.ScanConfig{ - { - Id: utils.StringPtr(uid.String()), - Name: utils.StringPtr("test"), - ScanFamiliesConfig: &scanFamiliesConfig, - Scheduled: &runtimeScheduleScanConfigType, - Scope: &scanScopeType, - }, - }, - Total: &total, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ConvertScanConfigs(tt.args.configs, tt.args.total) - if (err != nil) != tt.wantErr { - t.Errorf("ConvertScanConfigs() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ConvertScanConfigs() got = %v, want %v", got, tt.want) - } - }) - } -} - -func TestConvertScanResult(t *testing.T) { - state := models.DONE - status := models.TargetScanStatus{ - Vulnerabilities: &models.TargetScanState{ - Errors: nil, - State: &state, - }, - Exploits: &models.TargetScanState{ - Errors: &[]string{"err"}, - State: &state, - }, - } - - vulsScan := models.VulnerabilityScan{ - Vulnerabilities: &[]models.Vulnerability{ - { - VulnerabilityInfo: &models.VulnerabilityInfo{ - VulnerabilityName: utils.StringPtr("name"), - }, - }, - }, - } - - vulScanB, err := json.Marshal(&vulsScan) - assert.NilError(t, err) - - statusB, err := json.Marshal(&status) - assert.NilError(t, err) - - uid := uuid.NewV4() - - type args struct { - scanResult *database.ScanResult - } - tests := []struct { - name string - args args - want *models.TargetScanResult - wantErr bool - }{ - { - name: "sanity", - args: args{ - scanResult: &database.ScanResult{ - Base: database.Base{ - ID: uid, - }, - ScanID: "1", - TargetID: "2", - Status: statusB, - Vulnerabilities: vulScanB, - }, - }, - want: &models.TargetScanResult{ - Id: utils.StringPtr(uid.String()), - ScanId: "1", - Status: &status, - TargetId: "2", - Vulnerabilities: &vulsScan, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ConvertScanResult(tt.args.scanResult) - if (err != nil) != tt.wantErr { - t.Errorf("ConvertScanResult() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ConvertScanResult() got = %v, want %v", got, tt.want) - } - }) - } -} - -func TestConvertScan(t *testing.T) { - scanSnap := models.ScanConfigData{ - ScanFamiliesConfig: &models.ScanFamiliesConfig{ - Vulnerabilities: &models.VulnerabilitiesConfig{ - Enabled: nil, - }, - }, - } - - scanSnapB, err := json.Marshal(&scanSnap) - assert.NilError(t, err) - - targetIDs := []string{"s"} - targetIDsB, err := json.Marshal(&targetIDs) - assert.NilError(t, err) - - id := uuid.NewV4() - - type args struct { - scan *database.Scan - } - tests := []struct { - name string - args args - want *models.Scan - wantErr bool - }{ - { - name: "sanity", - args: args{ - scan: &database.Scan{ - Base: database.Base{ - ID: id, - }, - ScanStartTime: nil, - ScanEndTime: nil, - ScanConfigID: utils.StringPtr("1"), - ScanConfigSnapshot: scanSnapB, - TargetIDs: targetIDsB, - }, - }, - want: &models.Scan{ - Id: utils.StringPtr(id.String()), - ScanConfig: &models.ScanConfigRelationship{ - Id: "1", - }, - ScanConfigSnapshot: &scanSnap, - TargetIDs: &targetIDs, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ConvertScan(tt.args.scan) - if (err != nil) != tt.wantErr { - t.Errorf("ConvertScan() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ConvertScan() got = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/backend/pkg/rest/convert/resttodb/params.go b/backend/pkg/rest/convert/resttodb/params.go deleted file mode 100644 index 6e4b7ac76..000000000 --- a/backend/pkg/rest/convert/resttodb/params.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright © 2022 Cisco Systems, Inc. and its affiliates. -// All rights reserved. -// -// 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 resttodb - -import ( - "github.com/openclarity/vmclarity/api/models" - "github.com/openclarity/vmclarity/backend/pkg/database" -) - -func ConvertGetTargetsParams(params models.GetTargetsParams) database.GetTargetsParams { - return database.GetTargetsParams{ - Filter: params.Filter, - Page: params.Page, - PageSize: params.PageSize, - } -} - -func ConvertGetScanResultsParams(params models.GetScanResultsParams) database.GetScanResultsParams { - return database.GetScanResultsParams{ - Filter: params.Filter, - Select: params.Select, - Page: params.Page, - PageSize: params.PageSize, - } -} - -func ConvertGetScanResultsScanResultIDParams(params models.GetScanResultsScanResultIDParams) database.GetScanResultsScanResultIDParams { - return database.GetScanResultsScanResultIDParams{ - Select: params.Select, - } -} - -func ConvertGetScansParams(params models.GetScansParams) database.GetScansParams { - return database.GetScansParams{ - Filter: params.Filter, - Page: params.Page, - PageSize: params.PageSize, - } -} - -func ConvertGetScanConfigsParams(params models.GetScanConfigsParams) database.GetScanConfigsParams { - return database.GetScanConfigsParams{ - Filter: params.Filter, - Page: params.Page, - PageSize: params.PageSize, - } -} diff --git a/backend/pkg/rest/scan_config_controller.go b/backend/pkg/rest/scan_config_controller.go index 2c31a6ea9..3efed708c 100644 --- a/backend/pkg/rest/scan_config_controller.go +++ b/backend/pkg/rest/scan_config_controller.go @@ -21,27 +21,30 @@ import ( "net/http" "github.com/labstack/echo/v4" - uuid "github.com/satori/go.uuid" - "gorm.io/gorm" "github.com/openclarity/vmclarity/api/models" "github.com/openclarity/vmclarity/backend/pkg/common" - "github.com/openclarity/vmclarity/backend/pkg/rest/convert/dbtorest" - "github.com/openclarity/vmclarity/backend/pkg/rest/convert/resttodb" + databaseTypes "github.com/openclarity/vmclarity/backend/pkg/database/types" "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" ) func (s *ServerImpl) GetScanConfigs(ctx echo.Context, params models.GetScanConfigsParams) error { - dbScanConfigs, total, err := s.dbHandler.ScanConfigsTable().GetScanConfigsAndTotal(resttodb.ConvertGetScanConfigsParams(params)) + scanConfigs, err := s.dbHandler.ScanConfigsTable().GetScanConfigs(params) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan configs from db: %v", err)) } + return sendResponse(ctx, http.StatusOK, scanConfigs) +} - converted, err := dbtorest.ConvertScanConfigs(dbScanConfigs, total) +func (s *ServerImpl) GetScanConfigsScanConfigID(ctx echo.Context, scanConfigID models.ScanConfigID, params models.GetScanConfigsScanConfigIDParams) error { + sc, err := s.dbHandler.ScanConfigsTable().GetScanConfig(scanConfigID, params) if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan configs: %v", err)) + if errors.Is(err, databaseTypes.ErrNotFound) { + return sendError(ctx, http.StatusNotFound, fmt.Sprintf("ScanConfig with ID %v not found", scanConfigID)) + } + return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan config from db. scanConfigID=%v: %v", scanConfigID, err)) } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, sc) } func (s *ServerImpl) PostScanConfigs(ctx echo.Context) error { @@ -51,32 +54,20 @@ func (s *ServerImpl) PostScanConfigs(ctx echo.Context) error { return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScanConfig(&scanConfig, "") - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan config: %v", err)) - } - createdScanConfig, err := s.dbHandler.ScanConfigsTable().CreateScanConfig(convertedDB) + createdScanConfig, err := s.dbHandler.ScanConfigsTable().CreateScanConfig(scanConfig) if err != nil { var conflictErr *common.ConflictError if errors.As(err, &conflictErr) { - convertedExist, err := dbtorest.ConvertScanConfig(createdScanConfig) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert existing scan config: %v", err)) - } existResponse := &models.ScanConfigExists{ Message: utils.StringPtr(conflictErr.Reason), - ScanConfig: convertedExist, + ScanConfig: &createdScanConfig, } return sendResponse(ctx, http.StatusConflict, existResponse) } return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to create scan config in db: %v", err)) } - converted, err := dbtorest.ConvertScanConfig(createdScanConfig) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan config: %v", err)) - } - return sendResponse(ctx, http.StatusCreated, converted) + return sendResponse(ctx, http.StatusCreated, createdScanConfig) } func (s *ServerImpl) DeleteScanConfigsScanConfigID(ctx echo.Context, scanConfigID models.ScanConfigID) error { @@ -84,14 +75,9 @@ func (s *ServerImpl) DeleteScanConfigsScanConfigID(ctx echo.Context, scanConfigI Message: utils.StringPtr(fmt.Sprintf("scan config %v deleted", scanConfigID)), } - scanConfigUUID, err := uuid.FromString(scanConfigID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scanConfigID %v to uuid: %v", scanConfigID, err)) - } - - if err := s.dbHandler.ScanConfigsTable().DeleteScanConfig(scanConfigUUID); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return sendError(ctx, http.StatusNotFound, err.Error()) + if err := s.dbHandler.ScanConfigsTable().DeleteScanConfig(scanConfigID); err != nil { + if errors.Is(err, databaseTypes.ErrNotFound) { + return sendError(ctx, http.StatusNotFound, fmt.Sprintf("ScanConfig with ID %v not found", scanConfigID)) } return sendError(ctx, http.StatusInternalServerError, err.Error()) } @@ -99,27 +85,6 @@ func (s *ServerImpl) DeleteScanConfigsScanConfigID(ctx echo.Context, scanConfigI return sendResponse(ctx, http.StatusNoContent, &success) } -func (s *ServerImpl) GetScanConfigsScanConfigID(ctx echo.Context, scanConfigID models.ScanConfigID) error { - scanConfigUUID, err := uuid.FromString(scanConfigID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scanConfigID %v to uuid: %v", scanConfigID, err)) - } - - sc, err := s.dbHandler.ScanConfigsTable().GetScanConfig(scanConfigUUID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return sendError(ctx, http.StatusNotFound, err.Error()) - } - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan config from db. scanConfigID=%v: %v", scanConfigID, err)) - } - - converted, err := dbtorest.ConvertScanConfig(sc) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan config. scanConfigID=%v: %v", scanConfigID, err)) - } - return sendResponse(ctx, http.StatusOK, converted) -} - func (s *ServerImpl) PatchScanConfigsScanConfigID(ctx echo.Context, scanConfigID models.ScanConfigID) error { var scanConfig models.ScanConfig err := ctx.Bind(&scanConfig) @@ -127,30 +92,18 @@ func (s *ServerImpl) PatchScanConfigsScanConfigID(ctx echo.Context, scanConfigID return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScanConfig(&scanConfig, scanConfigID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan config: %v", err)) - } - - // check that a scan config with that id exists. - _, err = s.dbHandler.ScanConfigsTable().GetScanConfig(convertedDB.ID) + // PATCH request might not contain the ID in the body, so set it from + // the URL field so that the DB layer knows which object is being updated. + scanConfig.Id = &scanConfigID + updatedScanConfig, err := s.dbHandler.ScanConfigsTable().UpdateScanConfig(scanConfig) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return sendError(ctx, http.StatusNotFound, fmt.Sprintf("scan config was not found. scanConfigID=%v: %v", scanConfigID, err)) + if errors.Is(err, databaseTypes.ErrNotFound) { + return sendError(ctx, http.StatusNotFound, fmt.Sprintf("ScanConfig with ID %v not found", scanConfigID)) } - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan config from db. scanConfigID=%v: %v", scanConfigID, err)) - } - - updatedScanConfig, err := s.dbHandler.ScanConfigsTable().UpdateScanConfig(convertedDB) - if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to update scan config in db. scanConfigID=%v: %v", scanConfigID, err)) } - converted, err := dbtorest.ConvertScanConfig(updatedScanConfig) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan config. scanConfigID=%v: %v", scanConfigID, err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, updatedScanConfig) } func (s *ServerImpl) PutScanConfigsScanConfigID(ctx echo.Context, scanConfigID models.ScanConfigID) error { @@ -160,28 +113,16 @@ func (s *ServerImpl) PutScanConfigsScanConfigID(ctx echo.Context, scanConfigID m return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScanConfig(&scanConfig, scanConfigID) + // PUT request might not contain the ID in the body, so set it from the + // URL field so that the DB layer knows which object is being updated. + scanConfig.Id = &scanConfigID + updatedScanConfig, err := s.dbHandler.ScanConfigsTable().SaveScanConfig(scanConfig) if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan config: %v", err)) - } - - // check that a scan config with that id exists. - _, err = s.dbHandler.ScanConfigsTable().GetScanConfig(convertedDB.ID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return sendError(ctx, http.StatusNotFound, fmt.Sprintf("scan config was not found. scanConfigID=%v: %v", scanConfigID, err)) + if errors.Is(err, databaseTypes.ErrNotFound) { + return sendError(ctx, http.StatusNotFound, fmt.Sprintf("ScanConfig with ID %v not found", scanConfigID)) } - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan config from db. scanConfigID=%v: %v", scanConfigID, err)) - } - - updatedScanConfig, err := s.dbHandler.ScanConfigsTable().SaveScanConfig(convertedDB) - if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to update scan config in db. scanConfigID=%v: %v", scanConfigID, err)) } - converted, err := dbtorest.ConvertScanConfig(updatedScanConfig) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan config. scanConfigID=%v: %v", scanConfigID, err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, updatedScanConfig) } diff --git a/backend/pkg/rest/scan_controller.go b/backend/pkg/rest/scan_controller.go index 00f68751e..3bb4cd8c9 100644 --- a/backend/pkg/rest/scan_controller.go +++ b/backend/pkg/rest/scan_controller.go @@ -21,27 +21,20 @@ import ( "net/http" "github.com/labstack/echo/v4" - uuid "github.com/satori/go.uuid" "gorm.io/gorm" "github.com/openclarity/vmclarity/api/models" "github.com/openclarity/vmclarity/backend/pkg/common" - "github.com/openclarity/vmclarity/backend/pkg/rest/convert/dbtorest" - "github.com/openclarity/vmclarity/backend/pkg/rest/convert/resttodb" "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" ) func (s *ServerImpl) GetScans(ctx echo.Context, params models.GetScansParams) error { - dbScans, total, err := s.dbHandler.ScansTable().GetScansAndTotal(resttodb.ConvertGetScansParams(params)) + scans, err := s.dbHandler.ScansTable().GetScans(params) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scans from db: %v", err)) } - converted, err := dbtorest.ConvertScans(dbScans, total) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scans: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, scans) } func (s *ServerImpl) PostScans(ctx echo.Context) error { @@ -51,32 +44,20 @@ func (s *ServerImpl) PostScans(ctx echo.Context) error { return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScan(&scan, "") - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan: %v", err)) - } - createdScan, err := s.dbHandler.ScansTable().CreateScan(convertedDB) + createdScan, err := s.dbHandler.ScansTable().CreateScan(scan) if err != nil { var conflictErr *common.ConflictError if errors.As(err, &conflictErr) { - convertedExist, err := dbtorest.ConvertScan(createdScan) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert existing scan: %v", err)) - } existResponse := &models.ScanExists{ Message: utils.StringPtr(conflictErr.Reason), - Scan: convertedExist, + Scan: &createdScan, } return sendResponse(ctx, http.StatusConflict, existResponse) } return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to create scan in db: %v", err)) } - converted, err := dbtorest.ConvertScan(createdScan) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan: %v", err)) - } - return sendResponse(ctx, http.StatusCreated, converted) + return sendResponse(ctx, http.StatusCreated, createdScan) } func (s *ServerImpl) DeleteScansScanID(ctx echo.Context, scanID models.ScanID) error { @@ -84,12 +65,7 @@ func (s *ServerImpl) DeleteScansScanID(ctx echo.Context, scanID models.ScanID) e Message: utils.StringPtr(fmt.Sprintf("scan %v deleted", scanID)), } - scanUUID, err := uuid.FromString(scanID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scanID %v to uuid: %v", scanID, err)) - } - - if err := s.dbHandler.ScansTable().DeleteScan(scanUUID); err != nil { + if err := s.dbHandler.ScansTable().DeleteScan(scanID); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, err.Error()) } @@ -100,24 +76,14 @@ func (s *ServerImpl) DeleteScansScanID(ctx echo.Context, scanID models.ScanID) e } func (s *ServerImpl) GetScansScanID(ctx echo.Context, scanID models.ScanID) error { - scanUUID, err := uuid.FromString(scanID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scanID %v to uuid: %v", scanID, err)) - } - - scan, err := s.dbHandler.ScansTable().GetScan(scanUUID) + scan, err := s.dbHandler.ScansTable().GetScan(scanID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, err.Error()) } return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan from db. id=%v: %v", scanID, err)) } - - converted, err := dbtorest.ConvertScan(scan) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, scan) } func (s *ServerImpl) PatchScansScanID(ctx echo.Context, scanID models.ScanID) error { @@ -127,13 +93,8 @@ func (s *ServerImpl) PatchScansScanID(ctx echo.Context, scanID models.ScanID) er return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScan(&scan, scanID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan: %v", err)) - } - // check that a scan with that id exists. - _, err = s.dbHandler.ScansTable().GetScan(convertedDB.ID) + _, err = s.dbHandler.ScansTable().GetScan(scanID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, fmt.Sprintf("scan was not found in db. scanID=%v: %v", scanID, err)) @@ -141,16 +102,12 @@ func (s *ServerImpl) PatchScansScanID(ctx echo.Context, scanID models.ScanID) er return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan from db. scanID=%v: %v", scanID, err)) } - updatedScan, err := s.dbHandler.ScansTable().UpdateScan(convertedDB) + updatedScan, err := s.dbHandler.ScansTable().UpdateScan(scan) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to update scan in db. scanID=%v: %v", scanID, err)) } - converted, err := dbtorest.ConvertScan(updatedScan) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, updatedScan) } func (s *ServerImpl) PutScansScanID(ctx echo.Context, scanID models.ScanID) error { @@ -160,13 +117,8 @@ func (s *ServerImpl) PutScansScanID(ctx echo.Context, scanID models.ScanID) erro return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScan(&scan, scanID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan: %v", err)) - } - // check that a scan with that id exists. - _, err = s.dbHandler.ScansTable().GetScan(convertedDB.ID) + _, err = s.dbHandler.ScansTable().GetScan(scanID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, fmt.Sprintf("scan was not found in db. scanID=%v: %v", scanID, err)) @@ -174,14 +126,10 @@ func (s *ServerImpl) PutScansScanID(ctx echo.Context, scanID models.ScanID) erro return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan from db. scanID=%v: %v", scanID, err)) } - updatedScan, err := s.dbHandler.ScansTable().SaveScan(convertedDB) + updatedScan, err := s.dbHandler.ScansTable().SaveScan(scan) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to update scan in db. scanID=%v: %v", scanID, err)) } - converted, err := dbtorest.ConvertScan(updatedScan) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, updatedScan) } diff --git a/backend/pkg/rest/scan_result_controller.go b/backend/pkg/rest/scan_result_controller.go index 79822a5d8..f62232cb6 100644 --- a/backend/pkg/rest/scan_result_controller.go +++ b/backend/pkg/rest/scan_result_controller.go @@ -21,28 +21,20 @@ import ( "net/http" "github.com/labstack/echo/v4" - uuid "github.com/satori/go.uuid" "gorm.io/gorm" "github.com/openclarity/vmclarity/api/models" "github.com/openclarity/vmclarity/backend/pkg/common" - "github.com/openclarity/vmclarity/backend/pkg/database" - "github.com/openclarity/vmclarity/backend/pkg/rest/convert/dbtorest" - "github.com/openclarity/vmclarity/backend/pkg/rest/convert/resttodb" "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" ) func (s *ServerImpl) GetScanResults(ctx echo.Context, params models.GetScanResultsParams) error { - dbScanResults, total, err := s.dbHandler.ScanResultsTable().GetScanResultsAndTotal(resttodb.ConvertGetScanResultsParams(params)) + dbScanResults, err := s.dbHandler.ScanResultsTable().GetScanResults(params) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scans results from db: %v", err)) } - converted, err := dbtorest.ConvertScanResults(dbScanResults, total) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan results: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, dbScanResults) } func (s *ServerImpl) PostScanResults(ctx echo.Context) error { @@ -52,52 +44,31 @@ func (s *ServerImpl) PostScanResults(ctx echo.Context) error { return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScanResult(&scanResult, "") - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan result: %v", err)) - } - createdScanResult, err := s.dbHandler.ScanResultsTable().CreateScanResult(convertedDB) + createdScanResult, err := s.dbHandler.ScanResultsTable().CreateScanResult(scanResult) if err != nil { var conflictErr *common.ConflictError if errors.As(err, &conflictErr) { - convertedExist, err := dbtorest.ConvertScanResult(createdScanResult) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert existing scan result: %v", err)) - } existResponse := &models.TargetScanResultExists{ Message: utils.StringPtr(conflictErr.Reason), - TargetScanResult: convertedExist, + TargetScanResult: &createdScanResult, } return sendResponse(ctx, http.StatusConflict, existResponse) } return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to create scan result in db: %v", err)) } - converted, err := dbtorest.ConvertScanResult(createdScanResult) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan result: %v", err)) - } - return sendResponse(ctx, http.StatusCreated, converted) + return sendResponse(ctx, http.StatusCreated, createdScanResult) } func (s *ServerImpl) GetScanResultsScanResultID(ctx echo.Context, scanResultID models.ScanResultID, params models.GetScanResultsScanResultIDParams) error { - scanResultUUID, err := uuid.FromString(scanResultID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scanResultID %v to uuid: %v", scanResultID, err)) - } - dbScanResult, err := s.dbHandler.ScanResultsTable().GetScanResult(scanResultUUID, resttodb.ConvertGetScanResultsScanResultIDParams(params)) + dbScanResult, err := s.dbHandler.ScanResultsTable().GetScanResult(scanResultID, params) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, err.Error()) } return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan result from db. scanResultID=%v: %v", scanResultID, err)) } - - converted, err := dbtorest.ConvertScanResult(dbScanResult) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan result: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, dbScanResult) } func (s *ServerImpl) PatchScanResultsScanResultID(ctx echo.Context, scanResultID models.ScanResultID) error { @@ -107,12 +78,8 @@ func (s *ServerImpl) PatchScanResultsScanResultID(ctx echo.Context, scanResultID return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScanResult(&scanResult, scanResultID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan result: %v", err)) - } // check that a scan result with that id exists. - _, err = s.dbHandler.ScanResultsTable().GetScanResult(convertedDB.ID, database.GetScanResultsScanResultIDParams{}) + _, err = s.dbHandler.ScanResultsTable().GetScanResult(scanResultID, models.GetScanResultsScanResultIDParams{}) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, fmt.Sprintf("scan result was not found. scanResultID=%v: %v", scanResultID, err)) @@ -120,16 +87,12 @@ func (s *ServerImpl) PatchScanResultsScanResultID(ctx echo.Context, scanResultID return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan result. scanResultID=%v: %v", scanResultID, err)) } - updatedScanResult, err := s.dbHandler.ScanResultsTable().UpdateScanResult(convertedDB) + updatedScanResult, err := s.dbHandler.ScanResultsTable().UpdateScanResult(scanResult) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to update scan result in db. scanResultID=%v: %v", scanResultID, err)) } - converted, err := dbtorest.ConvertScanResult(updatedScanResult) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan result: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, updatedScanResult) } func (s *ServerImpl) PutScanResultsScanResultID(ctx echo.Context, scanResultID models.ScanResultID) error { @@ -139,13 +102,8 @@ func (s *ServerImpl) PutScanResultsScanResultID(ctx echo.Context, scanResultID m return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertScanResult(&scanResult, scanResultID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan result: %v", err)) - } - // check that a scan result with that id exists. - _, err = s.dbHandler.ScanResultsTable().GetScanResult(convertedDB.ID, database.GetScanResultsScanResultIDParams{}) + _, err = s.dbHandler.ScanResultsTable().GetScanResult(scanResultID, models.GetScanResultsScanResultIDParams{}) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, fmt.Sprintf("scan result was not found. scanResultID=%v: %v", scanResultID, err)) @@ -153,14 +111,10 @@ func (s *ServerImpl) PutScanResultsScanResultID(ctx echo.Context, scanResultID m return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get scan result. scanResultID=%v: %v", scanResultID, err)) } - updatedScanResult, err := s.dbHandler.ScanResultsTable().SaveScanResult(convertedDB) + updatedScanResult, err := s.dbHandler.ScanResultsTable().SaveScanResult(scanResult) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to update scan result in db. scanResultID=%v: %v", scanResultID, err)) } - converted, err := dbtorest.ConvertScanResult(updatedScanResult) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert scan result: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, updatedScanResult) } diff --git a/backend/pkg/rest/server.go b/backend/pkg/rest/server.go index 274b123e2..5ffb0cb80 100644 --- a/backend/pkg/rest/server.go +++ b/backend/pkg/rest/server.go @@ -27,7 +27,7 @@ import ( "github.com/openclarity/vmclarity/api/server" "github.com/openclarity/vmclarity/backend/pkg/common" - "github.com/openclarity/vmclarity/backend/pkg/database" + databaseTypes "github.com/openclarity/vmclarity/backend/pkg/database/types" ) const ( @@ -36,7 +36,7 @@ const ( ) type ServerImpl struct { - dbHandler database.Database + dbHandler databaseTypes.Database } type Server struct { @@ -76,7 +76,7 @@ func createEchoServer() (*echo.Echo, error) { return e, nil } -func (s *Server) RegisterHandlers(dbHandler database.Database) { +func (s *Server) RegisterHandlers(dbHandler databaseTypes.Database) { serverImpl := &ServerImpl{ dbHandler: dbHandler, } diff --git a/backend/pkg/rest/target_controller.go b/backend/pkg/rest/target_controller.go index ca8548db6..3bbe7611e 100644 --- a/backend/pkg/rest/target_controller.go +++ b/backend/pkg/rest/target_controller.go @@ -21,27 +21,20 @@ import ( "net/http" "github.com/labstack/echo/v4" - uuid "github.com/satori/go.uuid" "gorm.io/gorm" "github.com/openclarity/vmclarity/api/models" "github.com/openclarity/vmclarity/backend/pkg/common" - "github.com/openclarity/vmclarity/backend/pkg/rest/convert/dbtorest" - "github.com/openclarity/vmclarity/backend/pkg/rest/convert/resttodb" "github.com/openclarity/vmclarity/runtime_scan/pkg/utils" ) func (s *ServerImpl) GetTargets(ctx echo.Context, params models.GetTargetsParams) error { - dbTargets, total, err := s.dbHandler.TargetsTable().GetTargetsAndTotal(resttodb.ConvertGetTargetsParams(params)) + dbTargets, err := s.dbHandler.TargetsTable().GetTargets(params) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get targets from db: %v", err)) } - converted, err := dbtorest.ConvertTargets(dbTargets, total) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert targets: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, dbTargets) } // nolint:cyclop @@ -52,41 +45,24 @@ func (s *ServerImpl) PostTargets(ctx echo.Context) error { return sendError(ctx, http.StatusBadRequest, fmt.Sprintf("failed to bind request: %v", err)) } - convertedDB, err := resttodb.ConvertTarget(&target, "") - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert target: %v", err)) - } - createdTarget, err := s.dbHandler.TargetsTable().CreateTarget(convertedDB) + createdTarget, err := s.dbHandler.TargetsTable().CreateTarget(target) if err != nil { var conflictErr *common.ConflictError if errors.As(err, &conflictErr) { - convertedExist, err := dbtorest.ConvertTarget(createdTarget) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert existing target: %v", err)) - } existResponse := &models.TargetExists{ Message: utils.StringPtr(conflictErr.Reason), - Target: convertedExist, + Target: &createdTarget, } return sendResponse(ctx, http.StatusConflict, existResponse) } return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to create target in db: %v", err)) } - converted, err := dbtorest.ConvertTarget(createdTarget) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert target: %v", err)) - } - return sendResponse(ctx, http.StatusCreated, converted) + return sendResponse(ctx, http.StatusCreated, createdTarget) } func (s *ServerImpl) GetTargetsTargetID(ctx echo.Context, targetID models.TargetID) error { - targetUUID, err := uuid.FromString(targetID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert targetID %v to uuid: %v", targetID, err)) - } - - target, err := s.dbHandler.TargetsTable().GetTarget(targetUUID) + target, err := s.dbHandler.TargetsTable().GetTarget(targetID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, err.Error()) @@ -94,11 +70,7 @@ func (s *ServerImpl) GetTargetsTargetID(ctx echo.Context, targetID models.Target return sendError(ctx, http.StatusInternalServerError, fmt.Errorf("failed to get target from db. targetID=%v: %v", targetID, err).Error()) } - converted, err := dbtorest.ConvertTarget(target) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert target: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, target) } func (s *ServerImpl) PutTargetsTargetID(ctx echo.Context, targetID models.TargetID) error { @@ -108,11 +80,7 @@ func (s *ServerImpl) PutTargetsTargetID(ctx echo.Context, targetID models.Target return sendError(ctx, http.StatusBadRequest, fmt.Errorf("failed to bind request: %v", err).Error()) } - convertedDB, err := resttodb.ConvertTarget(&target, targetID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert target: %v", err)) - } - _, err = s.dbHandler.TargetsTable().GetTarget(convertedDB.ID) + _, err = s.dbHandler.TargetsTable().GetTarget(targetID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, fmt.Sprintf("target was not found in db. targetID=%v", targetID)) @@ -120,28 +88,20 @@ func (s *ServerImpl) PutTargetsTargetID(ctx echo.Context, targetID models.Target return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to get target from db: %v", err)) } - updatedTarget, err := s.dbHandler.TargetsTable().SaveTarget(convertedDB) + updatedTarget, err := s.dbHandler.TargetsTable().SaveTarget(target) if err != nil { return sendError(ctx, http.StatusInternalServerError, fmt.Errorf("failed to update target in db. targetID=%v: %v", targetID, err).Error()) } - converted, err := dbtorest.ConvertTarget(updatedTarget) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert target: %v", err)) - } - return sendResponse(ctx, http.StatusOK, converted) + return sendResponse(ctx, http.StatusOK, updatedTarget) } func (s *ServerImpl) DeleteTargetsTargetID(ctx echo.Context, targetID models.TargetID) error { success := models.Success{ Message: utils.StringPtr(fmt.Sprintf("target %v deleted", targetID)), } - targetUUID, err := uuid.FromString(targetID) - if err != nil { - return sendError(ctx, http.StatusInternalServerError, fmt.Sprintf("failed to convert targetID %v to uuid: %v", targetID, err)) - } - if err := s.dbHandler.TargetsTable().DeleteTarget(targetUUID); err != nil { + if err := s.dbHandler.TargetsTable().DeleteTarget(targetID); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return sendError(ctx, http.StatusNotFound, err.Error()) } diff --git a/go.mod b/go.mod index 78413bf65..28f4e88d4 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/openclarity/vmclarity go 1.19 require ( + github.com/CiscoM31/godata v1.0.7 github.com/CycloneDX/cyclonedx-go v0.6.0 github.com/Masterminds/sprig/v3 v3.2.3 github.com/Portshift/go-utils v0.0.0-20220421083203-89265d8a6487 @@ -11,8 +12,10 @@ require ( github.com/aws/aws-sdk-go-v2/service/ec2 v1.63.1 github.com/cenkalti/backoff v2.2.1+incompatible github.com/deepmap/oapi-codegen v1.12.3 + github.com/evanphx/json-patch/v5 v5.6.0 github.com/ghodss/yaml v1.0.0 github.com/google/go-cmp v0.5.9 + github.com/google/uuid v1.3.0 github.com/labstack/echo/v4 v4.9.1 github.com/openclarity/kubeclarity/cli v0.0.0-00010101000000-000000000000 github.com/openclarity/kubeclarity/shared v0.0.0 @@ -26,8 +29,9 @@ require ( github.com/vulsio/go-exploitdb v0.4.2 golang.org/x/exp v0.0.0-20220823124025-807a23277127 gopkg.in/yaml.v3 v3.0.1 + gorm.io/datatypes v1.1.0 gorm.io/driver/sqlite v1.3.6 - gorm.io/gorm v1.23.5 + gorm.io/gorm v1.23.8 gotest.tools/v3 v3.4.0 k8s.io/mount-utils v0.26.2 ) @@ -192,6 +196,7 @@ require ( github.com/go-playground/validator/v10 v10.11.1 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-restruct/restruct v1.2.0-alpha // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/go-test/deep v1.0.8 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/googleapis v1.4.1 // indirect @@ -215,7 +220,6 @@ require ( github.com/google/licenseclassifier/v2 v2.0.0-pre6 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/trillian v1.5.0 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/google/wire v0.5.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect github.com/googleapis/gax-go/v2 v2.6.0 // indirect @@ -282,7 +286,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect - github.com/mattn/go-sqlite3 v1.14.12 // indirect + github.com/mattn/go-sqlite3 v1.14.15 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mholt/archiver/v3 v3.5.1 // indirect github.com/microsoft/go-rustaudit v0.0.0-20220808201409-204dfee52032 // indirect @@ -445,6 +449,7 @@ require ( gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gorm.io/driver/mysql v1.4.4 // indirect helm.sh/helm/v3 v3.10.3 // indirect k8s.io/api v0.25.3 // indirect k8s.io/apiextensions-apiserver v0.25.2 // indirect diff --git a/go.sum b/go.sum index 3efcdde2a..b8f0b8653 100644 --- a/go.sum +++ b/go.sum @@ -156,6 +156,8 @@ github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/CiscoM31/godata v1.0.7 h1:y3FHdICAU9j+IkK6E66ezCghaQSamFbYoj/YEHig0kY= +github.com/CiscoM31/godata v1.0.7/go.mod h1:ZMiT6JuD3Rm83HEtiTx4JEChsd25YCrxchKGag/sdTc= github.com/CycloneDX/cyclonedx-go v0.6.0 h1:SizWGbZzFTC/O/1yh072XQBMxfvsoWqd//oKCIyzFyE= github.com/CycloneDX/cyclonedx-go v0.6.0/go.mod h1:nQCiF4Tvrg5Ieu8qPhYMvzPGMu5I7fANZkrSsJjl5mg= github.com/CycloneDX/cyclonedx-gomod v1.2.0 h1:LgBL5TXwmluFqaum/8tXaPlwZvy1sSbBNKGV179E+Sk= @@ -821,6 +823,8 @@ github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQL github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= @@ -983,8 +987,9 @@ github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsX github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -1054,6 +1059,8 @@ github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= @@ -1373,8 +1380,10 @@ github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= @@ -1383,9 +1392,13 @@ github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5 github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g= +github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= @@ -1394,7 +1407,9 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1: github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.7/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= @@ -1403,6 +1418,7 @@ github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4 github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE= +github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= @@ -1410,6 +1426,7 @@ github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXg github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc= +github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= @@ -1648,8 +1665,9 @@ github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGw github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -1659,6 +1677,7 @@ github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88J github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= +github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= github.com/microsoft/go-rustaudit v0.0.0-20220808201409-204dfee52032 h1:TLygBUBxikNJJfLwgm+Qwdgq1FtfV8Uh7bcxRyTzK8s= github.com/microsoft/go-rustaudit v0.0.0-20220808201409-204dfee52032/go.mod h1:vYT9HE7WCvL64iVeZylKmCsWKfE+JZ8105iuh2Trk8g= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -3314,17 +3333,24 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.1.0 h1:EVp1Z28N4ACpYFK1nHboEIJGIFfjY7vLeieDk8jSHJA= +gorm.io/datatypes v1.1.0/go.mod h1:SH2K9R+2RMjuX1CkCONrPwoe9JzVv2hkQvEu4bXGojE= gorm.io/driver/mysql v1.1.0/go.mod h1:KdrTanmfLPPyAOeYGyG+UpDys7/7eeWT1zCq+oekYnU= +gorm.io/driver/mysql v1.4.4 h1:MX0K9Qvy0Na4o7qSC/YI7XxqUw5KDw01umqgID+svdQ= +gorm.io/driver/mysql v1.4.4/go.mod h1:BCg8cKI+R0j/rZRQxeKis/forqRwRSYOR8OM3Wo6hOM= gorm.io/driver/postgres v1.1.0/go.mod h1:hXQIwafeRjJvUm+OMxcFWyswJ/vevcpPLlGocwAwuqw= +gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc= gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= gorm.io/driver/sqlite v1.3.6 h1:Fi8xNYCUplOqWiPa3/GuCeowRNBRGTf62DEmhMDHeQQ= gorm.io/driver/sqlite v1.3.6/go.mod h1:Sg1/pvnKtbQ7jLXxfZa+jSHvoX8hoZA8cn4xllOMTgE= +gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= gorm.io/gorm v1.21.10/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= -gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM= gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.8 h1:h8sGJ+biDgBA1AD1Ha9gFCx7h8npU7AsLdlkX0n2TpE= +gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=