From 2d11a78008f7ebd6ffb1d5cfa8fdaa358b12b7f5 Mon Sep 17 00:00:00 2001 From: Agustin Groh Date: Mon, 5 Jan 2026 10:30:08 -0300 Subject: [PATCH 1/3] feat:SP-3882 Include Exploit Prediction Scoring System (EPSS) to vulnerability response --- CHANGELOG.md | 10 +- Makefile | 6 - go.mod | 2 +- go.sum | 4 +- pkg/dtos/vulnerability_output.go | 6 + pkg/models/epss.go | 55 ++++++ pkg/models/epss_test.go | 187 +++++++++++++++++++++ pkg/models/tests/epss.sql | 10 ++ pkg/service/vulnerability_service.go | 7 +- pkg/usecase/vulnerability_use_case.go | 65 +++++-- pkg/usecase/vulnerability_use_case_test.go | 5 +- 11 files changed, 333 insertions(+), 24 deletions(-) create mode 100644 pkg/models/epss.go create mode 100644 pkg/models/epss_test.go create mode 100644 pkg/models/tests/epss.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 3af08f1..20e5c37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Upcoming changes... +## [0.8.0] - 2026/01/05 +### Added +- Included Exploit Prediction Scoring System (EPSS) to vulnerability response +### Changed +- Upgraded `scanoss/papi` to v0.28.0 + ## [0.7.0] - 2025/11/13 ### Changed - Optimized query performance for retrieving vulnerabilities by PURL version using CTE (Common Table Expression) approach in `pkg/models/vulns_purl.go:111` @@ -81,4 +87,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [0.5.0]: https://github.com/scanoss/vulnerabilities/compare/v0.4.0...v0.5.0 [0.6.0]: https://github.com/scanoss/vulnerabilities/compare/v0.5.0...v0.6.0 [0.6.1]: https://github.com/scanoss/vulnerabilities/compare/v0.6.0...v0.6.1 -[0.6.2]: https://github.com/scanoss/vulnerabilities/compare/v0.6.1...v0.6.2 \ No newline at end of file +[0.6.2]: https://github.com/scanoss/vulnerabilities/compare/v0.6.1...v0.6.2 +[0.7.0]: https://github.com/scanoss/vulnerabilities/compare/v0.6.2...v0.7.0 +[0.8.0]: https://github.com/scanoss/vulnerabilities/compare/v0.7.0...v0.8.0 \ No newline at end of file diff --git a/Makefile b/Makefile index e3a1d0b..1a3624b 100644 --- a/Makefile +++ b/Makefile @@ -79,12 +79,6 @@ build_arm: version ## Build an ARM 64 binary go generate ./pkg/cmd/server.go GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-w -s" -o ./target/scanoss-vulnerabilities-api-linux-arm64 ./cmd/server -build_arm: version ## Build an ARM 64 binary - @echo "Building ARM binary $(VERSION)..." - go generate ./pkg/cmd/server.go - GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-w -s" -o ./target/scanoss-vulnerabilities-api-darwin-arm64 ./cmd/server - - package: package_amd ## Build & Package an AMD 64 binary package_amd: version ## Build & Package an AMD 64 binary diff --git a/go.mod b/go.mod index 8d931cb..55bb621 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/pandatix/go-cvss v0.6.2 github.com/scanoss/go-grpc-helper v0.9.0 github.com/scanoss/go-models v0.2.0 - github.com/scanoss/papi v0.17.0 + github.com/scanoss/papi v0.28.0 github.com/scanoss/zap-logging-helper v0.4.0 go.uber.org/zap v1.27.0 google.golang.org/grpc v1.75.0 diff --git a/go.sum b/go.sum index fa6cf91..e19f966 100644 --- a/go.sum +++ b/go.sum @@ -659,8 +659,8 @@ github.com/scanoss/go-purl-helper v0.2.1 h1:jp960a585ycyJSlqZky1NatMJBIQi/JGITDf github.com/scanoss/go-purl-helper v0.2.1/go.mod h1:v20/bKD8G+vGrILdiq6r0hyRD2bO8frCJlu9drEcQ38= github.com/scanoss/ipfilter/v2 v2.0.2 h1:GaB9i8kVJg9JQZm5XGStYkEpiaCVdsrj7ezI2wV/oh8= github.com/scanoss/ipfilter/v2 v2.0.2/go.mod h1:AwrpX4XGbZ7EKISMi1d6E5csBk1nWB8+ugpvXHFcTpA= -github.com/scanoss/papi v0.17.0 h1:YKS6hN1hXbE0yV9LwAridOreNLTPfrwFozUfbrAYv+Y= -github.com/scanoss/papi v0.17.0/go.mod h1:Z4E/4IpwYdzHHRJXTgBCGG1GjksgrFjNW5cvhbKUfeU= +github.com/scanoss/papi v0.28.0 h1:uvevFYoxwzvSH1hvgBoAkScIGTK2U1+rLzHSoJdnARk= +github.com/scanoss/papi v0.28.0/go.mod h1:Z4E/4IpwYdzHHRJXTgBCGG1GjksgrFjNW5cvhbKUfeU= github.com/scanoss/zap-logging-helper v0.4.0 h1:2qTYoaFa9+MlD2/1wmPtiDHfh+42NIEwgKVU3rPpl0Y= github.com/scanoss/zap-logging-helper v0.4.0/go.mod h1:9QuEZcq73g/0Izv1tWeOWukoIK0oTBzM4jSNQ5kRR1w= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= diff --git a/pkg/dtos/vulnerability_output.go b/pkg/dtos/vulnerability_output.go index 904212d..1b63658 100644 --- a/pkg/dtos/vulnerability_output.go +++ b/pkg/dtos/vulnerability_output.go @@ -51,6 +51,11 @@ type CVSS struct { CvssSeverity string `json:"cvss_severity"` } +type EPSS struct { + Percentile float32 `json:"percentile"` + Probability float32 `json:"probability"` +} + type VulnerabilitiesOutput struct { ID string `json:"id"` Cve string `json:"cve"` @@ -61,6 +66,7 @@ type VulnerabilitiesOutput struct { Modified u.OnlyDate `json:"modified"` Source string `json:"source"` Cvss []CVSS `json:"cvss"` + Epss EPSS `json:"epss"` } // ParseVulnerabilityOutput converts the input byte array to a VulnerabilityOutput structure. diff --git a/pkg/models/epss.go b/pkg/models/epss.go new file mode 100644 index 0000000..cacc47c --- /dev/null +++ b/pkg/models/epss.go @@ -0,0 +1,55 @@ +package models + +import ( + "context" + + "go.uber.org/zap" + + "scanoss.com/vulnerabilities/pkg/config" + + "github.com/jmoiron/sqlx" + + scdb "github.com/scanoss/go-grpc-helper/pkg/grpc/database" +) + +type EPSSModel struct { + db *sqlx.DB + config *config.ServerConfig + sc *scdb.DBQueryContext + s *zap.SugaredLogger +} + +type EPSS struct { + Cve string `db:"cve"` + Epss float32 `db:"epss"` + Percentile float32 `db:"percentile"` +} + +// NewEPSSModel creates a new instance of the EPSS Model. +func NewEPSSModel(s *zap.SugaredLogger, config *config.ServerConfig, db *sqlx.DB) *EPSSModel { + return &EPSSModel{ + db: db, + config: config, + sc: scdb.NewDBSelectContext(s, db, nil, config.App.Trace), + s: s, + } +} + +// GetEPSSByCVEs List of EPSS by CVEs. +func (m *EPSSModel) GetEPSSByCVEs(ctx context.Context, cves []string) ([]EPSS, error) { + if len(cves) == 0 { + return []EPSS{}, nil + } + var epss []EPSS + query, args, err := sqlx.In("SELECT cve, epss, percentile FROM epss_data WHERE cve IN (?)", cves) + if err != nil { + return nil, err + } + query = m.db.Rebind(query) + err = m.sc.SelectContext(ctx, &epss, query, args...) + if err != nil { + m.s.Errorf("Failed to get EPSS data for CVEs: %v", err) + return nil, err + } + return epss, nil +} diff --git a/pkg/models/epss_test.go b/pkg/models/epss_test.go new file mode 100644 index 0000000..84bc010 --- /dev/null +++ b/pkg/models/epss_test.go @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2018-2025 SCANOSS.COM + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package models + +import ( + "context" + + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + + "testing" + + myconfig "scanoss.com/vulnerabilities/pkg/config" + + zlog "github.com/scanoss/zap-logging-helper/pkg/logger" + + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" +) + +func TestGetEPSSByCVEs(t *testing.T) { + ctx := context.Background() + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + + s := ctxzap.Extract(ctx).Sugar() + + db, err := sqlx.Connect("sqlite3", ":memory:") + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer CloseDB(db) + + err = loadSQLData(db, nil, nil, "./tests/epss.sql") + if err != nil { + t.Fatalf("failed to load SQL test data: %v", err) + } + + serverConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + + epssModel := NewEPSSModel(s, serverConfig, db) + + // Test with multiple CVEs + cves := []string{"CVE-2017-9302", "CVE-2015-0269", "CVE-2018-10083"} + results, err := epssModel.GetEPSSByCVEs(ctx, cves) + if err != nil { + t.Errorf("GetEPSSByCVEs() error = %v", err) + } + if len(results) != 3 { + t.Errorf("GetEPSSByCVEs() expected 3 results, got %d", len(results)) + } +} + +func TestGetEPSSByCVEsEmpty(t *testing.T) { + ctx := context.Background() + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := ctxzap.Extract(ctx).Sugar() + + db, err := sqlx.Connect("sqlite3", ":memory:") + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer CloseDB(db) + + serverConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + + err = loadSQLData(db, nil, nil, "./tests/epss.sql") + if err != nil { + t.Fatalf("failed to load SQL test data: %v", err) + } + + epssModel := NewEPSSModel(s, serverConfig, db) + + // Test with empty slice + results, err := epssModel.GetEPSSByCVEs(ctx, []string{}) + if err != nil { + t.Errorf("GetEPSSByCVEs() with empty slice error = %v", err) + } + if len(results) != 0 { + t.Errorf("GetEPSSByCVEs() with empty slice expected 0 results, got %d", len(results)) + } +} + +func TestGetEPSSByCVEsNotFound(t *testing.T) { + ctx := context.Background() + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := ctxzap.Extract(ctx).Sugar() + + db, err := sqlx.Connect("sqlite3", ":memory:") + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer CloseDB(db) + + serverConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + + err = loadSQLData(db, nil, nil, "./tests/epss.sql") + if err != nil { + t.Fatalf("failed to load SQL test data: %v", err) + } + + epssModel := NewEPSSModel(s, serverConfig, db) + + // Test with non-existent CVEs + cves := []string{"CVE-NONEXISTENT-1", "CVE-NONEXISTENT-2"} + results, err := epssModel.GetEPSSByCVEs(ctx, cves) + if err != nil { + t.Errorf("GetEPSSByCVEs() error = %v", err) + } + if len(results) != 0 { + t.Errorf("GetEPSSByCVEs() with non-existent CVEs expected 0 results, got %d", len(results)) + } +} + +func TestGetEPSSByCVEsSingleCVE(t *testing.T) { + ctx := context.Background() + err := zlog.NewSugaredDevLogger() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) + } + defer zlog.SyncZap() + s := ctxzap.Extract(ctx).Sugar() + + db, err := sqlx.Connect("sqlite3", ":memory:") + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer CloseDB(db) + + serverConfig, err := myconfig.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + + err = loadSQLData(db, nil, nil, "./tests/epss.sql") + if err != nil { + t.Fatalf("failed to load SQL test data: %v", err) + } + + epssModel := NewEPSSModel(s, serverConfig, db) + + // Test with single CVE + cves := []string{"CVE-2018-10083"} + results, err := epssModel.GetEPSSByCVEs(ctx, cves) + if err != nil { + t.Errorf("GetEPSSByCVEs() error = %v", err) + } + if len(results) != 1 { + t.Errorf("GetEPSSByCVEs() expected 1 result, got %d", len(results)) + } + if len(results) > 0 && results[0].Cve != "CVE-2018-10083" { + t.Errorf("GetEPSSByCVEs() expected CVE-2018-10083, got %s", results[0].Cve) + } +} diff --git a/pkg/models/tests/epss.sql b/pkg/models/tests/epss.sql new file mode 100644 index 0000000..3a251c2 --- /dev/null +++ b/pkg/models/tests/epss.sql @@ -0,0 +1,10 @@ +DROP TABLE IF EXISTS epss_data; +CREATE TABLE "epss_data" ( + "cve" TEXT NOT NULL UNIQUE, + "epss" REAL NOT NULL, + "percentile" REAL NOT NULL, + PRIMARY KEY ("cve") +); +INSERT INTO epss_data (cve, epss, percentile) VALUES ('CVE-2017-9302', 0.00143, 0.5124); +INSERT INTO epss_data (cve, epss, percentile) VALUES ('CVE-2015-0269', 0.00285, 0.6832); +INSERT INTO epss_data (cve, epss, percentile) VALUES ('CVE-2018-10083', 0.00891, 0.8215); \ No newline at end of file diff --git a/pkg/service/vulnerability_service.go b/pkg/service/vulnerability_service.go index 44fe65d..53d9ffe 100644 --- a/pkg/service/vulnerability_service.go +++ b/pkg/service/vulnerability_service.go @@ -53,6 +53,7 @@ func (d vulnerabilityServer) Echo(ctx context.Context, request *common.EchoReque // //nolint:staticcheck // SA1019: keeping deprecated pb.VulnerabilityRequest and pb.VulnerabilityResponse for backward compatibility func (d vulnerabilityServer) GetVulnerabilities(ctx context.Context, request *pb.VulnerabilityRequest) (*pb.VulnerabilityResponse, error) { + s := ctxzap.Extract(ctx).Sugar() // Sanitize request components, invalidComponents, err := adapters.FromVulnerabilityRequestToComponentDTO(request) // Convert to internal DTO for processing if err != nil { @@ -67,7 +68,7 @@ func (d vulnerabilityServer) GetVulnerabilities(ctx context.Context, request *pb return &pb.VulnerabilityResponse{Status: &statusResp}, errors.New("no request data supplied") } - vulUseCase := usecase.NewVulnerabilityUseCase(ctx, d.db, d.config) + vulUseCase := usecase.NewVulnerabilityUseCase(s, d.config, d.db) vulnerabilities, err := vulUseCase.Execute(ctx, components) if err != nil { zlog.S.Errorf("Failed to get Vulnerabilities: %v", err) @@ -191,7 +192,7 @@ func (d vulnerabilityServer) GetComponentVulnerabilities(ctx context.Context, re errors.New("problem parsing Vulnerability input data") } - vulUseCase := usecase.NewVulnerabilityUseCase(ctx, d.db, d.config) + vulUseCase := usecase.NewVulnerabilityUseCase(s, d.config, d.db) vulnerabilities, err := vulUseCase.Execute(ctx, []dtos.ComponentDTO{componentDTO}) if err != nil { @@ -222,7 +223,7 @@ func (d vulnerabilityServer) GetComponentsVulnerabilities(ctx context.Context, r errors.New("problem parsing Vulnerability input data") } - vulUseCase := usecase.NewVulnerabilityUseCase(ctx, d.db, d.config) + vulUseCase := usecase.NewVulnerabilityUseCase(s, d.config, d.db) vulnerabilities, err := vulUseCase.Execute(ctx, componentDTOs) if err != nil { diff --git a/pkg/usecase/vulnerability_use_case.go b/pkg/usecase/vulnerability_use_case.go index b7d729e..30688c5 100644 --- a/pkg/usecase/vulnerability_use_case.go +++ b/pkg/usecase/vulnerability_use_case.go @@ -23,9 +23,8 @@ import ( "github.com/jmoiron/sqlx" "github.com/scanoss/go-models/pkg/scanoss" "github.com/scanoss/go-models/pkg/types" - zlog "github.com/scanoss/zap-logging-helper/pkg/logger" - - myconfig "scanoss.com/vulnerabilities/pkg/config" + "go.uber.org/zap" + "scanoss.com/vulnerabilities/pkg/config" "scanoss.com/vulnerabilities/pkg/dtos" "scanoss.com/vulnerabilities/pkg/helpers" "scanoss.com/vulnerabilities/pkg/models" @@ -37,26 +36,28 @@ type IVulnerabilityUseCase interface { type VulnerabilityUseCase struct { db *sqlx.DB - config *myconfig.ServerConfig + config *config.ServerConfig + s *zap.SugaredLogger } -func NewVulnerabilityUseCase(ctx context.Context, db *sqlx.DB, config *myconfig.ServerConfig) *VulnerabilityUseCase { +func NewVulnerabilityUseCase(s *zap.SugaredLogger, config *config.ServerConfig, db *sqlx.DB) *VulnerabilityUseCase { return &VulnerabilityUseCase{ db: db, config: config, + s: s, } } func (us VulnerabilityUseCase) Execute(ctx context.Context, components []dtos.ComponentDTO) (dtos.VulnerabilityOutput, error) { - zlog.S.Infof("Processing Vulnerabilities request: %v", components) + us.s.Infof("Processing Vulnerabilities request: %v", components) if len(components) == 0 { return dtos.VulnerabilityOutput{}, errors.New("no request data supplied") } - zlog.S.Infof("Getting DB Connection from pool: %v", ctx) + us.s.Infof("Getting DB Connection from pool: %v", ctx) conn, err := us.db.Connx(ctx) // Get a connection from the pool if err != nil { - zlog.S.Errorf("Failed to get a database connection from the pool: %v", err) + us.s.Errorf("Failed to get a database connection from the pool: %v", err) return dtos.VulnerabilityOutput{}, errors.New("problem getting database pool connection") } defer models.CloseConn(conn) @@ -70,7 +71,7 @@ func (us VulnerabilityUseCase) Execute(ctx context.Context, components []dtos.Co Requirement: c.Requirement, }) if err != nil { - zlog.S.Warnf("Failed to get component: %s, %s", c.Purl, c.Requirement) + us.s.Warnf("Failed to get component: %s, %s", c.Purl, c.Requirement) continue } if component.Version != "" { @@ -81,8 +82,13 @@ func (us VulnerabilityUseCase) Execute(ctx context.Context, components []dtos.Co // Gets OSV vulnerabilities only if enabled var osvVulnerabilities = dtos.VulnerabilityOutput{} if us.config.Source.OSV.Enabled { +<<<<<<< Updated upstream zlog.S.Infof("vulnerabilities: OSV enabled") osvUseCase := NewOSVUseCase(us.config.Source.OSV.APIBaseURL, us.config.Source.OSV.InfoBaseURL) +======= + us.s.Infof("vulnerabilities: OSV enabled") + osvUseCase := NewOSVUseCase(us.config) +>>>>>>> Stashed changes osvVulnerabilities = osvUseCase.Execute(components) } // ************* OSV Use case end *************** / @@ -92,14 +98,53 @@ func (us VulnerabilityUseCase) Execute(ctx context.Context, components []dtos.Co localVulUc := NewLocalVulnerabilitiesUseCase(ctx, conn, us.config) localVulnerabilities, err = localVulUc.GetVulnerabilities(components) if err != nil { - zlog.S.Errorf("Failed to get Vulnerabilities: %v", err) + us.s.Errorf("Failed to get Vulnerabilities: %v", err) return dtos.VulnerabilityOutput{}, errors.New("problems encountered extracting vulnerability data") } } + // Merge OSV and local vulnerabilities in one response. Avoids duplicated vulnerabilities := helpers.MergeOSVAndLocalVulnerabilities(localVulnerabilities, osvVulnerabilities) + + // Add EPSS data + us.enrichWithEPSS(ctx, &vulnerabilities) return vulnerabilities, nil } +// enrichWithEPSS fetches EPSS data for all CVEs and adds it to the vulnerabilities. +func (us VulnerabilityUseCase) enrichWithEPSS(ctx context.Context, vulnerabilities *dtos.VulnerabilityOutput) { + var cves []string + for _, c := range vulnerabilities.Components { + for _, v := range c.Vulnerabilities { + if v.Cve != "" { + cves = append(cves, v.Cve) + } + } + } + if len(cves) == 0 { + return + } + EPSSModel := models.NewEPSSModel(us.s, us.config, us.db) + epss, err := EPSSModel.GetEPSSByCVEs(ctx, cves) + if err != nil { + us.s.Errorf("Failed to get EPSS data for CVEs: %v", err) + return + } + + epssMap := make(map[string]models.EPSS, len(epss)) + for _, e := range epss { + epssMap[e.Cve] = e + } + + for i := range vulnerabilities.Components { + for j := range vulnerabilities.Components[i].Vulnerabilities { + if val, ok := epssMap[vulnerabilities.Components[i].Vulnerabilities[j].Cve]; ok { + vulnerabilities.Components[i].Vulnerabilities[j].Epss.Probability = val.Epss + vulnerabilities.Components[i].Vulnerabilities[j].Epss.Percentile = val.Percentile + } + } + } +} + // Optional: Type assertion to ensure VulnerabilityUseCase implements IVulnerabilityUseCase. var _ IVulnerabilityUseCase = (*VulnerabilityUseCase)(nil) diff --git a/pkg/usecase/vulnerability_use_case_test.go b/pkg/usecase/vulnerability_use_case_test.go index 0f8c0b8..39fdc81 100644 --- a/pkg/usecase/vulnerability_use_case_test.go +++ b/pkg/usecase/vulnerability_use_case_test.go @@ -21,6 +21,8 @@ import ( "fmt" "testing" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "scanoss.com/vulnerabilities/pkg/dtos" myconfig "scanoss.com/vulnerabilities/pkg/config" @@ -38,6 +40,7 @@ func TestVulnerabilityUseCase(t *testing.T) { t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) } defer zlog.SyncZap() + s := ctxzap.Extract(ctx).Sugar() db, err := sqlx.Connect("sqlite3", "file::memory:?cache=shared&uri=true") if err != nil { t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) @@ -57,7 +60,7 @@ func TestVulnerabilityUseCase(t *testing.T) { t.Fatalf("an error '%s' was not expected when loading server config", err) } serverConfig.Source.OSV.Enabled = false - vulnUseCase := NewVulnerabilityUseCase(ctx, db, serverConfig) + vulnUseCase := NewVulnerabilityUseCase(s, serverConfig, db) tests := []struct { name string From 10ec0ddaf1963d8758ab1540247626d657b9efea Mon Sep 17 00:00:00 2001 From: Agustin Groh Date: Mon, 5 Jan 2026 15:49:26 -0300 Subject: [PATCH 2/3] chore:SP-3887 refactor OSV usecase --- CHANGELOG.md | 1 + pkg/config/server_config.go | 2 + pkg/usecase/OSV_use_case.go | 176 ++++++++++++--------- pkg/usecase/OSV_use_case_test.go | 17 +- pkg/usecase/vulnerability_use_case.go | 18 +-- pkg/usecase/vulnerability_use_case_test.go | 2 +- 6 files changed, 118 insertions(+), 98 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20e5c37..7f8eab1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Included Exploit Prediction Scoring System (EPSS) to vulnerability response ### Changed +- Refactored OSV use case - Upgraded `scanoss/papi` to v0.28.0 ## [0.7.0] - 2025/11/13 diff --git a/pkg/config/server_config.go b/pkg/config/server_config.go index 5839319..15bbcf6 100644 --- a/pkg/config/server_config.go +++ b/pkg/config/server_config.go @@ -76,6 +76,7 @@ type ServerConfig struct { APIBaseURL string `env:"VULN_OSV_API_BASE_URL"` InfoBaseURL string `env:"VULN_OSV_INFO_BASE_URL"` Enabled bool `env:"VULN_OSV_SOURCE_ENABLED"` + APIWorkers int `env:"VULN_OSV_API_WORKERS"` } SCANOSS struct { Enabled bool `env:"VULN_SCANOSS_SOURCE_ENABLED"` @@ -121,6 +122,7 @@ func setServerConfigDefaults(cfg *ServerConfig) { cfg.Source.OSV.APIBaseURL = "https://api.osv.dev/v1" cfg.Source.OSV.InfoBaseURL = "https://osv.dev/vulnerability" cfg.Source.OSV.Enabled = true + cfg.Source.OSV.APIWorkers = 5 cfg.Source.SCANOSS.Enabled = true } diff --git a/pkg/usecase/OSV_use_case.go b/pkg/usecase/OSV_use_case.go index aa9e5db..4cc90a7 100644 --- a/pkg/usecase/OSV_use_case.go +++ b/pkg/usecase/OSV_use_case.go @@ -20,11 +20,13 @@ import ( "bytes" "context" "encoding/json" - "io" "net/http" - "sync" "time" + "go.uber.org/zap" + + "scanoss.com/vulnerabilities/pkg/config" + "scanoss.com/vulnerabilities/pkg/dtos" "scanoss.com/vulnerabilities/pkg/utils" @@ -45,18 +47,20 @@ type OSVRequest struct { type OSVUseCase struct { OSVAPIBaseURL string OSVInfoBaseURL string - semaphore chan struct{} // Used to limit concurrent requests - client *http.Client // Single shared + client *http.Client // Single shared + MaxAPIWorkers int + s *zap.SugaredLogger } -func NewOSVUseCase(osvAPIBaseURL string, osvInfoBaseURL string) *OSVUseCase { +func NewOSVUseCase(s *zap.SugaredLogger, config *config.ServerConfig) *OSVUseCase { return &OSVUseCase{ - OSVAPIBaseURL: osvAPIBaseURL, - OSVInfoBaseURL: osvInfoBaseURL, - semaphore: make(chan struct{}, 4), + OSVAPIBaseURL: config.Source.OSV.APIBaseURL, + OSVInfoBaseURL: config.Source.OSV.InfoBaseURL, client: &http.Client{ - Timeout: 10 * time.Second, + Timeout: 15 * time.Second, }, + MaxAPIWorkers: config.Source.OSV.APIWorkers, + s: s, } } @@ -83,90 +87,104 @@ func (us OSVUseCase) Execute(dto []dtos.ComponentDTO) dtos.VulnerabilityOutput { } func (us OSVUseCase) processRequests(requests []OSVRequest) dtos.VulnerabilityOutput { - var wg sync.WaitGroup - ctx, cancel := context.WithCancel(context.Background()) - results := make(chan dtos.VulnerabilityComponentOutput, len(requests)) + numJobs := len(requests) + jobs := make(chan OSVRequest, numJobs) + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() - for _, request := range requests { - wg.Add(1) - go func(req OSVRequest) { - defer wg.Done() - // Try to acquire semaphore - us.semaphore <- struct{}{} // Will block if 4 requests are already running - defer func() { <-us.semaphore }() // Release when done - - select { - case <-ctx.Done(): - return - default: - r, _ := us.processRequest(req) - results <- r - } - }(request) + results := make(chan dtos.VulnerabilityComponentOutput, numJobs) + workers := min(us.MaxAPIWorkers, numJobs) + for i := 0; i < workers; i++ { + go us.processRequest(ctx, jobs, results) } - wg.Wait() - close(results) + + for _, r := range requests { + jobs <- r + } + close(jobs) // Collect all results into a slice var response = dtos.VulnerabilityOutput{ Components: []dtos.VulnerabilityComponentOutput{}, } - for result := range results { + for i := 0; i < numJobs; i++ { + result := <-results response.Components = append(response.Components, result) } - return response } -func (us OSVUseCase) processRequest(osvRequest OSVRequest) (dtos.VulnerabilityComponentOutput, error) { - out, err := json.Marshal(osvRequest) - if err != nil { - zlog.S.Errorf("Failed to marshal request: %s", err) - return dtos.VulnerabilityComponentOutput{}, err - } - - req, err := http.NewRequest(http.MethodPost, us.OSVAPIBaseURL+"/query", bytes.NewBuffer(out)) - if err != nil { - zlog.S.Errorf("Failed to create HTTP request: %s", err) - return dtos.VulnerabilityComponentOutput{}, err - } - req.Header.Set("Content-Type", "application/json") - - // Use a shared HTTP client to avoid creating a new one every call - resp, err := us.client.Do(req) - if err != nil { - zlog.S.Errorf("HTTP request failed: %s", err) - return dtos.VulnerabilityComponentOutput{}, err - } - - defer func(Body io.ReadCloser) { - err = Body.Close() - if err != nil { - zlog.S.Errorf("Failed to close HTTP response body: %s", err) +// processRequest is a worker function that processes OSV vulnerability requests concurrently. +// It reads requests from the jobs channel, queries the OSV API for each request, and sends +// the results to the results channel. The worker terminates when the jobs channel is closed +// or when the context is cancelled. +func (us OSVUseCase) processRequest(ctx context.Context, jobs chan OSVRequest, results chan dtos.VulnerabilityComponentOutput) { + for { + select { + case j, ok := <-jobs: + if !ok { + return // Channel closed, stop worker + } + response := dtos.VulnerabilityComponentOutput{ + Purl: j.Package.Purl, + Requirement: j.Requirement, + Version: j.Version, + } + out, err := json.Marshal(struct { + Version string `json:"version,omitempty"` + Package OSVPackageRequest `json:"package"` + }{ + Version: j.Version, + Package: j.Package, + }) + if err != nil { + us.s.Errorf("Failed to marshal request: %s", err) + results <- response + continue + } + req, err := http.NewRequest(http.MethodPost, us.OSVAPIBaseURL+"/query", bytes.NewBuffer(out)) + if err != nil { + us.s.Errorf("Failed to create HTTP request: %s", err) + results <- response + continue + } + req.Header.Set("Content-Type", "application/json") + + // Use a shared HTTP client to avoid creating a new one every call + resp, err := us.client.Do(req) + if err != nil { + us.s.Errorf("HTTP request failed: %s", err) + results <- response + continue + } + // Check for non-200 HTTP responses + if resp.StatusCode != http.StatusOK { + us.s.Errorf("Unexpected HTTP status: %d", resp.StatusCode) + err = resp.Body.Close() + if err != nil { + us.s.Errorf("Failed to close response body: %s", err) + } + results <- response + continue + } + var OSVResponse dtos.OSVResponseDTO + err = json.NewDecoder(resp.Body).Decode(&OSVResponse) + if err != nil { + us.s.Errorf("Failed to decode response: %s", err) + results <- response + continue + } + err = resp.Body.Close() + if err != nil { + us.s.Errorf("Failed to close response body: %s", err) + } + response.Vulnerabilities = us.mapOSVVulnerabilities(OSVResponse.Vulns) + results <- response + case <-ctx.Done(): + // Cancellation signal received: stop working and return immediately + us.s.Debugf("Worker: Cancellation signal received, stopping.") + return } - }(resp.Body) - - // Check for non-200 HTTP responses - if resp.StatusCode != http.StatusOK { - zlog.S.Errorf("Unexpected HTTP status: %d", resp.StatusCode) - return dtos.VulnerabilityComponentOutput{}, err - } - - var OSVResponse dtos.OSVResponseDTO - err = json.NewDecoder(resp.Body).Decode(&OSVResponse) - if err != nil { - // Handle error - zlog.S.Errorf("Failed to decode response: %s", err) - return dtos.VulnerabilityComponentOutput{}, err - } - - response := dtos.VulnerabilityComponentOutput{ - Purl: osvRequest.Package.Purl, - Requirement: osvRequest.Requirement, - Version: osvRequest.Version, - Vulnerabilities: us.mapOSVVulnerabilities(OSVResponse.Vulns), } - return response, nil } // mapOSVVulnerabilities converts OSV vulnerabilities to the required DTO structure. diff --git a/pkg/usecase/OSV_use_case_test.go b/pkg/usecase/OSV_use_case_test.go index 5ef9e94..fde298d 100644 --- a/pkg/usecase/OSV_use_case_test.go +++ b/pkg/usecase/OSV_use_case_test.go @@ -17,10 +17,13 @@ package usecase import ( + "context" "testing" - zlog "github.com/scanoss/zap-logging-helper/pkg/logger" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + zlog "github.com/scanoss/zap-logging-helper/pkg/logger" + "scanoss.com/vulnerabilities/pkg/config" "scanoss.com/vulnerabilities/pkg/dtos" ) @@ -30,6 +33,14 @@ func TestOSVUseCase(t *testing.T) { t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) } defer zlog.SyncZap() + ctx := ctxzap.ToContext(context.Background(), zlog.L) + s := ctxzap.Extract(ctx).Sugar() + + serverConfig, err := config.NewServerConfig(nil) + if err != nil { + t.Fatalf("failed to load Config: %v", err) + } + testCases := []struct { name string input []dtos.ComponentDTO @@ -47,9 +58,7 @@ func TestOSVUseCase(t *testing.T) { }, }, } - OSVBaseURL := "https://api.osv.dev/v1" - OSVInfoBaseURL := "https://test.osv.dev/vulnerability" - OSVUseCase := NewOSVUseCase(OSVBaseURL, OSVInfoBaseURL) + OSVUseCase := NewOSVUseCase(s, serverConfig) for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { r := OSVUseCase.Execute(tc.input) diff --git a/pkg/usecase/vulnerability_use_case.go b/pkg/usecase/vulnerability_use_case.go index 30688c5..e06cf73 100644 --- a/pkg/usecase/vulnerability_use_case.go +++ b/pkg/usecase/vulnerability_use_case.go @@ -49,12 +49,11 @@ func NewVulnerabilityUseCase(s *zap.SugaredLogger, config *config.ServerConfig, } func (us VulnerabilityUseCase) Execute(ctx context.Context, components []dtos.ComponentDTO) (dtos.VulnerabilityOutput, error) { - us.s.Infof("Processing Vulnerabilities request: %v", components) + us.s.Debugf("Processing Vulnerabilities request: %v", components) if len(components) == 0 { return dtos.VulnerabilityOutput{}, errors.New("no request data supplied") } - - us.s.Infof("Getting DB Connection from pool: %v", ctx) + us.s.Debugf("Getting DB Connection from pool: %v", ctx) conn, err := us.db.Connx(ctx) // Get a connection from the pool if err != nil { us.s.Errorf("Failed to get a database connection from the pool: %v", err) @@ -82,13 +81,8 @@ func (us VulnerabilityUseCase) Execute(ctx context.Context, components []dtos.Co // Gets OSV vulnerabilities only if enabled var osvVulnerabilities = dtos.VulnerabilityOutput{} if us.config.Source.OSV.Enabled { -<<<<<<< Updated upstream - zlog.S.Infof("vulnerabilities: OSV enabled") - osvUseCase := NewOSVUseCase(us.config.Source.OSV.APIBaseURL, us.config.Source.OSV.InfoBaseURL) -======= - us.s.Infof("vulnerabilities: OSV enabled") - osvUseCase := NewOSVUseCase(us.config) ->>>>>>> Stashed changes + us.s.Debugf("vulnerabilities: OSV enabled") + osvUseCase := NewOSVUseCase(us.s, us.config) osvVulnerabilities = osvUseCase.Execute(components) } // ************* OSV Use case end *************** / @@ -102,10 +96,8 @@ func (us VulnerabilityUseCase) Execute(ctx context.Context, components []dtos.Co return dtos.VulnerabilityOutput{}, errors.New("problems encountered extracting vulnerability data") } } - // Merge OSV and local vulnerabilities in one response. Avoids duplicated vulnerabilities := helpers.MergeOSVAndLocalVulnerabilities(localVulnerabilities, osvVulnerabilities) - // Add EPSS data us.enrichWithEPSS(ctx, &vulnerabilities) return vulnerabilities, nil @@ -130,12 +122,10 @@ func (us VulnerabilityUseCase) enrichWithEPSS(ctx context.Context, vulnerabiliti us.s.Errorf("Failed to get EPSS data for CVEs: %v", err) return } - epssMap := make(map[string]models.EPSS, len(epss)) for _, e := range epss { epssMap[e.Cve] = e } - for i := range vulnerabilities.Components { for j := range vulnerabilities.Components[i].Vulnerabilities { if val, ok := epssMap[vulnerabilities.Components[i].Vulnerabilities[j].Cve]; ok { diff --git a/pkg/usecase/vulnerability_use_case_test.go b/pkg/usecase/vulnerability_use_case_test.go index 39fdc81..db3d59b 100644 --- a/pkg/usecase/vulnerability_use_case_test.go +++ b/pkg/usecase/vulnerability_use_case_test.go @@ -34,12 +34,12 @@ import ( ) func TestVulnerabilityUseCase(t *testing.T) { - ctx := context.Background() err := zlog.NewSugaredDevLogger() if err != nil { t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) } defer zlog.SyncZap() + ctx := ctxzap.ToContext(context.Background(), zlog.L) s := ctxzap.Extract(ctx).Sugar() db, err := sqlx.Connect("sqlite3", "file::memory:?cache=shared&uri=true") if err != nil { From 0326017bfd310ca08f1b3708d81d6750ad66fe82 Mon Sep 17 00:00:00 2001 From: Agustin Groh Date: Tue, 6 Jan 2026 09:36:25 -0300 Subject: [PATCH 3/3] chore: remove unused code --- CHANGELOG.md | 2 +- pkg/adapters/vulnerability_support.go | 3 +- pkg/models/all_urls.go | 252 -------------- pkg/models/all_urls_test.go | 422 ----------------------- pkg/models/cpe_purl.go | 5 +- pkg/models/golang_projects.go | 341 ------------------- pkg/models/golang_projects_test.go | 469 -------------------------- pkg/models/vulns_purl.go | 3 +- pkg/utils/purl.go | 116 +------ pkg/utils/purl_test.go | 223 ------------ 10 files changed, 9 insertions(+), 1827 deletions(-) delete mode 100644 pkg/models/all_urls.go delete mode 100644 pkg/models/all_urls_test.go delete mode 100644 pkg/models/golang_projects.go delete mode 100644 pkg/models/golang_projects_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f8eab1..771d312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Upcoming changes... -## [0.8.0] - 2026/01/05 +## [0.8.0] - 2026/01/07 ### Added - Included Exploit Prediction Scoring System (EPSS) to vulnerability response ### Changed diff --git a/pkg/adapters/vulnerability_support.go b/pkg/adapters/vulnerability_support.go index a3d8139..8c67ede 100644 --- a/pkg/adapters/vulnerability_support.go +++ b/pkg/adapters/vulnerability_support.go @@ -22,6 +22,7 @@ import ( "fmt" "strings" + purlhelper "github.com/scanoss/go-purl-helper/pkg" common "github.com/scanoss/papi/api/commonv2" pb "github.com/scanoss/papi/api/vulnerabilitiesv2" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" @@ -33,7 +34,7 @@ func sanitizeComponents(components []dtos.ComponentDTO) ([]dtos.ComponentDTO, [] var sanitized []dtos.ComponentDTO var invalid []dtos.ComponentDTO for _, component := range components { - _, err := utils.PurlFromString(component.Purl) + _, err := purlhelper.PurlFromString(component.Purl) if err != nil { invalid = append(invalid, component) continue diff --git a/pkg/models/all_urls.go b/pkg/models/all_urls.go deleted file mode 100644 index 95d0aa6..0000000 --- a/pkg/models/all_urls.go +++ /dev/null @@ -1,252 +0,0 @@ -// Package models SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2018-2025 SCANOSS.COM - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package models - -import ( - "context" - "errors" - "fmt" - "sort" - "strings" - - "github.com/Masterminds/semver/v3" - "github.com/jmoiron/sqlx" - "go.uber.org/zap" - - "scanoss.com/vulnerabilities/pkg/utils" -) - -type AllUrlsModel struct { - ctx context.Context - s *zap.SugaredLogger - conn *sqlx.Conn - project *ProjectModel - golangProj *GolangProjects -} - -type AllURL struct { - Component string `db:"component"` - Version string `db:"version"` - SemVer string `db:"semver"` - License string `db:"license"` - LicenseID string `db:"license_id"` - IsSpdx bool `db:"is_spdx"` - PurlName string `db:"purl_name"` - MineID int32 `db:"mine_id"` - URL string `db:"-"` -} - -// SQL Query constants. -const ( - purlSQLQuerySelect = "SELECT component, v.version_name AS version, v.semver AS semver," - licSpdxSQLQuery = " l.license_name AS license, l.spdx_id AS license_id, l.is_spdx AS is_spdx," - verLeftJoinSQL = " LEFT JOIN versions v ON u.version_id = v.id" - licLeftJoinSQL = " LEFT JOIN licenses l ON u.license_id = l.id" - mineLeftJoinSQL = " LEFT JOIN mines m ON u.mine_id = m.id" -) - -// NewAllURLModel creates a new instance of the 'All URL' Model. -func NewAllURLModel(ctx context.Context, s *zap.SugaredLogger, conn *sqlx.Conn, project *ProjectModel, golangProj *GolangProjects) *AllUrlsModel { - return &AllUrlsModel{ctx: ctx, s: s, conn: conn, project: project, golangProj: golangProj} -} - -// GetURLsByPurlString searches for component details of the specified Purl string (and optional requirement). -func (m *AllUrlsModel) GetURLsByPurlString(purlString, purlReq string) (AllURL, error) { - if len(purlString) == 0 { - m.s.Error("Please specify a valid Purl String to query") - return AllURL{}, errors.New("please specify a valid Purl String to query") - } - purl, err := utils.PurlFromString(purlString) - if err != nil { - return AllURL{}, err - } - purlName, err := utils.PurlNameFromString(purlString) // Make sure we just have the bare minimum for a Purl Name - if err != nil { - return AllURL{}, err - } - // TODO check what to do if we get a "file" requirement - if len(purlReq) > 0 && strings.HasPrefix(purlReq, "file:") { // internal dependency requirement. Assume latest - m.s.Debugf("Removing 'local' requirement for purl: %v (req: %v)", purlString, purlReq) - purlReq = "" - } - if len(purl.Version) == 0 && len(purlReq) > 0 { // No version specified, but we might have a specific version in the Requirement - ver := utils.GetVersionFromReq(purlReq) - if len(ver) > 0 { - purl.Version = ver // Switch to exact version search (faster) - purlReq = "" - } - } - if purl.Type == "golang" { - allURL, err := m.golangProj.GetGoLangURLByPurl(purl, purlName, purlReq) // Search a separate table for golang dependencies - // If no golang package/license is found, but it's a GitHub component, search GitHub for it - if err == nil && (len(allURL.Component) == 0 || len(allURL.License) == 0) && strings.HasPrefix(purlString, "pkg:golang/github.com/") { - if len(allURL.Component) == 0 { - m.s.Debugf("Didn't find component in golang projects table for %v. Checking all urls...", purlString) - } else if len(allURL.License) == 0 { - m.s.Debugf("Didn't find license in golang projects table for %v. Checking all urls...", purlString) - } - purlString = utils.ConvertGoPurlStringToGithub(purlString) // Convert to GitHub purl - purl, err = utils.PurlFromString(purlString) - if err != nil { - return AllURL{}, err - } - purlName, err = utils.PurlNameFromString(purlString) // Make sure we just have the bare minimum for a Purl Name - if err != nil { - return AllURL{}, err - } - m.s.Debugf("Now searching All Urls for Purl: %#v, PurlName: %v", purl, purlName) - } else { - return allURL, err - } - } - if len(purl.Version) > 0 { - return m.GetURLsByPurlNameTypeVersion(purlName, purl.Type, purl.Version) - } - return m.GetURLsByPurlNameType(purlName, purl.Type, purlReq) -} - -// GetURLsByPurlNameType searches for component details of the specified Purl Name/Type (and optional requirement). -func (m *AllUrlsModel) GetURLsByPurlNameType(purlName, purlType, purlReq string) (AllURL, error) { - if len(purlName) == 0 { - m.s.Error("Please specify a valid Purl Name to query") - return AllURL{}, errors.New("please specify a valid Purl Name to query") - } - if len(purlType) == 0 { - m.s.Errorf("Please specify a valid Purl Type to query: %v", purlName) - return AllURL{}, errors.New("please specify a valid Purl Type to query") - } - query := purlSQLQuerySelect + licSpdxSQLQuery + " purl_name, mine_id FROM all_urls u" + - mineLeftJoinSQL + licLeftJoinSQL + verLeftJoinSQL + - " WHERE m.purl_type = $1 AND u.purl_name = $2 ORDER BY date DESC" - var allUrls []AllURL - err := m.conn.SelectContext(m.ctx, &allUrls, query, purlType, purlName) - if err != nil { - m.s.Errorf("Failed to query all urls table for %v - %v: %v", purlType, purlName, err) - return AllURL{}, fmt.Errorf("failed to query the all urls table: %v", err) - } - m.s.Debugf("Found %v results for %v, %v.", len(allUrls), purlType, purlName) - // Pick one URL to return (checking for license details also) - return pickOneURL(m.s, m.project, allUrls, purlName, purlType, purlReq) -} - -// GetURLsByPurlNameTypeVersion searches for component details of the specified Purl Name/Type and version. -func (m *AllUrlsModel) GetURLsByPurlNameTypeVersion(purlName, purlType, purlVersion string) (AllURL, error) { - if len(purlName) == 0 { - m.s.Error("Please specify a valid Purl Name to query") - return AllURL{}, errors.New("please specify a valid Purl Name to query") - } - if len(purlType) == 0 { - m.s.Error("Please specify a valid Purl Type to query") - return AllURL{}, errors.New("please specify a valid Purl Type to query") - } - if len(purlVersion) == 0 { - m.s.Error("Please specify a valid Purl Version to query") - return AllURL{}, errors.New("please specify a valid Purl Version to query") - } - query := purlSQLQuerySelect + licSpdxSQLQuery + " purl_name, mine_id FROM all_urls u" + - mineLeftJoinSQL + licLeftJoinSQL + verLeftJoinSQL + - " WHERE m.purl_type = $1 AND u.purl_name = $2 AND v.version_name = $3 ORDER BY date DESC" - var allUrls []AllURL - err := m.conn.SelectContext(m.ctx, &allUrls, query, purlType, purlName, purlVersion) - if err != nil { - m.s.Errorf("Failed to query all urls table for %v - %v: %v", purlType, purlName, err) - return AllURL{}, fmt.Errorf("failed to query the all urls table: %v", err) - } - m.s.Debugf("Found %v results for %v, %v.", len(allUrls), purlType, purlName) - // Pick one URL to return (checking for license details also) - return pickOneURL(m.s, m.project, allUrls, purlName, purlType, "") -} - -func pickOneURL(s *zap.SugaredLogger, projModel *ProjectModel, allUrls []AllURL, purlName, purlType, purlReq string) (AllURL, error) { - if len(allUrls) == 0 { - s.Infof("No component match (in urls) found for %v, %v", purlName, purlType) - return AllURL{}, nil - } - var c *semver.Constraints - var urlMap = make(map[*semver.Version]AllURL) - if len(purlReq) > 0 { - s.Debugf("Building version constraint for %v: %v", purlName, purlReq) - var err error - c, err = semver.NewConstraint(purlReq) - if err != nil { - s.Warnf("Encountered an issue parsing version constraint string '%v' (%v,%v): %v", purlReq, purlName, purlType, err) - } - } - s.Debugf("Checking versions...") - for _, url := range allUrls { - if len(url.SemVer) > 0 || len(url.Version) > 0 { - v, err := semver.NewVersion(url.Version) - if err != nil && len(url.SemVer) > 0 { - s.Debugf("Failed to parse SemVer: '%v'. Trying Version instead: %v (%v)", url.Version, url.SemVer, err) - v, err = semver.NewVersion(url.SemVer) // Semver failed, try the normal version - } - if err != nil { - s.Warnf("Encountered an issue parsing version string '%v' (%v) for %v: %v. Using v0.0.0", url.Version, url.SemVer, url, err) - v, err = semver.NewVersion("v0.0.0") // Semver failed, just use a standard version zero (for now) - } - if err == nil { - if c == nil || c.Check(v) { - _, ok := urlMap[v] - if !ok { - urlMap[v] = url // fits inside the constraint and hasn't already been stored - } - } - } - } else { - s.Infof("Skipping match as it doesn't have a version: %#v", url) - } - } - if len(urlMap) == 0 { // TODO should we return the latest version anyway? - s.Warnf("No component match found for %v, %v after filter %v", purlName, purlType, purlReq) - return AllURL{}, nil - } - var versions = make([]*semver.Version, len(urlMap)) - var vi = 0 - for version := range urlMap { // Save the list of versions so they can be sorted - versions[vi] = version - vi++ - } - sort.Sort(semver.Collection(versions)) - version := versions[len(versions)-1] // Get the latest (acceptable) URL version - s.Debugf("Sorted versions: %v. Highest: %v", versions, version) - - url, ok := urlMap[version] // Retrieve the latest accepted URL version - if !ok { - s.Errorf("Problem retrieving URL data for %v (%v, %v)", version, purlName, purlType) - return AllURL{}, fmt.Errorf("failed to retrieve specific URL version: %v", version) - } - url.URL, _ = utils.ProjectURL(purlName, purlType) - - s.Debugf("Selected version: %#v", url) - if len(url.License) == 0 && projModel != nil { // Check for a project license if we don't have a component one - project, err := projModel.GetProjectByPurlName(purlName, url.MineID) - switch { - case err != nil: - s.Warnf("Problem searching projects table for %v, %v", purlName, purlType) - case len(project.License) > 0: - s.Debugf("Adding project license data to %v from %v", url, project) - url.License = project.License - url.IsSpdx = project.IsSpdx - url.LicenseID = project.LicenseID - case len(project.GitLicense) > 0: - s.Debugf("Adding project git license data to %v from %v", url, project) - url.License = project.GitLicense - url.IsSpdx = project.GitIsSpdx - url.LicenseID = project.GitLicenseID - } - } - return url, nil // Return the best component match -} diff --git a/pkg/models/all_urls_test.go b/pkg/models/all_urls_test.go deleted file mode 100644 index 613241b..0000000 --- a/pkg/models/all_urls_test.go +++ /dev/null @@ -1,422 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2018-2025 SCANOSS.COM - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package models - -import ( - "context" - "fmt" - "testing" - - zlog "github.com/scanoss/zap-logging-helper/pkg/logger" - - "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" - "github.com/jmoiron/sqlx" - "scanoss.com/vulnerabilities/pkg/config" -) - -func TestAllUrlsSearch(t *testing.T) { - ctx := context.Background() - err := zlog.NewSugaredDevLogger() - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) - } - defer zlog.SyncZap() - db, err := sqlx.Connect("sqlite3", ":memory:") - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseDB(db) - conn, err := db.Connx(ctx) // Get a connection from the pool - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseConn(conn) - err = loadTestSQLDataFiles(db, ctx, conn, []string{ - "../models/tests/all_urls.sql", - "../models/tests/mines.sql", - "../models/tests/projects.sql", - "../models/tests/golang_projects.sql", - "../models/tests/licenses.sql", - "../models/tests/versions.sql", - }) - if err != nil { - t.Fatalf("failed to load SQL test data: %+v", err) - } - myConfig, err := config.NewServerConfig(nil) - if err != nil { - t.Fatalf("failed to load server config: %+v", err) - } - myConfig.Components.CommitMissing = true - myConfig.Database.Trace = true - allUrlsModel := NewAllURLModel(ctx, zlog.S, conn, NewProjectModel(ctx, zlog.S, conn), - NewGolangProjectModel(ctx, zlog.S, conn, myConfig)) - - allUrls, err := allUrlsModel.GetURLsByPurlNameType("tablestyle", "gem", "") - if err != nil { - t.Errorf("all_urls.GetUrlsByPurlName() error = %v", err) - } - if len(allUrls.PurlName) == 0 { - t.Errorf("all_urls.GetUrlsByPurlName() No URLs returned from query") - } - fmt.Printf("All Urls: %#v\n", allUrls) - - allUrls, err = allUrlsModel.GetURLsByPurlNameType("NONEXISTENT", "none", "") - if err != nil { - t.Errorf("all_urls.GetUrlsByPurlName() error = %v", err) - } - if len(allUrls.PurlName) > 0 { - t.Errorf("all_urls.GetURLsByPurlNameType() URLs found when none should be: %v", allUrlsModel) - } - fmt.Printf("No Urls: %v\n", allUrls) - - _, err = allUrlsModel.GetURLsByPurlNameType("NONEXISTENT", "", "") - if err == nil { - t.Errorf("all_urls.GetURLsByPurlString() error = did not get an error") - } else { - fmt.Printf("Got expected error = %v\n", err) - } - _, err = allUrlsModel.GetURLsByPurlNameType("", "", "") - if err == nil { - t.Errorf("all_urls.GetURLsByPurlString() error = did not get an error") - } else { - fmt.Printf("Got expected error = %v\n", err) - } - _, err = allUrlsModel.GetURLsByPurlString("", "") - if err == nil { - t.Errorf("all_urls.GetURLsByPurlString() error = did not get an error") - } else { - fmt.Printf("Got expected error = %v\n", err) - } - _, err = allUrlsModel.GetURLsByPurlString("rubbish-purl", "") - if err == nil { - t.Errorf("all_urls.GetURLsByPurlString() error = did not get an error") - } else { - fmt.Printf("Got expected error = %v\n", err) - } - allUrls, err = allUrlsModel.GetURLsByPurlString("pkg:gem/tablestyle", "") - if err != nil { - t.Errorf("all_urls.GetURLsByPurlString() error = %v", err) - } - if len(allUrls.PurlName) == 0 { - t.Errorf("all_urls.GetURLsByPurlString() No URLs returned from query") - } - fmt.Printf("All Urls: %v\n", allUrls) - - allUrls, err = allUrlsModel.GetURLsByPurlString("pkg:golang/google.golang.org/grpc", "") - if err != nil { - t.Errorf("all_urls.GetURLsByPurlString() error = %v", err) - } - if len(allUrls.PurlName) == 0 { - t.Errorf("all_urls.GetURLsByPurlString() No URLs returned from query") - } - fmt.Printf("Golang URL: %v\n", allUrls) - - fmt.Printf("Searching for pkg:golang/github.com/scanoss/dependencies") - allUrls, err = allUrlsModel.GetURLsByPurlString("pkg:golang/github.com/scanoss/dependencies", "") - if err != nil { - t.Errorf("all_urls.GetURLsByPurlString() error = %v", err) - } - if len(allUrls.PurlName) == 0 { - t.Errorf("all_urls.GetURLsByPurlString() No URLs returned from query") - } - fmt.Printf("Golang URL: %v\n", allUrls) -} - -func TestAllUrlsSearchVersion(t *testing.T) { - ctx := context.Background() - err := zlog.NewSugaredDevLogger() - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) - } - defer zlog.SyncZap() - db, err := sqlx.Connect("sqlite3", ":memory:") - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseDB(db) - conn, err := db.Connx(ctx) // Get a connection from the pool - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseConn(conn) - err = loadTestSQLDataFiles(db, ctx, conn, []string{ - "../models/tests/all_urls.sql", - "../models/tests/mines.sql", - "../models/tests/projects.sql", - "../models/tests/golang_projects.sql", - "../models/tests/licenses.sql", - "../models/tests/versions.sql", - }) - if err != nil { - t.Fatalf("failed to load SQL test data: %v", err) - } - s := ctxzap.Extract(ctx).Sugar() - myConfig, err := config.NewServerConfig(nil) - if err != nil { - t.Fatalf("an error '%s' was not expected when loading config", err) - } - myConfig.Components.CommitMissing = true - myConfig.Database.Trace = true - allUrlsModel := NewAllURLModel(ctx, s, conn, NewProjectModel(ctx, s, conn), - NewGolangProjectModel(ctx, s, conn, myConfig)) - - allUrls, err := allUrlsModel.GetURLsByPurlNameTypeVersion("tablestyle", "gem", "0.0.12") - if err != nil { - t.Errorf("all_urls.GetUrlsByPurlName() error = %v", err) - } - if len(allUrls.PurlName) == 0 { - t.Errorf("all_urls.GetURLsByPurlNameTypeVersion() No URLs returned from query") - } - fmt.Printf("All Urls Version: %#v\n", allUrls) - - allUrls, err = allUrlsModel.GetURLsByPurlString("pkg:gem/tablestyle@0.0.7", "") - if err != nil { - t.Errorf("all_urls.GetURLsByPurlString() error = failed to find purl by version string") - } - fmt.Printf("All Urls Version String: %#v\n", allUrls) - - _, err = allUrlsModel.GetURLsByPurlNameTypeVersion("", "", "") - if err == nil { - t.Errorf("all_urls.GetURLsByPurlNameTypeVersion() error = did not get an error") - } else { - fmt.Printf("Got expected error = %v\n", err) - } - _, err = allUrlsModel.GetURLsByPurlNameTypeVersion("NONEXISTENT", "", "") - if err == nil { - t.Errorf("all_urls.GetURLsByPurlNameTypeVersion() error = did not get an error") - } else { - fmt.Printf("Got expected error = %v\n", err) - } - _, err = allUrlsModel.GetURLsByPurlNameTypeVersion("NONEXISTENT", "NONEXISTENT", "") - if err == nil { - t.Errorf("all_urls.GetURLsByPurlNameTypeVersion() error = did not get an error") - } else { - fmt.Printf("Got expected error = %v\n", err) - } - - allUrls, err = allUrlsModel.GetURLsByPurlString("pkg:gem/tablestyle", "22.22.22") // Shouldn't exist - if err != nil { - t.Errorf("all_urls.GetURLsByPurlString() error = failed to find purl by version string") - } else if len(allUrls.PurlName) > 0 { - t.Errorf("all_urls.GetURLsByPurlString() error = Found match, when we shouldn't: %v", allUrls) - } -} - -func TestAllUrlsSearchVersionRequirement(t *testing.T) { - ctx := context.Background() - err := zlog.NewSugaredDevLogger() - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) - } - defer zlog.SyncZap() - db, err := sqlx.Connect("sqlite3", ":memory:") - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseDB(db) - conn, err := db.Connx(ctx) // Get a connection from the pool - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseConn(conn) - err = loadTestSQLDataFiles(db, ctx, conn, []string{ - "../models/tests/all_urls.sql", - "../models/tests/mines.sql", - "../models/tests/projects.sql", - "../models/tests/golang_projects.sql", - "../models/tests/licenses.sql", - "../models/tests/versions.sql", - }) - if err != nil { - t.Fatalf("failed to load SQL test data: %v", err) - } - s := ctxzap.Extract(ctx).Sugar() - myConfig, err := config.NewServerConfig(nil) - - if err != nil { - t.Fatalf("failed to load Config: %v", err) - } - myConfig.Components.CommitMissing = true - myConfig.Database.Trace = true - allUrlsModel := NewAllURLModel(ctx, s, conn, NewProjectModel(ctx, s, conn), - NewGolangProjectModel(ctx, s, conn, myConfig)) - - allUrls, err := allUrlsModel.GetURLsByPurlString("pkg:gem/tablestyle", ">0.0.4") - if err != nil { - t.Errorf("all_urls.GetUrlsByPurlName() error = %v", err) - } - if len(allUrls.PurlName) == 0 { - t.Errorf("all_urls.GetURLsByPurlString() No URLs returned from query") - } - fmt.Printf("All Urls Version: %#v\n", allUrls) - - allUrls, err = allUrlsModel.GetURLsByPurlString("pkg:gem/tablestyle", "<0.0.4>") - if err != nil { - t.Errorf("all_urls.GetUrlsByPurlName() error = %v", err) - } - if len(allUrls.PurlName) == 0 { - t.Errorf("all_urls.GetURLsByPurlString() No URLs returned from query") - } -} - -func TestAllUrlsSearchNoProject(t *testing.T) { - ctx := context.Background() - err := zlog.NewSugaredDevLogger() - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) - } - defer zlog.SyncZap() - db, err := sqlx.Connect("sqlite3", ":memory:") - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseDB(db) - conn, err := db.Connx(ctx) // Get a connection from the pool - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseConn(conn) - err = loadTestSQLDataFiles(db, ctx, conn, []string{ - "../models/tests/all_urls.sql", - "../models/tests/mines.sql", - "../models/tests/projects.sql", - "../models/tests/golang_projects.sql", - "../models/tests/licenses.sql", - "../models/tests/versions.sql", - }) - if err != nil { - t.Fatalf("failed to load SQL test data: %v", err) - } - s := ctxzap.Extract(ctx).Sugar() - myConfig, err := config.NewServerConfig(nil) - if err != nil { - t.Fatalf("failed to load Config: %v", err) - } - myConfig.Components.CommitMissing = true - myConfig.App.Trace = true - allUrlsModel := NewAllURLModel(ctx, s, conn, nil, NewGolangProjectModel(ctx, s, conn, myConfig)) - - allUrls, err := allUrlsModel.GetURLsByPurlNameType("tablestyle", "gem", "0.0.8") - if err != nil { - t.Errorf("all_urls.GetUrlsByPurlName() error = %v", err) - } - if len(allUrls.PurlName) == 0 { - t.Errorf("all_urls.GetURLsByPurlNameType() No URLs returned from query") - } - fmt.Printf("All Urls: %#v\n", allUrls) -} - -func TestAllUrlsSearchNoLicense(t *testing.T) { - ctx := context.Background() - err := zlog.NewSugaredDevLogger() - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) - } - defer zlog.SyncZap() - db, err := sqlx.Connect("sqlite3", ":memory:") - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseDB(db) - conn, err := db.Connx(ctx) // Get a connection from the pool - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseConn(conn) - err = loadTestSQLDataFiles(db, ctx, conn, []string{ - "../models/tests/all_urls.sql", - "../models/tests/mines.sql", - "../models/tests/projects.sql", - "../models/tests/golang_projects.sql", - "../models/tests/licenses.sql", - "../models/tests/versions.sql", - }) - if err != nil { - t.Fatalf("failed to load SQL test data: %v", err) - } - s := ctxzap.Extract(ctx).Sugar() - myConfig, err := config.NewServerConfig(nil) - if err != nil { - t.Fatalf("failed to load Config: %v", err) - } - myConfig.Components.CommitMissing = true - myConfig.App.Trace = true - allUrlsModel := NewAllURLModel(ctx, s, conn, NewProjectModel(ctx, s, conn), - NewGolangProjectModel(ctx, s, conn, myConfig)) - - allUrls, err := allUrlsModel.GetURLsByPurlString("pkg:gem/tablestyle@0.0.8", "") - if err != nil { - t.Errorf("all_urls.GetURLsByPurlString() error = %v", err) - } - if len(allUrls.PurlName) == 0 { - t.Errorf("all_urls.GetURLsByPurlString() No URLs returned from query") - } - fmt.Printf("All (with project) Urls: %#v\n", allUrls) -} - -func TestAllUrlsSearchBadSql(t *testing.T) { - ctx := context.Background() - err := zlog.NewSugaredDevLogger() - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) - } - defer zlog.SyncZap() - db, err := sqlx.Connect("sqlite3", ":memory:") - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseDB(db) - conn, err := db.Connx(ctx) // Get a connection from the pool - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseConn(conn) - myConfig, err := config.NewServerConfig(nil) - if err != nil { - t.Fatalf("failed to load Config: %v", err) - } - myConfig.Components.CommitMissing = true - myConfig.App.Trace = true - allUrlsModel := NewAllURLModel(ctx, zlog.S, conn, NewProjectModel(ctx, zlog.S, conn), - NewGolangProjectModel(ctx, zlog.S, conn, myConfig)) - _, err = allUrlsModel.GetURLsByPurlString("pkg:gem/tablestyle", "") - if err == nil { - t.Errorf("all_urls.GetURLsByPurlString() error = did not get an error") - } else { - fmt.Printf("Got expected error = %v\n", err) - } - _, err = allUrlsModel.GetURLsByPurlString("pkg:gem/tablestyle@0.0.8", "") - if err == nil { - t.Errorf("all_urls.GetURLsByPurlString() error = did not get an error: %v", err) - } else { - fmt.Printf("Got expected error = %v\n", err) - } - // Load some tables (leaving out projects) - err = loadTestSQLDataFiles(db, ctx, conn, []string{"./tests/mines.sql", "./tests/all_urls.sql", "./tests/licenses.sql", "./tests/versions.sql"}) - if err != nil { - t.Fatalf("failed to load SQL test data: %v", err) - } - // allUrls, err := allUrlsModel.GetURLsByPurlNameType("tablestyle", "gem", "") - allUrls, err := allUrlsModel.GetURLsByPurlString("pkg:gem/tablestyle@0.0.8", "") - if err != nil { - t.Errorf("all_urls.GetUrlsByPurlName() error = %v", err) - } - if len(allUrls.PurlName) == 0 { - t.Errorf("all_urls.GetURLsByPurlNameType() No URLs returned from query") - } - fmt.Printf("All Urls: %v\n", allUrls) -} diff --git a/pkg/models/cpe_purl.go b/pkg/models/cpe_purl.go index 46495b1..f4018f4 100644 --- a/pkg/models/cpe_purl.go +++ b/pkg/models/cpe_purl.go @@ -23,6 +23,7 @@ import ( "github.com/Masterminds/semver/v3" "github.com/jmoiron/sqlx" + purlhelper "github.com/scanoss/go-purl-helper/pkg" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" "scanoss.com/vulnerabilities/pkg/utils" ) @@ -49,7 +50,7 @@ func (m *CpePurlModel) GetCpeByPurl(purlString, purlReq string) ([]CpePurl, erro zlog.S.Errorf("Please specify a valid Purl String to query") return []CpePurl{}, errors.New("please specify a valid Purl String to query") } - purl, err := utils.PurlFromString(purlString) + purl, err := purlhelper.PurlFromString(purlString) if err != nil { return []CpePurl{}, err } @@ -58,7 +59,7 @@ func (m *CpePurlModel) GetCpeByPurl(purlString, purlReq string) ([]CpePurl, erro purlString = utils.PurlRemoveFromVersionComponent(purlString) // Make sure to get the minimum purl pkg:github... if len(purlVersion) == 0 && len(purlReq) > 0 { // No version specified, but we might have a specific version in the Requirement - ver := utils.GetVersionFromReq(purlReq) + ver := purlhelper.GetVersionFromReq(purlReq) if len(ver) > 0 { purlVersion = ver // Switch to exact version search (faster) purlReq = "" diff --git a/pkg/models/golang_projects.go b/pkg/models/golang_projects.go deleted file mode 100644 index 4cce396..0000000 --- a/pkg/models/golang_projects.go +++ /dev/null @@ -1,341 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2018-2025 SCANOSS.COM - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package models - -import ( - "context" - "database/sql" - "errors" - "fmt" - - pkggodevclient "github.com/guseggert/pkggodev-client" - "github.com/jmoiron/sqlx" - "github.com/package-url/packageurl-go" - "go.uber.org/zap" - - myconfig "scanoss.com/vulnerabilities/pkg/config" - "scanoss.com/vulnerabilities/pkg/utils" -) - -type GolangProjects struct { - ctx context.Context - s *zap.SugaredLogger - conn *sqlx.Conn - config *myconfig.ServerConfig - ver *VersionModel - lic *LicenseModel - mine *MineModel - project *ProjectModel // TODO Do we add golang component to the projects table? -} - -// NewGolangProjectModel creates a new instance of Golang Project Model. -func NewGolangProjectModel(ctx context.Context, s *zap.SugaredLogger, conn *sqlx.Conn, config *myconfig.ServerConfig) *GolangProjects { - return &GolangProjects{ctx: ctx, s: s, conn: conn, - ver: NewVersionModel(ctx, conn), - lic: NewLicenseModel(ctx, s, conn), - mine: NewMineModel(ctx, s, conn), - project: NewProjectModel(ctx, s, conn), - config: config, - } -} - -// GetGoLangURLByPurlString searches the Golang Projects for the specified Purl (and requirement). -func (m *GolangProjects) GetGoLangURLByPurlString(purlString, purlReq string) (AllURL, error) { - if len(purlString) == 0 { - m.s.Error("Please specify a valid Purl String to query") - return AllURL{}, errors.New("please specify a valid Purl String to query") - } - purl, err := utils.PurlFromString(purlString) - if err != nil { - return AllURL{}, err - } - purlName, err := utils.PurlNameFromString(purlString) - if err != nil { - return AllURL{}, err - } - if len(purl.Version) == 0 && len(purlReq) > 0 { // No version specified, but we might have a specific version in the Requirement - ver := utils.GetVersionFromReq(purlReq) - if len(ver) > 0 { - purl.Version = ver - purlReq = "" - } - } - return m.GetGoLangURLByPurl(purl, purlName, purlReq) -} - -// GetGoLangURLByPurl searches the Golang Projects for the specified Purl Package (and optional requirement). -func (m *GolangProjects) GetGoLangURLByPurl(purl packageurl.PackageURL, purlName, purlReq string) (AllURL, error) { - if len(purl.Version) > 0 { - return m.GetGolangUrlsByPurlNameTypeVersion(purlName, purl.Type, purl.Version) - } - return m.GetGolangUrlsByPurlNameType(purlName, purl.Type, purlReq) -} - -// GetGolangUrlsByPurlNameType searches Golang Project for the specified Purl by Purl Type (and optional requirement). -func (m *GolangProjects) GetGolangUrlsByPurlNameType(purlName, purlType, purlReq string) (AllURL, error) { - if len(purlName) == 0 { - m.s.Error("Please specify a valid Purl Name to query") - return AllURL{}, errors.New("please specify a valid Purl Name to query") - } - if len(purlType) == 0 { - m.s.Errorf("Please specify a valid Purl Type to query: %v", purlName) - return AllURL{}, errors.New("please specify a valid Purl Type to query") - } - query := "SELECT component, v.version_name AS version, v.semver AS semver," + - " l.license_name AS license, l.spdx_id AS license_id, l.is_spdx AS is_spdx," + - " purl_name, mine_id FROM golang_projects u" + - " LEFT JOIN mines m ON u.mine_id = m.id" + - " LEFT JOIN licenses l ON u.license_id = l.id" + - " LEFT JOIN versions v ON u.version_id = v.id" + - " WHERE m.purl_type = $1 AND u.purl_name = $2 AND is_indexed = True" + - " ORDER BY version_date DESC" - var allURLs []AllURL - err := m.conn.SelectContext(m.ctx, &allURLs, query, purlType, purlName) - if err != nil { - m.s.Errorf("Failed to query golang projects table for %v - %v: %v", purlType, purlName, err) - return AllURL{}, fmt.Errorf("failed to query the golang projects table: %v", err) - } - m.s.Debugf("Found %v results for %v, %v.", len(allURLs), purlType, purlName) - if len(allURLs) == 0 { // Check pkg.go.dev for the latest data - m.s.Debugf("Checking PkgGoDev for live info...") - allURL, err := m.getLatestPkgGoDev(purlName, purlType, "") - if err == nil { - m.s.Debugf("Retrieved golang data from pkg.go.dev: %#v", allURL) - allURLs = append(allURLs, allURL) - } else { - m.s.Infof("Ran into an issue looking up pkg.go.dev for: %v. Ignoring", purlName) - } - } - - // Pick the most appropriate version to return - return pickOneURL(m.s, m.project, allURLs, purlName, purlType, purlReq) -} - -// GetGolangUrlsByPurlNameTypeVersion searches Golang Projects for specified Purl, Type and Version. -func (m *GolangProjects) GetGolangUrlsByPurlNameTypeVersion(purlName, purlType, purlVersion string) (AllURL, error) { - if len(purlName) == 0 { - m.s.Error("Please specify a valid Purl Name to query") - return AllURL{}, errors.New("please specify a valid Purl Name to query") - } - if len(purlType) == 0 { - m.s.Error("Please specify a valid Purl Type to query") - return AllURL{}, errors.New("please specify a valid Purl Type to query") - } - if len(purlVersion) == 0 { - m.s.Error("Please specify a valid Purl Version to query") - return AllURL{}, errors.New("please specify a valid Purl Version to query") - } - query := "SELECT component, v.version_name AS version, v.semver AS semver," + - " l.license_name AS license, l.spdx_id AS license_id, l.is_spdx AS is_spdx," + - " purl_name, mine_id FROM golang_projects u" + - " LEFT JOIN mines m ON u.mine_id = m.id" + - " LEFT JOIN licenses l ON u.license_id = l.id" + - " LEFT JOIN versions v ON u.version_id = v.id" + - " WHERE m.purl_type = $1 AND u.purl_name = $2 AND v.version_name = $3 AND is_indexed = True" + - " ORDER BY version_date DESC" - var allURLs []AllURL - err := m.conn.SelectContext(m.ctx, &allURLs, query, purlType, purlName, purlVersion) - if err != nil { - m.s.Errorf("Failed to query golang projects table for %v - %v: %v", purlType, purlName, err) - return AllURL{}, fmt.Errorf("failed to query the golang projects table: %v", err) - } - m.s.Debugf("Found %v results for %v, %v.", len(allURLs), purlType, purlName) - if len(allURLs) > 0 { // We found an entry. Let's check if it has license data - allURL, err2 := pickOneURL(m.s, m.project, allURLs, purlName, purlType, "") - if len(allURL.License) == 0 { // No license data found. Need to search for live info - m.s.Debugf("Couldn't find license data for component. Need to search live data") - allURLs = allURLs[:0] - } else { - return allURL, err2 // Return the component details - } - } - if len(allURLs) == 0 { // Check pkg.go.dev for the latest data - m.s.Debugf("Checking PkgGoDev for live info...") - allURL, err := m.getLatestPkgGoDev(purlName, purlType, purlVersion) - if err == nil { - m.s.Debugf("Retrieved golang data from pkg.go.dev: %#v", allURL) - allURLs = append(allURLs, allURL) - } else { - m.s.Infof("Ran into an issue looking up pkg.go.dev for: %v - %v. Ignoring", purlName, purlVersion) - } - } - // Pick the most appropriate version to return - return pickOneURL(m.s, m.project, allURLs, purlName, purlType, "") -} - -// savePkg writes the given package details to the Golang Projects table. -// -//goland:noinspection ALL -func (m *GolangProjects) savePkg(allURL AllURL, version Version, license License, comp *pkggodevclient.Package) error { - if len(allURL.PurlName) == 0 { - m.s.Error("Please specify a valid Purl to save") - return errors.New("please specify a valid Purl to save") - } - if allURL.MineID <= 0 { - m.s.Error("Please specify a valid mine id to save") - return errors.New("please specify a valid mine id to save") - } - if version.ID <= 0 || len(version.VersionName) == 0 { - m.s.Error("Please specify a valid version to save") - return errors.New("please specify a valid version to save") - } - if license.ID <= 0 || len(license.LicenseName) == 0 { - m.s.Error("Please specify a valid license to save") - return errors.New("please specify a valid license to save") - } - if comp == nil { - m.s.Error("Please specify a valid component package to save") - return errors.New("please specify a valid component package to save") - } - m.s.Debugf("Attempting to save '%#v' - %#v to the golang_projects table...", allURL, version) - // Search for an existing entry first - var existingPurl string - err := m.conn.QueryRowxContext(m.ctx, - "SELECT purl_name FROM golang_projects"+ - " WHERE purl_name = $1 AND version = $2", - allURL.PurlName, allURL.Version, - ).Scan(&existingPurl) - if err != nil && err != sql.ErrNoRows { - m.s.Warnf("Error: Problem encountered searching golang_projects table for %v: %v", allURL, err) - } - var purlName string - sqlQueryType := "insert" - if len(existingPurl) > 0 { - // update entry - sqlQueryType = "update" - m.s.Debugf("Updating new Golang project: %#v", comp) - //goland:noinspection ALL - err = m.conn.QueryRowxContext(m.ctx, - "UPDATE golang_projects SET component = $1, version = $2, version_id = $3, version_date = $4,"+ - " is_module = $5, is_package = $6, license = $7, license_id = $8, has_valid_go_mod_file = $9,"+ - " has_redistributable_license = $10, has_tagged_version = $11, has_stable_version = $12,"+ - " repository = $13, is_indexed = $14, purl_name = $15, mine_id = $16"+ - " WHERE purl_name = $17 AND version = $18"+ - " RETURNING purl_name", - allURL.Component, allURL.Version, version.ID, comp.Published, - comp.IsModule, comp.IsPackage, license.LicenseName, license.ID, comp.HasValidGoModFile, - comp.HasRedistributableLicense, comp.HasTaggedVersion, comp.HasStableVersion, - comp.Repository, true, allURL.PurlName, allURL.MineID, - allURL.PurlName, allURL.Version, - ).Scan(&purlName) - } else { - m.s.Debugf("Inserting new Golang project: %#v", comp) - // insert new entry - err = m.conn.QueryRowxContext(m.ctx, - "INSERT INTO golang_projects (component, version, version_id, version_date, is_module, is_package,"+ - " license, license_id, has_valid_go_mod_file, has_redistributable_license, has_tagged_version,"+ - " has_stable_version, repository, is_indexed, purl_name, mine_id, index_timestamp)"+ - " VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)"+ - " RETURNING purl_name", - allURL.Component, allURL.Version, version.ID, comp.Published, - comp.IsModule, comp.IsPackage, license.LicenseName, license.ID, comp.HasValidGoModFile, - comp.HasRedistributableLicense, comp.HasTaggedVersion, comp.HasStableVersion, - comp.Repository, true, allURL.PurlName, allURL.MineID, "", - ).Scan(&purlName) - } - if err != nil { - m.s.Errorf("Error: Failed to %v new component into golang_projects table for %v - %#v: %v", sqlQueryType, allURL, comp, err) - return fmt.Errorf("failed to %v new component into golang projects: %v", sqlQueryType, err) - } - m.s.Debugf("Completed %v of %v", sqlQueryType, purlName) - return nil -} - -// getLatestPkgGoDev retrieves the latest information about a Golang Package from https://pkg.go.dev -// If requested (via config), it will commit that data to the Golang Projects table. -func (m *GolangProjects) getLatestPkgGoDev(purlName, purlType, purlVersion string) (AllURL, error) { - allURL, pkg, latest, err := m.queryPkgGoDev(purlName, purlVersion) - if err != nil { - return allURL, err - } - cleansedLicense, err := CleanseLicenseName(allURL.License) - if err != nil { - return allURL, err - } - - m.s.Infof("Getting license name: %s, %+v", cleansedLicense, m.config.Components.CommitMissing) - - license, err := m.lic.GetLicenseByName(cleansedLicense, m.config.Components.CommitMissing) - if err != nil { - m.s.Warnf("No license details in DB for: %v, %+v", cleansedLicense, err) - } - if len(license.LicenseName) == 0 { - m.s.Warnf("No license details in DB for: %v", cleansedLicense) - } else { - allURL.License = license.LicenseName - allURL.LicenseID = license.LicenseID - allURL.IsSpdx = license.IsSpdx - } - version, _ := m.ver.GetVersionByName(allURL.Version, m.config.Components.CommitMissing) - if len(version.VersionName) == 0 { - m.s.Warnf("No version details in DB for: %v", allURL.Version) - } - mineIDs, _ := m.mine.GetMineIdsByPurlType(purlType) - if len(mineIDs) > 0 { - allURL.MineID = mineIDs[0] // Assign the first mine id - } else { - m.s.Warnf("No mine details in DB for purl type: %v", purlType) - } - // Package is not the "latest" version (i.e. queried with a version) and we've been requested to save it - if !latest && m.config.Components.CommitMissing { - _ = m.savePkg(allURL, version, license, pkg) - } - return allURL, nil -} - -// queryPkgGoDev retrieves the latest information about a Golang Package from https://pkg.go.dev -func (m *GolangProjects) queryPkgGoDev(purlName, purlVersion string) (AllURL, *pkggodevclient.Package, bool, error) { - if len(purlName) == 0 { - m.s.Errorf("Please specify a valid Purl Name to query") - return AllURL{}, nil, false, errors.New("please specify a valid Purl Name to query") - } - client := pkggodevclient.New() - - if client == nil { - return AllURL{}, nil, false, errors.New("failed to create pkg.go.dev client") - } - - pkg := purlName - if len(purlVersion) > 0 { - pkg = fmt.Sprintf("%s@%s", purlName, purlVersion) - } - latest := false - m.s.Debugf("Checking pkg.go.dev for the latest info: %v", pkg) - comp, err := client.DescribePackage(pkggodevclient.DescribePackageRequest{Package: pkg}) - if err != nil && len(purlVersion) > 0 { - // We have a version zero search, so look for the latest one - m.s.Debugf("Failed to query pkg.go.dev for %v: %v. Trying without version...", pkg, err) - comp, err = client.DescribePackage(pkggodevclient.DescribePackageRequest{Package: purlName}) - latest = true // Mark that this information is from the latest package and not a specific version - } - if err != nil { - m.s.Warnf("Failed to query pkg.go.dev for %v: %v", pkg, err) - return AllURL{}, nil, latest, fmt.Errorf("failed to query pkg.go.dev: %v", err) - } - var version = comp.Version - if len(purlVersion) > 0 { - version = purlVersion // Force the requested version if specified (the returned value can be concatenated) - } - allURL := AllURL{ - Component: purlName, - Version: version, - License: comp.License, - PurlName: purlName, - URL: fmt.Sprintf("https://%v", comp.Repository), - } - return allURL, comp, latest, nil -} diff --git a/pkg/models/golang_projects_test.go b/pkg/models/golang_projects_test.go deleted file mode 100644 index afd02bf..0000000 --- a/pkg/models/golang_projects_test.go +++ /dev/null @@ -1,469 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2018-2025 SCANOSS.COM - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package models - -import ( - "context" - "fmt" - "testing" - - "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" - pkggodevclient "github.com/guseggert/pkggodev-client" - "github.com/jmoiron/sqlx" - zlog "github.com/scanoss/zap-logging-helper/pkg/logger" - - "scanoss.com/vulnerabilities/pkg/config" -) - -const ScanossPapiURL = "github.com/scanoss/papi" -const VersionV001 = "v0.0.1" -const VersionV002 = "v0.0.2" -const MITLicense = "MIT" - -func TestGolangProjectUrlsSearch(t *testing.T) { - ctx := context.Background() - err := zlog.NewSugaredDevLogger() - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) - } - defer zlog.SyncZap() - db, err := sqlx.Connect("sqlite3", ":memory:") - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseDB(db) - conn, err := db.Connx(ctx) // Get a connection from the pool - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseConn(conn) - err = LoadTestSQLData(db, ctx, conn) - if err != nil { - t.Fatalf("failed to load SQL test data: %v", err) - } - myConfig, err := config.NewServerConfig(nil) - if err != nil { - t.Fatalf("failed to load Config: %v", err) - } - myConfig.Components.CommitMissing = true - myConfig.Database.Trace = true - golangProjModel := NewGolangProjectModel(ctx, zlog.S, conn, myConfig) - - url, err := golangProjModel.GetGolangUrlsByPurlNameType("google.golang.org/grpc", "golang", "") - if err != nil { - t.Errorf("FAILED: golang_projects.GetUrlsByPurlName() error = %v", err) - } - if len(url.PurlName) == 0 { - t.Errorf("FAILED: golang_projects.GetGoLangURLByPurlString() No URLs returned from query") - } - fmt.Printf("Golang URL: %#v\n", url) - - url, err = golangProjModel.GetGolangUrlsByPurlNameType("NONEXISTENT", "none", "") - if err != nil { - t.Errorf("FAILED: golang_projects.GetGolangUrlsByPurlNameType() error = %v", err) - } - if len(url.PurlName) > 0 { - t.Errorf("FAILED: golang_projects.GetGolangUrlsByPurlNameType() URLs found when none should be: %v", golangProjModel) - } - fmt.Printf("No Urls: %v\n", url) - - _, err = golangProjModel.GetGolangUrlsByPurlNameType("NONEXISTENT", "", "") - if err == nil { - t.Errorf("FAILED: golang_projects.GetGolangUrlsByPurlNameType() error = did not get an error") - } else { - fmt.Printf("Got expected error = %v\n", err) - } - _, err = golangProjModel.GetGolangUrlsByPurlNameType("", "", "") - if err == nil { - t.Errorf("FAILED: golang_projects.GetURLsByPurlString() error = did not get an error") - } else { - fmt.Printf("Got expected error = %v\n", err) - } - _, err = golangProjModel.GetGoLangURLByPurlString("", "") - if err == nil { - t.Errorf("FAILED: golang_projects.GetURLsByPurlString() error = did not get an error") - } else { - fmt.Printf("Got expected error = %v\n", err) - } - _, err = golangProjModel.GetGoLangURLByPurlString("rubbish-purl", "") - if err == nil { - t.Errorf("FAILED: golang_projects.GetGoLangURLByPurlString() error = did not get an error") - } else { - fmt.Printf("Got expected error = %v\n", err) - } - url, err = golangProjModel.GetGoLangURLByPurlString("pkg:golang/google.golang.org/grpc", "") - if err != nil { - t.Errorf("FAILED: golang_projects.GetGoLangURLByPurlString() error = %v", err) - } - if len(url.PurlName) == 0 { - t.Errorf("FAILED: golang_projects.GetGoLangURLByPurlString() No URLs returned from query") - } - fmt.Printf("Golang URL: %v\n", url) -} - -func TestGolangProjectsSearchVersion(t *testing.T) { - ctx := context.Background() - err := zlog.NewSugaredDevLogger() - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) - } - defer zlog.SyncZap() - db, err := sqlx.Connect("sqlite3", ":memory:") - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseDB(db) - conn, err := db.Connx(ctx) // Get a connection from the pool - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseConn(conn) - err = LoadTestSQLData(db, ctx, conn) - if err != nil { - t.Fatalf("failed to load SQL test data: %v", err) - } - myConfig, err := config.NewServerConfig(nil) - if err != nil { - t.Fatalf("FAILED: failed to load Config: %v", err) - } - myConfig.Components.CommitMissing = true - myConfig.Database.Trace = true - golangProjModel := NewGolangProjectModel(ctx, zlog.S, conn, myConfig) - - url, err := golangProjModel.GetGolangUrlsByPurlNameTypeVersion("google.golang.org/grpc", "golang", "1.19.0") - if err != nil { - t.Errorf("FAILED: golang_projects.GetGolangUrlsByPurlNameTypeVersion() error = %v", err) - } - if len(url.PurlName) == 0 { - t.Errorf("FAILED: golang_projects.GetGolangUrlsByPurlNameTypeVersion() No URLs returned from query") - } - fmt.Printf("Golang URL Version: %#v\n", url) - - url, err = golangProjModel.GetGoLangURLByPurlString("pkg:golang/google.golang.org/grpc@v1.19.0", "") - if err != nil { - t.Errorf("FAILED: golang_projects.GetGoLangURLByPurlString() error = failed to find purl by version string") - } - fmt.Printf("Golang URL Version: %#v\n", url) - - _, err = golangProjModel.GetGolangUrlsByPurlNameTypeVersion("", "", "") - if err == nil { - t.Errorf("FAILED: golang_projects.GetGolangUrlsByPurlNameTypeVersion() error = did not get an error") - } else { - fmt.Printf("Got expected error = %v\n", err) - } - - _, err = golangProjModel.GetGolangUrlsByPurlNameTypeVersion("NONEXISTENT", "", "") - if err == nil { - t.Errorf("FAILED: golang_projects.GetGolangUrlsByPurlNameTypeVersion() error = did not get an error") - } else { - fmt.Printf("Got expected error = %v\n", err) - } - - _, err = golangProjModel.GetGolangUrlsByPurlNameTypeVersion("NONEXISTENT", "NONEXISTENT", "") - if err == nil { - t.Errorf("FAILED: golang_projects.GetGolangUrlsByPurlNameTypeVersion() error = did not get an error") - } else { - fmt.Printf("Got expected error = %v\n", err) - } - - url, err = golangProjModel.GetGoLangURLByPurlString("pkg:golang/google.golang.org/grpc", "22.22.22") // Shouldn't exist - if err != nil { - t.Errorf("FAILED: golang_projects.GetGoLangURLByPurlString() error = failed to find purl by version string") - } - url, err = golangProjModel.GetGoLangURLByPurlString("pkg:golang/google.golang.org/grpc", "=v1.19.0") - if err != nil { - t.Errorf("FAILED: golang_projects.GetGoLangURLByPurlString() error = %v", err) - } - if len(url.PurlName) == 0 { - t.Errorf("FAILED: golang_projects.GetGoLangURLByPurlString() No URLs returned from query") - } - fmt.Printf("Golang URL: %v\n", url) - url, err = golangProjModel.GetGoLangURLByPurlString("pkg:golang/google.golang.org/grpc", "==v1.19.0") - if err != nil { - t.Errorf("FAILED: golang_projects.GetGoLangURLByPurlString() error = %v", err) - } - if len(url.PurlName) == 0 { - t.Errorf("FAILED: golang_projects.GetGoLangURLByPurlString() No URLs returned from query") - } - fmt.Printf("Golang URL: %v\n", url) - - url, err = golangProjModel.GetGoLangURLByPurlString("pkg:golang/google.golang.org/grpc@1.7.0", "") // Should be missing license - if err != nil { - t.Errorf("FAILED: golang_projects.GetGoLangURLByPurlString() error = %v", err) - } - if len(url.License) == 0 { - t.Errorf("FAILED: golang_projects.GetGoLangURLByPurlString() No URL License returned from query") - } - fmt.Printf("Golang URL: %v\n", url) -} - -func TestGolangProjectsSearchVersionRequirement(t *testing.T) { - ctx := context.Background() - err := zlog.NewSugaredDevLogger() - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) - } - defer zlog.SyncZap() - db, err := sqlx.Connect("sqlite3", ":memory:") - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseDB(db) - conn, err := db.Connx(ctx) // Get a connection from the pool - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseConn(conn) - err = LoadTestSQLData(db, ctx, conn) - if err != nil { - t.Fatalf("failed to load SQL test data: %v", err) - } - myConfig, err := config.NewServerConfig(nil) - if err != nil { - t.Fatalf("failed to load Config: %v", err) - } - myConfig.Components.CommitMissing = true - golangProjModel := NewGolangProjectModel(ctx, zlog.S, conn, myConfig) - - url, err := golangProjModel.GetGoLangURLByPurlString("pkg:golang/google.golang.org/grpc", ">0.0.4") - if err != nil { - t.Errorf("FAILED: golang_projects.GetUrlsByPurlName() error = %v", err) - } - if len(url.PurlName) == 0 { - t.Errorf("FAILED: golang_projects.GetUrlsByPurlName() No URLs returned from query") - } - fmt.Printf("Golang URL Version: %#v\n", url) - - url, err = golangProjModel.GetGoLangURLByPurlString("pkg:golang/google.golang.org/grpc", "v0.0.0-201910101010-s3333") - if err != nil { - t.Errorf("FAILED: golang_projects.GetUrlsByPurlName() error = %v", err) - } - if len(url.PurlName) == 0 { - t.Errorf("FAILED: golang_projects.GetUrlsByPurlName() No URLs returned from query") - } - fmt.Printf("Golang URL Version: %#v\n", url) -} - -func TestGolangPkgGoDev(t *testing.T) { - // Setup test environment and models - golangProjModel, cleanup := setupTestEnvironment(t) - defer cleanup() - - // Run subtests - t.Run("QueryPkgGoDev", testQueryPkgGoDev(golangProjModel)) - t.Run("GetLatestPkgGoDev", testGetLatestPkgGoDev(golangProjModel)) - t.Run("SavePkg", testSavePkg(golangProjModel)) -} - -// setupTestEnvironment creates the test environment and returns cleanup function. -func setupTestEnvironment(t *testing.T) (*GolangProjects, func()) { - t.Helper() - ctx := context.Background() - - err := zlog.NewSugaredDevLogger() - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) - } - - db, err := sqlx.Connect("sqlite3", ":memory:") - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - - conn, err := db.Connx(ctx) - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - - err = LoadTestSQLData(db, ctx, conn) - if err != nil { - t.Fatalf("failed to load SQL test data: %v", err) - } - - myConfig, err := config.NewServerConfig(nil) - if err != nil { - t.Fatalf("failed to load Config: %v", err) - } - myConfig.Components.CommitMissing = true - - golangProjModel := NewGolangProjectModel(ctx, zlog.S, conn, myConfig) - - cleanup := func() { - CloseConn(conn) - CloseDB(db) - zlog.SyncZap() - } - - return golangProjModel, cleanup -} - -func testQueryPkgGoDev(model *GolangProjects) func(t *testing.T) { - return func(t *testing.T) { - _, _, _, err := model.queryPkgGoDev("", "") - if err == nil { - t.Error("FAILED: golang_projects.queryPkgGoDev() error = did not get an error") - } - } -} - -func testGetLatestPkgGoDev(model *GolangProjects) func(t *testing.T) { - return func(t *testing.T) { - t.Run("GRPC Package", func(t *testing.T) { - url, err := model.getLatestPkgGoDev("google.golang.org/grpc", "golang", "v0.0.0-201910101010-s3333") - if err != nil { - t.Errorf("error = %v", err) - } - if len(url.PurlName) == 0 { - t.Error("No URLs returned from query") - } - }) - - t.Run("Scanoss Package", func(t *testing.T) { - url, err := model.getLatestPkgGoDev(ScanossPapiURL, "golang", "v0.0.3") - if err != nil { - t.Errorf("error = %v", err) - } - if len(url.PurlName) == 0 { - t.Error("No URLs returned from query") - } - }) - } -} - -func testSavePkg(model *GolangProjects) func(t *testing.T) { - return func(t *testing.T) { - t.Run("Empty URL", func(t *testing.T) { - var allURL AllURL - var license License - var version Version - - err := model.savePkg(allURL, version, license, nil) - if err == nil { - t.Error("expected error with empty URL") - } - }) - - t.Run("Missing MineID", func(t *testing.T) { - allURL := AllURL{PurlName: ScanossPapiURL} - var license License - var version Version - - err := model.savePkg(allURL, version, license, nil) - if err == nil { - t.Error("expected error with missing MineID") - } - }) - - t.Run("Valid Package", func(t *testing.T) { - allURL, version, license, comp := createValidTestData() - err := model.savePkg(allURL, version, license, &comp) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - }) - - t.Run("Updated Version", func(t *testing.T) { - allURL, version, license, comp := createValidTestData() - allURL.Version = VersionV002 - version.VersionName = VersionV002 - comp.Version = VersionV002 - - err := model.savePkg(allURL, version, license, &comp) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - }) - } -} - -func createValidTestData() (AllURL, Version, License, pkggodevclient.Package) { - allURL := AllURL{ - PurlName: ScanossPapiURL, - MineID: 45, - Version: VersionV001, - } - - version := Version{ - VersionName: VersionV001, - ID: 5958021, - } - - license := License{ - LicenseName: MITLicense, - ID: 5614, - } - - comp := pkggodevclient.Package{ - Package: ScanossPapiURL, - IsPackage: true, - IsModule: true, - Version: VersionV001, - License: MITLicense, - HasRedistributableLicense: true, - HasStableVersion: true, - HasTaggedVersion: true, - HasValidGoModFile: true, - Repository: ScanossPapiURL, - } - - return allURL, version, license, comp -} - -func TestGolangProjectsSearchBadSql(t *testing.T) { - ctx := context.Background() - err := zlog.NewSugaredDevLogger() - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a sugared logger", err) - } - defer zlog.SyncZap() - db, err := sqlx.Connect("sqlite3", ":memory:") - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseDB(db) - conn, err := db.Connx(ctx) // Get a connection from the pool - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - defer CloseConn(conn) - s := ctxzap.Extract(ctx).Sugar() - myConfig, err := config.NewServerConfig(nil) - if err != nil { - t.Fatalf("failed to load Config: %v", err) - } - myConfig.Components.CommitMissing = true - golangProjModel := NewGolangProjectModel(ctx, s, conn, myConfig) - - _, err = golangProjModel.GetGoLangURLByPurlString("pkg:golang/google.golang.org/grpc", "") - if err == nil { - t.Errorf("FAILED: golang_projects.GetGoLangURLByPurlString() error = did not get an error") - } else { - fmt.Printf("Got expected error = %v\n", err) - } - _, err = golangProjModel.GetGoLangURLByPurlString("pkg:golang/google.golang.org/grpc@1.19.0", "") - if err == nil { - t.Errorf("FAILED: golang_projects.GetGoLangURLByPurlString() error = did not get an error") - } else { - fmt.Printf("Got expected error = %v\n", err) - } - _, err = golangProjModel.getLatestPkgGoDev("github.com/scanoss/does-not-exist", "golang", "v0.0.99") - if err == nil { - t.Errorf("FAILED: golang_projects.getLatestPkgGoDev() error = did not get an error: %v", err) - } else { - fmt.Printf("Got expected error = %v\n", err) - } -} diff --git a/pkg/models/vulns_purl.go b/pkg/models/vulns_purl.go index 7d9c998..e13515b 100644 --- a/pkg/models/vulns_purl.go +++ b/pkg/models/vulns_purl.go @@ -22,6 +22,7 @@ import ( "fmt" "strings" + purlhelper "github.com/scanoss/go-purl-helper/pkg" zlog "github.com/scanoss/zap-logging-helper/pkg/logger" "github.com/jmoiron/sqlx" @@ -59,7 +60,7 @@ func (m *VulnsForPurlModel) GetVulnsByPurl(purl string, version string) ([]Vulns } // used to valid the PURL - _, err := utils.PurlFromString(purl) + _, err := purlhelper.PurlFromString(purl) if err != nil { return []VulnsForPurl{}, err } diff --git a/pkg/utils/purl.go b/pkg/utils/purl.go index 85f4051..41fe685 100644 --- a/pkg/utils/purl.go +++ b/pkg/utils/purl.go @@ -17,128 +17,14 @@ package utils import ( - "errors" - "fmt" "regexp" - "strings" - - zlog "github.com/scanoss/zap-logging-helper/pkg/logger" - - "github.com/package-url/packageurl-go" ) -var pkgRegex = regexp.MustCompile(`^pkg:(?P\w+)/(?P.+)$`) // regex to parse purl name from purl string -var typeRegex = regexp.MustCompile(`^(npm|nuget)$`) // regex to parse purl types that should not be lower cased -var vRegex = regexp.MustCompile(`^(=|==|)(?P\w+\S+)$`) // regex to extract version from requirement field +// regex to extract version from requirement field. var fromVRegex = regexp.MustCompile(`(@|\?|#).+`) -// PurlFromString takes an input Purl string and returns a decomposed structure of all the elements. -func PurlFromString(purlString string) (packageurl.PackageURL, error) { - if len(purlString) == 0 { - return packageurl.PackageURL{}, errors.New("no Purl string specified to parse") - } - purl, err := packageurl.FromString(purlString) - if err != nil { - return packageurl.PackageURL{}, err - } - return purl, nil -} - -// PurlNameFromString take an input Purl string and returns the Purl Name only. -func PurlNameFromString(purlString string) (string, error) { - if len(purlString) == 0 { - return "", fmt.Errorf("no purl string supplied to parse") - } - matches := pkgRegex.FindStringSubmatch(purlString) - if len(matches) > 0 { - ti := pkgRegex.SubexpIndex("type") - ni := pkgRegex.SubexpIndex("name") - if ni >= 0 { - // Remove any version@/subpath?/qualifiers# info from the PURL - pn := strings.Split(strings.Split(strings.Split(matches[ni], "@")[0], "?")[0], "#")[0] - // Lowercase the purl name if it's not on the exclusion list (defined in the regex) - if ti >= 0 && !typeRegex.MatchString(matches[ti]) { - pn = strings.ToLower(pn) - } - return pn, nil - } - } - return "", fmt.Errorf("no purl name found in '%v'", purlString) -} - // PurlRemoveFromVersionComponent From a purlString removes everything that proceds @. // See purl specs scheme:type/namespace/name@version?qualifiers#subpath. func PurlRemoveFromVersionComponent(purlString string) string { return fromVRegex.ReplaceAllString(purlString, "") } - -// ConvertPurlString takes an input PURL and checks to see if anything needs to be modified before search the KB. -func ConvertPurlString(purlString string) string { - // Replace Golang GitHub package reference with just GitHub - if len(purlString) > 0 && strings.HasPrefix(purlString, "pkg:golang/github.com/") { - s := strings.ReplaceAll(purlString, "pkg:golang/github.com/", "pkg:github/") - p := strings.Split(s, "/") - if len(p) >= 3 { - return fmt.Sprintf("%s/%s/%s", p[0], p[1], p[2]) // Only return the GitHub part of the url - } - return s - } - return purlString -} - -// GetVersionFromReq parses a requirement string looking for an exact version specifier. -func GetVersionFromReq(purlReq string) string { - matches := vRegex.FindStringSubmatch(purlReq) - if len(matches) > 0 { - ni := vRegex.SubexpIndex("name") - if ni >= 0 { - zlog.S.Debugf("Changing requirement %v to Version %v", purlReq, matches[ni]) - return matches[ni] - } - } - return "" -} - -// ProjectURL returns a browsable URL for the given purl type and name. -func ProjectURL(purlName, purlType string) (string, error) { - if len(purlName) == 0 { - return "", fmt.Errorf("no purl name supplied") - } - if len(purlType) == 0 { - return "", fmt.Errorf("no purl type supplied") - } - switch purlType { - case "github": - return fmt.Sprintf("https://github.com/%v", purlName), nil - case "npm": - return fmt.Sprintf("https://www.npmjs.com/package/%v", purlName), nil - case "maven": - return fmt.Sprintf("https://mvnrepository.com/artifact/%v", purlName), nil - case "gem": - return fmt.Sprintf("https://rubygems.org/gems/%v", purlName), nil - case "pypi": - return fmt.Sprintf("https://pypi.org/project/%v", purlName), nil - case "golang": - return fmt.Sprintf("https://pkg.go.dev/%v", purlName), nil - } - return "", fmt.Errorf("no url prefix found for '%v': %v", purlType, purlName) -} - -// ConvertGoPurlStringToGithub takes an input PURL string and converts it to its GitHub equivalent if possible. -func ConvertGoPurlStringToGithub(purlString string) string { - // Replace Golang GitHub package reference with just GitHub - if len(purlString) > 0 && strings.HasPrefix(purlString, "pkg:golang/github.com/") { - s := strings.ReplaceAll(purlString, "pkg:golang/github.com/", "pkg:github/") - p := strings.Split(s, "/") - if len(p) >= 3 { - return fmt.Sprintf("%s/%s/%s", p[0], p[1], p[2]) // Only return the GitHub part of the url - } - return s - } - return purlString -} - -func StripVersionFromPurl(input string) string { - purl := strings.Split(input, "@") - return purl[0] -} diff --git a/pkg/utils/purl_test.go b/pkg/utils/purl_test.go index f1922dc..77c6bef 100644 --- a/pkg/utils/purl_test.go +++ b/pkg/utils/purl_test.go @@ -19,114 +19,8 @@ package utils import ( "reflect" "testing" - - "github.com/package-url/packageurl-go" ) -// Help with test details can be found here: https://go.dev/doc/code - -func TestPurlFromString(t *testing.T) { - w, _ := packageurl.FromString("pkg:maven/io.prestosql/presto-main@v1.0") - w2, _ := packageurl.FromString("pkg:npm/%40babel/core") - tests := []struct { - name string - input string - want packageurl.PackageURL - wantErr bool - }{ - { - name: "Purl from String", - input: "pkg:maven/io.prestosql/presto-main@v1.0", - want: w, - }, - { - name: "Purl from String", - input: "pkg:npm/%40babel/core", - want: w2, - }, - { - name: "Empty String", - input: "", - want: w, - wantErr: true, - }, - { - name: "Rubbish String", - input: "rubbish.string", - want: w, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := PurlFromString(tt.input) - if (err != nil) != tt.wantErr { - t.Errorf("utils.PurlFromString() error = %v, wantErr %v", err, tt.wantErr) - return - } - t.Logf("Got: %v: '%v' '%v' '%v' '%v' '%v' '%v'", got, got.Type, got.Namespace, got.Name, got.Version, got.Qualifiers, got.Subpath) - t.Logf("Exp: %v: '%v' '%v' '%v' '%v' '%v' '%v'", tt.want, tt.want.Type, tt.want.Namespace, tt.want.Name, tt.want.Version, tt.want.Qualifiers, tt.want.Subpath) - if err == nil && !reflect.DeepEqual(got, tt.want) { - t.Errorf("utils.PurlFromString() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestPurlNameFromString(t *testing.T) { - tests := []struct { - name string - input string - want string - wantErr bool - }{ - { - name: "Maven", - input: "pkg:maven/io.prestosql/Presto-main@v1.0", - want: "io.prestosql/presto-main", - }, - { - name: "NPM1", - input: "pkg:npm/%40babel/Core", - want: "%40babel/Core", - }, - { - name: "NPM2", - input: "pkg:npm/%40babel/Core@7.0.0", - want: "%40babel/Core", - }, - { - name: "NPM3", - input: "pkg:npm/Core@0.0.1", - want: "Core", - }, - { - name: "Empty String", - input: "", - want: "", - wantErr: true, - }, - { - name: "Rubbish String", - input: "rubbish.string", - want: "", - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := PurlNameFromString(tt.input) - if (err != nil) != tt.wantErr { - t.Errorf("utils.PurlFromString() error = %v, wantErr %v", err, tt.wantErr) - return - } - if err == nil && !reflect.DeepEqual(got, tt.want) { - t.Errorf("utils.PurlFromString() = %v, want %v", got, tt.want) - } - }) - } -} - func TestPurlRemoveFromVersionComponent(t *testing.T) { tests := []struct { name string @@ -168,120 +62,3 @@ func TestPurlRemoveFromVersionComponent(t *testing.T) { }) } } - -func TestConvertPurlString(t *testing.T) { - tests := []struct { - name string - input string - want string - }{ - { - name: "Maven", - input: "pkg:maven/io.prestosql/presto-main@v1.0", - want: "pkg:maven/io.prestosql/presto-main@v1.0", - }, - { - name: "Golang1", - input: "pkg:golang/github.com/scanoss/papi", - want: "pkg:github/scanoss/papi", - }, - { - name: "Golang2", - input: "pkg:golang/github.com/scanoss/papi/v2", - want: "pkg:github/scanoss/papi", - }, - { - name: "Empty String", - input: "", - want: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := ConvertPurlString(tt.input) - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("utils.PurlFromString() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestPurlUrl(t *testing.T) { - tests := []struct { - name string - pname string - ptype string - want string - wantErr bool - }{ - { - name: "GitHub", - pname: "scanoss/scanoss.py", - ptype: "github", - want: "https://github.com/scanoss/scanoss.py", - }, - { - name: "Maven", - pname: "io.prestosql/presto-main", - ptype: "maven", - want: "https://mvnrepository.com/artifact/io.prestosql/presto-main", - }, - { - name: "NPM", - pname: "%40babel/core", - ptype: "npm", - want: "https://www.npmjs.com/package/%40babel/core", - }, - { - name: "PyPI", - pname: "scanoss", - ptype: "pypi", - want: "https://pypi.org/project/scanoss", - }, - { - name: "Gem", - pname: "tablestyle", - ptype: "gem", - want: "https://rubygems.org/gems/tablestyle", - }, - { - name: "Golang", - pname: "github.com/scanoss/papi", - ptype: "golang", - want: "https://pkg.go.dev/github.com/scanoss/papi", - }, - { - name: "Empty String1", - pname: "", - ptype: "gem", - want: "", - wantErr: true, - }, - { - name: "Empty String2", - pname: "io.prestosql/presto-main", - ptype: "", - want: "", - wantErr: true, - }, - { - name: "Rubbish String", - pname: "rubbish.string", - ptype: "rubbish.string", - want: "", - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ProjectURL(tt.pname, tt.ptype) - if (err != nil) != tt.wantErr { - t.Errorf("utils.ProjectURL() error = %v, wantErr %v", err, tt.wantErr) - return - } - if err == nil && !reflect.DeepEqual(got, tt.want) { - t.Errorf("utils.ProjectURL() = %v, want %v", got, tt.want) - } - }) - } -}