From 37d5d781b0f149293dd11f34d46b0e3432e287af Mon Sep 17 00:00:00 2001 From: rafi Date: Fri, 29 May 2026 11:41:36 +0200 Subject: [PATCH 01/26] feat: add asset attestation daemon and DTOs Signed-off-by: rafi --- daemons/attestation_daemon.go | 95 +++++++++++++++++++++ daemons/daemon_asset_pipeline.go | 1 + daemons/providers.go | 6 ++ dtos/devguard_asset_attestation_dto.go | 34 ++++++++ dtos/devguard_asset_attestation_schema.json | 88 +++++++++++++++++++ shared/common_interfaces.go | 2 + tests/fx_test_app.go | 1 + tests/fx_test_helpers.go | 2 + 8 files changed, 229 insertions(+) create mode 100644 daemons/attestation_daemon.go create mode 100644 dtos/devguard_asset_attestation_dto.go create mode 100644 dtos/devguard_asset_attestation_schema.json diff --git a/daemons/attestation_daemon.go b/daemons/attestation_daemon.go new file mode 100644 index 000000000..7d9176c21 --- /dev/null +++ b/daemons/attestation_daemon.go @@ -0,0 +1,95 @@ +package daemons + +import ( + "context" + "encoding/json" + "log/slog" + "time" + + "github.com/google/uuid" + "github.com/l3montree-dev/devguard/database/models" + "github.com/l3montree-dev/devguard/dtos" + "github.com/l3montree-dev/devguard/monitoring" +) + +const secondsPerHour = 3600.0 + +// GenerateDevguardAttestations is a pipeline stage that computes the DevGuard asset +// metrics attestation for every asset version and upserts it into the attestations table. +// It runs after CollectStats so that risk data is already up to date. +func (runner *DaemonRunner) GenerateDevguardAttestations(input <-chan assetWithProjectAndOrg, errChan chan<- pipelineError) <-chan assetWithProjectAndOrg { + out := make(chan assetWithProjectAndOrg) + + go func() { + defer func() { + close(out) + monitoring.RecoverPanic("generate devguard attestations panic") + }() + + for assetWithDetails := range input { + stageCtx, span := daemonTracer.Start(assetWithDetails.ctx, "pipeline.generate-devguard-attestations") + + for _, assetVersion := range assetWithDetails.assetVersions { + for _, artifact := range assetVersion.Artifacts { + if err := runner.GenerateAndStoreDevguardAttestation(stageCtx, assetVersion.AssetID, assetVersion.Name, artifact.ArtifactName); err != nil { + slog.Error("could not generate devguard attestation", + "assetID", assetWithDetails.asset.ID, + "assetVersion", assetVersion.Name, + "artifactName", artifact.ArtifactName, + "err", err, + ) + // non-fatal: log and continue to next artifact + } + } + } + + span.End() + out <- assetWithDetails + } + }() + + return out +} + +func (runner *DaemonRunner) GenerateAndStoreDevguardAttestation(ctx context.Context, assetID uuid.UUID, assetVersionName string, artifactName string) error { + averages, err := runner.statisticsRepository.AverageFixingTimes(ctx, nil, assetVersionName, assetID) + if err != nil { + return err + } + + attestationDTO := dtos.DevguardAssetAttestationDTO{ + Type: dtos.DevguardAssetAttestationPredicateType, + GeneratedAt: time.Now().UTC(), + SchemaVersion: "1.0.0", + MeanTimeToRemediate: dtos.MeanTimeToRemediateDTO{ + RiskLowAvgHours: averages.RiskAvgLow / secondsPerHour, + RiskMediumAvgHours: averages.RiskAvgMedium / secondsPerHour, + RiskHighAvgHours: averages.RiskAvgHigh / secondsPerHour, + RiskCriticalAvgHours: averages.RiskAvgCritical / secondsPerHour, + CVSSLowAvgHours: averages.CVSSAvgLow / secondsPerHour, + CVSSMediumAvgHours: averages.CVSSAvgMedium / secondsPerHour, + CVSSHighAvgHours: averages.CVSSAvgHigh / secondsPerHour, + CVSSCriticalAvgHours: averages.CVSSAvgCritical / secondsPerHour, + }, + } + + content, err := json.Marshal(attestationDTO) + if err != nil { + return err + } + + var contentMap map[string]any + if err := json.Unmarshal(content, &contentMap); err != nil { + return err + } + + attestation := models.Attestation{ + AssetID: assetID, + AssetVersionName: assetVersionName, + ArtifactName: artifactName, + PredicateType: dtos.DevguardAssetAttestationPredicateType, + Content: contentMap, + } + + return runner.attestationRepository.Create(ctx, nil, &attestation) +} diff --git a/daemons/daemon_asset_pipeline.go b/daemons/daemon_asset_pipeline.go index 13833668b..a05122420 100644 --- a/daemons/daemon_asset_pipeline.go +++ b/daemons/daemon_asset_pipeline.go @@ -61,6 +61,7 @@ func (runner *DaemonRunner) runPipeline(ctx context.Context, idsChan <-chan uuid ch = runner.SyncTickets(ch, errChan) ch = runner.ResolveDifferencesInTicketState(ch, errChan) ch = runner.CollectStats(ch, errChan) + ch = runner.GenerateDevguardAttestations(ch, errChan) utils.WaitForChannelDrain(ch) // we can close the error channel now // since it is a chan<-pipelineError we can be sure that all errors have been sent diff --git a/daemons/providers.go b/daemons/providers.go index d8870d294..c293f7d83 100644 --- a/daemons/providers.go +++ b/daemons/providers.go @@ -62,6 +62,8 @@ type DaemonRunner struct { maliciousPackageChecker shared.MaliciousPackageChecker vulnDBImportService shared.VulnDBService vexRuleService shared.VEXRuleService + attestationRepository shared.AttestationRepository + statisticsRepository shared.StatisticsRepository debugOptions DebugOptions fixedVersionResolver shared.FixedVersionResolver @@ -106,6 +108,8 @@ func NewDaemonRunner( vulnDBImportService shared.VulnDBService, vexRuleService shared.VEXRuleService, fixedVersionResolver shared.FixedVersionResolver, + attestationRepository shared.AttestationRepository, + statisticsRepository shared.StatisticsRepository, ) *DaemonRunner { return &DaemonRunner{ db: db, @@ -137,6 +141,8 @@ func NewDaemonRunner( vulnDBImportService: vulnDBImportService, vexRuleService: vexRuleService, fixedVersionResolver: fixedVersionResolver, + attestationRepository: attestationRepository, + statisticsRepository: statisticsRepository, } } diff --git a/dtos/devguard_asset_attestation_dto.go b/dtos/devguard_asset_attestation_dto.go new file mode 100644 index 000000000..f29b607d9 --- /dev/null +++ b/dtos/devguard_asset_attestation_dto.go @@ -0,0 +1,34 @@ +package dtos + +import "time" + +// DevguardAssetAttestationPredicateType is the predicate type URI used when storing this attestation in the attestations table. +const DevguardAssetAttestationPredicateType = "https://devguard.org/attestation/asset-metrics/v1" + +// DevguardAssetAttestationDTO represents a security attestation for an asset. +// It captures measurable metrics about how quickly vulnerabilities are being addressed. +type DevguardAssetAttestationDTO struct { + // Metadata + Type string `json:"type"` + GeneratedAt time.Time `json:"generatedAt"` + SchemaVersion string `json:"schemaVersion"` + + // Average time (in hours) to close vulnerabilities, grouped by severity. + // Based on risk score classification. + MeanTimeToRemediate MeanTimeToRemediateDTO `json:"meanTimeToRemediate"` +} + +// MeanTimeToRemediateDTO holds average remediation durations in hours, split by severity. +type MeanTimeToRemediateDTO struct { + // Risk-based severity buckets (DevGuard risk score) + RiskLowAvgHours float64 `json:"riskLowAvgHours"` + RiskMediumAvgHours float64 `json:"riskMediumAvgHours"` + RiskHighAvgHours float64 `json:"riskHighAvgHours"` + RiskCriticalAvgHours float64 `json:"riskCriticalAvgHours"` + + // CVSS-based severity buckets + CVSSLowAvgHours float64 `json:"cvssLowAvgHours"` + CVSSMediumAvgHours float64 `json:"cvsssMediumAvgHours"` + CVSSHighAvgHours float64 `json:"cvssHighAvgHours"` + CVSSCriticalAvgHours float64 `json:"cvssCriticalAvgHours"` +} diff --git a/dtos/devguard_asset_attestation_schema.json b/dtos/devguard_asset_attestation_schema.json new file mode 100644 index 000000000..4e7ec2fe1 --- /dev/null +++ b/dtos/devguard_asset_attestation_schema.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://devguard.org/schemas/asset-attestation/v1", + "title": "DevguardAssetAttestation", + "description": "Security attestation for a DevGuard asset. Captures measurable metrics about how quickly vulnerabilities are being addressed.", + "type": "object", + "required": [ + "type", + "generatedAt", + "schemaVersion", + "meanTimeToRemediate" + ], + "properties": { + "type": { + "type": "string", + "description": "Predicate type URI identifying this attestation format.", + "const": "https://devguard.org/attestation/asset-metrics/v1" + }, + "generatedAt": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when this attestation was generated." + }, + "schemaVersion": { + "type": "string", + "description": "Version of the attestation schema (semver).", + "example": "1.0.0" + }, + "meanTimeToRemediate": { + "type": "object", + "description": "Average time in hours to close (fix/accept/false-positive) vulnerabilities, grouped by severity.", + "required": [ + "riskLowAvgHours", + "riskMediumAvgHours", + "riskHighAvgHours", + "riskCriticalAvgHours", + "cvssLowAvgHours", + "cvsssMediumAvgHours", + "cvssHighAvgHours", + "cvssCriticalAvgHours" + ], + "properties": { + "riskLowAvgHours": { + "type": "number", + "minimum": 0, + "description": "Average hours to remediate low-risk vulnerabilities (DevGuard risk score)." + }, + "riskMediumAvgHours": { + "type": "number", + "minimum": 0, + "description": "Average hours to remediate medium-risk vulnerabilities (DevGuard risk score)." + }, + "riskHighAvgHours": { + "type": "number", + "minimum": 0, + "description": "Average hours to remediate high-risk vulnerabilities (DevGuard risk score)." + }, + "riskCriticalAvgHours": { + "type": "number", + "minimum": 0, + "description": "Average hours to remediate critical-risk vulnerabilities (DevGuard risk score)." + }, + "cvssLowAvgHours": { + "type": "number", + "minimum": 0, + "description": "Average hours to remediate low-CVSS vulnerabilities." + }, + "cvsssMediumAvgHours": { + "type": "number", + "minimum": 0, + "description": "Average hours to remediate medium-CVSS vulnerabilities." + }, + "cvssHighAvgHours": { + "type": "number", + "minimum": 0, + "description": "Average hours to remediate high-CVSS vulnerabilities." + }, + "cvssCriticalAvgHours": { + "type": "number", + "minimum": 0, + "description": "Average hours to remediate critical-CVSS vulnerabilities." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/shared/common_interfaces.go b/shared/common_interfaces.go index c5f3f5312..66c13e17c 100644 --- a/shared/common_interfaces.go +++ b/shared/common_interfaces.go @@ -44,6 +44,7 @@ type DaemonRunner interface { UpdateFixedVersions(ctx context.Context) error UpdateVulnDB(ctx context.Context) error UpdateOpenSourceInsightInformation(ctx context.Context) error + GenerateAndStoreDevguardAttestation(ctx context.Context, assetID uuid.UUID, assetVersionName string, artifactName string) error Start(ctx context.Context) } @@ -157,6 +158,7 @@ type AttestationRepository interface { utils.Repository[string, models.Attestation, DB] GetByAssetID(ctx context.Context, tx DB, assetID uuid.UUID) ([]models.Attestation, error) GetByAssetVersionAndAssetID(ctx context.Context, tx DB, assetID uuid.UUID, assetVersion string) ([]models.Attestation, error) + Create(ctx context.Context, tx DB, attestation *models.Attestation) error } type ArtifactRepository interface { diff --git a/tests/fx_test_app.go b/tests/fx_test_app.go index 3dbceda13..7dee485aa 100644 --- a/tests/fx_test_app.go +++ b/tests/fx_test_app.go @@ -96,6 +96,7 @@ type TestApp struct { VulnEventRepository shared.VulnEventRepository ComponentProjectRepository shared.ComponentProjectRepository StatisticsRepository shared.StatisticsRepository + AttestationRepository shared.AttestationRepository LicenseRiskRepository shared.LicenseRiskRepository GitLabOauth2TokenRepository shared.GitLabOauth2TokenRepository GitlabIntegrationRepository shared.GitlabIntegrationRepository diff --git a/tests/fx_test_helpers.go b/tests/fx_test_helpers.go index 5f8e4d2ed..4444b6bab 100644 --- a/tests/fx_test_helpers.go +++ b/tests/fx_test_helpers.go @@ -180,6 +180,8 @@ func (f *TestFixture) CreateDaemonRunner() *daemons.DaemonRunner { f.App.VulnDBService, f.App.VexRuleService, f.App.FixedVersionResolver, + f.App.AttestationRepository, + f.App.StatisticsRepository, ) } From 8e37608e5779c6306e48b754e520421787b126cf Mon Sep 17 00:00:00 2001 From: rafi Date: Tue, 2 Jun 2026 11:34:30 +0200 Subject: [PATCH 02/26] feat: add compliance service and artifact-scoped attestation lookup Signed-off-by: rafi --- .../policies/vulnerability_fix_time_sla.rego | 32 ++++++++++ .../repositories/attestation_repository.go | 9 +++ services/compliance_service.go | 63 +++++++++++++++++++ shared/common_interfaces.go | 6 ++ 4 files changed, 110 insertions(+) create mode 100644 compliance/attestation-compliance-policies/policies/vulnerability_fix_time_sla.rego create mode 100644 services/compliance_service.go diff --git a/compliance/attestation-compliance-policies/policies/vulnerability_fix_time_sla.rego b/compliance/attestation-compliance-policies/policies/vulnerability_fix_time_sla.rego new file mode 100644 index 000000000..03ca17bb5 --- /dev/null +++ b/compliance/attestation-compliance-policies/policies/vulnerability_fix_time_sla.rego @@ -0,0 +1,32 @@ +# METADATA +# title: Vulnerability fix time SLA +# custom: +# description: Ensures that the average time to remediate vulnerabilities meets defined SLA thresholds — critical < 1 day, high < 3 days, medium < 14 days, low < 30 days. +# priority: 1 +# predicateType: https://devguard.org/attestation/asset-metrics/v1 +# relatedResources: [] +# tags: +# - Security +# complianceFrameworks: [] +package compliance + +import rego.v1 + +default compliant := false + +# SLA thresholds in hours +critical_max_hours := 24 +high_max_hours := 72 +medium_max_hours := 336 +low_max_hours := 720 + +compliant if { + input.type == "https://devguard.org/attestation/asset-metrics/v1" + + mttr := input.meanTimeToRemediate + + mttr.riskCriticalAvgHours < critical_max_hours + mttr.riskHighAvgHours < high_max_hours + mttr.riskMediumAvgHours < medium_max_hours + mttr.riskLowAvgHours < low_max_hours +} diff --git a/database/repositories/attestation_repository.go b/database/repositories/attestation_repository.go index c3b6491ce..2085dad5d 100644 --- a/database/repositories/attestation_repository.go +++ b/database/repositories/attestation_repository.go @@ -43,6 +43,15 @@ func (a *attestationRepository) GetByAssetVersionAndAssetID(ctx context.Context, return attestationList, nil } +func (a *attestationRepository) GetByArtifactAndAssetVersionAndAssetID(ctx context.Context, tx *gorm.DB, artifactName string, assetVersion string, assetID uuid.UUID) ([]models.Attestation, error) { + var attestationList []models.Attestation + err := a.GetDB(ctx, tx).Where("asset_id = ? AND asset_version_name = ? AND artifact_name = ?", assetID, assetVersion, artifactName).Find(&attestationList).Error + if err != nil { + return attestationList, err + } + return attestationList, nil +} + func (a *attestationRepository) Create(ctx context.Context, tx *gorm.DB, attestation *models.Attestation) error { return a.GetDB(ctx, tx).Clauses(clause.OnConflict{ Columns: []clause.Column{ diff --git a/services/compliance_service.go b/services/compliance_service.go new file mode 100644 index 000000000..1c58aa65f --- /dev/null +++ b/services/compliance_service.go @@ -0,0 +1,63 @@ +// Copyright (C) 2025 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" + + "github.com/google/uuid" + "github.com/l3montree-dev/devguard/compliance" + "github.com/l3montree-dev/devguard/database/models" + "github.com/l3montree-dev/devguard/shared" +) + +type ComplianceService struct { + attestationRepository shared.AttestationRepository + policyRepository shared.PolicyRepository +} + +func NewComplianceService(attestationRepository shared.AttestationRepository, policyRepository shared.PolicyRepository) *ComplianceService { + return &ComplianceService{ + attestationRepository: attestationRepository, + policyRepository: policyRepository, + } +} + +func (s *ComplianceService) ArtifactCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) ([]compliance.PolicyEvaluation, error) { + attestations, err := s.attestationRepository.GetByArtifactAndAssetVersionAndAssetID(ctx, nil, artifact.ArtifactName, assetVersion.Name, assetVersion.AssetID) + if err != nil { + return nil, err + } + + policies, err := s.policyRepository.FindByProjectID(ctx, nil, projectID) + if err != nil { + return nil, err + } + + results := make([]compliance.PolicyEvaluation, 0, len(policies)) +foundMatch: + for _, policy := range policies { + for _, attestation := range attestations { + if attestation.PredicateType != policy.PredicateType { + continue + } + results = append(results, compliance.Eval(policy, attestation.Content)) + continue foundMatch + } + results = append(results, compliance.Eval(policy, nil)) + } + + return results, nil +} diff --git a/shared/common_interfaces.go b/shared/common_interfaces.go index 66c13e17c..99bf8645d 100644 --- a/shared/common_interfaces.go +++ b/shared/common_interfaces.go @@ -25,6 +25,7 @@ import ( "github.com/google/uuid" toto "github.com/in-toto/in-toto-golang/in_toto" + "github.com/l3montree-dev/devguard/compliance" "github.com/l3montree-dev/devguard/database/models" "github.com/l3montree-dev/devguard/dtos" "github.com/l3montree-dev/devguard/dtos/sarif" @@ -158,6 +159,7 @@ type AttestationRepository interface { utils.Repository[string, models.Attestation, DB] GetByAssetID(ctx context.Context, tx DB, assetID uuid.UUID) ([]models.Attestation, error) GetByAssetVersionAndAssetID(ctx context.Context, tx DB, assetID uuid.UUID, assetVersion string) ([]models.Attestation, error) + GetByArtifactAndAssetVersionAndAssetID(ctx context.Context, tx DB, artifactName string, assetVersion string, assetID uuid.UUID) ([]models.Attestation, error) Create(ctx context.Context, tx DB, attestation *models.Attestation) error } @@ -171,6 +173,10 @@ type ArtifactRepository interface { CleanupOrphanedRecords(ctx context.Context) error } +type ComplianceService interface { + ArtifactCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) ([]compliance.PolicyEvaluation, error) +} + type ReleaseRepository interface { utils.Repository[uuid.UUID, models.Release, DB] GetByProjectID(ctx context.Context, tx DB, projectID uuid.UUID) ([]models.Release, error) From 26bfbda4204c38639a9dbb668463a13e460d9246 Mon Sep 17 00:00:00 2001 From: rafi Date: Wed, 3 Jun 2026 10:41:46 +0200 Subject: [PATCH 03/26] add compliance risk tracking Signed-off-by: rafi --- controllers/compliance_risk_controller.go | 154 +++++++++++ daemons/attestation_daemon.go | 43 +++ daemons/daemon_asset_pipeline.go | 1 + daemons/providers.go | 6 + ...20260602000000_add_compliance_risks.up.sql | 55 ++++ database/models/compliance_risk_model.go | 82 ++++++ database/models/vulnevent_model.go | 9 + .../compliance_risk_repository.go | 113 ++++++++ database/repositories/providers.go | 1 + dtos/compliance_risk_dto.go | 33 +++ dtos/vulnevent_dto.go | 1 + router/compliance_risk_router.go | 24 ++ router/providers.go | 1 + services/compliance_risk_service.go | 257 ++++++++++++++++++ services/providers.go | 2 + shared/common_interfaces.go | 15 + tests/fx_test_app.go | 3 + tests/fx_test_helpers.go | 2 + transformer/compliance_risk_transformer.go | 31 +++ 19 files changed, 833 insertions(+) create mode 100644 controllers/compliance_risk_controller.go create mode 100644 database/migrations/20260602000000_add_compliance_risks.up.sql create mode 100644 database/models/compliance_risk_model.go create mode 100644 database/repositories/compliance_risk_repository.go create mode 100644 dtos/compliance_risk_dto.go create mode 100644 router/compliance_risk_router.go create mode 100644 services/compliance_risk_service.go create mode 100644 transformer/compliance_risk_transformer.go diff --git a/controllers/compliance_risk_controller.go b/controllers/compliance_risk_controller.go new file mode 100644 index 000000000..fb3752a49 --- /dev/null +++ b/controllers/compliance_risk_controller.go @@ -0,0 +1,154 @@ +package controllers + +import ( + "encoding/json" + "log/slog" + + "github.com/l3montree-dev/devguard/database/models" + "github.com/l3montree-dev/devguard/dtos" + "github.com/l3montree-dev/devguard/shared" + "github.com/l3montree-dev/devguard/transformer" + "github.com/l3montree-dev/devguard/utils" + "github.com/labstack/echo/v4" +) + +type ComplianceRiskController struct { + complianceRiskRepository shared.ComplianceRiskRepository + complianceRiskService shared.ComplianceRiskService +} + +func NewComplianceRiskController(repo shared.ComplianceRiskRepository, svc shared.ComplianceRiskService) *ComplianceRiskController { + return &ComplianceRiskController{ + complianceRiskRepository: repo, + complianceRiskService: svc, + } +} + +type complianceRiskStatus struct { + StatusType string `json:"status"` + Justification string `json:"justification"` + MechanicalJustification dtos.MechanicalJustificationType `json:"mechanicalJustification"` +} + +func convertComplianceRiskToDetailedDTO(r models.ComplianceRisk) dtos.DetailedComplianceRiskDTO { + return dtos.DetailedComplianceRiskDTO{ + ComplianceRiskDTO: transformer.ComplianceRiskToDTO(r), + Events: utils.Map(r.Events, func(ev models.VulnEvent) dtos.VulnEventDTO { + return dtos.VulnEventDTO{ + ID: ev.ID, + Type: ev.Type, + VulnID: ev.GetVulnID(), + UserID: ev.UserID, + Justification: ev.Justification, + MechanicalJustification: ev.MechanicalJustification, + OriginalAssetVersionName: ev.OriginalAssetVersionName, + VulnerabilityName: r.PolicyID, + ArbitraryJSONData: ev.GetArbitraryJSONData(), + CreatedAt: ev.CreatedAt, + CreatedByVexRule: ev.CreatedByVexRule, + } + }), + } +} + +func (c *ComplianceRiskController) ListPaged(ctx shared.Context) error { + assetVersion := shared.GetAssetVersion(ctx) + + pagedResp, err := c.complianceRiskRepository.GetAllComplianceRisksForAssetVersionPaged( + ctx.Request().Context(), nil, + assetVersion.AssetID, + assetVersion.Name, + shared.GetPageInfo(ctx), + ctx.QueryParam("search"), + shared.GetFilterQuery(ctx), + shared.GetSortQuery(ctx), + ) + if err != nil { + return echo.NewHTTPError(500, "could not get compliance risks").WithInternal(err) + } + + return ctx.JSON(200, pagedResp.Map(func(r models.ComplianceRisk) any { + return convertComplianceRiskToDetailedDTO(r) + })) +} + +func (c *ComplianceRiskController) Read(ctx shared.Context) error { + riskID, _, err := shared.GetVulnID(ctx) + if err != nil { + return echo.NewHTTPError(400, "could not get compliance risk ID") + } + risk, err := c.complianceRiskRepository.Read(ctx.Request().Context(), nil, riskID) + if err != nil { + return echo.NewHTTPError(404, "could not find compliance risk") + } + return ctx.JSON(200, convertComplianceRiskToDetailedDTO(risk)) +} + +func (c *ComplianceRiskController) CreateEvent(ctx shared.Context) error { + thirdPartyIntegration := shared.GetThirdPartyIntegration(ctx) + riskID, _, err := shared.GetVulnID(ctx) + if err != nil { + return echo.NewHTTPError(400, "invalid compliance risk id") + } + + risk, err := c.complianceRiskRepository.Read(ctx.Request().Context(), nil, riskID) + if err != nil { + return echo.NewHTTPError(404, "could not find compliance risk") + } + + var status complianceRiskStatus + if err := json.NewDecoder(ctx.Request().Body).Decode(&status); err != nil { + return echo.NewHTTPError(400, "invalid payload").WithInternal(err) + } + if err := models.CheckStatusType(status.StatusType); err != nil { + return echo.NewHTTPError(400, "invalid status type") + } + + userID := shared.GetSession(ctx).GetUserID() + userAgent := ctx.Request().UserAgent() + + event, err := c.complianceRiskService.UpdateComplianceRiskState(ctx.Request().Context(), nil, userID, &risk, status.StatusType, status.Justification, status.MechanicalJustification, &userAgent) + if err != nil { + return echo.NewHTTPError(500, "could not create compliance risk event").WithInternal(err) + } + + if err := thirdPartyIntegration.HandleEvent(ctx.Request().Context(), shared.VulnEvent{ + Ctx: ctx, + Event: event, + }, &userAgent); err != nil { + slog.Error("could not handle third-party event for compliance risk", "err", err) + return echo.NewHTTPError(500, "could not create compliance risk event").WithInternal(err) + } + + return ctx.JSON(200, convertComplianceRiskToDetailedDTO(risk)) +} + +func (c *ComplianceRiskController) Mitigate(ctx shared.Context) error { + var justification struct { + Comment string `json:"comment"` + } + if err := ctx.Bind(&justification); err != nil { + return echo.NewHTTPError(500, "could not bind the request to a justification") + } + + riskID, _, err := shared.GetVulnID(ctx) + if err != nil { + return echo.NewHTTPError(400, "invalid compliance risk id") + } + + userAgent := ctx.Request().UserAgent() + thirdPartyIntegrations := shared.GetThirdPartyIntegration(ctx) + + if err := thirdPartyIntegrations.HandleEvent(ctx.Request().Context(), shared.ManualMitigateEvent{ + Ctx: ctx, + Justification: justification.Comment, + }, &userAgent); err != nil { + return echo.NewHTTPError(500, "could not mitigate compliance risk").WithInternal(err) + } + + risk, err := c.complianceRiskRepository.Read(ctx.Request().Context(), nil, riskID) + if err != nil { + return echo.NewHTTPError(404, "could not find compliance risk") + } + return ctx.JSON(200, convertComplianceRiskToDetailedDTO(risk)) +} diff --git a/daemons/attestation_daemon.go b/daemons/attestation_daemon.go index 7d9176c21..2946afb19 100644 --- a/daemons/attestation_daemon.go +++ b/daemons/attestation_daemon.go @@ -12,6 +12,49 @@ import ( "github.com/l3montree-dev/devguard/monitoring" ) +func (runner *DaemonRunner) CheckArtifactCompliance(input <-chan assetWithProjectAndOrg, errChan chan<- pipelineError) <-chan assetWithProjectAndOrg { + out := make(chan assetWithProjectAndOrg) + + go func() { + defer func() { + close(out) + monitoring.RecoverPanic("check artifact compliance panic") + }() + + for assetWithDetails := range input { + stageCtx, span := daemonTracer.Start(assetWithDetails.ctx, "pipeline.check-artifact-compliance") + + for _, assetVersion := range assetWithDetails.assetVersions { + for _, artifact := range assetVersion.Artifacts { + evaluations, err := runner.complianceService.ArtifactCompliance(stageCtx, assetWithDetails.project.ID, assetVersion, artifact) + if err != nil { + slog.Error("could not evaluate artifact compliance", + "assetID", assetWithDetails.asset.ID, + "assetVersion", assetVersion.Name, + "artifactName", artifact.ArtifactName, + "err", err, + ) + continue + } + if err := runner.complianceRiskService.HandleArtifactCompliance(stageCtx, nil, "system", nil, assetVersion, artifact, evaluations); err != nil { + slog.Error("could not handle artifact compliance risks", + "assetID", assetWithDetails.asset.ID, + "assetVersion", assetVersion.Name, + "artifactName", artifact.ArtifactName, + "err", err, + ) + } + } + } + + span.End() + out <- assetWithDetails + } + }() + + return out +} + const secondsPerHour = 3600.0 // GenerateDevguardAttestations is a pipeline stage that computes the DevGuard asset diff --git a/daemons/daemon_asset_pipeline.go b/daemons/daemon_asset_pipeline.go index a05122420..6417c1256 100644 --- a/daemons/daemon_asset_pipeline.go +++ b/daemons/daemon_asset_pipeline.go @@ -62,6 +62,7 @@ func (runner *DaemonRunner) runPipeline(ctx context.Context, idsChan <-chan uuid ch = runner.ResolveDifferencesInTicketState(ch, errChan) ch = runner.CollectStats(ch, errChan) ch = runner.GenerateDevguardAttestations(ch, errChan) + ch = runner.CheckArtifactCompliance(ch, errChan) utils.WaitForChannelDrain(ch) // we can close the error channel now // since it is a chan<-pipelineError we can be sure that all errors have been sent diff --git a/daemons/providers.go b/daemons/providers.go index c293f7d83..0b5c5e403 100644 --- a/daemons/providers.go +++ b/daemons/providers.go @@ -64,6 +64,8 @@ type DaemonRunner struct { vexRuleService shared.VEXRuleService attestationRepository shared.AttestationRepository statisticsRepository shared.StatisticsRepository + complianceService shared.ComplianceService + complianceRiskService shared.ComplianceRiskService debugOptions DebugOptions fixedVersionResolver shared.FixedVersionResolver @@ -110,6 +112,8 @@ func NewDaemonRunner( fixedVersionResolver shared.FixedVersionResolver, attestationRepository shared.AttestationRepository, statisticsRepository shared.StatisticsRepository, + complianceService shared.ComplianceService, + complianceRiskService shared.ComplianceRiskService, ) *DaemonRunner { return &DaemonRunner{ db: db, @@ -143,6 +147,8 @@ func NewDaemonRunner( fixedVersionResolver: fixedVersionResolver, attestationRepository: attestationRepository, statisticsRepository: statisticsRepository, + complianceService: complianceService, + complianceRiskService: complianceRiskService, } } diff --git a/database/migrations/20260602000000_add_compliance_risks.up.sql b/database/migrations/20260602000000_add_compliance_risks.up.sql new file mode 100644 index 000000000..fb76e851f --- /dev/null +++ b/database/migrations/20260602000000_add_compliance_risks.up.sql @@ -0,0 +1,55 @@ +CREATE TABLE IF NOT EXISTS public.compliance_risks ( + id uuid NOT NULL, + asset_version_name text NOT NULL, + asset_id uuid NOT NULL, + message text, + scanner_ids text NOT NULL DEFAULT '', + state text DEFAULT 'open' NOT NULL, + last_detected timestamp with time zone DEFAULT now() NOT NULL, + ticket_id text, + ticket_url text, + manual_ticket_creation boolean DEFAULT false, + created_at timestamp with time zone, + updated_at timestamp with time zone, + deleted_at timestamp with time zone, + policy_id text NOT NULL, + CONSTRAINT compliance_risks_pkey PRIMARY KEY (id), + CONSTRAINT fk_compliance_risks_asset_versions FOREIGN KEY (asset_version_name, asset_id) + REFERENCES public.asset_versions (name, asset_id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS public.artifact_compliance_risks ( + artifact_artifact_name text NOT NULL, + artifact_asset_version_name text NOT NULL, + artifact_asset_id uuid NOT NULL, + compliance_risk_id uuid NOT NULL, + CONSTRAINT artifact_compliance_risks_pkey PRIMARY KEY (artifact_artifact_name, artifact_asset_version_name, artifact_asset_id, compliance_risk_id), + CONSTRAINT fk_artifact_compliance_risks_artifact FOREIGN KEY (artifact_artifact_name, artifact_asset_version_name, artifact_asset_id) + REFERENCES public.artifacts (artifact_name, asset_version_name, asset_id) ON DELETE CASCADE, + CONSTRAINT fk_artifact_compliance_risks_compliance_risk FOREIGN KEY (compliance_risk_id) + REFERENCES public.compliance_risks (id) ON DELETE CASCADE +); + +-- Add compliance_risk_id column to vuln_events +ALTER TABLE public.vuln_events + ADD COLUMN IF NOT EXISTS compliance_risk_id uuid; + +ALTER TABLE public.vuln_events + ADD CONSTRAINT fk_vuln_events_compliance_risk FOREIGN KEY (compliance_risk_id) + REFERENCES public.compliance_risks (id) ON DELETE CASCADE; + +-- Drop old one_vuln_parent check (only 3 columns) and replace with updated version including compliance_risk_id +ALTER TABLE public.vuln_events DROP CONSTRAINT IF EXISTS one_vuln_parent; + +ALTER TABLE public.vuln_events ADD CONSTRAINT one_vuln_parent CHECK ( + (dependency_vuln_id IS NOT NULL)::int + + (license_risk_id IS NOT NULL)::int + + (first_party_vuln_id IS NOT NULL)::int + + (compliance_risk_id IS NOT NULL)::int = 1 +); + +CREATE INDEX IF NOT EXISTS idx_compliance_risks_asset_version + ON public.compliance_risks (asset_version_name, asset_id); + +CREATE INDEX IF NOT EXISTS idx_artifact_compliance_risks_compliance_risk_id + ON public.artifact_compliance_risks (compliance_risk_id); diff --git a/database/models/compliance_risk_model.go b/database/models/compliance_risk_model.go new file mode 100644 index 000000000..4229ee275 --- /dev/null +++ b/database/models/compliance_risk_model.go @@ -0,0 +1,82 @@ +// 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 ( + "fmt" + + "github.com/google/uuid" + "github.com/l3montree-dev/devguard/dtos" + "github.com/l3montree-dev/devguard/utils" + "gorm.io/gorm" +) + +type ComplianceRisk struct { + Vulnerability + + PolicyID string `json:"policyId" gorm:"type:text;"` + + Events []VulnEvent `gorm:"foreignKey:ComplianceRiskID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;" json:"events"` + + Artifacts []Artifact `json:"artifacts" gorm:"many2many:artifact_compliance_risks;constraint:OnDelete:CASCADE"` +} + +func (complianceRisk ComplianceRisk) TableName() string { + return "compliance_risks" +} + +func (complianceRisk ComplianceRisk) GetType() dtos.VulnType { + return dtos.VulnTypeComplianceRisk +} + +func (complianceRisk *ComplianceRisk) CalculateHash() uuid.UUID { + return utils.HashToUUID(fmt.Sprintf("%s/%s/%s", complianceRisk.PolicyID, complianceRisk.AssetVersionName, complianceRisk.AssetID)) +} + +func (complianceRisk *ComplianceRisk) BeforeSave(tx *gorm.DB) error { + complianceRisk.ID = complianceRisk.CalculateHash() + return nil +} + +func (complianceRisk ComplianceRisk) GetEvents() []VulnEvent { + return complianceRisk.Events +} + +func (complianceRisk *ComplianceRisk) GetArtifacts() []Artifact { + return complianceRisk.Artifacts +} + +func (complianceRisk ComplianceRisk) GetAssetVersionName() string { + return complianceRisk.AssetVersionName +} + +func (complianceRisk ComplianceRisk) AssetVersionIndependentHash() string { + return utils.HashString(complianceRisk.PolicyID) +} + +func (complianceRisk *ComplianceRisk) GetArtifactNames() string { + names := "" + for _, a := range complianceRisk.Artifacts { + if names != "" { + names += ", " + } + names += a.ArtifactName + } + return names +} + +func (complianceRisk ComplianceRisk) Title() string { + return fmt.Sprintf("Compliance risk for policy %s", complianceRisk.PolicyID) +} diff --git a/database/models/vulnevent_model.go b/database/models/vulnevent_model.go index 1e97aabd7..e614746ca 100644 --- a/database/models/vulnevent_model.go +++ b/database/models/vulnevent_model.go @@ -17,6 +17,7 @@ type VulnEvent struct { DependencyVulnID *uuid.UUID `json:"dependencyVulnId" gorm:"type:uuid;column:dependency_vuln_id"` LicenseRiskID *uuid.UUID `json:"licenseRiskId" gorm:"type:uuid;column:license_risk_id"` FirstPartyVulnID *uuid.UUID `json:"firstPartyVulnId" gorm:"type:uuid;column:first_party_vuln_id"` + ComplianceRiskID *uuid.UUID `json:"complianceRiskId" gorm:"type:uuid;column:compliance_risk_id"` UserID string `json:"userId"` Justification *string `json:"justification" gorm:"type:text;"` MechanicalJustification dtos.MechanicalJustificationType `json:"mechanicalJustification" gorm:"type:text;"` @@ -45,6 +46,9 @@ func (event VulnEvent) GetVulnID() uuid.UUID { if event.FirstPartyVulnID != nil { return *event.FirstPartyVulnID } + if event.ComplianceRiskID != nil { + return *event.ComplianceRiskID + } return uuid.Nil } @@ -59,6 +63,9 @@ func (event VulnEvent) GetVulnType() dtos.VulnType { if event.FirstPartyVulnID != nil { return dtos.VulnTypeFirstPartyVuln } + if event.ComplianceRiskID != nil { + return dtos.VulnTypeComplianceRisk + } return "" } @@ -71,6 +78,8 @@ func SetVulnIDOnEvent(event *VulnEvent, vulnID uuid.UUID, vulnType dtos.VulnType event.LicenseRiskID = &vulnID case dtos.VulnTypeFirstPartyVuln: event.FirstPartyVulnID = &vulnID + case dtos.VulnTypeComplianceRisk: + event.ComplianceRiskID = &vulnID } } diff --git a/database/repositories/compliance_risk_repository.go b/database/repositories/compliance_risk_repository.go new file mode 100644 index 000000000..3ec01bd2c --- /dev/null +++ b/database/repositories/compliance_risk_repository.go @@ -0,0 +1,113 @@ +package repositories + +import ( + "context" + + "github.com/google/uuid" + "github.com/l3montree-dev/devguard/database/models" + "github.com/l3montree-dev/devguard/shared" + "github.com/l3montree-dev/devguard/statemachine" + "github.com/l3montree-dev/devguard/utils" + "gorm.io/gorm" +) + +type ComplianceRiskRepository struct { + utils.Repository[uuid.UUID, models.ComplianceRisk, *gorm.DB] + db *gorm.DB +} + +func NewComplianceRiskRepository(db *gorm.DB) *ComplianceRiskRepository { + return &ComplianceRiskRepository{ + db: db, + Repository: newGormRepository[uuid.UUID, models.ComplianceRisk](db), + } +} + +func (r *ComplianceRiskRepository) GetAllComplianceRisksForAssetVersion(ctx context.Context, tx *gorm.DB, assetID uuid.UUID, assetVersionName string) ([]models.ComplianceRisk, error) { + var result []models.ComplianceRisk + err := r.GetDB(ctx, tx).Preload("Artifacts").Where("asset_id = ? AND asset_version_name = ?", assetID, assetVersionName).Find(&result).Error + return result, err +} + +func (r *ComplianceRiskRepository) GetAllComplianceRisksForAssetVersionPaged(ctx context.Context, tx *gorm.DB, assetID uuid.UUID, assetVersionName string, pageInfo shared.PageInfo, search string, filter []shared.FilterQuery, sort []shared.SortQuery) (shared.Paged[models.ComplianceRisk], error) { + var count int64 + var risks []models.ComplianceRisk + + q := r.GetDB(ctx, tx).Model(&models.ComplianceRisk{}). + Preload("Artifacts"). + Joins("LEFT JOIN artifact_compliance_risks ON artifact_compliance_risks.compliance_risk_id = compliance_risks.id"). + Where("compliance_risks.asset_version_name = ?", assetVersionName). + Where("compliance_risks.asset_id = ?", assetID). + Distinct() + + for _, f := range filter { + q = q.Where(f.SQL(), f.Value()) + } + + if len(search) > 2 { + q = q.Where("compliance_risks.policy_id ILIKE ?", "%"+search+"%") + } + + if err := q.Session(&gorm.Session{}).Distinct("compliance_risks.id").Count(&count).Error; err != nil { + return shared.Paged[models.ComplianceRisk]{}, err + } + + err := q.Limit(pageInfo.PageSize).Offset((pageInfo.Page - 1) * pageInfo.PageSize).Find(&risks).Error + if err != nil { + return shared.Paged[models.ComplianceRisk]{}, err + } + return shared.NewPaged(pageInfo, count, risks), nil +} + +func (r *ComplianceRiskRepository) Read(ctx context.Context, tx *gorm.DB, id uuid.UUID) (models.ComplianceRisk, error) { + var risk models.ComplianceRisk + err := r.GetDB(ctx, tx).Where("id = ?", id). + Preload("Artifacts"). + Preload("Events", func(db *gorm.DB) *gorm.DB { + return db.Order("created_at ASC") + }). + First(&risk).Error + return risk, err +} + +func (r *ComplianceRiskRepository) ApplyAndSave(ctx context.Context, tx *gorm.DB, risk *models.ComplianceRisk, ev *models.VulnEvent) error { + if tx == nil { + return r.Transaction(ctx, func(d *gorm.DB) error { + return r.applyAndSave(ctx, d, risk, ev) + }) + } + return r.applyAndSave(ctx, tx, risk, ev) +} + +func (r *ComplianceRiskRepository) applyAndSave(ctx context.Context, tx *gorm.DB, risk *models.ComplianceRisk, ev *models.VulnEvent) error { + statemachine.Apply(risk, *ev) + if err := r.Save(ctx, tx, risk); err != nil { + return err + } + if err := r.GetDB(ctx, tx).Save(ev).Error; err != nil { + return err + } + risk.Events = append(risk.Events, *ev) + return nil +} + +func (r *ComplianceRiskRepository) GetComplianceRisksByOtherAssetVersions(ctx context.Context, tx *gorm.DB, assetVersionName string, assetID uuid.UUID) ([]models.ComplianceRisk, error) { + var risks []models.ComplianceRisk + q := r.GetDB(ctx, tx). + Preload("Events", func(db *gorm.DB) *gorm.DB { + return db.Order("created_at ASC") + }). + Preload("Artifacts"). + Where("compliance_risks.asset_version_name != ? AND compliance_risks.asset_id = ?", assetVersionName, assetID) + if err := q.Find(&risks).Error; err != nil { + return nil, err + } + return risks, nil +} + +func (r *ComplianceRiskRepository) SaveBatch(ctx context.Context, tx *gorm.DB, risks []models.ComplianceRisk) error { + if len(risks) == 0 { + return nil + } + return r.GetDB(ctx, tx).Save(&risks).Error +} diff --git a/database/repositories/providers.go b/database/repositories/providers.go index 886db5816..33440da01 100644 --- a/database/repositories/providers.go +++ b/database/repositories/providers.go @@ -43,6 +43,7 @@ var Module = fx.Options( fx.Provide(fx.Annotate(NewAttestationRepository, fx.As(new(shared.AttestationRepository)))), fx.Provide(fx.Annotate(NewPolicyRepository, fx.As(new(shared.PolicyRepository)))), fx.Provide(fx.Annotate(NewLicenseRiskRepository, fx.As(new(shared.LicenseRiskRepository)))), + fx.Provide(fx.Annotate(NewComplianceRiskRepository, fx.As(new(shared.ComplianceRiskRepository)))), fx.Provide(fx.Annotate(NewWebhookRepository, fx.As(new(shared.WebhookIntegrationRepository)))), fx.Provide(fx.Annotate(NewArtifactRepository, fx.As(new(shared.ArtifactRepository)))), fx.Provide(fx.Annotate(NewInvitationRepository, fx.As(new(shared.InvitationRepository)))), diff --git a/dtos/compliance_risk_dto.go b/dtos/compliance_risk_dto.go new file mode 100644 index 000000000..0805bfaf0 --- /dev/null +++ b/dtos/compliance_risk_dto.go @@ -0,0 +1,33 @@ +package dtos + +import ( + "time" + + "github.com/google/uuid" +) + +type ComplianceRiskArtifactDTO struct { + ArtifactName string `json:"artifactName"` + AssetVersionName string `json:"assetVersionName"` + AssetID string `json:"assetId"` +} + +type ComplianceRiskDTO struct { + ID uuid.UUID `json:"id"` + Message *string `json:"message"` + AssetVersionName string `json:"assetVersionName"` + AssetID string `json:"assetId"` + State VulnState `json:"state"` + CreatedAt time.Time `json:"createdAt"` + TicketID *string `json:"ticketId"` + TicketURL *string `json:"ticketUrl"` + ManualTicketCreation bool `json:"manualTicketCreation"` + + PolicyID string `json:"policyId"` + Artifacts []ComplianceRiskArtifactDTO `json:"artifacts"` +} + +type DetailedComplianceRiskDTO struct { + ComplianceRiskDTO + Events []VulnEventDTO `json:"events"` +} diff --git a/dtos/vulnevent_dto.go b/dtos/vulnevent_dto.go index 8ed1b7977..890fa79b4 100644 --- a/dtos/vulnevent_dto.go +++ b/dtos/vulnevent_dto.go @@ -14,6 +14,7 @@ const ( VulnTypeDependencyVuln VulnType = "dependencyVuln" VulnTypeFirstPartyVuln VulnType = "firstPartyVuln" VulnTypeLicenseRisk VulnType = "licenseRisk" + VulnTypeComplianceRisk VulnType = "complianceRisk" ) const ( diff --git a/router/compliance_risk_router.go b/router/compliance_risk_router.go new file mode 100644 index 000000000..020259827 --- /dev/null +++ b/router/compliance_risk_router.go @@ -0,0 +1,24 @@ +package router + +import ( + "github.com/l3montree-dev/devguard/controllers" + "github.com/l3montree-dev/devguard/middlewares" + "github.com/labstack/echo/v4" +) + +type ComplianceRiskRouter struct { + *echo.Group +} + +func NewComplianceRiskRouter( + assetVersionGroup AssetVersionRouter, + controller *controllers.ComplianceRiskController, +) ComplianceRiskRouter { + g := assetVersionGroup.Group.Group("/compliance-risks") + g.GET("/", controller.ListPaged) + g.GET("/:complianceRiskID/", controller.Read) + g.POST("/:complianceRiskID/", controller.CreateEvent, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) + g.POST("/:complianceRiskID/mitigate/", controller.Mitigate, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) + + return ComplianceRiskRouter{Group: g} +} diff --git a/router/providers.go b/router/providers.go index f2f799529..3a45d6ba5 100644 --- a/router/providers.go +++ b/router/providers.go @@ -10,6 +10,7 @@ var RouterModule = fx.Options( fx.Provide(NewDependencyVulnRouter), fx.Provide(NewFirstPartyVulnRouter), fx.Provide(NewLicenseRiskRouter), + fx.Provide(NewComplianceRiskRouter), fx.Provide(NewOrgRouter), fx.Provide(NewProjectRouter), fx.Provide(NewSessionRouter), diff --git a/services/compliance_risk_service.go b/services/compliance_risk_service.go new file mode 100644 index 000000000..3ad9fa914 --- /dev/null +++ b/services/compliance_risk_service.go @@ -0,0 +1,257 @@ +package services + +import ( + "context" + "log/slog" + "time" + + "github.com/l3montree-dev/devguard/compliance" + "github.com/l3montree-dev/devguard/database/models" + "github.com/l3montree-dev/devguard/dtos" + "github.com/l3montree-dev/devguard/shared" + "github.com/l3montree-dev/devguard/statemachine" + "github.com/l3montree-dev/devguard/utils" +) + +type ComplianceRiskService struct { + complianceRiskRepository shared.ComplianceRiskRepository + vulnEventRepository shared.VulnEventRepository +} + +var _ shared.ComplianceRiskService = (*ComplianceRiskService)(nil) + +func NewComplianceRiskService(complianceRiskRepository shared.ComplianceRiskRepository, vulnEventRepository shared.VulnEventRepository) *ComplianceRiskService { + return &ComplianceRiskService{ + complianceRiskRepository: complianceRiskRepository, + vulnEventRepository: vulnEventRepository, + } +} + +// HandleArtifactCompliance processes policy evaluations for an artifact and manages the +// lifecycle of compliance risks: new detections, branch-diffing, artifact association, and fixes. +func (s *ComplianceRiskService) HandleArtifactCompliance(ctx context.Context, tx shared.DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, evaluations []compliance.PolicyEvaluation) error { + // fetch all existing compliance risks for this asset version (across all artifacts) + existingRisks, err := s.complianceRiskRepository.GetAllComplianceRisksForAssetVersion(ctx, tx, assetVersion.AssetID, assetVersion.Name) + if err != nil { + return err + } + + // build risks for every evaluation — compliant ones start fixed, non-compliant ones open + foundRisks := make([]models.ComplianceRisk, 0, len(evaluations)) + for _, eval := range evaluations { + compliant := eval.Compliant != nil && *eval.Compliant + state := dtos.VulnStateOpen + if compliant { + state = dtos.VulnStateFixed + } + foundRisks = append(foundRisks, models.ComplianceRisk{ + Vulnerability: models.Vulnerability{ + AssetVersionName: assetVersion.Name, + AssetID: assetVersion.AssetID, + AssetVersion: assetVersion, + State: state, + LastDetected: time.Now(), + }, + PolicyID: eval.Policy.ID.String(), + }) + } + + // compare found risks with existing ones using hash-based identity + comparison := utils.CompareSlices(foundRisks, existingRisks, func(r models.ComplianceRisk) string { + return r.CalculateHash().String() + }) + + newRisks := comparison.OnlyInA + fixedRisks := comparison.OnlyInB + inBoth := comparison.InBoth + + // get risks from other branches for branch-diffing of new detections + existingRisksOnOtherBranch, err := s.complianceRiskRepository.GetComplianceRisksByOtherAssetVersions(ctx, tx, assetVersion.Name, assetVersion.AssetID) + if err != nil { + slog.Error("could not get existing compliance risks on other branches", "err", err) + return err + } + existingRisksOnOtherBranch = utils.Filter(existingRisksOnOtherBranch, func(r models.ComplianceRisk) bool { + return r.State != dtos.VulnStateFixed + }) + + // branch-diff new risks + branchDiff := statemachine.DiffVulnsBetweenBranches( + utils.Map(newRisks, utils.Ptr), + utils.Map(existingRisksOnOtherBranch, utils.Ptr), + ) + + // determine which "fixed" risks are truly fixed everywhere vs just removed from this artifact + existingNeedsAssoc := make([]models.ComplianceRisk, 0) + for _, r := range inBoth { + alreadyAssoc := utils.Any(r.Artifacts, func(a models.Artifact) bool { + return a.ArtifactName == artifact.ArtifactName + }) + if !alreadyAssoc { + existingNeedsAssoc = append(existingNeedsAssoc, r) + } + } + + existingNeedsDissoc := make([]models.ComplianceRisk, 0) + finallyFixed := make([]models.ComplianceRisk, 0) + for _, r := range fixedRisks { + if len(r.Artifacts) > 1 { + existingNeedsDissoc = append(existingNeedsDissoc, r) + } else if len(r.Artifacts) == 1 && r.Artifacts[0].ArtifactName != artifact.ArtifactName { + existingNeedsDissoc = append(existingNeedsDissoc, r) + } else { + finallyFixed = append(finallyFixed, r) + } + } + + return s.complianceRiskRepository.Transaction(ctx, func(db shared.DB) error { + // risks that exist on other branches: copy event history + if err := s.UserDetectedExistingComplianceRiskOnDifferentBranch(ctx, db, artifact.ArtifactName, branchDiff.ExistingOnOtherBranches, assetVersion); err != nil { + slog.Error("error processing existing compliance risk on different branch", "err", err) + return err + } + + // brand-new risks never seen before + newToAllBranches := utils.Map(utils.DereferenceSlice(branchDiff.NewToAllBranches), func(r models.ComplianceRisk) models.ComplianceRisk { return r }) + if err := s.UserDetectedComplianceRisks(ctx, db, userID, userAgent, assetVersion.Name, artifact.ArtifactName, newToAllBranches); err != nil { + return err + } + + // risks now fixed everywhere + if err := s.UserFixedComplianceRisks(ctx, db, userID, userAgent, finallyFixed); err != nil { + return err + } + + // risks seen in this artifact for the first time (already exist in other artifacts) + if err := s.UserDetectedComplianceRiskInAnotherArtifact(ctx, db, existingNeedsAssoc, artifact.ArtifactName); err != nil { + return err + } + + // risks no longer seen in this artifact (still exists in others) + if err := s.UserDidNotDetectComplianceRiskInArtifactAnymore(ctx, db, existingNeedsDissoc, artifact.ArtifactName); err != nil { + return err + } + + return nil + }) +} + +func (s *ComplianceRiskService) UserDetectedExistingComplianceRiskOnDifferentBranch(ctx context.Context, tx shared.DB, artifactName string, matches []statemachine.BranchVulnMatch[*models.ComplianceRisk], assetVersion models.AssetVersion) error { + if len(matches) == 0 { + return nil + } + + risks := utils.Map(matches, func(m statemachine.BranchVulnMatch[*models.ComplianceRisk]) models.ComplianceRisk { + r := *m.CurrentBranchVuln + r.Artifacts = append(r.Artifacts, models.Artifact{ + ArtifactName: artifactName, + AssetVersionName: assetVersion.Name, + AssetID: assetVersion.AssetID, + }) + return r + }) + events := utils.Map(matches, func(m statemachine.BranchVulnMatch[*models.ComplianceRisk]) []models.VulnEvent { + return m.EventsToCopy + }) + + if err := s.complianceRiskRepository.SaveBatch(ctx, tx, risks); err != nil { + return err + } + return s.vulnEventRepository.SaveBatch(ctx, tx, utils.Flat(events)) +} + +func (s *ComplianceRiskService) UserDetectedComplianceRisks(ctx context.Context, tx shared.DB, userID string, userAgent *string, assetVersionName, artifactName string, risks []models.ComplianceRisk) error { + if len(risks) == 0 { + return nil + } + events := make([]models.VulnEvent, len(risks)) + for i := range risks { + risks[i].Artifacts = append(risks[i].Artifacts, models.Artifact{ + ArtifactName: artifactName, + AssetVersionName: assetVersionName, + AssetID: risks[i].AssetID, + }) + ev := models.NewDetectedEvent(risks[i].CalculateHash(), dtos.VulnTypeComplianceRisk, userID, dtos.RiskCalculationReport{}, artifactName, false, userAgent) + statemachine.Apply(&risks[i], ev) + events[i] = ev + } + if err := s.complianceRiskRepository.SaveBatch(ctx, tx, risks); err != nil { + return err + } + return s.vulnEventRepository.SaveBatch(ctx, tx, events) +} + +func (s *ComplianceRiskService) UserFixedComplianceRisks(ctx context.Context, tx shared.DB, userID string, userAgent *string, risks []models.ComplianceRisk) error { + if len(risks) == 0 { + return nil + } + events := make([]models.VulnEvent, len(risks)) + for i := range risks { + ev := models.NewFixedEvent(risks[i].CalculateHash(), dtos.VulnTypeComplianceRisk, userID, "", false, userAgent) + statemachine.Apply(&risks[i], ev) + events[i] = ev + } + if err := s.complianceRiskRepository.SaveBatch(ctx, tx, risks); err != nil { + return err + } + return s.vulnEventRepository.SaveBatch(ctx, tx, events) +} + +func (s *ComplianceRiskService) UserDetectedComplianceRiskInAnotherArtifact(ctx context.Context, tx shared.DB, risks []models.ComplianceRisk, artifactName string) error { + if len(risks) == 0 { + return nil + } + for i := range risks { + if err := tx.Exec( + "INSERT INTO artifact_compliance_risks (artifact_artifact_name, artifact_asset_version_name, artifact_asset_id, compliance_risk_id) VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING", + artifactName, risks[i].AssetVersionName, risks[i].AssetID, risks[i].CalculateHash(), + ).Error; err != nil { + return err + } + } + return nil +} + +func (s *ComplianceRiskService) UserDidNotDetectComplianceRiskInArtifactAnymore(ctx context.Context, tx shared.DB, risks []models.ComplianceRisk, artifactName string) error { + if len(risks) == 0 { + return nil + } + for i := range risks { + if err := tx.Exec( + "DELETE FROM artifact_compliance_risks WHERE artifact_artifact_name = ? AND artifact_asset_version_name = ? AND artifact_asset_id = ? AND compliance_risk_id = ?", + artifactName, risks[i].AssetVersionName, risks[i].AssetID, risks[i].ID, + ).Error; err != nil { + return err + } + } + return nil +} + +func (s *ComplianceRiskService) UpdateComplianceRiskState(ctx context.Context, tx shared.DB, userID string, risk *models.ComplianceRisk, statusType string, justification string, mechanicalJustification dtos.MechanicalJustificationType, userAgent *string) (models.VulnEvent, error) { + if tx == nil { + var ev models.VulnEvent + var err error + err = s.complianceRiskRepository.Transaction(ctx, func(d shared.DB) error { + ev, err = s.updateComplianceRiskState(ctx, d, userID, risk, statusType, justification, mechanicalJustification, userAgent) + return err + }) + return ev, err + } + return s.updateComplianceRiskState(ctx, tx, userID, risk, statusType, justification, mechanicalJustification, userAgent) +} + +func (s *ComplianceRiskService) updateComplianceRiskState(ctx context.Context, tx shared.DB, userID string, risk *models.ComplianceRisk, statusType string, justification string, mechanicalJustification dtos.MechanicalJustificationType, userAgent *string) (models.VulnEvent, error) { + var ev models.VulnEvent + switch dtos.VulnEventType(statusType) { + case dtos.EventTypeAccepted: + ev = models.NewAcceptedEvent(risk.CalculateHash(), dtos.VulnTypeComplianceRisk, userID, justification, false, userAgent) + case dtos.EventTypeFalsePositive: + ev = models.NewFalsePositiveEvent(risk.CalculateHash(), dtos.VulnTypeComplianceRisk, userID, justification, mechanicalJustification, risk.GetArtifactNames(), false, userAgent) + case dtos.EventTypeReopened: + ev = models.NewReopenedEvent(risk.CalculateHash(), dtos.VulnTypeComplianceRisk, userID, justification, false, userAgent) + case dtos.EventTypeComment: + ev = models.NewCommentEvent(risk.CalculateHash(), dtos.VulnTypeComplianceRisk, userID, justification, false, userAgent) + } + err := s.complianceRiskRepository.ApplyAndSave(ctx, tx, risk, &ev) + return ev, err +} diff --git a/services/providers.go b/services/providers.go index 479d5824e..7a7bc9d43 100644 --- a/services/providers.go +++ b/services/providers.go @@ -18,6 +18,8 @@ var ServiceModule = fx.Options( fx.Provide(fx.Annotate(NewConfigService, fx.As(new(shared.ConfigService)))), fx.Provide(fx.Annotate(NewFirstPartyVulnService, fx.As(new(shared.FirstPartyVulnService)))), fx.Provide(fx.Annotate(NewLicenseRiskService, fx.As(new(shared.LicenseRiskService)))), + fx.Provide(fx.Annotate(NewComplianceService, fx.As(new(shared.ComplianceService)))), + fx.Provide(fx.Annotate(NewComplianceRiskService, fx.As(new(shared.ComplianceRiskService)))), fx.Provide(fx.Annotate(NewProjectService, fx.As(new(shared.ProjectService)))), fx.Provide(fx.Annotate(NewAssetService, fx.As(new(shared.AssetService)))), fx.Provide(fx.Annotate(NewComponentService, fx.As(new(shared.ComponentService)))), diff --git a/shared/common_interfaces.go b/shared/common_interfaces.go index 99bf8645d..7b4c20be9 100644 --- a/shared/common_interfaces.go +++ b/shared/common_interfaces.go @@ -292,6 +292,21 @@ type LicenseRiskRepository interface { ApplyAndSave(ctx context.Context, tx DB, licenseRisk *models.LicenseRisk, vulnEvent *models.VulnEvent) error } +type ComplianceRiskRepository interface { + utils.Repository[uuid.UUID, models.ComplianceRisk, DB] + GetAllComplianceRisksForAssetVersion(ctx context.Context, tx DB, assetID uuid.UUID, assetVersionName string) ([]models.ComplianceRisk, error) + GetAllComplianceRisksForAssetVersionPaged(ctx context.Context, tx DB, assetID uuid.UUID, assetVersionName string, pageInfo PageInfo, search string, filter []FilterQuery, sort []SortQuery) (Paged[models.ComplianceRisk], error) + GetComplianceRisksByOtherAssetVersions(ctx context.Context, tx DB, assetVersionName string, assetID uuid.UUID) ([]models.ComplianceRisk, error) + Read(ctx context.Context, tx DB, id uuid.UUID) (models.ComplianceRisk, error) + ApplyAndSave(ctx context.Context, tx DB, risk *models.ComplianceRisk, ev *models.VulnEvent) error + SaveBatch(ctx context.Context, tx DB, risks []models.ComplianceRisk) error +} + +type ComplianceRiskService interface { + HandleArtifactCompliance(ctx context.Context, tx DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, evaluations []compliance.PolicyEvaluation) error + UpdateComplianceRiskState(ctx context.Context, tx DB, userID string, risk *models.ComplianceRisk, statusType string, justification string, mechanicalJustification dtos.MechanicalJustificationType, userAgent *string) (models.VulnEvent, error) +} + type InTotoLinkRepository interface { utils.Repository[uuid.UUID, models.InTotoLink, DB] FindByAssetAndSupplyChainID(ctx context.Context, tx DB, assetID uuid.UUID, supplyChainID string) ([]models.InTotoLink, error) diff --git a/tests/fx_test_app.go b/tests/fx_test_app.go index 7dee485aa..4412b4f2d 100644 --- a/tests/fx_test_app.go +++ b/tests/fx_test_app.go @@ -97,6 +97,9 @@ type TestApp struct { ComponentProjectRepository shared.ComponentProjectRepository StatisticsRepository shared.StatisticsRepository AttestationRepository shared.AttestationRepository + ComplianceService shared.ComplianceService + ComplianceRiskService shared.ComplianceRiskService + ComplianceRiskRepository shared.ComplianceRiskRepository LicenseRiskRepository shared.LicenseRiskRepository GitLabOauth2TokenRepository shared.GitLabOauth2TokenRepository GitlabIntegrationRepository shared.GitlabIntegrationRepository diff --git a/tests/fx_test_helpers.go b/tests/fx_test_helpers.go index 4444b6bab..a326f971c 100644 --- a/tests/fx_test_helpers.go +++ b/tests/fx_test_helpers.go @@ -182,6 +182,8 @@ func (f *TestFixture) CreateDaemonRunner() *daemons.DaemonRunner { f.App.FixedVersionResolver, f.App.AttestationRepository, f.App.StatisticsRepository, + f.App.ComplianceService, + f.App.ComplianceRiskService, ) } diff --git a/transformer/compliance_risk_transformer.go b/transformer/compliance_risk_transformer.go new file mode 100644 index 000000000..f6d5ab4a3 --- /dev/null +++ b/transformer/compliance_risk_transformer.go @@ -0,0 +1,31 @@ +package transformer + +import ( + "github.com/l3montree-dev/devguard/database/models" + "github.com/l3montree-dev/devguard/dtos" +) + +func ComplianceRiskToDTO(r models.ComplianceRisk) dtos.ComplianceRiskDTO { + artifacts := make([]dtos.ComplianceRiskArtifactDTO, len(r.Artifacts)) + for i, a := range r.Artifacts { + artifacts[i] = dtos.ComplianceRiskArtifactDTO{ + ArtifactName: a.ArtifactName, + AssetVersionName: a.AssetVersionName, + AssetID: a.AssetID.String(), + } + } + + return dtos.ComplianceRiskDTO{ + ID: r.ID, + Message: r.Message, + AssetVersionName: r.AssetVersionName, + AssetID: r.AssetID.String(), + State: r.State, + CreatedAt: r.CreatedAt, + TicketID: r.TicketID, + TicketURL: r.TicketURL, + ManualTicketCreation: r.ManualTicketCreation, + PolicyID: r.PolicyID, + Artifacts: artifacts, + } +} From 1601dd2d1e439a34dc472124fa18fc2cad0ef6c8 Mon Sep 17 00:00:00 2001 From: rafi Date: Wed, 3 Jun 2026 11:33:25 +0200 Subject: [PATCH 04/26] add zip upload and recalculate endpoints for compliance risks Signed-off-by: rafi --- controllers/compliance_risk_controller.go | 125 +++++++++++++++++++++- controllers/providers.go | 1 + router/compliance_risk_router.go | 15 +-- 3 files changed, 134 insertions(+), 7 deletions(-) diff --git a/controllers/compliance_risk_controller.go b/controllers/compliance_risk_controller.go index fb3752a49..ba88f060a 100644 --- a/controllers/compliance_risk_controller.go +++ b/controllers/compliance_risk_controller.go @@ -1,9 +1,13 @@ package controllers import ( + "archive/zip" + "bytes" "encoding/json" + "io" "log/slog" + "github.com/l3montree-dev/devguard/compliance" "github.com/l3montree-dev/devguard/database/models" "github.com/l3montree-dev/devguard/dtos" "github.com/l3montree-dev/devguard/shared" @@ -15,12 +19,24 @@ import ( type ComplianceRiskController struct { complianceRiskRepository shared.ComplianceRiskRepository complianceRiskService shared.ComplianceRiskService + complianceService shared.ComplianceService + attestationRepository shared.AttestationRepository + artifactRepository shared.ArtifactRepository } -func NewComplianceRiskController(repo shared.ComplianceRiskRepository, svc shared.ComplianceRiskService) *ComplianceRiskController { +func NewComplianceRiskController( + repo shared.ComplianceRiskRepository, + svc shared.ComplianceRiskService, + complianceService shared.ComplianceService, + attestationRepository shared.AttestationRepository, + artifactRepository shared.ArtifactRepository, +) *ComplianceRiskController { return &ComplianceRiskController{ complianceRiskRepository: repo, complianceRiskService: svc, + complianceService: complianceService, + attestationRepository: attestationRepository, + artifactRepository: artifactRepository, } } @@ -152,3 +168,110 @@ func (c *ComplianceRiskController) Mitigate(ctx shared.Context) error { } return ctx.JSON(200, convertComplianceRiskToDetailedDTO(risk)) } + +// RecalculateFromService fetches evaluations via complianceService.ArtifactCompliance and recalculates risks. +func (c *ComplianceRiskController) RecalculateFromService(ctx shared.Context) error { + assetVersion := shared.GetAssetVersion(ctx) + artifact := shared.GetArtifact(ctx) + project := shared.GetProject(ctx) + userAgent := ctx.Request().UserAgent() + userID := shared.GetSession(ctx).GetUserID() + + evaluations, err := c.complianceService.ArtifactCompliance(ctx.Request().Context(), project.ID, assetVersion, artifact) + if err != nil { + return echo.NewHTTPError(500, "could not evaluate artifact compliance").WithInternal(err) + } + + if err := c.complianceRiskService.HandleArtifactCompliance(ctx.Request().Context(), nil, userID, &userAgent, assetVersion, artifact, evaluations); err != nil { + return echo.NewHTTPError(500, "could not handle artifact compliance risks").WithInternal(err) + } + + return ctx.JSON(200, evaluations) +} + +// UploadZip accepts a ZIP file containing attestation files and an evaluations.json. +// It saves each attestation and then recalculates compliance risks based on the evaluations. +func (c *ComplianceRiskController) UploadZip(ctx shared.Context) error { + assetVersion := shared.GetAssetVersion(ctx) + artifact := shared.GetArtifact(ctx) + userAgent := ctx.Request().UserAgent() + userID := shared.GetSession(ctx).GetUserID() + + file, err := ctx.FormFile("file") + if err != nil { + return echo.NewHTTPError(400, "missing zip file").WithInternal(err) + } + + src, err := file.Open() + if err != nil { + return echo.NewHTTPError(500, "could not open zip file").WithInternal(err) + } + defer src.Close() + + data, err := io.ReadAll(src) + if err != nil { + return echo.NewHTTPError(500, "could not read zip file").WithInternal(err) + } + + zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) + if err != nil { + return echo.NewHTTPError(400, "invalid zip file").WithInternal(err) + } + + var evaluations []compliance.PolicyEvaluation + + for _, f := range zr.File { + rc, err := f.Open() + if err != nil { + slog.Warn("could not open file in zip", "name", f.Name, "err", err) + continue + } + content, err := io.ReadAll(rc) + rc.Close() + if err != nil { + slog.Warn("could not read file in zip", "name", f.Name, "err", err) + continue + } + + if f.Name == "evaluations.json" { + if err := json.Unmarshal(content, &evaluations); err != nil { + return echo.NewHTTPError(400, "invalid evaluations.json in zip").WithInternal(err) + } + continue + } + + // treat remaining files as attestations; predicateType is read from the JSON content + var contentMap map[string]any + if err := json.Unmarshal(content, &contentMap); err != nil { + slog.Warn("skipping non-JSON attestation file in zip", "name", f.Name, "err", err) + continue + } + + predicateType, ok := contentMap["predicateType"].(string) + if !ok || predicateType == "" { + slog.Warn("attestation file missing predicateType field, skipping", "name", f.Name) + continue + } + + attestation := models.Attestation{ + AssetID: assetVersion.AssetID, + AssetVersionName: assetVersion.Name, + ArtifactName: artifact.ArtifactName, + PredicateType: predicateType, + Content: contentMap, + } + if err := c.attestationRepository.Create(ctx.Request().Context(), nil, &attestation); err != nil { + slog.Error("could not save attestation from zip", "name", f.Name, "err", err) + } + } + + if evaluations == nil { + return echo.NewHTTPError(400, "evaluations.json not found in zip") + } + + if err := c.complianceRiskService.HandleArtifactCompliance(ctx.Request().Context(), nil, userID, &userAgent, assetVersion, artifact, evaluations); err != nil { + return echo.NewHTTPError(500, "could not handle artifact compliance risks").WithInternal(err) + } + + return ctx.JSON(200, evaluations) +} diff --git a/controllers/providers.go b/controllers/providers.go index 193f9c996..0f219b984 100644 --- a/controllers/providers.go +++ b/controllers/providers.go @@ -94,6 +94,7 @@ var ControllerModule = fx.Options( fx.Provide(NewAttestationController), fx.Provide(NewInToToController), fx.Provide(NewPolicyController), + fx.Provide(NewComplianceRiskController), // Integrations fx.Provide(NewIntegrationController), diff --git a/router/compliance_risk_router.go b/router/compliance_risk_router.go index 020259827..f693511d6 100644 --- a/router/compliance_risk_router.go +++ b/router/compliance_risk_router.go @@ -14,11 +14,14 @@ func NewComplianceRiskRouter( assetVersionGroup AssetVersionRouter, controller *controllers.ComplianceRiskController, ) ComplianceRiskRouter { - g := assetVersionGroup.Group.Group("/compliance-risks") - g.GET("/", controller.ListPaged) - g.GET("/:complianceRiskID/", controller.Read) - g.POST("/:complianceRiskID/", controller.CreateEvent, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) - g.POST("/:complianceRiskID/mitigate/", controller.Mitigate, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) + complianceRisksRouter := assetVersionGroup.Group.Group("/compliance-risks") + complianceRisksRouter.GET("/", controller.ListPaged) + complianceRisksRouter.GET("/:complianceRiskID/", controller.Read) + complianceRisksRouter.POST("/:complianceRiskID/", controller.CreateEvent, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) + complianceRisksRouter.POST("/:complianceRiskID/mitigate/", controller.Mitigate, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) - return ComplianceRiskRouter{Group: g} + complianceRisksRouter.POST("/recalculate/", controller.RecalculateFromService, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) + complianceRisksRouter.POST("/upload-zip/", controller.UploadZip, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) + + return ComplianceRiskRouter{Group: complianceRisksRouter} } From 8519317069b44e95a417e320e2da39877fa2eef9 Mon Sep 17 00:00:00 2001 From: rafi Date: Wed, 3 Jun 2026 12:16:45 +0200 Subject: [PATCH 05/26] add attestation service Signed-off-by: rafi --- controllers/attestation_controller.go | 14 +++--- controllers/compliance_controller.go | 10 ++--- controllers/compliance_risk_controller.go | 8 ++-- daemons/attestation_daemon.go | 2 +- daemons/providers.go | 6 +-- services/attestation_service.go | 52 +++++++++++++++++++++++ services/providers.go | 1 + shared/common_interfaces.go | 7 +++ tests/fx_test_app.go | 1 + tests/fx_test_helpers.go | 2 +- 10 files changed, 82 insertions(+), 21 deletions(-) create mode 100644 services/attestation_service.go diff --git a/controllers/attestation_controller.go b/controllers/attestation_controller.go index da07feba6..1b6399cb6 100644 --- a/controllers/attestation_controller.go +++ b/controllers/attestation_controller.go @@ -12,14 +12,14 @@ import ( ) type AttestationController struct { - attestationRepository shared.AttestationRepository - artifactRepository shared.ArtifactRepository + attestationService shared.AttestationService + artifactRepository shared.ArtifactRepository } -func NewAttestationController(repository shared.AttestationRepository, artifactRepository shared.ArtifactRepository) *AttestationController { +func NewAttestationController(attestationService shared.AttestationService, artifactRepository shared.ArtifactRepository) *AttestationController { return &AttestationController{ - attestationRepository: repository, - artifactRepository: artifactRepository, + attestationService: attestationService, + artifactRepository: artifactRepository, } } @@ -38,7 +38,7 @@ func (a *AttestationController) List(ctx shared.Context) error { asset := shared.GetAsset(ctx) assetVersion := shared.GetAssetVersion(ctx) - attestationList, err := a.attestationRepository.GetByAssetVersionAndAssetID(ctx.Request().Context(), nil, asset.GetID(), assetVersion.Name) + attestationList, err := a.attestationService.GetByAssetVersionAndAssetID(ctx.Request().Context(), nil, asset.GetID(), assetVersion.Name) if err != nil { return err } @@ -100,7 +100,7 @@ func (a *AttestationController) Create(ctx shared.Context) error { return echo.NewHTTPError(400, fmt.Sprintf("could not validate request: %s", err.Error())) } attestation.Content = jsonContent - err = a.attestationRepository.Create(ctx.Request().Context(), nil, &attestation) + err = a.attestationService.Create(ctx.Request().Context(), nil, &attestation) if err != nil { return err } diff --git a/controllers/compliance_controller.go b/controllers/compliance_controller.go index a4abaabaa..6c8614dd8 100644 --- a/controllers/compliance_controller.go +++ b/controllers/compliance_controller.go @@ -12,21 +12,21 @@ import ( type ComplianceController struct { assetVersionRepository shared.AssetVersionRepository - attestationRepository shared.AttestationRepository + attestationService shared.AttestationService policyRepository shared.PolicyRepository } -func NewComplianceController(assetVersionRepository shared.AssetVersionRepository, attestationRepository shared.AttestationRepository, policyRepository shared.PolicyRepository) *ComplianceController { +func NewComplianceController(assetVersionRepository shared.AssetVersionRepository, attestationService shared.AttestationService, policyRepository shared.PolicyRepository) *ComplianceController { return &ComplianceController{ assetVersionRepository: assetVersionRepository, policyRepository: policyRepository, - attestationRepository: attestationRepository, + attestationService: attestationService, } } func (c *ComplianceController) getAssetVersionCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion) ([]compliance.PolicyEvaluation, error) { // get the attestation - attestations, err := c.attestationRepository.GetByAssetVersionAndAssetID(ctx, nil, assetVersion.AssetID, assetVersion.Name) + attestations, err := c.attestationService.GetByAssetVersionAndAssetID(ctx, nil, assetVersion.AssetID, assetVersion.Name) if err != nil { return nil, err } @@ -72,7 +72,7 @@ func (c *ComplianceController) Details(ctx shared.Context) error { return ctx.JSON(404, nil) } - attestations, err := c.attestationRepository.GetByAssetVersionAndAssetID(ctx.Request().Context(), nil, assetVersion.AssetID, assetVersion.Name) + attestations, err := c.attestationService.GetByAssetVersionAndAssetID(ctx.Request().Context(), nil, assetVersion.AssetID, assetVersion.Name) if err != nil { return ctx.JSON(500, nil) diff --git a/controllers/compliance_risk_controller.go b/controllers/compliance_risk_controller.go index ba88f060a..6291b3cd4 100644 --- a/controllers/compliance_risk_controller.go +++ b/controllers/compliance_risk_controller.go @@ -20,7 +20,7 @@ type ComplianceRiskController struct { complianceRiskRepository shared.ComplianceRiskRepository complianceRiskService shared.ComplianceRiskService complianceService shared.ComplianceService - attestationRepository shared.AttestationRepository + attestationService shared.AttestationService artifactRepository shared.ArtifactRepository } @@ -28,14 +28,14 @@ func NewComplianceRiskController( repo shared.ComplianceRiskRepository, svc shared.ComplianceRiskService, complianceService shared.ComplianceService, - attestationRepository shared.AttestationRepository, + attestationService shared.AttestationService, artifactRepository shared.ArtifactRepository, ) *ComplianceRiskController { return &ComplianceRiskController{ complianceRiskRepository: repo, complianceRiskService: svc, complianceService: complianceService, - attestationRepository: attestationRepository, + attestationService: attestationService, artifactRepository: artifactRepository, } } @@ -260,7 +260,7 @@ func (c *ComplianceRiskController) UploadZip(ctx shared.Context) error { PredicateType: predicateType, Content: contentMap, } - if err := c.attestationRepository.Create(ctx.Request().Context(), nil, &attestation); err != nil { + if err := c.attestationService.Create(ctx.Request().Context(), nil, &attestation); err != nil { slog.Error("could not save attestation from zip", "name", f.Name, "err", err) } } diff --git a/daemons/attestation_daemon.go b/daemons/attestation_daemon.go index 2946afb19..a253b43af 100644 --- a/daemons/attestation_daemon.go +++ b/daemons/attestation_daemon.go @@ -134,5 +134,5 @@ func (runner *DaemonRunner) GenerateAndStoreDevguardAttestation(ctx context.Cont Content: contentMap, } - return runner.attestationRepository.Create(ctx, nil, &attestation) + return runner.attestationService.Create(ctx, nil, &attestation) } diff --git a/daemons/providers.go b/daemons/providers.go index 0b5c5e403..c84f09b7e 100644 --- a/daemons/providers.go +++ b/daemons/providers.go @@ -62,7 +62,7 @@ type DaemonRunner struct { maliciousPackageChecker shared.MaliciousPackageChecker vulnDBImportService shared.VulnDBService vexRuleService shared.VEXRuleService - attestationRepository shared.AttestationRepository + attestationService shared.AttestationService statisticsRepository shared.StatisticsRepository complianceService shared.ComplianceService complianceRiskService shared.ComplianceRiskService @@ -110,7 +110,7 @@ func NewDaemonRunner( vulnDBImportService shared.VulnDBService, vexRuleService shared.VEXRuleService, fixedVersionResolver shared.FixedVersionResolver, - attestationRepository shared.AttestationRepository, + attestationService shared.AttestationService, statisticsRepository shared.StatisticsRepository, complianceService shared.ComplianceService, complianceRiskService shared.ComplianceRiskService, @@ -145,7 +145,7 @@ func NewDaemonRunner( vulnDBImportService: vulnDBImportService, vexRuleService: vexRuleService, fixedVersionResolver: fixedVersionResolver, - attestationRepository: attestationRepository, + attestationService: attestationService, statisticsRepository: statisticsRepository, complianceService: complianceService, complianceRiskService: complianceRiskService, diff --git a/services/attestation_service.go b/services/attestation_service.go new file mode 100644 index 000000000..2035ed8be --- /dev/null +++ b/services/attestation_service.go @@ -0,0 +1,52 @@ +// 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" + + "github.com/google/uuid" + "github.com/l3montree-dev/devguard/database/models" + "github.com/l3montree-dev/devguard/shared" +) + +type AttestationService struct { + attestationRepository shared.AttestationRepository +} + +var _ shared.AttestationService = (*AttestationService)(nil) + +func NewAttestationService(attestationRepository shared.AttestationRepository) *AttestationService { + return &AttestationService{ + attestationRepository: attestationRepository, + } +} + +func (s *AttestationService) GetByAssetID(ctx context.Context, tx shared.DB, assetID uuid.UUID) ([]models.Attestation, error) { + return s.attestationRepository.GetByAssetID(ctx, tx, assetID) +} + +func (s *AttestationService) GetByAssetVersionAndAssetID(ctx context.Context, tx shared.DB, assetID uuid.UUID, assetVersion string) ([]models.Attestation, error) { + return s.attestationRepository.GetByAssetVersionAndAssetID(ctx, tx, assetID, assetVersion) +} + +func (s *AttestationService) GetByArtifactAndAssetVersionAndAssetID(ctx context.Context, tx shared.DB, artifactName string, assetVersion string, assetID uuid.UUID) ([]models.Attestation, error) { + return s.attestationRepository.GetByArtifactAndAssetVersionAndAssetID(ctx, tx, artifactName, assetVersion, assetID) +} + +func (s *AttestationService) Create(ctx context.Context, tx shared.DB, attestation *models.Attestation) error { + return s.attestationRepository.Create(ctx, tx, attestation) +} diff --git a/services/providers.go b/services/providers.go index 7a7bc9d43..79117efb5 100644 --- a/services/providers.go +++ b/services/providers.go @@ -27,6 +27,7 @@ var ServiceModule = fx.Options( fx.Provide(func() http.Client { return utils.EgressClient }), fx.Provide(fx.Annotate(NewCSAFService, fx.As(new(shared.CSAFService)))), fx.Provide(fx.Annotate(NewArtifactService, fx.As(new(shared.ArtifactService)))), + fx.Provide(fx.Annotate(NewAttestationService, fx.As(new(shared.AttestationService)))), fx.Provide(fx.Annotate(NewStatisticsService, fx.As(new(shared.StatisticsService)))), fx.Provide(fx.Annotate(NewInTotoService, fx.As(new(shared.InTotoVerifierService)))), fx.Provide(fx.Annotate(NewOrgService, fx.As(new(shared.OrgService)))), diff --git a/shared/common_interfaces.go b/shared/common_interfaces.go index 7b4c20be9..2bab6f22c 100644 --- a/shared/common_interfaces.go +++ b/shared/common_interfaces.go @@ -163,6 +163,13 @@ type AttestationRepository interface { Create(ctx context.Context, tx DB, attestation *models.Attestation) error } +type AttestationService interface { + GetByAssetID(ctx context.Context, tx DB, assetID uuid.UUID) ([]models.Attestation, error) + GetByAssetVersionAndAssetID(ctx context.Context, tx DB, assetID uuid.UUID, assetVersion string) ([]models.Attestation, error) + GetByArtifactAndAssetVersionAndAssetID(ctx context.Context, tx DB, artifactName string, assetVersion string, assetID uuid.UUID) ([]models.Attestation, error) + Create(ctx context.Context, tx DB, attestation *models.Attestation) error +} + type ArtifactRepository interface { utils.Repository[string, models.Artifact, DB] GetByAssetIDAndAssetVersionName(ctx context.Context, tx DB, assetID uuid.UUID, assetVersionName string) ([]models.Artifact, error) diff --git a/tests/fx_test_app.go b/tests/fx_test_app.go index 4412b4f2d..f2e1054b1 100644 --- a/tests/fx_test_app.go +++ b/tests/fx_test_app.go @@ -97,6 +97,7 @@ type TestApp struct { ComponentProjectRepository shared.ComponentProjectRepository StatisticsRepository shared.StatisticsRepository AttestationRepository shared.AttestationRepository + AttestationService shared.AttestationService ComplianceService shared.ComplianceService ComplianceRiskService shared.ComplianceRiskService ComplianceRiskRepository shared.ComplianceRiskRepository diff --git a/tests/fx_test_helpers.go b/tests/fx_test_helpers.go index a326f971c..074c6ba94 100644 --- a/tests/fx_test_helpers.go +++ b/tests/fx_test_helpers.go @@ -180,7 +180,7 @@ func (f *TestFixture) CreateDaemonRunner() *daemons.DaemonRunner { f.App.VulnDBService, f.App.VexRuleService, f.App.FixedVersionResolver, - f.App.AttestationRepository, + f.App.AttestationService, f.App.StatisticsRepository, f.App.ComplianceService, f.App.ComplianceRiskService, From b9ae1a6b9eaa5791f5d5a44eae2289c8b8773425 Mon Sep 17 00:00:00 2001 From: rafi Date: Wed, 3 Jun 2026 12:55:38 +0200 Subject: [PATCH 06/26] add predicate type and attestation timestamp to compliance risks Signed-off-by: rafi --- compliance/rego.go | 8 +++++--- .../migrations/20260602000000_add_compliance_risks.up.sql | 2 ++ database/models/compliance_risk_model.go | 7 +++++-- dtos/compliance_risk_dto.go | 6 ++++-- services/compliance_risk_service.go | 4 +++- services/compliance_service.go | 4 +++- transformer/compliance_risk_transformer.go | 2 ++ 7 files changed, 24 insertions(+), 9 deletions(-) diff --git a/compliance/rego.go b/compliance/rego.go index cb54544c1..17b9f4714 100644 --- a/compliance/rego.go +++ b/compliance/rego.go @@ -8,6 +8,7 @@ import ( "regexp" "sort" "strings" + "time" "github.com/l3montree-dev/devguard/database/models" "github.com/l3montree-dev/devguard/utils" @@ -48,9 +49,10 @@ type PolicyFS struct { type PolicyEvaluation struct { models.Policy - Compliant *bool `json:"compliant"` - Violations []string `json:"violations"` - RawEvaluationResult map[string]any `json:"rawEvaluationResult"` + Compliant *bool `json:"compliant"` + Violations []string `json:"violations"` + RawEvaluationResult map[string]any `json:"rawEvaluationResult"` + AttestationUpdatedAt *time.Time `json:"attestationUpdatedAt"` } var packageRegexp = regexp.MustCompile(`(?m)^package compliance`) diff --git a/database/migrations/20260602000000_add_compliance_risks.up.sql b/database/migrations/20260602000000_add_compliance_risks.up.sql index fb76e851f..b4b4e3841 100644 --- a/database/migrations/20260602000000_add_compliance_risks.up.sql +++ b/database/migrations/20260602000000_add_compliance_risks.up.sql @@ -13,6 +13,8 @@ CREATE TABLE IF NOT EXISTS public.compliance_risks ( updated_at timestamp with time zone, deleted_at timestamp with time zone, policy_id text NOT NULL, + predicate_type text NOT NULL DEFAULT '', + attestation_updated_at timestamp with time zone, CONSTRAINT compliance_risks_pkey PRIMARY KEY (id), CONSTRAINT fk_compliance_risks_asset_versions FOREIGN KEY (asset_version_name, asset_id) REFERENCES public.asset_versions (name, asset_id) ON DELETE CASCADE diff --git a/database/models/compliance_risk_model.go b/database/models/compliance_risk_model.go index 4229ee275..38b9e6e8d 100644 --- a/database/models/compliance_risk_model.go +++ b/database/models/compliance_risk_model.go @@ -16,6 +16,7 @@ package models import ( "fmt" + "time" "github.com/google/uuid" "github.com/l3montree-dev/devguard/dtos" @@ -26,7 +27,9 @@ import ( type ComplianceRisk struct { Vulnerability - PolicyID string `json:"policyId" gorm:"type:text;"` + PolicyID string `json:"policyId" gorm:"type:text;"` + PredicateType string `json:"predicateType" gorm:"type:text;"` + AttestationUpdatedAt *time.Time `json:"attestationUpdatedAt" gorm:"type:timestamptz;"` Events []VulnEvent `gorm:"foreignKey:ComplianceRiskID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;" json:"events"` @@ -42,7 +45,7 @@ func (complianceRisk ComplianceRisk) GetType() dtos.VulnType { } func (complianceRisk *ComplianceRisk) CalculateHash() uuid.UUID { - return utils.HashToUUID(fmt.Sprintf("%s/%s/%s", complianceRisk.PolicyID, complianceRisk.AssetVersionName, complianceRisk.AssetID)) + return utils.HashToUUID(fmt.Sprintf("%s/%s/%s/%s", complianceRisk.PolicyID, complianceRisk.PredicateType, complianceRisk.AssetVersionName, complianceRisk.AssetID)) } func (complianceRisk *ComplianceRisk) BeforeSave(tx *gorm.DB) error { diff --git a/dtos/compliance_risk_dto.go b/dtos/compliance_risk_dto.go index 0805bfaf0..4282d436d 100644 --- a/dtos/compliance_risk_dto.go +++ b/dtos/compliance_risk_dto.go @@ -23,8 +23,10 @@ type ComplianceRiskDTO struct { TicketURL *string `json:"ticketUrl"` ManualTicketCreation bool `json:"manualTicketCreation"` - PolicyID string `json:"policyId"` - Artifacts []ComplianceRiskArtifactDTO `json:"artifacts"` + PolicyID string `json:"policyId"` + PredicateType string `json:"predicateType"` + AttestationUpdatedAt *time.Time `json:"attestationUpdatedAt"` + Artifacts []ComplianceRiskArtifactDTO `json:"artifacts"` } type DetailedComplianceRiskDTO struct { diff --git a/services/compliance_risk_service.go b/services/compliance_risk_service.go index 3ad9fa914..66f3da416 100644 --- a/services/compliance_risk_service.go +++ b/services/compliance_risk_service.go @@ -52,7 +52,9 @@ func (s *ComplianceRiskService) HandleArtifactCompliance(ctx context.Context, tx State: state, LastDetected: time.Now(), }, - PolicyID: eval.Policy.ID.String(), + PolicyID: eval.Policy.ID.String(), + PredicateType: eval.Policy.PredicateType, + AttestationUpdatedAt: eval.AttestationUpdatedAt, }) } diff --git a/services/compliance_service.go b/services/compliance_service.go index 1c58aa65f..c2599bada 100644 --- a/services/compliance_service.go +++ b/services/compliance_service.go @@ -53,7 +53,9 @@ foundMatch: if attestation.PredicateType != policy.PredicateType { continue } - results = append(results, compliance.Eval(policy, attestation.Content)) + eval := compliance.Eval(policy, attestation.Content) + eval.AttestationUpdatedAt = &attestation.UpdatedAt + results = append(results, eval) continue foundMatch } results = append(results, compliance.Eval(policy, nil)) diff --git a/transformer/compliance_risk_transformer.go b/transformer/compliance_risk_transformer.go index f6d5ab4a3..b7e36965c 100644 --- a/transformer/compliance_risk_transformer.go +++ b/transformer/compliance_risk_transformer.go @@ -26,6 +26,8 @@ func ComplianceRiskToDTO(r models.ComplianceRisk) dtos.ComplianceRiskDTO { TicketURL: r.TicketURL, ManualTicketCreation: r.ManualTicketCreation, PolicyID: r.PolicyID, + PredicateType: r.PredicateType, + AttestationUpdatedAt: r.AttestationUpdatedAt, Artifacts: artifacts, } } From 4d2c767581c5b2c566e68d78f7324b1be4a2ac77 Mon Sep 17 00:00:00 2001 From: rafi Date: Wed, 3 Jun 2026 15:49:06 +0200 Subject: [PATCH 07/26] add artifact-scoped attestation list endpoint Signed-off-by: rafi --- controllers/attestation_controller.go | 15 +++++++++++++++ router/artifact_router.go | 2 ++ router/router_test.go | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/controllers/attestation_controller.go b/controllers/attestation_controller.go index 1b6399cb6..6c83e6bc8 100644 --- a/controllers/attestation_controller.go +++ b/controllers/attestation_controller.go @@ -46,6 +46,21 @@ func (a *AttestationController) List(ctx shared.Context) error { return ctx.JSON(200, attestationList) } +func (a *AttestationController) ListByArtifact(ctx shared.Context) error { + + asset := shared.GetAsset(ctx) + assetVersion := shared.GetAssetVersion(ctx) + + artifact := shared.GetArtifact(ctx) + + attestationList, err := a.attestationService.GetByArtifactAndAssetVersionAndAssetID(ctx.Request().Context(), nil, artifact.ArtifactName, assetVersion.Name, asset.GetID()) + if err != nil { + return err + } + + return ctx.JSON(200, attestationList) +} + // @Summary Create attestation // @Tags Attestations // @Security CookieAuth diff --git a/router/artifact_router.go b/router/artifact_router.go index 3b94c3246..05e08afcc 100644 --- a/router/artifact_router.go +++ b/router/artifact_router.go @@ -30,6 +30,7 @@ func NewArtifactRouter( assetVersionGroup AssetVersionRouter, artifactController *controllers.ArtifactController, externalReferenceController *controllers.ExternalReferenceController, + attestationController *controllers.AttestationController, artifactRepository shared.ArtifactRepository, assetRepository shared.AssetRepository, ) ArtifactRouter { @@ -43,6 +44,7 @@ func NewArtifactRouter( artifactRouter.GET("/vex.xml/", artifactController.VEXXML) artifactRouter.GET("/sbom.pdf/", artifactController.BuildPDFFromSBOM) artifactRouter.GET("/vulnerability-report.pdf/", artifactController.BuildVulnerabilityReportPDF) + artifactRouter.GET("/attestations/", attestationController.ListByArtifact) artifactRouter.DELETE("/", artifactController.DeleteArtifact, middlewares.NeededScope([]string{"manage"}), assetScopedRBAC(shared.ObjectAsset, shared.ActionUpdate)) artifactRouter.PUT("/", artifactController.UpdateArtifact, middlewares.NeededScope([]string{"manage"}), assetScopedRBAC(shared.ObjectAsset, shared.ActionUpdate)) diff --git a/router/router_test.go b/router/router_test.go index 7b7251bba..d7e8ac462 100644 --- a/router/router_test.go +++ b/router/router_test.go @@ -280,7 +280,7 @@ func buildSecurityTestServer(t *testing.T, ac *mocks.AccessControl) *echo.Echo { NewFirstPartyVulnRouter(assetVersionRouter, new(controllers.FirstPartyVulnController), new(controllers.VulnEventController)) NewLicenseRiskRouter(assetVersionRouter, new(controllers.LicenseRiskController)) NewVEXRuleRouter(assetVersionRouter, new(controllers.VEXRuleController)) - NewArtifactRouter(assetVersionRouter, new(controllers.ArtifactController), new(controllers.ExternalReferenceController), artifactRepo, assetRepo) + NewArtifactRouter(assetVersionRouter, new(controllers.ArtifactController), new(controllers.ExternalReferenceController), new(controllers.AttestationController), artifactRepo, assetRepo) NewExternalReferenceRouter(assetVersionRouter, new(controllers.ExternalReferenceController), assetRepo) return e From 188c2ba063d4c847ebcf90b6b51ece4d36a67be2 Mon Sep 17 00:00:00 2001 From: rafi Date: Fri, 5 Jun 2026 11:14:50 +0200 Subject: [PATCH 08/26] add policy name to compliance risk DTO and reuse ArtifactDTO Signed-off-by: rafi --- ...20260602000000_add_compliance_risks.up.sql | 3 ++ dtos/compliance_risk_dto.go | 33 ++++++++----------- transformer/compliance_risk_transformer.go | 6 ++-- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/database/migrations/20260602000000_add_compliance_risks.up.sql b/database/migrations/20260602000000_add_compliance_risks.up.sql index b4b4e3841..9e1569828 100644 --- a/database/migrations/20260602000000_add_compliance_risks.up.sql +++ b/database/migrations/20260602000000_add_compliance_risks.up.sql @@ -36,6 +36,9 @@ CREATE TABLE IF NOT EXISTS public.artifact_compliance_risks ( ALTER TABLE public.vuln_events ADD COLUMN IF NOT EXISTS compliance_risk_id uuid; +ALTER TABLE public.vuln_events + DROP CONSTRAINT IF EXISTS fk_vuln_events_compliance_risk; + ALTER TABLE public.vuln_events ADD CONSTRAINT fk_vuln_events_compliance_risk FOREIGN KEY (compliance_risk_id) REFERENCES public.compliance_risks (id) ON DELETE CASCADE; diff --git a/dtos/compliance_risk_dto.go b/dtos/compliance_risk_dto.go index 4282d436d..ab080576d 100644 --- a/dtos/compliance_risk_dto.go +++ b/dtos/compliance_risk_dto.go @@ -6,27 +6,22 @@ import ( "github.com/google/uuid" ) -type ComplianceRiskArtifactDTO struct { - ArtifactName string `json:"artifactName"` - AssetVersionName string `json:"assetVersionName"` - AssetID string `json:"assetId"` -} - type ComplianceRiskDTO struct { - ID uuid.UUID `json:"id"` - Message *string `json:"message"` - AssetVersionName string `json:"assetVersionName"` - AssetID string `json:"assetId"` - State VulnState `json:"state"` - CreatedAt time.Time `json:"createdAt"` - TicketID *string `json:"ticketId"` - TicketURL *string `json:"ticketUrl"` - ManualTicketCreation bool `json:"manualTicketCreation"` + ID uuid.UUID `json:"id"` + Message *string `json:"message"` + AssetVersionName string `json:"assetVersionName"` + AssetID string `json:"assetId"` + State VulnState `json:"state"` + CreatedAt time.Time `json:"createdAt"` + TicketID *string `json:"ticketId"` + TicketURL *string `json:"ticketUrl"` + ManualTicketCreation bool `json:"manualTicketCreation"` - PolicyID string `json:"policyId"` - PredicateType string `json:"predicateType"` - AttestationUpdatedAt *time.Time `json:"attestationUpdatedAt"` - Artifacts []ComplianceRiskArtifactDTO `json:"artifacts"` + PolicyID string `json:"policyId"` + PolicyName string `json:"policyName"` + PredicateType string `json:"predicateType"` + AttestationUpdatedAt *time.Time `json:"attestationUpdatedAt"` + Artifacts []ArtifactDTO `json:"artifacts,omitempty"` } type DetailedComplianceRiskDTO struct { diff --git a/transformer/compliance_risk_transformer.go b/transformer/compliance_risk_transformer.go index b7e36965c..aee60051c 100644 --- a/transformer/compliance_risk_transformer.go +++ b/transformer/compliance_risk_transformer.go @@ -6,12 +6,12 @@ import ( ) func ComplianceRiskToDTO(r models.ComplianceRisk) dtos.ComplianceRiskDTO { - artifacts := make([]dtos.ComplianceRiskArtifactDTO, len(r.Artifacts)) + artifacts := make([]dtos.ArtifactDTO, len(r.Artifacts)) for i, a := range r.Artifacts { - artifacts[i] = dtos.ComplianceRiskArtifactDTO{ + artifacts[i] = dtos.ArtifactDTO{ ArtifactName: a.ArtifactName, AssetVersionName: a.AssetVersionName, - AssetID: a.AssetID.String(), + AssetID: a.AssetID, } } From d64e8d2e5f1fab1b9d71a10de058709e3a676218 Mon Sep 17 00:00:00 2001 From: rafi Date: Fri, 5 Jun 2026 15:41:12 +0200 Subject: [PATCH 09/26] refine compliance risk model, DTOs and add policy transformer Signed-off-by: rafi --- controllers/compliance_risk_controller.go | 3 +- ...20260602000000_add_compliance_risks.up.sql | 4 +- database/models/compliance_risk_model.go | 9 ++- docs/evaluations-schema.json | 78 +++++++++++++++++++ dtos/compliance_risk_dto.go | 21 ++--- dtos/policy_dto.go | 15 ++++ services/compliance_risk_service.go | 19 ++--- services/compliance_service.go | 15 ++-- shared/common_interfaces.go | 5 +- transformer/compliance_risk_transformer.go | 28 +++---- transformer/policy_transformer.go | 42 ++++++++++ 11 files changed, 191 insertions(+), 48 deletions(-) create mode 100644 docs/evaluations-schema.json create mode 100644 transformer/policy_transformer.go diff --git a/controllers/compliance_risk_controller.go b/controllers/compliance_risk_controller.go index 6291b3cd4..e4072ba2f 100644 --- a/controllers/compliance_risk_controller.go +++ b/controllers/compliance_risk_controller.go @@ -7,7 +7,6 @@ import ( "io" "log/slog" - "github.com/l3montree-dev/devguard/compliance" "github.com/l3montree-dev/devguard/database/models" "github.com/l3montree-dev/devguard/dtos" "github.com/l3montree-dev/devguard/shared" @@ -218,7 +217,7 @@ func (c *ComplianceRiskController) UploadZip(ctx shared.Context) error { return echo.NewHTTPError(400, "invalid zip file").WithInternal(err) } - var evaluations []compliance.PolicyEvaluation + var evaluations []dtos.PolicyEvaluationDTO for _, f := range zr.File { rc, err := f.Open() diff --git a/database/migrations/20260602000000_add_compliance_risks.up.sql b/database/migrations/20260602000000_add_compliance_risks.up.sql index 9e1569828..a5d01bf2f 100644 --- a/database/migrations/20260602000000_add_compliance_risks.up.sql +++ b/database/migrations/20260602000000_add_compliance_risks.up.sql @@ -3,7 +3,6 @@ CREATE TABLE IF NOT EXISTS public.compliance_risks ( asset_version_name text NOT NULL, asset_id uuid NOT NULL, message text, - scanner_ids text NOT NULL DEFAULT '', state text DEFAULT 'open' NOT NULL, last_detected timestamp with time zone DEFAULT now() NOT NULL, ticket_id text, @@ -13,7 +12,10 @@ CREATE TABLE IF NOT EXISTS public.compliance_risks ( updated_at timestamp with time zone, deleted_at timestamp with time zone, policy_id text NOT NULL, + policy_title text NOT NULL DEFAULT '', + policy_description text, predicate_type text NOT NULL DEFAULT '', + attestation_violations text[], attestation_updated_at timestamp with time zone, CONSTRAINT compliance_risks_pkey PRIMARY KEY (id), CONSTRAINT fk_compliance_risks_asset_versions FOREIGN KEY (asset_version_name, asset_id) diff --git a/database/models/compliance_risk_model.go b/database/models/compliance_risk_model.go index 38b9e6e8d..ad03d5740 100644 --- a/database/models/compliance_risk_model.go +++ b/database/models/compliance_risk_model.go @@ -27,9 +27,12 @@ import ( type ComplianceRisk struct { Vulnerability - PolicyID string `json:"policyId" gorm:"type:text;"` - PredicateType string `json:"predicateType" gorm:"type:text;"` - AttestationUpdatedAt *time.Time `json:"attestationUpdatedAt" gorm:"type:timestamptz;"` + PolicyID string `json:"policyId" gorm:"type:text;"` + PolicyTitle string `json:"policyTitle" gorm:"type:text;"` + PolicyDescription *string `json:"policyDescription" gorm:"type:text;"` + PredicateType string `json:"predicateType" gorm:"type:text;"` + AttestationViolations []string `json:"attestationViolations" gorm:"type:text[];"` + AttestationUpdatedAt *time.Time `json:"attestationUpdatedAt" gorm:"type:timestamptz;"` Events []VulnEvent `gorm:"foreignKey:ComplianceRiskID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;" json:"events"` diff --git a/docs/evaluations-schema.json b/docs/evaluations-schema.json new file mode 100644 index 000000000..b6050c644 --- /dev/null +++ b/docs/evaluations-schema.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://devguard.dev/schemas/evaluations.json", + "title": "PolicyEvaluations", + "description": "Array of policy evaluation results uploaded as evaluations.json inside the attestation ZIP.", + "type": "array", + "items": { + "type": "object", + "required": ["policyId", "policyTitle", "state", "predicateType"], + "properties": { + "policyId": { + "type": "string", + "description": "ID of the evaluated policy." + }, + "policyTitle": { + "type": "string", + "description": "Human-readable title of the policy." + }, + "policyDescription": { + "type": ["string", "null"], + "description": "Optional description of the policy." + }, + "state": { + "type": "string", + "enum": ["open", "fixed"], + "description": "Compliance state. Use 'open' for a violation, 'fixed' for a passing policy." + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when this evaluation was created (RFC 3339)." + }, + "predicateType": { + "type": "string", + "description": "The in-toto predicate type this policy evaluates against (e.g. 'https://slsa.dev/provenance/v1')." + }, + "attestationUpdatedAt": { + "type": ["string", "null"], + "format": "date-time", + "description": "Timestamp of the attestation that was evaluated." + }, + "attestationViolations": { + "type": ["array", "null"], + "items": { + "type": "string" + }, + "description": "List of violation messages when the policy is not satisfied." + } + }, + "additionalProperties": false + }, + "examples": [ + [ + { + "policyId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "policyTitle": "SLSA Provenance Required", + "policyDescription": "Every artifact must have a valid SLSA provenance attestation.", + "state": "open", + "createdAt": "2026-06-05T12:00:00Z", + "predicateType": "https://slsa.dev/provenance/v1", + "attestationUpdatedAt": "2026-06-05T11:50:00Z", + "attestationViolations": [ + "builder.id does not match expected value" + ] + }, + { + "policyId": "b2c3d4e5-f6a7-8901-bcde-f12345678901", + "policyTitle": "SBOM Present", + "policyDescription": null, + "state": "fixed", + "createdAt": "2026-06-05T12:00:00Z", + "predicateType": "https://cyclonedx.org/bom/v1.4", + "attestationUpdatedAt": "2026-06-05T11:55:00Z", + "attestationViolations": [] + } + ] + ] +} diff --git a/dtos/compliance_risk_dto.go b/dtos/compliance_risk_dto.go index ab080576d..49d863105 100644 --- a/dtos/compliance_risk_dto.go +++ b/dtos/compliance_risk_dto.go @@ -7,21 +7,24 @@ import ( ) type ComplianceRiskDTO struct { - ID uuid.UUID `json:"id"` - Message *string `json:"message"` - AssetVersionName string `json:"assetVersionName"` - AssetID string `json:"assetId"` + ID uuid.UUID `json:"id"` + AssetVersionName string `json:"assetVersionName"` + AssetID string `json:"assetId"` + Artifacts []ArtifactDTO `json:"artifacts,omitempty"` + + PolicyID string `json:"policyId"` + PolicyTitle string `json:"policyTitle"` + PolicyDescription *string `json:"policyDescription"` + State VulnState `json:"state"` CreatedAt time.Time `json:"createdAt"` TicketID *string `json:"ticketId"` TicketURL *string `json:"ticketUrl"` ManualTicketCreation bool `json:"manualTicketCreation"` - PolicyID string `json:"policyId"` - PolicyName string `json:"policyName"` - PredicateType string `json:"predicateType"` - AttestationUpdatedAt *time.Time `json:"attestationUpdatedAt"` - Artifacts []ArtifactDTO `json:"artifacts,omitempty"` + PredicateType string `json:"predicateType"` + AttestationUpdatedAt *time.Time `json:"attestationUpdatedAt"` + AttestationViolations []string `json:"attestationViolations"` } type DetailedComplianceRiskDTO struct { diff --git a/dtos/policy_dto.go b/dtos/policy_dto.go index 2a67d4981..e7325ad33 100644 --- a/dtos/policy_dto.go +++ b/dtos/policy_dto.go @@ -15,6 +15,8 @@ package dtos +import "time" + type PolicyDTO struct { Title string `json:"title"` Description string `json:"description"` @@ -22,3 +24,16 @@ type PolicyDTO struct { PredicateType string `json:"predicateType"` Rego string `json:"rego"` } + +type PolicyEvaluationDTO struct { + PolicyID string `json:"policyId"` + PolicyTitle string `json:"policyTitle"` + PolicyDescription *string `json:"policyDescription"` + + State VulnState `json:"state"` + CreatedAt time.Time `json:"createdAt"` + + PredicateType string `json:"predicateType"` + AttestationUpdatedAt *time.Time `json:"attestationUpdatedAt"` + AttestationViolations []string `json:"attestationViolations"` +} diff --git a/services/compliance_risk_service.go b/services/compliance_risk_service.go index 66f3da416..5c76448b0 100644 --- a/services/compliance_risk_service.go +++ b/services/compliance_risk_service.go @@ -5,7 +5,6 @@ import ( "log/slog" "time" - "github.com/l3montree-dev/devguard/compliance" "github.com/l3montree-dev/devguard/database/models" "github.com/l3montree-dev/devguard/dtos" "github.com/l3montree-dev/devguard/shared" @@ -29,7 +28,7 @@ func NewComplianceRiskService(complianceRiskRepository shared.ComplianceRiskRepo // HandleArtifactCompliance processes policy evaluations for an artifact and manages the // lifecycle of compliance risks: new detections, branch-diffing, artifact association, and fixes. -func (s *ComplianceRiskService) HandleArtifactCompliance(ctx context.Context, tx shared.DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, evaluations []compliance.PolicyEvaluation) error { +func (s *ComplianceRiskService) HandleArtifactCompliance(ctx context.Context, tx shared.DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, evaluations []dtos.PolicyEvaluationDTO) error { // fetch all existing compliance risks for this asset version (across all artifacts) existingRisks, err := s.complianceRiskRepository.GetAllComplianceRisksForAssetVersion(ctx, tx, assetVersion.AssetID, assetVersion.Name) if err != nil { @@ -39,22 +38,20 @@ func (s *ComplianceRiskService) HandleArtifactCompliance(ctx context.Context, tx // build risks for every evaluation — compliant ones start fixed, non-compliant ones open foundRisks := make([]models.ComplianceRisk, 0, len(evaluations)) for _, eval := range evaluations { - compliant := eval.Compliant != nil && *eval.Compliant - state := dtos.VulnStateOpen - if compliant { - state = dtos.VulnStateFixed - } foundRisks = append(foundRisks, models.ComplianceRisk{ Vulnerability: models.Vulnerability{ AssetVersionName: assetVersion.Name, AssetID: assetVersion.AssetID, AssetVersion: assetVersion, - State: state, + State: eval.State, LastDetected: time.Now(), }, - PolicyID: eval.Policy.ID.String(), - PredicateType: eval.Policy.PredicateType, - AttestationUpdatedAt: eval.AttestationUpdatedAt, + PolicyID: eval.PolicyID, + PolicyTitle: eval.PolicyTitle, + PolicyDescription: eval.PolicyDescription, + PredicateType: eval.PredicateType, + AttestationViolations: eval.AttestationViolations, + AttestationUpdatedAt: eval.AttestationUpdatedAt, }) } diff --git a/services/compliance_service.go b/services/compliance_service.go index c2599bada..a93232f2f 100644 --- a/services/compliance_service.go +++ b/services/compliance_service.go @@ -20,7 +20,10 @@ import ( "github.com/google/uuid" "github.com/l3montree-dev/devguard/compliance" "github.com/l3montree-dev/devguard/database/models" + "github.com/l3montree-dev/devguard/dtos" "github.com/l3montree-dev/devguard/shared" + "github.com/l3montree-dev/devguard/transformer" + "github.com/l3montree-dev/devguard/utils" ) type ComplianceService struct { @@ -35,18 +38,18 @@ func NewComplianceService(attestationRepository shared.AttestationRepository, po } } -func (s *ComplianceService) ArtifactCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) ([]compliance.PolicyEvaluation, error) { +func (s *ComplianceService) ArtifactCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) ([]dtos.PolicyEvaluationDTO, error) { attestations, err := s.attestationRepository.GetByArtifactAndAssetVersionAndAssetID(ctx, nil, artifact.ArtifactName, assetVersion.Name, assetVersion.AssetID) if err != nil { return nil, err } - policies, err := s.policyRepository.FindByProjectID(ctx, nil, projectID) + policies, err := s.policyRepository.FindCommunityManagedPolicies(ctx, nil) if err != nil { return nil, err } - results := make([]compliance.PolicyEvaluation, 0, len(policies)) + evals := make([]compliance.PolicyEvaluation, 0, len(policies)) foundMatch: for _, policy := range policies { for _, attestation := range attestations { @@ -55,11 +58,11 @@ foundMatch: } eval := compliance.Eval(policy, attestation.Content) eval.AttestationUpdatedAt = &attestation.UpdatedAt - results = append(results, eval) + evals = append(evals, eval) continue foundMatch } - results = append(results, compliance.Eval(policy, nil)) + evals = append(evals, compliance.Eval(policy, nil)) } - return results, nil + return utils.Map(evals, transformer.PolicyEvaluationToDTO), nil } diff --git a/shared/common_interfaces.go b/shared/common_interfaces.go index b4a4e44bd..1c0589b10 100644 --- a/shared/common_interfaces.go +++ b/shared/common_interfaces.go @@ -25,7 +25,6 @@ import ( "github.com/google/uuid" toto "github.com/in-toto/in-toto-golang/in_toto" - "github.com/l3montree-dev/devguard/compliance" "github.com/l3montree-dev/devguard/database/models" "github.com/l3montree-dev/devguard/dtos" "github.com/l3montree-dev/devguard/dtos/sarif" @@ -181,7 +180,7 @@ type ArtifactRepository interface { } type ComplianceService interface { - ArtifactCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) ([]compliance.PolicyEvaluation, error) + ArtifactCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) ([]dtos.PolicyEvaluationDTO, error) } type ReleaseRepository interface { @@ -310,7 +309,7 @@ type ComplianceRiskRepository interface { } type ComplianceRiskService interface { - HandleArtifactCompliance(ctx context.Context, tx DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, evaluations []compliance.PolicyEvaluation) error + HandleArtifactCompliance(ctx context.Context, tx DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, evaluations []dtos.PolicyEvaluationDTO) error UpdateComplianceRiskState(ctx context.Context, tx DB, userID string, risk *models.ComplianceRisk, statusType string, justification string, mechanicalJustification dtos.MechanicalJustificationType, userAgent *string) (models.VulnEvent, error) } diff --git a/transformer/compliance_risk_transformer.go b/transformer/compliance_risk_transformer.go index aee60051c..b6122acbb 100644 --- a/transformer/compliance_risk_transformer.go +++ b/transformer/compliance_risk_transformer.go @@ -16,18 +16,20 @@ func ComplianceRiskToDTO(r models.ComplianceRisk) dtos.ComplianceRiskDTO { } return dtos.ComplianceRiskDTO{ - ID: r.ID, - Message: r.Message, - AssetVersionName: r.AssetVersionName, - AssetID: r.AssetID.String(), - State: r.State, - CreatedAt: r.CreatedAt, - TicketID: r.TicketID, - TicketURL: r.TicketURL, - ManualTicketCreation: r.ManualTicketCreation, - PolicyID: r.PolicyID, - PredicateType: r.PredicateType, - AttestationUpdatedAt: r.AttestationUpdatedAt, - Artifacts: artifacts, + ID: r.ID, + AssetVersionName: r.AssetVersionName, + AssetID: r.AssetID.String(), + State: r.State, + CreatedAt: r.CreatedAt, + TicketID: r.TicketID, + TicketURL: r.TicketURL, + ManualTicketCreation: r.ManualTicketCreation, + PolicyID: r.PolicyID, + PolicyTitle: r.PolicyTitle, + PolicyDescription: r.PolicyDescription, + PredicateType: r.PredicateType, + AttestationViolations: r.AttestationViolations, + AttestationUpdatedAt: r.AttestationUpdatedAt, + Artifacts: artifacts, } } diff --git a/transformer/policy_transformer.go b/transformer/policy_transformer.go new file mode 100644 index 000000000..213b9e092 --- /dev/null +++ b/transformer/policy_transformer.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 transformer + +import ( + "github.com/l3montree-dev/devguard/compliance" + "github.com/l3montree-dev/devguard/dtos" +) + +func PolicyEvaluationToDTO(e compliance.PolicyEvaluation) dtos.PolicyEvaluationDTO { + state := dtos.VulnStateOpen + if e.Compliant != nil && *e.Compliant { + state = dtos.VulnStateFixed + } + var desc *string + if e.Policy.Description != "" { + d := e.Policy.Description + desc = &d + } + return dtos.PolicyEvaluationDTO{ + PolicyID: e.Policy.ID.String(), + PolicyTitle: e.Policy.Title, + PolicyDescription: desc, + State: state, + PredicateType: e.Policy.PredicateType, + AttestationViolations: e.Violations, + AttestationUpdatedAt: e.AttestationUpdatedAt, + } +} From fd08e6d5ba3371f6d896a21a8a1ea3047546e043 Mon Sep 17 00:00:00 2001 From: rafi Date: Fri, 5 Jun 2026 16:33:31 +0200 Subject: [PATCH 10/26] move attestation logic from daemon to service Signed-off-by: rafi --- daemons/attestation_daemon.go | 51 +------------------------------- services/attestation_service.go | 52 ++++++++++++++++++++++++++++++++- shared/common_interfaces.go | 3 +- 3 files changed, 53 insertions(+), 53 deletions(-) diff --git a/daemons/attestation_daemon.go b/daemons/attestation_daemon.go index a253b43af..0a2acb2d3 100644 --- a/daemons/attestation_daemon.go +++ b/daemons/attestation_daemon.go @@ -1,14 +1,8 @@ package daemons import ( - "context" - "encoding/json" "log/slog" - "time" - "github.com/google/uuid" - "github.com/l3montree-dev/devguard/database/models" - "github.com/l3montree-dev/devguard/dtos" "github.com/l3montree-dev/devguard/monitoring" ) @@ -74,7 +68,7 @@ func (runner *DaemonRunner) GenerateDevguardAttestations(input <-chan assetWithP for _, assetVersion := range assetWithDetails.assetVersions { for _, artifact := range assetVersion.Artifacts { - if err := runner.GenerateAndStoreDevguardAttestation(stageCtx, assetVersion.AssetID, assetVersion.Name, artifact.ArtifactName); err != nil { + if err := runner.attestationService.GenerateAndStoreDevguardAttestation(stageCtx, assetVersion.AssetID, assetVersion.Name, artifact.ArtifactName); err != nil { slog.Error("could not generate devguard attestation", "assetID", assetWithDetails.asset.ID, "assetVersion", assetVersion.Name, @@ -93,46 +87,3 @@ func (runner *DaemonRunner) GenerateDevguardAttestations(input <-chan assetWithP return out } - -func (runner *DaemonRunner) GenerateAndStoreDevguardAttestation(ctx context.Context, assetID uuid.UUID, assetVersionName string, artifactName string) error { - averages, err := runner.statisticsRepository.AverageFixingTimes(ctx, nil, assetVersionName, assetID) - if err != nil { - return err - } - - attestationDTO := dtos.DevguardAssetAttestationDTO{ - Type: dtos.DevguardAssetAttestationPredicateType, - GeneratedAt: time.Now().UTC(), - SchemaVersion: "1.0.0", - MeanTimeToRemediate: dtos.MeanTimeToRemediateDTO{ - RiskLowAvgHours: averages.RiskAvgLow / secondsPerHour, - RiskMediumAvgHours: averages.RiskAvgMedium / secondsPerHour, - RiskHighAvgHours: averages.RiskAvgHigh / secondsPerHour, - RiskCriticalAvgHours: averages.RiskAvgCritical / secondsPerHour, - CVSSLowAvgHours: averages.CVSSAvgLow / secondsPerHour, - CVSSMediumAvgHours: averages.CVSSAvgMedium / secondsPerHour, - CVSSHighAvgHours: averages.CVSSAvgHigh / secondsPerHour, - CVSSCriticalAvgHours: averages.CVSSAvgCritical / secondsPerHour, - }, - } - - content, err := json.Marshal(attestationDTO) - if err != nil { - return err - } - - var contentMap map[string]any - if err := json.Unmarshal(content, &contentMap); err != nil { - return err - } - - attestation := models.Attestation{ - AssetID: assetID, - AssetVersionName: assetVersionName, - ArtifactName: artifactName, - PredicateType: dtos.DevguardAssetAttestationPredicateType, - Content: contentMap, - } - - return runner.attestationService.Create(ctx, nil, &attestation) -} diff --git a/services/attestation_service.go b/services/attestation_service.go index 2035ed8be..213a972c7 100644 --- a/services/attestation_service.go +++ b/services/attestation_service.go @@ -17,21 +17,28 @@ package services import ( "context" + "encoding/json" + "time" "github.com/google/uuid" "github.com/l3montree-dev/devguard/database/models" + "github.com/l3montree-dev/devguard/dtos" "github.com/l3montree-dev/devguard/shared" ) +const secondsPerHour = 3600.0 + type AttestationService struct { attestationRepository shared.AttestationRepository + statisticsRepository shared.StatisticsRepository } var _ shared.AttestationService = (*AttestationService)(nil) -func NewAttestationService(attestationRepository shared.AttestationRepository) *AttestationService { +func NewAttestationService(attestationRepository shared.AttestationRepository, statisticsRepository shared.StatisticsRepository) *AttestationService { return &AttestationService{ attestationRepository: attestationRepository, + statisticsRepository: statisticsRepository, } } @@ -50,3 +57,46 @@ func (s *AttestationService) GetByArtifactAndAssetVersionAndAssetID(ctx context. func (s *AttestationService) Create(ctx context.Context, tx shared.DB, attestation *models.Attestation) error { return s.attestationRepository.Create(ctx, tx, attestation) } + +func (s *AttestationService) GenerateAndStoreDevguardAttestation(ctx context.Context, assetID uuid.UUID, assetVersionName string, artifactName string) error { + averages, err := s.statisticsRepository.AverageFixingTimes(ctx, nil, assetVersionName, assetID) + if err != nil { + return err + } + + attestationDTO := dtos.DevguardAssetAttestationDTO{ + Type: dtos.DevguardAssetAttestationPredicateType, + GeneratedAt: time.Now().UTC(), + SchemaVersion: "1.0.0", + MeanTimeToRemediate: dtos.MeanTimeToRemediateDTO{ + RiskLowAvgHours: averages.RiskAvgLow / secondsPerHour, + RiskMediumAvgHours: averages.RiskAvgMedium / secondsPerHour, + RiskHighAvgHours: averages.RiskAvgHigh / secondsPerHour, + RiskCriticalAvgHours: averages.RiskAvgCritical / secondsPerHour, + CVSSLowAvgHours: averages.CVSSAvgLow / secondsPerHour, + CVSSMediumAvgHours: averages.CVSSAvgMedium / secondsPerHour, + CVSSHighAvgHours: averages.CVSSAvgHigh / secondsPerHour, + CVSSCriticalAvgHours: averages.CVSSAvgCritical / secondsPerHour, + }, + } + + content, err := json.Marshal(attestationDTO) + if err != nil { + return err + } + + var contentMap map[string]any + if err := json.Unmarshal(content, &contentMap); err != nil { + return err + } + + attestation := models.Attestation{ + AssetID: assetID, + AssetVersionName: assetVersionName, + ArtifactName: artifactName, + PredicateType: dtos.DevguardAssetAttestationPredicateType, + Content: contentMap, + } + + return s.attestationRepository.Create(ctx, nil, &attestation) +} diff --git a/shared/common_interfaces.go b/shared/common_interfaces.go index 1c0589b10..11fd09824 100644 --- a/shared/common_interfaces.go +++ b/shared/common_interfaces.go @@ -44,8 +44,6 @@ type DaemonRunner interface { UpdateFixedVersions(ctx context.Context) error UpdateVulnDB(ctx context.Context) error UpdateOpenSourceInsightInformation(ctx context.Context) error - GenerateAndStoreDevguardAttestation(ctx context.Context, assetID uuid.UUID, assetVersionName string, artifactName string) error - Start(ctx context.Context) } @@ -167,6 +165,7 @@ type AttestationService interface { GetByAssetVersionAndAssetID(ctx context.Context, tx DB, assetID uuid.UUID, assetVersion string) ([]models.Attestation, error) GetByArtifactAndAssetVersionAndAssetID(ctx context.Context, tx DB, artifactName string, assetVersion string, assetID uuid.UUID) ([]models.Attestation, error) Create(ctx context.Context, tx DB, attestation *models.Attestation) error + GenerateAndStoreDevguardAttestation(ctx context.Context, assetID uuid.UUID, assetVersionName string, artifactName string) error } type ArtifactRepository interface { From 5af001ec5d52019a0c44fccba05ff73362465601 Mon Sep 17 00:00:00 2001 From: rafi Date: Fri, 5 Jun 2026 17:40:33 +0200 Subject: [PATCH 11/26] fix mocks Signed-off-by: rafi --- controllers/attestation_test.go | 12 +- mocks/mock_AttestationRepository.go | 102 +- mocks/mock_AttestationService.go | 413 +++++++ mocks/mock_ComplianceRiskRepository.go | 1386 ++++++++++++++++++++++++ mocks/mock_ComplianceRiskService.go | 230 ++++ mocks/mock_ComplianceService.go | 121 +++ 6 files changed, 2250 insertions(+), 14 deletions(-) create mode 100644 mocks/mock_AttestationService.go create mode 100644 mocks/mock_ComplianceRiskRepository.go create mode 100644 mocks/mock_ComplianceRiskService.go create mode 100644 mocks/mock_ComplianceService.go diff --git a/controllers/attestation_test.go b/controllers/attestation_test.go index 3ffeae3ee..625eba5dc 100644 --- a/controllers/attestation_test.go +++ b/controllers/attestation_test.go @@ -26,12 +26,12 @@ func TestList(t *testing.T) { shared.SetAsset(ctx, asset) shared.SetAssetVersion(ctx, assetVersion) - attestationRepository := mocks.NewAttestationRepository(t) - attestationRepository.On("GetByAssetVersionAndAssetID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]models.Attestation{ + attestationService := mocks.NewAttestationService(t) + attestationService.On("GetByAssetVersionAndAssetID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]models.Attestation{ {PredicateType: "not ocol name"}, }, nil) - attestationController := NewAttestationController(attestationRepository, mocks.NewArtifactRepository(t)) + attestationController := NewAttestationController(attestationService, mocks.NewArtifactRepository(t)) result := attestationController.List(ctx) if result != nil { t.Fail() @@ -49,9 +49,9 @@ func TestList(t *testing.T) { shared.SetAsset(ctx, asset) shared.SetAssetVersion(ctx, assetVersion) - attestationRepository := mocks.NewAttestationRepository(t) - attestationRepository.On("GetByAssetVersionAndAssetID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]models.Attestation{}, fmt.Errorf("Something went wrong")) - attestationController := NewAttestationController(attestationRepository, mocks.NewArtifactRepository(t)) + attestationService := mocks.NewAttestationService(t) + attestationService.On("GetByAssetVersionAndAssetID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]models.Attestation{}, fmt.Errorf("Something went wrong")) + attestationController := NewAttestationController(attestationService, mocks.NewArtifactRepository(t)) result := attestationController.List(ctx) diff --git a/mocks/mock_AttestationRepository.go b/mocks/mock_AttestationRepository.go index 6cd045a27..49cc07340 100644 --- a/mocks/mock_AttestationRepository.go +++ b/mocks/mock_AttestationRepository.go @@ -277,8 +277,8 @@ func (_c *AttestationRepository_CleanupOrphanedRecords_Call) RunAndReturn(run fu } // Create provides a mock function for the type AttestationRepository -func (_mock *AttestationRepository) Create(ctx context.Context, tx shared.DB, t *models.Attestation) error { - ret := _mock.Called(ctx, tx, t) +func (_mock *AttestationRepository) Create(ctx context.Context, tx shared.DB, attestation *models.Attestation) error { + ret := _mock.Called(ctx, tx, attestation) if len(ret) == 0 { panic("no return value specified for Create") @@ -286,7 +286,7 @@ func (_mock *AttestationRepository) Create(ctx context.Context, tx shared.DB, t var r0 error if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, *models.Attestation) error); ok { - r0 = returnFunc(ctx, tx, t) + r0 = returnFunc(ctx, tx, attestation) } else { r0 = ret.Error(0) } @@ -301,12 +301,12 @@ type AttestationRepository_Create_Call struct { // Create is a helper method to define mock.On call // - ctx context.Context // - tx shared.DB -// - t *models.Attestation -func (_e *AttestationRepository_Expecter) Create(ctx interface{}, tx interface{}, t interface{}) *AttestationRepository_Create_Call { - return &AttestationRepository_Create_Call{Call: _e.mock.On("Create", ctx, tx, t)} +// - attestation *models.Attestation +func (_e *AttestationRepository_Expecter) Create(ctx interface{}, tx interface{}, attestation interface{}) *AttestationRepository_Create_Call { + return &AttestationRepository_Create_Call{Call: _e.mock.On("Create", ctx, tx, attestation)} } -func (_c *AttestationRepository_Create_Call) Run(run func(ctx context.Context, tx shared.DB, t *models.Attestation)) *AttestationRepository_Create_Call { +func (_c *AttestationRepository_Create_Call) Run(run func(ctx context.Context, tx shared.DB, attestation *models.Attestation)) *AttestationRepository_Create_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -334,7 +334,7 @@ func (_c *AttestationRepository_Create_Call) Return(err error) *AttestationRepos return _c } -func (_c *AttestationRepository_Create_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, t *models.Attestation) error) *AttestationRepository_Create_Call { +func (_c *AttestationRepository_Create_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, attestation *models.Attestation) error) *AttestationRepository_Create_Call { _c.Call.Return(run) return _c } @@ -528,6 +528,92 @@ func (_c *AttestationRepository_DeleteBatch_Call) RunAndReturn(run func(ctx cont return _c } +// GetByArtifactAndAssetVersionAndAssetID provides a mock function for the type AttestationRepository +func (_mock *AttestationRepository) GetByArtifactAndAssetVersionAndAssetID(ctx context.Context, tx shared.DB, artifactName string, assetVersion string, assetID uuid.UUID) ([]models.Attestation, error) { + ret := _mock.Called(ctx, tx, artifactName, assetVersion, assetID) + + if len(ret) == 0 { + panic("no return value specified for GetByArtifactAndAssetVersionAndAssetID") + } + + var r0 []models.Attestation + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, string, string, uuid.UUID) ([]models.Attestation, error)); ok { + return returnFunc(ctx, tx, artifactName, assetVersion, assetID) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, string, string, uuid.UUID) []models.Attestation); ok { + r0 = returnFunc(ctx, tx, artifactName, assetVersion, assetID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.Attestation) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, shared.DB, string, string, uuid.UUID) error); ok { + r1 = returnFunc(ctx, tx, artifactName, assetVersion, assetID) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// AttestationRepository_GetByArtifactAndAssetVersionAndAssetID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetByArtifactAndAssetVersionAndAssetID' +type AttestationRepository_GetByArtifactAndAssetVersionAndAssetID_Call struct { + *mock.Call +} + +// GetByArtifactAndAssetVersionAndAssetID is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - artifactName string +// - assetVersion string +// - assetID uuid.UUID +func (_e *AttestationRepository_Expecter) GetByArtifactAndAssetVersionAndAssetID(ctx interface{}, tx interface{}, artifactName interface{}, assetVersion interface{}, assetID interface{}) *AttestationRepository_GetByArtifactAndAssetVersionAndAssetID_Call { + return &AttestationRepository_GetByArtifactAndAssetVersionAndAssetID_Call{Call: _e.mock.On("GetByArtifactAndAssetVersionAndAssetID", ctx, tx, artifactName, assetVersion, assetID)} +} + +func (_c *AttestationRepository_GetByArtifactAndAssetVersionAndAssetID_Call) Run(run func(ctx context.Context, tx shared.DB, artifactName string, assetVersion string, assetID uuid.UUID)) *AttestationRepository_GetByArtifactAndAssetVersionAndAssetID_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 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 string + if args[3] != nil { + arg3 = args[3].(string) + } + var arg4 uuid.UUID + if args[4] != nil { + arg4 = args[4].(uuid.UUID) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + ) + }) + return _c +} + +func (_c *AttestationRepository_GetByArtifactAndAssetVersionAndAssetID_Call) Return(attestations []models.Attestation, err error) *AttestationRepository_GetByArtifactAndAssetVersionAndAssetID_Call { + _c.Call.Return(attestations, err) + return _c +} + +func (_c *AttestationRepository_GetByArtifactAndAssetVersionAndAssetID_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, artifactName string, assetVersion string, assetID uuid.UUID) ([]models.Attestation, error)) *AttestationRepository_GetByArtifactAndAssetVersionAndAssetID_Call { + _c.Call.Return(run) + return _c +} + // GetByAssetID provides a mock function for the type AttestationRepository func (_mock *AttestationRepository) GetByAssetID(ctx context.Context, tx shared.DB, assetID uuid.UUID) ([]models.Attestation, error) { ret := _mock.Called(ctx, tx, assetID) diff --git a/mocks/mock_AttestationService.go b/mocks/mock_AttestationService.go new file mode 100644 index 000000000..05b11ed52 --- /dev/null +++ b/mocks/mock_AttestationService.go @@ -0,0 +1,413 @@ +// 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" +) + +// NewAttestationService creates a new instance of AttestationService. 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 NewAttestationService(t interface { + mock.TestingT + Cleanup(func()) +}) *AttestationService { + mock := &AttestationService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// AttestationService is an autogenerated mock type for the AttestationService type +type AttestationService struct { + mock.Mock +} + +type AttestationService_Expecter struct { + mock *mock.Mock +} + +func (_m *AttestationService) EXPECT() *AttestationService_Expecter { + return &AttestationService_Expecter{mock: &_m.Mock} +} + +// Create provides a mock function for the type AttestationService +func (_mock *AttestationService) Create(ctx context.Context, tx shared.DB, attestation *models.Attestation) error { + ret := _mock.Called(ctx, tx, attestation) + + 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.Attestation) error); ok { + r0 = returnFunc(ctx, tx, attestation) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// AttestationService_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type AttestationService_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - attestation *models.Attestation +func (_e *AttestationService_Expecter) Create(ctx interface{}, tx interface{}, attestation interface{}) *AttestationService_Create_Call { + return &AttestationService_Create_Call{Call: _e.mock.On("Create", ctx, tx, attestation)} +} + +func (_c *AttestationService_Create_Call) Run(run func(ctx context.Context, tx shared.DB, attestation *models.Attestation)) *AttestationService_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.Attestation + if args[2] != nil { + arg2 = args[2].(*models.Attestation) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *AttestationService_Create_Call) Return(err error) *AttestationService_Create_Call { + _c.Call.Return(err) + return _c +} + +func (_c *AttestationService_Create_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, attestation *models.Attestation) error) *AttestationService_Create_Call { + _c.Call.Return(run) + return _c +} + +// GenerateAndStoreDevguardAttestation provides a mock function for the type AttestationService +func (_mock *AttestationService) GenerateAndStoreDevguardAttestation(ctx context.Context, assetID uuid.UUID, assetVersionName string, artifactName string) error { + ret := _mock.Called(ctx, assetID, assetVersionName, artifactName) + + if len(ret) == 0 { + panic("no return value specified for GenerateAndStoreDevguardAttestation") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, string, string) error); ok { + r0 = returnFunc(ctx, assetID, assetVersionName, artifactName) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// AttestationService_GenerateAndStoreDevguardAttestation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GenerateAndStoreDevguardAttestation' +type AttestationService_GenerateAndStoreDevguardAttestation_Call struct { + *mock.Call +} + +// GenerateAndStoreDevguardAttestation is a helper method to define mock.On call +// - ctx context.Context +// - assetID uuid.UUID +// - assetVersionName string +// - artifactName string +func (_e *AttestationService_Expecter) GenerateAndStoreDevguardAttestation(ctx interface{}, assetID interface{}, assetVersionName interface{}, artifactName interface{}) *AttestationService_GenerateAndStoreDevguardAttestation_Call { + return &AttestationService_GenerateAndStoreDevguardAttestation_Call{Call: _e.mock.On("GenerateAndStoreDevguardAttestation", ctx, assetID, assetVersionName, artifactName)} +} + +func (_c *AttestationService_GenerateAndStoreDevguardAttestation_Call) Run(run func(ctx context.Context, assetID uuid.UUID, assetVersionName string, artifactName string)) *AttestationService_GenerateAndStoreDevguardAttestation_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) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 string + if args[3] != nil { + arg3 = args[3].(string) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *AttestationService_GenerateAndStoreDevguardAttestation_Call) Return(err error) *AttestationService_GenerateAndStoreDevguardAttestation_Call { + _c.Call.Return(err) + return _c +} + +func (_c *AttestationService_GenerateAndStoreDevguardAttestation_Call) RunAndReturn(run func(ctx context.Context, assetID uuid.UUID, assetVersionName string, artifactName string) error) *AttestationService_GenerateAndStoreDevguardAttestation_Call { + _c.Call.Return(run) + return _c +} + +// GetByArtifactAndAssetVersionAndAssetID provides a mock function for the type AttestationService +func (_mock *AttestationService) GetByArtifactAndAssetVersionAndAssetID(ctx context.Context, tx shared.DB, artifactName string, assetVersion string, assetID uuid.UUID) ([]models.Attestation, error) { + ret := _mock.Called(ctx, tx, artifactName, assetVersion, assetID) + + if len(ret) == 0 { + panic("no return value specified for GetByArtifactAndAssetVersionAndAssetID") + } + + var r0 []models.Attestation + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, string, string, uuid.UUID) ([]models.Attestation, error)); ok { + return returnFunc(ctx, tx, artifactName, assetVersion, assetID) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, string, string, uuid.UUID) []models.Attestation); ok { + r0 = returnFunc(ctx, tx, artifactName, assetVersion, assetID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.Attestation) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, shared.DB, string, string, uuid.UUID) error); ok { + r1 = returnFunc(ctx, tx, artifactName, assetVersion, assetID) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// AttestationService_GetByArtifactAndAssetVersionAndAssetID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetByArtifactAndAssetVersionAndAssetID' +type AttestationService_GetByArtifactAndAssetVersionAndAssetID_Call struct { + *mock.Call +} + +// GetByArtifactAndAssetVersionAndAssetID is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - artifactName string +// - assetVersion string +// - assetID uuid.UUID +func (_e *AttestationService_Expecter) GetByArtifactAndAssetVersionAndAssetID(ctx interface{}, tx interface{}, artifactName interface{}, assetVersion interface{}, assetID interface{}) *AttestationService_GetByArtifactAndAssetVersionAndAssetID_Call { + return &AttestationService_GetByArtifactAndAssetVersionAndAssetID_Call{Call: _e.mock.On("GetByArtifactAndAssetVersionAndAssetID", ctx, tx, artifactName, assetVersion, assetID)} +} + +func (_c *AttestationService_GetByArtifactAndAssetVersionAndAssetID_Call) Run(run func(ctx context.Context, tx shared.DB, artifactName string, assetVersion string, assetID uuid.UUID)) *AttestationService_GetByArtifactAndAssetVersionAndAssetID_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 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 string + if args[3] != nil { + arg3 = args[3].(string) + } + var arg4 uuid.UUID + if args[4] != nil { + arg4 = args[4].(uuid.UUID) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + ) + }) + return _c +} + +func (_c *AttestationService_GetByArtifactAndAssetVersionAndAssetID_Call) Return(attestations []models.Attestation, err error) *AttestationService_GetByArtifactAndAssetVersionAndAssetID_Call { + _c.Call.Return(attestations, err) + return _c +} + +func (_c *AttestationService_GetByArtifactAndAssetVersionAndAssetID_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, artifactName string, assetVersion string, assetID uuid.UUID) ([]models.Attestation, error)) *AttestationService_GetByArtifactAndAssetVersionAndAssetID_Call { + _c.Call.Return(run) + return _c +} + +// GetByAssetID provides a mock function for the type AttestationService +func (_mock *AttestationService) GetByAssetID(ctx context.Context, tx shared.DB, assetID uuid.UUID) ([]models.Attestation, error) { + ret := _mock.Called(ctx, tx, assetID) + + if len(ret) == 0 { + panic("no return value specified for GetByAssetID") + } + + var r0 []models.Attestation + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) ([]models.Attestation, error)); ok { + return returnFunc(ctx, tx, assetID) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) []models.Attestation); ok { + r0 = returnFunc(ctx, tx, assetID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.Attestation) + } + } + 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 +} + +// AttestationService_GetByAssetID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetByAssetID' +type AttestationService_GetByAssetID_Call struct { + *mock.Call +} + +// GetByAssetID is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - assetID uuid.UUID +func (_e *AttestationService_Expecter) GetByAssetID(ctx interface{}, tx interface{}, assetID interface{}) *AttestationService_GetByAssetID_Call { + return &AttestationService_GetByAssetID_Call{Call: _e.mock.On("GetByAssetID", ctx, tx, assetID)} +} + +func (_c *AttestationService_GetByAssetID_Call) Run(run func(ctx context.Context, tx shared.DB, assetID uuid.UUID)) *AttestationService_GetByAssetID_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 *AttestationService_GetByAssetID_Call) Return(attestations []models.Attestation, err error) *AttestationService_GetByAssetID_Call { + _c.Call.Return(attestations, err) + return _c +} + +func (_c *AttestationService_GetByAssetID_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, assetID uuid.UUID) ([]models.Attestation, error)) *AttestationService_GetByAssetID_Call { + _c.Call.Return(run) + return _c +} + +// GetByAssetVersionAndAssetID provides a mock function for the type AttestationService +func (_mock *AttestationService) GetByAssetVersionAndAssetID(ctx context.Context, tx shared.DB, assetID uuid.UUID, assetVersion string) ([]models.Attestation, error) { + ret := _mock.Called(ctx, tx, assetID, assetVersion) + + if len(ret) == 0 { + panic("no return value specified for GetByAssetVersionAndAssetID") + } + + var r0 []models.Attestation + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID, string) ([]models.Attestation, error)); ok { + return returnFunc(ctx, tx, assetID, assetVersion) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID, string) []models.Attestation); ok { + r0 = returnFunc(ctx, tx, assetID, assetVersion) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.Attestation) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, shared.DB, uuid.UUID, string) error); ok { + r1 = returnFunc(ctx, tx, assetID, assetVersion) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// AttestationService_GetByAssetVersionAndAssetID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetByAssetVersionAndAssetID' +type AttestationService_GetByAssetVersionAndAssetID_Call struct { + *mock.Call +} + +// GetByAssetVersionAndAssetID is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - assetID uuid.UUID +// - assetVersion string +func (_e *AttestationService_Expecter) GetByAssetVersionAndAssetID(ctx interface{}, tx interface{}, assetID interface{}, assetVersion interface{}) *AttestationService_GetByAssetVersionAndAssetID_Call { + return &AttestationService_GetByAssetVersionAndAssetID_Call{Call: _e.mock.On("GetByAssetVersionAndAssetID", ctx, tx, assetID, assetVersion)} +} + +func (_c *AttestationService_GetByAssetVersionAndAssetID_Call) Run(run func(ctx context.Context, tx shared.DB, assetID uuid.UUID, assetVersion string)) *AttestationService_GetByAssetVersionAndAssetID_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) + } + var arg3 string + if args[3] != nil { + arg3 = args[3].(string) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *AttestationService_GetByAssetVersionAndAssetID_Call) Return(attestations []models.Attestation, err error) *AttestationService_GetByAssetVersionAndAssetID_Call { + _c.Call.Return(attestations, err) + return _c +} + +func (_c *AttestationService_GetByAssetVersionAndAssetID_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, assetID uuid.UUID, assetVersion string) ([]models.Attestation, error)) *AttestationService_GetByAssetVersionAndAssetID_Call { + _c.Call.Return(run) + return _c +} diff --git a/mocks/mock_ComplianceRiskRepository.go b/mocks/mock_ComplianceRiskRepository.go new file mode 100644 index 000000000..76c1b1627 --- /dev/null +++ b/mocks/mock_ComplianceRiskRepository.go @@ -0,0 +1,1386 @@ +// 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" +) + +// NewComplianceRiskRepository creates a new instance of ComplianceRiskRepository. 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 NewComplianceRiskRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *ComplianceRiskRepository { + mock := &ComplianceRiskRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// ComplianceRiskRepository is an autogenerated mock type for the ComplianceRiskRepository type +type ComplianceRiskRepository struct { + mock.Mock +} + +type ComplianceRiskRepository_Expecter struct { + mock *mock.Mock +} + +func (_m *ComplianceRiskRepository) EXPECT() *ComplianceRiskRepository_Expecter { + return &ComplianceRiskRepository_Expecter{mock: &_m.Mock} +} + +// Activate provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) 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 +} + +// ComplianceRiskRepository_Activate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Activate' +type ComplianceRiskRepository_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 *ComplianceRiskRepository_Expecter) Activate(ctx interface{}, tx interface{}, id interface{}) *ComplianceRiskRepository_Activate_Call { + return &ComplianceRiskRepository_Activate_Call{Call: _e.mock.On("Activate", ctx, tx, id)} +} + +func (_c *ComplianceRiskRepository_Activate_Call) Run(run func(ctx context.Context, tx shared.DB, id uuid.UUID)) *ComplianceRiskRepository_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 *ComplianceRiskRepository_Activate_Call) Return(err error) *ComplianceRiskRepository_Activate_Call { + _c.Call.Return(err) + return _c +} + +func (_c *ComplianceRiskRepository_Activate_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, id uuid.UUID) error) *ComplianceRiskRepository_Activate_Call { + _c.Call.Return(run) + return _c +} + +// All provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) All(ctx context.Context, tx shared.DB) ([]models.ComplianceRisk, error) { + ret := _mock.Called(ctx, tx) + + if len(ret) == 0 { + panic("no return value specified for All") + } + + var r0 []models.ComplianceRisk + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB) ([]models.ComplianceRisk, error)); ok { + return returnFunc(ctx, tx) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB) []models.ComplianceRisk); ok { + r0 = returnFunc(ctx, tx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.ComplianceRisk) + } + } + 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 +} + +// ComplianceRiskRepository_All_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'All' +type ComplianceRiskRepository_All_Call struct { + *mock.Call +} + +// All is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +func (_e *ComplianceRiskRepository_Expecter) All(ctx interface{}, tx interface{}) *ComplianceRiskRepository_All_Call { + return &ComplianceRiskRepository_All_Call{Call: _e.mock.On("All", ctx, tx)} +} + +func (_c *ComplianceRiskRepository_All_Call) Run(run func(ctx context.Context, tx shared.DB)) *ComplianceRiskRepository_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 *ComplianceRiskRepository_All_Call) Return(complianceRisks []models.ComplianceRisk, err error) *ComplianceRiskRepository_All_Call { + _c.Call.Return(complianceRisks, err) + return _c +} + +func (_c *ComplianceRiskRepository_All_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB) ([]models.ComplianceRisk, error)) *ComplianceRiskRepository_All_Call { + _c.Call.Return(run) + return _c +} + +// ApplyAndSave provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) ApplyAndSave(ctx context.Context, tx shared.DB, risk *models.ComplianceRisk, ev *models.VulnEvent) error { + ret := _mock.Called(ctx, tx, risk, ev) + + if len(ret) == 0 { + panic("no return value specified for ApplyAndSave") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, *models.ComplianceRisk, *models.VulnEvent) error); ok { + r0 = returnFunc(ctx, tx, risk, ev) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// ComplianceRiskRepository_ApplyAndSave_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ApplyAndSave' +type ComplianceRiskRepository_ApplyAndSave_Call struct { + *mock.Call +} + +// ApplyAndSave is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - risk *models.ComplianceRisk +// - ev *models.VulnEvent +func (_e *ComplianceRiskRepository_Expecter) ApplyAndSave(ctx interface{}, tx interface{}, risk interface{}, ev interface{}) *ComplianceRiskRepository_ApplyAndSave_Call { + return &ComplianceRiskRepository_ApplyAndSave_Call{Call: _e.mock.On("ApplyAndSave", ctx, tx, risk, ev)} +} + +func (_c *ComplianceRiskRepository_ApplyAndSave_Call) Run(run func(ctx context.Context, tx shared.DB, risk *models.ComplianceRisk, ev *models.VulnEvent)) *ComplianceRiskRepository_ApplyAndSave_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.ComplianceRisk + if args[2] != nil { + arg2 = args[2].(*models.ComplianceRisk) + } + var arg3 *models.VulnEvent + if args[3] != nil { + arg3 = args[3].(*models.VulnEvent) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *ComplianceRiskRepository_ApplyAndSave_Call) Return(err error) *ComplianceRiskRepository_ApplyAndSave_Call { + _c.Call.Return(err) + return _c +} + +func (_c *ComplianceRiskRepository_ApplyAndSave_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, risk *models.ComplianceRisk, ev *models.VulnEvent) error) *ComplianceRiskRepository_ApplyAndSave_Call { + _c.Call.Return(run) + return _c +} + +// Begin provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) 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 +} + +// ComplianceRiskRepository_Begin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Begin' +type ComplianceRiskRepository_Begin_Call struct { + *mock.Call +} + +// Begin is a helper method to define mock.On call +// - ctx context.Context +func (_e *ComplianceRiskRepository_Expecter) Begin(ctx interface{}) *ComplianceRiskRepository_Begin_Call { + return &ComplianceRiskRepository_Begin_Call{Call: _e.mock.On("Begin", ctx)} +} + +func (_c *ComplianceRiskRepository_Begin_Call) Run(run func(ctx context.Context)) *ComplianceRiskRepository_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 *ComplianceRiskRepository_Begin_Call) Return(v shared.DB) *ComplianceRiskRepository_Begin_Call { + _c.Call.Return(v) + return _c +} + +func (_c *ComplianceRiskRepository_Begin_Call) RunAndReturn(run func(ctx context.Context) shared.DB) *ComplianceRiskRepository_Begin_Call { + _c.Call.Return(run) + return _c +} + +// CleanupOrphanedRecords provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) 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 +} + +// ComplianceRiskRepository_CleanupOrphanedRecords_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CleanupOrphanedRecords' +type ComplianceRiskRepository_CleanupOrphanedRecords_Call struct { + *mock.Call +} + +// CleanupOrphanedRecords is a helper method to define mock.On call +// - ctx context.Context +func (_e *ComplianceRiskRepository_Expecter) CleanupOrphanedRecords(ctx interface{}) *ComplianceRiskRepository_CleanupOrphanedRecords_Call { + return &ComplianceRiskRepository_CleanupOrphanedRecords_Call{Call: _e.mock.On("CleanupOrphanedRecords", ctx)} +} + +func (_c *ComplianceRiskRepository_CleanupOrphanedRecords_Call) Run(run func(ctx context.Context)) *ComplianceRiskRepository_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 *ComplianceRiskRepository_CleanupOrphanedRecords_Call) Return(err error) *ComplianceRiskRepository_CleanupOrphanedRecords_Call { + _c.Call.Return(err) + return _c +} + +func (_c *ComplianceRiskRepository_CleanupOrphanedRecords_Call) RunAndReturn(run func(ctx context.Context) error) *ComplianceRiskRepository_CleanupOrphanedRecords_Call { + _c.Call.Return(run) + return _c +} + +// Create provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) Create(ctx context.Context, tx shared.DB, t *models.ComplianceRisk) 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.ComplianceRisk) error); ok { + r0 = returnFunc(ctx, tx, t) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// ComplianceRiskRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type ComplianceRiskRepository_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - t *models.ComplianceRisk +func (_e *ComplianceRiskRepository_Expecter) Create(ctx interface{}, tx interface{}, t interface{}) *ComplianceRiskRepository_Create_Call { + return &ComplianceRiskRepository_Create_Call{Call: _e.mock.On("Create", ctx, tx, t)} +} + +func (_c *ComplianceRiskRepository_Create_Call) Run(run func(ctx context.Context, tx shared.DB, t *models.ComplianceRisk)) *ComplianceRiskRepository_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.ComplianceRisk + if args[2] != nil { + arg2 = args[2].(*models.ComplianceRisk) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *ComplianceRiskRepository_Create_Call) Return(err error) *ComplianceRiskRepository_Create_Call { + _c.Call.Return(err) + return _c +} + +func (_c *ComplianceRiskRepository_Create_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, t *models.ComplianceRisk) error) *ComplianceRiskRepository_Create_Call { + _c.Call.Return(run) + return _c +} + +// CreateBatch provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) CreateBatch(ctx context.Context, tx shared.DB, ts []models.ComplianceRisk) 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.ComplianceRisk) error); ok { + r0 = returnFunc(ctx, tx, ts) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// ComplianceRiskRepository_CreateBatch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateBatch' +type ComplianceRiskRepository_CreateBatch_Call struct { + *mock.Call +} + +// CreateBatch is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - ts []models.ComplianceRisk +func (_e *ComplianceRiskRepository_Expecter) CreateBatch(ctx interface{}, tx interface{}, ts interface{}) *ComplianceRiskRepository_CreateBatch_Call { + return &ComplianceRiskRepository_CreateBatch_Call{Call: _e.mock.On("CreateBatch", ctx, tx, ts)} +} + +func (_c *ComplianceRiskRepository_CreateBatch_Call) Run(run func(ctx context.Context, tx shared.DB, ts []models.ComplianceRisk)) *ComplianceRiskRepository_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.ComplianceRisk + if args[2] != nil { + arg2 = args[2].([]models.ComplianceRisk) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *ComplianceRiskRepository_CreateBatch_Call) Return(err error) *ComplianceRiskRepository_CreateBatch_Call { + _c.Call.Return(err) + return _c +} + +func (_c *ComplianceRiskRepository_CreateBatch_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, ts []models.ComplianceRisk) error) *ComplianceRiskRepository_CreateBatch_Call { + _c.Call.Return(run) + return _c +} + +// Delete provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) 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 +} + +// ComplianceRiskRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type ComplianceRiskRepository_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 *ComplianceRiskRepository_Expecter) Delete(ctx interface{}, tx interface{}, id interface{}) *ComplianceRiskRepository_Delete_Call { + return &ComplianceRiskRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, tx, id)} +} + +func (_c *ComplianceRiskRepository_Delete_Call) Run(run func(ctx context.Context, tx shared.DB, id uuid.UUID)) *ComplianceRiskRepository_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 *ComplianceRiskRepository_Delete_Call) Return(err error) *ComplianceRiskRepository_Delete_Call { + _c.Call.Return(err) + return _c +} + +func (_c *ComplianceRiskRepository_Delete_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, id uuid.UUID) error) *ComplianceRiskRepository_Delete_Call { + _c.Call.Return(run) + return _c +} + +// DeleteBatch provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) DeleteBatch(ctx context.Context, tx shared.DB, ids []models.ComplianceRisk) 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.ComplianceRisk) error); ok { + r0 = returnFunc(ctx, tx, ids) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// ComplianceRiskRepository_DeleteBatch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteBatch' +type ComplianceRiskRepository_DeleteBatch_Call struct { + *mock.Call +} + +// DeleteBatch is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - ids []models.ComplianceRisk +func (_e *ComplianceRiskRepository_Expecter) DeleteBatch(ctx interface{}, tx interface{}, ids interface{}) *ComplianceRiskRepository_DeleteBatch_Call { + return &ComplianceRiskRepository_DeleteBatch_Call{Call: _e.mock.On("DeleteBatch", ctx, tx, ids)} +} + +func (_c *ComplianceRiskRepository_DeleteBatch_Call) Run(run func(ctx context.Context, tx shared.DB, ids []models.ComplianceRisk)) *ComplianceRiskRepository_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.ComplianceRisk + if args[2] != nil { + arg2 = args[2].([]models.ComplianceRisk) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *ComplianceRiskRepository_DeleteBatch_Call) Return(err error) *ComplianceRiskRepository_DeleteBatch_Call { + _c.Call.Return(err) + return _c +} + +func (_c *ComplianceRiskRepository_DeleteBatch_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, ids []models.ComplianceRisk) error) *ComplianceRiskRepository_DeleteBatch_Call { + _c.Call.Return(run) + return _c +} + +// GetAllComplianceRisksForAssetVersion provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) GetAllComplianceRisksForAssetVersion(ctx context.Context, tx shared.DB, assetID uuid.UUID, assetVersionName string) ([]models.ComplianceRisk, error) { + ret := _mock.Called(ctx, tx, assetID, assetVersionName) + + if len(ret) == 0 { + panic("no return value specified for GetAllComplianceRisksForAssetVersion") + } + + var r0 []models.ComplianceRisk + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID, string) ([]models.ComplianceRisk, error)); ok { + return returnFunc(ctx, tx, assetID, assetVersionName) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID, string) []models.ComplianceRisk); ok { + r0 = returnFunc(ctx, tx, assetID, assetVersionName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.ComplianceRisk) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, shared.DB, uuid.UUID, string) error); ok { + r1 = returnFunc(ctx, tx, assetID, assetVersionName) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// ComplianceRiskRepository_GetAllComplianceRisksForAssetVersion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllComplianceRisksForAssetVersion' +type ComplianceRiskRepository_GetAllComplianceRisksForAssetVersion_Call struct { + *mock.Call +} + +// GetAllComplianceRisksForAssetVersion is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - assetID uuid.UUID +// - assetVersionName string +func (_e *ComplianceRiskRepository_Expecter) GetAllComplianceRisksForAssetVersion(ctx interface{}, tx interface{}, assetID interface{}, assetVersionName interface{}) *ComplianceRiskRepository_GetAllComplianceRisksForAssetVersion_Call { + return &ComplianceRiskRepository_GetAllComplianceRisksForAssetVersion_Call{Call: _e.mock.On("GetAllComplianceRisksForAssetVersion", ctx, tx, assetID, assetVersionName)} +} + +func (_c *ComplianceRiskRepository_GetAllComplianceRisksForAssetVersion_Call) Run(run func(ctx context.Context, tx shared.DB, assetID uuid.UUID, assetVersionName string)) *ComplianceRiskRepository_GetAllComplianceRisksForAssetVersion_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) + } + var arg3 string + if args[3] != nil { + arg3 = args[3].(string) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *ComplianceRiskRepository_GetAllComplianceRisksForAssetVersion_Call) Return(complianceRisks []models.ComplianceRisk, err error) *ComplianceRiskRepository_GetAllComplianceRisksForAssetVersion_Call { + _c.Call.Return(complianceRisks, err) + return _c +} + +func (_c *ComplianceRiskRepository_GetAllComplianceRisksForAssetVersion_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, assetID uuid.UUID, assetVersionName string) ([]models.ComplianceRisk, error)) *ComplianceRiskRepository_GetAllComplianceRisksForAssetVersion_Call { + _c.Call.Return(run) + return _c +} + +// GetAllComplianceRisksForAssetVersionPaged provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) GetAllComplianceRisksForAssetVersionPaged(ctx context.Context, tx shared.DB, assetID uuid.UUID, assetVersionName string, pageInfo shared.PageInfo, search string, filter []shared.FilterQuery, sort []shared.SortQuery) (shared.Paged[models.ComplianceRisk], error) { + ret := _mock.Called(ctx, tx, assetID, assetVersionName, pageInfo, search, filter, sort) + + if len(ret) == 0 { + panic("no return value specified for GetAllComplianceRisksForAssetVersionPaged") + } + + var r0 shared.Paged[models.ComplianceRisk] + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID, string, shared.PageInfo, string, []shared.FilterQuery, []shared.SortQuery) (shared.Paged[models.ComplianceRisk], error)); ok { + return returnFunc(ctx, tx, assetID, assetVersionName, pageInfo, search, filter, sort) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID, string, shared.PageInfo, string, []shared.FilterQuery, []shared.SortQuery) shared.Paged[models.ComplianceRisk]); ok { + r0 = returnFunc(ctx, tx, assetID, assetVersionName, pageInfo, search, filter, sort) + } else { + r0 = ret.Get(0).(shared.Paged[models.ComplianceRisk]) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, shared.DB, uuid.UUID, string, shared.PageInfo, string, []shared.FilterQuery, []shared.SortQuery) error); ok { + r1 = returnFunc(ctx, tx, assetID, assetVersionName, pageInfo, search, filter, sort) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// ComplianceRiskRepository_GetAllComplianceRisksForAssetVersionPaged_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllComplianceRisksForAssetVersionPaged' +type ComplianceRiskRepository_GetAllComplianceRisksForAssetVersionPaged_Call struct { + *mock.Call +} + +// GetAllComplianceRisksForAssetVersionPaged is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - assetID uuid.UUID +// - assetVersionName string +// - pageInfo shared.PageInfo +// - search string +// - filter []shared.FilterQuery +// - sort []shared.SortQuery +func (_e *ComplianceRiskRepository_Expecter) GetAllComplianceRisksForAssetVersionPaged(ctx interface{}, tx interface{}, assetID interface{}, assetVersionName interface{}, pageInfo interface{}, search interface{}, filter interface{}, sort interface{}) *ComplianceRiskRepository_GetAllComplianceRisksForAssetVersionPaged_Call { + return &ComplianceRiskRepository_GetAllComplianceRisksForAssetVersionPaged_Call{Call: _e.mock.On("GetAllComplianceRisksForAssetVersionPaged", ctx, tx, assetID, assetVersionName, pageInfo, search, filter, sort)} +} + +func (_c *ComplianceRiskRepository_GetAllComplianceRisksForAssetVersionPaged_Call) Run(run func(ctx context.Context, tx shared.DB, assetID uuid.UUID, assetVersionName string, pageInfo shared.PageInfo, search string, filter []shared.FilterQuery, sort []shared.SortQuery)) *ComplianceRiskRepository_GetAllComplianceRisksForAssetVersionPaged_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) + } + var arg3 string + if args[3] != nil { + arg3 = args[3].(string) + } + var arg4 shared.PageInfo + if args[4] != nil { + arg4 = args[4].(shared.PageInfo) + } + var arg5 string + if args[5] != nil { + arg5 = args[5].(string) + } + var arg6 []shared.FilterQuery + if args[6] != nil { + arg6 = args[6].([]shared.FilterQuery) + } + var arg7 []shared.SortQuery + if args[7] != nil { + arg7 = args[7].([]shared.SortQuery) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + arg5, + arg6, + arg7, + ) + }) + return _c +} + +func (_c *ComplianceRiskRepository_GetAllComplianceRisksForAssetVersionPaged_Call) Return(paged shared.Paged[models.ComplianceRisk], err error) *ComplianceRiskRepository_GetAllComplianceRisksForAssetVersionPaged_Call { + _c.Call.Return(paged, err) + return _c +} + +func (_c *ComplianceRiskRepository_GetAllComplianceRisksForAssetVersionPaged_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, assetID uuid.UUID, assetVersionName string, pageInfo shared.PageInfo, search string, filter []shared.FilterQuery, sort []shared.SortQuery) (shared.Paged[models.ComplianceRisk], error)) *ComplianceRiskRepository_GetAllComplianceRisksForAssetVersionPaged_Call { + _c.Call.Return(run) + return _c +} + +// GetComplianceRisksByOtherAssetVersions provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) GetComplianceRisksByOtherAssetVersions(ctx context.Context, tx shared.DB, assetVersionName string, assetID uuid.UUID) ([]models.ComplianceRisk, error) { + ret := _mock.Called(ctx, tx, assetVersionName, assetID) + + if len(ret) == 0 { + panic("no return value specified for GetComplianceRisksByOtherAssetVersions") + } + + var r0 []models.ComplianceRisk + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, string, uuid.UUID) ([]models.ComplianceRisk, error)); ok { + return returnFunc(ctx, tx, assetVersionName, assetID) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, string, uuid.UUID) []models.ComplianceRisk); ok { + r0 = returnFunc(ctx, tx, assetVersionName, assetID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.ComplianceRisk) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, shared.DB, string, uuid.UUID) error); ok { + r1 = returnFunc(ctx, tx, assetVersionName, assetID) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// ComplianceRiskRepository_GetComplianceRisksByOtherAssetVersions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetComplianceRisksByOtherAssetVersions' +type ComplianceRiskRepository_GetComplianceRisksByOtherAssetVersions_Call struct { + *mock.Call +} + +// GetComplianceRisksByOtherAssetVersions is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - assetVersionName string +// - assetID uuid.UUID +func (_e *ComplianceRiskRepository_Expecter) GetComplianceRisksByOtherAssetVersions(ctx interface{}, tx interface{}, assetVersionName interface{}, assetID interface{}) *ComplianceRiskRepository_GetComplianceRisksByOtherAssetVersions_Call { + return &ComplianceRiskRepository_GetComplianceRisksByOtherAssetVersions_Call{Call: _e.mock.On("GetComplianceRisksByOtherAssetVersions", ctx, tx, assetVersionName, assetID)} +} + +func (_c *ComplianceRiskRepository_GetComplianceRisksByOtherAssetVersions_Call) Run(run func(ctx context.Context, tx shared.DB, assetVersionName string, assetID uuid.UUID)) *ComplianceRiskRepository_GetComplianceRisksByOtherAssetVersions_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 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 uuid.UUID + if args[3] != nil { + arg3 = args[3].(uuid.UUID) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *ComplianceRiskRepository_GetComplianceRisksByOtherAssetVersions_Call) Return(complianceRisks []models.ComplianceRisk, err error) *ComplianceRiskRepository_GetComplianceRisksByOtherAssetVersions_Call { + _c.Call.Return(complianceRisks, err) + return _c +} + +func (_c *ComplianceRiskRepository_GetComplianceRisksByOtherAssetVersions_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, assetVersionName string, assetID uuid.UUID) ([]models.ComplianceRisk, error)) *ComplianceRiskRepository_GetComplianceRisksByOtherAssetVersions_Call { + _c.Call.Return(run) + return _c +} + +// GetDB provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) 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 +} + +// ComplianceRiskRepository_GetDB_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDB' +type ComplianceRiskRepository_GetDB_Call struct { + *mock.Call +} + +// GetDB is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +func (_e *ComplianceRiskRepository_Expecter) GetDB(ctx interface{}, tx interface{}) *ComplianceRiskRepository_GetDB_Call { + return &ComplianceRiskRepository_GetDB_Call{Call: _e.mock.On("GetDB", ctx, tx)} +} + +func (_c *ComplianceRiskRepository_GetDB_Call) Run(run func(ctx context.Context, tx shared.DB)) *ComplianceRiskRepository_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 *ComplianceRiskRepository_GetDB_Call) Return(v shared.DB) *ComplianceRiskRepository_GetDB_Call { + _c.Call.Return(v) + return _c +} + +func (_c *ComplianceRiskRepository_GetDB_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB) shared.DB) *ComplianceRiskRepository_GetDB_Call { + _c.Call.Return(run) + return _c +} + +// List provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) List(ctx context.Context, tx shared.DB, ids []uuid.UUID) ([]models.ComplianceRisk, error) { + ret := _mock.Called(ctx, tx, ids) + + if len(ret) == 0 { + panic("no return value specified for List") + } + + var r0 []models.ComplianceRisk + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, []uuid.UUID) ([]models.ComplianceRisk, error)); ok { + return returnFunc(ctx, tx, ids) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, []uuid.UUID) []models.ComplianceRisk); ok { + r0 = returnFunc(ctx, tx, ids) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.ComplianceRisk) + } + } + 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 +} + +// ComplianceRiskRepository_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List' +type ComplianceRiskRepository_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 *ComplianceRiskRepository_Expecter) List(ctx interface{}, tx interface{}, ids interface{}) *ComplianceRiskRepository_List_Call { + return &ComplianceRiskRepository_List_Call{Call: _e.mock.On("List", ctx, tx, ids)} +} + +func (_c *ComplianceRiskRepository_List_Call) Run(run func(ctx context.Context, tx shared.DB, ids []uuid.UUID)) *ComplianceRiskRepository_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 *ComplianceRiskRepository_List_Call) Return(complianceRisks []models.ComplianceRisk, err error) *ComplianceRiskRepository_List_Call { + _c.Call.Return(complianceRisks, err) + return _c +} + +func (_c *ComplianceRiskRepository_List_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, ids []uuid.UUID) ([]models.ComplianceRisk, error)) *ComplianceRiskRepository_List_Call { + _c.Call.Return(run) + return _c +} + +// Read provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) Read(ctx context.Context, tx shared.DB, id uuid.UUID) (models.ComplianceRisk, error) { + ret := _mock.Called(ctx, tx, id) + + if len(ret) == 0 { + panic("no return value specified for Read") + } + + var r0 models.ComplianceRisk + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) (models.ComplianceRisk, error)); ok { + return returnFunc(ctx, tx, id) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) models.ComplianceRisk); ok { + r0 = returnFunc(ctx, tx, id) + } else { + r0 = ret.Get(0).(models.ComplianceRisk) + } + 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 +} + +// ComplianceRiskRepository_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' +type ComplianceRiskRepository_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 *ComplianceRiskRepository_Expecter) Read(ctx interface{}, tx interface{}, id interface{}) *ComplianceRiskRepository_Read_Call { + return &ComplianceRiskRepository_Read_Call{Call: _e.mock.On("Read", ctx, tx, id)} +} + +func (_c *ComplianceRiskRepository_Read_Call) Run(run func(ctx context.Context, tx shared.DB, id uuid.UUID)) *ComplianceRiskRepository_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 *ComplianceRiskRepository_Read_Call) Return(complianceRisk models.ComplianceRisk, err error) *ComplianceRiskRepository_Read_Call { + _c.Call.Return(complianceRisk, err) + return _c +} + +func (_c *ComplianceRiskRepository_Read_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, id uuid.UUID) (models.ComplianceRisk, error)) *ComplianceRiskRepository_Read_Call { + _c.Call.Return(run) + return _c +} + +// Save provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) Save(ctx context.Context, tx shared.DB, t *models.ComplianceRisk) 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.ComplianceRisk) error); ok { + r0 = returnFunc(ctx, tx, t) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// ComplianceRiskRepository_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save' +type ComplianceRiskRepository_Save_Call struct { + *mock.Call +} + +// Save is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - t *models.ComplianceRisk +func (_e *ComplianceRiskRepository_Expecter) Save(ctx interface{}, tx interface{}, t interface{}) *ComplianceRiskRepository_Save_Call { + return &ComplianceRiskRepository_Save_Call{Call: _e.mock.On("Save", ctx, tx, t)} +} + +func (_c *ComplianceRiskRepository_Save_Call) Run(run func(ctx context.Context, tx shared.DB, t *models.ComplianceRisk)) *ComplianceRiskRepository_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.ComplianceRisk + if args[2] != nil { + arg2 = args[2].(*models.ComplianceRisk) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *ComplianceRiskRepository_Save_Call) Return(err error) *ComplianceRiskRepository_Save_Call { + _c.Call.Return(err) + return _c +} + +func (_c *ComplianceRiskRepository_Save_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, t *models.ComplianceRisk) error) *ComplianceRiskRepository_Save_Call { + _c.Call.Return(run) + return _c +} + +// SaveBatch provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) SaveBatch(ctx context.Context, tx shared.DB, risks []models.ComplianceRisk) error { + ret := _mock.Called(ctx, tx, risks) + + 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.ComplianceRisk) error); ok { + r0 = returnFunc(ctx, tx, risks) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// ComplianceRiskRepository_SaveBatch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveBatch' +type ComplianceRiskRepository_SaveBatch_Call struct { + *mock.Call +} + +// SaveBatch is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - risks []models.ComplianceRisk +func (_e *ComplianceRiskRepository_Expecter) SaveBatch(ctx interface{}, tx interface{}, risks interface{}) *ComplianceRiskRepository_SaveBatch_Call { + return &ComplianceRiskRepository_SaveBatch_Call{Call: _e.mock.On("SaveBatch", ctx, tx, risks)} +} + +func (_c *ComplianceRiskRepository_SaveBatch_Call) Run(run func(ctx context.Context, tx shared.DB, risks []models.ComplianceRisk)) *ComplianceRiskRepository_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.ComplianceRisk + if args[2] != nil { + arg2 = args[2].([]models.ComplianceRisk) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *ComplianceRiskRepository_SaveBatch_Call) Return(err error) *ComplianceRiskRepository_SaveBatch_Call { + _c.Call.Return(err) + return _c +} + +func (_c *ComplianceRiskRepository_SaveBatch_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, risks []models.ComplianceRisk) error) *ComplianceRiskRepository_SaveBatch_Call { + _c.Call.Return(run) + return _c +} + +// SaveBatchBestEffort provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) SaveBatchBestEffort(ctx context.Context, tx shared.DB, ts []models.ComplianceRisk) 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.ComplianceRisk) error); ok { + r0 = returnFunc(ctx, tx, ts) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// ComplianceRiskRepository_SaveBatchBestEffort_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveBatchBestEffort' +type ComplianceRiskRepository_SaveBatchBestEffort_Call struct { + *mock.Call +} + +// SaveBatchBestEffort is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - ts []models.ComplianceRisk +func (_e *ComplianceRiskRepository_Expecter) SaveBatchBestEffort(ctx interface{}, tx interface{}, ts interface{}) *ComplianceRiskRepository_SaveBatchBestEffort_Call { + return &ComplianceRiskRepository_SaveBatchBestEffort_Call{Call: _e.mock.On("SaveBatchBestEffort", ctx, tx, ts)} +} + +func (_c *ComplianceRiskRepository_SaveBatchBestEffort_Call) Run(run func(ctx context.Context, tx shared.DB, ts []models.ComplianceRisk)) *ComplianceRiskRepository_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.ComplianceRisk + if args[2] != nil { + arg2 = args[2].([]models.ComplianceRisk) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *ComplianceRiskRepository_SaveBatchBestEffort_Call) Return(err error) *ComplianceRiskRepository_SaveBatchBestEffort_Call { + _c.Call.Return(err) + return _c +} + +func (_c *ComplianceRiskRepository_SaveBatchBestEffort_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, ts []models.ComplianceRisk) error) *ComplianceRiskRepository_SaveBatchBestEffort_Call { + _c.Call.Return(run) + return _c +} + +// Transaction provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) 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 +} + +// ComplianceRiskRepository_Transaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Transaction' +type ComplianceRiskRepository_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 *ComplianceRiskRepository_Expecter) Transaction(ctx interface{}, fn interface{}) *ComplianceRiskRepository_Transaction_Call { + return &ComplianceRiskRepository_Transaction_Call{Call: _e.mock.On("Transaction", ctx, fn)} +} + +func (_c *ComplianceRiskRepository_Transaction_Call) Run(run func(ctx context.Context, fn func(tx shared.DB) error)) *ComplianceRiskRepository_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 *ComplianceRiskRepository_Transaction_Call) Return(err error) *ComplianceRiskRepository_Transaction_Call { + _c.Call.Return(err) + return _c +} + +func (_c *ComplianceRiskRepository_Transaction_Call) RunAndReturn(run func(ctx context.Context, fn func(tx shared.DB) error) error) *ComplianceRiskRepository_Transaction_Call { + _c.Call.Return(run) + return _c +} + +// Upsert provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) Upsert(ctx context.Context, tx shared.DB, t *[]*models.ComplianceRisk, 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.ComplianceRisk, []clause.Column, []string) error); ok { + r0 = returnFunc(ctx, tx, t, conflictingColumns, updateOnly) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// ComplianceRiskRepository_Upsert_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Upsert' +type ComplianceRiskRepository_Upsert_Call struct { + *mock.Call +} + +// Upsert is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - t *[]*models.ComplianceRisk +// - conflictingColumns []clause.Column +// - updateOnly []string +func (_e *ComplianceRiskRepository_Expecter) Upsert(ctx interface{}, tx interface{}, t interface{}, conflictingColumns interface{}, updateOnly interface{}) *ComplianceRiskRepository_Upsert_Call { + return &ComplianceRiskRepository_Upsert_Call{Call: _e.mock.On("Upsert", ctx, tx, t, conflictingColumns, updateOnly)} +} + +func (_c *ComplianceRiskRepository_Upsert_Call) Run(run func(ctx context.Context, tx shared.DB, t *[]*models.ComplianceRisk, conflictingColumns []clause.Column, updateOnly []string)) *ComplianceRiskRepository_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.ComplianceRisk + if args[2] != nil { + arg2 = args[2].(*[]*models.ComplianceRisk) + } + 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 *ComplianceRiskRepository_Upsert_Call) Return(err error) *ComplianceRiskRepository_Upsert_Call { + _c.Call.Return(err) + return _c +} + +func (_c *ComplianceRiskRepository_Upsert_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, t *[]*models.ComplianceRisk, conflictingColumns []clause.Column, updateOnly []string) error) *ComplianceRiskRepository_Upsert_Call { + _c.Call.Return(run) + return _c +} diff --git a/mocks/mock_ComplianceRiskService.go b/mocks/mock_ComplianceRiskService.go new file mode 100644 index 000000000..d555a3611 --- /dev/null +++ b/mocks/mock_ComplianceRiskService.go @@ -0,0 +1,230 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "context" + + "github.com/l3montree-dev/devguard/database/models" + "github.com/l3montree-dev/devguard/dtos" + "github.com/l3montree-dev/devguard/shared" + mock "github.com/stretchr/testify/mock" +) + +// NewComplianceRiskService creates a new instance of ComplianceRiskService. 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 NewComplianceRiskService(t interface { + mock.TestingT + Cleanup(func()) +}) *ComplianceRiskService { + mock := &ComplianceRiskService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// ComplianceRiskService is an autogenerated mock type for the ComplianceRiskService type +type ComplianceRiskService struct { + mock.Mock +} + +type ComplianceRiskService_Expecter struct { + mock *mock.Mock +} + +func (_m *ComplianceRiskService) EXPECT() *ComplianceRiskService_Expecter { + return &ComplianceRiskService_Expecter{mock: &_m.Mock} +} + +// HandleArtifactCompliance provides a mock function for the type ComplianceRiskService +func (_mock *ComplianceRiskService) HandleArtifactCompliance(ctx context.Context, tx shared.DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, evaluations []dtos.PolicyEvaluationDTO) error { + ret := _mock.Called(ctx, tx, userID, userAgent, assetVersion, artifact, evaluations) + + if len(ret) == 0 { + panic("no return value specified for HandleArtifactCompliance") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, string, *string, models.AssetVersion, models.Artifact, []dtos.PolicyEvaluationDTO) error); ok { + r0 = returnFunc(ctx, tx, userID, userAgent, assetVersion, artifact, evaluations) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// ComplianceRiskService_HandleArtifactCompliance_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HandleArtifactCompliance' +type ComplianceRiskService_HandleArtifactCompliance_Call struct { + *mock.Call +} + +// HandleArtifactCompliance is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - userID string +// - userAgent *string +// - assetVersion models.AssetVersion +// - artifact models.Artifact +// - evaluations []dtos.PolicyEvaluationDTO +func (_e *ComplianceRiskService_Expecter) HandleArtifactCompliance(ctx interface{}, tx interface{}, userID interface{}, userAgent interface{}, assetVersion interface{}, artifact interface{}, evaluations interface{}) *ComplianceRiskService_HandleArtifactCompliance_Call { + return &ComplianceRiskService_HandleArtifactCompliance_Call{Call: _e.mock.On("HandleArtifactCompliance", ctx, tx, userID, userAgent, assetVersion, artifact, evaluations)} +} + +func (_c *ComplianceRiskService_HandleArtifactCompliance_Call) Run(run func(ctx context.Context, tx shared.DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, evaluations []dtos.PolicyEvaluationDTO)) *ComplianceRiskService_HandleArtifactCompliance_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 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 *string + if args[3] != nil { + arg3 = args[3].(*string) + } + var arg4 models.AssetVersion + if args[4] != nil { + arg4 = args[4].(models.AssetVersion) + } + var arg5 models.Artifact + if args[5] != nil { + arg5 = args[5].(models.Artifact) + } + var arg6 []dtos.PolicyEvaluationDTO + if args[6] != nil { + arg6 = args[6].([]dtos.PolicyEvaluationDTO) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + arg5, + arg6, + ) + }) + return _c +} + +func (_c *ComplianceRiskService_HandleArtifactCompliance_Call) Return(err error) *ComplianceRiskService_HandleArtifactCompliance_Call { + _c.Call.Return(err) + return _c +} + +func (_c *ComplianceRiskService_HandleArtifactCompliance_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, evaluations []dtos.PolicyEvaluationDTO) error) *ComplianceRiskService_HandleArtifactCompliance_Call { + _c.Call.Return(run) + return _c +} + +// UpdateComplianceRiskState provides a mock function for the type ComplianceRiskService +func (_mock *ComplianceRiskService) UpdateComplianceRiskState(ctx context.Context, tx shared.DB, userID string, risk *models.ComplianceRisk, statusType string, justification string, mechanicalJustification dtos.MechanicalJustificationType, userAgent *string) (models.VulnEvent, error) { + ret := _mock.Called(ctx, tx, userID, risk, statusType, justification, mechanicalJustification, userAgent) + + if len(ret) == 0 { + panic("no return value specified for UpdateComplianceRiskState") + } + + var r0 models.VulnEvent + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, string, *models.ComplianceRisk, string, string, dtos.MechanicalJustificationType, *string) (models.VulnEvent, error)); ok { + return returnFunc(ctx, tx, userID, risk, statusType, justification, mechanicalJustification, userAgent) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, string, *models.ComplianceRisk, string, string, dtos.MechanicalJustificationType, *string) models.VulnEvent); ok { + r0 = returnFunc(ctx, tx, userID, risk, statusType, justification, mechanicalJustification, userAgent) + } else { + r0 = ret.Get(0).(models.VulnEvent) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, shared.DB, string, *models.ComplianceRisk, string, string, dtos.MechanicalJustificationType, *string) error); ok { + r1 = returnFunc(ctx, tx, userID, risk, statusType, justification, mechanicalJustification, userAgent) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// ComplianceRiskService_UpdateComplianceRiskState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateComplianceRiskState' +type ComplianceRiskService_UpdateComplianceRiskState_Call struct { + *mock.Call +} + +// UpdateComplianceRiskState is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - userID string +// - risk *models.ComplianceRisk +// - statusType string +// - justification string +// - mechanicalJustification dtos.MechanicalJustificationType +// - userAgent *string +func (_e *ComplianceRiskService_Expecter) UpdateComplianceRiskState(ctx interface{}, tx interface{}, userID interface{}, risk interface{}, statusType interface{}, justification interface{}, mechanicalJustification interface{}, userAgent interface{}) *ComplianceRiskService_UpdateComplianceRiskState_Call { + return &ComplianceRiskService_UpdateComplianceRiskState_Call{Call: _e.mock.On("UpdateComplianceRiskState", ctx, tx, userID, risk, statusType, justification, mechanicalJustification, userAgent)} +} + +func (_c *ComplianceRiskService_UpdateComplianceRiskState_Call) Run(run func(ctx context.Context, tx shared.DB, userID string, risk *models.ComplianceRisk, statusType string, justification string, mechanicalJustification dtos.MechanicalJustificationType, userAgent *string)) *ComplianceRiskService_UpdateComplianceRiskState_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 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 *models.ComplianceRisk + if args[3] != nil { + arg3 = args[3].(*models.ComplianceRisk) + } + var arg4 string + if args[4] != nil { + arg4 = args[4].(string) + } + var arg5 string + if args[5] != nil { + arg5 = args[5].(string) + } + var arg6 dtos.MechanicalJustificationType + if args[6] != nil { + arg6 = args[6].(dtos.MechanicalJustificationType) + } + var arg7 *string + if args[7] != nil { + arg7 = args[7].(*string) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + arg5, + arg6, + arg7, + ) + }) + return _c +} + +func (_c *ComplianceRiskService_UpdateComplianceRiskState_Call) Return(vulnEvent models.VulnEvent, err error) *ComplianceRiskService_UpdateComplianceRiskState_Call { + _c.Call.Return(vulnEvent, err) + return _c +} + +func (_c *ComplianceRiskService_UpdateComplianceRiskState_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, userID string, risk *models.ComplianceRisk, statusType string, justification string, mechanicalJustification dtos.MechanicalJustificationType, userAgent *string) (models.VulnEvent, error)) *ComplianceRiskService_UpdateComplianceRiskState_Call { + _c.Call.Return(run) + return _c +} diff --git a/mocks/mock_ComplianceService.go b/mocks/mock_ComplianceService.go new file mode 100644 index 000000000..49f7649e7 --- /dev/null +++ b/mocks/mock_ComplianceService.go @@ -0,0 +1,121 @@ +// 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/dtos" + mock "github.com/stretchr/testify/mock" +) + +// NewComplianceService creates a new instance of ComplianceService. 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 NewComplianceService(t interface { + mock.TestingT + Cleanup(func()) +}) *ComplianceService { + mock := &ComplianceService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// ComplianceService is an autogenerated mock type for the ComplianceService type +type ComplianceService struct { + mock.Mock +} + +type ComplianceService_Expecter struct { + mock *mock.Mock +} + +func (_m *ComplianceService) EXPECT() *ComplianceService_Expecter { + return &ComplianceService_Expecter{mock: &_m.Mock} +} + +// ArtifactCompliance provides a mock function for the type ComplianceService +func (_mock *ComplianceService) ArtifactCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) ([]dtos.PolicyEvaluationDTO, error) { + ret := _mock.Called(ctx, projectID, assetVersion, artifact) + + if len(ret) == 0 { + panic("no return value specified for ArtifactCompliance") + } + + var r0 []dtos.PolicyEvaluationDTO + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, models.AssetVersion, models.Artifact) ([]dtos.PolicyEvaluationDTO, error)); ok { + return returnFunc(ctx, projectID, assetVersion, artifact) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, models.AssetVersion, models.Artifact) []dtos.PolicyEvaluationDTO); ok { + r0 = returnFunc(ctx, projectID, assetVersion, artifact) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]dtos.PolicyEvaluationDTO) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, uuid.UUID, models.AssetVersion, models.Artifact) error); ok { + r1 = returnFunc(ctx, projectID, assetVersion, artifact) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// ComplianceService_ArtifactCompliance_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ArtifactCompliance' +type ComplianceService_ArtifactCompliance_Call struct { + *mock.Call +} + +// ArtifactCompliance is a helper method to define mock.On call +// - ctx context.Context +// - projectID uuid.UUID +// - assetVersion models.AssetVersion +// - artifact models.Artifact +func (_e *ComplianceService_Expecter) ArtifactCompliance(ctx interface{}, projectID interface{}, assetVersion interface{}, artifact interface{}) *ComplianceService_ArtifactCompliance_Call { + return &ComplianceService_ArtifactCompliance_Call{Call: _e.mock.On("ArtifactCompliance", ctx, projectID, assetVersion, artifact)} +} + +func (_c *ComplianceService_ArtifactCompliance_Call) Run(run func(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact)) *ComplianceService_ArtifactCompliance_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) + } + var arg2 models.AssetVersion + if args[2] != nil { + arg2 = args[2].(models.AssetVersion) + } + var arg3 models.Artifact + if args[3] != nil { + arg3 = args[3].(models.Artifact) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *ComplianceService_ArtifactCompliance_Call) Return(policyEvaluationDTOs []dtos.PolicyEvaluationDTO, err error) *ComplianceService_ArtifactCompliance_Call { + _c.Call.Return(policyEvaluationDTOs, err) + return _c +} + +func (_c *ComplianceService_ArtifactCompliance_Call) RunAndReturn(run func(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) ([]dtos.PolicyEvaluationDTO, error)) *ComplianceService_ArtifactCompliance_Call { + _c.Call.Return(run) + return _c +} From 9094f16a82b2eab4a547bd13503ae55443d47440 Mon Sep 17 00:00:00 2001 From: rafi Date: Tue, 9 Jun 2026 09:45:37 +0200 Subject: [PATCH 12/26] remove standalone policy layer, consolidate into compliance risk Signed-off-by: rafi --- cmd/devguard-scanner/commands/attestations.go | 3 +- cmd/devguard-scanner/scanner/eval_policy.go | 175 ++---------- .../scanner/eval_policy_test.go | 128 --------- compliance/rego.go | 225 +++++++++++---- compliance/rego_test.go | 134 +++++++-- controllers/compliance_controller.go | 132 --------- controllers/compliance_risk_controller.go | 29 +- controllers/policy_controller.go | 265 ------------------ controllers/providers.go | 2 - daemons/attestation_daemon.go | 4 +- database/models/compliance_risk_model.go | 19 +- database/models/policy_model.go | 21 -- database/models/project_model.go | 2 - database/repositories/policy_repository.go | 51 ---- database/repositories/project_repository.go | 15 - database/repositories/providers.go | 1 - dtos/compliance_risk_dto.go | 16 +- dtos/policy_dto.go | 39 --- router/asset_router.go | 3 - router/asset_version_router.go | 4 +- router/compliance_risk_router.go | 2 +- router/org_router.go | 7 - router/project_router.go | 4 - router/router_test.go | 23 +- services/compliance_risk_service.go | 166 +++++++++-- services/compliance_service.go | 19 +- shared/common_interfaces.go | 13 +- tests/project_controller_test.go | 10 +- transformer/compliance_risk_transformer.go | 34 ++- transformer/policy_transformer.go | 42 --- 30 files changed, 520 insertions(+), 1068 deletions(-) delete mode 100644 cmd/devguard-scanner/scanner/eval_policy_test.go delete mode 100644 controllers/compliance_controller.go delete mode 100644 controllers/policy_controller.go delete mode 100644 database/models/policy_model.go delete mode 100644 database/repositories/policy_repository.go delete mode 100644 dtos/policy_dto.go delete mode 100644 transformer/policy_transformer.go diff --git a/cmd/devguard-scanner/commands/attestations.go b/cmd/devguard-scanner/commands/attestations.go index 81704380f..83a40e5ef 100644 --- a/cmd/devguard-scanner/commands/attestations.go +++ b/cmd/devguard-scanner/commands/attestations.go @@ -22,6 +22,7 @@ import ( "log/slog" "net/http" "os" + "path/filepath" "strings" "github.com/google/uuid" @@ -78,7 +79,7 @@ func attestationsCmd(cmd *cobra.Command, args []string) error { defer os.Remove(policyPath) } - sarifResult, evals, err := scanner.EvaluatePolicyAgainstAttestations(image, policyPath, attestations) + sarifResult, evals, err := scanner.EvaluatePolicyAgainstAttestations(fmt.Sprintf("oci://%s", image), filepath.Dir(policyPath), attestations) if err != nil { return err } diff --git a/cmd/devguard-scanner/scanner/eval_policy.go b/cmd/devguard-scanner/scanner/eval_policy.go index c1ee43527..6b8fc2bed 100644 --- a/cmd/devguard-scanner/scanner/eval_policy.go +++ b/cmd/devguard-scanner/scanner/eval_policy.go @@ -17,171 +17,44 @@ package scanner import ( "encoding/json" "fmt" - "os" - "path/filepath" "github.com/l3montree-dev/devguard/compliance" "github.com/l3montree-dev/devguard/dtos/sarif" "github.com/l3montree-dev/devguard/utils" ) -func EvaluatePolicyAgainstAttestations(image string, policyPath string, attestations []map[string]any) (*sarif.SarifSchema210Json, []compliance.PolicyEvaluation, error) { - policyContent, err := os.ReadFile(policyPath) - if err != nil { - return nil, nil, fmt.Errorf("could not read policy file: %w", err) - } +func EvaluatePolicyAgainstAttestations(srcPath string, policyPath string, attestations []map[string]any) (*sarif.SarifSchema210Json, []compliance.PolicyEvaluation, error) { - policy, err := compliance.NewPolicy(filepath.Base(policyPath), string(policyContent)) + policies, err := compliance.GetPoliciesFromFS(policyPath) if err != nil { - return nil, nil, fmt.Errorf("could not parse policy: %w", err) + return nil, nil, fmt.Errorf("could not load policies from FS: %w", err) } - model := compliance.ConvertPolicyFsToModel(*policy) + evaluations := make([]compliance.PolicyEvaluation, 0) - filtered := attestations - if policy.PredicateType != "" { - filtered = []map[string]any{} +foundMatch: + for _, policy := range policies { for _, attestation := range attestations { - if predicateType, ok := attestation["predicateType"].(string); ok && predicateType == policy.PredicateType { - filtered = append(filtered, attestation) + predicateType, _ := attestation["predicateType"].(string) + if predicateType != policy.PredicateType { + continue } + raw, err := json.Marshal(attestation) + if err != nil { + return nil, nil, fmt.Errorf("could not marshal attestation: %w", err) + } + input, err := utils.ExtractAttestationPayload(string(raw)) + if err != nil { + return nil, nil, fmt.Errorf("could not extract attestation payload: %w", err) + } + eval := compliance.Eval(policy, input) + evaluations = append(evaluations, eval) + continue foundMatch } - if len(filtered) == 0 { - return nil, nil, fmt.Errorf("no attestations found for predicate type %s", policy.PredicateType) - } - } - - var evaluations []compliance.PolicyEvaluation - for _, attestation := range filtered { - raw, err := json.Marshal(attestation) - if err != nil { - return nil, nil, fmt.Errorf("could not marshal attestation: %w", err) - } - - input, err := utils.ExtractAttestationPayload(string(raw)) - if err != nil { - return nil, nil, fmt.Errorf("could not extract attestation payload: %w", err) - } - - evaluations = append(evaluations, compliance.Eval(model, input)) - } - - sarif := buildSarifFromPolicy(image, *policy, evaluations) - return &sarif, evaluations, nil -} - -func buildSarifFromPolicy(image string, policy compliance.PolicyFS, evaluations []compliance.PolicyEvaluation) sarif.SarifSchema210Json { - ruleID := policy.Filename - ruleName := policy.Title - - var helpURI *string - if len(policy.RelatedResources) > 0 { - helpURI = &policy.RelatedResources[0] - } - - rule := sarif.ReportingDescriptor{ - ID: ruleID, - Name: &ruleName, - ShortDescription: &sarif.MultiformatMessageString{ - Text: policy.Title, - }, - FullDescription: &sarif.MultiformatMessageString{ - Text: policy.Description, - }, - Help: &sarif.MultiformatMessageString{ - Text: policy.Description, - }, - HelpURI: helpURI, - Properties: &sarif.PropertyBag{ - Tags: policy.Tags, - AdditionalProperties: map[string]any{ - "priority": policy.Priority, - "relatedResources": policy.RelatedResources, - "complianceFrameworks": policy.ComplianceFrameworks, - "predicateType": policy.PredicateType, - }, - }, - } - - location := func(message string) sarif.Location { - uri := fmt.Sprintf("oci://%s", image) - - return sarif.Location{ - PhysicalLocation: sarif.PhysicalLocation{ - ArtifactLocation: sarif.ArtifactLocation{ - URI: &uri, - }, - }, - Message: sarif.Message{ - Text: message, - }, - } - } - - var results []sarif.Result - seen := make(map[string]bool) - addResult := func(r sarif.Result) { - key := string(r.Kind) + "|" + r.Message.Text - if !seen[key] { - seen[key] = true - results = append(results, r) - } - } - for _, evaluation := range evaluations { - if evaluation.Compliant != nil && *evaluation.Compliant { - addResult(sarif.Result{ - Kind: sarif.ResultKindPass, - RuleID: &ruleID, - Message: sarif.Message{ - Text: "Policy compliant", - }, - Locations: []sarif.Location{ - location("The attestation is compliant with the policy."), - }, - Properties: &sarif.PropertyBag{ - Tags: policy.Tags, - AdditionalProperties: map[string]any{ - "precision": "high", - }, - }, - }) - continue - } - for _, violation := range evaluation.Violations { - addResult(sarif.Result{ - Kind: sarif.ResultKindFail, - RuleID: &ruleID, - Message: sarif.Message{ - Text: violation, - }, - Locations: []sarif.Location{ - location(violation), - }, - Properties: &sarif.PropertyBag{ - Tags: policy.Tags, - AdditionalProperties: map[string]any{ - "precision": "high", - }, - }, - }) - } + eval := compliance.Eval(policy, nil) + evaluations = append(evaluations, eval) } - driver := sarif.ToolComponent{ - Name: "devguard-attestations", - Rules: []sarif.ReportingDescriptor{rule}, - } - - return sarif.SarifSchema210Json{ - Version: sarif.SarifSchema210JsonVersionA210, - Schema: utils.Ptr("https://json.schemastore.org/sarif-2.1.0.json"), - Runs: []sarif.Run{ - { - Tool: sarif.Tool{ - Driver: driver, - }, - Results: results, - }, - }, - } + sarifResult := compliance.BuildSarifFromPolicies(srcPath, evaluations) + return &sarifResult, evaluations, nil } diff --git a/cmd/devguard-scanner/scanner/eval_policy_test.go b/cmd/devguard-scanner/scanner/eval_policy_test.go deleted file mode 100644 index cb69e7a71..000000000 --- a/cmd/devguard-scanner/scanner/eval_policy_test.go +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (C) 2025 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 scanner - -import ( - "testing" - - "github.com/l3montree-dev/devguard/compliance" - "github.com/l3montree-dev/devguard/dtos/sarif" - "github.com/l3montree-dev/devguard/utils" -) - -func resultKey(r sarif.Result) string { - kind := string(r.Kind) - return kind + "|" + r.Message.Text -} - -func hasDuplicateResults(results []sarif.Result) bool { - seen := make(map[string]bool, len(results)) - for _, r := range results { - k := resultKey(r) - if seen[k] { - return true - } - seen[k] = true - } - return false -} - -func TestBuildSarifFromPolicy_NoDuplicateResults(t *testing.T) { - policy := compliance.PolicyFS{ - PolicyMetadata: compliance.PolicyMetadata{ - Filename: "test-policy.rego", - Title: "Test Policy", - Description: "A test policy", - Tags: []string{"test"}, - }, - } - - t.Run("duplicate violations across evaluations produce no duplicates", func(t *testing.T) { - compliant := false - evaluations := []compliance.PolicyEvaluation{ - {Compliant: &compliant, Violations: []string{"missing signature", "untrusted source"}}, - {Compliant: &compliant, Violations: []string{"missing signature", "untrusted source"}}, // same violations again - } - - result := buildSarifFromPolicy("registry.example.com/image:latest", policy, evaluations) - results := result.Runs[0].Results - - if hasDuplicateResults(results) { - t.Errorf("buildSarifFromPolicy returned duplicate result entries: %v", results) - } - }) - - t.Run("same violation repeated within one evaluation produces no duplicates", func(t *testing.T) { - compliant := false - evaluations := []compliance.PolicyEvaluation{ - {Compliant: &compliant, Violations: []string{"missing signature", "missing signature"}}, - } - - result := buildSarifFromPolicy("registry.example.com/image:latest", policy, evaluations) - results := result.Runs[0].Results - - if hasDuplicateResults(results) { - t.Errorf("buildSarifFromPolicy returned duplicate result entries: %v", results) - } - }) - - t.Run("multiple compliant evaluations produce no duplicate pass results", func(t *testing.T) { - compliant := true - evaluations := []compliance.PolicyEvaluation{ - {Compliant: &compliant, Violations: nil}, - {Compliant: &compliant, Violations: nil}, - {Compliant: &compliant, Violations: nil}, - } - - result := buildSarifFromPolicy("registry.example.com/image:latest", policy, evaluations) - results := result.Runs[0].Results - - if hasDuplicateResults(results) { - t.Errorf("buildSarifFromPolicy returned duplicate pass result entries: %v", results) - } - }) - - t.Run("mix of compliant and non-compliant evaluations with overlapping violations", func(t *testing.T) { - compliant := true - notCompliant := false - evaluations := []compliance.PolicyEvaluation{ - {Compliant: &compliant, Violations: nil}, - {Compliant: ¬Compliant, Violations: []string{"missing signature"}}, - {Compliant: ¬Compliant, Violations: []string{"missing signature"}}, - {Compliant: &compliant, Violations: nil}, - } - - result := buildSarifFromPolicy("registry.example.com/image:latest", policy, evaluations) - results := result.Runs[0].Results - - if hasDuplicateResults(results) { - t.Errorf("buildSarifFromPolicy returned duplicate result entries: %v", results) - } - }) - - t.Run("single evaluation with no violations produces no results", func(t *testing.T) { - evaluations := []compliance.PolicyEvaluation{ - {Compliant: utils.Ptr(true), Violations: nil}, - } - - result := buildSarifFromPolicy("registry.example.com/image:latest", policy, evaluations) - results := result.Runs[0].Results - - if hasDuplicateResults(results) { - t.Errorf("buildSarifFromPolicy returned duplicate result entries: %v", results) - } - }) -} diff --git a/compliance/rego.go b/compliance/rego.go index 17b9f4714..20f08858a 100644 --- a/compliance/rego.go +++ b/compliance/rego.go @@ -8,9 +8,8 @@ import ( "regexp" "sort" "strings" - "time" - "github.com/l3montree-dev/devguard/database/models" + "github.com/l3montree-dev/devguard/dtos/sarif" "github.com/l3montree-dev/devguard/utils" "github.com/open-policy-agent/opa/v1/rego" "gopkg.in/yaml.v2" @@ -47,14 +46,6 @@ type PolicyFS struct { Content string } -type PolicyEvaluation struct { - models.Policy - Compliant *bool `json:"compliant"` - Violations []string `json:"violations"` - RawEvaluationResult map[string]any `json:"rawEvaluationResult"` - AttestationUpdatedAt *time.Time `json:"attestationUpdatedAt"` -} - var packageRegexp = regexp.MustCompile(`(?m)^package compliance`) var metadataRegexp = regexp.MustCompile(`^\s*#\s*METADATA`) @@ -108,66 +99,47 @@ func parseMetadata(fileName string, content string) (PolicyMetadata, error) { }, nil } -func ConvertPolicyFsToModel(policy PolicyFS) models.Policy { - return models.Policy{ - Rego: policy.Content, - Description: policy.Description, - Title: policy.Title, - PredicateType: policy.PredicateType, - OpaqueID: &policy.Filename, - OrganizationID: nil, - } -} - -func NewPolicy(filename string, content string) (*PolicyFS, error) { - metadata, err := parseMetadata(filename, content) - if err != nil { - return nil, err - } - - return &PolicyFS{ - PolicyMetadata: metadata, - Content: content, - }, nil +type PolicyEvaluation struct { + PolicyID string + PolicyTitle string + PolicyDescription string + PolicyRelatedResources []string + PolicyTags []string + PolicyPriority int + PredicateType string + ComplianceFrameworks []string + Compliant *bool + Violations []string + RawEvaluationResult map[string]any + AttestationContent *string } -func Eval(p models.Policy, input any) PolicyEvaluation { - +func Eval(policy PolicyFS, input any) PolicyEvaluation { if input == nil { - return PolicyEvaluation{ - Policy: p, - Compliant: nil, - } + return PolicyEvaluation{Compliant: nil} } r := rego.New( rego.Query("data.compliance"), - rego.Module("", p.Rego), + rego.Module(policy.Filename, policy.Content), ) ctx := context.TODO() query, err := r.PrepareForEval(ctx) if err != nil { - return PolicyEvaluation{ - Policy: p, - Compliant: nil, - } + return PolicyEvaluation{Compliant: nil} } - rs, err := query.Eval(context.TODO(), rego.EvalInput(input)) + rs, err := query.Eval(ctx, rego.EvalInput(input)) if err != nil { - return PolicyEvaluation{ - Policy: p, - Compliant: nil, - } + return PolicyEvaluation{Compliant: nil} } - var violations = []string{} + var violations []string var rawEvalResult map[string]any var compliant *bool if len(rs) > 0 { value := rs[0].Expressions[0].Value - // cast value to map if v, ok := value.(map[string]any); ok { rawEvalResult = v if v["compliant"] != nil { @@ -184,10 +156,18 @@ func Eval(p models.Policy, input any) PolicyEvaluation { } return PolicyEvaluation{ - Policy: p, - Compliant: compliant, - Violations: violations, - RawEvaluationResult: rawEvalResult, + PolicyID: policy.Filename, + PolicyTitle: policy.Title, + PolicyDescription: policy.Description, + PolicyRelatedResources: policy.RelatedResources, + PolicyTags: policy.Tags, + PolicyPriority: policy.Priority, + PredicateType: policy.PredicateType, + ComplianceFrameworks: policy.ComplianceFrameworks, + Compliant: compliant, + Violations: violations, + RawEvaluationResult: rawEvalResult, + AttestationContent: nil, } } @@ -196,26 +176,31 @@ func Eval(p models.Policy, input any) PolicyEvaluation { //go:embed attestation-compliance-policies/policies/*.rego var policiesFs embed.FS -func GetCommunityManagedPoliciesFromFS() []PolicyFS { +func GetPoliciesFromFS(policyDir string) ([]PolicyFS, error) { // fetch all policies - policyFiles, err := policiesFs.ReadDir("attestation-compliance-policies/policies") + policyFiles, err := policiesFs.ReadDir(policyDir) if err != nil { - return nil + return nil, err } var policies []PolicyFS for _, file := range policyFiles { - content, err := policiesFs.ReadFile(filepath.Join("attestation-compliance-policies/policies", file.Name())) + content, err := policiesFs.ReadFile(filepath.Join(policyDir, file.Name())) if err != nil { continue } - policy, err := NewPolicy(file.Name(), string(content)) + metadata, err := parseMetadata(file.Name(), string(content)) if err != nil { - continue + return nil, err + } + + policy := PolicyFS{ + PolicyMetadata: metadata, + Content: string(content), } - policies = append(policies, *policy) + policies = append(policies, policy) } // sort the policies by priority - use a stable sort @@ -223,5 +208,123 @@ func GetCommunityManagedPoliciesFromFS() []PolicyFS { return policies[i].Priority < policies[j].Priority }) - return policies + return policies, nil +} + +func BuildSarifFromPolicies(srcPath string, evaluations []PolicyEvaluation) sarif.SarifSchema210Json { + rules := make([]sarif.ReportingDescriptor, 0, len(evaluations)) + results := make([]sarif.Result, 0) + seenResults := make(map[string]bool) + addResult := func(r sarif.Result) { + key := string(r.Kind) + "|" + r.Message.Text + if !seenResults[key] { + seenResults[key] = true + results = append(results, r) + } + } + for _, evaluation := range evaluations { + ruleID := evaluation.PolicyID + ruleName := evaluation.PolicyTitle + + var helpURI *string + if len(evaluation.PolicyRelatedResources) > 0 { + helpURI = &evaluation.PolicyRelatedResources[0] + } + + rule := sarif.ReportingDescriptor{ + ID: ruleID, + Name: &ruleName, + ShortDescription: &sarif.MultiformatMessageString{ + Text: evaluation.PolicyTitle, + }, + FullDescription: &sarif.MultiformatMessageString{ + Text: evaluation.PolicyDescription, + }, + Help: &sarif.MultiformatMessageString{ + Text: evaluation.PolicyDescription, + }, + HelpURI: helpURI, + Properties: &sarif.PropertyBag{ + Tags: evaluation.PolicyTags, + AdditionalProperties: map[string]any{ + "priority": evaluation.PolicyPriority, + "relatedResources": evaluation.PolicyRelatedResources, + "complianceFrameworks": evaluation.ComplianceFrameworks, + "predicateType": evaluation.PredicateType, + }, + }, + } + + rules = append(rules, rule) + + artifactLocation := sarif.ArtifactLocation{URI: &srcPath} + props := &sarif.PropertyBag{ + Tags: evaluation.PolicyTags, + AdditionalProperties: map[string]any{ + "precision": "high", + }, + } + + if evaluation.Compliant == nil { + addResult(sarif.Result{ + Kind: sarif.ResultKindOpen, + RuleID: &ruleID, + Message: sarif.Message{ + Text: "No attestation found for policy — compliance could not be determined.", + }, + Locations: []sarif.Location{ + {PhysicalLocation: sarif.PhysicalLocation{ArtifactLocation: artifactLocation}}, + }, + Properties: props, + }) + continue + } + + if *evaluation.Compliant { + addResult(sarif.Result{ + Kind: sarif.ResultKindPass, + RuleID: &ruleID, + Message: sarif.Message{ + Text: "Policy compliant", + }, + Locations: []sarif.Location{ + {PhysicalLocation: sarif.PhysicalLocation{ArtifactLocation: artifactLocation}}, + }, + Properties: props, + }) + continue + } + + for _, violation := range evaluation.Violations { + addResult(sarif.Result{ + Kind: sarif.ResultKindFail, + RuleID: &ruleID, + Message: sarif.Message{ + Text: violation, + }, + Locations: []sarif.Location{ + {PhysicalLocation: sarif.PhysicalLocation{ArtifactLocation: artifactLocation}}, + }, + Properties: props, + }) + } + } + + driver := sarif.ToolComponent{ + Name: "devguard-attestations", + Rules: rules, + } + + return sarif.SarifSchema210Json{ + Version: sarif.SarifSchema210JsonVersionA210, + Schema: utils.Ptr("https://json.schemastore.org/sarif-2.1.0.json"), + Runs: []sarif.Run{ + { + Tool: sarif.Tool{ + Driver: driver, + }, + Results: results, + }, + }, + } } diff --git a/compliance/rego_test.go b/compliance/rego_test.go index f4e9b275d..8263a6a4e 100644 --- a/compliance/rego_test.go +++ b/compliance/rego_test.go @@ -5,6 +5,7 @@ import ( "os" "testing" + "github.com/l3montree-dev/devguard/dtos/sarif" "github.com/l3montree-dev/devguard/utils" "github.com/stretchr/testify/assert" ) @@ -28,41 +29,19 @@ func TestEval(t *testing.T) { t.Fatal(err) } - // create a new policy - policy, err := NewPolicy("", string(policyContent)) + metadata, err := parseMetadata("", string(policyContent)) if err != nil { t.Fatal(err) } - - model := ConvertPolicyFsToModel(*policy) + policy := PolicyFS{PolicyMetadata: metadata, Content: string(policyContent)} // evaluate the policy - res := Eval(model, input) + res := Eval(policy, input) if res.Compliant == nil || *res.Compliant != true { t.Fatal(res) } } -func TestNewPolicy(t *testing.T) { - t.Run("should parse the metadata", func(t *testing.T) { - // read the example-policy.rego file - policyContent, err := os.ReadFile("testfiles/example-policy.rego") - if err != nil { - t.Fatal(err) - } - - // create a new policy - policy, err := NewPolicy("", string(policyContent)) - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, "Build from signed source", policy.Title) - assert.Equal(t, "This policy checks if the build was done from a signed commit.", policy.Description) - assert.Equal(t, []string{"iso27001", "A.8 Access Control"}, policy.Tags) - }) -} - func TestOnlyOsiApprovedLicensesPolicy(t *testing.T) { sbomContent, err := os.ReadFile("./testfiles/sbom.json") if err != nil { @@ -74,10 +53,11 @@ func TestOnlyOsiApprovedLicensesPolicy(t *testing.T) { t.Fatal(err) } - policy, err := NewPolicy("", string(policyContent)) + metadata, err := parseMetadata("", string(policyContent)) if err != nil { t.Fatal(err) } + policy := PolicyFS{PolicyMetadata: metadata, Content: string(policyContent)} // parse the sbom var input any @@ -86,11 +66,9 @@ func TestOnlyOsiApprovedLicensesPolicy(t *testing.T) { t.Fatal(err) } - model := ConvertPolicyFsToModel(*policy) - result := Eval(model, input) + result := Eval(policy, input) expectedResult := &PolicyEvaluation{ - Policy: model, Compliant: utils.Ptr(false), Violations: []string{ "Component \"github.com/cloudflare/circl\" uses non-OSI approved license \"non-standard\"", @@ -129,3 +107,101 @@ func TestOnlyOsiApprovedLicensesPolicy(t *testing.T) { assert.Subset(t, expectedResult.Violations, result.Violations) assert.Subset(t, result.Violations, expectedResult.Violations) } + +func resultKey(r sarif.Result) string { + return string(r.Kind) + "|" + r.Message.Text +} + +func hasDuplicateResults(results []sarif.Result) bool { + seen := make(map[string]bool, len(results)) + for _, r := range results { + k := resultKey(r) + if seen[k] { + return true + } + seen[k] = true + } + return false +} + +func makeEvaluations(policy PolicyFS, evals []PolicyEvaluation) []PolicyEvaluation { + for i := range evals { + evals[i].PolicyID = policy.Filename + evals[i].PolicyTitle = policy.Title + evals[i].PolicyDescription = policy.Description + evals[i].PolicyTags = policy.Tags + } + return evals +} + +func TestBuildSarifFromPolicies_NoDuplicateResults(t *testing.T) { + policy := PolicyFS{ + PolicyMetadata: PolicyMetadata{ + Filename: "test-policy.rego", + Title: "Test Policy", + Description: "A test policy", + Tags: []string{"test"}, + }, + } + + t.Run("duplicate violations across evaluations produce no duplicates", func(t *testing.T) { + compliant := false + evaluations := makeEvaluations(policy, []PolicyEvaluation{ + {Compliant: &compliant, Violations: []string{"missing signature", "untrusted source"}}, + {Compliant: &compliant, Violations: []string{"missing signature", "untrusted source"}}, + }) + results := BuildSarifFromPolicies("registry.example.com/image:latest", evaluations).Runs[0].Results + if hasDuplicateResults(results) { + t.Errorf("BuildSarifFromPolicies returned duplicate result entries: %v", results) + } + }) + + t.Run("same violation repeated within one evaluation produces no duplicates", func(t *testing.T) { + compliant := false + evaluations := makeEvaluations(policy, []PolicyEvaluation{ + {Compliant: &compliant, Violations: []string{"missing signature", "missing signature"}}, + }) + results := BuildSarifFromPolicies("registry.example.com/image:latest", evaluations).Runs[0].Results + if hasDuplicateResults(results) { + t.Errorf("BuildSarifFromPolicies returned duplicate result entries: %v", results) + } + }) + + t.Run("multiple compliant evaluations produce no duplicate pass results", func(t *testing.T) { + compliant := true + evaluations := makeEvaluations(policy, []PolicyEvaluation{ + {Compliant: &compliant}, + {Compliant: &compliant}, + {Compliant: &compliant}, + }) + results := BuildSarifFromPolicies("registry.example.com/image:latest", evaluations).Runs[0].Results + if hasDuplicateResults(results) { + t.Errorf("BuildSarifFromPolicies returned duplicate pass result entries: %v", results) + } + }) + + t.Run("mix of compliant and non-compliant evaluations with overlapping violations", func(t *testing.T) { + compliant := true + notCompliant := false + evaluations := makeEvaluations(policy, []PolicyEvaluation{ + {Compliant: &compliant}, + {Compliant: ¬Compliant, Violations: []string{"missing signature"}}, + {Compliant: ¬Compliant, Violations: []string{"missing signature"}}, + {Compliant: &compliant}, + }) + results := BuildSarifFromPolicies("registry.example.com/image:latest", evaluations).Runs[0].Results + if hasDuplicateResults(results) { + t.Errorf("BuildSarifFromPolicies returned duplicate result entries: %v", results) + } + }) + + t.Run("single evaluation with no violations produces no results", func(t *testing.T) { + evaluations := makeEvaluations(policy, []PolicyEvaluation{ + {Compliant: utils.Ptr(true)}, + }) + results := BuildSarifFromPolicies("registry.example.com/image:latest", evaluations).Runs[0].Results + if hasDuplicateResults(results) { + t.Errorf("BuildSarifFromPolicies returned duplicate result entries: %v", results) + } + }) +} diff --git a/controllers/compliance_controller.go b/controllers/compliance_controller.go deleted file mode 100644 index 6c8614dd8..000000000 --- a/controllers/compliance_controller.go +++ /dev/null @@ -1,132 +0,0 @@ -package controllers - -import ( - "context" - _ "embed" - - "github.com/google/uuid" - "github.com/l3montree-dev/devguard/compliance" - "github.com/l3montree-dev/devguard/database/models" - "github.com/l3montree-dev/devguard/shared" -) - -type ComplianceController struct { - assetVersionRepository shared.AssetVersionRepository - attestationService shared.AttestationService - policyRepository shared.PolicyRepository -} - -func NewComplianceController(assetVersionRepository shared.AssetVersionRepository, attestationService shared.AttestationService, policyRepository shared.PolicyRepository) *ComplianceController { - return &ComplianceController{ - assetVersionRepository: assetVersionRepository, - policyRepository: policyRepository, - attestationService: attestationService, - } -} - -func (c *ComplianceController) getAssetVersionCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion) ([]compliance.PolicyEvaluation, error) { - // get the attestation - attestations, err := c.attestationService.GetByAssetVersionAndAssetID(ctx, nil, assetVersion.AssetID, assetVersion.Name) - if err != nil { - return nil, err - } - - policies, err := c.policyRepository.FindByProjectID(ctx, nil, projectID) - if err != nil { - return nil, err - } - - results := make([]compliance.PolicyEvaluation, 0, len(policies)) -foundMatch: - for _, policy := range policies { - // check if we find an attestation that matches - for _, attestation := range attestations { - if attestation.PredicateType != policy.PredicateType { - continue - } - res := compliance.Eval(policy, attestation.Content) - // this matches - lets add it - results = append(results, res) - continue foundMatch - } - // we did not find any attestation that matches - lets add the policy with a nil result - results = append(results, compliance.Eval(policy, nil)) - } - - // compliance.Evaluate the policy - return results, nil -} - -func (c *ComplianceController) Details(ctx shared.Context) error { - assetVersion := shared.GetAssetVersion(ctx) - - p := ctx.Param("policy") - // parse the uuid - policyID, err := uuid.Parse(p) - if err != nil { - return ctx.JSON(400, nil) - } - // get all policies - policy, err := c.policyRepository.Read(ctx.Request().Context(), nil, policyID) - if err != nil { - return ctx.JSON(404, nil) - } - - attestations, err := c.attestationService.GetByAssetVersionAndAssetID(ctx.Request().Context(), nil, assetVersion.AssetID, assetVersion.Name) - - if err != nil { - return ctx.JSON(500, nil) - } - - // look for the right attestations - for _, attestation := range attestations { - if attestation.PredicateType == policy.PredicateType { - res := compliance.Eval(policy, attestation.Content) - return ctx.JSON(200, res) - } - } - // we did not find any attestation that matches - lets add the policy with a nil result - return ctx.JSON(200, compliance.Eval(policy, nil)) -} - -func (c *ComplianceController) AssetCompliance(ctx shared.Context) error { - asset := shared.GetAsset(ctx) - assetVersion, err := shared.MaybeGetAssetVersion(ctx) - if err != nil { - // we need to get the default asset version - assetVersion, err = c.assetVersionRepository.GetDefaultAssetVersion(ctx.Request().Context(), nil, asset.ID) - if err != nil { - return ctx.JSON(404, nil) - } - } - - project := shared.GetProject(ctx) - - results, err := c.getAssetVersionCompliance(ctx.Request().Context(), project.ID, assetVersion) - if err != nil { - return ctx.JSON(500, nil) - } - - return ctx.JSON(200, results) -} - -func (c *ComplianceController) ProjectCompliance(ctx shared.Context) error { - // get all default asset version from the project - project := shared.GetProject(ctx) - assetVersions, err := c.assetVersionRepository.GetDefaultAssetVersionsByProjectID(ctx.Request().Context(), nil, project.ID) - - if err != nil { - return ctx.JSON(500, nil) - } - - results := make([][]compliance.PolicyEvaluation, 0, len(assetVersions)) - for _, assetVersion := range assetVersions { - compliance, err := c.getAssetVersionCompliance(ctx.Request().Context(), project.ID, assetVersion) - if err != nil { - return ctx.JSON(500, nil) - } - - results = append(results, compliance) - } - return ctx.JSON(200, results) -} diff --git a/controllers/compliance_risk_controller.go b/controllers/compliance_risk_controller.go index e4072ba2f..c4314f1ec 100644 --- a/controllers/compliance_risk_controller.go +++ b/controllers/compliance_risk_controller.go @@ -9,6 +9,7 @@ import ( "github.com/l3montree-dev/devguard/database/models" "github.com/l3montree-dev/devguard/dtos" + "github.com/l3montree-dev/devguard/dtos/sarif" "github.com/l3montree-dev/devguard/shared" "github.com/l3montree-dev/devguard/transformer" "github.com/l3montree-dev/devguard/utils" @@ -168,24 +169,24 @@ func (c *ComplianceRiskController) Mitigate(ctx shared.Context) error { return ctx.JSON(200, convertComplianceRiskToDetailedDTO(risk)) } -// RecalculateFromService fetches evaluations via complianceService.ArtifactCompliance and recalculates risks. -func (c *ComplianceRiskController) RecalculateFromService(ctx shared.Context) error { +// EvaluateArtifactCompliance fetches evaluations via complianceService.ArtifactCompliance and recalculates risks. +func (c *ComplianceRiskController) EvaluateArtifactCompliance(ctx shared.Context) error { assetVersion := shared.GetAssetVersion(ctx) artifact := shared.GetArtifact(ctx) project := shared.GetProject(ctx) userAgent := ctx.Request().UserAgent() userID := shared.GetSession(ctx).GetUserID() - evaluations, err := c.complianceService.ArtifactCompliance(ctx.Request().Context(), project.ID, assetVersion, artifact) + sarifDoc, err := c.complianceService.ArtifactCompliance(ctx.Request().Context(), project.ID, assetVersion, artifact) if err != nil { return echo.NewHTTPError(500, "could not evaluate artifact compliance").WithInternal(err) } - if err := c.complianceRiskService.HandleArtifactCompliance(ctx.Request().Context(), nil, userID, &userAgent, assetVersion, artifact, evaluations); err != nil { + if err := c.complianceRiskService.HandleArtifactCompliance(ctx.Request().Context(), nil, userID, &userAgent, assetVersion, artifact, sarifDoc); err != nil { return echo.NewHTTPError(500, "could not handle artifact compliance risks").WithInternal(err) } - return ctx.JSON(200, evaluations) + return ctx.JSON(200, sarifDoc) } // UploadZip accepts a ZIP file containing attestation files and an evaluations.json. @@ -217,7 +218,7 @@ func (c *ComplianceRiskController) UploadZip(ctx shared.Context) error { return echo.NewHTTPError(400, "invalid zip file").WithInternal(err) } - var evaluations []dtos.PolicyEvaluationDTO + var sarifDoc *sarif.SarifSchema210Json for _, f := range zr.File { rc, err := f.Open() @@ -232,10 +233,12 @@ func (c *ComplianceRiskController) UploadZip(ctx shared.Context) error { continue } - if f.Name == "evaluations.json" { - if err := json.Unmarshal(content, &evaluations); err != nil { - return echo.NewHTTPError(400, "invalid evaluations.json in zip").WithInternal(err) + if f.Name == "sarif.json" { + var doc sarif.SarifSchema210Json + if err := json.Unmarshal(content, &doc); err != nil { + return echo.NewHTTPError(400, "invalid sarif.json in zip").WithInternal(err) } + sarifDoc = &doc continue } @@ -264,13 +267,13 @@ func (c *ComplianceRiskController) UploadZip(ctx shared.Context) error { } } - if evaluations == nil { - return echo.NewHTTPError(400, "evaluations.json not found in zip") + if sarifDoc == nil { + return echo.NewHTTPError(400, "sarif.json not found in zip") } - if err := c.complianceRiskService.HandleArtifactCompliance(ctx.Request().Context(), nil, userID, &userAgent, assetVersion, artifact, evaluations); err != nil { + if err := c.complianceRiskService.HandleArtifactCompliance(ctx.Request().Context(), nil, userID, &userAgent, assetVersion, artifact, *sarifDoc); err != nil { return echo.NewHTTPError(500, "could not handle artifact compliance risks").WithInternal(err) } - return ctx.JSON(200, evaluations) + return ctx.JSON(200, sarifDoc) } diff --git a/controllers/policy_controller.go b/controllers/policy_controller.go deleted file mode 100644 index b580df62e..000000000 --- a/controllers/policy_controller.go +++ /dev/null @@ -1,265 +0,0 @@ -package controllers - -import ( - "context" - "log/slog" - "sync" - - "github.com/google/uuid" - "github.com/l3montree-dev/devguard/compliance" - "github.com/l3montree-dev/devguard/database/models" - "github.com/l3montree-dev/devguard/dtos" - "github.com/l3montree-dev/devguard/shared" - "github.com/l3montree-dev/devguard/utils" -) - -type PolicyController struct { - policyRepository shared.PolicyRepository - projectRepository shared.ProjectRepository -} - -func NewPolicyController(policyRepository shared.PolicyRepository, projectRepository shared.ProjectRepository) *PolicyController { - c := &PolicyController{ - policyRepository: policyRepository, - projectRepository: projectRepository, - } - - if err := c.migratePolicies(); err != nil { - panic(err) - } - return c -} - -func (c *PolicyController) migratePolicies() error { - ctx := context.Background() - // we need to migrate the policies from the old format to the new format - // this is only needed for the first time we run the application - // after that we can remove this function - policies := compliance.GetCommunityManagedPoliciesFromFS() - policyModels := make([]models.Policy, len(policies)) - for i, policy := range policies { - policyModels[i] = compliance.ConvertPolicyFsToModel(policy) - } - - // get all community managed policies from the database - dbPolicies, err := c.policyRepository.FindCommunityManagedPolicies(ctx, nil) - if err != nil { - return err - } - - // compare the policies - comp := utils.CompareSlices(policyModels, dbPolicies, func(p models.Policy) string { - return *p.OpaqueID - }) - - toCreate := comp.OnlyInA - toUpdate := comp.InBothB // use the B elements - those are the new policies read from disk - toDelete := comp.OnlyInB - - // set the id for the policies to update - for i := range toUpdate { - for j := range dbPolicies { - if dbPolicies[j].OpaqueID == toUpdate[i].OpaqueID { - toUpdate[i].ID = dbPolicies[j].ID - break - } - } - } - - // create the policies - if len(toCreate) > 0 { - if err := c.policyRepository.CreateBatch(ctx, nil, toCreate); err != nil { - return err - } - } - - // update the policies - if len(toUpdate) > 0 { - if err := c.policyRepository.SaveBatch(ctx, nil, toUpdate); err != nil { - return err - } - } - - // delete the policies - if len(toDelete) > 0 { - wg := sync.WaitGroup{} - for _, policy := range toDelete { - wg.Add(1) - go func(p models.Policy) { - defer wg.Done() - err := c.policyRepository.GetDB(ctx, nil).Model(&p).Association("Projects").Clear() - if err != nil { - slog.Warn("failed to clear projects association for policy", "policyID", p.ID, "error", err) - return - } - }(policy) - } - wg.Wait() - if err := c.policyRepository.DeleteBatch(ctx, nil, toDelete); err != nil { - return err - } - } - - return nil -} - -func (c *PolicyController) GetOrganizationPolicies(ctx shared.Context) error { - - org := shared.GetOrg(ctx) - policies, err := c.policyRepository.FindByOrganizationID(ctx.Request().Context(), nil, org.ID) - - if err != nil { - return err - } - - // include the community managed policies - communityPolicies, err := c.policyRepository.FindCommunityManagedPolicies(ctx.Request().Context(), nil) - if err != nil { - return err - } - - return ctx.JSON(200, append(policies, communityPolicies...)) -} - -func (c *PolicyController) GetProjectPolicies(ctx shared.Context) error { - project := shared.GetProject(ctx) - policies, err := c.policyRepository.FindByProjectID(ctx.Request().Context(), nil, project.ID) - - if err != nil { - return err - } - - return ctx.JSON(200, policies) -} - -func (c *PolicyController) GetPolicy(ctx shared.Context) error { - policyID := ctx.Param("policyID") - - // parse the uuid - policyUUID, err := uuid.Parse(policyID) - if err != nil { - return err - } - - policy, err := c.policyRepository.Read(ctx.Request().Context(), nil, policyUUID) - - if err != nil { - return err - } - - return ctx.JSON(200, policy) -} - -func (c *PolicyController) CreatePolicy(ctx shared.Context) error { - policy := dtos.PolicyDTO{} - if err := ctx.Bind(&policy); err != nil { - return err - } - - org := shared.GetOrg(ctx) - - // create a new policy model - policyModel := models.Policy{ - Rego: policy.Rego, - Description: policy.Description, - Title: policy.Title, - PredicateType: policy.PredicateType, - OrganizationID: utils.Ptr(org.ID), - OpaqueID: nil, - } - - // create the policy - if err := c.policyRepository.Create(ctx.Request().Context(), nil, &policyModel); err != nil { - return err - } - - return ctx.JSON(201, policy) -} - -func (c *PolicyController) UpdatePolicy(ctx shared.Context) error { - policyID := ctx.Param("policyID") - - // parse the uuid - policyUUID, err := uuid.Parse(policyID) - if err != nil { - return err - } - - policy := dtos.PolicyDTO{} - if err := ctx.Bind(&policy); err != nil { - return err - } - - org := shared.GetOrg(ctx) - - // create a new policy model - policyModel := models.Policy{ - ID: policyUUID, - Rego: policy.Rego, - Description: policy.Description, - Title: policy.Title, - PredicateType: policy.PredicateType, - OrganizationID: utils.Ptr(org.ID), - } - - if err := c.policyRepository.Save(ctx.Request().Context(), nil, &policyModel); err != nil { - return err - } - - return ctx.JSON(200, policyModel) -} - -func (c *PolicyController) DeletePolicy(ctx shared.Context) error { - policyID := ctx.Param("policyID") - - // parse the uuid - policyUUID, err := uuid.Parse(policyID) - if err != nil { - return err - } - - // delete the policy - if err := c.policyRepository.Delete(ctx.Request().Context(), nil, policyUUID); err != nil { - return err - } - - return ctx.NoContent(204) -} - -func (c *PolicyController) EnablePolicyForProject(ctx shared.Context) error { - policyID := ctx.Param("policyID") - - project := shared.GetProject(ctx) - - // parse the uuid - policyUUID, err := uuid.Parse(policyID) - if err != nil { - return err - } - - // enable the policy for the project - if err := c.projectRepository.EnablePolicyForProject(ctx.Request().Context(), nil, project.ID, policyUUID); err != nil { - return err - } - - return ctx.NoContent(204) -} - -func (c *PolicyController) DisablePolicyForProject(ctx shared.Context) error { - policyID := ctx.Param("policyID") - - // parse the uuid - policyUUID, err := uuid.Parse(policyID) - if err != nil { - return err - } - - project := shared.GetProject(ctx) - - // disable the policy for the project - if err := c.projectRepository.DisablePolicyForProject(ctx.Request().Context(), nil, project.ID, policyUUID); err != nil { - return err - } - - return ctx.NoContent(204) -} diff --git a/controllers/providers.go b/controllers/providers.go index 0f219b984..fb96666f9 100644 --- a/controllers/providers.go +++ b/controllers/providers.go @@ -90,10 +90,8 @@ var ControllerModule = fx.Options( // Security & Compliance fx.Provide(NewCSAFController), - fx.Provide(NewComplianceController), fx.Provide(NewAttestationController), fx.Provide(NewInToToController), - fx.Provide(NewPolicyController), fx.Provide(NewComplianceRiskController), // Integrations diff --git a/daemons/attestation_daemon.go b/daemons/attestation_daemon.go index 0a2acb2d3..a56680310 100644 --- a/daemons/attestation_daemon.go +++ b/daemons/attestation_daemon.go @@ -20,7 +20,7 @@ func (runner *DaemonRunner) CheckArtifactCompliance(input <-chan assetWithProjec for _, assetVersion := range assetWithDetails.assetVersions { for _, artifact := range assetVersion.Artifacts { - evaluations, err := runner.complianceService.ArtifactCompliance(stageCtx, assetWithDetails.project.ID, assetVersion, artifact) + sarifDoc, err := runner.complianceService.ArtifactCompliance(stageCtx, assetWithDetails.project.ID, assetVersion, artifact) if err != nil { slog.Error("could not evaluate artifact compliance", "assetID", assetWithDetails.asset.ID, @@ -30,7 +30,7 @@ func (runner *DaemonRunner) CheckArtifactCompliance(input <-chan assetWithProjec ) continue } - if err := runner.complianceRiskService.HandleArtifactCompliance(stageCtx, nil, "system", nil, assetVersion, artifact, evaluations); err != nil { + if err := runner.complianceRiskService.HandleArtifactCompliance(stageCtx, nil, "system", nil, assetVersion, artifact, sarifDoc); err != nil { slog.Error("could not handle artifact compliance risks", "assetID", assetWithDetails.asset.ID, "assetVersion", assetVersion.Name, diff --git a/database/models/compliance_risk_model.go b/database/models/compliance_risk_model.go index ad03d5740..958507aaa 100644 --- a/database/models/compliance_risk_model.go +++ b/database/models/compliance_risk_model.go @@ -16,7 +16,6 @@ package models import ( "fmt" - "time" "github.com/google/uuid" "github.com/l3montree-dev/devguard/dtos" @@ -27,12 +26,18 @@ import ( type ComplianceRisk struct { Vulnerability - PolicyID string `json:"policyId" gorm:"type:text;"` - PolicyTitle string `json:"policyTitle" gorm:"type:text;"` - PolicyDescription *string `json:"policyDescription" gorm:"type:text;"` - PredicateType string `json:"predicateType" gorm:"type:text;"` - AttestationViolations []string `json:"attestationViolations" gorm:"type:text[];"` - AttestationUpdatedAt *time.Time `json:"attestationUpdatedAt" gorm:"type:timestamptz;"` + PolicyID string `json:"policyId" gorm:"type:text;"` + PolicyTitle string `json:"policyTitle" gorm:"type:text;"` + PolicyDescription *string `json:"policyDescription" gorm:"type:text;"` + PolicyRelatedResources []string `json:"policyRelatedResources" gorm:"type:text[];"` + PolicyTags []string `json:"policyTags" gorm:"type:text[];"` + PolicyPriority int `json:"policyPriority"` + ComplianceFrameworks []string `json:"complianceFrameworks" gorm:"type:text[];"` + PredicateType string `json:"predicateType" gorm:"type:text;"` + + AttestationContent *string `json:"attestationContent" gorm:"type:text;"` + + AttestationViolations []string `json:"attestationViolations" gorm:"type:text[];"` Events []VulnEvent `gorm:"foreignKey:ComplianceRiskID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;" json:"events"` diff --git a/database/models/policy_model.go b/database/models/policy_model.go deleted file mode 100644 index 3c1966b64..000000000 --- a/database/models/policy_model.go +++ /dev/null @@ -1,21 +0,0 @@ -package models - -import "github.com/google/uuid" - -type Policy struct { - ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` - Rego string `json:"rego"` - Title string `json:"title"` - PredicateType string `json:"predicateType"` - Description string `json:"description"` - - OrganizationID *uuid.UUID `json:"organizationId"` // will be null for global policies - Organization *Org `json:"organization" gorm:"foreignKey:OrganizationID;references:ID;constraint:OnDelete:CASCADE;"` - - OpaqueID *string `json:"opaqueId" gorm:"unique"` // only used by global policies maintained by the community and migrated by the system - Projects []Project `json:"projects" gorm:"many2many:project_enabled_policies;constraint:OnDelete:CASCADE;"` -} - -func (m Policy) TableName() string { - return "policies" -} diff --git a/database/models/project_model.go b/database/models/project_model.go index deec3e8dd..aa7450861 100644 --- a/database/models/project_model.go +++ b/database/models/project_model.go @@ -43,8 +43,6 @@ type Project struct { ConfigFiles databasetypes.JSONB `json:"configFiles" gorm:"type:jsonb"` - EnabledPolicies []Policy `json:"enabledPolicies" gorm:"many2many:project_enabled_policies;constraint:OnDelete:CASCADE;"` - ExternalEntityID *string `json:"externalEntityId" gorm:"uniqueIndex:unique_external_entity;"` ExternalEntityProviderID *string `json:"externalEntityProviderId" gorm:"uniqueIndex:unique_external_entity;"` ExternalEntityParentID *string `json:"externalEntityProviderParentId" gorm:"type:text;"` diff --git a/database/repositories/policy_repository.go b/database/repositories/policy_repository.go deleted file mode 100644 index 717fba7a3..000000000 --- a/database/repositories/policy_repository.go +++ /dev/null @@ -1,51 +0,0 @@ -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 policyRepository struct { - db *gorm.DB - utils.Repository[uuid.UUID, models.Policy, *gorm.DB] -} - -func NewPolicyRepository(db *gorm.DB) *policyRepository { - return &policyRepository{ - db: db, - Repository: newGormRepository[uuid.UUID, models.Policy](db), - } -} - -func (r *policyRepository) FindByProjectID(ctx context.Context, tx *gorm.DB, projectID uuid.UUID) ([]models.Policy, error) { - // we need to use the project_enabled_policies pivot table to get the policies for a project - var policies []models.Policy - if err := r.GetDB(ctx, tx).Joins("JOIN project_enabled_policies ON project_enabled_policies.policy_id = policies.id"). - Where("project_enabled_policies.project_id = ?", projectID). - Find(&policies).Error; err != nil { - return nil, err - } - - return policies, nil -} - -func (r *policyRepository) FindCommunityManagedPolicies(ctx context.Context, tx *gorm.DB) ([]models.Policy, error) { - // where organization id is nil - var policies []models.Policy - if err := r.GetDB(ctx, tx).Where("organization_id IS NULL").Find(&policies).Error; err != nil { - return nil, err - } - return policies, nil -} - -func (r *policyRepository) FindByOrganizationID(ctx context.Context, tx *gorm.DB, organizationID uuid.UUID) ([]models.Policy, error) { - var policies []models.Policy - if err := r.GetDB(ctx, tx).Find(&policies, "organization_id = ?", organizationID).Error; err != nil { - return nil, err - } - return policies, nil -} diff --git a/database/repositories/project_repository.go b/database/repositories/project_repository.go index 9400464e1..277a00c51 100644 --- a/database/repositories/project_repository.go +++ b/database/repositories/project_repository.go @@ -426,21 +426,6 @@ func (g *projectRepository) GetDirectChildProjects(ctx context.Context, tx *gorm return projects, err } -func (g *projectRepository) EnablePolicyForProject(ctx context.Context, tx *gorm.DB, projectID uuid.UUID, policyID uuid.UUID) error { - return g.GetDB(ctx, tx).Model(&models.Project{ - Model: models.Model{ - ID: projectID, - }, - }).Association("EnabledPolicies").Append(&models.Policy{ID: policyID}) -} -func (g *projectRepository) DisablePolicyForProject(ctx context.Context, tx *gorm.DB, projectID uuid.UUID, policyID uuid.UUID) error { - return g.GetDB(ctx, tx).Model(&models.Project{ - Model: models.Model{ - ID: projectID, - }, - }).Association("EnabledPolicies").Delete(&models.Policy{ID: policyID}) -} - func (g *projectRepository) EnableCommunityManagedPolicies(ctx context.Context, tx *gorm.DB, projectID uuid.UUID) error { // community policies can be identified by their "organization_id" being nil return g.GetDB(ctx, tx).Exec(` diff --git a/database/repositories/providers.go b/database/repositories/providers.go index 33440da01..0c57020f5 100644 --- a/database/repositories/providers.go +++ b/database/repositories/providers.go @@ -41,7 +41,6 @@ var Module = fx.Options( fx.Provide(fx.Annotate(NewInTotoLinkRepository, fx.As(new(shared.InTotoLinkRepository)))), fx.Provide(fx.Annotate(NewSupplyChainRepository, fx.As(new(shared.SupplyChainRepository)))), fx.Provide(fx.Annotate(NewAttestationRepository, fx.As(new(shared.AttestationRepository)))), - fx.Provide(fx.Annotate(NewPolicyRepository, fx.As(new(shared.PolicyRepository)))), fx.Provide(fx.Annotate(NewLicenseRiskRepository, fx.As(new(shared.LicenseRiskRepository)))), fx.Provide(fx.Annotate(NewComplianceRiskRepository, fx.As(new(shared.ComplianceRiskRepository)))), fx.Provide(fx.Annotate(NewWebhookRepository, fx.As(new(shared.WebhookIntegrationRepository)))), diff --git a/dtos/compliance_risk_dto.go b/dtos/compliance_risk_dto.go index 49d863105..c74c58019 100644 --- a/dtos/compliance_risk_dto.go +++ b/dtos/compliance_risk_dto.go @@ -12,9 +12,13 @@ type ComplianceRiskDTO struct { AssetID string `json:"assetId"` Artifacts []ArtifactDTO `json:"artifacts,omitempty"` - PolicyID string `json:"policyId"` - PolicyTitle string `json:"policyTitle"` - PolicyDescription *string `json:"policyDescription"` + PolicyID string `json:"policyId"` + PolicyTitle string `json:"policyTitle"` + PolicyDescription *string `json:"policyDescription"` + PolicyRelatedResources []string `json:"policyRelatedResources"` + PolicyTags []string `json:"policyTags"` + PolicyPriority int `json:"policyPriority"` + ComplianceFrameworks []string `json:"complianceFrameworks"` State VulnState `json:"state"` CreatedAt time.Time `json:"createdAt"` @@ -22,9 +26,9 @@ type ComplianceRiskDTO struct { TicketURL *string `json:"ticketUrl"` ManualTicketCreation bool `json:"manualTicketCreation"` - PredicateType string `json:"predicateType"` - AttestationUpdatedAt *time.Time `json:"attestationUpdatedAt"` - AttestationViolations []string `json:"attestationViolations"` + PredicateType string `json:"predicateType"` + AttestationContent *string `json:"attestationContent"` + AttestationViolations []string `json:"attestationViolations"` } type DetailedComplianceRiskDTO struct { diff --git a/dtos/policy_dto.go b/dtos/policy_dto.go deleted file mode 100644 index e7325ad33..000000000 --- a/dtos/policy_dto.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2025 l3montree UG (haftungsbeschraenkt) -// -// 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 dtos - -import "time" - -type PolicyDTO struct { - Title string `json:"title"` - Description string `json:"description"` - Priority int `json:"priority"` - PredicateType string `json:"predicateType"` - Rego string `json:"rego"` -} - -type PolicyEvaluationDTO struct { - PolicyID string `json:"policyId"` - PolicyTitle string `json:"policyTitle"` - PolicyDescription *string `json:"policyDescription"` - - State VulnState `json:"state"` - CreatedAt time.Time `json:"createdAt"` - - PredicateType string `json:"predicateType"` - AttestationUpdatedAt *time.Time `json:"attestationUpdatedAt"` - AttestationViolations []string `json:"attestationViolations"` -} diff --git a/router/asset_router.go b/router/asset_router.go index b44b918c3..42b513337 100644 --- a/router/asset_router.go +++ b/router/asset_router.go @@ -32,7 +32,6 @@ func NewAssetRouter( assetController *controllers.AssetController, dependencyProxyController *dependencyfirewall.DependencyProxyController, assetVersionController *controllers.AssetVersionController, - complianceController *controllers.ComplianceController, statisticsController *controllers.StatisticsController, componentController *controllers.ComponentController, intotoController *controllers.InToToController, @@ -49,8 +48,6 @@ func NewAssetRouter( assetRouter := projectGroup.Group.Group("/assets/:assetSlug", assetScopedRBAC(shared.ObjectAsset, shared.ActionRead)) assetRouter.GET("/", assetController.Read) - assetRouter.GET("/compliance/", complianceController.AssetCompliance) - assetRouter.GET("/compliance/:policy/", complianceController.Details) assetRouter.GET("/number-of-exploits/", statisticsController.GetCVESWithKnownExploits) assetRouter.GET("/components/licenses/", componentController.LicenseDistribution) assetRouter.GET("/config-files/:config-file/", assetController.GetConfigFile) diff --git a/router/asset_version_router.go b/router/asset_version_router.go index 6a026ecd8..9abd26a76 100644 --- a/router/asset_version_router.go +++ b/router/asset_version_router.go @@ -30,7 +30,6 @@ func NewAssetVersionRouter( assetGroup AssetRouter, assetVersionController *controllers.AssetVersionController, firstPartyVulnController *controllers.FirstPartyVulnController, - complianceController *controllers.ComplianceController, componentController *controllers.ComponentController, statisticsController *controllers.StatisticsController, attestationController *controllers.AttestationController, @@ -50,8 +49,7 @@ func NewAssetVersionRouter( assetVersionRouter.GET("/vex.json/", assetVersionController.VEXJSON) assetVersionRouter.GET("/sbom.json/", assetVersionController.SBOMJSON) assetVersionRouter.GET("/", assetVersionController.Read) - assetVersionRouter.GET("/compliance/", complianceController.AssetCompliance) - assetVersionRouter.GET("/compliance/:policy/", complianceController.Details) + assetVersionRouter.GET("/metrics/", assetVersionController.Metrics) assetVersionRouter.GET("/components/licenses/", componentController.LicenseDistribution) assetVersionRouter.GET("/affected-components/", assetVersionController.AffectedComponents) diff --git a/router/compliance_risk_router.go b/router/compliance_risk_router.go index f693511d6..fc0da0544 100644 --- a/router/compliance_risk_router.go +++ b/router/compliance_risk_router.go @@ -20,7 +20,7 @@ func NewComplianceRiskRouter( complianceRisksRouter.POST("/:complianceRiskID/", controller.CreateEvent, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) complianceRisksRouter.POST("/:complianceRiskID/mitigate/", controller.Mitigate, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) - complianceRisksRouter.POST("/recalculate/", controller.RecalculateFromService, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) + complianceRisksRouter.POST("/evaluate/", controller.EvaluateArtifactCompliance, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) complianceRisksRouter.POST("/upload-zip/", controller.UploadZip, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) return ComplianceRiskRouter{Group: complianceRisksRouter} diff --git a/router/org_router.go b/router/org_router.go index eccc375e0..fb2871913 100644 --- a/router/org_router.go +++ b/router/org_router.go @@ -36,7 +36,6 @@ func NewOrgRouter( dependencyProxyController *dependencyfirewall.DependencyProxyController, dependencyVulnController *controllers.DependencyVulnController, firstPartyVulnController *controllers.FirstPartyVulnController, - policyController *controllers.PolicyController, integrationController *controllers.IntegrationController, webhookIntegration *controllers.WebhookController, externalEntityProviderService shared.ExternalEntityProviderService, @@ -75,8 +74,6 @@ func NewOrgRouter( organizationRouter.GET("/content-tree/", orgController.ContentTree) organizationRouter.GET("/dependency-vulns/", dependencyVulnController.ListByOrgPaged) organizationRouter.GET("/first-party-vulns/", firstPartyVulnController.ListByOrgPaged) - organizationRouter.GET("/policies/", policyController.GetOrganizationPolicies) - organizationRouter.GET("/policies/:policyID/", policyController.GetPolicy) organizationRouter.GET("/members/", orgController.Members) organizationRouter.GET("/integrations/finish-installation/", integrationController.FinishInstallation) organizationRouter.GET("/projects/", projectController.List) @@ -89,18 +86,14 @@ func NewOrgRouter( organizationUpdateAccessControlRequired.POST("/integrations/jira/test-and-save/", integrationController.TestAndSaveJiraIntegration) organizationUpdateAccessControlRequired.POST("/integrations/webhook/test-and-save/", webhookIntegration.Save) organizationUpdateAccessControlRequired.POST("/integrations/webhook/test/", webhookIntegration.Test) - organizationUpdateAccessControlRequired.POST("/policies/", policyController.CreatePolicy) - organizationUpdateAccessControlRequired.POST("/integrations/gitlab/test-and-save/", integrationController.TestAndSaveGitlabIntegration) organizationUpdateAccessControlRequired.POST("/projects/", projectController.Create) - organizationUpdateAccessControlRequired.DELETE("/policies/:policyID/", policyController.DeletePolicy) organizationUpdateAccessControlRequired.DELETE("/integrations/gitlab/:gitlab_integration_id/", integrationController.DeleteGitLabAccessToken) organizationUpdateAccessControlRequired.DELETE("/members/:userID/", orgController.RemoveMember) organizationUpdateAccessControlRequired.DELETE("/integrations/jira/:jira_integration_id/", integrationController.DeleteJiraAccessToken) organizationUpdateAccessControlRequired.DELETE("/integrations/webhook/:id/", webhookIntegration.Delete) organizationUpdateAccessControlRequired.PATCH("/", orgController.Update) - organizationUpdateAccessControlRequired.PUT("/policies/:policyID/", policyController.UpdatePolicy) organizationUpdateAccessControlRequired.PUT("/members/:userID/", orgController.ChangeRole) organizationUpdateAccessControlRequired.PUT("/integrations/webhook/:id/", webhookIntegration.Update) diff --git a/router/project_router.go b/router/project_router.go index d3f6ba6b8..86cda60e7 100644 --- a/router/project_router.go +++ b/router/project_router.go @@ -33,7 +33,6 @@ func NewProjectRouter( assetController *controllers.AssetController, dependencyProxyController *dependencyfirewall.DependencyProxyController, dependencyVulnController *controllers.DependencyVulnController, - policyController *controllers.PolicyController, releaseController *controllers.ReleaseController, statisticsController *controllers.StatisticsController, webhookIntegration *controllers.WebhookController, @@ -49,7 +48,6 @@ func NewProjectRouter( projectRouter := organizationGroup.Group.Group("/projects/:projectSlug", projectScopedRBAC(shared.ObjectProject, shared.ActionRead)) projectRouter.GET("/", projectController.Read) projectRouter.GET("/resources/", projectController.ListSubProjectsAndAssets) - projectRouter.GET("/policies/", policyController.GetProjectPolicies) projectRouter.GET("/dependency-vulns/", dependencyVulnController.ListByProjectPaged) projectRouter.GET("/assets/", assetController.List) projectRouter.GET("/members/", projectController.Members) @@ -79,14 +77,12 @@ func NewProjectRouter( projectUpdateAccessControlRequired.POST("/releases/:releaseID/items/", releaseController.AddItem) projectUpdateAccessControlRequired.DELETE("/integrations/webhook/:id/", webhookIntegration.Delete) - projectUpdateAccessControlRequired.DELETE("/policies/:policyID/", policyController.DisablePolicyForProject) projectUpdateAccessControlRequired.DELETE("/", projectController.Delete) projectUpdateAccessControlRequired.DELETE("/members/:userID/", projectController.RemoveMember) projectUpdateAccessControlRequired.DELETE("/releases/:releaseID/", releaseController.Delete) projectUpdateAccessControlRequired.DELETE("/releases/:releaseID/items/:itemID/", releaseController.RemoveItem) projectUpdateAccessControlRequired.PUT("/integrations/webhook/:id/", webhookIntegration.Update) - projectUpdateAccessControlRequired.PUT("/policies/:policyID/", policyController.EnablePolicyForProject) projectUpdateAccessControlRequired.PATCH("/", projectController.Update) projectUpdateAccessControlRequired.PUT("/members/:userID/", projectController.ChangeRole) projectUpdateAccessControlRequired.PATCH("/releases/:releaseID/", releaseController.Update) diff --git a/router/router_test.go b/router/router_test.go index d7e8ac462..a0882b0ef 100644 --- a/router/router_test.go +++ b/router/router_test.go @@ -84,15 +84,15 @@ var intentionallyPublicPaths = map[string]bool{ // Key format: "METHOD /full/echo/path/template/" var memberOnlyPaths = map[string]bool{ // Vuln triage actions — any member who can read the asset version may triage findings. - "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/dependency-vulns/sync/": true, - "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/dependency-vulns/batch/": true, - "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/dependency-vulns/:dependencyVulnID/": true, - "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/dependency-vulns/:dependencyVulnID/mitigate/": true, - "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/first-party-vulns/:firstPartyVulnID/": true, - "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/first-party-vulns/:firstPartyVulnID/mitigate/": true, - "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/license-risks/": true, - "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/license-risks/:licenseRiskID/": true, - "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/license-risks/:licenseRiskID/mitigate/": true, + "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/dependency-vulns/sync/": true, + "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/dependency-vulns/batch/": true, + "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/dependency-vulns/:dependencyVulnID/": true, + "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/dependency-vulns/:dependencyVulnID/mitigate/": true, + "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/first-party-vulns/:firstPartyVulnID/": true, + "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/first-party-vulns/:firstPartyVulnID/mitigate/": true, + "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/license-risks/": true, + "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/license-risks/:licenseRiskID/": true, + "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/license-risks/:licenseRiskID/mitigate/": true, "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/license-risks/:licenseRiskID/final-license-decision/": true, // VEX rules — any member may create/edit/delete VEX rules (membership = passed read RBAC). "POST /api/v1/organizations/:organization/projects/:project/assets/:assetSlug/refs/:assetVersionSlug/vex-rules/": true, @@ -219,7 +219,6 @@ func buildSecurityTestServer(t *testing.T, ac *mocks.AccessControl) *echo.Echo { new(dependencyfirewall.DependencyProxyController), new(controllers.DependencyVulnController), new(controllers.FirstPartyVulnController), - new(controllers.PolicyController), new(controllers.IntegrationController), new(controllers.WebhookController), extEntityService, @@ -235,12 +234,10 @@ func buildSecurityTestServer(t *testing.T, ac *mocks.AccessControl) *echo.Echo { new(controllers.AssetController), new(dependencyfirewall.DependencyProxyController), new(controllers.DependencyVulnController), - new(controllers.PolicyController), new(controllers.ReleaseController), new(controllers.StatisticsController), new(controllers.WebhookController), projectRepo, - new(controllers.ComponentController), ) assetRouter := NewAssetRouter( @@ -248,7 +245,6 @@ func buildSecurityTestServer(t *testing.T, ac *mocks.AccessControl) *echo.Echo { new(controllers.AssetController), new(dependencyfirewall.DependencyProxyController), new(controllers.AssetVersionController), - new(controllers.ComplianceController), new(controllers.StatisticsController), new(controllers.ComponentController), new(controllers.InToToController), @@ -261,7 +257,6 @@ func buildSecurityTestServer(t *testing.T, ac *mocks.AccessControl) *echo.Echo { assetRouter, new(controllers.AssetVersionController), new(controllers.FirstPartyVulnController), - new(controllers.ComplianceController), new(controllers.ComponentController), new(controllers.StatisticsController), new(controllers.AttestationController), diff --git a/services/compliance_risk_service.go b/services/compliance_risk_service.go index 5c76448b0..0250ed050 100644 --- a/services/compliance_risk_service.go +++ b/services/compliance_risk_service.go @@ -7,6 +7,7 @@ import ( "github.com/l3montree-dev/devguard/database/models" "github.com/l3montree-dev/devguard/dtos" + "github.com/l3montree-dev/devguard/dtos/sarif" "github.com/l3montree-dev/devguard/shared" "github.com/l3montree-dev/devguard/statemachine" "github.com/l3montree-dev/devguard/utils" @@ -26,34 +27,16 @@ func NewComplianceRiskService(complianceRiskRepository shared.ComplianceRiskRepo } } -// HandleArtifactCompliance processes policy evaluations for an artifact and manages the +// HandleArtifactCompliance processes a SARIF compliance report for an artifact and manages the // lifecycle of compliance risks: new detections, branch-diffing, artifact association, and fixes. -func (s *ComplianceRiskService) HandleArtifactCompliance(ctx context.Context, tx shared.DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, evaluations []dtos.PolicyEvaluationDTO) error { +func (s *ComplianceRiskService) HandleArtifactCompliance(ctx context.Context, tx shared.DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, sarifDoc sarif.SarifSchema210Json) error { // fetch all existing compliance risks for this asset version (across all artifacts) existingRisks, err := s.complianceRiskRepository.GetAllComplianceRisksForAssetVersion(ctx, tx, assetVersion.AssetID, assetVersion.Name) if err != nil { return err } - // build risks for every evaluation — compliant ones start fixed, non-compliant ones open - foundRisks := make([]models.ComplianceRisk, 0, len(evaluations)) - for _, eval := range evaluations { - foundRisks = append(foundRisks, models.ComplianceRisk{ - Vulnerability: models.Vulnerability{ - AssetVersionName: assetVersion.Name, - AssetID: assetVersion.AssetID, - AssetVersion: assetVersion, - State: eval.State, - LastDetected: time.Now(), - }, - PolicyID: eval.PolicyID, - PolicyTitle: eval.PolicyTitle, - PolicyDescription: eval.PolicyDescription, - PredicateType: eval.PredicateType, - AttestationViolations: eval.AttestationViolations, - AttestationUpdatedAt: eval.AttestationUpdatedAt, - }) - } + foundRisks := sarifToComplianceRisks(sarifDoc, assetVersion) // compare found risks with existing ones using hash-based identity comparison := utils.CompareSlices(foundRisks, existingRisks, func(r models.ComplianceRisk) string { @@ -254,3 +237,144 @@ func (s *ComplianceRiskService) updateComplianceRiskState(ctx context.Context, t err := s.complianceRiskRepository.ApplyAndSave(ctx, tx, risk, &ev) return ev, err } + +// sarifToComplianceRisks converts a SARIF document into ComplianceRisk models for the given asset version. +// Each SARIF rule becomes one risk; its state is derived from the result kinds (pass/fail/open). +func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion models.AssetVersion) []models.ComplianceRisk { + if len(sarifDoc.Runs) == 0 { + return nil + } + run := sarifDoc.Runs[0] + + type ruleInfo struct { + title string + description *string + predicateType string + relatedResources []string + tags []string + priority int + complianceFrameworks []string + } + ruleMap := make(map[string]ruleInfo, len(run.Tool.Driver.Rules)) + for _, rule := range run.Tool.Driver.Rules { + var desc *string + if rule.FullDescription != nil && rule.FullDescription.Text != "" { + d := rule.FullDescription.Text + desc = &d + } + var predicateType string + if rule.Properties != nil { + if pt, ok := rule.Properties.AdditionalProperties["predicateType"].(string); ok { + predicateType = pt + } + } + title := rule.ID + if rule.ShortDescription != nil { + title = rule.ShortDescription.Text + } + + relatedResources := make([]string, 0) + if rule.Properties != nil { + if rr, ok := rule.Properties.AdditionalProperties["relatedResources"].([]any); ok { + for _, r := range rr { + if rStr, ok := r.(string); ok { + relatedResources = append(relatedResources, rStr) + } + } + } + } + + var tags []string + if rule.Properties != nil { + tags = rule.Properties.Tags + } + + var complianceFrameworks []string + if rule.Properties != nil { + if cf, ok := rule.Properties.AdditionalProperties["complianceFrameworks"].([]any); ok { + for _, c := range cf { + if cStr, ok := c.(string); ok { + complianceFrameworks = append(complianceFrameworks, cStr) + } + } + } + } + + var priority int + if rule.Properties != nil { + if p, ok := rule.Properties.AdditionalProperties["priority"].(int); ok { + priority = p + } else if pFloat, ok := rule.Properties.AdditionalProperties["priority"].(float64); ok { + priority = int(pFloat) + } + } + + ruleMap[rule.ID] = ruleInfo{title: title, description: desc, predicateType: predicateType, relatedResources: relatedResources, tags: tags, priority: priority, complianceFrameworks: complianceFrameworks} + } + + type policyResult struct { + kind sarif.ResultKind + violations []string + } + resultMap := make(map[string]*policyResult, len(ruleMap)) + + for _, result := range run.Results { + if result.RuleID == nil { + continue + } + ruleID := *result.RuleID + pr := resultMap[ruleID] + if pr == nil { + pr = &policyResult{} + resultMap[ruleID] = pr + } + + switch result.Kind { + case sarif.ResultKindFail: + pr.kind = sarif.ResultKindFail + pr.violations = append(pr.violations, result.Message.Text) + case sarif.ResultKindOpen: + if pr.kind != sarif.ResultKindFail { + pr.kind = sarif.ResultKindOpen + } + case sarif.ResultKindPass: + if pr.kind == "" { + pr.kind = sarif.ResultKindPass + } + } + + } + + risks := make([]models.ComplianceRisk, 0, len(ruleMap)) + for ruleID, info := range ruleMap { + state := dtos.VulnStateOpen + var violations []string + + if pr := resultMap[ruleID]; pr != nil { + switch pr.kind { + case sarif.ResultKindPass: + state = dtos.VulnStateFixed + case sarif.ResultKindFail: + state = dtos.VulnStateOpen + violations = pr.violations + } + } + + risks = append(risks, models.ComplianceRisk{ + Vulnerability: models.Vulnerability{ + AssetVersionName: assetVersion.Name, + AssetID: assetVersion.AssetID, + AssetVersion: assetVersion, + State: state, + LastDetected: time.Now(), + }, + PolicyID: ruleID, + PolicyTitle: info.title, + PolicyDescription: info.description, + PredicateType: info.predicateType, + AttestationViolations: violations, + }) + } + + return risks +} diff --git a/services/compliance_service.go b/services/compliance_service.go index a93232f2f..87335db24 100644 --- a/services/compliance_service.go +++ b/services/compliance_service.go @@ -20,33 +20,29 @@ import ( "github.com/google/uuid" "github.com/l3montree-dev/devguard/compliance" "github.com/l3montree-dev/devguard/database/models" - "github.com/l3montree-dev/devguard/dtos" + "github.com/l3montree-dev/devguard/dtos/sarif" "github.com/l3montree-dev/devguard/shared" - "github.com/l3montree-dev/devguard/transformer" - "github.com/l3montree-dev/devguard/utils" ) type ComplianceService struct { attestationRepository shared.AttestationRepository - policyRepository shared.PolicyRepository } -func NewComplianceService(attestationRepository shared.AttestationRepository, policyRepository shared.PolicyRepository) *ComplianceService { +func NewComplianceService(attestationRepository shared.AttestationRepository) *ComplianceService { return &ComplianceService{ attestationRepository: attestationRepository, - policyRepository: policyRepository, } } -func (s *ComplianceService) ArtifactCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) ([]dtos.PolicyEvaluationDTO, error) { +func (s *ComplianceService) ArtifactCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) (sarif.SarifSchema210Json, error) { attestations, err := s.attestationRepository.GetByArtifactAndAssetVersionAndAssetID(ctx, nil, artifact.ArtifactName, assetVersion.Name, assetVersion.AssetID) if err != nil { - return nil, err + return sarif.SarifSchema210Json{}, err } - policies, err := s.policyRepository.FindCommunityManagedPolicies(ctx, nil) + policies, err := compliance.GetPoliciesFromFS("attestation-compliance-policies/policies") if err != nil { - return nil, err + return sarif.SarifSchema210Json{}, err } evals := make([]compliance.PolicyEvaluation, 0, len(policies)) @@ -57,12 +53,11 @@ foundMatch: continue } eval := compliance.Eval(policy, attestation.Content) - eval.AttestationUpdatedAt = &attestation.UpdatedAt evals = append(evals, eval) continue foundMatch } evals = append(evals, compliance.Eval(policy, nil)) } - return utils.Map(evals, transformer.PolicyEvaluationToDTO), nil + return compliance.BuildSarifFromPolicies("", evals), nil } diff --git a/shared/common_interfaces.go b/shared/common_interfaces.go index 11fd09824..04fa4ff2c 100644 --- a/shared/common_interfaces.go +++ b/shared/common_interfaces.go @@ -97,8 +97,6 @@ type ProjectRepository interface { GetByProjectIDs(ctx context.Context, tx DB, projectIDs []uuid.UUID) ([]models.Project, error) List(ctx context.Context, tx DB, idSlice []uuid.UUID, parentID *uuid.UUID, organizationID uuid.UUID) ([]models.Project, error) ListPaged(ctx context.Context, tx DB, projectIDs []uuid.UUID, parentID *uuid.UUID, orgID uuid.UUID, pageInfo PageInfo, search string, filter []FilterQuery, sort []SortQuery) (Paged[models.Project], error) - EnablePolicyForProject(ctx context.Context, tx DB, projectID uuid.UUID, policyID uuid.UUID) error - DisablePolicyForProject(ctx context.Context, tx DB, projectID uuid.UUID, policyID uuid.UUID) error Upsert(ctx context.Context, tx DB, projects *[]*models.Project, conflictingColumns []clause.Column, toUpdate []string) error EnableCommunityManagedPolicies(ctx context.Context, tx DB, projectID uuid.UUID) error UpsertSplit(ctx context.Context, tx DB, externalProviderID string, projects []*models.Project) ([]*models.Project, []*models.Project, error) @@ -111,13 +109,6 @@ type Verifier interface { VerifyRequestSignature(ctx context.Context, req *http.Request) (string, string, error) } -type PolicyRepository interface { - utils.Repository[uuid.UUID, models.Policy, DB] - FindByProjectID(ctx context.Context, tx DB, projectID uuid.UUID) ([]models.Policy, error) - FindByOrganizationID(ctx context.Context, tx DB, organizationID uuid.UUID) ([]models.Policy, error) - 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) @@ -179,7 +170,7 @@ type ArtifactRepository interface { } type ComplianceService interface { - ArtifactCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) ([]dtos.PolicyEvaluationDTO, error) + ArtifactCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) (sarif.SarifSchema210Json, error) } type ReleaseRepository interface { @@ -308,7 +299,7 @@ type ComplianceRiskRepository interface { } type ComplianceRiskService interface { - HandleArtifactCompliance(ctx context.Context, tx DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, evaluations []dtos.PolicyEvaluationDTO) error + HandleArtifactCompliance(ctx context.Context, tx DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, sarifDoc sarif.SarifSchema210Json) error UpdateComplianceRiskState(ctx context.Context, tx DB, userID string, risk *models.ComplianceRisk, statusType string, justification string, mechanicalJustification dtos.MechanicalJustificationType, userAgent *string) (models.VulnEvent, error) } diff --git a/tests/project_controller_test.go b/tests/project_controller_test.go index cc4355fc0..1bf86b985 100644 --- a/tests/project_controller_test.go +++ b/tests/project_controller_test.go @@ -25,14 +25,6 @@ func TestProjectCreation(t *testing.T) { t.Run("should enable all community policies by default", func(t *testing.T) { e := echo.New() rec := httptest.NewRecorder() - // create a community policy - communityPolicy := models.Policy{ - Title: "Community Policy 1", - Description: "This is a community policy", - OrganizationID: nil, // nil means it's a community policy - } - - assert.Nil(t, f.DB.Create(&communityPolicy).Error) requestBody := map[string]string{ "name": "new-project", @@ -81,7 +73,7 @@ func TestProjectCreation(t *testing.T) { err = f.DB.Preload("EnabledPolicies").First(&createdProject, "slug = ?", requestBody["name"]).Error assert.Nil(t, err) - assert.Len(t, createdProject.EnabledPolicies, 1) + }) t.Run("should generate a unique slug", func(t *testing.T) { diff --git a/transformer/compliance_risk_transformer.go b/transformer/compliance_risk_transformer.go index b6122acbb..9afd026ad 100644 --- a/transformer/compliance_risk_transformer.go +++ b/transformer/compliance_risk_transformer.go @@ -16,20 +16,24 @@ func ComplianceRiskToDTO(r models.ComplianceRisk) dtos.ComplianceRiskDTO { } return dtos.ComplianceRiskDTO{ - ID: r.ID, - AssetVersionName: r.AssetVersionName, - AssetID: r.AssetID.String(), - State: r.State, - CreatedAt: r.CreatedAt, - TicketID: r.TicketID, - TicketURL: r.TicketURL, - ManualTicketCreation: r.ManualTicketCreation, - PolicyID: r.PolicyID, - PolicyTitle: r.PolicyTitle, - PolicyDescription: r.PolicyDescription, - PredicateType: r.PredicateType, - AttestationViolations: r.AttestationViolations, - AttestationUpdatedAt: r.AttestationUpdatedAt, - Artifacts: artifacts, + ID: r.ID, + AssetVersionName: r.AssetVersionName, + AssetID: r.AssetID.String(), + State: r.State, + CreatedAt: r.CreatedAt, + TicketID: r.TicketID, + TicketURL: r.TicketURL, + ManualTicketCreation: r.ManualTicketCreation, + PolicyID: r.PolicyID, + PolicyTitle: r.PolicyTitle, + PolicyDescription: r.PolicyDescription, + PolicyRelatedResources: r.PolicyRelatedResources, + PolicyTags: r.PolicyTags, + PolicyPriority: r.PolicyPriority, + ComplianceFrameworks: r.ComplianceFrameworks, + PredicateType: r.PredicateType, + AttestationContent: r.AttestationContent, + AttestationViolations: r.AttestationViolations, + Artifacts: artifacts, } } diff --git a/transformer/policy_transformer.go b/transformer/policy_transformer.go deleted file mode 100644 index 213b9e092..000000000 --- a/transformer/policy_transformer.go +++ /dev/null @@ -1,42 +0,0 @@ -// 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 transformer - -import ( - "github.com/l3montree-dev/devguard/compliance" - "github.com/l3montree-dev/devguard/dtos" -) - -func PolicyEvaluationToDTO(e compliance.PolicyEvaluation) dtos.PolicyEvaluationDTO { - state := dtos.VulnStateOpen - if e.Compliant != nil && *e.Compliant { - state = dtos.VulnStateFixed - } - var desc *string - if e.Policy.Description != "" { - d := e.Policy.Description - desc = &d - } - return dtos.PolicyEvaluationDTO{ - PolicyID: e.Policy.ID.String(), - PolicyTitle: e.Policy.Title, - PolicyDescription: desc, - State: state, - PredicateType: e.Policy.PredicateType, - AttestationViolations: e.Violations, - AttestationUpdatedAt: e.AttestationUpdatedAt, - } -} From eb6af0e67f915c3fab5f37e6cfc3ac7ff74a5e6f Mon Sep 17 00:00:00 2001 From: rafi Date: Tue, 9 Jun 2026 09:46:06 +0200 Subject: [PATCH 13/26] fix mocks Signed-off-by: rafi --- mocks/mock_ComplianceRiskService.go | 23 ++--- mocks/mock_ComplianceService.go | 20 ++-- mocks/mock_ProjectRepository.go | 138 ---------------------------- 3 files changed, 21 insertions(+), 160 deletions(-) diff --git a/mocks/mock_ComplianceRiskService.go b/mocks/mock_ComplianceRiskService.go index d555a3611..8080ba3d0 100644 --- a/mocks/mock_ComplianceRiskService.go +++ b/mocks/mock_ComplianceRiskService.go @@ -9,6 +9,7 @@ import ( "github.com/l3montree-dev/devguard/database/models" "github.com/l3montree-dev/devguard/dtos" + "github.com/l3montree-dev/devguard/dtos/sarif" "github.com/l3montree-dev/devguard/shared" mock "github.com/stretchr/testify/mock" ) @@ -41,16 +42,16 @@ func (_m *ComplianceRiskService) EXPECT() *ComplianceRiskService_Expecter { } // HandleArtifactCompliance provides a mock function for the type ComplianceRiskService -func (_mock *ComplianceRiskService) HandleArtifactCompliance(ctx context.Context, tx shared.DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, evaluations []dtos.PolicyEvaluationDTO) error { - ret := _mock.Called(ctx, tx, userID, userAgent, assetVersion, artifact, evaluations) +func (_mock *ComplianceRiskService) HandleArtifactCompliance(ctx context.Context, tx shared.DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, sarifDoc sarif.SarifSchema210Json) error { + ret := _mock.Called(ctx, tx, userID, userAgent, assetVersion, artifact, sarifDoc) if len(ret) == 0 { panic("no return value specified for HandleArtifactCompliance") } var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, string, *string, models.AssetVersion, models.Artifact, []dtos.PolicyEvaluationDTO) error); ok { - r0 = returnFunc(ctx, tx, userID, userAgent, assetVersion, artifact, evaluations) + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, string, *string, models.AssetVersion, models.Artifact, sarif.SarifSchema210Json) error); ok { + r0 = returnFunc(ctx, tx, userID, userAgent, assetVersion, artifact, sarifDoc) } else { r0 = ret.Error(0) } @@ -69,12 +70,12 @@ type ComplianceRiskService_HandleArtifactCompliance_Call struct { // - userAgent *string // - assetVersion models.AssetVersion // - artifact models.Artifact -// - evaluations []dtos.PolicyEvaluationDTO -func (_e *ComplianceRiskService_Expecter) HandleArtifactCompliance(ctx interface{}, tx interface{}, userID interface{}, userAgent interface{}, assetVersion interface{}, artifact interface{}, evaluations interface{}) *ComplianceRiskService_HandleArtifactCompliance_Call { - return &ComplianceRiskService_HandleArtifactCompliance_Call{Call: _e.mock.On("HandleArtifactCompliance", ctx, tx, userID, userAgent, assetVersion, artifact, evaluations)} +// - sarifDoc sarif.SarifSchema210Json +func (_e *ComplianceRiskService_Expecter) HandleArtifactCompliance(ctx interface{}, tx interface{}, userID interface{}, userAgent interface{}, assetVersion interface{}, artifact interface{}, sarifDoc interface{}) *ComplianceRiskService_HandleArtifactCompliance_Call { + return &ComplianceRiskService_HandleArtifactCompliance_Call{Call: _e.mock.On("HandleArtifactCompliance", ctx, tx, userID, userAgent, assetVersion, artifact, sarifDoc)} } -func (_c *ComplianceRiskService_HandleArtifactCompliance_Call) Run(run func(ctx context.Context, tx shared.DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, evaluations []dtos.PolicyEvaluationDTO)) *ComplianceRiskService_HandleArtifactCompliance_Call { +func (_c *ComplianceRiskService_HandleArtifactCompliance_Call) Run(run func(ctx context.Context, tx shared.DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, sarifDoc sarif.SarifSchema210Json)) *ComplianceRiskService_HandleArtifactCompliance_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -100,9 +101,9 @@ func (_c *ComplianceRiskService_HandleArtifactCompliance_Call) Run(run func(ctx if args[5] != nil { arg5 = args[5].(models.Artifact) } - var arg6 []dtos.PolicyEvaluationDTO + var arg6 sarif.SarifSchema210Json if args[6] != nil { - arg6 = args[6].([]dtos.PolicyEvaluationDTO) + arg6 = args[6].(sarif.SarifSchema210Json) } run( arg0, @@ -122,7 +123,7 @@ func (_c *ComplianceRiskService_HandleArtifactCompliance_Call) Return(err error) return _c } -func (_c *ComplianceRiskService_HandleArtifactCompliance_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, evaluations []dtos.PolicyEvaluationDTO) error) *ComplianceRiskService_HandleArtifactCompliance_Call { +func (_c *ComplianceRiskService_HandleArtifactCompliance_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, userID string, userAgent *string, assetVersion models.AssetVersion, artifact models.Artifact, sarifDoc sarif.SarifSchema210Json) error) *ComplianceRiskService_HandleArtifactCompliance_Call { _c.Call.Return(run) return _c } diff --git a/mocks/mock_ComplianceService.go b/mocks/mock_ComplianceService.go index 49f7649e7..d3ed9263f 100644 --- a/mocks/mock_ComplianceService.go +++ b/mocks/mock_ComplianceService.go @@ -9,7 +9,7 @@ import ( "github.com/google/uuid" "github.com/l3montree-dev/devguard/database/models" - "github.com/l3montree-dev/devguard/dtos" + "github.com/l3montree-dev/devguard/dtos/sarif" mock "github.com/stretchr/testify/mock" ) @@ -41,24 +41,22 @@ func (_m *ComplianceService) EXPECT() *ComplianceService_Expecter { } // ArtifactCompliance provides a mock function for the type ComplianceService -func (_mock *ComplianceService) ArtifactCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) ([]dtos.PolicyEvaluationDTO, error) { +func (_mock *ComplianceService) ArtifactCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) (sarif.SarifSchema210Json, error) { ret := _mock.Called(ctx, projectID, assetVersion, artifact) if len(ret) == 0 { panic("no return value specified for ArtifactCompliance") } - var r0 []dtos.PolicyEvaluationDTO + var r0 sarif.SarifSchema210Json var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, models.AssetVersion, models.Artifact) ([]dtos.PolicyEvaluationDTO, error)); ok { + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, models.AssetVersion, models.Artifact) (sarif.SarifSchema210Json, error)); ok { return returnFunc(ctx, projectID, assetVersion, artifact) } - if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, models.AssetVersion, models.Artifact) []dtos.PolicyEvaluationDTO); ok { + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, models.AssetVersion, models.Artifact) sarif.SarifSchema210Json); ok { r0 = returnFunc(ctx, projectID, assetVersion, artifact) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]dtos.PolicyEvaluationDTO) - } + r0 = ret.Get(0).(sarif.SarifSchema210Json) } if returnFunc, ok := ret.Get(1).(func(context.Context, uuid.UUID, models.AssetVersion, models.Artifact) error); ok { r1 = returnFunc(ctx, projectID, assetVersion, artifact) @@ -110,12 +108,12 @@ func (_c *ComplianceService_ArtifactCompliance_Call) Run(run func(ctx context.Co return _c } -func (_c *ComplianceService_ArtifactCompliance_Call) Return(policyEvaluationDTOs []dtos.PolicyEvaluationDTO, err error) *ComplianceService_ArtifactCompliance_Call { - _c.Call.Return(policyEvaluationDTOs, err) +func (_c *ComplianceService_ArtifactCompliance_Call) Return(sarifSchema210Json sarif.SarifSchema210Json, err error) *ComplianceService_ArtifactCompliance_Call { + _c.Call.Return(sarifSchema210Json, err) return _c } -func (_c *ComplianceService_ArtifactCompliance_Call) RunAndReturn(run func(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) ([]dtos.PolicyEvaluationDTO, error)) *ComplianceService_ArtifactCompliance_Call { +func (_c *ComplianceService_ArtifactCompliance_Call) RunAndReturn(run func(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) (sarif.SarifSchema210Json, error)) *ComplianceService_ArtifactCompliance_Call { _c.Call.Return(run) return _c } diff --git a/mocks/mock_ProjectRepository.go b/mocks/mock_ProjectRepository.go index 73ccd679f..131aa4d3f 100644 --- a/mocks/mock_ProjectRepository.go +++ b/mocks/mock_ProjectRepository.go @@ -299,75 +299,6 @@ func (_c *ProjectRepository_Delete_Call) RunAndReturn(run func(ctx context.Conte return _c } -// DisablePolicyForProject provides a mock function for the type ProjectRepository -func (_mock *ProjectRepository) DisablePolicyForProject(ctx context.Context, tx shared.DB, projectID uuid.UUID, policyID uuid.UUID) error { - ret := _mock.Called(ctx, tx, projectID, policyID) - - if len(ret) == 0 { - panic("no return value specified for DisablePolicyForProject") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID, uuid.UUID) error); ok { - r0 = returnFunc(ctx, tx, projectID, policyID) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// ProjectRepository_DisablePolicyForProject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisablePolicyForProject' -type ProjectRepository_DisablePolicyForProject_Call struct { - *mock.Call -} - -// DisablePolicyForProject is a helper method to define mock.On call -// - ctx context.Context -// - tx shared.DB -// - projectID uuid.UUID -// - policyID uuid.UUID -func (_e *ProjectRepository_Expecter) DisablePolicyForProject(ctx interface{}, tx interface{}, projectID interface{}, policyID interface{}) *ProjectRepository_DisablePolicyForProject_Call { - return &ProjectRepository_DisablePolicyForProject_Call{Call: _e.mock.On("DisablePolicyForProject", ctx, tx, projectID, policyID)} -} - -func (_c *ProjectRepository_DisablePolicyForProject_Call) Run(run func(ctx context.Context, tx shared.DB, projectID uuid.UUID, policyID uuid.UUID)) *ProjectRepository_DisablePolicyForProject_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) - } - var arg3 uuid.UUID - if args[3] != nil { - arg3 = args[3].(uuid.UUID) - } - run( - arg0, - arg1, - arg2, - arg3, - ) - }) - return _c -} - -func (_c *ProjectRepository_DisablePolicyForProject_Call) Return(err error) *ProjectRepository_DisablePolicyForProject_Call { - _c.Call.Return(err) - return _c -} - -func (_c *ProjectRepository_DisablePolicyForProject_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, projectID uuid.UUID, policyID uuid.UUID) error) *ProjectRepository_DisablePolicyForProject_Call { - _c.Call.Return(run) - return _c -} - // EnableCommunityManagedPolicies provides a mock function for the type ProjectRepository func (_mock *ProjectRepository) EnableCommunityManagedPolicies(ctx context.Context, tx shared.DB, projectID uuid.UUID) error { ret := _mock.Called(ctx, tx, projectID) @@ -431,75 +362,6 @@ func (_c *ProjectRepository_EnableCommunityManagedPolicies_Call) RunAndReturn(ru return _c } -// EnablePolicyForProject provides a mock function for the type ProjectRepository -func (_mock *ProjectRepository) EnablePolicyForProject(ctx context.Context, tx shared.DB, projectID uuid.UUID, policyID uuid.UUID) error { - ret := _mock.Called(ctx, tx, projectID, policyID) - - if len(ret) == 0 { - panic("no return value specified for EnablePolicyForProject") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID, uuid.UUID) error); ok { - r0 = returnFunc(ctx, tx, projectID, policyID) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// ProjectRepository_EnablePolicyForProject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnablePolicyForProject' -type ProjectRepository_EnablePolicyForProject_Call struct { - *mock.Call -} - -// EnablePolicyForProject is a helper method to define mock.On call -// - ctx context.Context -// - tx shared.DB -// - projectID uuid.UUID -// - policyID uuid.UUID -func (_e *ProjectRepository_Expecter) EnablePolicyForProject(ctx interface{}, tx interface{}, projectID interface{}, policyID interface{}) *ProjectRepository_EnablePolicyForProject_Call { - return &ProjectRepository_EnablePolicyForProject_Call{Call: _e.mock.On("EnablePolicyForProject", ctx, tx, projectID, policyID)} -} - -func (_c *ProjectRepository_EnablePolicyForProject_Call) Run(run func(ctx context.Context, tx shared.DB, projectID uuid.UUID, policyID uuid.UUID)) *ProjectRepository_EnablePolicyForProject_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) - } - var arg3 uuid.UUID - if args[3] != nil { - arg3 = args[3].(uuid.UUID) - } - run( - arg0, - arg1, - arg2, - arg3, - ) - }) - return _c -} - -func (_c *ProjectRepository_EnablePolicyForProject_Call) Return(err error) *ProjectRepository_EnablePolicyForProject_Call { - _c.Call.Return(err) - return _c -} - -func (_c *ProjectRepository_EnablePolicyForProject_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, projectID uuid.UUID, policyID uuid.UUID) error) *ProjectRepository_EnablePolicyForProject_Call { - _c.Call.Return(run) - return _c -} - // GetByOrgID provides a mock function for the type ProjectRepository func (_mock *ProjectRepository) GetByOrgID(ctx context.Context, tx shared.DB, organizationID uuid.UUID) ([]models.Project, error) { ret := _mock.Called(ctx, tx, organizationID) From 90783b95fb08ce2fbe1d5f2af66f821e787a2195 Mon Sep 17 00:00:00 2001 From: rafi Date: Tue, 9 Jun 2026 10:01:19 +0200 Subject: [PATCH 14/26] remove policy layer mocks and update compliance risk migration Signed-off-by: rafi --- ...add_compliance_risks_drop_policies.up.sql} | 9 + mocks/mock_ExternalPropertyFileReference.go | 36 - mocks/mock_PolicyRepository.go | 1271 ----------------- mocks/mock_ReportingDescriptorReference.go | 36 - mocks/mock_VulnDBImportService.go | 260 ---- router/router_test.go | 1 + 6 files changed, 10 insertions(+), 1603 deletions(-) rename database/migrations/{20260602000000_add_compliance_risks.up.sql => 20260602000000_add_compliance_risks_drop_policies.up.sql} (90%) delete mode 100644 mocks/mock_ExternalPropertyFileReference.go delete mode 100644 mocks/mock_PolicyRepository.go delete mode 100644 mocks/mock_ReportingDescriptorReference.go delete mode 100644 mocks/mock_VulnDBImportService.go diff --git a/database/migrations/20260602000000_add_compliance_risks.up.sql b/database/migrations/20260602000000_add_compliance_risks_drop_policies.up.sql similarity index 90% rename from database/migrations/20260602000000_add_compliance_risks.up.sql rename to database/migrations/20260602000000_add_compliance_risks_drop_policies.up.sql index a5d01bf2f..d1799b306 100644 --- a/database/migrations/20260602000000_add_compliance_risks.up.sql +++ b/database/migrations/20260602000000_add_compliance_risks_drop_policies.up.sql @@ -1,3 +1,7 @@ +-- Drop policy tables (data is now embedded in compliance_risks) +DROP TABLE IF EXISTS public.project_enabled_policies; +DROP TABLE IF EXISTS public.policies; + CREATE TABLE IF NOT EXISTS public.compliance_risks ( id uuid NOT NULL, asset_version_name text NOT NULL, @@ -15,6 +19,11 @@ CREATE TABLE IF NOT EXISTS public.compliance_risks ( policy_title text NOT NULL DEFAULT '', policy_description text, predicate_type text NOT NULL DEFAULT '', + policy_related_resources text[], + policy_tags text[], + policy_priority integer, + compliance_frameworks text[], + attestation_content text, attestation_violations text[], attestation_updated_at timestamp with time zone, CONSTRAINT compliance_risks_pkey PRIMARY KEY (id), diff --git a/mocks/mock_ExternalPropertyFileReference.go b/mocks/mock_ExternalPropertyFileReference.go deleted file mode 100644 index a2163049d..000000000 --- a/mocks/mock_ExternalPropertyFileReference.go +++ /dev/null @@ -1,36 +0,0 @@ -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package mocks - -import ( - mock "github.com/stretchr/testify/mock" -) - -// NewExternalPropertyFileReference creates a new instance of ExternalPropertyFileReference. 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 NewExternalPropertyFileReference(t interface { - mock.TestingT - Cleanup(func()) -}) *ExternalPropertyFileReference { - mock := &ExternalPropertyFileReference{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// ExternalPropertyFileReference is an autogenerated mock type for the ExternalPropertyFileReference type -type ExternalPropertyFileReference struct { - mock.Mock -} - -type ExternalPropertyFileReference_Expecter struct { - mock *mock.Mock -} - -func (_m *ExternalPropertyFileReference) EXPECT() *ExternalPropertyFileReference_Expecter { - return &ExternalPropertyFileReference_Expecter{mock: &_m.Mock} -} diff --git a/mocks/mock_PolicyRepository.go b/mocks/mock_PolicyRepository.go deleted file mode 100644 index 0f5fee02e..000000000 --- a/mocks/mock_PolicyRepository.go +++ /dev/null @@ -1,1271 +0,0 @@ -// 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" -) - -// NewPolicyRepository creates a new instance of PolicyRepository. 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 NewPolicyRepository(t interface { - mock.TestingT - Cleanup(func()) -}) *PolicyRepository { - mock := &PolicyRepository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// PolicyRepository is an autogenerated mock type for the PolicyRepository type -type PolicyRepository struct { - mock.Mock -} - -type PolicyRepository_Expecter struct { - mock *mock.Mock -} - -func (_m *PolicyRepository) EXPECT() *PolicyRepository_Expecter { - return &PolicyRepository_Expecter{mock: &_m.Mock} -} - -// Activate provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) 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 -} - -// PolicyRepository_Activate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Activate' -type PolicyRepository_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 *PolicyRepository_Expecter) Activate(ctx interface{}, tx interface{}, id interface{}) *PolicyRepository_Activate_Call { - return &PolicyRepository_Activate_Call{Call: _e.mock.On("Activate", ctx, tx, id)} -} - -func (_c *PolicyRepository_Activate_Call) Run(run func(ctx context.Context, tx shared.DB, id uuid.UUID)) *PolicyRepository_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 *PolicyRepository_Activate_Call) Return(err error) *PolicyRepository_Activate_Call { - _c.Call.Return(err) - return _c -} - -func (_c *PolicyRepository_Activate_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, id uuid.UUID) error) *PolicyRepository_Activate_Call { - _c.Call.Return(run) - return _c -} - -// All provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) All(ctx context.Context, tx shared.DB) ([]models.Policy, error) { - ret := _mock.Called(ctx, tx) - - if len(ret) == 0 { - panic("no return value specified for All") - } - - var r0 []models.Policy - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB) ([]models.Policy, error)); ok { - return returnFunc(ctx, tx) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB) []models.Policy); ok { - r0 = returnFunc(ctx, tx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]models.Policy) - } - } - 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 -} - -// PolicyRepository_All_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'All' -type PolicyRepository_All_Call struct { - *mock.Call -} - -// All is a helper method to define mock.On call -// - ctx context.Context -// - tx shared.DB -func (_e *PolicyRepository_Expecter) All(ctx interface{}, tx interface{}) *PolicyRepository_All_Call { - return &PolicyRepository_All_Call{Call: _e.mock.On("All", ctx, tx)} -} - -func (_c *PolicyRepository_All_Call) Run(run func(ctx context.Context, tx shared.DB)) *PolicyRepository_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 *PolicyRepository_All_Call) Return(policys []models.Policy, err error) *PolicyRepository_All_Call { - _c.Call.Return(policys, err) - return _c -} - -func (_c *PolicyRepository_All_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB) ([]models.Policy, error)) *PolicyRepository_All_Call { - _c.Call.Return(run) - return _c -} - -// Begin provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) 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 -} - -// PolicyRepository_Begin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Begin' -type PolicyRepository_Begin_Call struct { - *mock.Call -} - -// Begin is a helper method to define mock.On call -// - ctx context.Context -func (_e *PolicyRepository_Expecter) Begin(ctx interface{}) *PolicyRepository_Begin_Call { - return &PolicyRepository_Begin_Call{Call: _e.mock.On("Begin", ctx)} -} - -func (_c *PolicyRepository_Begin_Call) Run(run func(ctx context.Context)) *PolicyRepository_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 *PolicyRepository_Begin_Call) Return(v shared.DB) *PolicyRepository_Begin_Call { - _c.Call.Return(v) - return _c -} - -func (_c *PolicyRepository_Begin_Call) RunAndReturn(run func(ctx context.Context) shared.DB) *PolicyRepository_Begin_Call { - _c.Call.Return(run) - return _c -} - -// CleanupOrphanedRecords provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) 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 -} - -// PolicyRepository_CleanupOrphanedRecords_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CleanupOrphanedRecords' -type PolicyRepository_CleanupOrphanedRecords_Call struct { - *mock.Call -} - -// CleanupOrphanedRecords is a helper method to define mock.On call -// - ctx context.Context -func (_e *PolicyRepository_Expecter) CleanupOrphanedRecords(ctx interface{}) *PolicyRepository_CleanupOrphanedRecords_Call { - return &PolicyRepository_CleanupOrphanedRecords_Call{Call: _e.mock.On("CleanupOrphanedRecords", ctx)} -} - -func (_c *PolicyRepository_CleanupOrphanedRecords_Call) Run(run func(ctx context.Context)) *PolicyRepository_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 *PolicyRepository_CleanupOrphanedRecords_Call) Return(err error) *PolicyRepository_CleanupOrphanedRecords_Call { - _c.Call.Return(err) - return _c -} - -func (_c *PolicyRepository_CleanupOrphanedRecords_Call) RunAndReturn(run func(ctx context.Context) error) *PolicyRepository_CleanupOrphanedRecords_Call { - _c.Call.Return(run) - return _c -} - -// Create provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) Create(ctx context.Context, tx shared.DB, t *models.Policy) 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.Policy) error); ok { - r0 = returnFunc(ctx, tx, t) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// PolicyRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' -type PolicyRepository_Create_Call struct { - *mock.Call -} - -// Create is a helper method to define mock.On call -// - ctx context.Context -// - tx shared.DB -// - t *models.Policy -func (_e *PolicyRepository_Expecter) Create(ctx interface{}, tx interface{}, t interface{}) *PolicyRepository_Create_Call { - return &PolicyRepository_Create_Call{Call: _e.mock.On("Create", ctx, tx, t)} -} - -func (_c *PolicyRepository_Create_Call) Run(run func(ctx context.Context, tx shared.DB, t *models.Policy)) *PolicyRepository_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.Policy - if args[2] != nil { - arg2 = args[2].(*models.Policy) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *PolicyRepository_Create_Call) Return(err error) *PolicyRepository_Create_Call { - _c.Call.Return(err) - return _c -} - -func (_c *PolicyRepository_Create_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, t *models.Policy) error) *PolicyRepository_Create_Call { - _c.Call.Return(run) - return _c -} - -// CreateBatch provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) CreateBatch(ctx context.Context, tx shared.DB, ts []models.Policy) 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.Policy) error); ok { - r0 = returnFunc(ctx, tx, ts) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// PolicyRepository_CreateBatch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateBatch' -type PolicyRepository_CreateBatch_Call struct { - *mock.Call -} - -// CreateBatch is a helper method to define mock.On call -// - ctx context.Context -// - tx shared.DB -// - ts []models.Policy -func (_e *PolicyRepository_Expecter) CreateBatch(ctx interface{}, tx interface{}, ts interface{}) *PolicyRepository_CreateBatch_Call { - return &PolicyRepository_CreateBatch_Call{Call: _e.mock.On("CreateBatch", ctx, tx, ts)} -} - -func (_c *PolicyRepository_CreateBatch_Call) Run(run func(ctx context.Context, tx shared.DB, ts []models.Policy)) *PolicyRepository_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.Policy - if args[2] != nil { - arg2 = args[2].([]models.Policy) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *PolicyRepository_CreateBatch_Call) Return(err error) *PolicyRepository_CreateBatch_Call { - _c.Call.Return(err) - return _c -} - -func (_c *PolicyRepository_CreateBatch_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, ts []models.Policy) error) *PolicyRepository_CreateBatch_Call { - _c.Call.Return(run) - return _c -} - -// Delete provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) 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 -} - -// PolicyRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' -type PolicyRepository_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 *PolicyRepository_Expecter) Delete(ctx interface{}, tx interface{}, id interface{}) *PolicyRepository_Delete_Call { - return &PolicyRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, tx, id)} -} - -func (_c *PolicyRepository_Delete_Call) Run(run func(ctx context.Context, tx shared.DB, id uuid.UUID)) *PolicyRepository_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 *PolicyRepository_Delete_Call) Return(err error) *PolicyRepository_Delete_Call { - _c.Call.Return(err) - return _c -} - -func (_c *PolicyRepository_Delete_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, id uuid.UUID) error) *PolicyRepository_Delete_Call { - _c.Call.Return(run) - return _c -} - -// DeleteBatch provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) DeleteBatch(ctx context.Context, tx shared.DB, ids []models.Policy) 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.Policy) error); ok { - r0 = returnFunc(ctx, tx, ids) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// PolicyRepository_DeleteBatch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteBatch' -type PolicyRepository_DeleteBatch_Call struct { - *mock.Call -} - -// DeleteBatch is a helper method to define mock.On call -// - ctx context.Context -// - tx shared.DB -// - ids []models.Policy -func (_e *PolicyRepository_Expecter) DeleteBatch(ctx interface{}, tx interface{}, ids interface{}) *PolicyRepository_DeleteBatch_Call { - return &PolicyRepository_DeleteBatch_Call{Call: _e.mock.On("DeleteBatch", ctx, tx, ids)} -} - -func (_c *PolicyRepository_DeleteBatch_Call) Run(run func(ctx context.Context, tx shared.DB, ids []models.Policy)) *PolicyRepository_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.Policy - if args[2] != nil { - arg2 = args[2].([]models.Policy) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *PolicyRepository_DeleteBatch_Call) Return(err error) *PolicyRepository_DeleteBatch_Call { - _c.Call.Return(err) - return _c -} - -func (_c *PolicyRepository_DeleteBatch_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, ids []models.Policy) error) *PolicyRepository_DeleteBatch_Call { - _c.Call.Return(run) - return _c -} - -// FindByOrganizationID provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) FindByOrganizationID(ctx context.Context, tx shared.DB, organizationID uuid.UUID) ([]models.Policy, error) { - ret := _mock.Called(ctx, tx, organizationID) - - if len(ret) == 0 { - panic("no return value specified for FindByOrganizationID") - } - - var r0 []models.Policy - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) ([]models.Policy, error)); ok { - return returnFunc(ctx, tx, organizationID) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) []models.Policy); ok { - r0 = returnFunc(ctx, tx, organizationID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]models.Policy) - } - } - if returnFunc, ok := ret.Get(1).(func(context.Context, shared.DB, uuid.UUID) error); ok { - r1 = returnFunc(ctx, tx, organizationID) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// PolicyRepository_FindByOrganizationID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindByOrganizationID' -type PolicyRepository_FindByOrganizationID_Call struct { - *mock.Call -} - -// FindByOrganizationID is a helper method to define mock.On call -// - ctx context.Context -// - tx shared.DB -// - organizationID uuid.UUID -func (_e *PolicyRepository_Expecter) FindByOrganizationID(ctx interface{}, tx interface{}, organizationID interface{}) *PolicyRepository_FindByOrganizationID_Call { - return &PolicyRepository_FindByOrganizationID_Call{Call: _e.mock.On("FindByOrganizationID", ctx, tx, organizationID)} -} - -func (_c *PolicyRepository_FindByOrganizationID_Call) Run(run func(ctx context.Context, tx shared.DB, organizationID uuid.UUID)) *PolicyRepository_FindByOrganizationID_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 *PolicyRepository_FindByOrganizationID_Call) Return(policys []models.Policy, err error) *PolicyRepository_FindByOrganizationID_Call { - _c.Call.Return(policys, err) - return _c -} - -func (_c *PolicyRepository_FindByOrganizationID_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, organizationID uuid.UUID) ([]models.Policy, error)) *PolicyRepository_FindByOrganizationID_Call { - _c.Call.Return(run) - return _c -} - -// FindByProjectID provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) FindByProjectID(ctx context.Context, tx shared.DB, projectID uuid.UUID) ([]models.Policy, error) { - ret := _mock.Called(ctx, tx, projectID) - - if len(ret) == 0 { - panic("no return value specified for FindByProjectID") - } - - var r0 []models.Policy - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) ([]models.Policy, error)); ok { - return returnFunc(ctx, tx, projectID) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) []models.Policy); ok { - r0 = returnFunc(ctx, tx, projectID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]models.Policy) - } - } - 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 -} - -// PolicyRepository_FindByProjectID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindByProjectID' -type PolicyRepository_FindByProjectID_Call struct { - *mock.Call -} - -// FindByProjectID is a helper method to define mock.On call -// - ctx context.Context -// - tx shared.DB -// - projectID uuid.UUID -func (_e *PolicyRepository_Expecter) FindByProjectID(ctx interface{}, tx interface{}, projectID interface{}) *PolicyRepository_FindByProjectID_Call { - return &PolicyRepository_FindByProjectID_Call{Call: _e.mock.On("FindByProjectID", ctx, tx, projectID)} -} - -func (_c *PolicyRepository_FindByProjectID_Call) Run(run func(ctx context.Context, tx shared.DB, projectID uuid.UUID)) *PolicyRepository_FindByProjectID_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 *PolicyRepository_FindByProjectID_Call) Return(policys []models.Policy, err error) *PolicyRepository_FindByProjectID_Call { - _c.Call.Return(policys, err) - return _c -} - -func (_c *PolicyRepository_FindByProjectID_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, projectID uuid.UUID) ([]models.Policy, error)) *PolicyRepository_FindByProjectID_Call { - _c.Call.Return(run) - return _c -} - -// FindCommunityManagedPolicies provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) FindCommunityManagedPolicies(ctx context.Context, tx shared.DB) ([]models.Policy, error) { - ret := _mock.Called(ctx, tx) - - if len(ret) == 0 { - panic("no return value specified for FindCommunityManagedPolicies") - } - - var r0 []models.Policy - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB) ([]models.Policy, error)); ok { - return returnFunc(ctx, tx) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB) []models.Policy); ok { - r0 = returnFunc(ctx, tx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]models.Policy) - } - } - 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 -} - -// PolicyRepository_FindCommunityManagedPolicies_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindCommunityManagedPolicies' -type PolicyRepository_FindCommunityManagedPolicies_Call struct { - *mock.Call -} - -// FindCommunityManagedPolicies is a helper method to define mock.On call -// - ctx context.Context -// - tx shared.DB -func (_e *PolicyRepository_Expecter) FindCommunityManagedPolicies(ctx interface{}, tx interface{}) *PolicyRepository_FindCommunityManagedPolicies_Call { - return &PolicyRepository_FindCommunityManagedPolicies_Call{Call: _e.mock.On("FindCommunityManagedPolicies", ctx, tx)} -} - -func (_c *PolicyRepository_FindCommunityManagedPolicies_Call) Run(run func(ctx context.Context, tx shared.DB)) *PolicyRepository_FindCommunityManagedPolicies_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 *PolicyRepository_FindCommunityManagedPolicies_Call) Return(policys []models.Policy, err error) *PolicyRepository_FindCommunityManagedPolicies_Call { - _c.Call.Return(policys, err) - return _c -} - -func (_c *PolicyRepository_FindCommunityManagedPolicies_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB) ([]models.Policy, error)) *PolicyRepository_FindCommunityManagedPolicies_Call { - _c.Call.Return(run) - return _c -} - -// GetDB provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) 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 -} - -// PolicyRepository_GetDB_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDB' -type PolicyRepository_GetDB_Call struct { - *mock.Call -} - -// GetDB is a helper method to define mock.On call -// - ctx context.Context -// - tx shared.DB -func (_e *PolicyRepository_Expecter) GetDB(ctx interface{}, tx interface{}) *PolicyRepository_GetDB_Call { - return &PolicyRepository_GetDB_Call{Call: _e.mock.On("GetDB", ctx, tx)} -} - -func (_c *PolicyRepository_GetDB_Call) Run(run func(ctx context.Context, tx shared.DB)) *PolicyRepository_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 *PolicyRepository_GetDB_Call) Return(v shared.DB) *PolicyRepository_GetDB_Call { - _c.Call.Return(v) - return _c -} - -func (_c *PolicyRepository_GetDB_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB) shared.DB) *PolicyRepository_GetDB_Call { - _c.Call.Return(run) - return _c -} - -// List provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) List(ctx context.Context, tx shared.DB, ids []uuid.UUID) ([]models.Policy, error) { - ret := _mock.Called(ctx, tx, ids) - - if len(ret) == 0 { - panic("no return value specified for List") - } - - var r0 []models.Policy - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, []uuid.UUID) ([]models.Policy, error)); ok { - return returnFunc(ctx, tx, ids) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, []uuid.UUID) []models.Policy); ok { - r0 = returnFunc(ctx, tx, ids) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]models.Policy) - } - } - 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 -} - -// PolicyRepository_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List' -type PolicyRepository_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 *PolicyRepository_Expecter) List(ctx interface{}, tx interface{}, ids interface{}) *PolicyRepository_List_Call { - return &PolicyRepository_List_Call{Call: _e.mock.On("List", ctx, tx, ids)} -} - -func (_c *PolicyRepository_List_Call) Run(run func(ctx context.Context, tx shared.DB, ids []uuid.UUID)) *PolicyRepository_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 *PolicyRepository_List_Call) Return(policys []models.Policy, err error) *PolicyRepository_List_Call { - _c.Call.Return(policys, err) - return _c -} - -func (_c *PolicyRepository_List_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, ids []uuid.UUID) ([]models.Policy, error)) *PolicyRepository_List_Call { - _c.Call.Return(run) - return _c -} - -// Read provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) Read(ctx context.Context, tx shared.DB, id uuid.UUID) (models.Policy, error) { - ret := _mock.Called(ctx, tx, id) - - if len(ret) == 0 { - panic("no return value specified for Read") - } - - var r0 models.Policy - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) (models.Policy, error)); ok { - return returnFunc(ctx, tx, id) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID) models.Policy); ok { - r0 = returnFunc(ctx, tx, id) - } else { - r0 = ret.Get(0).(models.Policy) - } - 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 -} - -// PolicyRepository_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' -type PolicyRepository_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 *PolicyRepository_Expecter) Read(ctx interface{}, tx interface{}, id interface{}) *PolicyRepository_Read_Call { - return &PolicyRepository_Read_Call{Call: _e.mock.On("Read", ctx, tx, id)} -} - -func (_c *PolicyRepository_Read_Call) Run(run func(ctx context.Context, tx shared.DB, id uuid.UUID)) *PolicyRepository_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 *PolicyRepository_Read_Call) Return(policy models.Policy, err error) *PolicyRepository_Read_Call { - _c.Call.Return(policy, err) - return _c -} - -func (_c *PolicyRepository_Read_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, id uuid.UUID) (models.Policy, error)) *PolicyRepository_Read_Call { - _c.Call.Return(run) - return _c -} - -// Save provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) Save(ctx context.Context, tx shared.DB, t *models.Policy) 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.Policy) error); ok { - r0 = returnFunc(ctx, tx, t) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// PolicyRepository_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save' -type PolicyRepository_Save_Call struct { - *mock.Call -} - -// Save is a helper method to define mock.On call -// - ctx context.Context -// - tx shared.DB -// - t *models.Policy -func (_e *PolicyRepository_Expecter) Save(ctx interface{}, tx interface{}, t interface{}) *PolicyRepository_Save_Call { - return &PolicyRepository_Save_Call{Call: _e.mock.On("Save", ctx, tx, t)} -} - -func (_c *PolicyRepository_Save_Call) Run(run func(ctx context.Context, tx shared.DB, t *models.Policy)) *PolicyRepository_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.Policy - if args[2] != nil { - arg2 = args[2].(*models.Policy) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *PolicyRepository_Save_Call) Return(err error) *PolicyRepository_Save_Call { - _c.Call.Return(err) - return _c -} - -func (_c *PolicyRepository_Save_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, t *models.Policy) error) *PolicyRepository_Save_Call { - _c.Call.Return(run) - return _c -} - -// SaveBatch provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) SaveBatch(ctx context.Context, tx shared.DB, ts []models.Policy) 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.Policy) error); ok { - r0 = returnFunc(ctx, tx, ts) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// PolicyRepository_SaveBatch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveBatch' -type PolicyRepository_SaveBatch_Call struct { - *mock.Call -} - -// SaveBatch is a helper method to define mock.On call -// - ctx context.Context -// - tx shared.DB -// - ts []models.Policy -func (_e *PolicyRepository_Expecter) SaveBatch(ctx interface{}, tx interface{}, ts interface{}) *PolicyRepository_SaveBatch_Call { - return &PolicyRepository_SaveBatch_Call{Call: _e.mock.On("SaveBatch", ctx, tx, ts)} -} - -func (_c *PolicyRepository_SaveBatch_Call) Run(run func(ctx context.Context, tx shared.DB, ts []models.Policy)) *PolicyRepository_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.Policy - if args[2] != nil { - arg2 = args[2].([]models.Policy) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *PolicyRepository_SaveBatch_Call) Return(err error) *PolicyRepository_SaveBatch_Call { - _c.Call.Return(err) - return _c -} - -func (_c *PolicyRepository_SaveBatch_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, ts []models.Policy) error) *PolicyRepository_SaveBatch_Call { - _c.Call.Return(run) - return _c -} - -// SaveBatchBestEffort provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) SaveBatchBestEffort(ctx context.Context, tx shared.DB, ts []models.Policy) 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.Policy) error); ok { - r0 = returnFunc(ctx, tx, ts) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// PolicyRepository_SaveBatchBestEffort_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveBatchBestEffort' -type PolicyRepository_SaveBatchBestEffort_Call struct { - *mock.Call -} - -// SaveBatchBestEffort is a helper method to define mock.On call -// - ctx context.Context -// - tx shared.DB -// - ts []models.Policy -func (_e *PolicyRepository_Expecter) SaveBatchBestEffort(ctx interface{}, tx interface{}, ts interface{}) *PolicyRepository_SaveBatchBestEffort_Call { - return &PolicyRepository_SaveBatchBestEffort_Call{Call: _e.mock.On("SaveBatchBestEffort", ctx, tx, ts)} -} - -func (_c *PolicyRepository_SaveBatchBestEffort_Call) Run(run func(ctx context.Context, tx shared.DB, ts []models.Policy)) *PolicyRepository_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.Policy - if args[2] != nil { - arg2 = args[2].([]models.Policy) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *PolicyRepository_SaveBatchBestEffort_Call) Return(err error) *PolicyRepository_SaveBatchBestEffort_Call { - _c.Call.Return(err) - return _c -} - -func (_c *PolicyRepository_SaveBatchBestEffort_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, ts []models.Policy) error) *PolicyRepository_SaveBatchBestEffort_Call { - _c.Call.Return(run) - return _c -} - -// Transaction provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) 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 -} - -// PolicyRepository_Transaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Transaction' -type PolicyRepository_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 *PolicyRepository_Expecter) Transaction(ctx interface{}, fn interface{}) *PolicyRepository_Transaction_Call { - return &PolicyRepository_Transaction_Call{Call: _e.mock.On("Transaction", ctx, fn)} -} - -func (_c *PolicyRepository_Transaction_Call) Run(run func(ctx context.Context, fn func(tx shared.DB) error)) *PolicyRepository_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 *PolicyRepository_Transaction_Call) Return(err error) *PolicyRepository_Transaction_Call { - _c.Call.Return(err) - return _c -} - -func (_c *PolicyRepository_Transaction_Call) RunAndReturn(run func(ctx context.Context, fn func(tx shared.DB) error) error) *PolicyRepository_Transaction_Call { - _c.Call.Return(run) - return _c -} - -// Upsert provides a mock function for the type PolicyRepository -func (_mock *PolicyRepository) Upsert(ctx context.Context, tx shared.DB, t *[]*models.Policy, 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.Policy, []clause.Column, []string) error); ok { - r0 = returnFunc(ctx, tx, t, conflictingColumns, updateOnly) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// PolicyRepository_Upsert_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Upsert' -type PolicyRepository_Upsert_Call struct { - *mock.Call -} - -// Upsert is a helper method to define mock.On call -// - ctx context.Context -// - tx shared.DB -// - t *[]*models.Policy -// - conflictingColumns []clause.Column -// - updateOnly []string -func (_e *PolicyRepository_Expecter) Upsert(ctx interface{}, tx interface{}, t interface{}, conflictingColumns interface{}, updateOnly interface{}) *PolicyRepository_Upsert_Call { - return &PolicyRepository_Upsert_Call{Call: _e.mock.On("Upsert", ctx, tx, t, conflictingColumns, updateOnly)} -} - -func (_c *PolicyRepository_Upsert_Call) Run(run func(ctx context.Context, tx shared.DB, t *[]*models.Policy, conflictingColumns []clause.Column, updateOnly []string)) *PolicyRepository_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.Policy - if args[2] != nil { - arg2 = args[2].(*[]*models.Policy) - } - 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 *PolicyRepository_Upsert_Call) Return(err error) *PolicyRepository_Upsert_Call { - _c.Call.Return(err) - return _c -} - -func (_c *PolicyRepository_Upsert_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, t *[]*models.Policy, conflictingColumns []clause.Column, updateOnly []string) error) *PolicyRepository_Upsert_Call { - _c.Call.Return(run) - return _c -} diff --git a/mocks/mock_ReportingDescriptorReference.go b/mocks/mock_ReportingDescriptorReference.go deleted file mode 100644 index e38e0efb6..000000000 --- a/mocks/mock_ReportingDescriptorReference.go +++ /dev/null @@ -1,36 +0,0 @@ -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package mocks - -import ( - mock "github.com/stretchr/testify/mock" -) - -// NewReportingDescriptorReference creates a new instance of ReportingDescriptorReference. 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 NewReportingDescriptorReference(t interface { - mock.TestingT - Cleanup(func()) -}) *ReportingDescriptorReference { - mock := &ReportingDescriptorReference{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// ReportingDescriptorReference is an autogenerated mock type for the ReportingDescriptorReference type -type ReportingDescriptorReference struct { - mock.Mock -} - -type ReportingDescriptorReference_Expecter struct { - mock *mock.Mock -} - -func (_m *ReportingDescriptorReference) EXPECT() *ReportingDescriptorReference_Expecter { - return &ReportingDescriptorReference_Expecter{mock: &_m.Mock} -} diff --git a/mocks/mock_VulnDBImportService.go b/mocks/mock_VulnDBImportService.go deleted file mode 100644 index fbc5bbcf9..000000000 --- a/mocks/mock_VulnDBImportService.go +++ /dev/null @@ -1,260 +0,0 @@ -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package mocks - -import ( - "context" - - mock "github.com/stretchr/testify/mock" -) - -// NewVulnDBImportService creates a new instance of VulnDBImportService. 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 NewVulnDBImportService(t interface { - mock.TestingT - Cleanup(func()) -}) *VulnDBImportService { - mock := &VulnDBImportService{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// VulnDBImportService is an autogenerated mock type for the VulnDBImportService type -type VulnDBImportService struct { - mock.Mock -} - -type VulnDBImportService_Expecter struct { - mock *mock.Mock -} - -func (_m *VulnDBImportService) EXPECT() *VulnDBImportService_Expecter { - return &VulnDBImportService_Expecter{mock: &_m.Mock} -} - -// CleanupOrphanedTables provides a mock function for the type VulnDBImportService -func (_mock *VulnDBImportService) CleanupOrphanedTables(ctx context.Context) error { - ret := _mock.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for CleanupOrphanedTables") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = returnFunc(ctx) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// VulnDBImportService_CleanupOrphanedTables_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CleanupOrphanedTables' -type VulnDBImportService_CleanupOrphanedTables_Call struct { - *mock.Call -} - -// CleanupOrphanedTables is a helper method to define mock.On call -// - ctx context.Context -func (_e *VulnDBImportService_Expecter) CleanupOrphanedTables(ctx interface{}) *VulnDBImportService_CleanupOrphanedTables_Call { - return &VulnDBImportService_CleanupOrphanedTables_Call{Call: _e.mock.On("CleanupOrphanedTables", ctx)} -} - -func (_c *VulnDBImportService_CleanupOrphanedTables_Call) Run(run func(ctx context.Context)) *VulnDBImportService_CleanupOrphanedTables_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 *VulnDBImportService_CleanupOrphanedTables_Call) Return(err error) *VulnDBImportService_CleanupOrphanedTables_Call { - _c.Call.Return(err) - return _c -} - -func (_c *VulnDBImportService_CleanupOrphanedTables_Call) RunAndReturn(run func(ctx context.Context) error) *VulnDBImportService_CleanupOrphanedTables_Call { - _c.Call.Return(run) - return _c -} - -// CreateTablesWithSuffix provides a mock function for the type VulnDBImportService -func (_mock *VulnDBImportService) CreateTablesWithSuffix(ctx context.Context, suffix string) error { - ret := _mock.Called(ctx, suffix) - - if len(ret) == 0 { - panic("no return value specified for CreateTablesWithSuffix") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = returnFunc(ctx, suffix) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// VulnDBImportService_CreateTablesWithSuffix_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateTablesWithSuffix' -type VulnDBImportService_CreateTablesWithSuffix_Call struct { - *mock.Call -} - -// CreateTablesWithSuffix is a helper method to define mock.On call -// - ctx context.Context -// - suffix string -func (_e *VulnDBImportService_Expecter) CreateTablesWithSuffix(ctx interface{}, suffix interface{}) *VulnDBImportService_CreateTablesWithSuffix_Call { - return &VulnDBImportService_CreateTablesWithSuffix_Call{Call: _e.mock.On("CreateTablesWithSuffix", ctx, suffix)} -} - -func (_c *VulnDBImportService_CreateTablesWithSuffix_Call) Run(run func(ctx context.Context, suffix string)) *VulnDBImportService_CreateTablesWithSuffix_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *VulnDBImportService_CreateTablesWithSuffix_Call) Return(err error) *VulnDBImportService_CreateTablesWithSuffix_Call { - _c.Call.Return(err) - return _c -} - -func (_c *VulnDBImportService_CreateTablesWithSuffix_Call) RunAndReturn(run func(ctx context.Context, suffix string) error) *VulnDBImportService_CreateTablesWithSuffix_Call { - _c.Call.Return(run) - return _c -} - -// ExportDiffs provides a mock function for the type VulnDBImportService -func (_mock *VulnDBImportService) ExportDiffs(ctx context.Context, extraTableNameSuffix string) error { - ret := _mock.Called(ctx, extraTableNameSuffix) - - if len(ret) == 0 { - panic("no return value specified for ExportDiffs") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = returnFunc(ctx, extraTableNameSuffix) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// VulnDBImportService_ExportDiffs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExportDiffs' -type VulnDBImportService_ExportDiffs_Call struct { - *mock.Call -} - -// ExportDiffs is a helper method to define mock.On call -// - ctx context.Context -// - extraTableNameSuffix string -func (_e *VulnDBImportService_Expecter) ExportDiffs(ctx interface{}, extraTableNameSuffix interface{}) *VulnDBImportService_ExportDiffs_Call { - return &VulnDBImportService_ExportDiffs_Call{Call: _e.mock.On("ExportDiffs", ctx, extraTableNameSuffix)} -} - -func (_c *VulnDBImportService_ExportDiffs_Call) Run(run func(ctx context.Context, extraTableNameSuffix string)) *VulnDBImportService_ExportDiffs_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *VulnDBImportService_ExportDiffs_Call) Return(err error) *VulnDBImportService_ExportDiffs_Call { - _c.Call.Return(err) - return _c -} - -func (_c *VulnDBImportService_ExportDiffs_Call) RunAndReturn(run func(ctx context.Context, extraTableNameSuffix string) error) *VulnDBImportService_ExportDiffs_Call { - _c.Call.Return(run) - return _c -} - -// ImportFromDiff provides a mock function for the type VulnDBImportService -func (_mock *VulnDBImportService) ImportFromDiff(ctx context.Context, extraTableNameSuffix *string) error { - ret := _mock.Called(ctx, extraTableNameSuffix) - - if len(ret) == 0 { - panic("no return value specified for ImportFromDiff") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, *string) error); ok { - r0 = returnFunc(ctx, extraTableNameSuffix) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// VulnDBImportService_ImportFromDiff_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ImportFromDiff' -type VulnDBImportService_ImportFromDiff_Call struct { - *mock.Call -} - -// ImportFromDiff is a helper method to define mock.On call -// - ctx context.Context -// - extraTableNameSuffix *string -func (_e *VulnDBImportService_Expecter) ImportFromDiff(ctx interface{}, extraTableNameSuffix interface{}) *VulnDBImportService_ImportFromDiff_Call { - return &VulnDBImportService_ImportFromDiff_Call{Call: _e.mock.On("ImportFromDiff", ctx, extraTableNameSuffix)} -} - -func (_c *VulnDBImportService_ImportFromDiff_Call) Run(run func(ctx context.Context, extraTableNameSuffix *string)) *VulnDBImportService_ImportFromDiff_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 *string - if args[1] != nil { - arg1 = args[1].(*string) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *VulnDBImportService_ImportFromDiff_Call) Return(err error) *VulnDBImportService_ImportFromDiff_Call { - _c.Call.Return(err) - return _c -} - -func (_c *VulnDBImportService_ImportFromDiff_Call) RunAndReturn(run func(ctx context.Context, extraTableNameSuffix *string) error) *VulnDBImportService_ImportFromDiff_Call { - _c.Call.Return(run) - return _c -} diff --git a/router/router_test.go b/router/router_test.go index a0882b0ef..c9356c32e 100644 --- a/router/router_test.go +++ b/router/router_test.go @@ -238,6 +238,7 @@ func buildSecurityTestServer(t *testing.T, ac *mocks.AccessControl) *echo.Echo { new(controllers.StatisticsController), new(controllers.WebhookController), projectRepo, + new(controllers.ComponentController), ) assetRouter := NewAssetRouter( From 0f01c5475e591b921f0808edec4857ced402e9cf Mon Sep 17 00:00:00 2001 From: rafi Date: Tue, 9 Jun 2026 13:01:35 +0200 Subject: [PATCH 15/26] refactor compliance risk and attestation evaluation logic Signed-off-by: rafi --- cmd/devguard-scanner/commands/attestations.go | 3 +- cmd/devguard-scanner/scanner/eval_policy.go | 46 ++++++----- compliance/rego.go | 20 ++++- controllers/compliance_risk_controller.go | 4 +- daemons/attestation_daemon.go | 2 - docs/evaluations-schema.json | 78 ------------------- dtos/devguard_asset_attestation_schema.json | 2 +- services/compliance_risk_service.go | 31 ++++++-- services/dependency_vuln_service.go | 2 + services/first_party_vuln_service.go | 2 + services/license_risk_service.go | 3 + shared/context_utils.go | 17 ++++ tests/project_controller_test.go | 54 ------------- 13 files changed, 94 insertions(+), 170 deletions(-) delete mode 100644 docs/evaluations-schema.json diff --git a/cmd/devguard-scanner/commands/attestations.go b/cmd/devguard-scanner/commands/attestations.go index 83a40e5ef..af3e6b3fa 100644 --- a/cmd/devguard-scanner/commands/attestations.go +++ b/cmd/devguard-scanner/commands/attestations.go @@ -22,7 +22,6 @@ import ( "log/slog" "net/http" "os" - "path/filepath" "strings" "github.com/google/uuid" @@ -79,7 +78,7 @@ func attestationsCmd(cmd *cobra.Command, args []string) error { defer os.Remove(policyPath) } - sarifResult, evals, err := scanner.EvaluatePolicyAgainstAttestations(fmt.Sprintf("oci://%s", image), filepath.Dir(policyPath), attestations) + sarifResult, evals, err := scanner.EvaluatePolicyAgainstAttestations(fmt.Sprintf("oci://%s", image), policyPath, attestations) if err != nil { return err } diff --git a/cmd/devguard-scanner/scanner/eval_policy.go b/cmd/devguard-scanner/scanner/eval_policy.go index 6b8fc2bed..bbb390a54 100644 --- a/cmd/devguard-scanner/scanner/eval_policy.go +++ b/cmd/devguard-scanner/scanner/eval_policy.go @@ -17,6 +17,8 @@ package scanner import ( "encoding/json" "fmt" + "os" + "path/filepath" "github.com/l3montree-dev/devguard/compliance" "github.com/l3montree-dev/devguard/dtos/sarif" @@ -25,35 +27,39 @@ import ( func EvaluatePolicyAgainstAttestations(srcPath string, policyPath string, attestations []map[string]any) (*sarif.SarifSchema210Json, []compliance.PolicyEvaluation, error) { - policies, err := compliance.GetPoliciesFromFS(policyPath) + content, err := os.ReadFile(policyPath) if err != nil { - return nil, nil, fmt.Errorf("could not load policies from FS: %w", err) + return nil, nil, fmt.Errorf("could not read policy file: %w", err) + } + + policy, err := compliance.PolicyFSFromContent(filepath.Base(policyPath), string(content)) + if err != nil { + return nil, nil, fmt.Errorf("could not parse policy: %w", err) } evaluations := make([]compliance.PolicyEvaluation, 0) foundMatch: - for _, policy := range policies { - for _, attestation := range attestations { - predicateType, _ := attestation["predicateType"].(string) - if predicateType != policy.PredicateType { - continue - } - raw, err := json.Marshal(attestation) - if err != nil { - return nil, nil, fmt.Errorf("could not marshal attestation: %w", err) - } - input, err := utils.ExtractAttestationPayload(string(raw)) - if err != nil { - return nil, nil, fmt.Errorf("could not extract attestation payload: %w", err) - } - eval := compliance.Eval(policy, input) - evaluations = append(evaluations, eval) - continue foundMatch + + for _, attestation := range attestations { + predicateType, _ := attestation["predicateType"].(string) + if predicateType != policy.PredicateType { + continue + } + raw, err := json.Marshal(attestation) + if err != nil { + return nil, nil, fmt.Errorf("could not marshal attestation: %w", err) + } + input, err := utils.ExtractAttestationPayload(string(raw)) + if err != nil { + return nil, nil, fmt.Errorf("could not extract attestation payload: %w", err) } - eval := compliance.Eval(policy, nil) + eval := compliance.Eval(policy, input) evaluations = append(evaluations, eval) + continue foundMatch } + eval := compliance.Eval(policy, nil) + evaluations = append(evaluations, eval) sarifResult := compliance.BuildSarifFromPolicies(srcPath, evaluations) return &sarifResult, evaluations, nil diff --git a/compliance/rego.go b/compliance/rego.go index 20f08858a..8c7882cb4 100644 --- a/compliance/rego.go +++ b/compliance/rego.go @@ -211,6 +211,14 @@ func GetPoliciesFromFS(policyDir string) ([]PolicyFS, error) { return policies, nil } +func PolicyFSFromContent(fileName, content string) (PolicyFS, error) { + metadata, err := parseMetadata(fileName, content) + if err != nil { + return PolicyFS{}, err + } + return PolicyFS{PolicyMetadata: metadata, Content: content}, nil +} + func BuildSarifFromPolicies(srcPath string, evaluations []PolicyEvaluation) sarif.SarifSchema210Json { rules := make([]sarif.ReportingDescriptor, 0, len(evaluations)) results := make([]sarif.Result, 0) @@ -258,11 +266,15 @@ func BuildSarifFromPolicies(srcPath string, evaluations []PolicyEvaluation) sari rules = append(rules, rule) artifactLocation := sarif.ArtifactLocation{URI: &srcPath} + additionalProps := map[string]any{ + "precision": "high", + } + if evaluation.AttestationContent != nil { + additionalProps["attestationContent"] = *evaluation.AttestationContent + } props := &sarif.PropertyBag{ - Tags: evaluation.PolicyTags, - AdditionalProperties: map[string]any{ - "precision": "high", - }, + Tags: evaluation.PolicyTags, + AdditionalProperties: additionalProps, } if evaluation.Compliant == nil { diff --git a/controllers/compliance_risk_controller.go b/controllers/compliance_risk_controller.go index c4314f1ec..0211b170a 100644 --- a/controllers/compliance_risk_controller.go +++ b/controllers/compliance_risk_controller.go @@ -189,8 +189,8 @@ func (c *ComplianceRiskController) EvaluateArtifactCompliance(ctx shared.Context return ctx.JSON(200, sarifDoc) } -// UploadZip accepts a ZIP file containing attestation files and an evaluations.json. -// It saves each attestation and then recalculates compliance risks based on the evaluations. +// UploadZip accepts a ZIP file containing attestation JSON files and a sarif.json. +// It saves each attestation and then recalculates compliance risks based on the SARIF. func (c *ComplianceRiskController) UploadZip(ctx shared.Context) error { assetVersion := shared.GetAssetVersion(ctx) artifact := shared.GetArtifact(ctx) diff --git a/daemons/attestation_daemon.go b/daemons/attestation_daemon.go index a56680310..11216a91f 100644 --- a/daemons/attestation_daemon.go +++ b/daemons/attestation_daemon.go @@ -49,8 +49,6 @@ func (runner *DaemonRunner) CheckArtifactCompliance(input <-chan assetWithProjec return out } -const secondsPerHour = 3600.0 - // GenerateDevguardAttestations is a pipeline stage that computes the DevGuard asset // metrics attestation for every asset version and upserts it into the attestations table. // It runs after CollectStats so that risk data is already up to date. diff --git a/docs/evaluations-schema.json b/docs/evaluations-schema.json deleted file mode 100644 index b6050c644..000000000 --- a/docs/evaluations-schema.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://devguard.dev/schemas/evaluations.json", - "title": "PolicyEvaluations", - "description": "Array of policy evaluation results uploaded as evaluations.json inside the attestation ZIP.", - "type": "array", - "items": { - "type": "object", - "required": ["policyId", "policyTitle", "state", "predicateType"], - "properties": { - "policyId": { - "type": "string", - "description": "ID of the evaluated policy." - }, - "policyTitle": { - "type": "string", - "description": "Human-readable title of the policy." - }, - "policyDescription": { - "type": ["string", "null"], - "description": "Optional description of the policy." - }, - "state": { - "type": "string", - "enum": ["open", "fixed"], - "description": "Compliance state. Use 'open' for a violation, 'fixed' for a passing policy." - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "Timestamp when this evaluation was created (RFC 3339)." - }, - "predicateType": { - "type": "string", - "description": "The in-toto predicate type this policy evaluates against (e.g. 'https://slsa.dev/provenance/v1')." - }, - "attestationUpdatedAt": { - "type": ["string", "null"], - "format": "date-time", - "description": "Timestamp of the attestation that was evaluated." - }, - "attestationViolations": { - "type": ["array", "null"], - "items": { - "type": "string" - }, - "description": "List of violation messages when the policy is not satisfied." - } - }, - "additionalProperties": false - }, - "examples": [ - [ - { - "policyId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "policyTitle": "SLSA Provenance Required", - "policyDescription": "Every artifact must have a valid SLSA provenance attestation.", - "state": "open", - "createdAt": "2026-06-05T12:00:00Z", - "predicateType": "https://slsa.dev/provenance/v1", - "attestationUpdatedAt": "2026-06-05T11:50:00Z", - "attestationViolations": [ - "builder.id does not match expected value" - ] - }, - { - "policyId": "b2c3d4e5-f6a7-8901-bcde-f12345678901", - "policyTitle": "SBOM Present", - "policyDescription": null, - "state": "fixed", - "createdAt": "2026-06-05T12:00:00Z", - "predicateType": "https://cyclonedx.org/bom/v1.4", - "attestationUpdatedAt": "2026-06-05T11:55:00Z", - "attestationViolations": [] - } - ] - ] -} diff --git a/dtos/devguard_asset_attestation_schema.json b/dtos/devguard_asset_attestation_schema.json index 4e7ec2fe1..55df35f50 100644 --- a/dtos/devguard_asset_attestation_schema.json +++ b/dtos/devguard_asset_attestation_schema.json @@ -65,7 +65,7 @@ "minimum": 0, "description": "Average hours to remediate low-CVSS vulnerabilities." }, - "cvsssMediumAvgHours": { + "cvssMediumAvgHours": { "type": "number", "minimum": 0, "description": "Average hours to remediate medium-CVSS vulnerabilities." diff --git a/services/compliance_risk_service.go b/services/compliance_risk_service.go index 0250ed050..ea3dd05a9 100644 --- a/services/compliance_risk_service.go +++ b/services/compliance_risk_service.go @@ -2,6 +2,7 @@ package services import ( "context" + "fmt" "log/slog" "time" @@ -233,6 +234,8 @@ func (s *ComplianceRiskService) updateComplianceRiskState(ctx context.Context, t ev = models.NewReopenedEvent(risk.CalculateHash(), dtos.VulnTypeComplianceRisk, userID, justification, false, userAgent) case dtos.EventTypeComment: ev = models.NewCommentEvent(risk.CalculateHash(), dtos.VulnTypeComplianceRisk, userID, justification, false, userAgent) + default: + return models.VulnEvent{}, fmt.Errorf("unsupported event type: %s", statusType) } err := s.complianceRiskRepository.ApplyAndSave(ctx, tx, risk, &ev) return ev, err @@ -313,8 +316,9 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode } type policyResult struct { - kind sarif.ResultKind - violations []string + kind sarif.ResultKind + violations []string + attestationContent *string } resultMap := make(map[string]*policyResult, len(ruleMap)) @@ -329,6 +333,12 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode resultMap[ruleID] = pr } + if result.Properties != nil { + if ac, ok := result.Properties.AdditionalProperties["attestationContent"].(string); ok && pr.attestationContent == nil { + pr.attestationContent = &ac + } + } + switch result.Kind { case sarif.ResultKindFail: pr.kind = sarif.ResultKindFail @@ -349,8 +359,10 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode for ruleID, info := range ruleMap { state := dtos.VulnStateOpen var violations []string + var attestationContent *string if pr := resultMap[ruleID]; pr != nil { + attestationContent = pr.attestationContent switch pr.kind { case sarif.ResultKindPass: state = dtos.VulnStateFixed @@ -368,11 +380,16 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode State: state, LastDetected: time.Now(), }, - PolicyID: ruleID, - PolicyTitle: info.title, - PolicyDescription: info.description, - PredicateType: info.predicateType, - AttestationViolations: violations, + PolicyID: ruleID, + PolicyTitle: info.title, + PolicyDescription: info.description, + PolicyRelatedResources: info.relatedResources, + PolicyTags: info.tags, + PolicyPriority: info.priority, + ComplianceFrameworks: info.complianceFrameworks, + PredicateType: info.predicateType, + AttestationViolations: violations, + AttestationContent: attestationContent, }) } diff --git a/services/dependency_vuln_service.go b/services/dependency_vuln_service.go index af679e783..9caf35de2 100644 --- a/services/dependency_vuln_service.go +++ b/services/dependency_vuln_service.go @@ -340,6 +340,8 @@ func (s *DependencyVulnService) createVulnEventAndApply(ctx context.Context, tx ev = models.NewCommentEvent(dependencyVuln.CalculateHash(), dtos.VulnTypeDependencyVuln, userID, justification, false, userAgent) case dtos.EventTypeFixed: ev = models.NewFixedEvent(dependencyVuln.CalculateHash(), dtos.VulnTypeDependencyVuln, userID, dependencyVuln.GetScannerIDsOrArtifactNames(), false, userAgent) + default: + return models.VulnEvent{}, fmt.Errorf("unsupported event type: %s", vulnEventType) } // Apply the event to the original vuln diff --git a/services/first_party_vuln_service.go b/services/first_party_vuln_service.go index 6d4ac7c32..8138abe24 100644 --- a/services/first_party_vuln_service.go +++ b/services/first_party_vuln_service.go @@ -120,6 +120,8 @@ func (s *firstPartyVulnService) updateFirstPartyVulnState(ctx context.Context, t ev = models.NewReopenedEvent(firstPartyVuln.CalculateHash(), dtos.VulnTypeFirstPartyVuln, userID, justification, false, userAgent) case dtos.EventTypeComment: ev = models.NewCommentEvent(firstPartyVuln.CalculateHash(), dtos.VulnTypeFirstPartyVuln, userID, justification, false, userAgent) + default: + return models.VulnEvent{}, fmt.Errorf("unsupported event type: %s", statusType) } return s.applyAndSave(ctx, tx, firstPartyVuln, &ev) diff --git a/services/license_risk_service.go b/services/license_risk_service.go index a8435b008..98fa98710 100644 --- a/services/license_risk_service.go +++ b/services/license_risk_service.go @@ -2,6 +2,7 @@ package services import ( "context" + "fmt" "log/slog" "slices" "strings" @@ -403,6 +404,8 @@ func (s *LicenseRiskService) updateLicenseRiskState(ctx context.Context, tx shar ev = models.NewReopenedEvent(licenseRisk.CalculateHash(), dtos.VulnTypeLicenseRisk, userID, justification, false, userAgent) case dtos.EventTypeComment: ev = models.NewCommentEvent(licenseRisk.CalculateHash(), dtos.VulnTypeLicenseRisk, userID, justification, false, userAgent) + default: + return models.VulnEvent{}, fmt.Errorf("unsupported event type: %s", statusType) } err := s.licenseRiskRepository.ApplyAndSave(ctx, tx, licenseRisk, &ev) diff --git a/shared/context_utils.go b/shared/context_utils.go index fc4807de5..c6213a42d 100644 --- a/shared/context_utils.go +++ b/shared/context_utils.go @@ -168,6 +168,23 @@ func GetVulnID(ctx Context) (uuid.UUID, dtos.VulnType, error) { return id, dtos.VulnTypeLicenseRisk, nil } + ComplianceRiskID := ctx.Param("complianceRiskID") + if ComplianceRiskID != "" { + id, err := uuid.Parse(ComplianceRiskID) + if err != nil { + return uuid.Nil, "", fmt.Errorf("invalid compliance risk id: %w", err) + } + return id, dtos.VulnTypeComplianceRisk, nil + } + ComplianceRiskIDFromGet, ok := ctx.Get("complianceRiskID").(string) + if ok && ComplianceRiskIDFromGet != "" { + id, err := uuid.Parse(ComplianceRiskIDFromGet) + if err != nil { + return uuid.Nil, "", fmt.Errorf("invalid compliance risk id: %w", err) + } + return id, dtos.VulnTypeComplianceRisk, nil + } + return uuid.Nil, "", fmt.Errorf("could not get vuln id") } diff --git a/tests/project_controller_test.go b/tests/project_controller_test.go index 1bf86b985..74e47fe5a 100644 --- a/tests/project_controller_test.go +++ b/tests/project_controller_test.go @@ -22,60 +22,6 @@ func TestProjectCreation(t *testing.T) { WithTestApp(t, "../initdb.sql", func(f *TestFixture) { org, project, _, _ := f.CreateOrgProjectAssetAndVersion() - t.Run("should enable all community policies by default", func(t *testing.T) { - e := echo.New() - rec := httptest.NewRecorder() - - requestBody := map[string]string{ - "name": "new-project", - "description": "This is a new project", - } - - b, err := json.Marshal(requestBody) - assert.Nil(t, err) - - req := httptest.NewRequest("POST", "/projects", bytes.NewBuffer(b)) - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - ctx := e.NewContext(req, rec) - shared.SetOrg(ctx, org) - session := mocks.NewAuthSession(t) - shared.SetSession(ctx, session) - rbac := mocks.NewAccessControl(t) - rbac.On("LinkDomainAndProjectRole", mock.Anything, shared.RoleAdmin, shared.RoleAdmin, mock.Anything).Return(nil) - rbac.On("InheritProjectRole", mock.Anything, shared.RoleAdmin, shared.RoleMember, mock.Anything).Return(nil) - rbac.On("AllowRoleInProject", mock.Anything, mock.Anything, shared.RoleAdmin, shared.ObjectUser, []shared.Action{ - shared.ActionCreate, - shared.ActionDelete, - shared.ActionUpdate, - }).Return(nil) - rbac.On("AllowRoleInProject", mock.Anything, mock.Anything, shared.RoleAdmin, shared.ObjectAsset, []shared.Action{ - shared.ActionCreate, - shared.ActionDelete, - shared.ActionUpdate, - }).Return(nil) - rbac.On("AllowRoleInProject", mock.Anything, mock.Anything, shared.RoleAdmin, shared.ObjectProject, []shared.Action{ - shared.ActionDelete, - shared.ActionUpdate, - }).Return(nil) - rbac.On("AllowRoleInProject", mock.Anything, mock.Anything, shared.RoleMember, shared.ObjectProject, []shared.Action{ - shared.ActionRead, - }).Return(nil) - rbac.On("AllowRoleInProject", mock.Anything, mock.Anything, shared.RoleMember, shared.ObjectAsset, []shared.Action{ - shared.ActionRead, - }).Return(nil) - - shared.SetRBAC(ctx, rbac) - - err = f.App.ProjectController.Create(ctx) - assert.Nil(t, err) - - var createdProject models.Project - err = f.DB.Preload("EnabledPolicies").First(&createdProject, "slug = ?", requestBody["name"]).Error - - assert.Nil(t, err) - - }) - t.Run("should generate a unique slug", func(t *testing.T) { // create a new project with the same name and slug as the existing project e := echo.New() From 906f3a6d28a8a766872df824eb5bf2de64418723 Mon Sep 17 00:00:00 2001 From: rafi Date: Tue, 9 Jun 2026 13:55:52 +0200 Subject: [PATCH 16/26] update compliance risk model, service, and evaluation logic Signed-off-by: rafi --- compliance/rego.go | 51 ++++++++--------- controllers/compliance_risk_controller.go | 21 +++++++ ..._add_compliance_risks_drop_policies.up.sql | 9 ++- database/models/compliance_risk_model.go | 11 ++-- dtos/compliance_risk_dto.go | 7 +-- router/compliance_risk_router.go | 1 + services/compliance_risk_service.go | 55 +++++++++---------- transformer/compliance_risk_transformer.go | 7 +-- 8 files changed, 90 insertions(+), 72 deletions(-) diff --git a/compliance/rego.go b/compliance/rego.go index 8c7882cb4..225266c31 100644 --- a/compliance/rego.go +++ b/compliance/rego.go @@ -25,9 +25,9 @@ type customYaml struct { Priority int `yaml:"priority"` Tags []string // used for mapping from policies to attestations - PredicateType string `yaml:"predicateType"` - RelatedResources []string `yaml:"relatedResources"` - ComplianceFrameworks []string `yaml:"complianceFrameworks"` + PredicateType string `yaml:"predicateType"` + RelatedResources []string `yaml:"relatedResources"` + Controls []string `yaml:"controls"` } type PolicyMetadata struct { @@ -36,6 +36,7 @@ type PolicyMetadata struct { Priority int `yaml:"priority" json:"priority"` Tags []string `yaml:"tags" json:"tags"` RelatedResources []string `yaml:"relatedResources" json:"relatedResources"` + Controls []string `yaml:"controls" json:"controls"` ComplianceFrameworks []string `yaml:"complianceFrameworks" json:"complianceFrameworks"` Filename string `json:"filename"` Content string `json:"content"` @@ -87,15 +88,14 @@ func parseMetadata(fileName string, content string) (PolicyMetadata, error) { } return PolicyMetadata{ - Title: metadata.Title, - Description: metadata.Custom.Description, - Priority: metadata.Custom.Priority, - Tags: metadata.Custom.Tags, - RelatedResources: metadata.Custom.RelatedResources, - ComplianceFrameworks: metadata.Custom.ComplianceFrameworks, - Filename: fileName, - PredicateType: metadata.Custom.PredicateType, - Content: content, + Title: metadata.Title, + Description: metadata.Custom.Description, + Priority: metadata.Custom.Priority, + Tags: metadata.Custom.Tags, + RelatedResources: metadata.Custom.RelatedResources, + Controls: metadata.Custom.Controls, + Filename: fileName, + PredicateType: metadata.Custom.PredicateType, }, nil } @@ -106,12 +106,12 @@ type PolicyEvaluation struct { PolicyRelatedResources []string PolicyTags []string PolicyPriority int - PredicateType string - ComplianceFrameworks []string + PolicyControls []string Compliant *bool Violations []string RawEvaluationResult map[string]any - AttestationContent *string + EvidenceType string + EvidenceContent *string } func Eval(policy PolicyFS, input any) PolicyEvaluation { @@ -162,12 +162,12 @@ func Eval(policy PolicyFS, input any) PolicyEvaluation { PolicyRelatedResources: policy.RelatedResources, PolicyTags: policy.Tags, PolicyPriority: policy.Priority, - PredicateType: policy.PredicateType, - ComplianceFrameworks: policy.ComplianceFrameworks, + EvidenceType: "json", + PolicyControls: policy.Controls, Compliant: compliant, Violations: violations, RawEvaluationResult: rawEvalResult, - AttestationContent: nil, + EvidenceContent: &policy.Content, } } @@ -255,10 +255,9 @@ func BuildSarifFromPolicies(srcPath string, evaluations []PolicyEvaluation) sari Properties: &sarif.PropertyBag{ Tags: evaluation.PolicyTags, AdditionalProperties: map[string]any{ - "priority": evaluation.PolicyPriority, - "relatedResources": evaluation.PolicyRelatedResources, - "complianceFrameworks": evaluation.ComplianceFrameworks, - "predicateType": evaluation.PredicateType, + "priority": evaluation.PolicyPriority, + "relatedResources": evaluation.PolicyRelatedResources, + "controls": evaluation.PolicyControls, }, }, } @@ -267,11 +266,13 @@ func BuildSarifFromPolicies(srcPath string, evaluations []PolicyEvaluation) sari artifactLocation := sarif.ArtifactLocation{URI: &srcPath} additionalProps := map[string]any{ - "precision": "high", + "precision": "high", + "evidenceType": evaluation.EvidenceType, } - if evaluation.AttestationContent != nil { - additionalProps["attestationContent"] = *evaluation.AttestationContent + if evaluation.EvidenceContent != nil { + additionalProps["evidenceContent"] = *evaluation.EvidenceContent } + props := &sarif.PropertyBag{ Tags: evaluation.PolicyTags, AdditionalProperties: additionalProps, diff --git a/controllers/compliance_risk_controller.go b/controllers/compliance_risk_controller.go index 0211b170a..11c49340c 100644 --- a/controllers/compliance_risk_controller.go +++ b/controllers/compliance_risk_controller.go @@ -100,6 +100,27 @@ func (c *ComplianceRiskController) Read(ctx shared.Context) error { return ctx.JSON(200, convertComplianceRiskToDetailedDTO(risk)) } +func (c *ComplianceRiskController) GetEvidence(ctx shared.Context) error { + riskID, _, err := shared.GetVulnID(ctx) + if err != nil { + return echo.NewHTTPError(400, "could not get compliance risk ID") + } + risk, err := c.complianceRiskRepository.Read(ctx.Request().Context(), nil, riskID) + if err != nil { + return echo.NewHTTPError(404, "could not find compliance risk") + } + if len(risk.EvidenceContent) == 0 { + return echo.NewHTTPError(404, "no evidence available") + } + + contentType := risk.EvidenceType + if contentType == "" || contentType == "json" { + contentType = "application/json" + } + + return ctx.Blob(200, contentType, risk.EvidenceContent) +} + func (c *ComplianceRiskController) CreateEvent(ctx shared.Context) error { thirdPartyIntegration := shared.GetThirdPartyIntegration(ctx) riskID, _, err := shared.GetVulnID(ctx) diff --git a/database/migrations/20260602000000_add_compliance_risks_drop_policies.up.sql b/database/migrations/20260602000000_add_compliance_risks_drop_policies.up.sql index d1799b306..3093a52ab 100644 --- a/database/migrations/20260602000000_add_compliance_risks_drop_policies.up.sql +++ b/database/migrations/20260602000000_add_compliance_risks_drop_policies.up.sql @@ -18,14 +18,13 @@ CREATE TABLE IF NOT EXISTS public.compliance_risks ( policy_id text NOT NULL, policy_title text NOT NULL DEFAULT '', policy_description text, - predicate_type text NOT NULL DEFAULT '', + evidence_type text NOT NULL DEFAULT '', policy_related_resources text[], policy_tags text[], policy_priority integer, - compliance_frameworks text[], - attestation_content text, - attestation_violations text[], - attestation_updated_at timestamp with time zone, + policyControls text[], + evidence_content bytea, + evidence_violations text[], CONSTRAINT compliance_risks_pkey PRIMARY KEY (id), CONSTRAINT fk_compliance_risks_asset_versions FOREIGN KEY (asset_version_name, asset_id) REFERENCES public.asset_versions (name, asset_id) ON DELETE CASCADE diff --git a/database/models/compliance_risk_model.go b/database/models/compliance_risk_model.go index 958507aaa..c5b59f867 100644 --- a/database/models/compliance_risk_model.go +++ b/database/models/compliance_risk_model.go @@ -32,12 +32,11 @@ type ComplianceRisk struct { PolicyRelatedResources []string `json:"policyRelatedResources" gorm:"type:text[];"` PolicyTags []string `json:"policyTags" gorm:"type:text[];"` PolicyPriority int `json:"policyPriority"` - ComplianceFrameworks []string `json:"complianceFrameworks" gorm:"type:text[];"` - PredicateType string `json:"predicateType" gorm:"type:text;"` + PolicyControls []string `json:"policyControls" gorm:"type:text[];"` + EvidenceType string `json:"evidenceType" gorm:"type:text;"` + EvidenceContent []byte `json:"evidenceContent" gorm:"type:bytea;"` - AttestationContent *string `json:"attestationContent" gorm:"type:text;"` - - AttestationViolations []string `json:"attestationViolations" gorm:"type:text[];"` + Violations []string `json:"violations" gorm:"type:text[];"` Events []VulnEvent `gorm:"foreignKey:ComplianceRiskID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;" json:"events"` @@ -53,7 +52,7 @@ func (complianceRisk ComplianceRisk) GetType() dtos.VulnType { } func (complianceRisk *ComplianceRisk) CalculateHash() uuid.UUID { - return utils.HashToUUID(fmt.Sprintf("%s/%s/%s/%s", complianceRisk.PolicyID, complianceRisk.PredicateType, complianceRisk.AssetVersionName, complianceRisk.AssetID)) + return utils.HashToUUID(fmt.Sprintf("%s/%s/%s", complianceRisk.PolicyID, complianceRisk.AssetVersionName, complianceRisk.AssetID)) } func (complianceRisk *ComplianceRisk) BeforeSave(tx *gorm.DB) error { diff --git a/dtos/compliance_risk_dto.go b/dtos/compliance_risk_dto.go index c74c58019..a486f19b5 100644 --- a/dtos/compliance_risk_dto.go +++ b/dtos/compliance_risk_dto.go @@ -18,7 +18,7 @@ type ComplianceRiskDTO struct { PolicyRelatedResources []string `json:"policyRelatedResources"` PolicyTags []string `json:"policyTags"` PolicyPriority int `json:"policyPriority"` - ComplianceFrameworks []string `json:"complianceFrameworks"` + PolicyControls []string `json:"policyControls"` State VulnState `json:"state"` CreatedAt time.Time `json:"createdAt"` @@ -26,9 +26,8 @@ type ComplianceRiskDTO struct { TicketURL *string `json:"ticketUrl"` ManualTicketCreation bool `json:"manualTicketCreation"` - PredicateType string `json:"predicateType"` - AttestationContent *string `json:"attestationContent"` - AttestationViolations []string `json:"attestationViolations"` + EvidenceType string `json:"evidenceType"` + Violations []string `json:"Violations"` } type DetailedComplianceRiskDTO struct { diff --git a/router/compliance_risk_router.go b/router/compliance_risk_router.go index fc0da0544..7d72272a4 100644 --- a/router/compliance_risk_router.go +++ b/router/compliance_risk_router.go @@ -17,6 +17,7 @@ func NewComplianceRiskRouter( complianceRisksRouter := assetVersionGroup.Group.Group("/compliance-risks") complianceRisksRouter.GET("/", controller.ListPaged) complianceRisksRouter.GET("/:complianceRiskID/", controller.Read) + complianceRisksRouter.GET("/:complianceRiskID/evidence/", controller.GetEvidence) complianceRisksRouter.POST("/:complianceRiskID/", controller.CreateEvent, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) complianceRisksRouter.POST("/:complianceRiskID/mitigate/", controller.Mitigate, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) diff --git a/services/compliance_risk_service.go b/services/compliance_risk_service.go index ea3dd05a9..6e860aefd 100644 --- a/services/compliance_risk_service.go +++ b/services/compliance_risk_service.go @@ -250,13 +250,13 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode run := sarifDoc.Runs[0] type ruleInfo struct { - title string - description *string - predicateType string - relatedResources []string - tags []string - priority int - complianceFrameworks []string + title string + description *string + relatedResources []string + tags []string + priority int + controls []string + evidenceType string } ruleMap := make(map[string]ruleInfo, len(run.Tool.Driver.Rules)) for _, rule := range run.Tool.Driver.Rules { @@ -265,12 +265,7 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode d := rule.FullDescription.Text desc = &d } - var predicateType string - if rule.Properties != nil { - if pt, ok := rule.Properties.AdditionalProperties["predicateType"].(string); ok { - predicateType = pt - } - } + title := rule.ID if rule.ShortDescription != nil { title = rule.ShortDescription.Text @@ -292,12 +287,12 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode tags = rule.Properties.Tags } - var complianceFrameworks []string + var controls []string if rule.Properties != nil { - if cf, ok := rule.Properties.AdditionalProperties["complianceFrameworks"].([]any); ok { + if cf, ok := rule.Properties.AdditionalProperties["controls"].([]any); ok { for _, c := range cf { if cStr, ok := c.(string); ok { - complianceFrameworks = append(complianceFrameworks, cStr) + controls = append(controls, cStr) } } } @@ -312,13 +307,14 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode } } - ruleMap[rule.ID] = ruleInfo{title: title, description: desc, predicateType: predicateType, relatedResources: relatedResources, tags: tags, priority: priority, complianceFrameworks: complianceFrameworks} + ruleMap[rule.ID] = ruleInfo{title: title, description: desc, relatedResources: relatedResources, tags: tags, priority: priority, controls: controls} } type policyResult struct { - kind sarif.ResultKind - violations []string - attestationContent *string + kind sarif.ResultKind + violations []string + evidenceContent []byte + evidenceType string } resultMap := make(map[string]*policyResult, len(ruleMap)) @@ -334,8 +330,11 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode } if result.Properties != nil { - if ac, ok := result.Properties.AdditionalProperties["attestationContent"].(string); ok && pr.attestationContent == nil { - pr.attestationContent = &ac + if ac, ok := result.Properties.AdditionalProperties["evidenceContent"].(string); ok && pr.evidenceContent == nil { + pr.evidenceContent = []byte(ac) + } + if et, ok := result.Properties.AdditionalProperties["evidenceType"].(string); ok { + pr.evidenceType = et } } @@ -359,10 +358,10 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode for ruleID, info := range ruleMap { state := dtos.VulnStateOpen var violations []string - var attestationContent *string + var evidenceContent []byte if pr := resultMap[ruleID]; pr != nil { - attestationContent = pr.attestationContent + evidenceContent = pr.evidenceContent switch pr.kind { case sarif.ResultKindPass: state = dtos.VulnStateFixed @@ -386,10 +385,10 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode PolicyRelatedResources: info.relatedResources, PolicyTags: info.tags, PolicyPriority: info.priority, - ComplianceFrameworks: info.complianceFrameworks, - PredicateType: info.predicateType, - AttestationViolations: violations, - AttestationContent: attestationContent, + PolicyControls: info.controls, + EvidenceType: resultMap[ruleID].evidenceType, + Violations: violations, + EvidenceContent: evidenceContent, }) } diff --git a/transformer/compliance_risk_transformer.go b/transformer/compliance_risk_transformer.go index 9afd026ad..6ba6c8a5c 100644 --- a/transformer/compliance_risk_transformer.go +++ b/transformer/compliance_risk_transformer.go @@ -30,10 +30,9 @@ func ComplianceRiskToDTO(r models.ComplianceRisk) dtos.ComplianceRiskDTO { PolicyRelatedResources: r.PolicyRelatedResources, PolicyTags: r.PolicyTags, PolicyPriority: r.PolicyPriority, - ComplianceFrameworks: r.ComplianceFrameworks, - PredicateType: r.PredicateType, - AttestationContent: r.AttestationContent, - AttestationViolations: r.AttestationViolations, + PolicyControls: r.PolicyControls, + EvidenceType: r.EvidenceType, + Violations: r.Violations, Artifacts: artifacts, } } From 589979a7f711c4e72578a837806ceb7872d76c0d Mon Sep 17 00:00:00 2001 From: rafi Date: Tue, 9 Jun 2026 14:23:42 +0200 Subject: [PATCH 17/26] fix lint Signed-off-by: rafi --- services/compliance_risk_service.go | 1 - 1 file changed, 1 deletion(-) diff --git a/services/compliance_risk_service.go b/services/compliance_risk_service.go index 6e860aefd..0af1e4250 100644 --- a/services/compliance_risk_service.go +++ b/services/compliance_risk_service.go @@ -256,7 +256,6 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode tags []string priority int controls []string - evidenceType string } ruleMap := make(map[string]ruleInfo, len(run.Tool.Driver.Rules)) for _, rule := range run.Tool.Driver.Rules { From b79729add59e448b92e9bf890725565be0f86f1a Mon Sep 17 00:00:00 2001 From: rafi Date: Tue, 9 Jun 2026 15:33:56 +0200 Subject: [PATCH 18/26] add policy frameworks logic Signed-off-by: rafi --- compliance/rego.go | 35 ++++---- controllers/compliance_risk_controller.go | 19 ++++- ..._add_compliance_risks_drop_policies.up.sql | 2 +- database/models/compliance_risk_model.go | 18 ++--- .../compliance_risk_repository.go | 23 ++++++ dtos/compliance_risk_dto.go | 19 +++-- mocks/mock_ComplianceRiskRepository.go | 80 +++++++++++++++++++ services/compliance_risk_service.go | 25 ++++-- shared/common_interfaces.go | 1 + transformer/compliance_risk_transformer.go | 2 +- 10 files changed, 179 insertions(+), 45 deletions(-) diff --git a/compliance/rego.go b/compliance/rego.go index 225266c31..0f4bf1bea 100644 --- a/compliance/rego.go +++ b/compliance/rego.go @@ -9,6 +9,7 @@ import ( "sort" "strings" + "github.com/l3montree-dev/devguard/dtos" "github.com/l3montree-dev/devguard/dtos/sarif" "github.com/l3montree-dev/devguard/utils" "github.com/open-policy-agent/opa/v1/rego" @@ -25,22 +26,22 @@ type customYaml struct { Priority int `yaml:"priority"` Tags []string // used for mapping from policies to attestations - PredicateType string `yaml:"predicateType"` - RelatedResources []string `yaml:"relatedResources"` - Controls []string `yaml:"controls"` + PredicateType string `yaml:"predicateType"` + RelatedResources []string `yaml:"relatedResources"` + PolicyFrameworks []dtos.PolicyFrameworks `yaml:"policyFrameworks"` } type PolicyMetadata struct { - Title string `yaml:"title" json:"title"` - Description string `yaml:"description" json:"description"` - Priority int `yaml:"priority" json:"priority"` - Tags []string `yaml:"tags" json:"tags"` - RelatedResources []string `yaml:"relatedResources" json:"relatedResources"` - Controls []string `yaml:"controls" json:"controls"` - ComplianceFrameworks []string `yaml:"complianceFrameworks" json:"complianceFrameworks"` - Filename string `json:"filename"` - Content string `json:"content"` - PredicateType string `yaml:"predicateType" json:"predicateType"` + Title string `yaml:"title" json:"title"` + Description string `yaml:"description" json:"description"` + Priority int `yaml:"priority" json:"priority"` + Tags []string `yaml:"tags" json:"tags"` + RelatedResources []string `yaml:"relatedResources" json:"relatedResources"` + PolicyFrameworks []dtos.PolicyFrameworks `yaml:"policyFrameworks" json:"policyFrameworks"` + ComplianceFrameworks []string `yaml:"complianceFrameworks" json:"complianceFrameworks"` + Filename string `json:"filename"` + Content string `json:"content"` + PredicateType string `yaml:"predicateType" json:"predicateType"` } type PolicyFS struct { PolicyMetadata @@ -93,7 +94,7 @@ func parseMetadata(fileName string, content string) (PolicyMetadata, error) { Priority: metadata.Custom.Priority, Tags: metadata.Custom.Tags, RelatedResources: metadata.Custom.RelatedResources, - Controls: metadata.Custom.Controls, + PolicyFrameworks: metadata.Custom.PolicyFrameworks, Filename: fileName, PredicateType: metadata.Custom.PredicateType, }, nil @@ -106,7 +107,7 @@ type PolicyEvaluation struct { PolicyRelatedResources []string PolicyTags []string PolicyPriority int - PolicyControls []string + PolicyFrameworks []dtos.PolicyFrameworks Compliant *bool Violations []string RawEvaluationResult map[string]any @@ -163,7 +164,7 @@ func Eval(policy PolicyFS, input any) PolicyEvaluation { PolicyTags: policy.Tags, PolicyPriority: policy.Priority, EvidenceType: "json", - PolicyControls: policy.Controls, + PolicyFrameworks: policy.PolicyFrameworks, Compliant: compliant, Violations: violations, RawEvaluationResult: rawEvalResult, @@ -257,7 +258,7 @@ func BuildSarifFromPolicies(srcPath string, evaluations []PolicyEvaluation) sari AdditionalProperties: map[string]any{ "priority": evaluation.PolicyPriority, "relatedResources": evaluation.PolicyRelatedResources, - "controls": evaluation.PolicyControls, + "policyFrameworks": evaluation.PolicyFrameworks, }, }, } diff --git a/controllers/compliance_risk_controller.go b/controllers/compliance_risk_controller.go index 11c49340c..60760fbd5 100644 --- a/controllers/compliance_risk_controller.go +++ b/controllers/compliance_risk_controller.go @@ -83,9 +83,22 @@ func (c *ComplianceRiskController) ListPaged(ctx shared.Context) error { return echo.NewHTTPError(500, "could not get compliance risks").WithInternal(err) } - return ctx.JSON(200, pagedResp.Map(func(r models.ComplianceRisk) any { - return convertComplianceRiskToDetailedDTO(r) - })) + frameworks, err := c.complianceRiskRepository.GetDistinctFrameworksForAssetVersion( + ctx.Request().Context(), nil, + assetVersion.AssetID, + assetVersion.Name, + ) + if err != nil { + return echo.NewHTTPError(500, "could not get frameworks").WithInternal(err) + } + + return ctx.JSON(200, struct { + shared.Paged[any] + Frameworks []string `json:"frameworks"` + }{ + Paged: pagedResp.Map(func(r models.ComplianceRisk) any { return convertComplianceRiskToDetailedDTO(r) }), + Frameworks: frameworks, + }) } func (c *ComplianceRiskController) Read(ctx shared.Context) error { diff --git a/database/migrations/20260602000000_add_compliance_risks_drop_policies.up.sql b/database/migrations/20260602000000_add_compliance_risks_drop_policies.up.sql index 3093a52ab..7a3b7fcb2 100644 --- a/database/migrations/20260602000000_add_compliance_risks_drop_policies.up.sql +++ b/database/migrations/20260602000000_add_compliance_risks_drop_policies.up.sql @@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS public.compliance_risks ( policy_related_resources text[], policy_tags text[], policy_priority integer, - policyControls text[], + "policyFrameworks" jsonb, evidence_content bytea, evidence_violations text[], CONSTRAINT compliance_risks_pkey PRIMARY KEY (id), diff --git a/database/models/compliance_risk_model.go b/database/models/compliance_risk_model.go index c5b59f867..8888e66be 100644 --- a/database/models/compliance_risk_model.go +++ b/database/models/compliance_risk_model.go @@ -26,15 +26,15 @@ import ( type ComplianceRisk struct { Vulnerability - PolicyID string `json:"policyId" gorm:"type:text;"` - PolicyTitle string `json:"policyTitle" gorm:"type:text;"` - PolicyDescription *string `json:"policyDescription" gorm:"type:text;"` - PolicyRelatedResources []string `json:"policyRelatedResources" gorm:"type:text[];"` - PolicyTags []string `json:"policyTags" gorm:"type:text[];"` - PolicyPriority int `json:"policyPriority"` - PolicyControls []string `json:"policyControls" gorm:"type:text[];"` - EvidenceType string `json:"evidenceType" gorm:"type:text;"` - EvidenceContent []byte `json:"evidenceContent" gorm:"type:bytea;"` + PolicyID string `json:"policyId" gorm:"type:text;"` + PolicyTitle string `json:"policyTitle" gorm:"type:text;"` + PolicyDescription *string `json:"policyDescription" gorm:"type:text;"` + PolicyRelatedResources []string `json:"policyRelatedResources" gorm:"type:text[];"` + PolicyTags []string `json:"policyTags" gorm:"type:text[];"` + PolicyPriority int `json:"policyPriority"` + PolicyFrameworks []dtos.PolicyFrameworks `json:"policyFrameworks" gorm:"column:policyFrameworks;type:jsonb;serializer:json"` + EvidenceType string `json:"evidenceType" gorm:"type:text;"` + EvidenceContent []byte `json:"evidenceContent" gorm:"type:bytea;"` Violations []string `json:"violations" gorm:"type:text[];"` diff --git a/database/repositories/compliance_risk_repository.go b/database/repositories/compliance_risk_repository.go index 3ec01bd2c..b5c345224 100644 --- a/database/repositories/compliance_risk_repository.go +++ b/database/repositories/compliance_risk_repository.go @@ -105,6 +105,29 @@ func (r *ComplianceRiskRepository) GetComplianceRisksByOtherAssetVersions(ctx co return risks, nil } +func (r *ComplianceRiskRepository) GetDistinctFrameworksForAssetVersion(ctx context.Context, tx *gorm.DB, assetID uuid.UUID, assetVersionName string) ([]string, error) { + type result struct { + Framework string + } + var rows []result + err := r.GetDB(ctx, tx).Raw(` + SELECT DISTINCT elem->>'framework' AS framework + FROM compliance_risks, + jsonb_array_elements("policyFrameworks") AS elem + WHERE asset_id = ? AND asset_version_name = ? + AND elem->>'framework' IS NOT NULL + ORDER BY framework + `, assetID, assetVersionName).Scan(&rows).Error + if err != nil { + return nil, err + } + frameworks := make([]string, len(rows)) + for i, row := range rows { + frameworks[i] = row.Framework + } + return frameworks, nil +} + func (r *ComplianceRiskRepository) SaveBatch(ctx context.Context, tx *gorm.DB, risks []models.ComplianceRisk) error { if len(risks) == 0 { return nil diff --git a/dtos/compliance_risk_dto.go b/dtos/compliance_risk_dto.go index a486f19b5..e62501d87 100644 --- a/dtos/compliance_risk_dto.go +++ b/dtos/compliance_risk_dto.go @@ -6,19 +6,24 @@ import ( "github.com/google/uuid" ) +type PolicyFrameworks struct { + Framework string `yaml:"framework" json:"framework"` + Controls []string `yaml:"controls" json:"controls"` +} + type ComplianceRiskDTO struct { ID uuid.UUID `json:"id"` AssetVersionName string `json:"assetVersionName"` AssetID string `json:"assetId"` Artifacts []ArtifactDTO `json:"artifacts,omitempty"` - PolicyID string `json:"policyId"` - PolicyTitle string `json:"policyTitle"` - PolicyDescription *string `json:"policyDescription"` - PolicyRelatedResources []string `json:"policyRelatedResources"` - PolicyTags []string `json:"policyTags"` - PolicyPriority int `json:"policyPriority"` - PolicyControls []string `json:"policyControls"` + PolicyID string `json:"policyId"` + PolicyTitle string `json:"policyTitle"` + PolicyDescription *string `json:"policyDescription"` + PolicyRelatedResources []string `json:"policyRelatedResources"` + PolicyTags []string `json:"policyTags"` + PolicyPriority int `json:"policyPriority"` + PolicyFrameworks []PolicyFrameworks `json:"policyFrameworks"` State VulnState `json:"state"` CreatedAt time.Time `json:"createdAt"` diff --git a/mocks/mock_ComplianceRiskRepository.go b/mocks/mock_ComplianceRiskRepository.go index 76c1b1627..2c96a1284 100644 --- a/mocks/mock_ComplianceRiskRepository.go +++ b/mocks/mock_ComplianceRiskRepository.go @@ -918,6 +918,86 @@ func (_c *ComplianceRiskRepository_GetDB_Call) RunAndReturn(run func(ctx context return _c } +// GetDistinctFrameworksForAssetVersion provides a mock function for the type ComplianceRiskRepository +func (_mock *ComplianceRiskRepository) GetDistinctFrameworksForAssetVersion(ctx context.Context, tx shared.DB, assetID uuid.UUID, assetVersionName string) ([]string, error) { + ret := _mock.Called(ctx, tx, assetID, assetVersionName) + + if len(ret) == 0 { + panic("no return value specified for GetDistinctFrameworksForAssetVersion") + } + + var r0 []string + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID, string) ([]string, error)); ok { + return returnFunc(ctx, tx, assetID, assetVersionName) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, shared.DB, uuid.UUID, string) []string); ok { + r0 = returnFunc(ctx, tx, assetID, assetVersionName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, shared.DB, uuid.UUID, string) error); ok { + r1 = returnFunc(ctx, tx, assetID, assetVersionName) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// ComplianceRiskRepository_GetDistinctFrameworksForAssetVersion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDistinctFrameworksForAssetVersion' +type ComplianceRiskRepository_GetDistinctFrameworksForAssetVersion_Call struct { + *mock.Call +} + +// GetDistinctFrameworksForAssetVersion is a helper method to define mock.On call +// - ctx context.Context +// - tx shared.DB +// - assetID uuid.UUID +// - assetVersionName string +func (_e *ComplianceRiskRepository_Expecter) GetDistinctFrameworksForAssetVersion(ctx interface{}, tx interface{}, assetID interface{}, assetVersionName interface{}) *ComplianceRiskRepository_GetDistinctFrameworksForAssetVersion_Call { + return &ComplianceRiskRepository_GetDistinctFrameworksForAssetVersion_Call{Call: _e.mock.On("GetDistinctFrameworksForAssetVersion", ctx, tx, assetID, assetVersionName)} +} + +func (_c *ComplianceRiskRepository_GetDistinctFrameworksForAssetVersion_Call) Run(run func(ctx context.Context, tx shared.DB, assetID uuid.UUID, assetVersionName string)) *ComplianceRiskRepository_GetDistinctFrameworksForAssetVersion_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) + } + var arg3 string + if args[3] != nil { + arg3 = args[3].(string) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *ComplianceRiskRepository_GetDistinctFrameworksForAssetVersion_Call) Return(strings []string, err error) *ComplianceRiskRepository_GetDistinctFrameworksForAssetVersion_Call { + _c.Call.Return(strings, err) + return _c +} + +func (_c *ComplianceRiskRepository_GetDistinctFrameworksForAssetVersion_Call) RunAndReturn(run func(ctx context.Context, tx shared.DB, assetID uuid.UUID, assetVersionName string) ([]string, error)) *ComplianceRiskRepository_GetDistinctFrameworksForAssetVersion_Call { + _c.Call.Return(run) + return _c +} + // List provides a mock function for the type ComplianceRiskRepository func (_mock *ComplianceRiskRepository) List(ctx context.Context, tx shared.DB, ids []uuid.UUID) ([]models.ComplianceRisk, error) { ret := _mock.Called(ctx, tx, ids) diff --git a/services/compliance_risk_service.go b/services/compliance_risk_service.go index 0af1e4250..fdf6e6d23 100644 --- a/services/compliance_risk_service.go +++ b/services/compliance_risk_service.go @@ -255,7 +255,7 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode relatedResources []string tags []string priority int - controls []string + policyFrameworks []dtos.PolicyFrameworks } ruleMap := make(map[string]ruleInfo, len(run.Tool.Driver.Rules)) for _, rule := range run.Tool.Driver.Rules { @@ -286,12 +286,23 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode tags = rule.Properties.Tags } - var controls []string + var policyFrameworks []dtos.PolicyFrameworks if rule.Properties != nil { - if cf, ok := rule.Properties.AdditionalProperties["controls"].([]any); ok { + if cf, ok := rule.Properties.AdditionalProperties["policyFrameworks"].([]any); ok { for _, c := range cf { - if cStr, ok := c.(string); ok { - controls = append(controls, cStr) + if cMap, ok := c.(map[string]any); ok { + pc := dtos.PolicyFrameworks{} + if fw, ok := cMap["framework"].(string); ok { + pc.Framework = fw + } + if ctls, ok := cMap["controls"].([]any); ok { + for _, ctl := range ctls { + if s, ok := ctl.(string); ok { + pc.Controls = append(pc.Controls, s) + } + } + } + policyFrameworks = append(policyFrameworks, pc) } } } @@ -306,7 +317,7 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode } } - ruleMap[rule.ID] = ruleInfo{title: title, description: desc, relatedResources: relatedResources, tags: tags, priority: priority, controls: controls} + ruleMap[rule.ID] = ruleInfo{title: title, description: desc, relatedResources: relatedResources, tags: tags, priority: priority, policyFrameworks: policyFrameworks} } type policyResult struct { @@ -384,7 +395,7 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode PolicyRelatedResources: info.relatedResources, PolicyTags: info.tags, PolicyPriority: info.priority, - PolicyControls: info.controls, + PolicyFrameworks: info.policyFrameworks, EvidenceType: resultMap[ruleID].evidenceType, Violations: violations, EvidenceContent: evidenceContent, diff --git a/shared/common_interfaces.go b/shared/common_interfaces.go index 04fa4ff2c..af1d70233 100644 --- a/shared/common_interfaces.go +++ b/shared/common_interfaces.go @@ -295,6 +295,7 @@ type ComplianceRiskRepository interface { GetComplianceRisksByOtherAssetVersions(ctx context.Context, tx DB, assetVersionName string, assetID uuid.UUID) ([]models.ComplianceRisk, error) Read(ctx context.Context, tx DB, id uuid.UUID) (models.ComplianceRisk, error) ApplyAndSave(ctx context.Context, tx DB, risk *models.ComplianceRisk, ev *models.VulnEvent) error + GetDistinctFrameworksForAssetVersion(ctx context.Context, tx DB, assetID uuid.UUID, assetVersionName string) ([]string, error) SaveBatch(ctx context.Context, tx DB, risks []models.ComplianceRisk) error } diff --git a/transformer/compliance_risk_transformer.go b/transformer/compliance_risk_transformer.go index 6ba6c8a5c..b2b24a5dd 100644 --- a/transformer/compliance_risk_transformer.go +++ b/transformer/compliance_risk_transformer.go @@ -30,7 +30,7 @@ func ComplianceRiskToDTO(r models.ComplianceRisk) dtos.ComplianceRiskDTO { PolicyRelatedResources: r.PolicyRelatedResources, PolicyTags: r.PolicyTags, PolicyPriority: r.PolicyPriority, - PolicyControls: r.PolicyControls, + PolicyFrameworks: r.PolicyFrameworks, EvidenceType: r.EvidenceType, Violations: r.Violations, Artifacts: artifacts, From 5aec45e05df4df5f962c94c20a8228aa38e11c25 Mon Sep 17 00:00:00 2001 From: rafi Date: Wed, 10 Jun 2026 12:57:01 +0200 Subject: [PATCH 19/26] feat: update compliance risk evaluation and rego policies Signed-off-by: rafi --- cmd/devguard-cli/commands/vulndb.go | 1 + cmd/devguard/main.go | 1 + .../author_committer_email_is_from_org.rego | 2 +- .../policies/branch_protection_enabled.rego | 6 +- .../policies/build_from_signed_source.rego | 6 +- .../policies/ci_image_has_digest_set.rego | 2 +- .../cia_requirements_set_for_asset.rego | 6 +- ..._review_for_changes_on_default_branch.rego | 6 +- .../policies/container_scanning_executed.rego | 6 +- .../policies/current_sbom_is_present.rego | 8 +- ...ation_channel_for_new_vulnerabilities.rego | 6 +- .../policies/only_osi_approved_licenses.rego | 6 +- .../policies/secret_scanning_executed.rego | 7 +- .../security_policy_present_in_repo.rego | 6 +- .../policies/signed_off_commit.rego | 2 +- ...oftware_composition_analysis_executed.rego | 6 +- .../policies/uses_sbom.rego | 8 +- .../policies/vulnerability_fix_time_sla.rego | 59 +++- compliance/rego.go | 122 ++++---- ..._add_compliance_risks_drop_policies.up.sql | 6 +- database/models/compliance_risk_model.go | 8 +- dtos/compliance_risk_dto.go | 1 + services/compliance_risk_service.go | 265 ++++++++++-------- transformer/compliance_risk_transformer.go | 1 + 24 files changed, 313 insertions(+), 234 deletions(-) diff --git a/cmd/devguard-cli/commands/vulndb.go b/cmd/devguard-cli/commands/vulndb.go index 451d40578..6b5e020b6 100644 --- a/cmd/devguard-cli/commands/vulndb.go +++ b/cmd/devguard-cli/commands/vulndb.go @@ -83,6 +83,7 @@ func migrateDB() { fx.Invoke(func(DependencyVulnRouter router.DependencyVulnRouter) {}), fx.Invoke(func(FirstPartyVulnRouter router.FirstPartyVulnRouter) {}), fx.Invoke(func(LicenseRiskRouter router.LicenseRiskRouter) {}), + fx.Invoke(func(ComplianceRiskRouter router.ComplianceRiskRouter) {}), fx.Invoke(func(ShareRouter router.ShareRouter) {}), fx.Invoke(func(VulnDBRouter router.VulnDBRouter) {}), fx.Invoke(func(dependencyProxyRouter router.DependencyProxyRouter) {}), diff --git a/cmd/devguard/main.go b/cmd/devguard/main.go index eca079150..1b1029304 100644 --- a/cmd/devguard/main.go +++ b/cmd/devguard/main.go @@ -136,6 +136,7 @@ func main() { fx.Invoke(func(DependencyVulnRouter router.DependencyVulnRouter) {}), fx.Invoke(func(FirstPartyVulnRouter router.FirstPartyVulnRouter) {}), fx.Invoke(func(LicenseRiskRouter router.LicenseRiskRouter) {}), + fx.Invoke(func(ComplianceRiskRouter router.ComplianceRiskRouter) {}), fx.Invoke(func(ShareRouter router.ShareRouter) {}), fx.Invoke(func(VulnDBRouter router.VulnDBRouter) {}), fx.Invoke(func(dependencyProxyRouter router.DependencyProxyRouter) {}), diff --git a/compliance/attestation-compliance-policies/policies/author_committer_email_is_from_org.rego b/compliance/attestation-compliance-policies/policies/author_committer_email_is_from_org.rego index a1fc322cb..151635102 100644 --- a/compliance/attestation-compliance-policies/policies/author_committer_email_is_from_org.rego +++ b/compliance/attestation-compliance-policies/policies/author_committer_email_is_from_org.rego @@ -7,7 +7,7 @@ # relatedResources: [] # tags: # - Legal -# complianceFrameworks: [] +# policyFrameworks: [] package compliance import rego.v1 diff --git a/compliance/attestation-compliance-policies/policies/branch_protection_enabled.rego b/compliance/attestation-compliance-policies/policies/branch_protection_enabled.rego index af9a60628..12ad9bc9e 100644 --- a/compliance/attestation-compliance-policies/policies/branch_protection_enabled.rego +++ b/compliance/attestation-compliance-policies/policies/branch_protection_enabled.rego @@ -9,8 +9,10 @@ # tags: # - ISO 27001 # - A.8.4 Access to source code -# complianceFrameworks: -# - ISO 27001 +# policyFrameworks: +# - framework: ISO 27001 +# controls: +# - A.8.4 package compliance import rego.v1 diff --git a/compliance/attestation-compliance-policies/policies/build_from_signed_source.rego b/compliance/attestation-compliance-policies/policies/build_from_signed_source.rego index 7a452e727..29e4ff154 100644 --- a/compliance/attestation-compliance-policies/policies/build_from_signed_source.rego +++ b/compliance/attestation-compliance-policies/policies/build_from_signed_source.rego @@ -8,8 +8,10 @@ # tags: # - ISO 27001 # - A.8 Access Control -# complianceFrameworks: -# - ISO 27001 +# policyFrameworks: +# - framework: ISO 27001 +# controls: +# - A.8 package compliance import rego.v1 diff --git a/compliance/attestation-compliance-policies/policies/ci_image_has_digest_set.rego b/compliance/attestation-compliance-policies/policies/ci_image_has_digest_set.rego index 8ef158627..6d053bf30 100644 --- a/compliance/attestation-compliance-policies/policies/ci_image_has_digest_set.rego +++ b/compliance/attestation-compliance-policies/policies/ci_image_has_digest_set.rego @@ -8,7 +8,7 @@ # tags: # - GitLab CI # - Legal -# complianceFrameworks: [] +# policyFrameworks: [] package compliance import rego.v1 diff --git a/compliance/attestation-compliance-policies/policies/cia_requirements_set_for_asset.rego b/compliance/attestation-compliance-policies/policies/cia_requirements_set_for_asset.rego index e8701f162..f660ecc65 100644 --- a/compliance/attestation-compliance-policies/policies/cia_requirements_set_for_asset.rego +++ b/compliance/attestation-compliance-policies/policies/cia_requirements_set_for_asset.rego @@ -9,8 +9,10 @@ # tags: # - ISO 27001 # - A.5.12 Classification of Information -# complianceFrameworks: -# - ISO 27001 +# policyFrameworks: +# - framework: ISO 27001 +# controls: +# - A.5.12 package compliance import rego.v1 diff --git a/compliance/attestation-compliance-policies/policies/code_review_for_changes_on_default_branch.rego b/compliance/attestation-compliance-policies/policies/code_review_for_changes_on_default_branch.rego index 409b034a7..1ba15c1bc 100644 --- a/compliance/attestation-compliance-policies/policies/code_review_for_changes_on_default_branch.rego +++ b/compliance/attestation-compliance-policies/policies/code_review_for_changes_on_default_branch.rego @@ -9,8 +9,10 @@ # tags: # - ISO 27001 # - A.8.4 Access to source code -# complianceFrameworks: -# - ISO 27001 +# policyFrameworks: +# - framework: ISO 27001 +# controls: +# - A.8.4 package compliance import rego.v1 diff --git a/compliance/attestation-compliance-policies/policies/container_scanning_executed.rego b/compliance/attestation-compliance-policies/policies/container_scanning_executed.rego index 29cad1ee0..aa2e644e2 100644 --- a/compliance/attestation-compliance-policies/policies/container_scanning_executed.rego +++ b/compliance/attestation-compliance-policies/policies/container_scanning_executed.rego @@ -9,8 +9,10 @@ # tags: # - ISO 27001 # - A.5.7 Threat intelligence -# complianceFrameworks: -# - ISO 27001 +# policyFrameworks: +# - framework: ISO 27001 +# controls: +# - A.5.7 package compliance import rego.v1 diff --git a/compliance/attestation-compliance-policies/policies/current_sbom_is_present.rego b/compliance/attestation-compliance-policies/policies/current_sbom_is_present.rego index a5a5d8aff..9dc027d23 100644 --- a/compliance/attestation-compliance-policies/policies/current_sbom_is_present.rego +++ b/compliance/attestation-compliance-policies/policies/current_sbom_is_present.rego @@ -10,8 +10,12 @@ # - A.5.7 Threat intelligence # - A.5.9 Inventory of information and other associated assets # - A.8.8 Management of technical vulnerabilities -# complianceFrameworks: -# - ISO 27001 +# policyFrameworks: +# - framework: ISO 27001 +# controls: +# - A.5.7 +# - A.5.9 +# - A.8.8 package compliance import rego.v1 diff --git a/compliance/attestation-compliance-policies/policies/notification_channel_for_new_vulnerabilities.rego b/compliance/attestation-compliance-policies/policies/notification_channel_for_new_vulnerabilities.rego index 53852a086..935a6310a 100644 --- a/compliance/attestation-compliance-policies/policies/notification_channel_for_new_vulnerabilities.rego +++ b/compliance/attestation-compliance-policies/policies/notification_channel_for_new_vulnerabilities.rego @@ -9,8 +9,10 @@ # tags: # - ISO 27001 # - A.8.8 Management of technical vulnerabilities -# complianceFrameworks: -# - ISO 27001 +# policyFrameworks: +# - framework: ISO 27001 +# controls: +# - A.8.8 package compliance import rego.v1 diff --git a/compliance/attestation-compliance-policies/policies/only_osi_approved_licenses.rego b/compliance/attestation-compliance-policies/policies/only_osi_approved_licenses.rego index 3a4beb60c..9cc3884ce 100644 --- a/compliance/attestation-compliance-policies/policies/only_osi_approved_licenses.rego +++ b/compliance/attestation-compliance-policies/policies/only_osi_approved_licenses.rego @@ -9,8 +9,10 @@ # tags: # - ISO 27001 # - A.5.32 Intellectual property rights -# complianceFrameworks: -# - ISO 27001 +# policyFrameworks: +# - framework: ISO 27001 +# controls: +# - A.5.32 package compliance import rego.v1 diff --git a/compliance/attestation-compliance-policies/policies/secret_scanning_executed.rego b/compliance/attestation-compliance-policies/policies/secret_scanning_executed.rego index 8f1b51f08..1743cbe2e 100644 --- a/compliance/attestation-compliance-policies/policies/secret_scanning_executed.rego +++ b/compliance/attestation-compliance-policies/policies/secret_scanning_executed.rego @@ -9,8 +9,11 @@ # tags: # - ISO 27001 # - A.5.7 Threat intelligence -# complianceFrameworks: -# - ISO 27001 +# policyFrameworks: +# - framework: ISO 27001 +# controls: +# - A.5.7 + package compliance import rego.v1 diff --git a/compliance/attestation-compliance-policies/policies/security_policy_present_in_repo.rego b/compliance/attestation-compliance-policies/policies/security_policy_present_in_repo.rego index 9febffac9..caa2965d2 100644 --- a/compliance/attestation-compliance-policies/policies/security_policy_present_in_repo.rego +++ b/compliance/attestation-compliance-policies/policies/security_policy_present_in_repo.rego @@ -8,9 +8,9 @@ # - https://github.com/ossf/scorecard/blob/main/docs/checks.md#security-policy # tags: # - Best Practices -# complianceFrameworks: -# - Best Practices -# - OpenSSF Scorecard +# policyFrameworks: +# - framework: Best Practices +# - framework: OpenSSF Scorecard package compliance import rego.v1 diff --git a/compliance/attestation-compliance-policies/policies/signed_off_commit.rego b/compliance/attestation-compliance-policies/policies/signed_off_commit.rego index 18ca7587a..137562c94 100644 --- a/compliance/attestation-compliance-policies/policies/signed_off_commit.rego +++ b/compliance/attestation-compliance-policies/policies/signed_off_commit.rego @@ -7,7 +7,7 @@ # relatedResources: [] # tags: # - Legal -# complianceFrameworks: [] +# policyFrameworks: [] package compliance import rego.v1 diff --git a/compliance/attestation-compliance-policies/policies/software_composition_analysis_executed.rego b/compliance/attestation-compliance-policies/policies/software_composition_analysis_executed.rego index f1e917653..edbe1fed3 100644 --- a/compliance/attestation-compliance-policies/policies/software_composition_analysis_executed.rego +++ b/compliance/attestation-compliance-policies/policies/software_composition_analysis_executed.rego @@ -9,8 +9,10 @@ # tags: # - ISO 27001 # - A.5.7 Threat intelligence -# complianceFrameworks: -# - ISO 27001 +# policyFrameworks: +# - framework: ISO 27001 +# controls: +# - A.5.7 package compliance import rego.v1 diff --git a/compliance/attestation-compliance-policies/policies/uses_sbom.rego b/compliance/attestation-compliance-policies/policies/uses_sbom.rego index 24257b161..b9c697213 100644 --- a/compliance/attestation-compliance-policies/policies/uses_sbom.rego +++ b/compliance/attestation-compliance-policies/policies/uses_sbom.rego @@ -10,8 +10,12 @@ # - A.5.7 Threat intelligence # - A.5.9 Inventory of information and other associated assets # - A.8.8 Management of technical vulnerabilities -# complianceFrameworks: -# - ISO 27001 +# policyFrameworks: +# - framework: ISO 27001 +# controls: +# - A.5.7 +# - A.5.9 +# - A.8.8 package compliance import rego.v1 diff --git a/compliance/attestation-compliance-policies/policies/vulnerability_fix_time_sla.rego b/compliance/attestation-compliance-policies/policies/vulnerability_fix_time_sla.rego index 03ca17bb5..69ff2ebad 100644 --- a/compliance/attestation-compliance-policies/policies/vulnerability_fix_time_sla.rego +++ b/compliance/attestation-compliance-policies/policies/vulnerability_fix_time_sla.rego @@ -7,7 +7,10 @@ # relatedResources: [] # tags: # - Security -# complianceFrameworks: [] +# policyFrameworks: +# - framework: L3montree Compliance +# controls: +# - Vulnerability Management package compliance import rego.v1 @@ -20,13 +23,55 @@ high_max_hours := 72 medium_max_hours := 336 low_max_hours := 720 -compliant if { +violations contains msg if { + input.type == "https://devguard.org/attestation/asset-metrics/v1" + input.meanTimeToRemediate.riskCriticalAvgHours == 0 + msg := "Mean time to remediate for critical vulnerabilities is 0 — no data available" +} + +violations contains msg if { + input.type == "https://devguard.org/attestation/asset-metrics/v1" + input.meanTimeToRemediate.riskHighAvgHours == 0 + msg := "Mean time to remediate for high vulnerabilities is 0 — no data available" +} + +violations contains msg if { + input.type == "https://devguard.org/attestation/asset-metrics/v1" + input.meanTimeToRemediate.riskMediumAvgHours == 0 + msg := "Mean time to remediate for medium vulnerabilities is 0 — no data available" +} + +violations contains msg if { + input.type == "https://devguard.org/attestation/asset-metrics/v1" + input.meanTimeToRemediate.riskLowAvgHours == 0 + msg := "Mean time to remediate for low vulnerabilities is 0 — no data available" +} + +violations contains msg if { + input.type == "https://devguard.org/attestation/asset-metrics/v1" + input.meanTimeToRemediate.riskCriticalAvgHours >= critical_max_hours + msg := sprintf("Mean time to remediate critical vulnerabilities (%d h) exceeds SLA of %d h", [input.meanTimeToRemediate.riskCriticalAvgHours, critical_max_hours]) +} + +violations contains msg if { + input.type == "https://devguard.org/attestation/asset-metrics/v1" + input.meanTimeToRemediate.riskHighAvgHours >= high_max_hours + msg := sprintf("Mean time to remediate high vulnerabilities (%d h) exceeds SLA of %d h", [input.meanTimeToRemediate.riskHighAvgHours, high_max_hours]) +} + +violations contains msg if { input.type == "https://devguard.org/attestation/asset-metrics/v1" + input.meanTimeToRemediate.riskMediumAvgHours >= medium_max_hours + msg := sprintf("Mean time to remediate medium vulnerabilities (%d h) exceeds SLA of %d h", [input.meanTimeToRemediate.riskMediumAvgHours, medium_max_hours]) +} - mttr := input.meanTimeToRemediate +violations contains msg if { + input.type == "https://devguard.org/attestation/asset-metrics/v1" + input.meanTimeToRemediate.riskLowAvgHours >= low_max_hours + msg := sprintf("Mean time to remediate low vulnerabilities (%d h) exceeds SLA of %d h", [input.meanTimeToRemediate.riskLowAvgHours, low_max_hours]) +} - mttr.riskCriticalAvgHours < critical_max_hours - mttr.riskHighAvgHours < high_max_hours - mttr.riskMediumAvgHours < medium_max_hours - mttr.riskLowAvgHours < low_max_hours +compliant if { + input.type == "https://devguard.org/attestation/asset-metrics/v1" + count(violations) == 0 } diff --git a/compliance/rego.go b/compliance/rego.go index 0f4bf1bea..775e7ceb0 100644 --- a/compliance/rego.go +++ b/compliance/rego.go @@ -22,9 +22,9 @@ type yamlPolicy struct { } type customYaml struct { - Description string `yaml:"description"` - Priority int `yaml:"priority"` - Tags []string + Description string `yaml:"description"` + Priority int `yaml:"priority"` + Tags []string `yaml:"tags"` // used for mapping from policies to attestations PredicateType string `yaml:"predicateType"` RelatedResources []string `yaml:"relatedResources"` @@ -116,8 +116,19 @@ type PolicyEvaluation struct { } func Eval(policy PolicyFS, input any) PolicyEvaluation { + result := PolicyEvaluation{ + PolicyID: policy.Filename, + PolicyTitle: policy.Title, + PolicyDescription: policy.Description, + PolicyRelatedResources: policy.RelatedResources, + PolicyTags: policy.Tags, + PolicyPriority: policy.Priority, + PolicyFrameworks: policy.PolicyFrameworks, + EvidenceType: "json", + EvidenceContent: &policy.Content, + } if input == nil { - return PolicyEvaluation{Compliant: nil} + return result } r := rego.New( @@ -128,12 +139,12 @@ func Eval(policy PolicyFS, input any) PolicyEvaluation { ctx := context.TODO() query, err := r.PrepareForEval(ctx) if err != nil { - return PolicyEvaluation{Compliant: nil} + return result } rs, err := query.Eval(ctx, rego.EvalInput(input)) if err != nil { - return PolicyEvaluation{Compliant: nil} + return result } var violations []string @@ -156,20 +167,11 @@ func Eval(policy PolicyFS, input any) PolicyEvaluation { } } - return PolicyEvaluation{ - PolicyID: policy.Filename, - PolicyTitle: policy.Title, - PolicyDescription: policy.Description, - PolicyRelatedResources: policy.RelatedResources, - PolicyTags: policy.Tags, - PolicyPriority: policy.Priority, - EvidenceType: "json", - PolicyFrameworks: policy.PolicyFrameworks, - Compliant: compliant, - Violations: violations, - RawEvaluationResult: rawEvalResult, - EvidenceContent: &policy.Content, - } + result.Compliant = compliant + result.Violations = violations + result.RawEvaluationResult = rawEvalResult + + return result } // embed the policies in the binary @@ -221,16 +223,16 @@ func PolicyFSFromContent(fileName, content string) (PolicyFS, error) { } func BuildSarifFromPolicies(srcPath string, evaluations []PolicyEvaluation) sarif.SarifSchema210Json { - rules := make([]sarif.ReportingDescriptor, 0, len(evaluations)) - results := make([]sarif.Result, 0) - seenResults := make(map[string]bool) - addResult := func(r sarif.Result) { - key := string(r.Kind) + "|" + r.Message.Text - if !seenResults[key] { - seenResults[key] = true - results = append(results, r) + rules := make([]sarif.ReportingDescriptor, 0) + results := make([]sarif.Result, 0, len(evaluations)) + seenRules := make(map[string]bool) + addRule := func(r sarif.ReportingDescriptor) { + if !seenRules[r.ID] { + seenRules[r.ID] = true + rules = append(rules, r) } } + for _, evaluation := range evaluations { ruleID := evaluation.PolicyID ruleName := evaluation.PolicyTitle @@ -263,12 +265,13 @@ func BuildSarifFromPolicies(srcPath string, evaluations []PolicyEvaluation) sari }, } - rules = append(rules, rule) + addRule(rule) artifactLocation := sarif.ArtifactLocation{URI: &srcPath} additionalProps := map[string]any{ "precision": "high", "evidenceType": evaluation.EvidenceType, + "violations": evaluation.Violations, } if evaluation.EvidenceContent != nil { additionalProps["evidenceContent"] = *evaluation.EvidenceContent @@ -278,50 +281,31 @@ func BuildSarifFromPolicies(srcPath string, evaluations []PolicyEvaluation) sari Tags: evaluation.PolicyTags, AdditionalProperties: additionalProps, } - - if evaluation.Compliant == nil { - addResult(sarif.Result{ - Kind: sarif.ResultKindOpen, - RuleID: &ruleID, - Message: sarif.Message{ - Text: "No attestation found for policy — compliance could not be determined.", - }, - Locations: []sarif.Location{ - {PhysicalLocation: sarif.PhysicalLocation{ArtifactLocation: artifactLocation}}, - }, - Properties: props, - }) - continue + var kind sarif.ResultKind + var message sarif.Message + var result sarif.Result + if evaluation.Compliant != nil && *evaluation.Compliant { + kind = sarif.ResultKindPass + message = sarif.Message{Text: "Policy compliant"} + } else if evaluation.Compliant != nil && !*evaluation.Compliant { + kind = sarif.ResultKindFail + message = sarif.Message{Text: "Policy not compliant"} + } else { + kind = sarif.ResultKindOpen + message = sarif.Message{Text: "No attestation found for policy — compliance could not be determined."} } - if *evaluation.Compliant { - addResult(sarif.Result{ - Kind: sarif.ResultKindPass, - RuleID: &ruleID, - Message: sarif.Message{ - Text: "Policy compliant", - }, - Locations: []sarif.Location{ - {PhysicalLocation: sarif.PhysicalLocation{ArtifactLocation: artifactLocation}}, - }, - Properties: props, - }) - continue + result = sarif.Result{ + Kind: kind, + RuleID: &ruleID, + Message: message, + Locations: []sarif.Location{ + {PhysicalLocation: sarif.PhysicalLocation{ArtifactLocation: artifactLocation}}, + }, + Properties: props, } - for _, violation := range evaluation.Violations { - addResult(sarif.Result{ - Kind: sarif.ResultKindFail, - RuleID: &ruleID, - Message: sarif.Message{ - Text: violation, - }, - Locations: []sarif.Location{ - {PhysicalLocation: sarif.PhysicalLocation{ArtifactLocation: artifactLocation}}, - }, - Properties: props, - }) - } + results = append(results, result) } driver := sarif.ToolComponent{ diff --git a/database/migrations/20260602000000_add_compliance_risks_drop_policies.up.sql b/database/migrations/20260602000000_add_compliance_risks_drop_policies.up.sql index 7a3b7fcb2..8e646f129 100644 --- a/database/migrations/20260602000000_add_compliance_risks_drop_policies.up.sql +++ b/database/migrations/20260602000000_add_compliance_risks_drop_policies.up.sql @@ -19,12 +19,12 @@ CREATE TABLE IF NOT EXISTS public.compliance_risks ( policy_title text NOT NULL DEFAULT '', policy_description text, evidence_type text NOT NULL DEFAULT '', - policy_related_resources text[], - policy_tags text[], + policy_related_resources jsonb DEFAULT '[]', + policy_tags jsonb DEFAULT '[]', policy_priority integer, "policyFrameworks" jsonb, evidence_content bytea, - evidence_violations text[], + violations jsonb DEFAULT '[]', CONSTRAINT compliance_risks_pkey PRIMARY KEY (id), CONSTRAINT fk_compliance_risks_asset_versions FOREIGN KEY (asset_version_name, asset_id) REFERENCES public.asset_versions (name, asset_id) ON DELETE CASCADE diff --git a/database/models/compliance_risk_model.go b/database/models/compliance_risk_model.go index 8888e66be..5ccab1c56 100644 --- a/database/models/compliance_risk_model.go +++ b/database/models/compliance_risk_model.go @@ -29,14 +29,16 @@ type ComplianceRisk struct { PolicyID string `json:"policyId" gorm:"type:text;"` PolicyTitle string `json:"policyTitle" gorm:"type:text;"` PolicyDescription *string `json:"policyDescription" gorm:"type:text;"` - PolicyRelatedResources []string `json:"policyRelatedResources" gorm:"type:text[];"` - PolicyTags []string `json:"policyTags" gorm:"type:text[];"` + PolicyRelatedResources []string `json:"policyRelatedResources" gorm:"type:jsonb;serializer:json"` + PolicyTags []string `json:"policyTags" gorm:"type:jsonb;serializer:json"` PolicyPriority int `json:"policyPriority"` PolicyFrameworks []dtos.PolicyFrameworks `json:"policyFrameworks" gorm:"column:policyFrameworks;type:jsonb;serializer:json"` EvidenceType string `json:"evidenceType" gorm:"type:text;"` EvidenceContent []byte `json:"evidenceContent" gorm:"type:bytea;"` - Violations []string `json:"violations" gorm:"type:text[];"` + Message string `json:"message" gorm:"type:text;"` + + Violations []string `json:"violations" gorm:"type:jsonb;serializer:json"` Events []VulnEvent `gorm:"foreignKey:ComplianceRiskID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;" json:"events"` diff --git a/dtos/compliance_risk_dto.go b/dtos/compliance_risk_dto.go index e62501d87..eff914239 100644 --- a/dtos/compliance_risk_dto.go +++ b/dtos/compliance_risk_dto.go @@ -31,6 +31,7 @@ type ComplianceRiskDTO struct { TicketURL *string `json:"ticketUrl"` ManualTicketCreation bool `json:"manualTicketCreation"` + Message string `json:"message"` EvidenceType string `json:"evidenceType"` Violations []string `json:"Violations"` } diff --git a/services/compliance_risk_service.go b/services/compliance_risk_service.go index fdf6e6d23..c2e2a28f3 100644 --- a/services/compliance_risk_service.go +++ b/services/compliance_risk_service.go @@ -247,159 +247,176 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode if len(sarifDoc.Runs) == 0 { return nil } - run := sarifDoc.Runs[0] - - type ruleInfo struct { - title string - description *string - relatedResources []string - tags []string - priority int - policyFrameworks []dtos.PolicyFrameworks - } - ruleMap := make(map[string]ruleInfo, len(run.Tool.Driver.Rules)) - for _, rule := range run.Tool.Driver.Rules { - var desc *string - if rule.FullDescription != nil && rule.FullDescription.Text != "" { - d := rule.FullDescription.Text - desc = &d - } - title := rule.ID - if rule.ShortDescription != nil { - title = rule.ShortDescription.Text + risks := make([]models.ComplianceRisk, 0) + for _, run := range sarifDoc.Runs { + + type ruleInfo struct { + title string + description *string + relatedResources []string + tags []string + priority int + policyFrameworks []dtos.PolicyFrameworks } + ruleMap := make(map[string]ruleInfo, len(run.Tool.Driver.Rules)) + for _, rule := range run.Tool.Driver.Rules { + var desc *string + if rule.FullDescription != nil && rule.FullDescription.Text != "" { + d := rule.FullDescription.Text + desc = &d + } - relatedResources := make([]string, 0) - if rule.Properties != nil { - if rr, ok := rule.Properties.AdditionalProperties["relatedResources"].([]any); ok { - for _, r := range rr { - if rStr, ok := r.(string); ok { - relatedResources = append(relatedResources, rStr) - } - } + title := rule.ID + if rule.ShortDescription != nil { + title = rule.ShortDescription.Text } - } - var tags []string - if rule.Properties != nil { - tags = rule.Properties.Tags - } + var tags []string + if rule.Properties != nil { + tags = rule.Properties.Tags + } - var policyFrameworks []dtos.PolicyFrameworks - if rule.Properties != nil { - if cf, ok := rule.Properties.AdditionalProperties["policyFrameworks"].([]any); ok { - for _, c := range cf { - if cMap, ok := c.(map[string]any); ok { - pc := dtos.PolicyFrameworks{} - if fw, ok := cMap["framework"].(string); ok { - pc.Framework = fw + relatedResources := make([]string, 0) + if rule.Properties != nil { + if rr, ok := rule.Properties.AdditionalProperties["relatedResources"].([]string); ok { + relatedResources = rr + } else if rr, ok := rule.Properties.AdditionalProperties["relatedResources"].([]any); ok { + for _, r := range rr { + if s, ok := r.(string); ok { + relatedResources = append(relatedResources, s) } - if ctls, ok := cMap["controls"].([]any); ok { - for _, ctl := range ctls { - if s, ok := ctl.(string); ok { - pc.Controls = append(pc.Controls, s) + } + } + } + + var policyFrameworks []dtos.PolicyFrameworks + if rule.Properties != nil { + if direct, ok := rule.Properties.AdditionalProperties["policyFrameworks"].([]dtos.PolicyFrameworks); ok { + policyFrameworks = direct + } else if cf, ok := rule.Properties.AdditionalProperties["policyFrameworks"].([]any); ok { + for _, c := range cf { + if cMap, ok := c.(map[string]any); ok { + pc := dtos.PolicyFrameworks{} + if fw, ok := cMap["framework"].(string); ok { + pc.Framework = fw + } + if ctls, ok := cMap["controls"].([]any); ok { + for _, ctl := range ctls { + if s, ok := ctl.(string); ok { + pc.Controls = append(pc.Controls, s) + } } } + policyFrameworks = append(policyFrameworks, pc) } - policyFrameworks = append(policyFrameworks, pc) } } } - } - var priority int - if rule.Properties != nil { - if p, ok := rule.Properties.AdditionalProperties["priority"].(int); ok { - priority = p - } else if pFloat, ok := rule.Properties.AdditionalProperties["priority"].(float64); ok { - priority = int(pFloat) + var priority int + if rule.Properties != nil { + if p, ok := rule.Properties.AdditionalProperties["priority"].(int); ok { + priority = p + } else if pFloat, ok := rule.Properties.AdditionalProperties["priority"].(float64); ok { + priority = int(pFloat) + } } - } - - ruleMap[rule.ID] = ruleInfo{title: title, description: desc, relatedResources: relatedResources, tags: tags, priority: priority, policyFrameworks: policyFrameworks} - } - - type policyResult struct { - kind sarif.ResultKind - violations []string - evidenceContent []byte - evidenceType string - } - resultMap := make(map[string]*policyResult, len(ruleMap)) - for _, result := range run.Results { - if result.RuleID == nil { - continue - } - ruleID := *result.RuleID - pr := resultMap[ruleID] - if pr == nil { - pr = &policyResult{} - resultMap[ruleID] = pr + ruleMap[rule.ID] = ruleInfo{title: title, description: desc, relatedResources: relatedResources, tags: tags, priority: priority, policyFrameworks: policyFrameworks} } - if result.Properties != nil { - if ac, ok := result.Properties.AdditionalProperties["evidenceContent"].(string); ok && pr.evidenceContent == nil { - pr.evidenceContent = []byte(ac) - } - if et, ok := result.Properties.AdditionalProperties["evidenceType"].(string); ok { - pr.evidenceType = et - } + type policyResult struct { + kind sarif.ResultKind + message sarif.Message + violations []string + evidenceContent []byte + evidenceType string } + resultMap := make(map[string]*policyResult, len(ruleMap)) - switch result.Kind { - case sarif.ResultKindFail: - pr.kind = sarif.ResultKindFail - pr.violations = append(pr.violations, result.Message.Text) - case sarif.ResultKindOpen: - if pr.kind != sarif.ResultKindFail { - pr.kind = sarif.ResultKindOpen + for _, result := range run.Results { + if result.RuleID == nil { + continue } - case sarif.ResultKindPass: - if pr.kind == "" { - pr.kind = sarif.ResultKindPass + ruleID := *result.RuleID + pr := resultMap[ruleID] + if pr == nil { + pr = &policyResult{} + resultMap[ruleID] = pr } - } - } + pr.message = result.Message - risks := make([]models.ComplianceRisk, 0, len(ruleMap)) - for ruleID, info := range ruleMap { - state := dtos.VulnStateOpen - var violations []string - var evidenceContent []byte + if result.Properties != nil { + if ac, ok := result.Properties.AdditionalProperties["evidenceContent"].(string); ok && pr.evidenceContent == nil { + pr.evidenceContent = []byte(ac) + } + if et, ok := result.Properties.AdditionalProperties["evidenceType"].(string); ok { + pr.evidenceType = et + } + if v, ok := result.Properties.AdditionalProperties["violations"].([]string); ok { + pr.violations = v + } + } - if pr := resultMap[ruleID]; pr != nil { - evidenceContent = pr.evidenceContent - switch pr.kind { - case sarif.ResultKindPass: - state = dtos.VulnStateFixed + switch result.Kind { case sarif.ResultKindFail: - state = dtos.VulnStateOpen - violations = pr.violations + pr.kind = sarif.ResultKindFail + case sarif.ResultKindOpen: + if pr.kind != sarif.ResultKindFail { + pr.kind = sarif.ResultKindOpen + } + case sarif.ResultKindPass: + if pr.kind == "" { + pr.kind = sarif.ResultKindPass + } } + } - risks = append(risks, models.ComplianceRisk{ - Vulnerability: models.Vulnerability{ - AssetVersionName: assetVersion.Name, - AssetID: assetVersion.AssetID, - AssetVersion: assetVersion, - State: state, - LastDetected: time.Now(), - }, - PolicyID: ruleID, - PolicyTitle: info.title, - PolicyDescription: info.description, - PolicyRelatedResources: info.relatedResources, - PolicyTags: info.tags, - PolicyPriority: info.priority, - PolicyFrameworks: info.policyFrameworks, - EvidenceType: resultMap[ruleID].evidenceType, - Violations: violations, - EvidenceContent: evidenceContent, - }) + for ruleID, info := range ruleMap { + state := dtos.VulnStateOpen + var violations []string + var evidenceContent []byte + var evidenceType string + var message string + + if pr := resultMap[ruleID]; pr != nil { + evidenceContent = pr.evidenceContent + switch pr.kind { + case sarif.ResultKindPass: + state = dtos.VulnStateFixed + case sarif.ResultKindFail: + state = dtos.VulnStateOpen + violations = pr.violations + } + evidenceType = pr.evidenceType + message = pr.message.Text + + } + + risks = append(risks, models.ComplianceRisk{ + Vulnerability: models.Vulnerability{ + AssetVersionName: assetVersion.Name, + AssetID: assetVersion.AssetID, + AssetVersion: assetVersion, + State: state, + LastDetected: time.Now(), + }, + PolicyID: ruleID, + PolicyTitle: info.title, + PolicyDescription: info.description, + PolicyRelatedResources: info.relatedResources, + PolicyTags: info.tags, + PolicyPriority: info.priority, + PolicyFrameworks: info.policyFrameworks, + EvidenceType: evidenceType, + Violations: violations, + EvidenceContent: evidenceContent, + Message: message, + }) + } } return risks diff --git a/transformer/compliance_risk_transformer.go b/transformer/compliance_risk_transformer.go index b2b24a5dd..a4d2a8844 100644 --- a/transformer/compliance_risk_transformer.go +++ b/transformer/compliance_risk_transformer.go @@ -34,5 +34,6 @@ func ComplianceRiskToDTO(r models.ComplianceRisk) dtos.ComplianceRiskDTO { EvidenceType: r.EvidenceType, Violations: r.Violations, Artifacts: artifacts, + Message: r.Message, } } From fa40329434a80e482c0296ca8c1b3b5243e1d353 Mon Sep 17 00:00:00 2001 From: rafi Date: Wed, 10 Jun 2026 14:38:42 +0200 Subject: [PATCH 20/26] feat: refine compliance risk and attestation evaluation logic Signed-off-by: rafi --- cmd/devguard-scanner/scanner/eval_policy.go | 32 ++++----- compliance/rego.go | 76 ++++++++++----------- compliance/rego_test.go | 30 ++++---- controllers/compliance_risk_controller.go | 6 +- daemons/attestation_daemon.go | 2 +- database/models/compliance_risk_model.go | 22 +++--- mocks/mock_ComplianceService.go | 22 +++--- router/compliance_risk_router.go | 2 +- services/compliance_risk_service.go | 8 +-- services/compliance_service.go | 4 +- shared/common_interfaces.go | 2 +- transformer/compliance_risk_transformer.go | 10 ++- 12 files changed, 113 insertions(+), 103 deletions(-) diff --git a/cmd/devguard-scanner/scanner/eval_policy.go b/cmd/devguard-scanner/scanner/eval_policy.go index bbb390a54..5464e1e09 100644 --- a/cmd/devguard-scanner/scanner/eval_policy.go +++ b/cmd/devguard-scanner/scanner/eval_policy.go @@ -32,35 +32,33 @@ func EvaluatePolicyAgainstAttestations(srcPath string, policyPath string, attest return nil, nil, fmt.Errorf("could not read policy file: %w", err) } - policy, err := compliance.PolicyFSFromContent(filepath.Base(policyPath), string(content)) + policy, err := compliance.GetPolicyFromFile(filepath.Base(policyPath), string(content)) if err != nil { return nil, nil, fmt.Errorf("could not parse policy: %w", err) } evaluations := make([]compliance.PolicyEvaluation, 0) -foundMatch: - for _, attestation := range attestations { + var eval compliance.PolicyEvaluation predicateType, _ := attestation["predicateType"].(string) if predicateType != policy.PredicateType { - continue - } - raw, err := json.Marshal(attestation) - if err != nil { - return nil, nil, fmt.Errorf("could not marshal attestation: %w", err) + eval = compliance.Eval(policy, nil) + } else { + raw, err := json.Marshal(attestation) + if err != nil { + return nil, nil, fmt.Errorf("could not marshal attestation: %w", err) + } + input, err := utils.ExtractAttestationPayload(string(raw)) + if err != nil { + return nil, nil, fmt.Errorf("could not extract attestation payload: %w", err) + } + eval = compliance.Eval(policy, input) } - input, err := utils.ExtractAttestationPayload(string(raw)) - if err != nil { - return nil, nil, fmt.Errorf("could not extract attestation payload: %w", err) - } - eval := compliance.Eval(policy, input) + evaluations = append(evaluations, eval) - continue foundMatch } - eval := compliance.Eval(policy, nil) - evaluations = append(evaluations, eval) - sarifResult := compliance.BuildSarifFromPolicies(srcPath, evaluations) + sarifResult := compliance.BuildSarifFromPoliciesEvaluations(srcPath, evaluations) return &sarifResult, evaluations, nil } diff --git a/compliance/rego.go b/compliance/rego.go index 775e7ceb0..bec68cff7 100644 --- a/compliance/rego.go +++ b/compliance/rego.go @@ -9,7 +9,7 @@ import ( "sort" "strings" - "github.com/l3montree-dev/devguard/dtos" + "github.com/l3montree-dev/devguard/database/models" "github.com/l3montree-dev/devguard/dtos/sarif" "github.com/l3montree-dev/devguard/utils" "github.com/open-policy-agent/opa/v1/rego" @@ -26,28 +26,43 @@ type customYaml struct { Priority int `yaml:"priority"` Tags []string `yaml:"tags"` // used for mapping from policies to attestations - PredicateType string `yaml:"predicateType"` - RelatedResources []string `yaml:"relatedResources"` - PolicyFrameworks []dtos.PolicyFrameworks `yaml:"policyFrameworks"` + PredicateType string `yaml:"predicateType"` + RelatedResources []string `yaml:"relatedResources"` + PolicyFrameworks []models.PolicyFrameworks `yaml:"policyFrameworks"` } type PolicyMetadata struct { - Title string `yaml:"title" json:"title"` - Description string `yaml:"description" json:"description"` - Priority int `yaml:"priority" json:"priority"` - Tags []string `yaml:"tags" json:"tags"` - RelatedResources []string `yaml:"relatedResources" json:"relatedResources"` - PolicyFrameworks []dtos.PolicyFrameworks `yaml:"policyFrameworks" json:"policyFrameworks"` - ComplianceFrameworks []string `yaml:"complianceFrameworks" json:"complianceFrameworks"` - Filename string `json:"filename"` - Content string `json:"content"` - PredicateType string `yaml:"predicateType" json:"predicateType"` + Title string `yaml:"title" json:"title"` + Description string `yaml:"description" json:"description"` + Priority int `yaml:"priority" json:"priority"` + Tags []string `yaml:"tags" json:"tags"` + RelatedResources []string `yaml:"relatedResources" json:"relatedResources"` + PolicyFrameworks []models.PolicyFrameworks `yaml:"policyFrameworks" json:"policyFrameworks"` + ComplianceFrameworks []string `yaml:"complianceFrameworks" json:"complianceFrameworks"` + Filename string `json:"filename"` + Content string `json:"content"` + PredicateType string `yaml:"predicateType" json:"predicateType"` } -type PolicyFS struct { +type Policy struct { PolicyMetadata Content string } +type PolicyEvaluation struct { + PolicyID string + PolicyTitle string + PolicyDescription string + PolicyRelatedResources []string + PolicyTags []string + PolicyPriority int + PolicyFrameworks []models.PolicyFrameworks + Compliant *bool + Violations []string + RawEvaluationResult map[string]any + EvidenceType string + EvidenceContent *string +} + var packageRegexp = regexp.MustCompile(`(?m)^package compliance`) var metadataRegexp = regexp.MustCompile(`^\s*#\s*METADATA`) @@ -100,22 +115,7 @@ func parseMetadata(fileName string, content string) (PolicyMetadata, error) { }, nil } -type PolicyEvaluation struct { - PolicyID string - PolicyTitle string - PolicyDescription string - PolicyRelatedResources []string - PolicyTags []string - PolicyPriority int - PolicyFrameworks []dtos.PolicyFrameworks - Compliant *bool - Violations []string - RawEvaluationResult map[string]any - EvidenceType string - EvidenceContent *string -} - -func Eval(policy PolicyFS, input any) PolicyEvaluation { +func Eval(policy Policy, input any) PolicyEvaluation { result := PolicyEvaluation{ PolicyID: policy.Filename, PolicyTitle: policy.Title, @@ -179,14 +179,14 @@ func Eval(policy PolicyFS, input any) PolicyEvaluation { //go:embed attestation-compliance-policies/policies/*.rego var policiesFs embed.FS -func GetPoliciesFromFS(policyDir string) ([]PolicyFS, error) { +func GetPoliciesFromFS(policyDir string) ([]Policy, error) { // fetch all policies policyFiles, err := policiesFs.ReadDir(policyDir) if err != nil { return nil, err } - var policies []PolicyFS + var policies []Policy for _, file := range policyFiles { content, err := policiesFs.ReadFile(filepath.Join(policyDir, file.Name())) if err != nil { @@ -198,7 +198,7 @@ func GetPoliciesFromFS(policyDir string) ([]PolicyFS, error) { return nil, err } - policy := PolicyFS{ + policy := Policy{ PolicyMetadata: metadata, Content: string(content), } @@ -214,15 +214,15 @@ func GetPoliciesFromFS(policyDir string) ([]PolicyFS, error) { return policies, nil } -func PolicyFSFromContent(fileName, content string) (PolicyFS, error) { +func GetPolicyFromFile(fileName, content string) (Policy, error) { metadata, err := parseMetadata(fileName, content) if err != nil { - return PolicyFS{}, err + return Policy{}, err } - return PolicyFS{PolicyMetadata: metadata, Content: content}, nil + return Policy{PolicyMetadata: metadata, Content: content}, nil } -func BuildSarifFromPolicies(srcPath string, evaluations []PolicyEvaluation) sarif.SarifSchema210Json { +func BuildSarifFromPoliciesEvaluations(srcPath string, evaluations []PolicyEvaluation) sarif.SarifSchema210Json { rules := make([]sarif.ReportingDescriptor, 0) results := make([]sarif.Result, 0, len(evaluations)) seenRules := make(map[string]bool) diff --git a/compliance/rego_test.go b/compliance/rego_test.go index 8263a6a4e..a8ddd4258 100644 --- a/compliance/rego_test.go +++ b/compliance/rego_test.go @@ -33,7 +33,7 @@ func TestEval(t *testing.T) { if err != nil { t.Fatal(err) } - policy := PolicyFS{PolicyMetadata: metadata, Content: string(policyContent)} + policy := Policy{PolicyMetadata: metadata, Content: string(policyContent)} // evaluate the policy res := Eval(policy, input) @@ -57,7 +57,7 @@ func TestOnlyOsiApprovedLicensesPolicy(t *testing.T) { if err != nil { t.Fatal(err) } - policy := PolicyFS{PolicyMetadata: metadata, Content: string(policyContent)} + policy := Policy{PolicyMetadata: metadata, Content: string(policyContent)} // parse the sbom var input any @@ -124,7 +124,7 @@ func hasDuplicateResults(results []sarif.Result) bool { return false } -func makeEvaluations(policy PolicyFS, evals []PolicyEvaluation) []PolicyEvaluation { +func makeEvaluations(policy Policy, evals []PolicyEvaluation) []PolicyEvaluation { for i := range evals { evals[i].PolicyID = policy.Filename evals[i].PolicyTitle = policy.Title @@ -134,8 +134,8 @@ func makeEvaluations(policy PolicyFS, evals []PolicyEvaluation) []PolicyEvaluati return evals } -func TestBuildSarifFromPolicies_NoDuplicateResults(t *testing.T) { - policy := PolicyFS{ +func TestBuildSarifFromPoliciesEvaluations_NoDuplicateResults(t *testing.T) { + policy := Policy{ PolicyMetadata: PolicyMetadata{ Filename: "test-policy.rego", Title: "Test Policy", @@ -150,9 +150,9 @@ func TestBuildSarifFromPolicies_NoDuplicateResults(t *testing.T) { {Compliant: &compliant, Violations: []string{"missing signature", "untrusted source"}}, {Compliant: &compliant, Violations: []string{"missing signature", "untrusted source"}}, }) - results := BuildSarifFromPolicies("registry.example.com/image:latest", evaluations).Runs[0].Results + results := BuildSarifFromPoliciesEvaluations("registry.example.com/image:latest", evaluations).Runs[0].Results if hasDuplicateResults(results) { - t.Errorf("BuildSarifFromPolicies returned duplicate result entries: %v", results) + t.Errorf("BuildSarifFromPoliciesEvaluations returned duplicate result entries: %v", results) } }) @@ -161,9 +161,9 @@ func TestBuildSarifFromPolicies_NoDuplicateResults(t *testing.T) { evaluations := makeEvaluations(policy, []PolicyEvaluation{ {Compliant: &compliant, Violations: []string{"missing signature", "missing signature"}}, }) - results := BuildSarifFromPolicies("registry.example.com/image:latest", evaluations).Runs[0].Results + results := BuildSarifFromPoliciesEvaluations("registry.example.com/image:latest", evaluations).Runs[0].Results if hasDuplicateResults(results) { - t.Errorf("BuildSarifFromPolicies returned duplicate result entries: %v", results) + t.Errorf("BuildSarifFromPoliciesEvaluations returned duplicate result entries: %v", results) } }) @@ -174,9 +174,9 @@ func TestBuildSarifFromPolicies_NoDuplicateResults(t *testing.T) { {Compliant: &compliant}, {Compliant: &compliant}, }) - results := BuildSarifFromPolicies("registry.example.com/image:latest", evaluations).Runs[0].Results + results := BuildSarifFromPoliciesEvaluations("registry.example.com/image:latest", evaluations).Runs[0].Results if hasDuplicateResults(results) { - t.Errorf("BuildSarifFromPolicies returned duplicate pass result entries: %v", results) + t.Errorf("BuildSarifFromPoliciesEvaluations returned duplicate pass result entries: %v", results) } }) @@ -189,9 +189,9 @@ func TestBuildSarifFromPolicies_NoDuplicateResults(t *testing.T) { {Compliant: ¬Compliant, Violations: []string{"missing signature"}}, {Compliant: &compliant}, }) - results := BuildSarifFromPolicies("registry.example.com/image:latest", evaluations).Runs[0].Results + results := BuildSarifFromPoliciesEvaluations("registry.example.com/image:latest", evaluations).Runs[0].Results if hasDuplicateResults(results) { - t.Errorf("BuildSarifFromPolicies returned duplicate result entries: %v", results) + t.Errorf("BuildSarifFromPoliciesEvaluations returned duplicate result entries: %v", results) } }) @@ -199,9 +199,9 @@ func TestBuildSarifFromPolicies_NoDuplicateResults(t *testing.T) { evaluations := makeEvaluations(policy, []PolicyEvaluation{ {Compliant: utils.Ptr(true)}, }) - results := BuildSarifFromPolicies("registry.example.com/image:latest", evaluations).Runs[0].Results + results := BuildSarifFromPoliciesEvaluations("registry.example.com/image:latest", evaluations).Runs[0].Results if hasDuplicateResults(results) { - t.Errorf("BuildSarifFromPolicies returned duplicate result entries: %v", results) + t.Errorf("BuildSarifFromPoliciesEvaluations returned duplicate result entries: %v", results) } }) } diff --git a/controllers/compliance_risk_controller.go b/controllers/compliance_risk_controller.go index 60760fbd5..8171dd547 100644 --- a/controllers/compliance_risk_controller.go +++ b/controllers/compliance_risk_controller.go @@ -203,15 +203,15 @@ func (c *ComplianceRiskController) Mitigate(ctx shared.Context) error { return ctx.JSON(200, convertComplianceRiskToDetailedDTO(risk)) } -// EvaluateArtifactCompliance fetches evaluations via complianceService.ArtifactCompliance and recalculates risks. -func (c *ComplianceRiskController) EvaluateArtifactCompliance(ctx shared.Context) error { +// RunAttestationEvaluation fetches evaluations via complianceService.EvaluateArtifactAttestations and recalculates risks. +func (c *ComplianceRiskController) RunAttestationEvaluation(ctx shared.Context) error { assetVersion := shared.GetAssetVersion(ctx) artifact := shared.GetArtifact(ctx) project := shared.GetProject(ctx) userAgent := ctx.Request().UserAgent() userID := shared.GetSession(ctx).GetUserID() - sarifDoc, err := c.complianceService.ArtifactCompliance(ctx.Request().Context(), project.ID, assetVersion, artifact) + sarifDoc, err := c.complianceService.EvaluateArtifactAttestations(ctx.Request().Context(), project.ID, assetVersion, artifact) if err != nil { return echo.NewHTTPError(500, "could not evaluate artifact compliance").WithInternal(err) } diff --git a/daemons/attestation_daemon.go b/daemons/attestation_daemon.go index 11216a91f..4aa921bef 100644 --- a/daemons/attestation_daemon.go +++ b/daemons/attestation_daemon.go @@ -20,7 +20,7 @@ func (runner *DaemonRunner) CheckArtifactCompliance(input <-chan assetWithProjec for _, assetVersion := range assetWithDetails.assetVersions { for _, artifact := range assetVersion.Artifacts { - sarifDoc, err := runner.complianceService.ArtifactCompliance(stageCtx, assetWithDetails.project.ID, assetVersion, artifact) + sarifDoc, err := runner.complianceService.EvaluateArtifactAttestations(stageCtx, assetWithDetails.project.ID, assetVersion, artifact) if err != nil { slog.Error("could not evaluate artifact compliance", "assetID", assetWithDetails.asset.ID, diff --git a/database/models/compliance_risk_model.go b/database/models/compliance_risk_model.go index 5ccab1c56..f9b80442b 100644 --- a/database/models/compliance_risk_model.go +++ b/database/models/compliance_risk_model.go @@ -23,18 +23,22 @@ import ( "gorm.io/gorm" ) +type PolicyFrameworks struct { + Framework string `yaml:"framework" json:"framework"` + Controls []string `yaml:"controls" json:"controls"` +} type ComplianceRisk struct { Vulnerability - PolicyID string `json:"policyId" gorm:"type:text;"` - PolicyTitle string `json:"policyTitle" gorm:"type:text;"` - PolicyDescription *string `json:"policyDescription" gorm:"type:text;"` - PolicyRelatedResources []string `json:"policyRelatedResources" gorm:"type:jsonb;serializer:json"` - PolicyTags []string `json:"policyTags" gorm:"type:jsonb;serializer:json"` - PolicyPriority int `json:"policyPriority"` - PolicyFrameworks []dtos.PolicyFrameworks `json:"policyFrameworks" gorm:"column:policyFrameworks;type:jsonb;serializer:json"` - EvidenceType string `json:"evidenceType" gorm:"type:text;"` - EvidenceContent []byte `json:"evidenceContent" gorm:"type:bytea;"` + PolicyID string `json:"policyId" gorm:"type:text;"` + PolicyTitle string `json:"policyTitle" gorm:"type:text;"` + PolicyDescription *string `json:"policyDescription" gorm:"type:text;"` + PolicyRelatedResources []string `json:"policyRelatedResources" gorm:"type:jsonb;serializer:json"` + PolicyTags []string `json:"policyTags" gorm:"type:jsonb;serializer:json"` + PolicyPriority int `json:"policyPriority"` + PolicyFrameworks []PolicyFrameworks `json:"policyFrameworks" gorm:"column:policyFrameworks;type:jsonb;serializer:json"` + EvidenceType string `json:"evidenceType" gorm:"type:text;"` + EvidenceContent []byte `json:"evidenceContent" gorm:"type:bytea;"` Message string `json:"message" gorm:"type:text;"` diff --git a/mocks/mock_ComplianceService.go b/mocks/mock_ComplianceService.go index d3ed9263f..f674a5778 100644 --- a/mocks/mock_ComplianceService.go +++ b/mocks/mock_ComplianceService.go @@ -40,12 +40,12 @@ func (_m *ComplianceService) EXPECT() *ComplianceService_Expecter { return &ComplianceService_Expecter{mock: &_m.Mock} } -// ArtifactCompliance provides a mock function for the type ComplianceService -func (_mock *ComplianceService) ArtifactCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) (sarif.SarifSchema210Json, error) { +// EvaluateArtifactAttestations provides a mock function for the type ComplianceService +func (_mock *ComplianceService) EvaluateArtifactAttestations(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) (sarif.SarifSchema210Json, error) { ret := _mock.Called(ctx, projectID, assetVersion, artifact) if len(ret) == 0 { - panic("no return value specified for ArtifactCompliance") + panic("no return value specified for EvaluateArtifactAttestations") } var r0 sarif.SarifSchema210Json @@ -66,21 +66,21 @@ func (_mock *ComplianceService) ArtifactCompliance(ctx context.Context, projectI return r0, r1 } -// ComplianceService_ArtifactCompliance_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ArtifactCompliance' -type ComplianceService_ArtifactCompliance_Call struct { +// ComplianceService_EvaluateArtifactAttestations_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EvaluateArtifactAttestations' +type ComplianceService_EvaluateArtifactAttestations_Call struct { *mock.Call } -// ArtifactCompliance is a helper method to define mock.On call +// EvaluateArtifactAttestations is a helper method to define mock.On call // - ctx context.Context // - projectID uuid.UUID // - assetVersion models.AssetVersion // - artifact models.Artifact -func (_e *ComplianceService_Expecter) ArtifactCompliance(ctx interface{}, projectID interface{}, assetVersion interface{}, artifact interface{}) *ComplianceService_ArtifactCompliance_Call { - return &ComplianceService_ArtifactCompliance_Call{Call: _e.mock.On("ArtifactCompliance", ctx, projectID, assetVersion, artifact)} +func (_e *ComplianceService_Expecter) EvaluateArtifactAttestations(ctx interface{}, projectID interface{}, assetVersion interface{}, artifact interface{}) *ComplianceService_EvaluateArtifactAttestations_Call { + return &ComplianceService_EvaluateArtifactAttestations_Call{Call: _e.mock.On("EvaluateArtifactAttestations", ctx, projectID, assetVersion, artifact)} } -func (_c *ComplianceService_ArtifactCompliance_Call) Run(run func(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact)) *ComplianceService_ArtifactCompliance_Call { +func (_c *ComplianceService_EvaluateArtifactAttestations_Call) Run(run func(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact)) *ComplianceService_EvaluateArtifactAttestations_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -108,12 +108,12 @@ func (_c *ComplianceService_ArtifactCompliance_Call) Run(run func(ctx context.Co return _c } -func (_c *ComplianceService_ArtifactCompliance_Call) Return(sarifSchema210Json sarif.SarifSchema210Json, err error) *ComplianceService_ArtifactCompliance_Call { +func (_c *ComplianceService_EvaluateArtifactAttestations_Call) Return(sarifSchema210Json sarif.SarifSchema210Json, err error) *ComplianceService_EvaluateArtifactAttestations_Call { _c.Call.Return(sarifSchema210Json, err) return _c } -func (_c *ComplianceService_ArtifactCompliance_Call) RunAndReturn(run func(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) (sarif.SarifSchema210Json, error)) *ComplianceService_ArtifactCompliance_Call { +func (_c *ComplianceService_EvaluateArtifactAttestations_Call) RunAndReturn(run func(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) (sarif.SarifSchema210Json, error)) *ComplianceService_EvaluateArtifactAttestations_Call { _c.Call.Return(run) return _c } diff --git a/router/compliance_risk_router.go b/router/compliance_risk_router.go index 7d72272a4..27177e34c 100644 --- a/router/compliance_risk_router.go +++ b/router/compliance_risk_router.go @@ -21,7 +21,7 @@ func NewComplianceRiskRouter( complianceRisksRouter.POST("/:complianceRiskID/", controller.CreateEvent, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) complianceRisksRouter.POST("/:complianceRiskID/mitigate/", controller.Mitigate, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) - complianceRisksRouter.POST("/evaluate/", controller.EvaluateArtifactCompliance, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) + complianceRisksRouter.POST("/evaluate/", controller.RunAttestationEvaluation, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) complianceRisksRouter.POST("/upload-zip/", controller.UploadZip, middlewares.NeededScope([]string{"manage"}), middlewares.DisallowPublicRequests) return ComplianceRiskRouter{Group: complianceRisksRouter} diff --git a/services/compliance_risk_service.go b/services/compliance_risk_service.go index c2e2a28f3..cd39ae0c9 100644 --- a/services/compliance_risk_service.go +++ b/services/compliance_risk_service.go @@ -257,7 +257,7 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode relatedResources []string tags []string priority int - policyFrameworks []dtos.PolicyFrameworks + policyFrameworks []models.PolicyFrameworks } ruleMap := make(map[string]ruleInfo, len(run.Tool.Driver.Rules)) for _, rule := range run.Tool.Driver.Rules { @@ -290,14 +290,14 @@ func sarifToComplianceRisks(sarifDoc sarif.SarifSchema210Json, assetVersion mode } } - var policyFrameworks []dtos.PolicyFrameworks + var policyFrameworks []models.PolicyFrameworks if rule.Properties != nil { - if direct, ok := rule.Properties.AdditionalProperties["policyFrameworks"].([]dtos.PolicyFrameworks); ok { + if direct, ok := rule.Properties.AdditionalProperties["policyFrameworks"].([]models.PolicyFrameworks); ok { policyFrameworks = direct } else if cf, ok := rule.Properties.AdditionalProperties["policyFrameworks"].([]any); ok { for _, c := range cf { if cMap, ok := c.(map[string]any); ok { - pc := dtos.PolicyFrameworks{} + pc := models.PolicyFrameworks{} if fw, ok := cMap["framework"].(string); ok { pc.Framework = fw } diff --git a/services/compliance_service.go b/services/compliance_service.go index 87335db24..41f4cbe86 100644 --- a/services/compliance_service.go +++ b/services/compliance_service.go @@ -34,7 +34,7 @@ func NewComplianceService(attestationRepository shared.AttestationRepository) *C } } -func (s *ComplianceService) ArtifactCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) (sarif.SarifSchema210Json, error) { +func (s *ComplianceService) EvaluateArtifactAttestations(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) (sarif.SarifSchema210Json, error) { attestations, err := s.attestationRepository.GetByArtifactAndAssetVersionAndAssetID(ctx, nil, artifact.ArtifactName, assetVersion.Name, assetVersion.AssetID) if err != nil { return sarif.SarifSchema210Json{}, err @@ -59,5 +59,5 @@ foundMatch: evals = append(evals, compliance.Eval(policy, nil)) } - return compliance.BuildSarifFromPolicies("", evals), nil + return compliance.BuildSarifFromPoliciesEvaluations("", evals), nil } diff --git a/shared/common_interfaces.go b/shared/common_interfaces.go index af1d70233..42234842e 100644 --- a/shared/common_interfaces.go +++ b/shared/common_interfaces.go @@ -170,7 +170,7 @@ type ArtifactRepository interface { } type ComplianceService interface { - ArtifactCompliance(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) (sarif.SarifSchema210Json, error) + EvaluateArtifactAttestations(ctx context.Context, projectID uuid.UUID, assetVersion models.AssetVersion, artifact models.Artifact) (sarif.SarifSchema210Json, error) } type ReleaseRepository interface { diff --git a/transformer/compliance_risk_transformer.go b/transformer/compliance_risk_transformer.go index a4d2a8844..c8b1f59a0 100644 --- a/transformer/compliance_risk_transformer.go +++ b/transformer/compliance_risk_transformer.go @@ -15,6 +15,14 @@ func ComplianceRiskToDTO(r models.ComplianceRisk) dtos.ComplianceRiskDTO { } } + policyFrameworks := make([]dtos.PolicyFrameworks, len(r.PolicyFrameworks)) + for i, pf := range r.PolicyFrameworks { + policyFrameworks[i] = dtos.PolicyFrameworks{ + Framework: pf.Framework, + Controls: pf.Controls, + } + } + return dtos.ComplianceRiskDTO{ ID: r.ID, AssetVersionName: r.AssetVersionName, @@ -30,7 +38,7 @@ func ComplianceRiskToDTO(r models.ComplianceRisk) dtos.ComplianceRiskDTO { PolicyRelatedResources: r.PolicyRelatedResources, PolicyTags: r.PolicyTags, PolicyPriority: r.PolicyPriority, - PolicyFrameworks: r.PolicyFrameworks, + PolicyFrameworks: policyFrameworks, EvidenceType: r.EvidenceType, Violations: r.Violations, Artifacts: artifacts, From 5642008079cc52b9c0c11723d09b6db57d3ff429 Mon Sep 17 00:00:00 2001 From: rafi Date: Wed, 10 Jun 2026 16:30:14 +0200 Subject: [PATCH 21/26] add metadata update Signed-off-by: rafi --- .../policies/vulnerability_fix_time_sla.rego | 1 + services/compliance_risk_service.go | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/compliance/attestation-compliance-policies/policies/vulnerability_fix_time_sla.rego b/compliance/attestation-compliance-policies/policies/vulnerability_fix_time_sla.rego index 69ff2ebad..6523b3d47 100644 --- a/compliance/attestation-compliance-policies/policies/vulnerability_fix_time_sla.rego +++ b/compliance/attestation-compliance-policies/policies/vulnerability_fix_time_sla.rego @@ -11,6 +11,7 @@ # - framework: L3montree Compliance # controls: # - Vulnerability Management + package compliance import rego.v1 diff --git a/services/compliance_risk_service.go b/services/compliance_risk_service.go index cd39ae0c9..be2ea9288 100644 --- a/services/compliance_risk_service.go +++ b/services/compliance_risk_service.go @@ -88,6 +88,11 @@ func (s *ComplianceRiskService) HandleArtifactCompliance(ctx context.Context, tx } return s.complianceRiskRepository.Transaction(ctx, func(db shared.DB) error { + // update policy/evidence metadata + if err := s.complianceRiskRepository.SaveBatch(ctx, db, inBoth); err != nil { + return err + } + // risks that exist on other branches: copy event history if err := s.UserDetectedExistingComplianceRiskOnDifferentBranch(ctx, db, artifact.ArtifactName, branchDiff.ExistingOnOtherBranches, assetVersion); err != nil { slog.Error("error processing existing compliance risk on different branch", "err", err) From 045bc35db4263e6f7ec399c90ecc2cab9f43de3c Mon Sep 17 00:00:00 2001 From: rafi Date: Wed, 10 Jun 2026 17:10:12 +0200 Subject: [PATCH 22/26] update compliance rego evaluation logic Signed-off-by: rafi --- compliance/rego.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/compliance/rego.go b/compliance/rego.go index bec68cff7..9cbd81a9b 100644 --- a/compliance/rego.go +++ b/compliance/rego.go @@ -233,6 +233,14 @@ func BuildSarifFromPoliciesEvaluations(srcPath string, evaluations []PolicyEvalu } } + seenResults := make(map[string]bool) + addResult := func(r sarif.Result) { + if !seenResults[*r.RuleID] { + seenResults[*r.RuleID] = true + results = append(results, r) + } + } + for _, evaluation := range evaluations { ruleID := evaluation.PolicyID ruleName := evaluation.PolicyTitle @@ -305,7 +313,7 @@ func BuildSarifFromPoliciesEvaluations(srcPath string, evaluations []PolicyEvalu Properties: props, } - results = append(results, result) + addResult(result) } driver := sarif.ToolComponent{ From 79673daf7ec7690900a3b9aaba352d75622c6325 Mon Sep 17 00:00:00 2001 From: rafi Date: Wed, 10 Jun 2026 17:14:43 +0200 Subject: [PATCH 23/26] update compliance risk DTO Signed-off-by: rafi --- dtos/compliance_risk_dto.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dtos/compliance_risk_dto.go b/dtos/compliance_risk_dto.go index eff914239..aa221045f 100644 --- a/dtos/compliance_risk_dto.go +++ b/dtos/compliance_risk_dto.go @@ -33,7 +33,7 @@ type ComplianceRiskDTO struct { Message string `json:"message"` EvidenceType string `json:"evidenceType"` - Violations []string `json:"Violations"` + Violations []string `json:"violations"` } type DetailedComplianceRiskDTO struct { From b322977a1af12526a6b7fa474beb09715a3948b0 Mon Sep 17 00:00:00 2001 From: rafi Date: Thu, 11 Jun 2026 09:05:55 +0200 Subject: [PATCH 24/26] remove unused external entity provider logic and cleanup interfaces Signed-off-by: rafi --- database/repositories/project_repository.go | 10 ------ services/external_entity_provider_service.go | 14 -------- .../external_entity_provider_service_test.go | 35 ------------------- services/project_service.go | 4 +-- shared/common_interfaces.go | 1 - 5 files changed, 1 insertion(+), 63 deletions(-) diff --git a/database/repositories/project_repository.go b/database/repositories/project_repository.go index 277a00c51..b253d20cb 100644 --- a/database/repositories/project_repository.go +++ b/database/repositories/project_repository.go @@ -426,16 +426,6 @@ func (g *projectRepository) GetDirectChildProjects(ctx context.Context, tx *gorm return projects, err } -func (g *projectRepository) EnableCommunityManagedPolicies(ctx context.Context, tx *gorm.DB, projectID uuid.UUID) error { - // community policies can be identified by their "organization_id" being nil - return g.GetDB(ctx, tx).Exec(` - INSERT INTO project_enabled_policies (project_id, policy_id) - SELECT ?, id - FROM policies - WHERE organization_id IS NULL - `, projectID).Error -} - func (g *projectRepository) Create(ctx context.Context, tx *gorm.DB, project *models.Project) error { // set the slug if not set slug, err := g.firstFreeSlug(ctx, tx, project.OrganizationID, project.Slug) diff --git a/services/external_entity_provider_service.go b/services/external_entity_provider_service.go index 986dc98f8..aff440b0a 100644 --- a/services/external_entity_provider_service.go +++ b/services/external_entity_provider_service.go @@ -136,10 +136,6 @@ func (s externalEntityProviderService) RefreshExternalEntityProviderProjects(ctx return nil, err } - if err := s.enableCommunityPoliciesForNewProjects(ctx.Request().Context(), created); err != nil { - return nil, err - } - projectsMap := s.createProjectsMap(ctx.Request().Context(), created, updated) assets, err := s.syncProjectsAndAssets(ctx, domainRBAC, user, projects, roles, append(created, updated...)) @@ -199,16 +195,6 @@ func (s externalEntityProviderService) upsertProjects(ctx context.Context, org m return created, updated, nil } -func (s externalEntityProviderService) enableCommunityPoliciesForNewProjects(ctx context.Context, created []models.Project) error { - for _, project := range created { - if err := s.projectRepository.EnableCommunityManagedPolicies(ctx, nil, project.ID); err != nil { - return fmt.Errorf("could not enable community managed policies for project %s: %w", project.Slug, err) - } - slog.Info("enabled community managed policies for project", "projectSlug", project.Slug, "projectID", project.ID) - } - return nil -} - func (s externalEntityProviderService) createProjectsMap(ctx context.Context, created, updated []models.Project) map[string]struct{} { projectsMap := make(map[string]struct{}, len(created)+len(updated)) for _, project := range append(created, updated...) { diff --git a/services/external_entity_provider_service_test.go b/services/external_entity_provider_service_test.go index 0d21b65d4..e2a48d83b 100644 --- a/services/external_entity_provider_service_test.go +++ b/services/external_entity_provider_service_test.go @@ -191,41 +191,6 @@ func TestUpsertProjects(t *testing.T) { }) } -func TestEnableCommunityPoliciesForNewProjects(t *testing.T) { - t.Run("successful enable", func(t *testing.T) { - projectRepo := mocks.NewProjectRepository(t) - service := createTestServiceWithRepo(t, projectRepo) - - projects := []models.Project{ - {Model: models.Model{ID: uuid.New()}, Slug: "project1"}, - {Model: models.Model{ID: uuid.New()}, Slug: "project2"}, - } - - for _, project := range projects { - projectRepo.On("EnableCommunityManagedPolicies", mock.Anything, mock.Anything, project.ID).Return(nil) - } - - err := service.enableCommunityPoliciesForNewProjects(context.Background(), projects) - - assert.NoError(t, err) - projectRepo.AssertExpectations(t) - }) - - t.Run("repository error", func(t *testing.T) { - projectRepo := mocks.NewProjectRepository(t) - service := createTestServiceWithRepo(t, projectRepo) - - projects := []models.Project{{Model: models.Model{ID: uuid.New()}, Slug: "project1"}} - - projectRepo.On("EnableCommunityManagedPolicies", mock.Anything, mock.Anything, projects[0].ID).Return(errors.New("policy error")) - - err := service.enableCommunityPoliciesForNewProjects(context.Background(), projects) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "could not enable community managed policies for project project1") - }) -} - func TestSyncOrgs(t *testing.T) { t.Run("successful sync with new organizations", func(t *testing.T) { ctx := createTestContext() diff --git a/services/project_service.go b/services/project_service.go index be2575273..4fbfbc4bf 100644 --- a/services/project_service.go +++ b/services/project_service.go @@ -61,9 +61,7 @@ func (s *projectService) CreateProject(ctx shared.Context, project *models.Proje return echo.NewHTTPError(500, "could not create project").WithInternal(err) } } - - // enable the default community policies - return s.projectRepository.EnableCommunityManagedPolicies(ctx.Request().Context(), tx, newProject.ID) + return nil }) if err != nil { slog.Error("could not create project", "err", err, "projectSlug", project.Slug, "projectID", project.ID) diff --git a/shared/common_interfaces.go b/shared/common_interfaces.go index 42234842e..53b16c601 100644 --- a/shared/common_interfaces.go +++ b/shared/common_interfaces.go @@ -98,7 +98,6 @@ type ProjectRepository interface { List(ctx context.Context, tx DB, idSlice []uuid.UUID, parentID *uuid.UUID, organizationID uuid.UUID) ([]models.Project, error) ListPaged(ctx context.Context, tx DB, projectIDs []uuid.UUID, parentID *uuid.UUID, orgID uuid.UUID, pageInfo PageInfo, search string, filter []FilterQuery, sort []SortQuery) (Paged[models.Project], error) Upsert(ctx context.Context, tx DB, projects *[]*models.Project, conflictingColumns []clause.Column, toUpdate []string) error - EnableCommunityManagedPolicies(ctx context.Context, tx DB, projectID uuid.UUID) error UpsertSplit(ctx context.Context, tx DB, externalProviderID string, projects []*models.Project) ([]*models.Project, []*models.Project, error) ListSubProjectsAndAssets(ctx context.Context, tx DB, allowedAssetIDs []string, allowedProjectIDs []uuid.UUID, parentID *uuid.UUID, orgID uuid.UUID, pageInfo PageInfo, search string, filter []FilterQuery, sort []SortQuery) (Paged[dtos.ProjectAssetDTO], error) SearchProjectsWithSubProjectsAndAssetsPaged(ctx context.Context, tx DB, allowedAssetIDs []string, allowedProjectIDs []string, parentID *uuid.UUID, orgID uuid.UUID, pageInfo PageInfo, search string, filter []FilterQuery, sort []SortQuery) (Paged[dtos.ProjectDTO], error) From 947aeb72ee753b044dde78c6f850a02ee8feb7ec Mon Sep 17 00:00:00 2001 From: Konstantin Zhukov Date: Tue, 16 Jun 2026 12:05:44 +0200 Subject: [PATCH 25/26] add frameworkContains filter and sort open compliance risks first --- database/repositories/compliance_risk_repository.go | 11 ++++++++++- shared/context_utils.go | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/database/repositories/compliance_risk_repository.go b/database/repositories/compliance_risk_repository.go index b5c345224..2f5c1de9b 100644 --- a/database/repositories/compliance_risk_repository.go +++ b/database/repositories/compliance_risk_repository.go @@ -52,7 +52,16 @@ func (r *ComplianceRiskRepository) GetAllComplianceRisksForAssetVersionPaged(ctx return shared.Paged[models.ComplianceRisk]{}, err } - err := q.Limit(pageInfo.PageSize).Offset((pageInfo.Page - 1) * pageInfo.PageSize).Find(&risks).Error + // Order open risks first, then newest. The open-first key is selected as an + // aliased column because the query uses SELECT DISTINCT (Postgres requires + // ORDER BY expressions to appear in the select list). Per-column sort is not + // yet supported server-side (the sort param is ignored). + err := q. + Select("compliance_risks.*, CASE WHEN compliance_risks.state = 'open' THEN 0 ELSE 1 END AS state_sort_order"). + Order("state_sort_order ASC"). + Order("compliance_risks.created_at DESC"). + Limit(pageInfo.PageSize).Offset((pageInfo.Page - 1) * pageInfo.PageSize). + Find(&risks).Error if err != nil { return shared.Paged[models.ComplianceRisk]{}, err } diff --git a/shared/context_utils.go b/shared/context_utils.go index c6213a42d..72ce65560 100644 --- a/shared/context_utils.go +++ b/shared/context_utils.go @@ -574,6 +574,11 @@ func (f FilterQuery) SQL() string { return field + " ILIKE ?" case "any": return "? = ANY(string_to_array(" + field + ", ' '))" + case "frameworkContains": + // Matches a JSONB array-of-objects column (e.g. policyFrameworks) where any + // element's "framework" key equals the value. Used by the compliance-risks + // framework filter. + return "EXISTS (SELECT 1 FROM jsonb_array_elements(" + field + ") AS e WHERE e->>'framework' = ?)" default: // default do an equals return field + " = ?" From 1596ad9ac7177eaccfd7f2e0ad106ea8b9cf1d61 Mon Sep 17 00:00:00 2001 From: Konstantin Zhukov Date: Tue, 16 Jun 2026 12:59:43 +0200 Subject: [PATCH 26/26] remove compliance risks sorting --- database/repositories/compliance_risk_repository.go | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/database/repositories/compliance_risk_repository.go b/database/repositories/compliance_risk_repository.go index 2f5c1de9b..b5c345224 100644 --- a/database/repositories/compliance_risk_repository.go +++ b/database/repositories/compliance_risk_repository.go @@ -52,16 +52,7 @@ func (r *ComplianceRiskRepository) GetAllComplianceRisksForAssetVersionPaged(ctx return shared.Paged[models.ComplianceRisk]{}, err } - // Order open risks first, then newest. The open-first key is selected as an - // aliased column because the query uses SELECT DISTINCT (Postgres requires - // ORDER BY expressions to appear in the select list). Per-column sort is not - // yet supported server-side (the sort param is ignored). - err := q. - Select("compliance_risks.*, CASE WHEN compliance_risks.state = 'open' THEN 0 ELSE 1 END AS state_sort_order"). - Order("state_sort_order ASC"). - Order("compliance_risks.created_at DESC"). - Limit(pageInfo.PageSize).Offset((pageInfo.Page - 1) * pageInfo.PageSize). - Find(&risks).Error + err := q.Limit(pageInfo.PageSize).Offset((pageInfo.Page - 1) * pageInfo.PageSize).Find(&risks).Error if err != nil { return shared.Paged[models.ComplianceRisk]{}, err }