diff --git a/.env.example b/.env.example index 3a140a58..e5666ce5 100644 --- a/.env.example +++ b/.env.example @@ -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%) diff --git a/cmd/devguard-cli/commands/vulndb.go b/cmd/devguard-cli/commands/vulndb.go index ef78989d..95f089a0 100644 --- a/cmd/devguard-cli/commands/vulndb.go +++ b/cmd/devguard-cli/commands/vulndb.go @@ -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 { diff --git a/cmd/devguard/main.go b/cmd/devguard/main.go index 62e32231..3446e752 100644 --- a/cmd/devguard/main.go +++ b/cmd/devguard/main.go @@ -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) { diff --git a/controllers/dependency_proxy_controller.go b/controllers/dependency_proxy_controller.go index 2bd29332..3f80325a 100644 --- a/controllers/dependency_proxy_controller.go +++ b/controllers/dependency_proxy_controller.go @@ -13,12 +13,14 @@ import ( "io" "log/slog" "net/http" + "net/url" "os" "path/filepath" "regexp" "strings" "time" + "github.com/google/uuid" "github.com/l3montree-dev/devguard/shared" "github.com/l3montree-dev/devguard/utils" "github.com/labstack/echo/v4" @@ -44,23 +46,50 @@ const ( PyPIProxy ProxyType = "pypi" ) -type DependencyProxyConfig struct { +type DependencyProxyCache struct { CacheDir string } +type DependencyProxyConfigs struct { + Rules []string `json:"rules"` + MinReleaseTime int `json:"minReleaseTime"` // in hours +} type DependencyProxyController struct { - maliciousChecker shared.MaliciousPackageChecker - cacheDir string - client *http.Client + assetRepository shared.AssetRepository + projectRepository shared.ProjectRepository + orgRepository shared.OrganizationRepository + dependencyProxyService shared.DependencyProxySecretService + maliciousChecker shared.MaliciousPackageChecker + cacheDir string + client *http.Client +} + +// trimProxyPrefix strips the /api/v1/dependency-proxy/[secret/] prefix from the path. +// The secret segment is optional to support routes with and without a secret. +func TrimProxyPrefix(path string, ecosystem ProxyType) string { + encodedPackage := regexp.MustCompile(`^/api/v1/dependency-proxy/(?:[^/]+/)?`+regexp.QuoteMeta(string(ecosystem))+`(?:/|$)`).ReplaceAllString(path, "") + decodedPackage, err := url.PathUnescape(encodedPackage) + if err != nil { + return encodedPackage + } + return decodedPackage } func NewDependencyProxyController( - config DependencyProxyConfig, + dependencyProxyService shared.DependencyProxySecretService, + config DependencyProxyCache, maliciousChecker shared.MaliciousPackageChecker, + assetRepository shared.AssetRepository, + projectRepository shared.ProjectRepository, + orgRepository shared.OrganizationRepository, ) *DependencyProxyController { return &DependencyProxyController{ - maliciousChecker: maliciousChecker, - cacheDir: config.CacheDir, + dependencyProxyService: dependencyProxyService, + maliciousChecker: maliciousChecker, + cacheDir: config.CacheDir, + assetRepository: assetRepository, + projectRepository: projectRepository, + orgRepository: orgRepository, client: &http.Client{ Timeout: 60 * time.Second, Transport: utils.EgressTransport, @@ -69,8 +98,16 @@ func NewDependencyProxyController( } func (d *DependencyProxyController) ProxyNPM(c shared.Context) error { + + configs, err := d.GetDependencyProxyConfigs(c) + if err != nil { + slog.Error("Error getting dependency proxy configs", "error", err) + } + + path := c.Request().URL.Path + // Get the full path after the prefix - requestPath := strings.TrimPrefix(c.Request().URL.Path, "/api/v1/dependency-proxy/npm") + requestPath := TrimProxyPrefix(path, NPMProxy) ctx, span := depProxyTracer.Start(c.Request().Context(), "dependency-proxy.npm", trace.WithAttributes( @@ -89,41 +126,68 @@ func (d *DependencyProxyController) ProxyNPM(c shared.Context) error { slog.Info("Proxy request", "proxy", "npm", "method", c.Request().Method, "path", requestPath) + cachePath := d.getCachePath(NPMProxy, requestPath) + + packageName, version := d.ParsePackageFromPath(NPMProxy, requestPath) + hasExplicitVersion := version != "" || strings.HasSuffix(requestPath, ".tgz") + // Check for malicious packages // For requests with explicit versions (e.g., .tgz files or package@version), check immediately // For metadata requests (package info without version), we need to fetch first to see which version would be used - packageName, version := d.ParsePackageFromPath(NPMProxy, requestPath) - hasExplicitVersion := version != "" || strings.HasSuffix(requestPath, ".tgz") - if blocked, reason := d.checkMaliciousPackage(ctx, NPMProxy, requestPath); blocked { - slog.Warn("Blocked malicious package", "proxy", "npm", "path", requestPath, "reason", reason) - // Also remove from cache if it exists to prevent serving cached malicious content - cachePath := d.getCachePath(NPMProxy, requestPath) - if err := os.Remove(cachePath); err == nil { - slog.Info("Removed malicious package from cache", "path", cachePath) + if hasExplicitVersion { + + notAllowed, notAllowedReason := d.CheckNotAllowedPackage(ctx, NPMProxy, requestPath, configs) + + if notAllowed { + return d.blockNotAllowedPackage(c, NPMProxy, requestPath, notAllowedReason) } - return d.blockMaliciousPackage(c, NPMProxy, requestPath, reason) - } - cachePath := d.getCachePath(NPMProxy, requestPath) + hasMalicious, reason := d.checkMaliciousPackage(ctx, NPMProxy, requestPath) + + if hasMalicious { + slog.Warn("Blocked malicious package", "proxy", "npm", "path", requestPath, "reason", reason) + // Also remove from cache if it exists to prevent serving cached malicious content + cachePath := d.getCachePath(NPMProxy, requestPath) + if err := os.Remove(cachePath); err == nil { + slog.Info("Removed malicious package from cache", "path", cachePath) + } + return d.blockMaliciousPackage(c, NPMProxy, requestPath, reason) + } - // Check cache - if d.isNPMCached(cachePath) { - slog.Debug("Cache hit", "proxy", "npm", "path", requestPath) - data, err := os.ReadFile(cachePath) - if err == nil { - // Verify cache integrity - if d.VerifyCacheIntegrity(cachePath, data) { - span.SetAttributes(attribute.Bool("proxy.cache_hit", true)) - return d.writeNPMResponse(c, data, requestPath, true) + // Check cache + if d.isNPMCached(cachePath) { + slog.Debug("Cache hit", "proxy", "npm", "path", requestPath) + data, err := os.ReadFile(cachePath) + if err == nil { + // Verify cache integrity + if d.VerifyCacheIntegrity(cachePath, data) { + if configs.MinReleaseTime > 0 { + if releaseTime, ok := d.ReadCachedReleaseTime(cachePath); ok { + if time.Since(releaseTime) > time.Duration(configs.MinReleaseTime)*time.Hour { + return d.blockTooNewPackage(c, NPMProxy, requestPath, releaseTime, configs.MinReleaseTime) + } + span.SetAttributes(attribute.Bool("proxy.cache_hit", true)) + return d.writeNPMResponse(c, data, requestPath, true) + } + // No cached release time - fall through to upstream to retrieve it + slog.Debug("No cached release time for MinReleaseTime check, refetching", "proxy", "npm", "path", requestPath) + } else { + span.SetAttributes(attribute.Bool("proxy.cache_hit", true)) + return d.writeNPMResponse(c, data, requestPath, true) + } + } else { + slog.Warn("Cache integrity verification failed, refetching", "proxy", "npm", "path", requestPath) + // Remove corrupted cache + os.Remove(cachePath) + os.Remove(cachePath + ".sha256") + } + } else { + slog.Warn("Cache read error", "proxy", "npm", "error", err) } - slog.Warn("Cache integrity verification failed, refetching", "proxy", "npm", "path", requestPath) - // Remove corrupted cache - os.Remove(cachePath) - os.Remove(cachePath + ".sha256") } - slog.Warn("Cache read error", "proxy", "npm", "error", err) + } span.SetAttributes(attribute.Bool("proxy.cache_hit", false)) @@ -148,12 +212,25 @@ func (d *DependencyProxyController) ProxyNPM(c shared.Context) error { return c.Blob(statusCode, headers.Get("Content-Type"), data) } + resolvedVersion, releaseTime := d.ExtractNPMVersionAndReleaseTimeFromMetadata(data) + // For metadata requests without explicit version, check the resolved version against malicious database - if d.maliciousChecker != nil && !hasExplicitVersion && packageName != "" { + if !hasExplicitVersion { + //check allowlist patterns before checking malicious database to prevent false positives on allowed packages + notAllowed, notAllowedReason := d.CheckNotAllowedPackage(ctx, NPMProxy, packageName+"@"+resolvedVersion, configs) + if notAllowed { + return d.blockNotAllowedPackage(c, NPMProxy, requestPath, notAllowedReason) + } + // Parse the JSON response to extract the version that would be installed - if resolvedVersion := d.ExtractNPMVersionFromMetadata(data); resolvedVersion != "" { + if resolvedVersion != "" && d.maliciousChecker != nil { slog.Debug("Checking resolved version for malicious package", "package", packageName, "version", resolvedVersion) - isMalicious, entry := d.maliciousChecker.IsMalicious(ctx, "npm", packageName, resolvedVersion) + isMalicious, entry, err := d.maliciousChecker.IsMalicious(ctx, "npm", packageName, resolvedVersion) + if err != nil { + slog.Error("Error checking malicious package", "proxy", "npm", "error", err) + return echo.NewHTTPError(500, "failed to check if package is malicious").WithInternal(err) + } + if isMalicious { reason := fmt.Sprintf("Package %s@%s is flagged as malicious (ID: %s)", packageName, resolvedVersion, entry.ID) if entry.Summary != "" { @@ -165,11 +242,25 @@ func (d *DependencyProxyController) ProxyNPM(c shared.Context) error { } } + // Check MinReleaseTime for metadata responses (non-tgz) + if configs.MinReleaseTime > 0 && !hasExplicitVersion && packageName != "" { + if time.Since(releaseTime) > time.Duration(configs.MinReleaseTime)*time.Hour { + return d.blockTooNewPackage(c, NPMProxy, requestPath, releaseTime, configs.MinReleaseTime) + } + + } + // Cache successful responses with integrity verification if err := d.CacheDataWithIntegrity(cachePath, data); err != nil { slog.Warn("Failed to cache response", "proxy", "npm", "error", err) } + // Store release time so MinReleaseTime can be enforced on future cache hits + + if err := d.CacheReleaseTime(cachePath, releaseTime); err != nil { + slog.Warn("Failed to cache release time", "proxy", "npm", "error", err) + } + // Copy important headers from upstream if contentType := headers.Get("Content-Type"); contentType != "" { c.Response().Header().Set("Content-Type", contentType) @@ -223,8 +314,9 @@ func (d *DependencyProxyController) ProxyNPMAudit(c shared.Context) error { } func (d *DependencyProxyController) ProxyGo(c shared.Context) error { + path := c.Request().URL.Path // Get the full path after the prefix - requestPath := strings.TrimPrefix(c.Request().URL.Path, "/api/v1/dependency-proxy/go") + requestPath := TrimProxyPrefix(path, GoProxy) ctx, span := depProxyTracer.Start(c.Request().Context(), "dependency-proxy.go", trace.WithAttributes( @@ -241,39 +333,72 @@ func (d *DependencyProxyController) ProxyGo(c shared.Context) error { return echo.NewHTTPError(http.StatusMethodNotAllowed, "Method not allowed") } + configs, err := d.GetDependencyProxyConfigs(c) + if err != nil { + slog.Error("Error getting dependency proxy configs", "error", err) + } + slog.Info("Proxy request", "proxy", "go", "method", c.Request().Method, "path", requestPath) - // Check for malicious packages BEFORE checking cache to prevent cache poisoning - if d.maliciousChecker != nil { - if blocked, reason := d.checkMaliciousPackage(c.Request().Context(), GoProxy, requestPath); blocked { - slog.Warn("Blocked malicious package", "proxy", "go", "path", requestPath, "reason", reason) - // Also remove from cache if it exists to prevent serving cached malicious content - cachePath := d.getCachePath(GoProxy, requestPath) - if err := os.Remove(cachePath); err == nil { - slog.Info("Removed malicious package from cache", "path", cachePath) - } - return d.blockMaliciousPackage(c, GoProxy, requestPath, reason) + cachePath := d.getCachePath(GoProxy, requestPath) + + packageName, version := d.ParsePackageFromPath(GoProxy, requestPath) + hasExplicitVersion := version != "" + + if hasExplicitVersion { + + //check config for not allowed patterns before doing anything else to fail fast and avoid unnecessary processing + notAllowed, notAllowedReason := d.CheckNotAllowedPackage(ctx, GoProxy, requestPath, configs) + if notAllowed { + return d.blockNotAllowedPackage(c, GoProxy, requestPath, notAllowedReason) } - } - cachePath := d.getCachePath(GoProxy, requestPath) + // Check for malicious packages BEFORE checking cache to prevent cache poisoning + if d.maliciousChecker != nil { + if blocked, reason := d.checkMaliciousPackage(c.Request().Context(), GoProxy, requestPath); blocked { + slog.Warn("Blocked malicious package", "proxy", "go", "path", requestPath, "reason", reason) + // Also remove from cache if it exists to prevent serving cached malicious content + cachePath := d.getCachePath(GoProxy, requestPath) + if err := os.Remove(cachePath); err == nil { + slog.Info("Removed malicious package from cache", "path", cachePath) + } + return d.blockMaliciousPackage(c, GoProxy, requestPath, reason) + } + } - // Check cache - if d.isGoCached(cachePath) { - slog.Debug("Cache hit", "proxy", "go", "path", requestPath) - data, err := os.ReadFile(cachePath) - if err == nil { - // Verify cache integrity - if d.VerifyCacheIntegrity(cachePath, data) { - span.SetAttributes(attribute.Bool("proxy.cache_hit", true)) - return d.writeGoResponse(c, data, requestPath, true) + // Check cache + + if d.isGoCached(cachePath) { + slog.Debug("Cache hit", "proxy", "go", "path", requestPath) + data, err := os.ReadFile(cachePath) + if err == nil { + // Verify cache integrity + if d.VerifyCacheIntegrity(cachePath, data) { + if configs.MinReleaseTime > 0 { + if releaseTime, ok := d.ReadCachedReleaseTime(cachePath); ok { + if time.Since(releaseTime) > time.Duration(configs.MinReleaseTime)*time.Hour { + return d.blockTooNewPackage(c, GoProxy, requestPath, releaseTime, configs.MinReleaseTime) + } + span.SetAttributes(attribute.Bool("proxy.cache_hit", true)) + return d.writeGoResponse(c, data, requestPath, true) + } + // No cached release time - fall through to upstream to retrieve it + slog.Debug("No cached release time for MinReleaseTime check, refetching", "proxy", "go", "path", requestPath) + } else { + span.SetAttributes(attribute.Bool("proxy.cache_hit", true)) + return d.writeGoResponse(c, data, requestPath, true) + } + } else { + slog.Warn("Cache integrity verification failed, refetching", "proxy", "go", "path", requestPath) + // Remove corrupted cache + os.Remove(cachePath) + os.Remove(cachePath + ".sha256") + } + } else { + slog.Warn("Cache read error", "proxy", "go", "error", err) } - slog.Warn("Cache integrity verification failed, refetching", "proxy", "go", "path", requestPath) - // Remove corrupted cache - os.Remove(cachePath) - os.Remove(cachePath + ".sha256") } - slog.Warn("Cache read error", "proxy", "go", "error", err) + } span.SetAttributes(attribute.Bool("proxy.cache_hit", false)) @@ -298,11 +423,55 @@ func (d *DependencyProxyController) ProxyGo(c shared.Context) error { return c.Blob(statusCode, headers.Get("Content-Type"), data) } + resolvedVersion, releaseTime, hasReleaseTime := d.ExtractGoVersionAndReleaseTime(data) + + // For requests without an explicit version (e.g. /@latest), use the same logic as npm proxy: + // check the resolved version against the allowlist rules and malicious database + if !hasExplicitVersion && resolvedVersion != "" { + notAllowed, notAllowedReason := d.CheckNotAllowedPackage(ctx, GoProxy, packageName+"@"+resolvedVersion, configs) + if notAllowed { + return d.blockNotAllowedPackage(c, GoProxy, requestPath, notAllowedReason) + } + + if d.maliciousChecker != nil { + slog.Debug("Checking resolved version for malicious package", "package", packageName, "version", resolvedVersion) + isMalicious, entry, err := d.maliciousChecker.IsMalicious(ctx, "go", packageName, resolvedVersion) + if err != nil { + slog.Error("Error checking malicious package", "proxy", "go", "error", err) + return echo.NewHTTPError(500, "failed to check if package is malicious").WithInternal(err) + } + if isMalicious { + reason := fmt.Sprintf("Package %s@%s is flagged as malicious (ID: %s)", packageName, resolvedVersion, entry.ID) + if entry.Summary != "" { + reason += ": " + entry.Summary + } + slog.Warn("Blocked malicious package after version resolution", "proxy", "go", "package", packageName, "version", resolvedVersion, "reason", reason) + return d.blockMaliciousPackage(c, GoProxy, requestPath, reason) + } + } + } + + // Check MinReleaseTime for .info responses or resolved-version responses (e.g. /@latest) + if configs.MinReleaseTime > 0 && hasReleaseTime { + if strings.HasSuffix(requestPath, ".info") || (!hasExplicitVersion && resolvedVersion != "") { + if time.Since(releaseTime) > time.Duration(configs.MinReleaseTime)*time.Hour { + return d.blockTooNewPackage(c, GoProxy, requestPath, releaseTime, configs.MinReleaseTime) + } + } + } + // Cache successful responses with integrity verification if err := d.CacheDataWithIntegrity(cachePath, data); err != nil { slog.Warn("Failed to cache response", "proxy", "go", "error", err) } + // Store release time so MinReleaseTime can be enforced on future cache hits + if hasReleaseTime && (strings.HasSuffix(requestPath, ".info") || (!hasExplicitVersion && resolvedVersion != "")) { + if err := d.CacheReleaseTime(cachePath, releaseTime); err != nil { + slog.Warn("Failed to cache release time", "proxy", "go", "error", err) + } + } + // Copy important headers from upstream if contentType := headers.Get("Content-Type"); contentType != "" { c.Response().Header().Set("Content-Type", contentType) @@ -316,7 +485,7 @@ func (d *DependencyProxyController) ProxyGo(c shared.Context) error { func (d *DependencyProxyController) ProxyPyPI(c shared.Context) error { // Get the full path after the prefix - requestPath := strings.TrimPrefix(c.Request().URL.Path, "/api/v1/dependency-proxy/pypi") + requestPath := TrimProxyPrefix(c.Request().URL.Path, PyPIProxy) ctx, span := depProxyTracer.Start(c.Request().Context(), "dependency-proxy.pypi", trace.WithAttributes( @@ -333,39 +502,69 @@ func (d *DependencyProxyController) ProxyPyPI(c shared.Context) error { return echo.NewHTTPError(http.StatusMethodNotAllowed, "Method not allowed") } + configs, err := d.GetDependencyProxyConfigs(c) + if err != nil { + slog.Error("Error getting dependency proxy configs", "error", err) + } + slog.Info("Proxy request", "proxy", "pypi", "method", c.Request().Method, "path", requestPath) - // Check for malicious packages BEFORE checking cache to prevent cache poisoning - if d.maliciousChecker != nil { - if blocked, reason := d.checkMaliciousPackage(c.Request().Context(), PyPIProxy, requestPath); blocked { - slog.Warn("Blocked malicious package", "proxy", "pypi", "path", requestPath, "reason", reason) - // Also remove from cache if it exists to prevent serving cached malicious content - cachePath := d.getCachePath(PyPIProxy, requestPath) - if err := os.Remove(cachePath); err == nil { - slog.Info("Removed malicious package from cache", "path", cachePath) - } - return d.blockMaliciousPackage(c, PyPIProxy, requestPath, reason) + cachePath := d.getCachePath(PyPIProxy, requestPath) + + pkgName, version := d.ParsePackageFromPath(PyPIProxy, requestPath) + hasExplicitVersion := version != "" + + if hasExplicitVersion { + //check config for not allowed patterns before doing anything else to fail fast and avoid unnecessary processing + notAllowed, notAllowedReason := d.CheckNotAllowedPackage(ctx, PyPIProxy, requestPath, configs) + if notAllowed { + return d.blockNotAllowedPackage(c, PyPIProxy, requestPath, notAllowedReason) } - } - cachePath := d.getCachePath(PyPIProxy, requestPath) + // Check for malicious packages BEFORE checking cache to prevent cache poisoning + if d.maliciousChecker != nil { + if blocked, reason := d.checkMaliciousPackage(c.Request().Context(), PyPIProxy, requestPath); blocked { + slog.Warn("Blocked malicious package", "proxy", "pypi", "path", requestPath, "reason", reason) + // Also remove from cache if it exists to prevent serving cached malicious content + cachePath := d.getCachePath(PyPIProxy, requestPath) + if err := os.Remove(cachePath); err == nil { + slog.Info("Removed malicious package from cache", "path", cachePath) + } + return d.blockMaliciousPackage(c, PyPIProxy, requestPath, reason) + } + } - // Check cache - if d.isPyPICached(cachePath) { - slog.Debug("Cache hit", "proxy", "pypi", "path", requestPath) - data, err := os.ReadFile(cachePath) - if err == nil { - // Verify cache integrity - if d.VerifyCacheIntegrity(cachePath, data) { - span.SetAttributes(attribute.Bool("proxy.cache_hit", true)) - return d.writePyPIResponse(c, data, requestPath, true) + // Check cache + if d.isPyPICached(cachePath) { + slog.Debug("Cache hit", "proxy", "pypi", "path", requestPath) + data, err := os.ReadFile(cachePath) + if err == nil { + // Verify cache integrity + if d.VerifyCacheIntegrity(cachePath, data) { + if configs.MinReleaseTime > 0 { + if releaseTime, ok := d.ReadCachedReleaseTime(cachePath); ok { + if time.Since(releaseTime) > time.Duration(configs.MinReleaseTime)*time.Hour { + return d.blockTooNewPackage(c, PyPIProxy, requestPath, releaseTime, configs.MinReleaseTime) + } + span.SetAttributes(attribute.Bool("proxy.cache_hit", true)) + return d.writePyPIResponse(c, data, requestPath, true) + } + // No cached release time - fall through to upstream to retrieve it + slog.Debug("No cached release time for MinReleaseTime check, refetching", "proxy", "pypi", "path", requestPath) + } else { + span.SetAttributes(attribute.Bool("proxy.cache_hit", true)) + return d.writePyPIResponse(c, data, requestPath, true) + } + } else { + slog.Warn("Cache integrity verification failed, refetching", "proxy", "pypi", "path", requestPath) + // Remove corrupted cache + os.Remove(cachePath) + os.Remove(cachePath + ".sha256") + } + } else { + slog.Warn("Cache read error", "proxy", "pypi", "error", err) } - slog.Warn("Cache integrity verification failed, refetching", "proxy", "pypi", "path", requestPath) - // Remove corrupted cache - os.Remove(cachePath) - os.Remove(cachePath + ".sha256") } - slog.Warn("Cache read error", "proxy", "pypi", "error", err) } span.SetAttributes(attribute.Bool("proxy.cache_hit", false)) @@ -390,11 +589,56 @@ func (d *DependencyProxyController) ProxyPyPI(c shared.Context) error { return c.Blob(statusCode, headers.Get("Content-Type"), data) } + // For simple/ requests (no explicit version), fetch the PyPI JSON API to get the resolved version — + // same logic as npm proxy: check allowlist rules and malicious database with the resolved version + var pypiReleaseTime time.Time + if !hasExplicitVersion { + resolvedVersion, releaseTime, ok := d.fetchPyPILatestVersionAndReleaseTime(ctx, pkgName) + if ok { + pypiReleaseTime = releaseTime + + notAllowed, notAllowedReason := d.CheckNotAllowedPackage(ctx, PyPIProxy, pkgName+"@"+resolvedVersion, configs) + if notAllowed { + return d.blockNotAllowedPackage(c, PyPIProxy, requestPath, notAllowedReason) + } + + if d.maliciousChecker != nil { + slog.Debug("Checking resolved version for malicious package", "package", pkgName, "version", resolvedVersion) + isMalicious, entry, err := d.maliciousChecker.IsMalicious(ctx, "pypi", pkgName, resolvedVersion) + if err != nil { + slog.Error("Error checking malicious package", "proxy", "pypi", "error", err) + return echo.NewHTTPError(500, "failed to check if package is malicious").WithInternal(err) + } + if isMalicious { + reason := fmt.Sprintf("Package %s@%s is flagged as malicious (ID: %s)", pkgName, resolvedVersion, entry.ID) + if entry.Summary != "" { + reason += ": " + entry.Summary + } + slog.Warn("Blocked malicious package after version resolution", "proxy", "pypi", "package", pkgName, "version", resolvedVersion, "reason", reason) + return d.blockMaliciousPackage(c, PyPIProxy, requestPath, reason) + } + } + + if configs.MinReleaseTime > 0 { + if time.Since(releaseTime) > time.Duration(configs.MinReleaseTime)*time.Hour { + return d.blockTooNewPackage(c, PyPIProxy, requestPath, releaseTime, configs.MinReleaseTime) + } + } + } + } + // Cache successful responses with integrity verification if err := d.CacheDataWithIntegrity(cachePath, data); err != nil { slog.Warn("Failed to cache response", "proxy", "pypi", "error", err) } + // Store release time so MinReleaseTime can be enforced on future cache hits + if !pypiReleaseTime.IsZero() { + if err := d.CacheReleaseTime(cachePath, pypiReleaseTime); err != nil { + slog.Warn("Failed to cache release time", "proxy", "pypi", "error", err) + } + } + // Copy important headers from upstream if contentType := headers.Get("Content-Type"); contentType != "" { c.Response().Header().Set("Content-Type", contentType) @@ -460,8 +704,11 @@ func (d *DependencyProxyController) isPyPICached(cachePath string) bool { func (d *DependencyProxyController) fetchFromUpstream(ctx context.Context, proxyType ProxyType, upstreamURL, requestPath string, headers http.Header, body io.Reader) ([]byte, http.Header, int, error) { // remove any trailing slashes from requestPath requestPath = strings.TrimRight(requestPath, "/") - url := upstreamURL + requestPath - slog.Debug("Fetching from upstream", "proxy", proxyType, "url", url) + url, err := url.JoinPath(upstreamURL, requestPath) + if err != nil { + return nil, nil, 0, fmt.Errorf("failed to join URL: %w", err) + } + slog.Debug("Fetching from upstream", "proxy", proxyType, "url", url, "bodyPresent", body != nil) // Determine HTTP method based on body presence method := "GET" @@ -498,7 +745,10 @@ func (d *DependencyProxyController) fetchFromUpstream(ctx context.Context, proxy func (d *DependencyProxyController) fetchNPMAuditFromUpstream(ctx context.Context, requestPath string, headers http.Header, bodyBytes []byte) ([]byte, http.Header, int, error) { // remove any trailing slashes from requestPath requestPath = strings.TrimRight(requestPath, "/") - url := npmRegistry + requestPath + url, err := url.JoinPath(npmRegistry, requestPath) + if err != nil { + return nil, nil, 0, fmt.Errorf("failed to join URL: %w", err) + } slog.Info("Fetching npm audit from upstream", "url", url, "bodySize", len(bodyBytes)) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes)) @@ -556,7 +806,10 @@ func (d *DependencyProxyController) fetchNPMAuditFromUpstream(ctx context.Contex func (d *DependencyProxyController) fetchPyPIFromUpstream(ctx context.Context, requestPath string, headers http.Header) ([]byte, http.Header, int, error) { // remove any trailing slashes from requestPath requestPath = strings.TrimRight(requestPath, "/") - url := pypiRegistry + requestPath + url, err := url.JoinPath(pypiRegistry, requestPath) + if err != nil { + return nil, nil, 0, fmt.Errorf("failed to join URL: %w", err) + } slog.Debug("Fetching from upstream", "proxy", "pypi", "url", url) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -614,6 +867,134 @@ func (d *DependencyProxyController) CacheDataWithIntegrity(cachePath string, dat return nil } +func (d *DependencyProxyController) GetDependencyProxyURLs(ctx shared.Context) error { + //get registry url from env + registryURL := os.Getenv("DEPENDENCY_PROXY_BASE_URL") + if registryURL == "" { + registryURL = "https://api.main.devguard.org/api/v1/dependency-proxy" + } + + var secret uuid.UUID + + reqCtx := ctx.Request().Context() + if asset, err := shared.MaybeGetAsset(ctx); err == nil { + proxy, err := d.dependencyProxyService.GetOrCreateByAssetID(reqCtx, asset.GetID()) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to get or create dependency proxy for asset: %v", err)) + } + secret = proxy.Secret + } else if project, err := shared.MaybeGetProject(ctx); err == nil { + proxy, err := d.dependencyProxyService.GetOrCreateByProjectID(reqCtx, project.GetID()) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to get or create dependency proxy for project: %v", err)) + } + secret = proxy.Secret + } else if org, err := shared.MaybeGetOrganization(ctx); err == nil { + proxy, err := d.dependencyProxyService.GetOrCreateByOrgID(reqCtx, org.GetID()) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to get or create dependency proxy for organization: %v", err)) + } + secret = proxy.Secret + } + + if secret == uuid.Nil { + return echo.NewHTTPError(http.StatusInternalServerError, "failed to determine scope for dependency proxy") + } + + proxies := map[string]string{} + proxies["npm"] = registryURL + "/" + secret.String() + "/npm/" + proxies["go"] = registryURL + "/" + secret.String() + "/go/" + proxies["pypi"] = registryURL + "/" + secret.String() + "/pypi/simple/" + + return ctx.JSON(http.StatusOK, proxies) +} + +func (d *DependencyProxyController) GetDependencyProxyConfigs(c shared.Context) (DependencyProxyConfigs, error) { + var configs DependencyProxyConfigs + + secret := c.Param("secret") + uuidSecret, err := uuid.Parse(secret) + if err != nil { + return configs, fmt.Errorf("invalid dependency proxy secret: %w", err) + } + + scope, uuid, err := d.dependencyProxyService.GetModelBySecret(c.Request().Context(), uuidSecret) + if err != nil { + return configs, fmt.Errorf("failed to get dependency proxy model by secret: %w", err) + } + + var configFilesJSON any + + switch scope { + case "asset": + asset, err := d.assetRepository.Read(c.Request().Context(), nil, uuid) + if err != nil { + return configs, fmt.Errorf("failed to read asset: %w", err) + } + + configFilesJSON = asset.ConfigFiles["dependency-proxy-configs"] + + case "project": + project, err := d.projectRepository.Read(c.Request().Context(), nil, uuid) + if err != nil { + return configs, fmt.Errorf("failed to read project: %w", err) + } + configFilesJSON = project.ConfigFiles["dependency-proxy-configs"] + case "organization": + org, err := d.orgRepository.Read(c.Request().Context(), nil, uuid) + if err != nil { + return configs, fmt.Errorf("failed to read organization: %w", err) + } + configFilesJSON = org.ConfigFiles["dependency-proxy-configs"] + default: + return configs, fmt.Errorf("invalid proxy scope: %s", scope) + } + + if configFilesJSON != nil { + s, ok := configFilesJSON.(string) + if !ok { + return configs, fmt.Errorf("unexpected config file json type: %T", configFilesJSON) + } + var raw struct { + Rules string `json:"rules"` + MinReleaseTime int `json:"minReleaseTime"` + } + if err := json.Unmarshal([]byte(s), &raw); err != nil { + return configs, fmt.Errorf("failed to unmarshal config file json into configs: %w", err) + } + configs.MinReleaseTime = raw.MinReleaseTime + for _, line := range strings.Split(raw.Rules, "\n") { + line = strings.TrimSpace(line) + if line != "" && !strings.HasPrefix(line, "#") { + configs.Rules = append(configs.Rules, line) + } + } + } + + return configs, nil +} + +// CacheReleaseTime stores the release time for a cached entry to enable MinReleaseTime checks on cache hits. +func (d *DependencyProxyController) CacheReleaseTime(cachePath string, releaseTime time.Time) error { + if releaseTime.IsZero() { + return nil + } + return os.WriteFile(cachePath+".releasetime", []byte(releaseTime.UTC().Format(time.RFC3339Nano)), 0644) +} + +// ReadCachedReleaseTime reads the stored release time for a cached entry. +func (d *DependencyProxyController) ReadCachedReleaseTime(cachePath string) (time.Time, bool) { + data, err := os.ReadFile(cachePath + ".releasetime") + if err != nil { + return time.Time{}, false + } + t, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(string(data))) + if err != nil { + return time.Time{}, false + } + return t, true +} + // VerifyCacheIntegrity checks if the cached data matches its stored hash func (d *DependencyProxyController) VerifyCacheIntegrity(cachePath string, data []byte) bool { hashPath := cachePath + ".sha256" @@ -738,14 +1119,14 @@ func (d *DependencyProxyController) ParsePackageFromPath(proxyType ProxyType, pa return pkgName, "" case GoProxy: - re := regexp.MustCompile(`^/([^@]+)(?:@v/([^/]+))?`) + re := regexp.MustCompile(`^([^@]+)(?:@v/([^/]+))?`) matches := re.FindStringSubmatch(path) if len(matches) > 1 { - moduleName := matches[1] + moduleName := strings.TrimPrefix(matches[1], "/") version := "" if len(matches) > 2 && matches[2] != "" { if matches[2] == "list" { - return moduleName, "" + return strings.TrimRight(moduleName, "/"), "" } version = strings.TrimSuffix(strings.TrimSuffix(matches[2], ".info"), ".mod") version = strings.TrimSuffix(version, ".zip") @@ -756,7 +1137,8 @@ func (d *DependencyProxyController) ParsePackageFromPath(proxyType ProxyType, pa case PyPIProxy: // PyPI simple API: /simple// or /packages/ // Extract package name from path like /simple/django/ or /packages/django-3.2.0-py3-none-any.whl - if after, ok := strings.CutPrefix(path, "/simple/"); ok { + path = strings.TrimPrefix(path, "/") + if after, ok := strings.CutPrefix(path, "simple/"); ok { pkgName := after pkgName = strings.TrimSuffix(pkgName, "/") return pkgName, "" @@ -774,6 +1156,82 @@ func (d *DependencyProxyController) ParsePackageFromPath(proxyType ProxyType, pa return "", "" } +// matchPattern matches a packagePurl against a pattern that may contain '*' wildcards. +// - *pattern* → contains +// - *pattern → contains (suffix match) +// - pattern* → starts with +// - a*b → starts with "a" and ends with "b" +// - pattern → exact match +func matchPattern(pattern, packagePurl string) bool { + parts := strings.Split(pattern, "*") + if len(parts) == 1 { + return packagePurl == pattern + } + // First part must be a prefix (empty if pattern starts with *) + if parts[0] != "" && !strings.HasPrefix(packagePurl, parts[0]) { + return false + } + // Last part must be a suffix (empty if pattern ends with *) + if parts[len(parts)-1] != "" && !strings.HasSuffix(packagePurl, parts[len(parts)-1]) { + return false + } + // Middle parts must appear in order + rest := packagePurl + for _, part := range parts { + if part == "" { + continue + } + idx := strings.Index(rest, part) + if idx == -1 { + return false + } + rest = rest[idx+len(part):] + } + return true +} + +func (d *DependencyProxyController) CheckNotAllowedPackage(ctx context.Context, proxyType ProxyType, path string, configs DependencyProxyConfigs) (bool, string) { + var packageName, version, packagePurl string + if strings.HasPrefix(path, "pkg:") { + // Path is already a PURL — use it directly. + packagePurl = path + } else { + packageName, version = d.ParsePackageFromPath(proxyType, path) + if packageName == "" { + return false, "" + } + packagePurl = fmt.Sprintf("pkg:%s/%s", proxyType, packageName) + if version != "" { + packagePurl += "@" + version + } + } + + // Rules are applied in order like gitignore: last matching rule wins. + // A rule prefixed with "!" negates the match (allowlist). + blocked := false + matchedRule := "" + for _, rule := range configs.Rules { + negate := strings.HasPrefix(rule, "!") + pattern := strings.TrimPrefix(rule, "!") + + matched := matchPattern(pattern, packagePurl) + + if matched { + blocked = !negate + matchedRule = rule + } + } + + if blocked { + displayName := packageName + if displayName == "" { + displayName = packagePurl + } + return true, fmt.Sprintf("Package %s is not allowed by rule: %s", displayName, matchedRule) + } + return false, "" +} + func (d *DependencyProxyController) checkMaliciousPackage(ctx context.Context, proxyType ProxyType, path string) (bool, string) { packageName, version := d.ParsePackageFromPath(proxyType, path) if packageName == "" { @@ -791,8 +1249,16 @@ func (d *DependencyProxyController) checkMaliciousPackage(ctx context.Context, p } slog.Debug("Checking package against malicious database", "ecosystem", ecosystem, "package", packageName, "version", version) + if d.maliciousChecker == nil { + slog.Debug("No malicious checker configured, skipping check") + return false, "" + } + isMalicious, entry, err := d.maliciousChecker.IsMalicious(ctx, ecosystem, packageName, version) + if err != nil { + slog.Error("Error checking malicious package", "proxy", proxyType, "error", err) + return false, "" + } - isMalicious, entry := d.maliciousChecker.IsMalicious(ctx, ecosystem, packageName, version) if isMalicious { reason := fmt.Sprintf("Package %s is flagged as malicious (ID: %s)", packageName, entry.ID) if entry.Summary != "" { @@ -806,19 +1272,49 @@ func (d *DependencyProxyController) checkMaliciousPackage(ctx context.Context, p // ExtractNPMVersionFromMetadata parses NPM package metadata JSON and extracts the "latest" version // This is used when npx or npm install is called without a specific version -func (d *DependencyProxyController) ExtractNPMVersionFromMetadata(data []byte) string { +func (d *DependencyProxyController) ExtractNPMVersionAndReleaseTimeFromMetadata(data []byte) (string, time.Time) { var metadata struct { DistTags struct { Latest string `json:"latest"` } `json:"dist-tags"` + Time map[string]time.Time `json:"time"` } if err := json.Unmarshal(data, &metadata); err != nil { slog.Debug("Failed to parse NPM metadata", "error", err) - return "" + return "", time.Time{} } - return metadata.DistTags.Latest + return metadata.DistTags.Latest, metadata.Time[metadata.DistTags.Latest] +} + +func (d *DependencyProxyController) blockNotAllowedPackage(c shared.Context, proxyType ProxyType, path, reason string) error { + span := trace.SpanFromContext(c.Request().Context()) + span.SetAttributes( + attribute.Bool("proxy.not_allowed_blocked", true), + attribute.String("proxy.block_reason", reason), + ) + span.SetStatus(codes.Error, "package blocked by rule") + + c.Response().Header().Set("X-Not-Allowed-Package", "blocked") + + slog.Warn("BLOCKED NOT ALLOWED PACKAGE", "path", path, "reason", reason) + + packageName, _ := d.ParsePackageFromPath(proxyType, path) + if packageName == "" { + packageName = "unknown" + } + span.SetAttributes(attribute.String("proxy.package", packageName)) + + response := map[string]any{ + "error": "Forbidden", + "message": "This package has been blocked by the dependency proxy rules", + "reason": reason, + "path": path, + "blocked": true, + } + + return c.JSON(http.StatusForbidden, response) } func (d *DependencyProxyController) blockMaliciousPackage(c shared.Context, proxyType ProxyType, path, reason string) error { @@ -850,3 +1346,85 @@ func (d *DependencyProxyController) blockMaliciousPackage(c shared.Context, prox return c.JSON(http.StatusForbidden, response) } + +// ExtractGoVersionAndReleaseTime parses a Go proxy .info response and returns the resolved version and its release time. +func (d *DependencyProxyController) ExtractGoVersionAndReleaseTime(data []byte) (string, time.Time, bool) { + var info struct { + Version string `json:"Version"` + Time time.Time `json:"Time"` + } + if err := json.Unmarshal(data, &info); err != nil || info.Time.IsZero() { + return "", time.Time{}, false + } + return info.Version, info.Time, true +} + +// ExtractPyPIReleaseTime parses a PyPI JSON API response and returns the resolved version and its upload time. +// If version is empty, it uses info.version (the current release). +func (d *DependencyProxyController) ExtractPyPIReleaseTime(data []byte, version string) (string, time.Time, bool) { + var metadata struct { + Info struct { + Version string `json:"version"` + } `json:"info"` + Releases map[string][]struct { + UploadTime string `json:"upload_time_iso_8601"` + } `json:"releases"` + } + if err := json.Unmarshal(data, &metadata); err != nil { + return "", time.Time{}, false + } + if version == "" { + version = metadata.Info.Version + } + files, ok := metadata.Releases[version] + if !ok || len(files) == 0 { + return version, time.Time{}, false + } + t, err := time.Parse(time.RFC3339Nano, files[0].UploadTime) + if err != nil { + return version, time.Time{}, false + } + return version, t, true +} + +// fetchPyPILatestVersionAndReleaseTime fetches the PyPI JSON API and returns the resolved version and its release time. +func (d *DependencyProxyController) fetchPyPILatestVersionAndReleaseTime(ctx context.Context, pkgName string) (string, time.Time, bool) { + data, _, statusCode, err := d.fetchPyPIFromUpstream(ctx, "/pypi/"+pkgName+"/json", http.Header{}) + if err != nil || statusCode != http.StatusOK { + return "", time.Time{}, false + } + return d.ExtractPyPIReleaseTime(data, "") +} + +func (d *DependencyProxyController) blockTooNewPackage(c shared.Context, proxyType ProxyType, path string, releaseTime time.Time, minReleaseTime int) error { + span := trace.SpanFromContext(c.Request().Context()) + span.SetAttributes( + attribute.Bool("proxy.too_new_blocked", true), + ) + span.SetStatus(codes.Error, "package too new") + + c.Response().Header().Set("X-Too-New-Package", "blocked") + + packageName, _ := d.ParsePackageFromPath(proxyType, path) + if packageName == "" { + packageName = "unknown" + } + span.SetAttributes(attribute.String("proxy.package", packageName)) + + reason := fmt.Sprintf("Package %s was released %s ago, which is less than the required minimum of %d hours", + packageName, + time.Since(releaseTime).Round(time.Minute), + minReleaseTime, + ) + slog.Warn("BLOCKED TOO NEW PACKAGE", "path", path, "reason", reason) + + response := map[string]any{ + "error": "Forbidden", + "message": "This package has been blocked because it was released too recently", + "reason": reason, + "path": path, + "blocked": true, + } + + return c.JSON(http.StatusForbidden, response) +} diff --git a/controllers/providers.go b/controllers/providers.go index 70f30deb..44bf37f2 100644 --- a/controllers/providers.go +++ b/controllers/providers.go @@ -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 != "" { @@ -47,7 +47,7 @@ func ProvideDependencyProxyConfig() DependencyProxyConfig { slog.Error("Failed to create cache directory", "error", err) } - return DependencyProxyConfig{ + return DependencyProxyCache{ CacheDir: cacheDir, } } @@ -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), ) diff --git a/controllers/vulndb_controller.go b/controllers/vulndb_controller.go index e07ed37a..1993fcbb 100644 --- a/controllers/vulndb_controller.go +++ b/controllers/vulndb_controller.go @@ -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) + } var componentDTO *dtos.ComponentDTO comp := models.Component{ID: purlString} diff --git a/database/migrations/20260410163018_add_dependency_proxy_secret.down.sql b/database/migrations/20260410163018_add_dependency_proxy_secret.down.sql new file mode 100644 index 00000000..e2bacc71 --- /dev/null +++ b/database/migrations/20260410163018_add_dependency_proxy_secret.down.sql @@ -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 . + +DROP TABLE IF EXISTS public.dependency_proxy_secrets; diff --git a/database/migrations/20260410163018_add_dependency_proxy_secret.up.sql b/database/migrations/20260410163018_add_dependency_proxy_secret.up.sql new file mode 100644 index 00000000..950ebbf4 --- /dev/null +++ b/database/migrations/20260410163018_add_dependency_proxy_secret.up.sql @@ -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 . + +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; diff --git a/database/models/dependency_proxy_secret_model.go b/database/models/dependency_proxy_secret_model.go new file mode 100644 index 00000000..4bf0c28e --- /dev/null +++ b/database/models/dependency_proxy_secret_model.go @@ -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 . + +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" +} diff --git a/database/repositories/dependency_proxy_secret_repository.go b/database/repositories/dependency_proxy_secret_repository.go new file mode 100644 index 00000000..0152da54 --- /dev/null +++ b/database/repositories/dependency_proxy_secret_repository.go @@ -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 . +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 + } + 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 +} diff --git a/database/repositories/providers.go b/database/repositories/providers.go index b274028c..886db581 100644 --- a/database/repositories/providers.go +++ b/database/repositories/providers.go @@ -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)))), ) diff --git a/mocks/mock_DependencyProxySecretRepository.go b/mocks/mock_DependencyProxySecretRepository.go new file mode 100644 index 00000000..c7af0f4e --- /dev/null +++ b/mocks/mock_DependencyProxySecretRepository.go @@ -0,0 +1,1415 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "context" + + "github.com/google/uuid" + "github.com/l3montree-dev/devguard/database/models" + "github.com/l3montree-dev/devguard/shared" + mock "github.com/stretchr/testify/mock" + "gorm.io/gorm/clause" +) + +// NewDependencyProxySecretRepository creates a new instance of DependencyProxySecretRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDependencyProxySecretRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *DependencyProxySecretRepository { + mock := &DependencyProxySecretRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// DependencyProxySecretRepository is an autogenerated mock type for the DependencyProxySecretRepository type +type DependencyProxySecretRepository struct { + mock.Mock +} + +type DependencyProxySecretRepository_Expecter struct { + mock *mock.Mock +} + +func (_m *DependencyProxySecretRepository) EXPECT() *DependencyProxySecretRepository_Expecter { + return &DependencyProxySecretRepository_Expecter{mock: &_m.Mock} +} + +// Activate provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) Activate(ctx context.Context, tx shared.DB, id uuid.UUID) error { + ret := _mock.Called(ctx, tx, id) + + if len(ret) == 0 { + panic("no return value specified for Activate") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) error); ok { + r0 = returnFunc(ctx, tx, id) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// DependencyProxySecretRepository_Activate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Activate' +type DependencyProxySecretRepository_Activate_Call struct { + *mock.Call +} + +// Activate is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - id uuid.UUID +func (_e *DependencyProxySecretRepository_Expecter) Activate(ctx interface{}, tx interface{}, id interface{}) *DependencyProxySecretRepository_Activate_Call { + return &DependencyProxySecretRepository_Activate_Call{Call: _e.mock.On("Activate", ctx, tx, id)} +} + +func (_c *DependencyProxySecretRepository_Activate_Call) Run(run func(ctx context.Context, tx shared.DB, id uuid.UUID)) *DependencyProxySecretRepository_Activate_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + var arg2 uuid.UUID + if args[2] != nil { + arg2 = args[2].(uuid.UUID) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_Activate_Call) Return(err error) *DependencyProxySecretRepository_Activate_Call { + _c.Call.Return(err) + return _c +} + +func (_c *DependencyProxySecretRepository_Activate_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, id uuid.UUID) error) *DependencyProxySecretRepository_Activate_Call { + _c.Call.Return(run) + return _c +} + +// All provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) All(ctx context.Context, tx shared.DB) ([]models.DependencyProxySecret, error) { + ret := _mock.Called(ctx, tx) + + if len(ret) == 0 { + panic("no return value specified for All") + } + + var r0 []models.DependencyProxySecret + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB) ([]models.DependencyProxySecret, error)); ok { + return returnFunc(ctx, tx) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB) []models.DependencyProxySecret); ok { + r0 = returnFunc(ctx, tx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.DependencyProxySecret) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, shared.DB) error); ok { + r1 = returnFunc(ctx, tx) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// DependencyProxySecretRepository_All_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'All' +type DependencyProxySecretRepository_All_Call struct { + *mock.Call +} + +// All is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +func (_e *DependencyProxySecretRepository_Expecter) All(ctx interface{}, tx interface{}) *DependencyProxySecretRepository_All_Call { + return &DependencyProxySecretRepository_All_Call{Call: _e.mock.On("All", ctx, tx)} +} + +func (_c *DependencyProxySecretRepository_All_Call) Run(run func(ctx context.Context, tx shared.DB)) *DependencyProxySecretRepository_All_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_All_Call) Return(dependencyProxySecrets []models.DependencyProxySecret, err error) *DependencyProxySecretRepository_All_Call { + _c.Call.Return(dependencyProxySecrets, err) + return _c +} + +func (_c *DependencyProxySecretRepository_All_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB) ([]models.DependencyProxySecret, error)) *DependencyProxySecretRepository_All_Call { + _c.Call.Return(run) + return _c +} + +// Begin provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) Begin(ctx context.Context) shared.DB { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Begin") + } + + var r0 shared.DB + if returnFunc, ok := ret.Get(0).(func(context.Context) shared.DB); ok { + r0 = returnFunc(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(shared.DB) + } + } + return r0 +} + +// DependencyProxySecretRepository_Begin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Begin' +type DependencyProxySecretRepository_Begin_Call struct { + *mock.Call +} + +// Begin is a helper method to define mock.On call +// - ctx context.Context +func (_e *DependencyProxySecretRepository_Expecter) Begin(ctx interface{}) *DependencyProxySecretRepository_Begin_Call { + return &DependencyProxySecretRepository_Begin_Call{Call: _e.mock.On("Begin", ctx)} +} + +func (_c *DependencyProxySecretRepository_Begin_Call) Run(run func(ctx context.Context)) *DependencyProxySecretRepository_Begin_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_Begin_Call) Return(v shared.DB) *DependencyProxySecretRepository_Begin_Call { + _c.Call.Return(v) + return _c +} + +func (_c *DependencyProxySecretRepository_Begin_Call) RunAndReturn(run func(ctx context.Context) shared.DB) *DependencyProxySecretRepository_Begin_Call { + _c.Call.Return(run) + return _c +} + +// CleanupOrphanedRecords provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) CleanupOrphanedRecords(ctx context.Context) error { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for CleanupOrphanedRecords") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = returnFunc(ctx) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// DependencyProxySecretRepository_CleanupOrphanedRecords_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CleanupOrphanedRecords' +type DependencyProxySecretRepository_CleanupOrphanedRecords_Call struct { + *mock.Call +} + +// CleanupOrphanedRecords is a helper method to define mock.On call +// - ctx context.Context +func (_e *DependencyProxySecretRepository_Expecter) CleanupOrphanedRecords(ctx interface{}) *DependencyProxySecretRepository_CleanupOrphanedRecords_Call { + return &DependencyProxySecretRepository_CleanupOrphanedRecords_Call{Call: _e.mock.On("CleanupOrphanedRecords", ctx)} +} + +func (_c *DependencyProxySecretRepository_CleanupOrphanedRecords_Call) Run(run func(ctx context.Context)) *DependencyProxySecretRepository_CleanupOrphanedRecords_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_CleanupOrphanedRecords_Call) Return(err error) *DependencyProxySecretRepository_CleanupOrphanedRecords_Call { + _c.Call.Return(err) + return _c +} + +func (_c *DependencyProxySecretRepository_CleanupOrphanedRecords_Call) RunAndReturn(run func(ctx context.Context) error) *DependencyProxySecretRepository_CleanupOrphanedRecords_Call { + _c.Call.Return(run) + return _c +} + +// Create provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) Create(ctx context.Context, tx shared.DB, t *models.DependencyProxySecret) error { + ret := _mock.Called(ctx, tx, t) + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, *models.DependencyProxySecret) error); ok { + r0 = returnFunc(ctx, tx, t) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// DependencyProxySecretRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type DependencyProxySecretRepository_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - t *models.DependencyProxySecret +func (_e *DependencyProxySecretRepository_Expecter) Create(ctx interface{}, tx interface{}, t interface{}) *DependencyProxySecretRepository_Create_Call { + return &DependencyProxySecretRepository_Create_Call{Call: _e.mock.On("Create", ctx, tx, t)} +} + +func (_c *DependencyProxySecretRepository_Create_Call) Run(run func(ctx context.Context, tx shared.DB, t *models.DependencyProxySecret)) *DependencyProxySecretRepository_Create_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + var arg2 *models.DependencyProxySecret + if args[2] != nil { + arg2 = args[2].(*models.DependencyProxySecret) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_Create_Call) Return(err error) *DependencyProxySecretRepository_Create_Call { + _c.Call.Return(err) + return _c +} + +func (_c *DependencyProxySecretRepository_Create_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, t *models.DependencyProxySecret) error) *DependencyProxySecretRepository_Create_Call { + _c.Call.Return(run) + return _c +} + +// CreateBatch provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) CreateBatch(ctx context.Context, tx shared.DB, ts []models.DependencyProxySecret) error { + ret := _mock.Called(ctx, tx, ts) + + if len(ret) == 0 { + panic("no return value specified for CreateBatch") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, []models.DependencyProxySecret) error); ok { + r0 = returnFunc(ctx, tx, ts) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// DependencyProxySecretRepository_CreateBatch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateBatch' +type DependencyProxySecretRepository_CreateBatch_Call struct { + *mock.Call +} + +// CreateBatch is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - ts []models.DependencyProxySecret +func (_e *DependencyProxySecretRepository_Expecter) CreateBatch(ctx interface{}, tx interface{}, ts interface{}) *DependencyProxySecretRepository_CreateBatch_Call { + return &DependencyProxySecretRepository_CreateBatch_Call{Call: _e.mock.On("CreateBatch", ctx, tx, ts)} +} + +func (_c *DependencyProxySecretRepository_CreateBatch_Call) Run(run func(ctx context.Context, tx shared.DB, ts []models.DependencyProxySecret)) *DependencyProxySecretRepository_CreateBatch_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + var arg2 []models.DependencyProxySecret + if args[2] != nil { + arg2 = args[2].([]models.DependencyProxySecret) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_CreateBatch_Call) Return(err error) *DependencyProxySecretRepository_CreateBatch_Call { + _c.Call.Return(err) + return _c +} + +func (_c *DependencyProxySecretRepository_CreateBatch_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, ts []models.DependencyProxySecret) error) *DependencyProxySecretRepository_CreateBatch_Call { + _c.Call.Return(run) + return _c +} + +// Delete provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) Delete(ctx context.Context, tx shared.DB, id uuid.UUID) error { + ret := _mock.Called(ctx, tx, id) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) error); ok { + r0 = returnFunc(ctx, tx, id) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// DependencyProxySecretRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type DependencyProxySecretRepository_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - id uuid.UUID +func (_e *DependencyProxySecretRepository_Expecter) Delete(ctx interface{}, tx interface{}, id interface{}) *DependencyProxySecretRepository_Delete_Call { + return &DependencyProxySecretRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, tx, id)} +} + +func (_c *DependencyProxySecretRepository_Delete_Call) Run(run func(ctx context.Context, tx shared.DB, id uuid.UUID)) *DependencyProxySecretRepository_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + var arg2 uuid.UUID + if args[2] != nil { + arg2 = args[2].(uuid.UUID) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_Delete_Call) Return(err error) *DependencyProxySecretRepository_Delete_Call { + _c.Call.Return(err) + return _c +} + +func (_c *DependencyProxySecretRepository_Delete_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, id uuid.UUID) error) *DependencyProxySecretRepository_Delete_Call { + _c.Call.Return(run) + return _c +} + +// DeleteBatch provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) DeleteBatch(ctx context.Context, tx shared.DB, ids []models.DependencyProxySecret) error { + ret := _mock.Called(ctx, tx, ids) + + if len(ret) == 0 { + panic("no return value specified for DeleteBatch") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, []models.DependencyProxySecret) error); ok { + r0 = returnFunc(ctx, tx, ids) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// DependencyProxySecretRepository_DeleteBatch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteBatch' +type DependencyProxySecretRepository_DeleteBatch_Call struct { + *mock.Call +} + +// DeleteBatch is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - ids []models.DependencyProxySecret +func (_e *DependencyProxySecretRepository_Expecter) DeleteBatch(ctx interface{}, tx interface{}, ids interface{}) *DependencyProxySecretRepository_DeleteBatch_Call { + return &DependencyProxySecretRepository_DeleteBatch_Call{Call: _e.mock.On("DeleteBatch", ctx, tx, ids)} +} + +func (_c *DependencyProxySecretRepository_DeleteBatch_Call) Run(run func(ctx context.Context, tx shared.DB, ids []models.DependencyProxySecret)) *DependencyProxySecretRepository_DeleteBatch_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + var arg2 []models.DependencyProxySecret + if args[2] != nil { + arg2 = args[2].([]models.DependencyProxySecret) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_DeleteBatch_Call) Return(err error) *DependencyProxySecretRepository_DeleteBatch_Call { + _c.Call.Return(err) + return _c +} + +func (_c *DependencyProxySecretRepository_DeleteBatch_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, ids []models.DependencyProxySecret) error) *DependencyProxySecretRepository_DeleteBatch_Call { + _c.Call.Return(run) + return _c +} + +// GetBySecret provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) GetBySecret(ctx context.Context, tx shared.DB, secret uuid.UUID) (models.DependencyProxySecret, error) { + ret := _mock.Called(ctx, tx, secret) + + if len(ret) == 0 { + panic("no return value specified for GetBySecret") + } + + var r0 models.DependencyProxySecret + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) (models.DependencyProxySecret, error)); ok { + return returnFunc(ctx, tx, secret) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) models.DependencyProxySecret); ok { + r0 = returnFunc(ctx, tx, secret) + } else { + r0 = ret.Get(0).(models.DependencyProxySecret) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, shared.DB, uuid.UUID) error); ok { + r1 = returnFunc(ctx, tx, secret) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// DependencyProxySecretRepository_GetBySecret_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBySecret' +type DependencyProxySecretRepository_GetBySecret_Call struct { + *mock.Call +} + +// GetBySecret is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - secret uuid.UUID +func (_e *DependencyProxySecretRepository_Expecter) GetBySecret(ctx interface{}, tx interface{}, secret interface{}) *DependencyProxySecretRepository_GetBySecret_Call { + return &DependencyProxySecretRepository_GetBySecret_Call{Call: _e.mock.On("GetBySecret", ctx, tx, secret)} +} + +func (_c *DependencyProxySecretRepository_GetBySecret_Call) Run(run func(ctx context.Context, tx shared.DB, secret uuid.UUID)) *DependencyProxySecretRepository_GetBySecret_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + var arg2 uuid.UUID + if args[2] != nil { + arg2 = args[2].(uuid.UUID) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_GetBySecret_Call) Return(dependencyProxySecret models.DependencyProxySecret, err error) *DependencyProxySecretRepository_GetBySecret_Call { + _c.Call.Return(dependencyProxySecret, err) + return _c +} + +func (_c *DependencyProxySecretRepository_GetBySecret_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, secret uuid.UUID) (models.DependencyProxySecret, error)) *DependencyProxySecretRepository_GetBySecret_Call { + _c.Call.Return(run) + return _c +} + +// GetDB provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) GetDB(ctx context.Context, tx shared.DB) shared.DB { + ret := _mock.Called(ctx, tx) + + if len(ret) == 0 { + panic("no return value specified for GetDB") + } + + var r0 shared.DB + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB) shared.DB); ok { + r0 = returnFunc(ctx, tx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(shared.DB) + } + } + return r0 +} + +// DependencyProxySecretRepository_GetDB_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDB' +type DependencyProxySecretRepository_GetDB_Call struct { + *mock.Call +} + +// GetDB is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +func (_e *DependencyProxySecretRepository_Expecter) GetDB(ctx interface{}, tx interface{}) *DependencyProxySecretRepository_GetDB_Call { + return &DependencyProxySecretRepository_GetDB_Call{Call: _e.mock.On("GetDB", ctx, tx)} +} + +func (_c *DependencyProxySecretRepository_GetDB_Call) Run(run func(ctx context.Context, tx shared.DB)) *DependencyProxySecretRepository_GetDB_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_GetDB_Call) Return(v shared.DB) *DependencyProxySecretRepository_GetDB_Call { + _c.Call.Return(v) + return _c +} + +func (_c *DependencyProxySecretRepository_GetDB_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB) shared.DB) *DependencyProxySecretRepository_GetDB_Call { + _c.Call.Return(run) + return _c +} + +// GetOrCreateByAssetID provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) GetOrCreateByAssetID(ctx context.Context, tx shared.DB, assetID uuid.UUID) (models.DependencyProxySecret, error) { + ret := _mock.Called(ctx, tx, assetID) + + if len(ret) == 0 { + panic("no return value specified for GetOrCreateByAssetID") + } + + var r0 models.DependencyProxySecret + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) (models.DependencyProxySecret, error)); ok { + return returnFunc(ctx, tx, assetID) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) models.DependencyProxySecret); ok { + r0 = returnFunc(ctx, tx, assetID) + } else { + r0 = ret.Get(0).(models.DependencyProxySecret) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, shared.DB, uuid.UUID) error); ok { + r1 = returnFunc(ctx, tx, assetID) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// DependencyProxySecretRepository_GetOrCreateByAssetID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOrCreateByAssetID' +type DependencyProxySecretRepository_GetOrCreateByAssetID_Call struct { + *mock.Call +} + +// GetOrCreateByAssetID is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - assetID uuid.UUID +func (_e *DependencyProxySecretRepository_Expecter) GetOrCreateByAssetID(ctx interface{}, tx interface{}, assetID interface{}) *DependencyProxySecretRepository_GetOrCreateByAssetID_Call { + return &DependencyProxySecretRepository_GetOrCreateByAssetID_Call{Call: _e.mock.On("GetOrCreateByAssetID", ctx, tx, assetID)} +} + +func (_c *DependencyProxySecretRepository_GetOrCreateByAssetID_Call) Run(run func(ctx context.Context, tx shared.DB, assetID uuid.UUID)) *DependencyProxySecretRepository_GetOrCreateByAssetID_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + var arg2 uuid.UUID + if args[2] != nil { + arg2 = args[2].(uuid.UUID) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_GetOrCreateByAssetID_Call) Return(dependencyProxySecret models.DependencyProxySecret, err error) *DependencyProxySecretRepository_GetOrCreateByAssetID_Call { + _c.Call.Return(dependencyProxySecret, err) + return _c +} + +func (_c *DependencyProxySecretRepository_GetOrCreateByAssetID_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, assetID uuid.UUID) (models.DependencyProxySecret, error)) *DependencyProxySecretRepository_GetOrCreateByAssetID_Call { + _c.Call.Return(run) + return _c +} + +// GetOrCreateByOrgID provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) GetOrCreateByOrgID(ctx context.Context, tx shared.DB, orgID uuid.UUID) (models.DependencyProxySecret, error) { + ret := _mock.Called(ctx, tx, orgID) + + if len(ret) == 0 { + panic("no return value specified for GetOrCreateByOrgID") + } + + var r0 models.DependencyProxySecret + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) (models.DependencyProxySecret, error)); ok { + return returnFunc(ctx, tx, orgID) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) models.DependencyProxySecret); ok { + r0 = returnFunc(ctx, tx, orgID) + } else { + r0 = ret.Get(0).(models.DependencyProxySecret) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, shared.DB, uuid.UUID) error); ok { + r1 = returnFunc(ctx, tx, orgID) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// DependencyProxySecretRepository_GetOrCreateByOrgID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOrCreateByOrgID' +type DependencyProxySecretRepository_GetOrCreateByOrgID_Call struct { + *mock.Call +} + +// GetOrCreateByOrgID is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - orgID uuid.UUID +func (_e *DependencyProxySecretRepository_Expecter) GetOrCreateByOrgID(ctx interface{}, tx interface{}, orgID interface{}) *DependencyProxySecretRepository_GetOrCreateByOrgID_Call { + return &DependencyProxySecretRepository_GetOrCreateByOrgID_Call{Call: _e.mock.On("GetOrCreateByOrgID", ctx, tx, orgID)} +} + +func (_c *DependencyProxySecretRepository_GetOrCreateByOrgID_Call) Run(run func(ctx context.Context, tx shared.DB, orgID uuid.UUID)) *DependencyProxySecretRepository_GetOrCreateByOrgID_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + var arg2 uuid.UUID + if args[2] != nil { + arg2 = args[2].(uuid.UUID) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_GetOrCreateByOrgID_Call) Return(dependencyProxySecret models.DependencyProxySecret, err error) *DependencyProxySecretRepository_GetOrCreateByOrgID_Call { + _c.Call.Return(dependencyProxySecret, err) + return _c +} + +func (_c *DependencyProxySecretRepository_GetOrCreateByOrgID_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, orgID uuid.UUID) (models.DependencyProxySecret, error)) *DependencyProxySecretRepository_GetOrCreateByOrgID_Call { + _c.Call.Return(run) + return _c +} + +// GetOrCreateByProjectID provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) GetOrCreateByProjectID(ctx context.Context, tx shared.DB, projectID uuid.UUID) (models.DependencyProxySecret, error) { + ret := _mock.Called(ctx, tx, projectID) + + if len(ret) == 0 { + panic("no return value specified for GetOrCreateByProjectID") + } + + var r0 models.DependencyProxySecret + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) (models.DependencyProxySecret, error)); ok { + return returnFunc(ctx, tx, projectID) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) models.DependencyProxySecret); ok { + r0 = returnFunc(ctx, tx, projectID) + } else { + r0 = ret.Get(0).(models.DependencyProxySecret) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, shared.DB, uuid.UUID) error); ok { + r1 = returnFunc(ctx, tx, projectID) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// DependencyProxySecretRepository_GetOrCreateByProjectID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOrCreateByProjectID' +type DependencyProxySecretRepository_GetOrCreateByProjectID_Call struct { + *mock.Call +} + +// GetOrCreateByProjectID is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - projectID uuid.UUID +func (_e *DependencyProxySecretRepository_Expecter) GetOrCreateByProjectID(ctx interface{}, tx interface{}, projectID interface{}) *DependencyProxySecretRepository_GetOrCreateByProjectID_Call { + return &DependencyProxySecretRepository_GetOrCreateByProjectID_Call{Call: _e.mock.On("GetOrCreateByProjectID", ctx, tx, projectID)} +} + +func (_c *DependencyProxySecretRepository_GetOrCreateByProjectID_Call) Run(run func(ctx context.Context, tx shared.DB, projectID uuid.UUID)) *DependencyProxySecretRepository_GetOrCreateByProjectID_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + var arg2 uuid.UUID + if args[2] != nil { + arg2 = args[2].(uuid.UUID) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_GetOrCreateByProjectID_Call) Return(dependencyProxySecret models.DependencyProxySecret, err error) *DependencyProxySecretRepository_GetOrCreateByProjectID_Call { + _c.Call.Return(dependencyProxySecret, err) + return _c +} + +func (_c *DependencyProxySecretRepository_GetOrCreateByProjectID_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, projectID uuid.UUID) (models.DependencyProxySecret, error)) *DependencyProxySecretRepository_GetOrCreateByProjectID_Call { + _c.Call.Return(run) + return _c +} + +// List provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) List(ctx context.Context, tx shared.DB, ids []uuid.UUID) ([]models.DependencyProxySecret, error) { + ret := _mock.Called(ctx, tx, ids) + + if len(ret) == 0 { + panic("no return value specified for List") + } + + var r0 []models.DependencyProxySecret + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, []uuid.UUID) ([]models.DependencyProxySecret, error)); ok { + return returnFunc(ctx, tx, ids) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, []uuid.UUID) []models.DependencyProxySecret); ok { + r0 = returnFunc(ctx, tx, ids) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.DependencyProxySecret) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, shared.DB, []uuid.UUID) error); ok { + r1 = returnFunc(ctx, tx, ids) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// DependencyProxySecretRepository_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List' +type DependencyProxySecretRepository_List_Call struct { + *mock.Call +} + +// List is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - ids []uuid.UUID +func (_e *DependencyProxySecretRepository_Expecter) List(ctx interface{}, tx interface{}, ids interface{}) *DependencyProxySecretRepository_List_Call { + return &DependencyProxySecretRepository_List_Call{Call: _e.mock.On("List", ctx, tx, ids)} +} + +func (_c *DependencyProxySecretRepository_List_Call) Run(run func(ctx context.Context, tx shared.DB, ids []uuid.UUID)) *DependencyProxySecretRepository_List_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + var arg2 []uuid.UUID + if args[2] != nil { + arg2 = args[2].([]uuid.UUID) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_List_Call) Return(dependencyProxySecrets []models.DependencyProxySecret, err error) *DependencyProxySecretRepository_List_Call { + _c.Call.Return(dependencyProxySecrets, err) + return _c +} + +func (_c *DependencyProxySecretRepository_List_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, ids []uuid.UUID) ([]models.DependencyProxySecret, error)) *DependencyProxySecretRepository_List_Call { + _c.Call.Return(run) + return _c +} + +// Read provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) Read(ctx context.Context, tx shared.DB, id uuid.UUID) (models.DependencyProxySecret, error) { + ret := _mock.Called(ctx, tx, id) + + if len(ret) == 0 { + panic("no return value specified for Read") + } + + var r0 models.DependencyProxySecret + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) (models.DependencyProxySecret, error)); ok { + return returnFunc(ctx, tx, id) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) models.DependencyProxySecret); ok { + r0 = returnFunc(ctx, tx, id) + } else { + r0 = ret.Get(0).(models.DependencyProxySecret) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, shared.DB, uuid.UUID) error); ok { + r1 = returnFunc(ctx, tx, id) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// DependencyProxySecretRepository_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' +type DependencyProxySecretRepository_Read_Call struct { + *mock.Call +} + +// Read is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - id uuid.UUID +func (_e *DependencyProxySecretRepository_Expecter) Read(ctx interface{}, tx interface{}, id interface{}) *DependencyProxySecretRepository_Read_Call { + return &DependencyProxySecretRepository_Read_Call{Call: _e.mock.On("Read", ctx, tx, id)} +} + +func (_c *DependencyProxySecretRepository_Read_Call) Run(run func(ctx context.Context, tx shared.DB, id uuid.UUID)) *DependencyProxySecretRepository_Read_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + var arg2 uuid.UUID + if args[2] != nil { + arg2 = args[2].(uuid.UUID) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_Read_Call) Return(dependencyProxySecret models.DependencyProxySecret, err error) *DependencyProxySecretRepository_Read_Call { + _c.Call.Return(dependencyProxySecret, err) + return _c +} + +func (_c *DependencyProxySecretRepository_Read_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, id uuid.UUID) (models.DependencyProxySecret, error)) *DependencyProxySecretRepository_Read_Call { + _c.Call.Return(run) + return _c +} + +// Save provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) Save(ctx context.Context, tx shared.DB, t *models.DependencyProxySecret) error { + ret := _mock.Called(ctx, tx, t) + + if len(ret) == 0 { + panic("no return value specified for Save") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, *models.DependencyProxySecret) error); ok { + r0 = returnFunc(ctx, tx, t) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// DependencyProxySecretRepository_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save' +type DependencyProxySecretRepository_Save_Call struct { + *mock.Call +} + +// Save is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - t *models.DependencyProxySecret +func (_e *DependencyProxySecretRepository_Expecter) Save(ctx interface{}, tx interface{}, t interface{}) *DependencyProxySecretRepository_Save_Call { + return &DependencyProxySecretRepository_Save_Call{Call: _e.mock.On("Save", ctx, tx, t)} +} + +func (_c *DependencyProxySecretRepository_Save_Call) Run(run func(ctx context.Context, tx shared.DB, t *models.DependencyProxySecret)) *DependencyProxySecretRepository_Save_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + var arg2 *models.DependencyProxySecret + if args[2] != nil { + arg2 = args[2].(*models.DependencyProxySecret) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_Save_Call) Return(err error) *DependencyProxySecretRepository_Save_Call { + _c.Call.Return(err) + return _c +} + +func (_c *DependencyProxySecretRepository_Save_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, t *models.DependencyProxySecret) error) *DependencyProxySecretRepository_Save_Call { + _c.Call.Return(run) + return _c +} + +// SaveBatch provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) SaveBatch(ctx context.Context, tx shared.DB, ts []models.DependencyProxySecret) error { + ret := _mock.Called(ctx, tx, ts) + + if len(ret) == 0 { + panic("no return value specified for SaveBatch") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, []models.DependencyProxySecret) error); ok { + r0 = returnFunc(ctx, tx, ts) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// DependencyProxySecretRepository_SaveBatch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveBatch' +type DependencyProxySecretRepository_SaveBatch_Call struct { + *mock.Call +} + +// SaveBatch is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - ts []models.DependencyProxySecret +func (_e *DependencyProxySecretRepository_Expecter) SaveBatch(ctx interface{}, tx interface{}, ts interface{}) *DependencyProxySecretRepository_SaveBatch_Call { + return &DependencyProxySecretRepository_SaveBatch_Call{Call: _e.mock.On("SaveBatch", ctx, tx, ts)} +} + +func (_c *DependencyProxySecretRepository_SaveBatch_Call) Run(run func(ctx context.Context, tx shared.DB, ts []models.DependencyProxySecret)) *DependencyProxySecretRepository_SaveBatch_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + var arg2 []models.DependencyProxySecret + if args[2] != nil { + arg2 = args[2].([]models.DependencyProxySecret) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_SaveBatch_Call) Return(err error) *DependencyProxySecretRepository_SaveBatch_Call { + _c.Call.Return(err) + return _c +} + +func (_c *DependencyProxySecretRepository_SaveBatch_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, ts []models.DependencyProxySecret) error) *DependencyProxySecretRepository_SaveBatch_Call { + _c.Call.Return(run) + return _c +} + +// SaveBatchBestEffort provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) SaveBatchBestEffort(ctx context.Context, tx shared.DB, ts []models.DependencyProxySecret) error { + ret := _mock.Called(ctx, tx, ts) + + if len(ret) == 0 { + panic("no return value specified for SaveBatchBestEffort") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, []models.DependencyProxySecret) error); ok { + r0 = returnFunc(ctx, tx, ts) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// DependencyProxySecretRepository_SaveBatchBestEffort_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveBatchBestEffort' +type DependencyProxySecretRepository_SaveBatchBestEffort_Call struct { + *mock.Call +} + +// SaveBatchBestEffort is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - ts []models.DependencyProxySecret +func (_e *DependencyProxySecretRepository_Expecter) SaveBatchBestEffort(ctx interface{}, tx interface{}, ts interface{}) *DependencyProxySecretRepository_SaveBatchBestEffort_Call { + return &DependencyProxySecretRepository_SaveBatchBestEffort_Call{Call: _e.mock.On("SaveBatchBestEffort", ctx, tx, ts)} +} + +func (_c *DependencyProxySecretRepository_SaveBatchBestEffort_Call) Run(run func(ctx context.Context, tx shared.DB, ts []models.DependencyProxySecret)) *DependencyProxySecretRepository_SaveBatchBestEffort_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + var arg2 []models.DependencyProxySecret + if args[2] != nil { + arg2 = args[2].([]models.DependencyProxySecret) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_SaveBatchBestEffort_Call) Return(err error) *DependencyProxySecretRepository_SaveBatchBestEffort_Call { + _c.Call.Return(err) + return _c +} + +func (_c *DependencyProxySecretRepository_SaveBatchBestEffort_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, ts []models.DependencyProxySecret) error) *DependencyProxySecretRepository_SaveBatchBestEffort_Call { + _c.Call.Return(run) + return _c +} + +// Transaction provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) Transaction(ctx context.Context, fn func(tx shared.DB) error) error { + ret := _mock.Called(ctx, fn) + + if len(ret) == 0 { + panic("no return value specified for Transaction") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, func(tx shared.DB) error) error); ok { + r0 = returnFunc(ctx, fn) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// DependencyProxySecretRepository_Transaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Transaction' +type DependencyProxySecretRepository_Transaction_Call struct { + *mock.Call +} + +// Transaction is a helper method to define mock.On call +// - ctx context.Context +// - fn func(tx shared.DB) error +func (_e *DependencyProxySecretRepository_Expecter) Transaction(ctx interface{}, fn interface{}) *DependencyProxySecretRepository_Transaction_Call { + return &DependencyProxySecretRepository_Transaction_Call{Call: _e.mock.On("Transaction", ctx, fn)} +} + +func (_c *DependencyProxySecretRepository_Transaction_Call) Run(run func(ctx context.Context, fn func(tx shared.DB) error)) *DependencyProxySecretRepository_Transaction_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 func(tx shared.DB) error + if args[1] != nil { + arg1 = args[1].(func(tx shared.DB) error) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_Transaction_Call) Return(err error) *DependencyProxySecretRepository_Transaction_Call { + _c.Call.Return(err) + return _c +} + +func (_c *DependencyProxySecretRepository_Transaction_Call) RunAndReturn(run func(ctx context.Context, fn func(tx shared.DB) error) error) *DependencyProxySecretRepository_Transaction_Call { + _c.Call.Return(run) + return _c +} + +// UpdateSecret provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) UpdateSecret(ctx context.Context, tx shared.DB, proxy models.DependencyProxySecret) (models.DependencyProxySecret, error) { + ret := _mock.Called(ctx, tx, proxy) + + if len(ret) == 0 { + panic("no return value specified for UpdateSecret") + } + + var r0 models.DependencyProxySecret + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, models.DependencyProxySecret) (models.DependencyProxySecret, error)); ok { + return returnFunc(ctx, tx, proxy) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, models.DependencyProxySecret) models.DependencyProxySecret); ok { + r0 = returnFunc(ctx, tx, proxy) + } else { + r0 = ret.Get(0).(models.DependencyProxySecret) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, shared.DB, models.DependencyProxySecret) error); ok { + r1 = returnFunc(ctx, tx, proxy) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// DependencyProxySecretRepository_UpdateSecret_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateSecret' +type DependencyProxySecretRepository_UpdateSecret_Call struct { + *mock.Call +} + +// UpdateSecret is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - proxy models.DependencyProxySecret +func (_e *DependencyProxySecretRepository_Expecter) UpdateSecret(ctx interface{}, tx interface{}, proxy interface{}) *DependencyProxySecretRepository_UpdateSecret_Call { + return &DependencyProxySecretRepository_UpdateSecret_Call{Call: _e.mock.On("UpdateSecret", ctx, tx, proxy)} +} + +func (_c *DependencyProxySecretRepository_UpdateSecret_Call) Run(run func(ctx context.Context, tx shared.DB, proxy models.DependencyProxySecret)) *DependencyProxySecretRepository_UpdateSecret_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + var arg2 models.DependencyProxySecret + if args[2] != nil { + arg2 = args[2].(models.DependencyProxySecret) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_UpdateSecret_Call) Return(dependencyProxySecret models.DependencyProxySecret, err error) *DependencyProxySecretRepository_UpdateSecret_Call { + _c.Call.Return(dependencyProxySecret, err) + return _c +} + +func (_c *DependencyProxySecretRepository_UpdateSecret_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, proxy models.DependencyProxySecret) (models.DependencyProxySecret, error)) *DependencyProxySecretRepository_UpdateSecret_Call { + _c.Call.Return(run) + return _c +} + +// Upsert provides a mock function for the type DependencyProxySecretRepository +func (_mock *DependencyProxySecretRepository) Upsert(ctx context.Context, tx shared.DB, t *[]*models.DependencyProxySecret, conflictingColumns []clause.Column, updateOnly []string) error { + ret := _mock.Called(ctx, tx, t, conflictingColumns, updateOnly) + + if len(ret) == 0 { + panic("no return value specified for Upsert") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, *[]*models.DependencyProxySecret, []clause.Column, []string) error); ok { + r0 = returnFunc(ctx, tx, t, conflictingColumns, updateOnly) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// DependencyProxySecretRepository_Upsert_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Upsert' +type DependencyProxySecretRepository_Upsert_Call struct { + *mock.Call +} + +// Upsert is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - t *[]*models.DependencyProxySecret +// - conflictingColumns []clause.Column +// - updateOnly []string +func (_e *DependencyProxySecretRepository_Expecter) Upsert(ctx interface{}, tx interface{}, t interface{}, conflictingColumns interface{}, updateOnly interface{}) *DependencyProxySecretRepository_Upsert_Call { + return &DependencyProxySecretRepository_Upsert_Call{Call: _e.mock.On("Upsert", ctx, tx, t, conflictingColumns, updateOnly)} +} + +func (_c *DependencyProxySecretRepository_Upsert_Call) Run(run func(ctx context.Context, tx shared.DB, t *[]*models.DependencyProxySecret, conflictingColumns []clause.Column, updateOnly []string)) *DependencyProxySecretRepository_Upsert_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 shared.DB + if args[1] != nil { + arg1 = args[1].(shared.DB) + } + var arg2 *[]*models.DependencyProxySecret + if args[2] != nil { + arg2 = args[2].(*[]*models.DependencyProxySecret) + } + var arg3 []clause.Column + if args[3] != nil { + arg3 = args[3].([]clause.Column) + } + var arg4 []string + if args[4] != nil { + arg4 = args[4].([]string) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + ) + }) + return _c +} + +func (_c *DependencyProxySecretRepository_Upsert_Call) Return(err error) *DependencyProxySecretRepository_Upsert_Call { + _c.Call.Return(err) + return _c +} + +func (_c *DependencyProxySecretRepository_Upsert_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, t *[]*models.DependencyProxySecret, conflictingColumns []clause.Column, updateOnly []string) error) *DependencyProxySecretRepository_Upsert_Call { + _c.Call.Return(run) + return _c +} diff --git a/mocks/mock_DependencyProxySecretService.go b/mocks/mock_DependencyProxySecretService.go new file mode 100644 index 00000000..674b3046 --- /dev/null +++ b/mocks/mock_DependencyProxySecretService.go @@ -0,0 +1,378 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "context" + + "github.com/google/uuid" + "github.com/l3montree-dev/devguard/database/models" + mock "github.com/stretchr/testify/mock" +) + +// NewDependencyProxySecretService creates a new instance of DependencyProxySecretService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDependencyProxySecretService(t interface { + mock.TestingT + Cleanup(func()) +}) *DependencyProxySecretService { + mock := &DependencyProxySecretService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// DependencyProxySecretService is an autogenerated mock type for the DependencyProxySecretService type +type DependencyProxySecretService struct { + mock.Mock +} + +type DependencyProxySecretService_Expecter struct { + mock *mock.Mock +} + +func (_m *DependencyProxySecretService) EXPECT() *DependencyProxySecretService_Expecter { + return &DependencyProxySecretService_Expecter{mock: &_m.Mock} +} + +// GetModelBySecret provides a mock function for the type DependencyProxySecretService +func (_mock *DependencyProxySecretService) GetModelBySecret(ctx context.Context, secret uuid.UUID) (string, uuid.UUID, error) { + ret := _mock.Called(ctx, secret) + + if len(ret) == 0 { + panic("no return value specified for GetModelBySecret") + } + + var r0 string + var r1 uuid.UUID + var r2 error + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID) (string, uuid.UUID, error)); ok { + return returnFunc(ctx, secret) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID) string); ok { + r0 = returnFunc(ctx, secret) + } else { + r0 = ret.Get(0).(string) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, uuid.UUID) uuid.UUID); ok { + r1 = returnFunc(ctx, secret) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(uuid.UUID) + } + } + if returnFunc, ok := ret.Get(2).(func(context.Context, uuid.UUID) error); ok { + r2 = returnFunc(ctx, secret) + } else { + r2 = ret.Error(2) + } + return r0, r1, r2 +} + +// DependencyProxySecretService_GetModelBySecret_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetModelBySecret' +type DependencyProxySecretService_GetModelBySecret_Call struct { + *mock.Call +} + +// GetModelBySecret is a helper method to define mock.On call +// - ctx context.Context +// - secret uuid.UUID +func (_e *DependencyProxySecretService_Expecter) GetModelBySecret(ctx interface{}, secret interface{}) *DependencyProxySecretService_GetModelBySecret_Call { + return &DependencyProxySecretService_GetModelBySecret_Call{Call: _e.mock.On("GetModelBySecret", ctx, secret)} +} + +func (_c *DependencyProxySecretService_GetModelBySecret_Call) Run(run func(ctx context.Context, secret uuid.UUID)) *DependencyProxySecretService_GetModelBySecret_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 uuid.UUID + if args[1] != nil { + arg1 = args[1].(uuid.UUID) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *DependencyProxySecretService_GetModelBySecret_Call) Return(s string, uUID uuid.UUID, err error) *DependencyProxySecretService_GetModelBySecret_Call { + _c.Call.Return(s, uUID, err) + return _c +} + +func (_c *DependencyProxySecretService_GetModelBySecret_Call) RunAndReturn(run func(ctx context.Context, secret uuid.UUID) (string, uuid.UUID, error)) *DependencyProxySecretService_GetModelBySecret_Call { + _c.Call.Return(run) + return _c +} + +// GetOrCreateByAssetID provides a mock function for the type DependencyProxySecretService +func (_mock *DependencyProxySecretService) GetOrCreateByAssetID(ctx context.Context, assetID uuid.UUID) (models.DependencyProxySecret, error) { + ret := _mock.Called(ctx, assetID) + + if len(ret) == 0 { + panic("no return value specified for GetOrCreateByAssetID") + } + + var r0 models.DependencyProxySecret + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID) (models.DependencyProxySecret, error)); ok { + return returnFunc(ctx, assetID) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID) models.DependencyProxySecret); ok { + r0 = returnFunc(ctx, assetID) + } else { + r0 = ret.Get(0).(models.DependencyProxySecret) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { + r1 = returnFunc(ctx, assetID) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// DependencyProxySecretService_GetOrCreateByAssetID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOrCreateByAssetID' +type DependencyProxySecretService_GetOrCreateByAssetID_Call struct { + *mock.Call +} + +// GetOrCreateByAssetID is a helper method to define mock.On call +// - ctx context.Context +// - assetID uuid.UUID +func (_e *DependencyProxySecretService_Expecter) GetOrCreateByAssetID(ctx interface{}, assetID interface{}) *DependencyProxySecretService_GetOrCreateByAssetID_Call { + return &DependencyProxySecretService_GetOrCreateByAssetID_Call{Call: _e.mock.On("GetOrCreateByAssetID", ctx, assetID)} +} + +func (_c *DependencyProxySecretService_GetOrCreateByAssetID_Call) Run(run func(ctx context.Context, assetID uuid.UUID)) *DependencyProxySecretService_GetOrCreateByAssetID_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 uuid.UUID + if args[1] != nil { + arg1 = args[1].(uuid.UUID) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *DependencyProxySecretService_GetOrCreateByAssetID_Call) Return(dependencyProxySecret models.DependencyProxySecret, err error) *DependencyProxySecretService_GetOrCreateByAssetID_Call { + _c.Call.Return(dependencyProxySecret, err) + return _c +} + +func (_c *DependencyProxySecretService_GetOrCreateByAssetID_Call) RunAndReturn(run func(ctx context.Context, assetID uuid.UUID) (models.DependencyProxySecret, error)) *DependencyProxySecretService_GetOrCreateByAssetID_Call { + _c.Call.Return(run) + return _c +} + +// GetOrCreateByOrgID provides a mock function for the type DependencyProxySecretService +func (_mock *DependencyProxySecretService) GetOrCreateByOrgID(ctx context.Context, orgID uuid.UUID) (models.DependencyProxySecret, error) { + ret := _mock.Called(ctx, orgID) + + if len(ret) == 0 { + panic("no return value specified for GetOrCreateByOrgID") + } + + var r0 models.DependencyProxySecret + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID) (models.DependencyProxySecret, error)); ok { + return returnFunc(ctx, orgID) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID) models.DependencyProxySecret); ok { + r0 = returnFunc(ctx, orgID) + } else { + r0 = ret.Get(0).(models.DependencyProxySecret) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { + r1 = returnFunc(ctx, orgID) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// DependencyProxySecretService_GetOrCreateByOrgID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOrCreateByOrgID' +type DependencyProxySecretService_GetOrCreateByOrgID_Call struct { + *mock.Call +} + +// GetOrCreateByOrgID is a helper method to define mock.On call +// - ctx context.Context +// - orgID uuid.UUID +func (_e *DependencyProxySecretService_Expecter) GetOrCreateByOrgID(ctx interface{}, orgID interface{}) *DependencyProxySecretService_GetOrCreateByOrgID_Call { + return &DependencyProxySecretService_GetOrCreateByOrgID_Call{Call: _e.mock.On("GetOrCreateByOrgID", ctx, orgID)} +} + +func (_c *DependencyProxySecretService_GetOrCreateByOrgID_Call) Run(run func(ctx context.Context, orgID uuid.UUID)) *DependencyProxySecretService_GetOrCreateByOrgID_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 uuid.UUID + if args[1] != nil { + arg1 = args[1].(uuid.UUID) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *DependencyProxySecretService_GetOrCreateByOrgID_Call) Return(dependencyProxySecret models.DependencyProxySecret, err error) *DependencyProxySecretService_GetOrCreateByOrgID_Call { + _c.Call.Return(dependencyProxySecret, err) + return _c +} + +func (_c *DependencyProxySecretService_GetOrCreateByOrgID_Call) RunAndReturn(run func(ctx context.Context, orgID uuid.UUID) (models.DependencyProxySecret, error)) *DependencyProxySecretService_GetOrCreateByOrgID_Call { + _c.Call.Return(run) + return _c +} + +// GetOrCreateByProjectID provides a mock function for the type DependencyProxySecretService +func (_mock *DependencyProxySecretService) GetOrCreateByProjectID(ctx context.Context, projectID uuid.UUID) (models.DependencyProxySecret, error) { + ret := _mock.Called(ctx, projectID) + + if len(ret) == 0 { + panic("no return value specified for GetOrCreateByProjectID") + } + + var r0 models.DependencyProxySecret + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID) (models.DependencyProxySecret, error)); ok { + return returnFunc(ctx, projectID) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID) models.DependencyProxySecret); ok { + r0 = returnFunc(ctx, projectID) + } else { + r0 = ret.Get(0).(models.DependencyProxySecret) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { + r1 = returnFunc(ctx, projectID) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// DependencyProxySecretService_GetOrCreateByProjectID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOrCreateByProjectID' +type DependencyProxySecretService_GetOrCreateByProjectID_Call struct { + *mock.Call +} + +// GetOrCreateByProjectID is a helper method to define mock.On call +// - ctx context.Context +// - projectID uuid.UUID +func (_e *DependencyProxySecretService_Expecter) GetOrCreateByProjectID(ctx interface{}, projectID interface{}) *DependencyProxySecretService_GetOrCreateByProjectID_Call { + return &DependencyProxySecretService_GetOrCreateByProjectID_Call{Call: _e.mock.On("GetOrCreateByProjectID", ctx, projectID)} +} + +func (_c *DependencyProxySecretService_GetOrCreateByProjectID_Call) Run(run func(ctx context.Context, projectID uuid.UUID)) *DependencyProxySecretService_GetOrCreateByProjectID_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 uuid.UUID + if args[1] != nil { + arg1 = args[1].(uuid.UUID) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *DependencyProxySecretService_GetOrCreateByProjectID_Call) Return(dependencyProxySecret models.DependencyProxySecret, err error) *DependencyProxySecretService_GetOrCreateByProjectID_Call { + _c.Call.Return(dependencyProxySecret, err) + return _c +} + +func (_c *DependencyProxySecretService_GetOrCreateByProjectID_Call) RunAndReturn(run func(ctx context.Context, projectID uuid.UUID) (models.DependencyProxySecret, error)) *DependencyProxySecretService_GetOrCreateByProjectID_Call { + _c.Call.Return(run) + return _c +} + +// UpdateSecret provides a mock function for the type DependencyProxySecretService +func (_mock *DependencyProxySecretService) UpdateSecret(ctx context.Context, proxy models.DependencyProxySecret) (models.DependencyProxySecret, error) { + ret := _mock.Called(ctx, proxy) + + if len(ret) == 0 { + panic("no return value specified for UpdateSecret") + } + + var r0 models.DependencyProxySecret + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, models.DependencyProxySecret) (models.DependencyProxySecret, error)); ok { + return returnFunc(ctx, proxy) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, models.DependencyProxySecret) models.DependencyProxySecret); ok { + r0 = returnFunc(ctx, proxy) + } else { + r0 = ret.Get(0).(models.DependencyProxySecret) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, models.DependencyProxySecret) error); ok { + r1 = returnFunc(ctx, proxy) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// DependencyProxySecretService_UpdateSecret_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateSecret' +type DependencyProxySecretService_UpdateSecret_Call struct { + *mock.Call +} + +// UpdateSecret is a helper method to define mock.On call +// - ctx context.Context +// - proxy models.DependencyProxySecret +func (_e *DependencyProxySecretService_Expecter) UpdateSecret(ctx interface{}, proxy interface{}) *DependencyProxySecretService_UpdateSecret_Call { + return &DependencyProxySecretService_UpdateSecret_Call{Call: _e.mock.On("UpdateSecret", ctx, proxy)} +} + +func (_c *DependencyProxySecretService_UpdateSecret_Call) Run(run func(ctx context.Context, proxy models.DependencyProxySecret)) *DependencyProxySecretService_UpdateSecret_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 models.DependencyProxySecret + if args[1] != nil { + arg1 = args[1].(models.DependencyProxySecret) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *DependencyProxySecretService_UpdateSecret_Call) Return(dependencyProxySecret models.DependencyProxySecret, err error) *DependencyProxySecretService_UpdateSecret_Call { + _c.Call.Return(dependencyProxySecret, err) + return _c +} + +func (_c *DependencyProxySecretService_UpdateSecret_Call) RunAndReturn(run func(ctx context.Context, proxy models.DependencyProxySecret) (models.DependencyProxySecret, error)) *DependencyProxySecretService_UpdateSecret_Call { + _c.Call.Return(run) + return _c +} diff --git a/mocks/mock_MaliciousPackageChecker.go b/mocks/mock_MaliciousPackageChecker.go index 986c31a1..cf3a2922 100644 --- a/mocks/mock_MaliciousPackageChecker.go +++ b/mocks/mock_MaliciousPackageChecker.go @@ -90,7 +90,7 @@ func (_c *MaliciousPackageChecker_DownloadAndProcessDB_Call) RunAndReturn(run fu } // IsMalicious provides a mock function for the type MaliciousPackageChecker -func (_mock *MaliciousPackageChecker) IsMalicious(ctx context.Context, ecosystem string, packageName string, version string) (bool, *dtos.OSV) { +func (_mock *MaliciousPackageChecker) IsMalicious(ctx context.Context, ecosystem string, packageName string, version string) (bool, *dtos.OSV, error) { ret := _mock.Called(ctx, ecosystem, packageName, version) if len(ret) == 0 { @@ -99,7 +99,8 @@ func (_mock *MaliciousPackageChecker) IsMalicious(ctx context.Context, ecosystem var r0 bool var r1 *dtos.OSV - if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) (bool, *dtos.OSV)); ok { + var r2 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) (bool, *dtos.OSV, error)); ok { return returnFunc(ctx, ecosystem, packageName, version) } if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) bool); ok { @@ -114,7 +115,12 @@ func (_mock *MaliciousPackageChecker) IsMalicious(ctx context.Context, ecosystem r1 = ret.Get(1).(*dtos.OSV) } } - return r0, r1 + if returnFunc, ok := ret.Get(2).(func(context.Context, string, string, string) error); ok { + r2 = returnFunc(ctx, ecosystem, packageName, version) + } else { + r2 = ret.Error(2) + } + return r0, r1, r2 } // MaliciousPackageChecker_IsMalicious_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsMalicious' @@ -159,12 +165,12 @@ func (_c *MaliciousPackageChecker_IsMalicious_Call) Run(run func(ctx context.Con return _c } -func (_c *MaliciousPackageChecker_IsMalicious_Call) Return(b bool, oSV *dtos.OSV) *MaliciousPackageChecker_IsMalicious_Call { - _c.Call.Return(b, oSV) +func (_c *MaliciousPackageChecker_IsMalicious_Call) Return(b bool, oSV *dtos.OSV, err error) *MaliciousPackageChecker_IsMalicious_Call { + _c.Call.Return(b, oSV, err) return _c } -func (_c *MaliciousPackageChecker_IsMalicious_Call) RunAndReturn(run func(ctx context.Context, ecosystem string, packageName string, version string) (bool, *dtos.OSV)) *MaliciousPackageChecker_IsMalicious_Call { +func (_c *MaliciousPackageChecker_IsMalicious_Call) RunAndReturn(run func(ctx context.Context, ecosystem string, packageName string, version string) (bool, *dtos.OSV, error)) *MaliciousPackageChecker_IsMalicious_Call { _c.Call.Return(run) return _c } diff --git a/router/asset_router.go b/router/asset_router.go index ba510f31..dc730c80 100644 --- a/router/asset_router.go +++ b/router/asset_router.go @@ -29,6 +29,7 @@ type AssetRouter struct { func NewAssetRouter( projectGroup ProjectRouter, assetController *controllers.AssetController, + dependencyProxyController *controllers.DependencyProxyController, assetVersionController *controllers.AssetVersionController, complianceController *controllers.ComplianceController, statisticsController *controllers.StatisticsController, @@ -52,6 +53,7 @@ func NewAssetRouter( assetRouter.GET("/number-of-exploits/", statisticsController.GetCVESWithKnownExploits) assetRouter.GET("/components/licenses/", componentController.LicenseDistribution) assetRouter.GET("/config-files/:config-file/", assetController.GetConfigFile) + assetRouter.GET("/dependency-proxy-urls/", dependencyProxyController.GetDependencyProxyURLs) assetRouter.GET("/refs/", assetVersionController.GetAssetVersionsByAssetID) assetRouter.PUT("/config-files/:config-file/", assetController.UpdateConfigFile, middlewares.NeededScope([]string{"manage"}), assetScopedRBAC(shared.ObjectAsset, shared.ActionUpdate)) assetRouter.GET("/in-toto/root.layout.json/", intotoController.RootLayout) diff --git a/router/org_router.go b/router/org_router.go index 31c97419..c524c527 100644 --- a/router/org_router.go +++ b/router/org_router.go @@ -31,6 +31,7 @@ func NewOrgRouter( sessionGroup SessionRouter, orgController *controllers.OrgController, projectController *controllers.ProjectController, + dependencyProxyController *controllers.DependencyProxyController, dependencyVulnController *controllers.DependencyVulnController, firstPartyVulnController *controllers.FirstPartyVulnController, policyController *controllers.PolicyController, @@ -63,6 +64,7 @@ func NewOrgRouter( organizationRouter.GET("/stats/vuln-statistics/", statisticsController.GetOrgStatistics, middlewares.NeededScope([]string{"manage"}), middlewares.OrganizationAccessControlMiddleware(shared.ObjectOrganization, shared.ActionUpdate)) // use ActionUpdate to control access only for admin users and above organizationRouter.GET("/config-files/:config-file/", orgController.GetConfigFile) + organizationRouter.GET("/dependency-proxy-urls/", dependencyProxyController.GetDependencyProxyURLs) organizationRouter.PUT("/config-files/:config-file/", orgController.UpdateConfigFile, middlewares.NeededScope([]string{"manage"}), middlewares.OrganizationAccessControlMiddleware(shared.ObjectOrganization, shared.ActionUpdate)) organizationRouter.GET("/trigger-sync/", externalEntityProviderService.TriggerSync) organizationRouter.GET("/", orgController.Read) diff --git a/router/project_router.go b/router/project_router.go index 18680782..7fdb4548 100644 --- a/router/project_router.go +++ b/router/project_router.go @@ -30,6 +30,7 @@ func NewProjectRouter( organizationGroup OrgRouter, projectController *controllers.ProjectController, assetController *controllers.AssetController, + dependencyProxyController *controllers.DependencyProxyController, dependencyVulnController *controllers.DependencyVulnController, policyController *controllers.PolicyController, releaseController *controllers.ReleaseController, @@ -52,6 +53,7 @@ func NewProjectRouter( projectRouter.GET("/assets/", assetController.List) projectRouter.GET("/members/", projectController.Members) projectRouter.GET("/config-files/:config-file/", projectController.GetConfigFile) + projectRouter.GET("/dependency-proxy-urls/", dependencyProxyController.GetDependencyProxyURLs) projectRouter.PUT("/config-files/:config-file/", projectController.UpdateConfigFile, middlewares.NeededScope([]string{"manage"}), projectScopedRBAC(shared.ObjectProject, shared.ActionUpdate)) projectRouter.GET("/releases/:releaseID/sbom.json/", releaseController.SBOMJSON) projectRouter.GET("/releases/:releaseID/sbom.xml/", releaseController.SBOMXML) diff --git a/router/providers.go b/router/providers.go index a544a6ff..52e73688 100644 --- a/router/providers.go +++ b/router/providers.go @@ -16,6 +16,7 @@ var RouterModule = fx.Options( fx.Provide(NewShareRouter), fx.Provide(NewVulnDBRouter), fx.Provide(NewDependencyProxyRouter), + fx.Provide(NewShareDependencyProxyRouter), fx.Provide(NewVEXRuleRouter), fx.Provide(NewExternalReferenceRouter), ) diff --git a/router/share_dependency_proxy_router.go b/router/share_dependency_proxy_router.go new file mode 100644 index 00000000..a48167c9 --- /dev/null +++ b/router/share_dependency_proxy_router.go @@ -0,0 +1,42 @@ +// 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 . + +package router + +import ( + "github.com/l3montree-dev/devguard/controllers" + "github.com/labstack/echo/v4" +) + +type ShareDependencyProxyRouter struct { + *echo.Group +} + +func NewShareDependencyProxyRouter( + apiV1Group APIV1Router, + dependencyProxyController *controllers.DependencyProxyController, +) ShareDependencyProxyRouter { + shareDependencyProxyRouter := apiV1Group.Group.Group("/dependency-proxy/:secret") + + shareDependencyProxyRouter.GET("/npm", dependencyProxyController.ProxyNPM) + shareDependencyProxyRouter.GET("/npm/*", dependencyProxyController.ProxyNPM) + + shareDependencyProxyRouter.GET("/go", dependencyProxyController.ProxyGo) + shareDependencyProxyRouter.GET("/go/*", dependencyProxyController.ProxyGo) + shareDependencyProxyRouter.GET("/pypi", dependencyProxyController.ProxyPyPI) + shareDependencyProxyRouter.GET("/pypi/*", dependencyProxyController.ProxyPyPI) + + return ShareDependencyProxyRouter{Group: shareDependencyProxyRouter} +} diff --git a/services/dependency_proxy_secret_service.go b/services/dependency_proxy_secret_service.go new file mode 100644 index 00000000..c7ff47b6 --- /dev/null +++ b/services/dependency_proxy_secret_service.go @@ -0,0 +1,79 @@ +// 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 . + +package services + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/l3montree-dev/devguard/database/models" + "github.com/l3montree-dev/devguard/shared" +) + +type dependencyProxySecretService struct { + dependencyProxySecretRepository shared.DependencyProxySecretRepository +} + +func NewDependencyProxyService(dependencyProxySecretRepository shared.DependencyProxySecretRepository) *dependencyProxySecretService { + return &dependencyProxySecretService{ + dependencyProxySecretRepository: dependencyProxySecretRepository, + } +} + +func (s *dependencyProxySecretService) GetOrCreateByOrgID(ctx context.Context, orgID uuid.UUID) (models.DependencyProxySecret, error) { + return s.dependencyProxySecretRepository.GetOrCreateByOrgID(ctx, nil, orgID) +} + +func (s *dependencyProxySecretService) GetOrCreateByProjectID(ctx context.Context, projectID uuid.UUID) (models.DependencyProxySecret, error) { + return s.dependencyProxySecretRepository.GetOrCreateByProjectID(ctx, nil, projectID) +} + +func (s *dependencyProxySecretService) GetOrCreateByAssetID(ctx context.Context, assetID uuid.UUID) (models.DependencyProxySecret, error) { + return s.dependencyProxySecretRepository.GetOrCreateByAssetID(ctx, nil, assetID) +} + +func (s *dependencyProxySecretService) UpdateSecret(ctx context.Context, proxy models.DependencyProxySecret) (models.DependencyProxySecret, error) { + + return s.dependencyProxySecretRepository.UpdateSecret(ctx, nil, proxy) +} + +func (s *dependencyProxySecretService) GetModelBySecret(ctx context.Context, secret uuid.UUID) (string, uuid.UUID, error) { + proxy, err := s.dependencyProxySecretRepository.GetBySecret(ctx, nil, secret) + if err != nil { + return "", uuid.Nil, err + } + + var modelID uuid.UUID + var scope string + if proxy.AssetID != nil { + scope = "asset" + modelID = *proxy.AssetID + } else if proxy.ProjectID != nil { + scope = "project" + modelID = *proxy.ProjectID + + } else if proxy.OrgID != nil { + scope = "organization" + modelID = *proxy.OrgID + } + + if modelID == uuid.Nil { + return "", uuid.Nil, fmt.Errorf("no model found for secret: %s", secret) + } + + return scope, modelID, nil +} diff --git a/services/providers.go b/services/providers.go index c4adf5d5..07ee0fae 100644 --- a/services/providers.go +++ b/services/providers.go @@ -35,4 +35,5 @@ var ServiceModule = fx.Options( fx.Provide(fx.Annotate(NewDependencyVulnService, fx.As(new(shared.DependencyVulnService)))), fx.Provide(fx.Annotate(NewOpenSourceInsightService, fx.As(new(shared.OpenSourceInsightService)))), fx.Provide(fx.Annotate(NewVEXRuleService, fx.As(new(shared.VEXRuleService)))), + fx.Provide(fx.Annotate(NewDependencyProxyService, fx.As(new(shared.DependencyProxySecretService)))), ) diff --git a/shared/common_interfaces.go b/shared/common_interfaces.go index 9570fb4d..e23748fe 100644 --- a/shared/common_interfaces.go +++ b/shared/common_interfaces.go @@ -116,6 +116,23 @@ type PolicyRepository interface { FindCommunityManagedPolicies(ctx context.Context, tx DB) ([]models.Policy, error) } +type DependencyProxySecretRepository interface { + utils.Repository[uuid.UUID, models.DependencyProxySecret, DB] + GetOrCreateByOrgID(ctx context.Context, tx DB, orgID uuid.UUID) (models.DependencyProxySecret, error) + GetOrCreateByProjectID(ctx context.Context, tx DB, projectID uuid.UUID) (models.DependencyProxySecret, error) + GetOrCreateByAssetID(ctx context.Context, tx DB, assetID uuid.UUID) (models.DependencyProxySecret, error) + UpdateSecret(ctx context.Context, tx DB, proxy models.DependencyProxySecret) (models.DependencyProxySecret, error) + GetBySecret(ctx context.Context, tx DB, secret uuid.UUID) (models.DependencyProxySecret, error) +} + +type DependencyProxySecretService interface { + GetOrCreateByOrgID(ctx context.Context, orgID uuid.UUID) (models.DependencyProxySecret, error) + GetOrCreateByProjectID(ctx context.Context, projectID uuid.UUID) (models.DependencyProxySecret, error) + GetOrCreateByAssetID(ctx context.Context, assetID uuid.UUID) (models.DependencyProxySecret, error) + UpdateSecret(ctx context.Context, proxy models.DependencyProxySecret) (models.DependencyProxySecret, error) + GetModelBySecret(ctx context.Context, secret uuid.UUID) (string, uuid.UUID, error) +} + type AssetRepository interface { utils.Repository[uuid.UUID, models.Asset, DB] GetAllowedAssetsByProjectID(ctx context.Context, tx DB, allowedAssetIDs []string, projectID uuid.UUID) ([]models.Asset, error) @@ -194,7 +211,7 @@ type AffectedComponentRepository interface { type MaliciousPackageChecker interface { DownloadAndProcessDB(ctx context.Context) error - IsMalicious(ctx context.Context, ecosystem, packageName, version string) (bool, *dtos.OSV) + IsMalicious(ctx context.Context, ecosystem, packageName, version string) (bool, *dtos.OSV, error) } type ComponentRepository interface { diff --git a/shared/context_utils.go b/shared/context_utils.go index 6fa50676..4f990cf0 100644 --- a/shared/context_utils.go +++ b/shared/context_utils.go @@ -318,6 +318,30 @@ func GetAssetVersion(ctx Context) models.AssetVersion { return ctx.Get("assetVersion").(models.AssetVersion) } +func MaybeGetOrganization(ctx Context) (models.Org, error) { + org, ok := ctx.Get("organization").(models.Org) + if !ok { + return models.Org{}, fmt.Errorf("could not get organization") + } + return org, nil +} + +func MaybeGetProject(ctx Context) (models.Project, error) { + project, ok := ctx.Get("project").(models.Project) + if !ok { + return models.Project{}, fmt.Errorf("could not get project") + } + return project, nil +} + +func MaybeGetAsset(ctx Context) (models.Asset, error) { + asset, ok := ctx.Get("asset").(models.Asset) + if !ok { + return models.Asset{}, fmt.Errorf("could not get asset") + } + return asset, nil +} + func MaybeGetAssetVersion(ctx Context) (models.AssetVersion, error) { assetVersion, ok := ctx.Get("assetVersion").(models.AssetVersion) if !ok { diff --git a/tests/dependency_proxy_controller_test.go b/tests/dependency_proxy_controller_test.go index c354cc35..c7431050 100644 --- a/tests/dependency_proxy_controller_test.go +++ b/tests/dependency_proxy_controller_test.go @@ -34,7 +34,7 @@ func TestDependencyProxyController_IntegrityVerification(t *testing.T) { tempDir := t.TempDir() // Create controller - config := controllers.DependencyProxyConfig{ + config := controllers.DependencyProxyCache{ CacheDir: tempDir, } @@ -42,7 +42,7 @@ func TestDependencyProxyController_IntegrityVerification(t *testing.T) { checker, err := vulndb.NewMaliciousPackageChecker(nil) require.NoError(t, err) - controller := controllers.NewDependencyProxyController(config, checker) + controller := controllers.NewDependencyProxyController(nil, config, checker, nil, nil, nil) t.Run("Cache with integrity verification", func(t *testing.T) { testData := []byte("test package content") @@ -132,14 +132,14 @@ func TestDependencyProxyController_IntegrityVerification(t *testing.T) { func TestDependencyProxyController_MaliciousPackageRemoval(t *testing.T) { tempDir := t.TempDir() - config := controllers.DependencyProxyConfig{ + config := controllers.DependencyProxyCache{ CacheDir: tempDir, } checker, err := vulndb.NewMaliciousPackageChecker(nil) require.NoError(t, err) - controller := controllers.NewDependencyProxyController(config, checker) + controller := controllers.NewDependencyProxyController(nil, config, checker, nil, nil, nil) t.Run("Malicious package removed from cache", func(t *testing.T) { // Cache a "malicious" package @@ -158,14 +158,14 @@ func TestDependencyProxyController_MaliciousPackageRemoval(t *testing.T) { func TestDependencyProxyController_ExtractNPMVersion(t *testing.T) { tempDir := t.TempDir() - config := controllers.DependencyProxyConfig{ + config := controllers.DependencyProxyCache{ CacheDir: tempDir, } checker, err := vulndb.NewMaliciousPackageChecker(nil) require.NoError(t, err) - controller := controllers.NewDependencyProxyController(config, checker) + controller := controllers.NewDependencyProxyController(nil, config, checker, nil, nil, nil) t.Run("Extract version from npm package metadata", func(t *testing.T) { // Create sample NPM package metadata JSON @@ -188,13 +188,13 @@ func TestDependencyProxyController_ExtractNPMVersion(t *testing.T) { // Test extractNPMVersionFromMetadata (we need to make this public for testing) // For now, we'll test the full flow - version := controller.ExtractNPMVersionFromMetadata(jsonData) + version, _ := controller.ExtractNPMVersionAndReleaseTimeFromMetadata(jsonData) assert.Equal(t, "1.2.3", version) }) t.Run("Extract version from malformed metadata", func(t *testing.T) { invalidJSON := []byte(`{"name": "test", "dist-tags": "invalid"}`) - version := controller.ExtractNPMVersionFromMetadata(invalidJSON) + version, _ := controller.ExtractNPMVersionAndReleaseTimeFromMetadata(invalidJSON) assert.Equal(t, "", version) }) @@ -205,7 +205,7 @@ func TestDependencyProxyController_ExtractNPMVersion(t *testing.T) { jsonData, err := json.Marshal(metadata) require.NoError(t, err) - version := controller.ExtractNPMVersionFromMetadata(jsonData) + version, _ := controller.ExtractNPMVersionAndReleaseTimeFromMetadata(jsonData) assert.Equal(t, "", version) }) } @@ -213,14 +213,14 @@ func TestDependencyProxyController_ExtractNPMVersion(t *testing.T) { func TestDependencyProxyController_NPMVersionResolution(t *testing.T) { tempDir := t.TempDir() - config := controllers.DependencyProxyConfig{ + config := controllers.DependencyProxyCache{ CacheDir: tempDir, } checker, err := vulndb.NewMaliciousPackageChecker(nil) require.NoError(t, err) - controller := controllers.NewDependencyProxyController(config, checker) + controller := controllers.NewDependencyProxyController(nil, config, checker, nil, nil, nil) t.Run("Verify extractNPMVersionFromMetadata extracts correct version", func(t *testing.T) { // Test that when metadata is fetched for a package without a version, @@ -235,7 +235,7 @@ func TestDependencyProxyController_NPMVersionResolution(t *testing.T) { jsonData, err := json.Marshal(metadata) require.NoError(t, err) - version := controller.ExtractNPMVersionFromMetadata(jsonData) + version, _ := controller.ExtractNPMVersionAndReleaseTimeFromMetadata(jsonData) assert.Equal(t, "3.1.4", version, "Should extract the latest version from dist-tags") }) @@ -256,17 +256,287 @@ func TestDependencyProxyController_NPMVersionResolution(t *testing.T) { }) } +func TestDependencyProxyController_CheckNotAllowedPackage(t *testing.T) { + tempDir := t.TempDir() + config := controllers.DependencyProxyCache{CacheDir: tempDir} + checker, err := vulndb.NewMaliciousPackageChecker(nil) + require.NoError(t, err) + controller := controllers.NewDependencyProxyController(nil, config, checker, nil, nil, nil) + ctx := t.Context() + + testCases := []struct { + name string + proxyType controllers.ProxyType + path string + rules []string + expectedBlocked bool + }{ + + { + name: "negation with minor wildcard unblocks matching version", + proxyType: controllers.NPMProxy, + path: "react@17.0.0", + rules: []string{"*", "!*react@17*"}, + expectedBlocked: false, + }, + { + name: "negation with different major version does not unblock", + proxyType: controllers.NPMProxy, + path: "react@17.0.0", + rules: []string{"*", "!*react@18.*"}, + expectedBlocked: true, + }, + { + name: "negation with patch wildcard unblocks matching version", + proxyType: controllers.NPMProxy, + path: "react@17.0.0", + rules: []string{"*", "!*react@17.0.*"}, + expectedBlocked: false, + }, + { + name: "negation with patch wildcard does not unblock non-matching minor version", + proxyType: controllers.NPMProxy, + path: "react@17.1.0", + rules: []string{"*", "!*react@17.0.*"}, + expectedBlocked: true, + }, + { + name: "negation with exact different version does not unblock", + proxyType: controllers.NPMProxy, + path: "react@17.0.0", + rules: []string{"*", "!*react@18.0.0"}, + expectedBlocked: true, + }, + { + name: "negation with exact matching version unblocks", + proxyType: controllers.NPMProxy, + path: "react@17.0.0", + rules: []string{"*", "!*react@17.0.0"}, + expectedBlocked: false, + }, + { + name: "negation with package name only unblocks any version", + proxyType: controllers.NPMProxy, + path: "react@17.0.0", + rules: []string{"*", "!*react*"}, + expectedBlocked: false, + }, + { + name: "wildcard rule blocks package by name", + proxyType: controllers.NPMProxy, + path: "react@17.0.0", + rules: []string{"*react*"}, + expectedBlocked: true, + }, + { + name: "wildcard rule blocks package with version suffix", + proxyType: controllers.NPMProxy, + path: "react@17.0.0", + rules: []string{"*react*"}, + expectedBlocked: true, + }, + { + name: "wildcard rule matches purl-format path", + proxyType: controllers.NPMProxy, + path: "pkg:npm/react@17.0.0", + rules: []string{"*react*"}, + expectedBlocked: true, + }, + { + name: "negation with slash prefix unblocks nested path", + proxyType: controllers.NPMProxy, + path: "bla/react", + rules: []string{"*", "!*/react*"}, + expectedBlocked: false, + }, + { + name: "negation with slash prefix unblocks top-level package", + proxyType: controllers.NPMProxy, + path: "react", + rules: []string{"*", "!*/react*"}, + expectedBlocked: false, + }, + { + name: "no rules — package is allowed", + proxyType: controllers.NPMProxy, + path: "/lodash", + rules: []string{}, + expectedBlocked: false, + }, + { + name: "exact purl match blocks package", + proxyType: controllers.NPMProxy, + path: "/lodash@1.0.0", + rules: []string{"pkg:npm/lodash@1.0.0"}, + expectedBlocked: true, + }, + { + name: "exact purl path blocks package", + proxyType: controllers.NPMProxy, + path: "pkg:npm/lodash@1.0.0", + rules: []string{"pkg:npm/lodash@1.0.0"}, + expectedBlocked: true, + }, + { + name: "wildcard blocks all packages", + proxyType: controllers.NPMProxy, + path: "/express", + rules: []string{"*"}, + expectedBlocked: true, + }, + { + name: "purl prefix wildcard blocks matching package", + proxyType: controllers.NPMProxy, + path: "/babel-core", + rules: []string{"pkg:npm/babel-core*"}, + expectedBlocked: true, + }, + { + name: "purl prefix wildcard does not block non-matching package", + proxyType: controllers.NPMProxy, + path: "/lodash", + rules: []string{"pkg:npm/babel-*@*"}, + expectedBlocked: false, + }, + { + name: "scoped package with version wildcard is blocked", + proxyType: controllers.NPMProxy, + path: "/@babel/core@1.0.0", + rules: []string{"pkg:npm/@babel/core@*"}, + expectedBlocked: true, + }, + { + name: "scoped package blocked by scope wildcard", + proxyType: controllers.NPMProxy, + path: "/@babel/core", + rules: []string{"pkg:npm/@babel/*"}, + expectedBlocked: true, + }, + { + name: "scoped wildcard does not block different scope", + proxyType: controllers.NPMProxy, + path: "/@types/node", + rules: []string{"pkg:npm/@babel/*@*"}, + expectedBlocked: false, + }, + { + name: "unresolvable path is not blocked", + proxyType: controllers.NPMProxy, + path: "/", + rules: []string{"*"}, + expectedBlocked: false, + }, + { + name: "PyPI package blocked by purl prefix rule", + proxyType: controllers.PyPIProxy, + path: "/simple/requests/", + rules: []string{"pkg:pypi/requests*"}, + expectedBlocked: true, + }, + { + name: "Go module blocked by purl prefix rule", + proxyType: controllers.GoProxy, + path: "/github.com/some/module/@v/list", + rules: []string{"pkg:go/github.com/some/module*"}, + expectedBlocked: true, + }, + { + name: "multiple matching rules — package is blocked", + proxyType: controllers.NPMProxy, + path: "/react", + rules: []string{"pkg:npm/*@*", "pkg:npm/react*"}, + expectedBlocked: true, + }, + { + name: "multiple non-matching rules — package is allowed", + proxyType: controllers.NPMProxy, + path: "/vue", + rules: []string{"pkg:npm/react@*", "pkg:npm/express@*"}, + expectedBlocked: false, + }, + { + name: "negation unblocks even when followed by non-matching rule", + proxyType: controllers.NPMProxy, + path: "/react", + rules: []string{"*", "!pkg:npm/react*", "pkg:npm/express@1.0.0"}, + expectedBlocked: false, + }, + { + name: "wildcard rule blocks all packages", + proxyType: controllers.NPMProxy, + path: "/react", + rules: []string{"*"}, + expectedBlocked: true, + }, + { + name: "single-star rule blocks single intermediate segment", + proxyType: controllers.NPMProxy, + path: "npm/a/lodash", + rules: []string{"*npm/*/lodash*"}, + expectedBlocked: true, + }, + { + name: "single-star rule blocks path without intermediate segment", + proxyType: controllers.NPMProxy, + path: "npm/lodash", + rules: []string{"*npm/*/lodash*"}, + expectedBlocked: true, + }, + { + name: "single-star rule blocks multi-segment path", + proxyType: controllers.NPMProxy, + path: "npm/a/b/lodash", + rules: []string{"*npm/*/lodash*"}, + expectedBlocked: true, + }, + { + name: "double-star rule blocks single intermediate segment", + proxyType: controllers.NPMProxy, + path: "npm/a/lodash", + rules: []string{"pkg:npm/**/lodash*"}, + expectedBlocked: true, + }, + { + name: "double-star rule blocks path without intermediate segment", + proxyType: controllers.NPMProxy, + path: "npm/lodash", + rules: []string{"pkg:npm/**/lodash*"}, + expectedBlocked: true, + }, + { + name: "double-star rule blocks multi-segment path", + proxyType: controllers.NPMProxy, + path: "npm/a/b/lodash", + rules: []string{"pkg:npm/**/lodash*"}, + expectedBlocked: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + settings := controllers.DependencyProxyConfigs{Rules: tc.rules} + blocked, reason := controller.CheckNotAllowedPackage(ctx, tc.proxyType, tc.path, settings) + assert.Equal(t, tc.expectedBlocked, blocked) + if tc.expectedBlocked { + assert.NotEmpty(t, reason) + } else { + assert.Empty(t, reason) + } + }) + } +} + func TestDependencyProxyController_ParseNPMPackagePath(t *testing.T) { tempDir := t.TempDir() - config := controllers.DependencyProxyConfig{ + config := controllers.DependencyProxyCache{ CacheDir: tempDir, } checker, err := vulndb.NewMaliciousPackageChecker(nil) require.NoError(t, err) - controller := controllers.NewDependencyProxyController(config, checker) + controller := controllers.NewDependencyProxyController(nil, config, checker, nil, nil, nil) testCases := []struct { name string @@ -308,3 +578,62 @@ func TestDependencyProxyController_ParseNPMPackagePath(t *testing.T) { }) } } + +func TestTrimProxyPrefix(t *testing.T) { + testCases := []struct { + name string + path string + ecosystem controllers.ProxyType + expected string + }{ + { + name: "NPM with secret", + path: "/api/v1/dependency-proxy/550e8400-e29b-41d4-a716-446655440000/npm/lodash", + ecosystem: controllers.NPMProxy, + expected: "lodash", + }, + { + name: "NPM without secret", + path: "/api/v1/dependency-proxy/npm/lodash", + ecosystem: controllers.NPMProxy, + expected: "lodash", + }, + { + name: "NPM scoped package with secret", + path: "/api/v1/dependency-proxy/550e8400-e29b-41d4-a716-446655440000/npm/@babel/core", + ecosystem: controllers.NPMProxy, + expected: "@babel/core", + }, + { + name: "NPM scoped package without secret", + path: "/api/v1/dependency-proxy/npm/@babel/core", + ecosystem: controllers.NPMProxy, + expected: "@babel/core", + }, + { + name: "Go with secret", + path: "/api/v1/dependency-proxy/550e8400-e29b-41d4-a716-446655440000/go/github.com/foo/bar", + ecosystem: controllers.GoProxy, + expected: "github.com/foo/bar", + }, + { + name: "Go without secret", + path: "/api/v1/dependency-proxy/go/github.com/foo/bar", + ecosystem: controllers.GoProxy, + expected: "github.com/foo/bar", + }, + { + name: "no match returns original path", + path: "/something/completely/different", + ecosystem: controllers.NPMProxy, + expected: "/something/completely/different", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := controllers.TrimProxyPrefix(tc.path, tc.ecosystem) + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/tests/malicious_packages_checker_test.go b/tests/malicious_packages_checker_test.go index aeaa5c2d..2b0ac591 100644 --- a/tests/malicious_packages_checker_test.go +++ b/tests/malicious_packages_checker_test.go @@ -83,6 +83,7 @@ func TestMaliciousPackageChecker(t *testing.T) { pkgName string version string expected bool + error bool }{ { name: "Malicious package with specific version", @@ -104,6 +105,7 @@ func TestMaliciousPackageChecker(t *testing.T) { pkgName: "fake-malicious-npm-package", version: "", expected: false, + error: true, }, { name: "Safe package", @@ -123,7 +125,7 @@ func TestMaliciousPackageChecker(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - isMalicious, entry := checker.IsMalicious(context.Background(), tt.ecosystem, tt.pkgName, tt.version) + isMalicious, entry, err := checker.IsMalicious(context.Background(), tt.ecosystem, tt.pkgName, tt.version) if isMalicious != tt.expected { t.Errorf("IsMalicious(%s, %s, %s) = %v, want %v", tt.ecosystem, tt.pkgName, tt.version, isMalicious, tt.expected) @@ -134,6 +136,11 @@ func TestMaliciousPackageChecker(t *testing.T) { if !isMalicious && entry != nil { t.Error("Expected entry to be nil for safe package") } + if tt.error { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + } }) } } diff --git a/vulndb/malicious_packages_checker.go b/vulndb/malicious_packages_checker.go index b40c92ad..c3f6171e 100644 --- a/vulndb/malicious_packages_checker.go +++ b/vulndb/malicious_packages_checker.go @@ -338,35 +338,35 @@ func (c *MaliciousPackageChecker) loadFakePackages(ctx context.Context) error { return c.repository.UpsertAffectedComponents(ctx, nil, affectedComponents) } -func (c *MaliciousPackageChecker) IsMalicious(ctx context.Context, ecosystem, packageName, version string) (bool, *dtos.OSV) { - // Build a purl for the package (include version for proper version matching) - var purl string - if version != "" { - purl = fmt.Sprintf("pkg:%s/%s@%s", strings.ToLower(ecosystem), strings.ToLower(packageName), version) - } else { - purl = fmt.Sprintf("pkg:%s/%s", strings.ToLower(ecosystem), strings.ToLower(packageName)) +func (c *MaliciousPackageChecker) IsMalicious(ctx context.Context, ecosystem, packageName, version string) (bool, *dtos.OSV, error) { + + if version == "" { + return false, nil, fmt.Errorf("version is required to check if a package is malicious") } + // construct purl for querying, the database uses purl matching to filter by version ranges, so we need to construct a valid purl here + purl := fmt.Sprintf("pkg:%s/%s@%s", strings.ToLower(ecosystem), strings.ToLower(packageName), version) + // Parse to normalize parsedPurl, err := packageurl.FromString(purl) if err != nil { slog.Debug("Failed to parse purl", "purl", purl, "error", err) - return false, nil + return false, nil, fmt.Errorf("failed to parse purl: %w", err) } // Query database using purl matching (similar to PurlComparer) components, err := c.repository.GetMaliciousAffectedComponents(ctx, nil, parsedPurl) if err != nil { slog.Debug("Failed to query malicious packages", "error", err) - return false, nil + return false, nil, fmt.Errorf("failed to query malicious packages: %w", err) } // If we got results from the query, the database already filtered by version ranges if len(components) > 0 { // Take the first match (database already did the version filtering) osv := components[0].MaliciousPackage.ToOSV() - return true, &osv + return true, &osv, nil } - return false, nil + return false, nil, nil }