Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/devguard-cli/commands/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ func runPipelineForAsset(assetIDStr, assetVersionSlug string) error {
integrations.Module,
vulndb.Module,
daemons.Module,
fixedversion.Module,
fx.Populate(&daemonRunner, &dependencyVulnRepository, &dependencyVulnService, &assetRepository, &assetVersionRepository),
)

Expand Down
29 changes: 12 additions & 17 deletions controllers/dependency_vuln_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/json"
"log/slog"
"slices"
"strings"
"time"

"go.opentelemetry.io/otel/trace"
Expand Down Expand Up @@ -495,43 +494,39 @@ func (controller DependencyVulnController) BatchCreateEvent(ctx shared.Context)

func (controller DependencyVulnController) GetRecommendation(ctx echo.Context) error {
packageName := ctx.QueryParam("packageName")
if packageName == "" {
packageName = ctx.QueryParam("depName")
}

if packageName == "" {
return echo.NewHTTPError(400, "missing packageName or depName")
}

currentValue := ctx.QueryParam("packageValue")
if currentValue == "" {
currentValue = ctx.QueryParam("currentValue")
}
currentValue := ctx.QueryParam("currentValue")

if currentValue == "" {
return echo.NewHTTPError(400, "missing packageValue or currentValue")
}

recommendedVersion, err := controller.dependencyVulnService.GetDirectDependencyFixedVersionByPackageName(ctx.Request().Context(), packageName)
recommendedVersion, err := controller.dependencyVulnRepository.GetDirectDependencyFixedVersionByPackageName(ctx.Request().Context(), nil, packageName)
if err != nil {
return echo.NewHTTPError(500, "could not get recommendation").WithInternal(err)
}
if recommendedVersion == nil || *recommendedVersion == "" {
return ctx.JSON(200, dtos.Recommendation{RecommendedVersion: ""})
}

version := extractVersionFromPURL(*recommendedVersion)
version, err := extractVersionFromPURL(*recommendedVersion)
if err != nil {
slog.Error("could not extract version from purl", "err", err, "recommendedVersion", *recommendedVersion)
return echo.NewHTTPError(500, "could not get recommendation").WithInternal(err)
}

return ctx.JSON(200, dtos.Recommendation{RecommendedVersion: version})
}

func extractVersionFromPURL(input string) string {
if !strings.HasPrefix(input, "pkg:") {
return input
}

func extractVersionFromPURL(input string) (string, error) {
parsed, err := packageurl.FromString(input)
if err != nil {
return input
return "", err
}

return parsed.Version
return parsed.Version, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Recovery migration placeholder for missing historical version.
-- Intentionally left empty.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Recovery migration placeholder for missing historical version.
-- Intentionally left empty to unblock environments that already reference
-- migration version 20260304170435 in schema_migrations.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
ALTER TABLE public.artifact_risk_history
DROP COLUMN IF EXISTS cve_purl_fixable_critical,
DROP COLUMN IF EXISTS cve_purl_fixable_high,
DROP COLUMN IF EXISTS cve_purl_fixable_medium,
DROP COLUMN IF EXISTS cve_purl_fixable_low,
DROP COLUMN IF EXISTS fixable_critical,
DROP COLUMN IF EXISTS fixable_high,
DROP COLUMN IF EXISTS fixable_medium,
DROP COLUMN IF EXISTS fixable_low;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
ALTER TABLE public.artifact_risk_history
ADD COLUMN IF NOT EXISTS fixable_low INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS fixable_medium INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS fixable_high INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS fixable_critical INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS cve_purl_fixable_low INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS cve_purl_fixable_medium INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS cve_purl_fixable_high INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS cve_purl_fixable_critical INTEGER DEFAULT 0;

10 changes: 10 additions & 0 deletions database/models/statistic_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ type Distribution struct {
Medium int `json:"medium"`
Critical int `json:"critical"`

FixableLow int `json:"fixableLow"`
FixableMedium int `json:"fixableMedium"`
FixableHigh int `json:"fixableHigh"`
FixableCritical int `json:"fixableCritical"`

LowCVSS int `json:"lowCvss"`
MediumCVSS int `json:"mediumCvss"`
HighCVSS int `json:"highCvss"`
Expand All @@ -37,6 +42,11 @@ type Distribution struct {
CVEPurlMediumCVSS int `json:"cvePurlMediumCvss"`
CVEPurlHighCVSS int `json:"cvePurlHighCvss"`
CVEPurlCriticalCVSS int `json:"cvePurlCriticalCvss"`

CVEPurlFixableLow int `json:"cvePurlFixableLow"`
CVEPurlFixableMedium int `json:"cvePurlFixableMedium"`
CVEPurlFixableHigh int `json:"cvePurlFixableHigh"`
CVEPurlFixableCritical int `json:"cvePurlFixableCritical"`
}

type History struct {
Expand Down
4 changes: 4 additions & 0 deletions database/repositories/artifact_risk_history_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ func (r *artifactRiskHistoryRepository) GetRiskHistoryByRelease(ctx context.Cont
arh.sum_closed_risk, arh.avg_closed_risk, arh.max_closed_risk, arh.min_closed_risk,
arh.open_dependency_vulns, arh.fixed_dependency_vulns,
arh.low, arh.medium, arh.high, arh.critical,
arh.fixable_low, arh.fixable_medium, arh.fixable_high, arh.fixable_critical,
arh.cve_purl_low, arh.cve_purl_medium, arh.cve_purl_high, arh.cve_purl_critical,
arh.cve_purl_fixable_low, arh.cve_purl_fixable_medium, arh.cve_purl_fixable_high, arh.cve_purl_fixable_critical,
arh.low_cvss, arh.medium_cvss, arh.high_cvss, arh.critical_cvss,
arh.cve_purl_low_cvss, arh.cve_purl_medium_cvss, arh.cve_purl_high_cvss, arh.cve_purl_critical_cvss
FROM artifact_risk_history arh
Expand All @@ -98,8 +100,10 @@ func (r *artifactRiskHistoryRepository) GetRiskHistoryForOrg(ctx context.Context
SELECT
day,
SUM(low) low, SUM(medium) medium, SUM(high) high, SUM(critical) critical,
SUM(fixable_low) fixable_low, SUM(fixable_medium) fixable_medium, SUM(fixable_high) fixable_high, SUM(fixable_critical) fixable_critical,
SUM(low_cvss) low_cvss, SUM(medium_cvss) medium_cvss, SUM(high_cvss) high_cvss, SUM(critical_cvss) critical_cvss,
SUM(cve_purl_low) cve_purl_low, SUM(cve_purl_medium) cve_purl_medium, SUM(cve_purl_high) cve_purl_high, SUM(cve_purl_critical) cve_purl_critical,
SUM(cve_purl_fixable_low) cve_purl_fixable_low, SUM(cve_purl_fixable_medium) cve_purl_fixable_medium, SUM(cve_purl_fixable_high) cve_purl_fixable_high, SUM(cve_purl_fixable_critical) cve_purl_fixable_critical,
SUM(cve_purl_low_cvss) cve_purl_low_cvss, SUM(cve_purl_medium_cvss) cve_purl_medium_cvss, SUM(cve_purl_high_cvss) cve_purl_high_cvss, SUM(cve_purl_critical_cvss) cve_purl_critical_cvss
FROM
artifact_risk_history a
Expand Down
2 changes: 1 addition & 1 deletion database/repositories/dependency_vuln_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ func (repository *dependencyVulnRepository) GetDirectDependencyFixedVersionByPac
WithContext(ctx).
Table("dependency_vulns").
Where("direct_dependency_fixed_version IS NOT NULL AND direct_dependency_fixed_version != ''").
Where("EXISTS (SELECT 1 FROM jsonb_array_elements(vulnerability_path) AS purl WHERE purl::text LIKE '%/' || ? || '@%')", packageName).
Where("vulnerability_path ->> 0 LIKE '%/' || ? || '@%'", packageName).
Select("direct_dependency_fixed_version").
Order("last_detected DESC").
Limit(1).
Expand Down
23 changes: 16 additions & 7 deletions database/repositories/statistics_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,23 +37,32 @@ func (r *statisticsRepository) TimeTravelDependencyVulnState(ctx context.Context
dependencyVulns := []models.DependencyVuln{}
var err error
if artifactName == nil && assetVersionName == nil {
err = r.GetDB(ctx, tx).Model(&models.DependencyVuln{}).Preload("CVE").Preload("Events", func(db *gorm.DB) *gorm.DB {
err = r.GetDB(ctx, tx).Model(&models.DependencyVuln{}).Select("dependency_vulns.*").Preload("CVE").Preload("Events", func(db *gorm.DB) *gorm.DB {
return db.Where("created_at <= ?", time).Order("created_at ASC")
}).
Joins("JOIN artifact_dependency_vulns adv ON adv.dependency_vuln_id = dependency_vulns.id").
Where("dependency_vulns.asset_id = ?", assetID).Where("created_at <= ?", time).
Find(&dependencyVulns).Error
} else if artifactName != nil {
err = r.GetDB(ctx, tx).Model(&models.DependencyVuln{}).Preload("CVE").Preload("Events", func(db *gorm.DB) *gorm.DB {
} else if artifactName != nil && assetVersionName == nil {
err = r.GetDB(ctx, tx).Model(&models.DependencyVuln{}).Select("dependency_vulns.*").Preload("CVE").Preload("Events", func(db *gorm.DB) *gorm.DB {
return db.Where("created_at <= ?", time).Order("created_at ASC")
}).
Joins("JOIN artifact_dependency_vulns adv ON adv.dependency_vuln_id = dependency_vulns.id").
Where("adv.artifact_asset_version_name = ?", *assetVersionName).Where("adv.artifact_asset_id = ?", assetID).Where("adv.artifact_artifact_name = ?", artifactName).Where("created_at <= ?", time).
Joins("JOIN artifact_dependency_vulns adv ON adv.dependency_vuln_id = dependency_vulns.id").Where("adv.artifact_asset_id = ?", assetID).Where("adv.artifact_artifact_name = ?", *artifactName).Where("created_at <= ?", time).
Find(&dependencyVulns).Error
} else if artifactName == nil && assetVersionName != nil {
err = r.GetDB(ctx, tx).Model(&models.DependencyVuln{}).Select("dependency_vulns.*").Preload("CVE").Preload("Events", func(db *gorm.DB) *gorm.DB {
return db.Where("created_at <= ?", time).Order("created_at ASC")
}).
Joins("JOIN artifact_dependency_vulns adv ON adv.dependency_vuln_id = dependency_vulns.id").Where("adv.artifact_asset_id = ?", assetID).Where("adv.asset_version_name = ?", *assetVersionName).Where("created_at <= ?", time).
Find(&dependencyVulns).Error
Comment on lines +47 to 57
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch dereferences *assetVersionName but the branch condition only checks artifactName != nil. If callers pass an artifact name without an asset version name, this will panic. Consider either (1) requiring assetVersionName != nil in this branch and returning an error otherwise, or (2) adjusting the query to not dereference assetVersionName when it is nil.

Copilot uses AI. Check for mistakes.
} else {
err = r.GetDB(ctx, tx).Model(&models.DependencyVuln{}).Preload("CVE").Preload("Events", func(db *gorm.DB) *gorm.DB {
// both defined
err = r.GetDB(ctx, tx).Model(&models.DependencyVuln{}).Select("dependency_vulns.*").Preload("CVE").Preload("Events", func(db *gorm.DB) *gorm.DB {
return db.Where("created_at <= ?", time).Order("created_at ASC")
}).Where("adv.artifact_asset_id = ?", assetID).Where("adv.artifact_artifact_name = ?", artifactName).Where("created_at <= ?", time).
}).
Joins("JOIN artifact_dependency_vulns adv ON adv.dependency_vuln_id = dependency_vulns.id").
Where("adv.artifact_asset_id = ?", assetID).
Where("adv.asset_version_name = ?", *assetVersionName).Where("adv.artifact_artifact_name = ?", *artifactName).Where("created_at <= ?", time).
Find(&dependencyVulns).Error
}
if err != nil {
Expand Down
10 changes: 10 additions & 0 deletions dtos/statistics_dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ type Distribution struct {
Medium int `json:"medium"`
Critical int `json:"critical"`

FixableLow int `json:"fixableLow"`
FixableMedium int `json:"fixableMedium"`
FixableHigh int `json:"fixableHigh"`
FixableCritical int `json:"fixableCritical"`

LowCVSS int `json:"lowCvss"`
MediumCVSS int `json:"mediumCvss"`
HighCVSS int `json:"highCvss"`
Expand All @@ -52,6 +57,11 @@ type Distribution struct {
CVEPurlMediumCVSS int `json:"cvePurlMediumCvss"`
CVEPurlHighCVSS int `json:"cvePurlHighCvss"`
CVEPurlCriticalCVSS int `json:"cvePurlCriticalCvss"`

CVEPurlFixableLow int `json:"cvePurlFixableLow"`
CVEPurlFixableMedium int `json:"cvePurlFixableMedium"`
CVEPurlFixableHigh int `json:"cvePurlFixableHigh"`
CVEPurlFixableCritical int `json:"cvePurlFixableCritical"`
}

type History struct {
Expand Down
68 changes: 0 additions & 68 deletions mocks/mock_DependencyVulnService.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions services/dependency_vuln_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,3 @@ func (s *DependencyVulnService) GetAllUniqueCVEsForAsset(ctx context.Context, as
}
return allVulns, nil
}

func (s *DependencyVulnService) GetDirectDependencyFixedVersionByPackageName(ctx context.Context, packageName string) (*string, error) {
return s.dependencyVulnRepository.GetDirectDependencyFixedVersionByPackageName(ctx, nil, packageName)
}
Loading
Loading