Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e7b8588
add functions to safely retrieve organization, project, and asset fro…
refoo0 Apr 15, 2026
d4f3843
add dependency proxy secrets model and migration scripts
refoo0 Apr 15, 2026
bafe189
add dependency proxy secret repository and service implementations
refoo0 Apr 15, 2026
abf3a7c
add dependency proxy repository and service to providers
refoo0 Apr 15, 2026
e3e03e8
add CheckNotAllowedPackage function and tests
refoo0 Apr 15, 2026
6edcd37
add dependency proxy controller to asset, org, and project routers
refoo0 Apr 15, 2026
8a3df65
add ShareDependencyProxyRouter and integrate into dependency proxy co…
refoo0 Apr 15, 2026
3f79556
rename ProvideDependencyProxyConfig to ProvideDependencyProxyCache
refoo0 Apr 15, 2026
6ed9da6
add dependency proxy base URL to environment configuration
refoo0 Apr 15, 2026
fa2a8dc
Merge remote-tracking branch 'origin/main' into add-dependency-proxy-…
refoo0 Apr 15, 2026
ac0a79c
update dependency proxy secret handling and improve error reporting
refoo0 Apr 15, 2026
c6af48f
fix lint
refoo0 Apr 15, 2026
8e8b7e9
add validation for disallowed package patterns in PyPI proxy and upda…
refoo0 Apr 15, 2026
0775e61
Update IsMalicious method to return an error for valid versions
refoo0 Apr 16, 2026
59ccc10
Enhance dependency proxy handling by improving path decoding and vali…
refoo0 Apr 17, 2026
fdd6491
Refactor Go proxy cache handling
refoo0 Apr 17, 2026
0cc01cc
Refactor package pattern matching logic and improve URL joining for N…
refoo0 Apr 17, 2026
09333d5
Merge remote-tracking branch 'origin/main' into add-dependency-proxy-…
refoo0 Apr 17, 2026
11b5eb2
Update dependency proxy configurations and enhance error handling
refoo0 Apr 17, 2026
c862c6d
Add mock for DependencyProxySecretService and update MaliciousPackage…
refoo0 Apr 17, 2026
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 .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ ALPINE_RELEASES_API=https://alpinelinux.org/releases.json


CSAF_PASSPHRASE=example-passphrase
DEPENDENCY_PROXY_BASE_URL=https://api.main.devguard.org/api/v1/dependency-proxy

# OpenTelemetry tracing
# Set to a value > 0 to enable tracing (e.g. 1.0 = 100%, 0.1 = 10%)
Expand Down
1 change: 1 addition & 0 deletions cmd/devguard-cli/commands/vulndb.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ func migrateDB() {
fx.Invoke(func(ShareRouter router.ShareRouter) {}),
fx.Invoke(func(VulnDBRouter router.VulnDBRouter) {}),
fx.Invoke(func(dependencyProxyRouter router.DependencyProxyRouter) {}),
fx.Invoke(func(shareDependencyProxyRouter router.ShareDependencyProxyRouter) {}),
fx.Invoke(func(lc fx.Lifecycle, server api.Server) {
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
Expand Down
1 change: 1 addition & 0 deletions cmd/devguard/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ func main() {
fx.Invoke(func(ShareRouter router.ShareRouter) {}),
fx.Invoke(func(VulnDBRouter router.VulnDBRouter) {}),
fx.Invoke(func(dependencyProxyRouter router.DependencyProxyRouter) {}),
fx.Invoke(func(shareDependencyProxyRouter router.ShareDependencyProxyRouter) {}),
fx.Invoke(func(FalsePositiveRuleRouter router.VEXRuleRouter) {}),
fx.Invoke(func(ExternalReferenceRouter router.ExternalReferenceRouter) {}),
fx.Invoke(func(lc fx.Lifecycle, server api.Server) {
Expand Down
782 changes: 680 additions & 102 deletions controllers/dependency_proxy_controller.go

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions controllers/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import (

var controllersTracer = otel.Tracer("devguard/controllers")

// ProvideDependencyProxyConfig creates the configuration for the dependency proxy
func ProvideDependencyProxyConfig() DependencyProxyConfig {
// ProvideDependencyProxyCache creates the configuration for the dependency proxy
func ProvideDependencyProxyCache() DependencyProxyCache {
var cacheDir string
dependencyProxyCacheDir := os.Getenv("DEPENDENCY_PROXY_CACHE_DIR")
if dependencyProxyCacheDir != "" {
Expand All @@ -47,7 +47,7 @@ func ProvideDependencyProxyConfig() DependencyProxyConfig {
slog.Error("Failed to create cache directory", "error", err)
}

return DependencyProxyConfig{
return DependencyProxyCache{
CacheDir: cacheDir,
}
}
Expand Down Expand Up @@ -108,7 +108,7 @@ var ControllerModule = fx.Options(
fx.Provide(NewScanController),

// Dependency Proxy
fx.Provide(ProvideDependencyProxyConfig),
fx.Provide(ProvideDependencyProxyCache),
fx.Provide(fx.Annotate(ProvideMaliciousPackageChecker, fx.As(new(shared.MaliciousPackageChecker)))),
fx.Provide(NewDependencyProxyController),
)
5 changes: 4 additions & 1 deletion controllers/vulndb_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,10 @@ func (c VulnDBController) PURLInspect(ctx shared.Context) error {
return echo.NewHTTPError(500, "failed to retrieve vulnerabilities for PURL").WithInternal(err)
}

_, maliciousPackage := c.maliciousPackageChecker.IsMalicious(ctx.Request().Context(), purl.Type, fmt.Sprintf("%s/%s", purl.Namespace, purl.Name), purl.Version)
_, maliciousPackage, err := c.maliciousPackageChecker.IsMalicious(ctx.Request().Context(), purl.Type, fmt.Sprintf("%s/%s", purl.Namespace, purl.Name), purl.Version)
if err != nil {
return echo.NewHTTPError(400, "failed to check if package is malicious").WithInternal(err)
}
Comment thread
refoo0 marked this conversation as resolved.

var componentDTO *dtos.ComponentDTO
comp := models.Component{ID: purlString}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- Copyright (C) 2026 l3montree GmbH
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as
-- published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License
-- along with this program. If not, see <https://www.gnu.org/licenses/>.

DROP TABLE IF EXISTS public.dependency_proxy_secrets;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
-- Copyright (C) 2026 l3montree GmbH
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as
-- published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License
-- along with this program. If not, see <https://www.gnu.org/licenses/>.

CREATE TABLE IF NOT EXISTS public.dependency_proxy_secrets (
secret uuid DEFAULT gen_random_uuid() PRIMARY KEY,
asset_id uuid,
project_id uuid,
org_id uuid,
CONSTRAINT dependency_proxy_secrets_exactly_one_scope CHECK (
((asset_id IS NOT NULL)::int + (project_id IS NOT NULL)::int + (org_id IS NOT NULL)::int) = 1
)
);
CREATE UNIQUE INDEX IF NOT EXISTS dependency_proxy_secrets_unique_asset_id
ON public.dependency_proxy_secrets (asset_id)
WHERE asset_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS dependency_proxy_secrets_unique_project_id
ON public.dependency_proxy_secrets (project_id)
WHERE project_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS dependency_proxy_secrets_unique_org_id
ON public.dependency_proxy_secrets (org_id)
WHERE org_id IS NOT NULL;
31 changes: 31 additions & 0 deletions database/models/dependency_proxy_secret_model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (C) 2026 l3montree GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

package models

import (
"github.com/google/uuid"
)

type DependencyProxySecret struct {
Secret uuid.UUID `gorm:"type:uuid;primaryKey; default:gen_random_uuid()"`
AssetID *uuid.UUID `gorm:"type:uuid;"`
ProjectID *uuid.UUID `gorm:"type:uuid;"`
OrgID *uuid.UUID `gorm:"type:uuid;"`
}

func (DependencyProxySecret) TableName() string {
return "dependency_proxy_secrets"
}
123 changes: 123 additions & 0 deletions database/repositories/dependency_proxy_secret_repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright (C) 2026 l3montree GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package repositories

import (
"context"

"github.com/google/uuid"
"github.com/l3montree-dev/devguard/database/models"
"github.com/l3montree-dev/devguard/utils"
"gorm.io/gorm"
)

type dependencyProxySecretRepository struct {
db *gorm.DB
utils.Repository[uuid.UUID, models.DependencyProxySecret, *gorm.DB]
}

func NewDependencyProxyRepository(db *gorm.DB) *dependencyProxySecretRepository {
return &dependencyProxySecretRepository{db: db, Repository: newGormRepository[uuid.UUID, models.DependencyProxySecret](db)}
}

func (r *dependencyProxySecretRepository) GetOrCreateByOrgID(ctx context.Context, tx *gorm.DB, orgID uuid.UUID) (models.DependencyProxySecret, error) {
var proxy models.DependencyProxySecret
err := r.db.WithContext(ctx).Where("org_id = ?", orgID).First(&proxy).Error
//check if not exists, then create a new one
if err != nil {
if err == gorm.ErrRecordNotFound {
newProxy := models.DependencyProxySecret{
OrgID: &orgID,
}

err = r.Create(ctx, r.db, &newProxy)
return newProxy, err
Comment thread
refoo0 marked this conversation as resolved.
}
return proxy, err
}
return proxy, err
}

func (r *dependencyProxySecretRepository) GetOrCreateByProjectID(ctx context.Context, tx *gorm.DB, projectID uuid.UUID) (models.DependencyProxySecret, error) {
var proxy models.DependencyProxySecret
err := r.db.WithContext(ctx).Where("project_id = ?", projectID).First(&proxy).Error
//check if not exists, then create a new one
if err != nil {
if err == gorm.ErrRecordNotFound {
newProxy := models.DependencyProxySecret{
ProjectID: &projectID,
}
err = r.Create(ctx, r.db, &newProxy)
return newProxy, err
}
return proxy, err
}
return proxy, err
}

func (r *dependencyProxySecretRepository) GetOrCreateByAssetID(ctx context.Context, tx *gorm.DB, assetID uuid.UUID) (models.DependencyProxySecret, error) {
var proxy models.DependencyProxySecret
err := r.db.WithContext(ctx).Where("asset_id = ?", assetID).First(&proxy).Error
//check if not exists, then create a new one
if err != nil {
if err == gorm.ErrRecordNotFound {
newProxy := models.DependencyProxySecret{
AssetID: &assetID,
}
err = r.Create(ctx, r.db, &newProxy)
return newProxy, err
}
return proxy, err
}
return proxy, err
}

func (r *dependencyProxySecretRepository) UpdateSecret(ctx context.Context, tx *gorm.DB, proxy models.DependencyProxySecret) (models.DependencyProxySecret, error) {
exec := func(db *gorm.DB) error {
newSecret := uuid.New()
if err := db.Delete(&models.DependencyProxySecret{}, "secret = ?", proxy.Secret).Error; err != nil {
return err
}
proxy.Secret = newSecret
if err := db.Create(&proxy).Error; err != nil {
return err
}
return nil
}
var err error
if tx != nil {
err = exec(tx.WithContext(ctx))
} else {
err = r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return exec(tx)
})
}
if err != nil {
return proxy, err
}

return proxy, nil
}

func (r *dependencyProxySecretRepository) GetBySecret(ctx context.Context, tx *gorm.DB, secret uuid.UUID) (models.DependencyProxySecret, error) {

var proxy models.DependencyProxySecret

if err := r.db.WithContext(ctx).Where("secret = ?", secret).First(&proxy).Error; err != nil {
return proxy, err
}

return proxy, nil
}
1 change: 1 addition & 0 deletions database/repositories/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,5 @@ var Module = fx.Options(
fx.Provide(fx.Annotate(NewVEXRuleRepository, fx.As(new(shared.VEXRuleRepository)))),
fx.Provide(fx.Annotate(NewExternalReferenceRepository, fx.As(new(shared.ExternalReferenceRepository)))),
fx.Provide(fx.Annotate(NewTrustedEntityRepository, fx.As(new(shared.TrustedEntityRepository)))),
fx.Provide(fx.Annotate(NewDependencyProxyRepository, fx.As(new(shared.DependencyProxySecretRepository)))),
)
Loading
Loading