diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDao.java index de5d2682a8..896dea031d 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDao.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDao.java @@ -22,6 +22,10 @@ import org.dependencytrack.notification.proto.v1.ComponentVulnAnalysisCompleteSubject; import org.dependencytrack.notification.proto.v1.NewVulnerabilitySubject; import org.dependencytrack.notification.proto.v1.NewVulnerableDependencySubject; +import org.dependencytrack.notification.proto.v1.Policy; +import org.dependencytrack.notification.proto.v1.PolicyCondition; +import org.dependencytrack.notification.proto.v1.PolicyViolation; +import org.dependencytrack.notification.proto.v1.PolicyViolationSubject; import org.dependencytrack.notification.proto.v1.Project; import org.dependencytrack.notification.proto.v1.Vulnerability; import org.dependencytrack.notification.proto.v1.VulnerabilityAnalysisDecisionChangeSubject; @@ -31,6 +35,7 @@ import org.dependencytrack.persistence.jdbi.mapping.NotificationSubjectNewVulnerabilityRowMapper; import org.dependencytrack.persistence.jdbi.mapping.NotificationSubjectProjectAuditChangeRowMapper; import org.dependencytrack.persistence.jdbi.mapping.NotificationVulnerabilityRowMapper; +import org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil; import org.dependencytrack.persistence.jdbi.query.GetProjectAuditChangeNotificationSubjectQuery; import org.jdbi.v3.sqlobject.SqlObject; import org.jdbi.v3.sqlobject.config.RegisterRowMapper; @@ -553,4 +558,99 @@ SELECT ARRAY_AGG(DISTINCT t."NAME") """) List getProjects(@Bind Collection projectUuids); + default List getForNewPolicyViolations(Collection violationIds) { + if (violationIds.isEmpty()) { + return List.of(); + } + + final var componentRowMapper = new NotificationComponentRowMapper(); + final var projectRowMapper = new NotificationProjectRowMapper(); + + return getHandle() + .createQuery(""" + SELECT pv."UUID" AS "violationUuid" + , pv."TYPE" AS "violationType" + , pv."TIMESTAMP" AS "violationTimestamp" + , pc."UUID" AS "conditionUuid" + , pc."SUBJECT" AS "conditionSubject" + , pc."OPERATOR" AS "conditionOperator" + , pc."VALUE" AS "conditionValue" + , po."UUID" AS "policyUuid" + , po."NAME" AS "policyName" + , po."VIOLATIONSTATE" AS "policyViolationState" + , va."SUPPRESSED" AS "analysisSuppressed" + , va."STATE" AS "analysisState" + , c."UUID" AS "componentUuid" + , c."GROUP" AS "componentGroup" + , c."NAME" AS "componentName" + , c."VERSION" AS "componentVersion" + , c."PURL" AS "componentPurl" + , c."MD5" AS "componentMd5" + , c."SHA1" AS "componentSha1" + , c."SHA_256" AS "componentSha256" + , c."SHA_512" AS "componentSha512" + , p."UUID" AS "projectUuid" + , p."NAME" AS "projectName" + , p."VERSION" AS "projectVersion" + , p."DESCRIPTION" AS "projectDescription" + , p."PURL" AS "projectPurl" + , (p."INACTIVE_SINCE" IS NULL) AS "isActive" + , ( + SELECT ARRAY_AGG(DISTINCT t."NAME") + FROM "TAG" AS t + INNER JOIN "PROJECTS_TAGS" AS pt + ON pt."TAG_ID" = t."ID" + WHERE pt."PROJECT_ID" = p."ID" + ) AS "projectTags" + FROM UNNEST(:violationIds) AS req(violation_id) + INNER JOIN "POLICYVIOLATION" AS pv + ON pv."ID" = req.violation_id + INNER JOIN "POLICYCONDITION" AS pc + ON pc."ID" = pv."POLICYCONDITION_ID" + INNER JOIN "POLICY" AS po + ON po."ID" = pc."POLICY_ID" + INNER JOIN "COMPONENT" AS c + ON c."ID" = pv."COMPONENT_ID" + INNER JOIN "PROJECT" AS p + ON p."ID" = pv."PROJECT_ID" + LEFT JOIN "VIOLATIONANALYSIS" AS va + ON va."POLICYVIOLATION_ID" = pv."ID" + WHERE va."SUPPRESSED" IS DISTINCT FROM TRUE + AND va."STATE" IS DISTINCT FROM 'APPROVED' + """) + .bindArray("violationIds", Long.class, violationIds) + .map((rs, ctx) -> { + final Component component = componentRowMapper.map(rs, ctx); + final Project project = projectRowMapper.map(rs, ctx); + + final Policy policy = Policy.newBuilder() + .setUuid(rs.getString("policyUuid")) + .setName(rs.getString("policyName")) + .setViolationState(rs.getString("policyViolationState")) + .build(); + + final PolicyCondition condition = PolicyCondition.newBuilder() + .setUuid(rs.getString("conditionUuid")) + .setSubject(rs.getString("conditionSubject")) + .setOperator(rs.getString("conditionOperator")) + .setValue(rs.getString("conditionValue")) + .setPolicy(policy) + .build(); + + final PolicyViolation violation = PolicyViolation.newBuilder() + .setUuid(rs.getString("violationUuid")) + .setType(rs.getString("violationType")) + .setTimestamp(RowMapperUtil.nullableTimestamp(rs, "violationTimestamp")) + .setCondition(condition) + .build(); + + return PolicyViolationSubject.newBuilder() + .setProject(project) + .setComponent(component) + .setPolicyViolation(violation) + .build(); + }) + .list(); + } + } diff --git a/apiserver/src/main/java/org/dependencytrack/policy/EvalProjectPoliciesActivity.java b/apiserver/src/main/java/org/dependencytrack/policy/EvalProjectPoliciesActivity.java index 5dad063988..e96c9dd891 100644 --- a/apiserver/src/main/java/org/dependencytrack/policy/EvalProjectPoliciesActivity.java +++ b/apiserver/src/main/java/org/dependencytrack/policy/EvalProjectPoliciesActivity.java @@ -25,9 +25,12 @@ import org.dependencytrack.policy.cel.CelPolicyEngine; import org.dependencytrack.proto.internal.workflow.v1.EvalProjectPoliciesArg; import org.jspecify.annotations.Nullable; +import org.slf4j.MDC; import java.util.UUID; +import static org.dependencytrack.common.MdcKeys.MDC_PROJECT_UUID; + /** * @since 5.7.0 */ @@ -46,7 +49,10 @@ public EvalProjectPoliciesActivity(CelPolicyEngine policyEngine) { throw new TerminalApplicationFailureException("No argument provided"); } - policyEngine.evaluateProject(UUID.fromString(argument.getProjectUuid())); + try (var ignored = MDC.putCloseable(MDC_PROJECT_UUID, argument.getProjectUuid())) { + policyEngine.evaluateProject(UUID.fromString(argument.getProjectUuid())); + } + return null; } diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/CelCommonPolicyLibrary.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/CelCommonPolicyLibrary.java index 739ada482c..20fa81539c 100644 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/CelCommonPolicyLibrary.java +++ b/apiserver/src/main/java/org/dependencytrack/policy/cel/CelCommonPolicyLibrary.java @@ -25,7 +25,7 @@ import io.github.nscuro.versatile.VersException; import jakarta.annotation.Nullable; import org.dependencytrack.model.RepositoryType; -import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.policy.cel.persistence.CelPolicyDao; import org.dependencytrack.proto.policy.v1.Component; import org.dependencytrack.proto.policy.v1.License; import org.dependencytrack.proto.policy.v1.Project; @@ -65,6 +65,7 @@ import static org.apache.commons.lang3.StringUtils.substringAfter; import static org.dependencytrack.persistence.jdbi.JdbiAttributes.ATTRIBUTE_QUERY_NAME; import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_COMPONENT; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_PROJECT; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_VERSION_DISTANCE; @@ -248,11 +249,8 @@ private static boolean matchesVersionDistance(Component component, String compar """.formatted(FUNC_COMPARE_VERSION_DISTANCE, component, component.getUuid(), component.getVersion(), component.getLatestVersion()), e); return false; } - final boolean isDirectDependency; - try (final var qm = new QueryManager(); - final var celQm = new CelPolicyQueryManager(qm)) { - isDirectDependency = celQm.isDirectDependency(component); - } + final boolean isDirectDependency = withJdbiHandle(handle -> + new CelPolicyDao(handle).isDirectDependency(component)); return isDirectDependency && org.dependencytrack.model.VersionDistance.evaluate(value, comparatorComputed, versionDistance); } @@ -686,7 +684,7 @@ private static boolean isExclusiveDependencyOf(final Component leafComponent, fi // If the component is a direct dependency of the project, // it can no longer be a dependency exclusively introduced // through another component. - if (isDirectDependency(jdbiHandle, leafComponent)) { + if (new CelPolicyDao(jdbiHandle).isDirectDependency(leafComponent)) { return false; } @@ -1087,25 +1085,4 @@ private static boolean containsExactly(final List lhs, final List rhs) return Objects.equals(lhs, rhs); } - private static boolean isDirectDependency(final Handle jdbiHandle, final Component component) { - final Query query = jdbiHandle.createQuery(""" - SELECT - 1 - FROM - "COMPONENT" AS "C" - INNER JOIN - "PROJECT" AS "P" ON "P"."ID" = "C"."PROJECT_ID" - WHERE - "C"."UUID" = :leafComponentUuid - AND "P"."DIRECT_DEPENDENCIES" @> JSONB_BUILD_ARRAY(JSONB_BUILD_OBJECT('uuid', :leafComponentUuid)) - """); - - return query - .define(ATTRIBUTE_QUERY_NAME, "%s#isDirectDependency".formatted(CelCommonPolicyLibrary.class.getSimpleName())) - .bind("leafComponentUuid", UUID.fromString(component.getUuid())) - .mapTo(Boolean.class) - .findOne() - .orElse(false); - } - } diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/CelPolicyEngine.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/CelPolicyEngine.java index 69e5b9c701..3f5a236d91 100644 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/CelPolicyEngine.java +++ b/apiserver/src/main/java/org/dependencytrack/policy/cel/CelPolicyEngine.java @@ -18,29 +18,19 @@ */ package org.dependencytrack.policy.cel; -import alpine.common.logging.Logger; -import com.fasterxml.jackson.core.JacksonException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; import com.google.api.expr.v1alpha1.Type; import com.google.protobuf.Timestamp; import com.google.protobuf.util.Timestamps; import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; import org.apache.commons.collections4.multimap.HashSetValuedHashMap; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.dependencytrack.common.Mappers; import org.dependencytrack.model.Policy; import org.dependencytrack.model.PolicyCondition; import org.dependencytrack.model.PolicyCondition.Subject; import org.dependencytrack.model.PolicyViolation; -import org.dependencytrack.model.Project; -import org.dependencytrack.model.Severity; -import org.dependencytrack.model.VulnerabilityAlias; -import org.dependencytrack.persistence.CollectionIntegerConverter; -import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.notification.JdbiNotificationEmitter; +import org.dependencytrack.persistence.jdbi.NotificationSubjectDao; +import org.dependencytrack.persistence.jdbi.ProjectDao; import org.dependencytrack.policy.cel.CelPolicyScriptHost.CacheMode; import org.dependencytrack.policy.cel.compat.CelPolicyScriptSourceBuilder; import org.dependencytrack.policy.cel.compat.ComponentAgeCelPolicyScriptSourceBuilder; @@ -57,19 +47,17 @@ import org.dependencytrack.policy.cel.compat.VersionCelPolicyScriptSourceBuilder; import org.dependencytrack.policy.cel.compat.VersionDistanceCelScriptBuilder; import org.dependencytrack.policy.cel.compat.VulnerabilityIdCelPolicyScriptSourceBuilder; -import org.dependencytrack.policy.cel.mapping.ComponentProjection; -import org.dependencytrack.policy.cel.mapping.LicenseProjection; -import org.dependencytrack.policy.cel.mapping.VulnerabilityProjection; import org.dependencytrack.policy.cel.persistence.CelPolicyDao; +import org.dependencytrack.policy.cel.persistence.CelPolicyDao.ComponentWithLicenseId; +import org.dependencytrack.proto.policy.v1.Component; +import org.dependencytrack.proto.policy.v1.License; +import org.dependencytrack.proto.policy.v1.Project; import org.dependencytrack.proto.policy.v1.Vulnerability; -import org.dependencytrack.util.NotificationUtil; -import org.dependencytrack.util.VulnerabilityUtil; import org.projectnessie.cel.tools.ScriptCreateException; import org.projectnessie.cel.tools.ScriptException; -import org.slf4j.MDC; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.math.BigDecimal; -import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -78,15 +66,13 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; +import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import static java.util.Collections.emptyList; import static org.apache.commons.collections4.MultiMapUtils.emptyMultiValuedMap; -import static org.apache.commons.lang3.StringUtils.trimToEmpty; -import static org.dependencytrack.common.MdcKeys.MDC_PROJECT_UUID; +import static org.dependencytrack.notification.api.NotificationFactory.createPolicyViolationNotification; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.inJdbiTransaction; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction; import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_COMPONENT; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_LICENSE; @@ -94,14 +80,9 @@ import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_PROJECT; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_VULNERABILITY; -/** - * A policy engine powered by the Common Expression Language (CEL). - * - * @since 5.1.0 - */ public class CelPolicyEngine { - private static final Logger LOGGER = Logger.getLogger(CelPolicyEngine.class); + private static final Logger LOGGER = LoggerFactory.getLogger(CelPolicyEngine.class); private static final Map SCRIPT_BUILDERS; static { @@ -133,405 +114,258 @@ public CelPolicyEngine() { this.scriptHost = scriptHost; } - /** - * Evaluate {@link Policy}s for a {@link Project}. - * - * @param uuid The {@link UUID} of the {@link Project} - */ - public void evaluateProject(final UUID uuid) { - final long startTimeNs = System.nanoTime(); - - try (final var qm = new QueryManager(); - final var celQm = new CelPolicyQueryManager(qm); - var ignoredMdcProjectUuid = MDC.putCloseable(MDC_PROJECT_UUID, uuid.toString())) { - // TODO: Should this entire procedure run in a single DB transaction? - // Would be better for atomicity, but could block DB connections for prolonged - // period of time for larger projects with many violations. - - final Project project = qm.getObjectByUuid(Project.class, uuid, List.of(Project.FetchGroup.IDENTIFIERS.name())); - if (project == null) { - LOGGER.warn("Project does not exist; Skipping"); - return; - } + public void evaluateProject(UUID uuid) { + // TODO: Should this entire procedure run in a single DB transaction? + // Would be better for atomicity, but could block DB connections for prolonged + // period of time for larger projects with many violations. - LOGGER.debug("Compiling policy scripts"); - final List> conditionScriptPairs = getApplicableConditionScriptPairs(celQm, project); - if (conditionScriptPairs.isEmpty()) { - LOGGER.info("No applicable policies found"); - celQm.reconcileViolations(project.getId(), emptyMultiValuedMap()); - return; - } + final Long projectId = withJdbiHandle( + handle -> handle.attach(ProjectDao.class).getProjectId(uuid)); + if (projectId == null) { + LOGGER.warn("Project does not exist; Skipping"); + return; + } - final MultiValuedMap requirements = determineScriptRequirements(conditionScriptPairs); - LOGGER.debug("Requirements for %d policy conditions: %s".formatted(conditionScriptPairs.size(), requirements)); + LOGGER.debug("Fetching applicable policies"); + final List applicablePolicies = withJdbiHandle( + handle -> new CelPolicyDao(handle).getApplicablePolicies(projectId)); + if (applicablePolicies.isEmpty()) { + LOGGER.info("No applicable policies found"); + inJdbiTransaction(handle -> + new CelPolicyDao(handle).reconcileViolations( + projectId, emptyMultiValuedMap())); + return; + } - final org.dependencytrack.proto.policy.v1.Project protoProject; - if (requirements.containsKey(TYPE_PROJECT)) { - protoProject = withJdbiHandle(handle -> handle.attach(CelPolicyDao.class).loadRequiredFields(project.getId(), requirements)); - } else { - protoProject = org.dependencytrack.proto.policy.v1.Project.getDefaultInstance(); - } - // Preload components for the entire project, to avoid excessive queries. - final List components = celQm.fetchAllComponents(project.getId(), requirements.get(TYPE_COMPONENT)); - - // Preload licenses for the entire project, as chances are high that they will be used by multiple components. - final Map licenseById; - if (requirements.containsKey(TYPE_LICENSE) || (requirements.containsKey(TYPE_COMPONENT) && requirements.get(TYPE_COMPONENT).contains("resolved_license"))) { - licenseById = celQm.fetchAllLicenses(project.getId(), requirements.get(TYPE_LICENSE), requirements.get(TYPE_LICENSE_GROUP)).stream() - .collect(Collectors.toMap( - projection -> projection.id, - CelPolicyEngine::mapToProto - )); + LOGGER.debug("Compiling policy scripts"); + final List policiesWithScripts = + compilePoliciesScripts(applicablePolicies); + if (policiesWithScripts.isEmpty()) { + LOGGER.info("No compilable policy conditions found"); + inJdbiTransaction(handle -> + new CelPolicyDao(handle).reconcileViolations( + projectId, emptyMultiValuedMap())); + return; + } + + final MultiValuedMap requirements = determineScriptRequirements(policiesWithScripts); + final long conditionCount = policiesWithScripts.stream().mapToLong(pws -> pws.conditionScripts().size()).sum(); + LOGGER.debug("Requirements for {} policy conditions: {}", conditionCount, requirements); + + final Project protoProject; + if (requirements.containsKey(TYPE_PROJECT)) { + protoProject = withJdbiHandle(handle -> + new CelPolicyDao(handle) + .loadRequiredFields(projectId, requirements)); + } else { + protoProject = Project.getDefaultInstance(); + } + + // Preload components for the entire project, to avoid excessive queries. + final Map componentsWithLicense = withJdbiHandle( + handle -> new CelPolicyDao(handle) + .fetchAllComponents(projectId, requirements.get(TYPE_COMPONENT))); + + // Preload licenses for the entire project, as chances are high that + // they will be used by multiple components. + final Map licenseById; + if (requirements.containsKey(TYPE_LICENSE) + || (requirements.containsKey(TYPE_COMPONENT) && requirements.get(TYPE_COMPONENT).contains("resolved_license"))) { + licenseById = withJdbiHandle( + handle -> new CelPolicyDao(handle) + .fetchAllLicenses( + projectId, + requirements.get(TYPE_LICENSE), + requirements.get(TYPE_LICENSE_GROUP))); + } else { + licenseById = Collections.emptyMap(); + } + + // Build final component protos, enriching with resolved licenses where applicable. + final var componentsById = new HashMap(); + for (final var entry : componentsWithLicense.entrySet()) { + final long componentId = entry.getKey(); + final ComponentWithLicenseId cwl = entry.getValue(); + if (cwl.resolvedLicenseId() != null && cwl.resolvedLicenseId() > 0) { + final License license = licenseById.get(cwl.resolvedLicenseId()); + if (license != null) { + componentsById.put(componentId, cwl.component().toBuilder() + .setResolvedLicense(license) + .build()); + } else { + LOGGER.warn(""" + Component with DB ID {} refers to license with ID {}, \ + but no license with that ID was found""", componentId, cwl.resolvedLicenseId()); + componentsById.put(componentId, cwl.component()); + } } else { - licenseById = Collections.emptyMap(); + componentsById.put(componentId, cwl.component()); } + } + + // Preload vulnerabilities for the entire project, + // as chances are high that they will be used by multiple components. + final Map protoVulnById; + final Map> vulnIdsByComponentId; + if (requirements.containsKey(TYPE_VULNERABILITY)) { + protoVulnById = withJdbiHandle(handle -> + new CelPolicyDao(handle) + .fetchAllVulnerabilities( + projectId, + requirements.get(TYPE_VULNERABILITY))); + + vulnIdsByComponentId = withJdbiHandle(handle -> + new CelPolicyDao(handle) + .fetchAllComponentsVulnerabilities(projectId)); + } else { + protoVulnById = Collections.emptyMap(); + vulnIdsByComponentId = Collections.emptyMap(); + } - // Preload vulnerabilities for the entire project, as chances are high that they will be used by multiple components. - final Map protoVulnById; - final Map> vulnIdsByComponentId; + final var violationsByComponentId = new ArrayListValuedHashMap(); + final Timestamp protoNow = Timestamps.now(); + + for (final Map.Entry entry : componentsById.entrySet()) { + final long componentId = entry.getKey(); + final Component protoComponent = entry.getValue(); + + final List protoVulns; if (requirements.containsKey(TYPE_VULNERABILITY)) { - protoVulnById = celQm.fetchAllVulnerabilities(project.getId(), requirements.get(TYPE_VULNERABILITY)).stream() - .collect(Collectors.toMap( - projection -> projection.id, - CelPolicyEngine::mapToProto - )); - - vulnIdsByComponentId = celQm.fetchAllComponentsVulnerabilities(project.getId()).stream() - .collect(Collectors.groupingBy( - projection -> projection.componentId, - Collectors.mapping(projection -> projection.vulnerabilityId, Collectors.toList()) - )); + protoVulns = vulnIdsByComponentId.getOrDefault(componentId, Set.of()).stream() + .map(protoVulnById::get) + .filter(Objects::nonNull) + .toList(); } else { - protoVulnById = Collections.emptyMap(); - vulnIdsByComponentId = Collections.emptyMap(); + protoVulns = List.of(); } - // Evaluate all policy conditions against all components. - final var conditionsViolated = new HashSetValuedHashMap(); - final Timestamp protoNow = Timestamps.now(); // Use consistent now timestamp for all evaluations. - for (final ComponentProjection component : components) { - final org.dependencytrack.proto.policy.v1.Component protoComponent = mapToProto(component, licenseById); - final List protoVulns = - vulnIdsByComponentId.getOrDefault(component.id, emptyList()).stream() - .map(protoVulnById::get) - .toList(); - - conditionsViolated.putAll(component.id, evaluateConditions(conditionScriptPairs, Map.of( - CelPolicyVariable.COMPONENT.variableName(), protoComponent, - CelPolicyVariable.PROJECT.variableName(), protoProject, - CelPolicyVariable.VULNS.variableName(), protoVulns, - CelPolicyVariable.NOW.variableName(), protoNow - ))); - } + evaluateComponentAgainstPolicies( + policiesWithScripts, + componentId, + Map.ofEntries( + Map.entry(CelPolicyVariable.COMPONENT.variableName(), protoComponent), + Map.entry(CelPolicyVariable.PROJECT.variableName(), protoProject), + Map.entry(CelPolicyVariable.VULNS.variableName(), protoVulns), + Map.entry(CelPolicyVariable.NOW.variableName(), protoNow)), + violationsByComponentId); + } - final var violationsByComponentId = new ArrayListValuedHashMap(); - for (final long componentId : conditionsViolated.keySet()) { - violationsByComponentId.putAll(componentId, evaluatePolicyOperators(conditionsViolated.get(componentId))); - } + final Set newViolationIds = inJdbiTransaction(handle -> + new CelPolicyDao(handle).reconcileViolations( + projectId, violationsByComponentId)); + LOGGER.info("Identified {} new violations", newViolationIds.size()); + + if (!newViolationIds.isEmpty()) { + useJdbiTransaction(handle -> new JdbiNotificationEmitter(handle).emitAll( + handle.attach(NotificationSubjectDao.class) + .getForNewPolicyViolations(newViolationIds) + .stream() + .map(subject -> createPolicyViolationNotification( + subject.getProject(), subject.getComponent(), subject.getPolicyViolation())) + .toList())); + } + } - final List newViolationIds = celQm.reconcileViolations(project.getId(), violationsByComponentId); - LOGGER.info("Identified %d new violations".formatted(newViolationIds.size())); + record ConditionScript(PolicyCondition condition, CelPolicyScript script) { + } - for (final Long newViolationId : newViolationIds) { - NotificationUtil.analyzeNotificationCriteria(qm, newViolationId); - } - } finally { - LOGGER.info("Evaluation completed in %s" - .formatted(Duration.ofNanos(System.nanoTime() - startTimeNs))); - } + record PolicyWithScripts(Policy policy, List conditionScripts) { } - public void evaluateComponent(final UUID uuid) { - // Evaluation of individual components is only triggered when they are added or modified - // manually. As this happens very rarely, in low frequencies (due to being manual actions), - // and because CEL policy evaluation is so efficient, it's not worth it to maintain extra - // logic to handle component evaluation. Instead, re-purpose to project evaluation. + private List compilePoliciesScripts(List policies) { + final var result = new ArrayList(); + for (final Policy policy : policies) { + final var conditionScripts = new ArrayList(); - final UUID projectUuid; - try (final var qm = new QueryManager(); - final var celQm = new CelPolicyQueryManager(qm)) { - projectUuid = celQm.getProjectUuidForComponentUuid(uuid); - } + for (final PolicyCondition condition : policy.getPolicyConditions()) { + final CelPolicyScript script = compileCondition(condition); + if (script != null) { + conditionScripts.add(new ConditionScript(condition, script)); + } + } - if (projectUuid == null) { - LOGGER.warn("Component with UUID %s does not exist; Skipping".formatted(uuid)); - return; + if (!conditionScripts.isEmpty()) { + result.add(new PolicyWithScripts(policy, conditionScripts)); + } } - evaluateProject(projectUuid); + return result; } + private MultiValuedMap determineScriptRequirements( + Collection policiesWithScripts) { + final var requirements = new HashSetValuedHashMap(); - /** - * Pre-compile the CEL scripts for all conditions of all applicable policies. - * Compiled scripts are cached in-memory by CelPolicyScriptHost, so if the same script - * is encountered for multiple components (possibly concurrently), the compilation is - * a one-time effort. - * - * @param celQm The {@link CelPolicyQueryManager} instance to use - * @param project The {@link Project} to get applicable conditions for - * @return {@link Pair}s of {@link PolicyCondition}s and {@link CelPolicyScript}s - */ - private List> getApplicableConditionScriptPairs(final CelPolicyQueryManager celQm, final Project project) { - final List policies = celQm.getApplicablePolicies(project); - if (policies.isEmpty()) { - return emptyList(); + for (final PolicyWithScripts policyWithScripts : policiesWithScripts) { + for (final ConditionScript conditionScript : policyWithScripts.conditionScripts()) { + requirements.putAll(conditionScript.script().getRequirements()); + } } - return policies.stream() - .map(Policy::getPolicyConditions) - .flatMap(Collection::stream) - .map(this::buildConditionScriptSrc) - .filter(Objects::nonNull) - .map(this::compileConditionScript) - .filter(Objects::nonNull) - .toList(); + return requirements; } - /** - * Check what kind of data we need to evaluate all policy conditions. - *

- * Some conditions will be very simple and won't require us to load additional data (e.g. "component PURL matches 'XYZ'"), - * whereas other conditions can span across multiple models, forcing us to load more data - * (e.g. "project has tag 'public-facing' and component has a vulnerability with severity 'critical'"). - *

- * What we want to avoid is loading data we don't need, and loading it multiple times. - * Instead, only load what's really needed, and only do so once. - * - * @param conditionScriptPairs {@link Pair}s of {@link PolicyCondition}s and corresponding {@link CelPolicyScript}s - * @return A {@link MultiValuedMap} containing all fields accessed on any {@link Type}, across all {@link CelPolicyScript}s - */ - private static MultiValuedMap determineScriptRequirements(final Collection> conditionScriptPairs) { - return conditionScriptPairs.stream() - .map(Pair::getRight) - .map(CelPolicyScript::getRequirements) - .reduce(new HashSetValuedHashMap<>(), (lhs, rhs) -> { - lhs.putAll(rhs); - return lhs; - }); - } - - private Pair buildConditionScriptSrc(final PolicyCondition policyCondition) { + private CelPolicyScript compileCondition(PolicyCondition policyCondition) { final CelPolicyScriptSourceBuilder scriptBuilder = SCRIPT_BUILDERS.get(policyCondition.getSubject()); if (scriptBuilder == null) { LOGGER.warn(""" - No script builder found that is capable of handling subjects of type %s;\ - Condition will be skipped""".formatted(policyCondition.getSubject())); + No script builder found that is capable of handling subjects of type {};\ + Condition will be skipped""", policyCondition.getSubject()); return null; } final String scriptSrc = scriptBuilder.apply(policyCondition); if (scriptSrc == null) { - LOGGER.warn("Unable to create CEL script for condition %s; Condition will be skipped".formatted(policyCondition.getUuid())); + LOGGER.warn( + "Unable to create CEL script for condition {}; Condition will be skipped", + policyCondition.getUuid()); return null; } - return Pair.of(policyCondition, scriptSrc); - } - - private Pair compileConditionScript(final Pair conditionScriptSrcPair) { - final CelPolicyScript script; try { - script = scriptHost.compile(conditionScriptSrcPair.getRight(), CacheMode.CACHE); + return scriptHost.compile(scriptSrc, CacheMode.CACHE); } catch (ScriptCreateException e) { - LOGGER.warn("Failed to compile script for condition %s; Condition will be skipped" - .formatted(conditionScriptSrcPair.getLeft().getUuid()), e); + LOGGER.warn( + "Failed to compile script for condition {}; Condition will be skipped", + policyCondition.getUuid(), e); return null; } - - return Pair.of(conditionScriptSrcPair.getLeft(), script); - } - - private static List evaluateConditions(final Collection> conditionScriptPairs, - final Map scriptArguments) { - final var conditionsViolated = new ArrayList(); - - for (final Pair conditionScriptPair : conditionScriptPairs) { - final PolicyCondition condition = conditionScriptPair.getLeft(); - final CelPolicyScript script = conditionScriptPair.getRight(); - - try { - if (script.execute(scriptArguments)) { - conditionsViolated.add(condition); - } - } catch (ScriptException e) { - LOGGER.warn("Failed to execute script for condition %s with arguments %s" - .formatted(condition.getUuid(), scriptArguments), e); - } - } - - return conditionsViolated; } - private static List evaluatePolicyOperators(final Collection conditionsViolated) { - final Map> violatedConditionsByPolicy = conditionsViolated.stream() - .collect(Collectors.groupingBy(PolicyCondition::getPolicy)); - - return violatedConditionsByPolicy.entrySet().stream() - .flatMap(policyAndViolatedConditions -> { - final Policy policy = policyAndViolatedConditions.getKey(); - final List violatedConditions = policyAndViolatedConditions.getValue(); - - if ((policy.getOperator() == Policy.Operator.ANY && !violatedConditions.isEmpty()) - || (policy.getOperator() == Policy.Operator.ALL && violatedConditions.size() == policy.getPolicyConditions().size())) { - // TODO: Only a single violation should be raised, and instead multiple matched conditions - // should be associated with it. Keeping the existing behavior in order to avoid having to - // touch too much persistence and REST API code. - return violatedConditions.stream() - .map(condition -> { - final var violation = new PolicyViolation(); - violation.setType(condition.getViolationType()); - // Note: violation.setComponent is intentionally omitted here, - // because the component must be an object attached to the persistence - // context. We don't have that at this point, we'll add it later. - violation.setPolicyCondition(condition); - violation.setTimestamp(new Date()); - return violation; - }); + private void evaluateComponentAgainstPolicies( + List policiesWithScripts, + long componentId, + Map scriptArgs, + MultiValuedMap violationsByComponentId) { + for (final PolicyWithScripts pws : policiesWithScripts) { + final Policy policy = pws.policy(); + final var violatedConditions = new ArrayList(); + + for (final ConditionScript cs : pws.conditionScripts()) { + try { + if (cs.script().execute(scriptArgs)) { + violatedConditions.add(cs.condition()); } - - return Stream.empty(); - }) - .filter(Objects::nonNull) - .toList(); - } - - private static org.dependencytrack.proto.policy.v1.Component mapToProto(final ComponentProjection projection, - final Map protoLicenseById) { - final org.dependencytrack.proto.policy.v1.Component.Builder componentBuilder = - org.dependencytrack.proto.policy.v1.Component.newBuilder() - .setUuid(trimToEmpty(projection.uuid)) - .setGroup(trimToEmpty(projection.group)) - .setName(trimToEmpty(projection.name)) - .setVersion(trimToEmpty(projection.version)) - .setClassifier(trimToEmpty(projection.classifier)) - .setCpe(trimToEmpty(projection.cpe)) - .setPurl(trimToEmpty(projection.purl)) - .setSwidTagId(trimToEmpty(projection.swidTagId)) - .setIsInternal(Optional.ofNullable(projection.internal).orElse(false)) - .setLicenseName(trimToEmpty(projection.licenseName)) - .setLicenseExpression(trimToEmpty(projection.licenseExpression)) - .setMd5(trimToEmpty(projection.md5)) - .setSha1(trimToEmpty(projection.sha1)) - .setSha256(trimToEmpty(projection.sha256)) - .setSha384(trimToEmpty(projection.sha384)) - .setSha512(trimToEmpty(projection.sha512)) - .setSha3256(trimToEmpty(projection.sha3_256)) - .setSha3384(trimToEmpty(projection.sha3_384)) - .setSha3512(trimToEmpty(projection.sha3_512)) - .setBlake2B256(trimToEmpty(projection.blake2b_256)) - .setBlake2B384(trimToEmpty(projection.blake2b_384)) - .setBlake2B512(trimToEmpty(projection.blake2b_512)) - .setBlake3(trimToEmpty(projection.blake3)); - Optional.ofNullable(projection.latestVersion).ifPresent(componentBuilder::setLatestVersion); - Optional.ofNullable(projection.publishedAt).map(Timestamps::fromDate).ifPresent(componentBuilder::setPublishedAt); - if (projection.resolvedLicenseId != null && projection.resolvedLicenseId > 0) { - final org.dependencytrack.proto.policy.v1.License protoLicense = protoLicenseById.get(projection.resolvedLicenseId); - if (protoLicense != null) { - componentBuilder.setResolvedLicense(protoLicenseById.get(projection.resolvedLicenseId)); - } else { - LOGGER.warn("Component with ID %d refers to license with ID %d, but no license with that ID was found" - .formatted(projection.id, projection.resolvedLicenseId)); - } - } - - return componentBuilder.build(); - } - - private static org.dependencytrack.proto.policy.v1.License mapToProto(final LicenseProjection projection) { - final org.dependencytrack.proto.policy.v1.License.Builder licenseBuilder = - org.dependencytrack.proto.policy.v1.License.newBuilder() - .setUuid(trimToEmpty(projection.uuid)) - .setId(trimToEmpty(projection.licenseId)) - .setName(trimToEmpty(projection.name)); - Optional.ofNullable(projection.isOsiApproved).ifPresent(licenseBuilder::setIsOsiApproved); - Optional.ofNullable(projection.isFsfLibre).ifPresent(licenseBuilder::setIsFsfLibre); - Optional.ofNullable(projection.isDeprecatedId).ifPresent(licenseBuilder::setIsDeprecatedId); - Optional.ofNullable(projection.isCustomLicense).ifPresent(licenseBuilder::setIsCustom); - - if (projection.licenseGroupsJson != null) { - try { - final ArrayNode groupsArray = Mappers.jsonMapper().readValue(projection.licenseGroupsJson, ArrayNode.class); - for (final JsonNode groupNode : groupsArray) { - licenseBuilder.addGroups(org.dependencytrack.proto.policy.v1.License.Group.newBuilder() - .setUuid(Optional.ofNullable(groupNode.get("uuid")).map(JsonNode::asText).orElse("")) - .setName(Optional.ofNullable(groupNode.get("name")).map(JsonNode::asText).orElse("")) - .build()); + } catch (ScriptException e) { + LOGGER.warn("Failed to execute script for condition {}", cs.condition().getUuid(), e); } - } catch (JacksonException e) { - LOGGER.warn("Failed to parse license groups from %s for license %s" - .formatted(projection.licenseGroupsJson, projection.id), e); } - } - - return licenseBuilder.build(); - } - private static final TypeReference> VULNERABILITY_ALIASES_TYPE_REF = new TypeReference<>() { - }; - - private static org.dependencytrack.proto.policy.v1.Vulnerability mapToProto(final VulnerabilityProjection projection) { - final org.dependencytrack.proto.policy.v1.Vulnerability.Builder builder = - org.dependencytrack.proto.policy.v1.Vulnerability.newBuilder() - .setUuid(trimToEmpty(projection.uuid)) - .setId(trimToEmpty(projection.vulnId)) - .setSource(trimToEmpty(projection.source)) - .setCvssv2Vector(trimToEmpty(projection.cvssV2Vector)) - .setCvssv3Vector(trimToEmpty(projection.cvssV3Vector)) - .setCvssv4Vector(trimToEmpty(projection.cvssV4Vector)) - .setOwaspRrVector(trimToEmpty(projection.owaspRrVector)); - Optional.ofNullable(projection.cvssV2BaseScore).map(BigDecimal::doubleValue).ifPresent(builder::setCvssv2BaseScore); - Optional.ofNullable(projection.cvssV2ImpactSubScore).map(BigDecimal::doubleValue).ifPresent(builder::setCvssv2ImpactSubscore); - Optional.ofNullable(projection.cvssV2ExploitabilitySubScore).map(BigDecimal::doubleValue).ifPresent(builder::setCvssv2ExploitabilitySubscore); - Optional.ofNullable(projection.cvssV3BaseScore).map(BigDecimal::doubleValue).ifPresent(builder::setCvssv3BaseScore); - Optional.ofNullable(projection.cvssV3ImpactSubScore).map(BigDecimal::doubleValue).ifPresent(builder::setCvssv3ImpactSubscore); - Optional.ofNullable(projection.cvssV3ExploitabilitySubScore).map(BigDecimal::doubleValue).ifPresent(builder::setCvssv3ExploitabilitySubscore); - Optional.ofNullable(projection.cvssV4Score).map(BigDecimal::doubleValue).ifPresent(builder::setCvssv4Score); - Optional.ofNullable(projection.owaspRrLikelihoodScore).map(BigDecimal::doubleValue).ifPresent(builder::setOwaspRrLikelihoodScore); - Optional.ofNullable(projection.owaspRrTechnicalImpactScore).map(BigDecimal::doubleValue).ifPresent(builder::setOwaspRrTechnicalImpactScore); - Optional.ofNullable(projection.owaspRrBusinessImpactScore).map(BigDecimal::doubleValue).ifPresent(builder::setOwaspRrBusinessImpactScore); - Optional.ofNullable(projection.epssScore).map(BigDecimal::doubleValue).ifPresent(builder::setEpssScore); - Optional.ofNullable(projection.epssPercentile).map(BigDecimal::doubleValue).ifPresent(builder::setEpssPercentile); - Optional.ofNullable(projection.created).map(Timestamps::fromDate).ifPresent(builder::setCreated); - Optional.ofNullable(projection.published).map(Timestamps::fromDate).ifPresent(builder::setPublished); - Optional.ofNullable(projection.updated).map(Timestamps::fromDate).ifPresent(builder::setUpdated); - Optional.ofNullable(projection.cwes) - .map(StringUtils::trimToNull) - .filter(Objects::nonNull) - .map(new CollectionIntegerConverter()::convertToAttribute) - .ifPresent(builder::addAllCwes); - - // Workaround for https://github.com/DependencyTrack/dependency-track/issues/2474. - final Severity severity = VulnerabilityUtil.getSeverity(projection.severity, - projection.cvssV2BaseScore, - projection.cvssV3BaseScore, - projection.cvssV4Score, - projection.owaspRrLikelihoodScore, - projection.owaspRrTechnicalImpactScore, - projection.owaspRrBusinessImpactScore); - builder.setSeverity(severity.name()); - - if (projection.aliasesJson != null) { - try { - Mappers.jsonMapper().readValue(projection.aliasesJson, VULNERABILITY_ALIASES_TYPE_REF).stream() - .flatMap(CelPolicyEngine::mapToProto) - .distinct() - .forEach(builder::addAliases); - } catch (JacksonException e) { - LOGGER.warn("Failed to parse aliases from %s for vulnerability %d" - .formatted(projection.aliasesJson, projection.id), e); + final boolean policyViolated = switch (policy.getOperator()) { + case ANY -> !violatedConditions.isEmpty(); + case ALL -> violatedConditions.size() == policy.getPolicyConditions().size(); + }; + + if (policyViolated) { + for (final PolicyCondition condition : violatedConditions) { + final var violation = new PolicyViolation(); + violation.setType(condition.getViolationType()); + violation.setPolicyCondition(condition); + violation.setTimestamp(new Date()); + violationsByComponentId.put(componentId, violation); + } } } - - return builder.build(); - } - - private static Stream mapToProto(final VulnerabilityAlias alias) { - return alias.getAllBySource().entrySet().stream() - .map(aliasEntry -> Vulnerability.Alias.newBuilder() - .setSource(aliasEntry.getKey().name()) - .setId(aliasEntry.getValue()) - .build()); } } diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/CelPolicyQueryManager.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/CelPolicyQueryManager.java deleted file mode 100644 index 094c7eb93c..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/CelPolicyQueryManager.java +++ /dev/null @@ -1,597 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.policy.cel; - -import alpine.common.logging.Logger; -import org.apache.commons.collections4.MultiValuedMap; -import org.apache.commons.collections4.multimap.HashSetValuedHashMap; -import org.dependencytrack.model.Component; -import org.dependencytrack.model.Policy; -import org.dependencytrack.model.PolicyViolation; -import org.dependencytrack.model.Project; -import org.dependencytrack.model.Tag; -import org.dependencytrack.persistence.QueryManager; -import org.dependencytrack.policy.cel.mapping.ComponentProjection; -import org.dependencytrack.policy.cel.mapping.ComponentsVulnerabilitiesProjection; -import org.dependencytrack.policy.cel.mapping.LicenseGroupProjection; -import org.dependencytrack.policy.cel.mapping.LicenseProjection; -import org.dependencytrack.policy.cel.mapping.PolicyViolationProjection; -import org.dependencytrack.policy.cel.mapping.VulnerabilityProjection; - -import javax.jdo.PersistenceManager; -import javax.jdo.Query; -import javax.jdo.datastore.JDOConnection; -import java.sql.Array; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.sql.Timestamp; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static java.sql.Connection.TRANSACTION_READ_COMMITTED; -import static org.dependencytrack.policy.cel.mapping.FieldMappingUtil.getFieldMappings; - -class CelPolicyQueryManager implements AutoCloseable { - - private static final Logger LOGGER = Logger.getLogger(CelPolicyQueryManager.class); - - private final PersistenceManager pm; - - CelPolicyQueryManager(final QueryManager qm) { - this.pm = qm.getPersistenceManager(); - } - - UUID getProjectUuidForComponentUuid(final UUID componentUuid) { - try (final var qm = new QueryManager()) { - final Query query = qm.getPersistenceManager().newQuery(Component.class); - query.setFilter("uuid == :uuid"); - query.setParameters(componentUuid); - query.setResult("project.uuid"); - try { - return query.executeResultUnique(UUID.class); - } finally { - query.closeAll(); - } - } - } - - List fetchAllComponents(final long projectId, final Collection protoFieldNames) { - String sqlSelectColumns = Stream.concat( - Stream.of(ComponentProjection.ID_FIELD_MAPPING), - getFieldMappings(ComponentProjection.class).stream() - .filter(mapping -> protoFieldNames.contains(mapping.protoFieldName())) - ) - .map(mapping -> "\"C\".\"%s\" AS \"%s\"".formatted(mapping.sqlColumnName(), mapping.javaFieldName())) - .collect(Collectors.joining(", ")); - if (protoFieldNames.contains("published_at")) { - sqlSelectColumns += ", \"publishedAt\""; - } - if (protoFieldNames.contains("latest_version")) { - sqlSelectColumns += ", \"latestVersion\""; - } - final Query query = pm.newQuery(Query.SQL, /* language=SQL */ """ - SELECT %s - FROM "COMPONENT" AS "C" - LEFT JOIN LATERAL ( - SELECT "PAM"."PUBLISHED_AT" AS "publishedAt" - FROM "PACKAGE_ARTIFACT_METADATA" "PAM" - WHERE "C"."PURL" = "PAM"."PURL" - ) AS "publishedAt" ON :shouldJoinPackageArtifactMetadata - LEFT JOIN LATERAL ( - SELECT "PM"."LATEST_VERSION" AS "latestVersion" - FROM "PACKAGE_ARTIFACT_METADATA" "PAM" - INNER JOIN "PACKAGE_METADATA" "PM" - ON "PM"."PURL" = "PAM"."PACKAGE_PURL" - WHERE "PAM"."PURL" = "C"."PURL" - ) AS "latestVersion" ON :shouldJoinPackageMetadata - WHERE "PROJECT_ID" = :projectId - """.formatted(sqlSelectColumns)); - query.setNamedParameters(Map.of( - "shouldJoinPackageArtifactMetadata", protoFieldNames.contains("published_at"), - "shouldJoinPackageMetadata", protoFieldNames.contains("latest_version"), - "projectId", projectId)); - try { - return List.copyOf(query.executeResultList(ComponentProjection.class)); - } finally { - query.closeAll(); - } - } - - /** - * Fetch all {@link org.dependencytrack.model.Component} {@code <->} {@link org.dependencytrack.model.Vulnerability} - * relationships for a given {@link Project}. - * - * @param projectId ID of the {@link Project} to fetch relationships for - * @return A {@link List} of {@link ComponentsVulnerabilitiesProjection} - */ - List fetchAllComponentsVulnerabilities(final long projectId) { - final Query query = pm.newQuery(Query.SQL, /* language=SQL */ """ - SELECT "CV"."COMPONENT_ID" AS "componentId" - , "CV"."VULNERABILITY_ID" AS "vulnerabilityId" - FROM "COMPONENTS_VULNERABILITIES" AS "CV" - INNER JOIN "COMPONENT" AS "C" - ON "C"."ID" = "CV"."COMPONENT_ID" - WHERE "C"."PROJECT_ID" = ? - AND EXISTS ( - SELECT 1 - FROM "FINDINGATTRIBUTION" AS fa - WHERE fa."COMPONENT_ID" = "C"."ID" - AND fa."VULNERABILITY_ID" = "CV"."VULNERABILITY_ID" - AND fa."DELETED_AT" IS NULL - ) - """); - query.setParameters(projectId); - try { - return List.copyOf(query.executeResultList(ComponentsVulnerabilitiesProjection.class)); - } finally { - query.closeAll(); - } - } - - List fetchAllLicenses(final long projectId, - final Collection licenseProtoFieldNames, - final Collection licenseGroupProtoFieldNames) { - final String licenseSqlSelectColumns = Stream.concat( - Stream.of(LicenseProjection.ID_FIELD_MAPPING), - getFieldMappings(LicenseProjection.class).stream() - .filter(mapping -> licenseProtoFieldNames.contains(mapping.protoFieldName())) - ) - .map(mapping -> "\"L\".\"%s\" AS \"%s\"".formatted(mapping.sqlColumnName(), mapping.javaFieldName())) - .collect(Collectors.joining(", ")); - - // If fetching license groups is not necessary, we can just query for licenses and be done with it. - if (!licenseProtoFieldNames.contains("groups")) { - final Query query = pm.newQuery(Query.SQL, """ - SELECT DISTINCT - %s - FROM - "LICENSE" AS "L" - INNER JOIN - "COMPONENT" AS "C" ON "C"."LICENSE_ID" = "L"."ID" - WHERE - "C"."PROJECT_ID" = ? - """.formatted(licenseSqlSelectColumns)); - query.setParameters(projectId); - try { - return List.copyOf(query.executeResultList(LicenseProjection.class)); - } finally { - query.closeAll(); - } - } - - // If groups are required, include them in the license query in order to avoid the 1+N problem. - // Licenses may or may not be assigned to a group. Licenses can be in multiple groups. - // - // Using a simple LEFT JOIN would result in duplicate license data being fetched, e.g.: - // - // | "L"."ID" | "L"."NAME" | "LG"."NAME" | - // | :------- | :--------- | :---------- | - // | 1 | foo | groupA | - // | 1 | foo | groupB | - // | 1 | foo | groupC | - // | 2 | bar | NULL | - // - // To avoid this, we instead aggregate license group fields for each license, and return them as JSON. - // The reason for choosing JSON over native arrays, is that DataNucleus can't deal with arrays cleanly. - // - // | "L"."ID" | "L"."NAME" | "licenseGroupsJson" | - // | :------- | :--------- | :------------------------------------------------------ | - // | 1 | foo | [{"name":"groupA"},{"name":"groupB"},{"name":"groupC"}] | - // | 2 | bar | [] | - - final String licenseSqlGroupByColumns = Stream.concat( - Stream.of(LicenseProjection.ID_FIELD_MAPPING), - getFieldMappings(LicenseProjection.class).stream() - .filter(mapping -> licenseProtoFieldNames.contains(mapping.protoFieldName())) - ) - .map(mapping -> "\"L\".\"%s\"".formatted(mapping.sqlColumnName())) - .collect(Collectors.joining(", ")); - - final String licenseGroupSqlSelectColumns = getFieldMappings(LicenseGroupProjection.class).stream() - .filter(mapping -> licenseGroupProtoFieldNames.contains(mapping.protoFieldName())) - .map(mapping -> "'%s', \"LG\".\"%s\"".formatted(mapping.javaFieldName(), mapping.sqlColumnName())) - .collect(Collectors.joining(", ")); - - final Query query = pm.newQuery(Query.SQL, """ - SELECT DISTINCT - "L"."ID" AS "id", - %s, - CAST(JSONB_AGG(DISTINCT JSONB_BUILD_OBJECT(%s)) AS TEXT) AS "licenseGroupsJson" - FROM - "LICENSE" AS "L" - INNER JOIN - "COMPONENT" AS "C" ON "C"."LICENSE_ID" = "L"."ID" - LEFT JOIN - "LICENSEGROUP_LICENSE" AS "LGL" ON "LGL"."LICENSE_ID" = "L"."ID" - LEFT JOIN - "LICENSEGROUP" AS "LG" ON "LG"."ID" = "LGL"."LICENSEGROUP_ID" - WHERE - "C"."PROJECT_ID" = ? - GROUP BY - %s - """.formatted(licenseSqlSelectColumns, licenseGroupSqlSelectColumns, licenseSqlGroupByColumns)); - query.setParameters(projectId); - try { - return List.copyOf(query.executeResultList(LicenseProjection.class)); - } finally { - query.closeAll(); - } - } - - List fetchAllVulnerabilities(final long projectId, final Collection protoFieldNames) { - String sqlSelectColumns = Stream.concat( - Stream.of(VulnerabilityProjection.ID_FIELD_MAPPING), - getFieldMappings(VulnerabilityProjection.class).stream() - .filter(mapping -> protoFieldNames.contains(mapping.protoFieldName())) - ) - .map(mapping -> "\"V\".\"%s\" AS \"%s\"".formatted(mapping.sqlColumnName(), mapping.javaFieldName())) - .collect(Collectors.joining(", ")); - - if (protoFieldNames.contains("aliases")) { - sqlSelectColumns += ", CAST(JSONB_VULN_ALIASES(\"V\".\"SOURCE\", \"V\".\"VULNID\") AS TEXT) AS \"aliasesJson\""; - } - if (protoFieldNames.contains("epss_score")) { - sqlSelectColumns += ", \"EP\".\"SCORE\" AS \"epssScore\""; - } - if (protoFieldNames.contains("epss_percentile")) { - sqlSelectColumns += ", \"EP\".\"PERCENTILE\" AS \"epssPercentile\""; - } - - final Query query = pm.newQuery(Query.SQL, /* language=SQL */ """ - SELECT DISTINCT %s - FROM "VULNERABILITY" AS "V" - INNER JOIN "COMPONENTS_VULNERABILITIES" AS "CV" - ON "CV"."VULNERABILITY_ID" = "V"."ID" - INNER JOIN "COMPONENT" AS "C" - ON "C"."ID" = "CV"."COMPONENT_ID" - LEFT JOIN "EPSS" AS "EP" - ON "V"."VULNID" = "EP"."CVE" - AND :shouldFetchEpss - WHERE "C"."PROJECT_ID" = :projectId - AND EXISTS ( - SELECT 1 - FROM "FINDINGATTRIBUTION" AS fa - WHERE fa."COMPONENT_ID" = "C"."ID" - AND fa."VULNERABILITY_ID" = "V"."ID" - AND fa."DELETED_AT" IS NULL - ) - """.formatted(sqlSelectColumns)); - query.setNamedParameters(Map.of( - "projectId", projectId, - "shouldFetchEpss", protoFieldNames.contains("epss_score") || protoFieldNames.contains("epss_percentile") - )); - try { - return List.copyOf(query.executeResultList(VulnerabilityProjection.class)); - } finally { - query.closeAll(); - } - } - - List reconcileViolations(final long projectId, final MultiValuedMap reportedViolationsByComponentId) { - // We want to send notifications for newly identified policy violations, - // so need to keep track of which violations we created. - final var newViolationIds = new ArrayList(); - - // DataNucleus does not support batch inserts, which is something we need in order to - // create new violations efficiently. Falling back to "raw" JDBC for the sake of efficiency. - final JDOConnection jdoConnection = pm.getDataStoreConnection(); - final var nativeConnection = (Connection) jdoConnection.getNativeConnection(); - Boolean originalAutoCommit = null; - Integer originalTrxIsolation = null; - - try { - // JDBC connections default to autocommit. - // We'll do multiple write operations here, and want to commit them all in a single transaction. - originalAutoCommit = nativeConnection.getAutoCommit(); - originalTrxIsolation = nativeConnection.getTransactionIsolation(); - nativeConnection.setAutoCommit(false); - nativeConnection.setTransactionIsolation(TRANSACTION_READ_COMMITTED); - - // First, query for all existing policy violations of the project, grouping them by component ID. - final var existingViolationsByComponentId = new HashSetValuedHashMap(); - try (final PreparedStatement ps = nativeConnection.prepareStatement(""" - SELECT - "ID" AS "id", - "COMPONENT_ID" AS "componentId", - "POLICYCONDITION_ID" AS "policyConditionId" - FROM - "POLICYVIOLATION" - WHERE - "PROJECT_ID" = ? - """)) { - ps.setLong(1, projectId); - - final ResultSet rs = ps.executeQuery(); - while (rs.next()) { - existingViolationsByComponentId.put( - rs.getLong("componentId"), - new PolicyViolationProjection( - rs.getLong("id"), - rs.getLong("policyConditionId") - )); - } - } - - // For each component that has existing and / or reported violations... - final Set componentIds = new HashSet<>(reportedViolationsByComponentId.keySet().size() + existingViolationsByComponentId.keySet().size()); - componentIds.addAll(reportedViolationsByComponentId.keySet()); - componentIds.addAll(existingViolationsByComponentId.keySet()); - - // ... determine which existing violations should be deleted (because they're no longer reported), - // and which reported violations should be created (because they have not been reported before). - // - // Violations not belonging to either of those buckets are reported, but already exist, - // meaning no action needs to be taken for them. - final var violationIdsToDelete = new ArrayList(); - final var violationsToCreate = new HashSetValuedHashMap(); - for (final Long componentId : componentIds) { - final Collection existingViolations = existingViolationsByComponentId.get(componentId); - final Collection reportedViolations = reportedViolationsByComponentId.get(componentId); - - if (reportedViolations == null || reportedViolations.isEmpty()) { - // Component has been removed, or does not have any violations anymore. - // All of its existing violations can be deleted. - violationIdsToDelete.addAll(existingViolations.stream().map(PolicyViolationProjection::id).toList()); - continue; - } - - if (existingViolations == null || existingViolations.isEmpty()) { - // Component did not have any violations before, but has some now. - // All reported violations must be newly created. - violationsToCreate.putAll(componentId, reportedViolations); - continue; - } - - // To determine which violations shall be deleted, find occurrences of violations appearing - // in the collection of existing violations, but not in the collection of reported violations. - existingViolations.stream() - .filter(existingViolation -> reportedViolations.stream().noneMatch(newViolation -> - newViolation.getPolicyCondition().getId() == existingViolation.policyConditionId())) - .map(PolicyViolationProjection::id) - .forEach(violationIdsToDelete::add); - - // To determine which violations shall be created, find occurrences of violations appearing - // in the collection of reported violations, but not in the collection of existing violations. - reportedViolations.stream() - .filter(reportedViolation -> existingViolations.stream().noneMatch(existingViolation -> - existingViolation.policyConditionId() == reportedViolation.getPolicyCondition().getId())) - .forEach(reportedViolation -> violationsToCreate.put(componentId, reportedViolation)); - } - - if (!violationsToCreate.isEmpty()) { - // For violations that need to be created, utilize batch inserts to limit database round-trips. - // Keep note of the IDs that were generated as part of the insert; For those we'll need to send - // notifications later. - - try (final PreparedStatement ps = nativeConnection.prepareStatement(""" - INSERT INTO "POLICYVIOLATION" - ("UUID", "TIMESTAMP", "COMPONENT_ID", "PROJECT_ID", "POLICYCONDITION_ID", "TYPE") - VALUES - (?, ?, ?, ?, ?, ?) - ON CONFLICT DO NOTHING - RETURNING "ID" - """, Statement.RETURN_GENERATED_KEYS)) { - for (final Map.Entry entry : violationsToCreate.entries()) { - ps.setObject(1, UUID.randomUUID()); - ps.setTimestamp(2, new Timestamp(entry.getValue().getTimestamp().getTime())); - ps.setLong(3, entry.getKey()); - ps.setLong(4, projectId); - ps.setLong(5, entry.getValue().getPolicyCondition().getId()); - ps.setString(6, entry.getValue().getType().name()); - ps.addBatch(); - } - ps.executeBatch(); - - final ResultSet rs = ps.getGeneratedKeys(); - while (rs.next()) { - newViolationIds.add(rs.getLong(1)); - } - } - } - - if (!violationIdsToDelete.isEmpty()) { - final Array violationIdsToDeleteArray = - nativeConnection.createArrayOf("BIGINT", violationIdsToDelete.toArray(new Long[0])); - - // First, bulk-delete any analysis comments attached to the violations. - try (final PreparedStatement ps = nativeConnection.prepareStatement(""" - DELETE FROM - "VIOLATIONANALYSISCOMMENT" AS "VAC" - USING - "VIOLATIONANALYSIS" AS "VA" - WHERE - "VAC"."VIOLATIONANALYSIS_ID" = "VA"."ID" - AND "VA"."POLICYVIOLATION_ID" = ANY(?) - """)) { - ps.setArray(1, violationIdsToDeleteArray); - ps.execute(); - } - - // Then, bulk-delete any analyses attached to the violations. - try (final PreparedStatement ps = nativeConnection.prepareStatement(""" - DELETE FROM - "VIOLATIONANALYSIS" - WHERE - "POLICYVIOLATION_ID" = ANY(?) - """)) { - ps.setArray(1, violationIdsToDeleteArray); - ps.execute(); - } - - // Finally, bulk-delete the actual violations. - try (final PreparedStatement ps = nativeConnection.prepareStatement(""" - DELETE FROM - "POLICYVIOLATION" - WHERE - "ID" = ANY(?) - """)) { - ps.setArray(1, violationIdsToDeleteArray); - ps.execute(); - } - } - - nativeConnection.commit(); - } catch (Exception e) { - try { - nativeConnection.rollback(); - } catch (SQLException ex) { - throw new RuntimeException(ex); - } - - throw new RuntimeException(e); - } finally { - try { - if (originalAutoCommit != null) { - nativeConnection.setAutoCommit(originalAutoCommit); - } - if (originalTrxIsolation != null) { - nativeConnection.setTransactionIsolation(originalTrxIsolation); - } - } catch (SQLException e) { - LOGGER.error("Failed to restore original connection settings (autoCommit=%s, trxIsolation=%d)" - .formatted(originalAutoCommit, originalTrxIsolation), e); - } - - jdoConnection.close(); - } - - return newViolationIds; - } - - List getApplicablePolicies(final Project project) { - var filter = """ - (this.projects.isEmpty() && this.tags.isEmpty()) - || (this.projects.contains(:project) - """; - var params = new HashMap(); - params.put("project", project); - - // To compensate for missing support for recursion of Common Table Expressions (CTEs) - // in JDO, we have to fetch the UUIDs of all parent projects upfront. Otherwise, we'll - // not be able to evaluate whether the policy is inherited from parent projects. - var variables = ""; - final List parentUuids = getParents(project); - if (!parentUuids.isEmpty()) { - filter += """ - || (this.includeChildren - && this.projects.contains(parentVar) - && :parentUuids.contains(parentVar.uuid)) - """; - variables += "org.dependencytrack.model.Project parentVar"; - params.put("parentUuids", parentUuids); - } - filter += ")"; - - // DataNucleus generates an invalid SQL query when using the idiomatic solution. - // The following works, but it's ugly and likely doesn't perform well if the project - // has many tags. Worth trying the idiomatic way again once DN has been updated to > 6.0.4. - // - // filter += " || (this.tags.contains(commonTag) && :project.tags.contains(commonTag))"; - // variables += "org.dependencytrack.model.Tag commonTag"; - if (project.getTags() != null && !project.getTags().isEmpty()) { - filter += " || ("; - int tagIndex = 0; - for (final Tag tag : project.getTags()) { - filter += "this.tags.contains(:tag" + (tagIndex) + ")"; - params.put("tag" + tagIndex, tag); - if (tagIndex < (project.getTags().size() - 1)) { - filter += " || "; - } - tagIndex++; - } - filter += ")"; - } - - final List policies; - final Query query = pm.newQuery(Policy.class); - try { - query.setFilter(filter); - query.setNamedParameters(params); - if (!variables.isEmpty()) { - query.declareVariables(variables); - } - policies = List.copyOf(query.executeList()); - } finally { - query.closeAll(); - } - - return policies; - } - - List getParents(final Project project) { - return getParents(project.getUuid(), new ArrayList<>()); - } - - List getParents(final UUID uuid, final List parents) { - final UUID parentUuid; - final Query query = pm.newQuery(Project.class); - try { - query.setFilter("uuid == :uuid && parent != null"); - query.setParameters(uuid); - query.setResult("parent.uuid"); - parentUuid = query.executeResultUnique(UUID.class); - } finally { - query.closeAll(); - } - - if (parentUuid == null) { - return parents; - } - - parents.add(parentUuid); - return getParents(parentUuid, parents); - } - - boolean isDirectDependency(final org.dependencytrack.proto.policy.v1.Component component) { - String queryString = /* language=SQL */ """ - SELECT COUNT(*) - FROM "COMPONENT" "C" - INNER JOIN "PROJECT" "P" - ON "P"."ID" = "C"."PROJECT_ID" - AND "P"."DIRECT_DEPENDENCIES" @> JSONB_BUILD_ARRAY(JSONB_BUILD_OBJECT('uuid', :uuid)) - WHERE "C"."UUID" = :uuid - """; - final Query query = pm.newQuery(Query.SQL, queryString); - query.setNamedParameters(Map.of("uuid", UUID.fromString(component.getUuid()))); - long result; - try { - result = query.executeResultUnique(Long.class); - } finally { - query.closeAll(); - } - return result == 1; - } - - @Override - public void close() { - // Noop - } - -} diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/CelVulnerabilityPolicyEvaluator.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/CelVulnerabilityPolicyEvaluator.java index 5503554c15..1879fafb82 100644 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/CelVulnerabilityPolicyEvaluator.java +++ b/apiserver/src/main/java/org/dependencytrack/policy/cel/CelVulnerabilityPolicyEvaluator.java @@ -18,7 +18,6 @@ */ package org.dependencytrack.policy.cel; -import alpine.common.logging.Logger; import com.google.api.expr.v1alpha1.Type; import com.google.protobuf.Timestamp; import com.google.protobuf.util.Timestamps; @@ -35,6 +34,8 @@ import org.dependencytrack.proto.policy.v1.Vulnerability; import org.projectnessie.cel.tools.ScriptCreateException; import org.projectnessie.cel.tools.ScriptExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collection; @@ -58,7 +59,7 @@ */ public class CelVulnerabilityPolicyEvaluator implements VulnerabilityPolicyEvaluator { - private static final Logger LOGGER = Logger.getLogger(CelVulnerabilityPolicyEvaluator.class); + private static final Logger LOGGER = LoggerFactory.getLogger(CelVulnerabilityPolicyEvaluator.class); private final CelPolicyScriptHost scriptHost; @@ -141,7 +142,7 @@ private Map> evaluateAllInternal( final Project scriptArgProject; if (scriptRequirements.containsKey(TYPE_PROJECT)) { scriptArgProject = withJdbiHandle(handle -> - handle.attach(CelPolicyDao.class).loadRequiredFields(projectId, scriptRequirements)); + new CelPolicyDao(handle).loadRequiredFields(projectId, scriptRequirements)); } else { scriptArgProject = Project.getDefaultInstance(); } @@ -150,7 +151,7 @@ private Map> evaluateAllInternal( final Map loadedComponentsById; if (scriptRequirements.containsKey(TYPE_COMPONENT)) { loadedComponentsById = withJdbiHandle(handle -> - handle.attach(CelPolicyDao.class).loadRequiredComponentFields( + new CelPolicyDao(handle).loadRequiredComponentFields( vulnIdsByComponentId.keySet(), scriptRequirements)); } else { loadedComponentsById = vulnIdsByComponentId.keySet().stream() @@ -167,7 +168,7 @@ private Map> evaluateAllInternal( final Map loadedVulnsById; if (scriptRequirements.containsKey(TYPE_VULNERABILITY)) { loadedVulnsById = withJdbiHandle(handle -> - handle.attach(CelPolicyDao.class).loadRequiredVulnerabilityFields( + new CelPolicyDao(handle).loadRequiredVulnerabilityFields( uniqueVulnIds, scriptRequirements)); } else { loadedVulnsById = uniqueVulnIds.stream() @@ -230,7 +231,7 @@ private static Map evaluateForComponent( policyLoop: for (final var policyNameAndScripts : compiledScriptsByPolicyName.entrySet()) { if (shortCircuitedPolicies.contains(policyNameAndScripts.getKey())) { - LOGGER.debug("Policy %s already short-circuited".formatted(policyNameAndScripts.getKey())); + LOGGER.debug("Policy {} already short-circuited", policyNameAndScripts.getKey()); continue; } @@ -249,15 +250,16 @@ private static Map evaluateForComponent( } else if (!conditionMatched) { for (final Map.Entry> otherPolicyNameAndScripts : compiledScriptsByPolicyName.entrySet()) { if (!otherPolicyNameAndScripts.getKey().equals(policyNameAndScripts.getKey()) && otherPolicyNameAndScripts.getValue().contains(script)) { - LOGGER.debug("Short-circuiting policy %s".formatted(otherPolicyNameAndScripts.getKey())); + LOGGER.debug("Short-circuiting policy {}", otherPolicyNameAndScripts.getKey()); shortCircuitedPolicies.add(otherPolicyNameAndScripts.getKey()); } } continue policyLoop; } } catch (ScriptExecutionException e) { - LOGGER.warn("Failed to execute script for condition #%d of policy %s with arguments %s" - .formatted(policyNameAndScripts.getValue().indexOf(script), policyNameAndScripts.getKey(), scriptArguments), e); + LOGGER.warn( + "Failed to execute script for condition #{} of policy {} with arguments {}", + policyNameAndScripts.getValue().indexOf(script), policyNameAndScripts.getKey(), scriptArguments, e); break policyLoop; } } @@ -271,8 +273,9 @@ private CelPolicyScript compileConditionScript(final String conditionScriptSrc) try { return scriptHost.compile(conditionScriptSrc, CelPolicyScriptHost.CacheMode.CACHE); } catch (ScriptCreateException e) { - LOGGER.warn("Failed to compile script %s; Condition will be skipped" - .formatted(conditionScriptSrc), e); + LOGGER.warn( + "Failed to compile script {}; Condition will be skipped", + conditionScriptSrc, e); return null; } } diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/ComponentProjection.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/ComponentProjection.java deleted file mode 100644 index 4238347e2d..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/ComponentProjection.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.policy.cel.mapping; - -import java.util.Date; - -public class ComponentProjection { - public static FieldMapping ID_FIELD_MAPPING = new FieldMapping("id", /* protoFieldName */ null, "ID"); - public long id; - - @MappedField(sqlColumnName = "UUID") - public String uuid; - - @MappedField(sqlColumnName = "GROUP") - public String group; - - @MappedField(sqlColumnName = "NAME") - public String name; - - @MappedField(sqlColumnName = "VERSION") - public String version; - - @MappedField(sqlColumnName = "CLASSIFIER") - public String classifier; - - @MappedField(sqlColumnName = "CPE") - public String cpe; - - @MappedField(sqlColumnName = "PURL") - public String purl; - - @MappedField(protoFieldName = "swid_tag_id", sqlColumnName = "SWIDTAGID") - public String swidTagId; - - @MappedField(protoFieldName = "is_internal", sqlColumnName = "INTERNAL") - public Boolean internal; - - @MappedField(sqlColumnName = "MD5") - public String md5; - - @MappedField(sqlColumnName = "SHA1") - public String sha1; - - @MappedField(sqlColumnName = "SHA_256") - public String sha256; - - @MappedField(sqlColumnName = "SHA_384") - public String sha384; - - @MappedField(sqlColumnName = "SHA_512") - public String sha512; - - @MappedField(sqlColumnName = "SHA3_256") - public String sha3_256; - - @MappedField(sqlColumnName = "SHA3_384") - public String sha3_384; - - @MappedField(sqlColumnName = "SHA3_512") - public String sha3_512; - - @MappedField(sqlColumnName = "BLAKE2B_256") - public String blake2b_256; - - @MappedField(sqlColumnName = "BLAKE2B_384") - public String blake2b_384; - - @MappedField(sqlColumnName = "BLAKE2B_512") - public String blake2b_512; - - @MappedField(sqlColumnName = "BLAKE3") - public String blake3; - @MappedField(protoFieldName = "resolved_license", sqlColumnName = "LICENSE_ID") - public Long resolvedLicenseId; - - @MappedField(protoFieldName = "license_name", sqlColumnName = "LICENSE") - public String licenseName; - - public Date publishedAt; - - public String latestVersion; - @MappedField(protoFieldName = "license_expression", sqlColumnName = "LICENSE_EXPRESSION") - public String licenseExpression; - -} diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/ComponentsVulnerabilitiesProjection.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/ComponentsVulnerabilitiesProjection.java deleted file mode 100644 index aefb00bd22..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/ComponentsVulnerabilitiesProjection.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.policy.cel.mapping; - -public class ComponentsVulnerabilitiesProjection { - - public Long componentId; - - public Long vulnerabilityId; - -} diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/FieldMapping.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/FieldMapping.java deleted file mode 100644 index c4091f3e0e..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/FieldMapping.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.policy.cel.mapping; - -public record FieldMapping(String javaFieldName, String protoFieldName, String sqlColumnName) { -} diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/FieldMappingUtil.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/FieldMappingUtil.java deleted file mode 100644 index 1479bdd00d..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/FieldMappingUtil.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.policy.cel.mapping; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -import static org.apache.commons.lang3.StringUtils.trimToNull; - -public final class FieldMappingUtil { - - private static final Map, List> FIELD_MAPPINGS_BY_CLASS = new ConcurrentHashMap<>(); - - private FieldMappingUtil() { - } - - public static List getFieldMappings(final Class clazz) { - return FIELD_MAPPINGS_BY_CLASS.computeIfAbsent(clazz, FieldMappingUtil::createFieldMappings); - } - - private static List createFieldMappings(final Class clazz) { - final var fieldMappings = new ArrayList(); - - for (final Field field : clazz.getDeclaredFields()) { - final MappedField mappedFieldAnnotation = field.getAnnotation(MappedField.class); - if (mappedFieldAnnotation == null) { - continue; - } - - final String javaFieldName = field.getName(); - final String protoFieldName = Optional.ofNullable(trimToNull(mappedFieldAnnotation.protoFieldName())).orElse(javaFieldName); - final String sqlColumnName = Optional.ofNullable(trimToNull(mappedFieldAnnotation.sqlColumnName())).orElseThrow(); - fieldMappings.add(new FieldMapping(javaFieldName, protoFieldName, sqlColumnName)); - } - - return fieldMappings; - } - -} diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/LicenseGroupProjection.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/LicenseGroupProjection.java deleted file mode 100644 index 30f3f9442b..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/LicenseGroupProjection.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.policy.cel.mapping; - -public class LicenseGroupProjection { - - @MappedField(sqlColumnName = "UUID") - public String uuid; - - @MappedField(sqlColumnName = "NAME") - public String name; - -} diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/LicenseProjection.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/LicenseProjection.java deleted file mode 100644 index 89033f56b0..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/LicenseProjection.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.policy.cel.mapping; - -public class LicenseProjection { - - public static FieldMapping ID_FIELD_MAPPING = new FieldMapping("id", /* protoFieldName */ null, "ID"); - - public long id; - - @MappedField(sqlColumnName = "UUID") - public String uuid; - - @MappedField(protoFieldName = "id", sqlColumnName = "LICENSEID") - public String licenseId; - - @MappedField(sqlColumnName = "NAME") - public String name; - - @MappedField(protoFieldName = "is_osi_approved", sqlColumnName = "ISOSIAPPROVED") - public Boolean isOsiApproved; - - @MappedField(protoFieldName = "is_fsf_libre", sqlColumnName = "FSFLIBRE") - public Boolean isFsfLibre; - - @MappedField(protoFieldName = "is_deprecated_id", sqlColumnName = "ISDEPRECATED") - public Boolean isDeprecatedId; - - @MappedField(protoFieldName = "is_custom", sqlColumnName = "ISCUSTOMLICENSE") - public Boolean isCustomLicense; - - public String licenseGroupsJson; - -} diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/MappedField.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/MappedField.java deleted file mode 100644 index fe0b04e2e7..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/MappedField.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.policy.cel.mapping; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface MappedField { - - /** - * Name of the field in the Protobuf schema. - *

- * If empty string (the default), the name of the annotated field will be assumed. - * - * @return Name of the Protobuf field - */ - String protoFieldName() default ""; - - /** - * Name of the SQL column corresponding to this field. - * - * @return Name of the SQL column - */ - String sqlColumnName(); - -} diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/PolicyViolationProjection.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/PolicyViolationProjection.java deleted file mode 100644 index a7c4d88588..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/PolicyViolationProjection.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.policy.cel.mapping; - -public record PolicyViolationProjection(Long id, Long policyConditionId) { -} diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/ProjectProjection.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/ProjectProjection.java deleted file mode 100644 index ff9cb328c4..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/ProjectProjection.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.policy.cel.mapping; - -import java.util.Date; - -public class ProjectProjection { - - public static FieldMapping ID_FIELD_MAPPING = new FieldMapping("id", /* protoFieldName */ null, "ID"); - - public long id; - - @MappedField(sqlColumnName = "UUID") - public String uuid; - - @MappedField(sqlColumnName = "GROUP") - public String group; - - @MappedField(sqlColumnName = "NAME") - public String name; - - @MappedField(sqlColumnName = "VERSION") - public String version; - - @MappedField(sqlColumnName = "CLASSIFIER") - public String classifier; - - @MappedField(sqlColumnName = "CPE") - public String cpe; - - @MappedField(sqlColumnName = "PURL") - public String purl; - - @MappedField(protoFieldName = "swid_tag_id", sqlColumnName = "SWIDTAGID") - public String swidTagId; - - @MappedField(protoFieldName = "last_bom_import", sqlColumnName = "LAST_BOM_IMPORTED") - public Date lastBomImport; - -} diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/ProjectPropertyProjection.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/ProjectPropertyProjection.java deleted file mode 100644 index 66171f0acf..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/ProjectPropertyProjection.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.policy.cel.mapping; - -public class ProjectPropertyProjection { - - @MappedField(sqlColumnName = "GROUPNAME") - public String group; - - @MappedField(sqlColumnName = "PROPERTYNAME") - public String name; - - @MappedField(sqlColumnName = "PROPERTYVALUE") - public String value; - - @MappedField(sqlColumnName = "PROPERTYTYPE") - public String type; - -} diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/VulnerabilityProjection.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/VulnerabilityProjection.java deleted file mode 100644 index 64a516c9a1..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/mapping/VulnerabilityProjection.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.policy.cel.mapping; - -import java.math.BigDecimal; -import java.util.Date; - -public class VulnerabilityProjection { - - public static FieldMapping ID_FIELD_MAPPING = new FieldMapping("id", /* protoFieldName */ null, "ID"); - - public long id; - - @MappedField(sqlColumnName = "UUID") - public String uuid; - - @MappedField(protoFieldName = "id", sqlColumnName = "VULNID") - public String vulnId; - - @MappedField(sqlColumnName = "SOURCE") - public String source; - - @MappedField(sqlColumnName = "CWES") - public String cwes; - - @MappedField(sqlColumnName = "CREATED") - public Date created; - - @MappedField(sqlColumnName = "PUBLISHED") - public Date published; - - @MappedField(sqlColumnName = "UPDATED") - public Date updated; - - @MappedField(sqlColumnName = "SEVERITY") - public String severity; - - @MappedField(protoFieldName = "cvssv2_base_score", sqlColumnName = "CVSSV2BASESCORE") - public BigDecimal cvssV2BaseScore; - - @MappedField(protoFieldName = "cvssv2_impact_subscore", sqlColumnName = "CVSSV2IMPACTSCORE") - public BigDecimal cvssV2ImpactSubScore; - - @MappedField(protoFieldName = "cvssv2_exploitability_subscore", sqlColumnName = "CVSSV2EXPLOITSCORE") - public BigDecimal cvssV2ExploitabilitySubScore; - - @MappedField(protoFieldName = "cvssv2_vector", sqlColumnName = "CVSSV2VECTOR") - public String cvssV2Vector; - - @MappedField(protoFieldName = "cvssv3_base_score", sqlColumnName = "CVSSV3BASESCORE") - public BigDecimal cvssV3BaseScore; - - @MappedField(protoFieldName = "cvssv3_impact_subscore", sqlColumnName = "CVSSV3IMPACTSCORE") - public BigDecimal cvssV3ImpactSubScore; - - @MappedField(protoFieldName = "cvssv3_exploitability_subscore", sqlColumnName = "CVSSV3EXPLOITSCORE") - public BigDecimal cvssV3ExploitabilitySubScore; - - @MappedField(protoFieldName = "cvssv3_vector", sqlColumnName = "CVSSV3VECTOR") - public String cvssV3Vector; - - @MappedField(protoFieldName = "cvssv4_score", sqlColumnName = "CVSSV4SCORE") - public BigDecimal cvssV4Score; - - @MappedField(protoFieldName = "cvssv4_vector", sqlColumnName = "CVSSV4VECTOR") - public String cvssV4Vector; - - @MappedField(protoFieldName = "owasp_rr_likelihood_score", sqlColumnName = "OWASPRRLIKELIHOODSCORE") - public BigDecimal owaspRrLikelihoodScore; - - @MappedField(protoFieldName = "owasp_rr_technical_impact_score", sqlColumnName = "OWASPRRTECHNICALIMPACTSCORE") - public BigDecimal owaspRrTechnicalImpactScore; - - @MappedField(protoFieldName = "owasp_rr_business_impact_score", sqlColumnName = "OWASPRRBUSINESSIMPACTSCORE") - public BigDecimal owaspRrBusinessImpactScore; - - @MappedField(protoFieldName = "owasp_rr_vector", sqlColumnName = "OWASPRRVECTOR") - public String owaspRrVector; - - public BigDecimal epssScore; - - public BigDecimal epssPercentile; - - public String aliasesJson; - -} diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyComponentRowMapper.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyComponentRowMapper.java index ce5027f612..b6748f28b8 100644 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyComponentRowMapper.java +++ b/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyComponentRowMapper.java @@ -28,7 +28,7 @@ import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.maybeSet; -public class CelPolicyComponentRowMapper implements RowMapper { +public final class CelPolicyComponentRowMapper implements RowMapper { @Override public Component map(final ResultSet rs, final StatementContext ctx) throws SQLException { @@ -50,6 +50,10 @@ public Component map(final ResultSet rs, final StatementContext ctx) throws SQLE maybeSet(rs, "sha3_256", ResultSet::getString, builder::setSha3256); maybeSet(rs, "sha3_384", ResultSet::getString, builder::setSha3384); maybeSet(rs, "sha3_512", ResultSet::getString, builder::setSha3512); + maybeSet(rs, "blake2b_256", ResultSet::getString, builder::setBlake2B256); + maybeSet(rs, "blake2b_384", ResultSet::getString, builder::setBlake2B384); + maybeSet(rs, "blake2b_512", ResultSet::getString, builder::setBlake2B512); + maybeSet(rs, "blake3", ResultSet::getString, builder::setBlake3); maybeSet(rs, "license_name", ResultSet::getString, builder::setLicenseName); maybeSet(rs, "license_expression", ResultSet::getString, builder::setLicenseExpression); maybeSet(rs, "published_at", RowMapperUtil::nullableTimestamp, builder::setPublishedAt); diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyDao.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyDao.java index ea5ef57646..f5d8b1c622 100644 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyDao.java +++ b/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyDao.java @@ -18,166 +18,565 @@ */ package org.dependencytrack.policy.cel.persistence; -import alpine.common.logging.Logger; import com.google.api.expr.v1alpha1.Type; import org.apache.commons.collections4.MultiValuedMap; -import org.dependencytrack.policy.cel.mapping.ComponentProjection; -import org.dependencytrack.policy.cel.mapping.ProjectProjection; -import org.dependencytrack.policy.cel.mapping.ProjectPropertyProjection; -import org.dependencytrack.policy.cel.mapping.VulnerabilityProjection; +import org.dependencytrack.model.Policy; +import org.dependencytrack.model.PolicyCondition; +import org.dependencytrack.model.PolicyViolation; import org.dependencytrack.proto.policy.v1.Component; +import org.dependencytrack.proto.policy.v1.License; import org.dependencytrack.proto.policy.v1.Project; import org.dependencytrack.proto.policy.v1.Vulnerability; -import org.jdbi.v3.sqlobject.config.KeyColumn; -import org.jdbi.v3.sqlobject.config.RegisterRowMapper; -import org.jdbi.v3.sqlobject.customizer.Bind; -import org.jdbi.v3.sqlobject.customizer.Define; -import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.core.Handle; +import org.jspecify.annotations.Nullable; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; -import java.util.stream.Collectors; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Stream; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_COMPONENT; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_PROJECT; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_PROJECT_METADATA; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_PROJECT_PROPERTY; import static org.dependencytrack.policy.cel.definition.CelPolicyTypes.TYPE_VULNERABILITY; -import static org.dependencytrack.policy.cel.mapping.FieldMappingUtil.getFieldMappings; - -public interface CelPolicyDao { - - Logger LOGGER = Logger.getLogger(CelPolicyDao.class); - - @SqlQuery(""" - SELECT ${fetchColumns?join(", ")} - FROM "PROJECT" AS p - <#if fetchColumns?filter(col -> col?contains("\\"metadata_tools\\""))?size gt 0> - INNER JOIN "PROJECT_METADATA" AS pm - ON pm."PROJECT_ID" = p."ID" - - <#if fetchColumns?filter(col -> col?contains("\\"bom_generated\\""))?size gt 0> - INNER JOIN "BOM" AS b - ON b."PROJECT_ID" = p."ID" - - <#if fetchPropertyColumns?size gt 0> - LEFT JOIN LATERAL ( - SELECT CAST(JSONB_AGG(DISTINCT JSONB_BUILD_OBJECT(${fetchPropertyColumns?join(", ")})) AS TEXT) AS "properties" - FROM "PROJECT_PROPERTY" AS pp - WHERE pp."PROJECT_ID" = p."ID" - ) AS "properties" ON TRUE - - <#if fetchColumns?seq_contains("\\"tags\\"")> - LEFT JOIN LATERAL ( - SELECT ARRAY_AGG(DISTINCT t."NAME") AS "tags" - FROM "TAG" AS t - INNER JOIN "PROJECTS_TAGS" AS pt - ON pt."TAG_ID" = t."ID" - WHERE pt."PROJECT_ID" = p."ID" - ) AS "tags" ON TRUE - - WHERE p."ID" = :id - """) - @RegisterRowMapper(CelPolicyProjectRowMapper.class) - Project getProject( - @Define List fetchColumns, - @Define List fetchPropertyColumns, - @Bind long id); - - @SqlQuery(""" - SELECT c."ID" AS db_id - <#if fetchColumns?size gt 0> - , ${fetchColumns?join(", ")} - - FROM "COMPONENT" AS c - <#if fetchColumns?seq_contains("\\"published_at\\"")> - LEFT JOIN LATERAL ( - SELECT pam."PUBLISHED_AT" AS "published_at" - FROM "PACKAGE_ARTIFACT_METADATA" AS pam - WHERE pam."PURL" = c."PURL" - ) AS "integrityMeta" ON TRUE - - <#if fetchColumns?seq_contains("\\"latest_version\\"")> - LEFT JOIN LATERAL ( - SELECT pm."LATEST_VERSION" AS "latest_version" - FROM "PACKAGE_ARTIFACT_METADATA" AS pam - JOIN "PACKAGE_METADATA" AS pm ON pm."PURL" = pam."PACKAGE_PURL" - WHERE pam."PURL" = c."PURL" - ) AS "repoMeta" ON TRUE - - WHERE c."ID" = ANY(:ids) - """) - @KeyColumn("db_id") - @RegisterRowMapper(CelPolicyComponentRowMapper.class) - Map getComponents(@Define List fetchColumns, @Bind Collection ids); - - @SqlQuery(""" - SELECT v."ID" AS db_id - <#if fetchColumns?size gt 0> - , ${fetchColumns?join(", ")} - - FROM "VULNERABILITY" AS v - <#if fetchColumns?seq_contains("e.\\"SCORE\\" AS \\"epss_score\\"") || fetchColumns?seq_contains("e.\\"PERCENTILE\\" AS \\"epss_percentile\\"")> - LEFT JOIN "EPSS" AS e - ON v."VULNID" = e."CVE" - - WHERE v."ID" = ANY(:ids) - """) - @KeyColumn("db_id") - @RegisterRowMapper(CelPolicyVulnerabilityRowMapper.class) - Map getVulnerabilities(@Define List fetchColumns, @Bind Collection ids); - - default Project loadRequiredFields(long projectId, final MultiValuedMap requirements) { +import static org.dependencytrack.policy.cel.persistence.CelPolicyFieldMappingRegistry.COMPONENT_FIELDS; +import static org.dependencytrack.policy.cel.persistence.CelPolicyFieldMappingRegistry.LICENSE_FIELDS; +import static org.dependencytrack.policy.cel.persistence.CelPolicyFieldMappingRegistry.LICENSE_GROUP_FIELDS; +import static org.dependencytrack.policy.cel.persistence.CelPolicyFieldMappingRegistry.PROJECT_FIELDS; +import static org.dependencytrack.policy.cel.persistence.CelPolicyFieldMappingRegistry.PROJECT_PROPERTY_FIELDS; +import static org.dependencytrack.policy.cel.persistence.CelPolicyFieldMappingRegistry.VULNERABILITY_FIELDS; +import static org.dependencytrack.policy.cel.persistence.CelPolicyFieldMappingRegistry.selectColumns; + +public final class CelPolicyDao { + + private final Handle jdbiHandle; + + public CelPolicyDao(Handle jdbiHandle) { + this.jdbiHandle = jdbiHandle; + } + + public record ComponentWithLicenseId( + Component component, + @Nullable Long resolvedLicenseId) { + } + + public Map fetchAllComponents( + long projectId, + Collection protoFieldNames) { + final List fetchColumns = new ArrayList<>(); + fetchColumns.add("c.\"ID\" AS db_id"); + + final boolean needsResolvedLicense = protoFieldNames.contains("resolved_license"); + final List fieldNames = protoFieldNames.stream() + .filter(fieldName -> !"resolved_license".equals(fieldName)) + .toList(); + fetchColumns.addAll(selectColumns(COMPONENT_FIELDS, fieldNames)); + + if (needsResolvedLicense) { + fetchColumns.add("c.\"LICENSE_ID\" AS resolved_license_id"); + } + + final boolean shouldJoinPam = + protoFieldNames.contains("published_at") + || protoFieldNames.contains("latest_version"); + + final var componentRowMapper = new CelPolicyComponentRowMapper(); + return jdbiHandle + .createQuery(/* language=InjectedFreeMarker */ """ + <#-- @ftlvariable name="fetchColumns" type="java.util.Collection" --> + <#-- @ftlvariable name="shouldJoinPam" type="boolean" --> + <#-- @ftlvariable name="shouldJoinLatestVersion" type="boolean" --> + SELECT ${fetchColumns?join(", ")} + FROM "COMPONENT" AS c + <#if shouldJoinPam!false> + LEFT JOIN "PACKAGE_ARTIFACT_METADATA" AS pam + ON pam."PURL" = c."PURL" + + <#if shouldJoinLatestVersion!false> + LEFT JOIN "PACKAGE_METADATA" AS pm + ON pm."PURL" = pam."PACKAGE_PURL" + + WHERE c."PROJECT_ID" = :projectId + """) + .define("fetchColumns", fetchColumns) + .define("shouldJoinPam", shouldJoinPam) + .define("shouldJoinLatestVersion", protoFieldNames.contains("latest_version")) + .bind("projectId", projectId) + .reduceResultSet( + new HashMap<>(), + (accumulator, rs, ctx) -> { + final long dbId = rs.getLong("db_id"); + Long licenseId = needsResolvedLicense + ? rs.getLong("resolved_license_id") + : null; + if (licenseId != null && rs.wasNull()) { + licenseId = null; + } + accumulator.put( + dbId, + new ComponentWithLicenseId( + componentRowMapper.map(rs, ctx), + licenseId)); + return accumulator; + }); + } + + public Map> fetchAllComponentsVulnerabilities(long projectId) { + return jdbiHandle + .createQuery(""" + SELECT cv."COMPONENT_ID" AS component_id + , cv."VULNERABILITY_ID" AS vulnerability_id + FROM "COMPONENTS_VULNERABILITIES" AS cv + INNER JOIN "COMPONENT" AS c + ON c."ID" = cv."COMPONENT_ID" + WHERE c."PROJECT_ID" = :projectId + AND EXISTS ( + SELECT 1 + FROM "FINDINGATTRIBUTION" AS fa + WHERE fa."COMPONENT_ID" = c."ID" + AND fa."VULNERABILITY_ID" = cv."VULNERABILITY_ID" + AND fa."DELETED_AT" IS NULL + ) + """) + .bind("projectId", projectId) + .reduceResultSet( + new HashMap<>(), + (accumulator, rs, ctx) -> { + final long componentId = rs.getLong("component_id"); + final long vulnerabilityId = rs.getLong("vulnerability_id"); + accumulator + .computeIfAbsent(componentId, k -> new HashSet<>()) + .add(vulnerabilityId); + return accumulator; + }); + } + + public Map fetchAllLicenses( + long projectId, + Collection licenseProtoFieldNames, + Collection licenseGroupProtoFieldNames) { + final List fetchColumns = new ArrayList<>(); + fetchColumns.add("l.\"ID\" AS db_id"); + fetchColumns.addAll(selectColumns(LICENSE_FIELDS, licenseProtoFieldNames)); + + if (!licenseProtoFieldNames.contains("groups")) { + final var licenseRowMapper = new CelPolicyLicenseRowMapper(); + return jdbiHandle + .createQuery(/* language=InjectedFreeMarker */ """ + <#-- @ftlvariable name="fetchColumns" type="java.util.Collection" --> + SELECT DISTINCT ${fetchColumns?join(", ")} + FROM "LICENSE" AS l + INNER JOIN "COMPONENT" AS c + ON c."LICENSE_ID" = l."ID" + WHERE c."PROJECT_ID" = :projectId + """) + .define("fetchColumns", fetchColumns) + .bind("projectId", projectId) + .reduceResultSet(new HashMap<>(), (accumulator, rs, ctx) -> { + final long dbId = rs.getLong("db_id"); + accumulator.put(dbId, licenseRowMapper.map(rs, ctx)); + return accumulator; + }); + } + + final List groupByColumns = Stream.concat( + Stream.of("l.\"ID\""), + LICENSE_FIELDS.stream() + .filter(fieldMapping -> licenseProtoFieldNames.contains(fieldMapping.protoFieldName())) + .map(CelPolicyFieldMappingRegistry.FieldMapping::sqlExpression)) + .toList(); + + final var groupObjectColumns = new ArrayList<>(LICENSE_GROUP_FIELDS.stream() + .filter(fieldMapping -> licenseGroupProtoFieldNames.contains(fieldMapping.protoFieldName())) + .map(fieldMapping -> "'%s', %s".formatted(fieldMapping.protoFieldName(), fieldMapping.sqlExpression())) + .toList()); + + // Always include UUID to ensure DISTINCT produces correct cardinality, + // even when no specific group fields are accessed. + if (licenseGroupProtoFieldNames.stream().noneMatch("uuid"::equals)) { + groupObjectColumns.addFirst("'uuid', lg.\"UUID\""); + } + + fetchColumns.add(""" + CAST( + COALESCE( + JSONB_AGG(DISTINCT JSONB_BUILD_OBJECT(%s)) FILTER (WHERE lg."ID" IS NOT NULL) + , CAST('[]' AS JSONB) + ) AS TEXT + ) AS groups_json\ + """.formatted(String.join(", ", groupObjectColumns))); + + final var licenseRowMapper = new CelPolicyLicenseRowMapper(); + return jdbiHandle + .createQuery(/* language=InjectedFreeMarker */ """ + <#-- @ftlvariable name="fetchColumns" type="java.util.Collection" --> + <#-- @ftlvariable name="groupByColumns" type="java.util.Collection" --> + SELECT DISTINCT + ${fetchColumns?join(", ")} + FROM "LICENSE" AS l + INNER JOIN "COMPONENT" AS c + ON c."LICENSE_ID" = l."ID" + LEFT JOIN "LICENSEGROUP_LICENSE" AS lgl + ON lgl."LICENSE_ID" = l."ID" + LEFT JOIN "LICENSEGROUP" AS lg + ON lg."ID" = lgl."LICENSEGROUP_ID" + WHERE c."PROJECT_ID" = :projectId + GROUP BY ${groupByColumns?join(", ")} + """) + .define("fetchColumns", fetchColumns) + .define("groupByColumns", groupByColumns) + .bind("projectId", projectId) + .reduceResultSet( + new HashMap<>(), + (accumulator, rs, ctx) -> { + final long dbId = rs.getLong("db_id"); + accumulator.put(dbId, licenseRowMapper.map(rs, ctx)); + return accumulator; + }); + } + + public Map fetchAllVulnerabilities( + long projectId, + Collection protoFieldNames) { + final List fetchColumns = new ArrayList<>(); + fetchColumns.add("v.\"ID\" AS db_id"); + fetchColumns.addAll(selectColumns(VULNERABILITY_FIELDS, protoFieldNames)); + + final boolean shouldFetchEpss = + protoFieldNames.contains("epss_score") + || protoFieldNames.contains("epss_percentile"); + + final var vulnRowMapper = new CelPolicyVulnerabilityRowMapper(); + return jdbiHandle + .createQuery(/* language=InjectedFreeMarker */ """ + <#-- @ftlvariable name="fetchColumns" type="java.util.Collection" --> + <#-- @ftlvariable name="shouldFetchEpss" type="boolean" --> + SELECT DISTINCT ${fetchColumns?join(", ")} + FROM "VULNERABILITY" AS v + INNER JOIN "COMPONENTS_VULNERABILITIES" AS cv + ON cv."VULNERABILITY_ID" = v."ID" + INNER JOIN "COMPONENT" AS c + ON c."ID" = cv."COMPONENT_ID" + <#if shouldFetchEpss!false> + LEFT JOIN "EPSS" AS ep + ON v."VULNID" = ep."CVE" + + WHERE c."PROJECT_ID" = :projectId + AND EXISTS ( + SELECT 1 + FROM "FINDINGATTRIBUTION" AS fa + WHERE fa."COMPONENT_ID" = c."ID" + AND fa."VULNERABILITY_ID" = v."ID" + AND fa."DELETED_AT" IS NULL + ) + """) + .define("fetchColumns", fetchColumns) + .define("shouldFetchEpss", shouldFetchEpss) + .bind("projectId", projectId) + .reduceResultSet( + new HashMap<>(), + (accumulator, rs, ctx) -> { + final long dbId = rs.getLong("db_id"); + accumulator.put(dbId, vulnRowMapper.map(rs, ctx)); + return accumulator; + }); + } + + public boolean isDirectDependency(Component component) { + return jdbiHandle + .createQuery(""" + SELECT EXISTS ( + SELECT 1 + FROM "COMPONENT" AS c + INNER JOIN "PROJECT" AS p + ON p."ID" = c."PROJECT_ID" + AND p."DIRECT_DEPENDENCIES" @> JSONB_BUILD_ARRAY(JSONB_BUILD_OBJECT('uuid', :uuid)) + WHERE c."UUID" = CAST(:uuid AS UUID) + ) + """) + .bind("uuid", component.getUuid()) + .mapTo(Boolean.class) + .one(); + } + + public List getApplicablePolicies(long projectId) { + return jdbiHandle + .createQuery(""" + SELECT p."ID" AS policy_id + , p."UUID" AS policy_uuid + , p."NAME" AS policy_name + , p."OPERATOR" AS policy_operator + , p."VIOLATIONSTATE" AS policy_violation_state + , pc."ID" AS condition_id + , pc."UUID" AS condition_uuid + , pc."OPERATOR" AS condition_operator + , pc."SUBJECT" AS condition_subject + , pc."VALUE" AS condition_value + , pc."VIOLATIONTYPE" AS condition_violation_type + FROM "POLICY" AS p + INNER JOIN "POLICYCONDITION" AS pc + ON pc."POLICY_ID" = p."ID" + WHERE p."ID" IN ( + -- "Global" policies without restrictions. + SELECT p2."ID" + FROM "POLICY" AS p2 + WHERE NOT EXISTS (SELECT 1 FROM "POLICY_PROJECTS" WHERE "POLICY_ID" = p2."ID") + AND NOT EXISTS (SELECT 1 FROM "POLICY_TAGS" WHERE "POLICY_ID" = p2."ID") + UNION + -- Policies restricted to the project, or a parent of the project. + SELECT pp."POLICY_ID" + FROM "POLICY_PROJECTS" AS pp + INNER JOIN "POLICY" AS p3 + ON p3."ID" = pp."POLICY_ID" + INNER JOIN "PROJECT_HIERARCHY" AS ph + ON ph."PARENT_PROJECT_ID" = pp."PROJECT_ID" + WHERE ph."CHILD_PROJECT_ID" = :projectId + AND (ph."DEPTH" = 0 OR p3."INCLUDE_CHILDREN") + UNION + -- Policies restricted tags shared with the project. + SELECT pt."POLICY_ID" + FROM "POLICY_TAGS" AS pt + INNER JOIN "PROJECTS_TAGS" AS prt + ON prt."TAG_ID" = pt."TAG_ID" + WHERE prt."PROJECT_ID" = :projectId + ) + ORDER BY p."ID" + , pc."ID" + """) + .bind("projectId", projectId) + .reduceResultSet( + new LinkedHashMap(), + (accumulator, rs, ctx) -> { + final long policyId = rs.getLong("policy_id"); + + Policy policy = accumulator.get(policyId); + if (policy == null) { + policy = new Policy(); + policy.setId(policyId); + policy.setUuid(rs.getObject("policy_uuid", UUID.class)); + policy.setName(rs.getString("policy_name")); + policy.setOperator(Policy.Operator.valueOf(rs.getString("policy_operator"))); + policy.setViolationState(Policy.ViolationState.valueOf(rs.getString("policy_violation_state"))); + policy.setPolicyConditions(new ArrayList<>()); + accumulator.put(policyId, policy); + } + + final var condition = new PolicyCondition(); + condition.setId(rs.getLong("condition_id")); + condition.setUuid(rs.getObject("condition_uuid", UUID.class)); + condition.setOperator(PolicyCondition.Operator.valueOf(rs.getString("condition_operator"))); + condition.setSubject(PolicyCondition.Subject.valueOf(rs.getString("condition_subject"))); + condition.setValue(rs.getString("condition_value")); + final String violationType = rs.getString("condition_violation_type"); + if (violationType != null) { + condition.setViolationType(PolicyViolation.Type.valueOf(violationType)); + } + condition.setPolicy(policy); + + policy.getPolicyConditions().add(condition); + + return accumulator; + }) + .values() + .stream() + .toList(); + } + + public Set reconcileViolations( + long projectId, + MultiValuedMap reportedViolationsByComponentId) { + if (reportedViolationsByComponentId.isEmpty()) { + jdbiHandle + .createUpdate(""" + DELETE FROM "POLICYVIOLATION" + WHERE "ID" IN ( + SELECT "ID" + FROM "POLICYVIOLATION" + WHERE "PROJECT_ID" = :projectId + ORDER BY "ID" + FOR UPDATE + ) + """) + .bind("projectId", projectId) + .execute(); + return Set.of(); + } + + final int size = reportedViolationsByComponentId.size(); + final var timestamps = new Timestamp[size]; + final var componentIds = new Long[size]; + final var projIds = new Long[size]; + final var condIds = new Long[size]; + final var types = new String[size]; + + int i = 0; + for (final var entry : reportedViolationsByComponentId.entries()) { + timestamps[i] = new Timestamp(entry.getValue().getTimestamp().getTime()); + componentIds[i] = entry.getKey(); + projIds[i] = projectId; + condIds[i] = entry.getValue().getPolicyCondition().getId(); + types[i] = entry.getValue().getType().name(); + i++; + } + + return jdbiHandle + .createQuery(""" + WITH created AS ( + INSERT INTO "POLICYVIOLATION" ( + "UUID" + , "TIMESTAMP" + , "COMPONENT_ID" + , "PROJECT_ID" + , "POLICYCONDITION_ID" + , "TYPE" + ) + SELECT GEN_RANDOM_UUID() + , t.* + FROM UNNEST(:timestamps, :componentIds, :projectIds, :policyConditionIds, :types) + AS t("TIMESTAMP", "COMPONENT_ID", "PROJECT_ID", "POLICYCONDITION_ID", "TYPE") + ORDER BY t."PROJECT_ID" + , t."COMPONENT_ID" + , t."POLICYCONDITION_ID" + ON CONFLICT DO NOTHING + RETURNING "ID" + ), + deleted AS ( + DELETE FROM "POLICYVIOLATION" + WHERE "ID" IN ( + SELECT "ID" + FROM "POLICYVIOLATION" + WHERE "PROJECT_ID" = :projectId + AND ("COMPONENT_ID", "POLICYCONDITION_ID") NOT IN ( + SELECT * FROM UNNEST(:componentIds, :policyConditionIds) + ) + ORDER BY "ID" + FOR UPDATE + ) + ) + SELECT "ID" FROM created + """) + .bind("timestamps", timestamps) + .bind("componentIds", componentIds) + .bind("projectIds", projIds) + .bind("policyConditionIds", condIds) + .bind("types", types) + .bind("projectId", projectId) + .mapTo(Long.class) + .set(); + } + + public Project loadRequiredFields(long projectId, MultiValuedMap requirements) { final Collection projectRequirements = requirements.get(TYPE_PROJECT); - if (projectRequirements == null || projectRequirements.isEmpty()) { + if (projectRequirements.isEmpty()) { return Project.getDefaultInstance(); } - final List sqlSelectColumns = getFieldMappings(ProjectProjection.class).stream() - .filter(fieldMapping -> projectRequirements.contains(fieldMapping.protoFieldName())) - .map(fieldMapping -> "p.\"%s\" AS \"%s\"".formatted(fieldMapping.sqlColumnName(), fieldMapping.protoFieldName())) - .collect(Collectors.toList()); + final var allProjectFieldNames = new ArrayList<>(projectRequirements); + final boolean needsMetadataTools = projectRequirements.contains("metadata") + && requirements.containsKey(TYPE_PROJECT_METADATA) + && requirements.get(TYPE_PROJECT_METADATA).contains("tools"); + final boolean needsBomGenerated = projectRequirements.contains("metadata") + && requirements.containsKey(TYPE_PROJECT_METADATA) + && requirements.get(TYPE_PROJECT_METADATA).contains("bom_generated"); - if (projectRequirements.contains("metadata") - && requirements.containsKey(TYPE_PROJECT_METADATA)) { - if (requirements.get(TYPE_PROJECT_METADATA).contains("tools")) { - sqlSelectColumns.add("pm.\"TOOLS\" AS \"metadata_tools\""); - } - if (requirements.get(TYPE_PROJECT_METADATA).contains("bom_generated")) { - sqlSelectColumns.add("b.\"GENERATED\" AS \"bom_generated\""); - } + if (needsMetadataTools) { + allProjectFieldNames.add("metadata_tools"); + } + if (needsBomGenerated) { + allProjectFieldNames.add("bom_generated"); } - if (projectRequirements.contains("is_active")) { - sqlSelectColumns.add("p.\"INACTIVE_SINCE\" AS \"inactive_since\""); + allProjectFieldNames.add("inactive_since"); } - final var sqlPropertySelectColumns = new ArrayList(); - if (projectRequirements.contains("properties") && requirements.containsKey(TYPE_PROJECT_PROPERTY)) { - sqlSelectColumns.add("\"properties\""); + final List fetchColumns = new ArrayList<>(selectColumns(PROJECT_FIELDS, allProjectFieldNames)); - getFieldMappings(ProjectPropertyProjection.class).stream() - .filter(mapping -> requirements.get(TYPE_PROJECT_PROPERTY).contains(mapping.protoFieldName())) - .map(mapping -> "'%s', pp.\"%s\"".formatted(mapping.protoFieldName(), mapping.sqlColumnName())) - .forEach(sqlPropertySelectColumns::add); + final var propertyColumns = new ArrayList(); + final boolean needsProperties = projectRequirements.contains("properties"); + if (needsProperties) { + fetchColumns.add("properties"); + if (requirements.containsKey(TYPE_PROJECT_PROPERTY)) { + PROJECT_PROPERTY_FIELDS.stream() + .filter(f -> requirements.get(TYPE_PROJECT_PROPERTY).contains(f.protoFieldName())) + .map(f -> "'%s', %s".formatted(f.protoFieldName(), f.sqlExpression())) + .forEach(propertyColumns::add); + } + + // Always include ID to ensure DISTINCT produces correct cardinality, + // even when no specific property fields are accessed. + if (propertyColumns.isEmpty()) { + propertyColumns.add("'_id', pp.\"ID\""); + } } - if (projectRequirements.contains("tags")) { - sqlSelectColumns.add("\"tags\""); + + final boolean needsTags = projectRequirements.contains("tags"); + if (needsTags) { + fetchColumns.add("tags"); } - final Project fetchedProject = getProject(sqlSelectColumns, sqlPropertySelectColumns, projectId); - if (fetchedProject == null) { + final Project project = jdbiHandle.createQuery(/* language=InjectedFreeMarker */ """ + <#-- @ftlvariable name="fetchColumns" type="java.util.Collection" --> + <#-- @ftlvariable name="needsMetadataTools" type="boolean" --> + <#-- @ftlvariable name="needsBomGenerated" type="boolean" --> + <#-- @ftlvariable name="needsProperties" type="boolean" --> + <#-- @ftlvariable name="propertyColumns" type="java.util.Collection" --> + <#-- @ftlvariable name="needsTags" type="boolean" --> + SELECT ${fetchColumns?join(", ")} + FROM "PROJECT" AS p + <#if needsMetadataTools!false> + INNER JOIN "PROJECT_METADATA" AS pm + ON pm."PROJECT_ID" = p."ID" + + <#if needsBomGenerated!false> + INNER JOIN "BOM" AS b + ON b."PROJECT_ID" = p."ID" + + <#if needsProperties!false> + LEFT JOIN LATERAL ( + SELECT CAST(JSONB_AGG(DISTINCT JSONB_BUILD_OBJECT(${propertyColumns?join(", ")})) AS TEXT) AS properties + FROM "PROJECT_PROPERTY" AS pp + WHERE pp."PROJECT_ID" = p."ID" + ) AS properties_sub ON TRUE + + <#if needsTags!false> + LEFT JOIN LATERAL ( + SELECT ARRAY_AGG(DISTINCT t."NAME") AS tags + FROM "TAG" AS t + INNER JOIN "PROJECTS_TAGS" AS pt + ON pt."TAG_ID" = t."ID" + WHERE pt."PROJECT_ID" = p."ID" + ) AS tags_sub ON TRUE + + WHERE p."ID" = :id + """) + .define("fetchColumns", fetchColumns) + .define("needsMetadataTools", needsMetadataTools) + .define("needsBomGenerated", needsBomGenerated) + .define("needsProperties", needsProperties) + .define("propertyColumns", propertyColumns) + .define("needsTags", needsTags) + .bind("id", projectId) + .map(new CelPolicyProjectRowMapper()) + .findOne() + .orElse(null); + + if (project == null) { throw new NoSuchElementException(); } - return fetchedProject; + return project; } - default Map loadRequiredComponentFields( + public Map loadRequiredComponentFields( Collection componentIds, MultiValuedMap requirements) { if (componentIds.isEmpty()) { @@ -185,7 +584,7 @@ default Map loadRequiredComponentFields( } final Collection componentRequirements = requirements.get(TYPE_COMPONENT); - if (componentRequirements == null || componentRequirements.isEmpty()) { + if (componentRequirements.isEmpty()) { final var result = new HashMap(); for (long componentId : componentIds) { result.put(componentId, Component.getDefaultInstance()); @@ -193,22 +592,44 @@ default Map loadRequiredComponentFields( return result; } - final List sqlSelectColumns = getFieldMappings(ComponentProjection.class).stream() - .filter(fieldMapping -> componentRequirements.contains(fieldMapping.protoFieldName())) - .map(fieldMapping -> "c.\"%s\" AS \"%s\"".formatted(fieldMapping.sqlColumnName(), fieldMapping.protoFieldName())) - .collect(Collectors.toList()); + final List fetchColumns = new ArrayList<>(selectColumns(COMPONENT_FIELDS, componentRequirements)); - if (componentRequirements.contains("latest_version")) { - sqlSelectColumns.add("\"latest_version\""); - } - if (componentRequirements.contains("published_at")) { - sqlSelectColumns.add("\"published_at\""); - } + final boolean needsLatestVersion = componentRequirements.contains("latest_version"); + final boolean needsPublishedAt = componentRequirements.contains("published_at"); + final boolean needsPam = needsPublishedAt || needsLatestVersion; - return getComponents(sqlSelectColumns, componentIds); + final var componentRowMapper = new CelPolicyComponentRowMapper(); + return jdbiHandle.createQuery(/* language=InjectedFreeMarker */ """ + <#-- @ftlvariable name="fetchColumns" type="java.util.Collection" --> + <#-- @ftlvariable name="needsPam" type="boolean" --> + <#-- @ftlvariable name="needsLatestVersion" type="boolean" --> + SELECT c."ID" AS db_id + <#if fetchColumns?size gt 0> + , ${fetchColumns?join(", ")} + + FROM "COMPONENT" AS c + <#if needsPam!false> + LEFT JOIN "PACKAGE_ARTIFACT_METADATA" AS pam + ON pam."PURL" = c."PURL" + + <#if needsLatestVersion!false> + LEFT JOIN "PACKAGE_METADATA" AS pm + ON pm."PURL" = pam."PACKAGE_PURL" + + WHERE c."ID" = ANY(:ids) + """) + .define("fetchColumns", fetchColumns) + .define("needsPam", needsPam) + .define("needsLatestVersion", needsLatestVersion) + .bindArray("ids", Long.class, componentIds) + .reduceResultSet(new HashMap<>(), (accumulator, rs, ctx) -> { + final long dbId = rs.getLong("db_id"); + accumulator.put(dbId, componentRowMapper.map(rs, ctx)); + return accumulator; + }); } - default Map loadRequiredVulnerabilityFields( + public Map loadRequiredVulnerabilityFields( Collection vulnIds, MultiValuedMap requirements) { if (vulnIds.isEmpty()) { @@ -216,7 +637,7 @@ default Map loadRequiredVulnerabilityFields( } final Collection vulnRequirements = requirements.get(TYPE_VULNERABILITY); - if (vulnRequirements == null || vulnRequirements.isEmpty()) { + if (vulnRequirements.isEmpty()) { final var result = new HashMap(); for (long vulnId : vulnIds) { result.put(vulnId, Vulnerability.getDefaultInstance()); @@ -224,28 +645,37 @@ default Map loadRequiredVulnerabilityFields( return result; } - final List sqlSelectColumns = getFieldMappings(VulnerabilityProjection.class).stream() - .filter(fieldMapping -> vulnRequirements.contains(fieldMapping.protoFieldName())) - .map(fieldMapping -> { - if ("cwes".equals(fieldMapping.protoFieldName())) { - return "STRING_TO_ARRAY(v.\"%s\", ',') AS \"%s\"" - .formatted(fieldMapping.sqlColumnName(), fieldMapping.protoFieldName()); - } - return "v.\"%s\" AS \"%s\"".formatted(fieldMapping.sqlColumnName(), fieldMapping.protoFieldName()); - }) - .collect(Collectors.toList()); + final List fetchColumns = new ArrayList<>(selectColumns(VULNERABILITY_FIELDS, vulnRequirements)); - if (vulnRequirements.contains("aliases")) { - sqlSelectColumns.add("JSONB_VULN_ALIASES(v.\"SOURCE\", v.\"VULNID\") AS \"aliases\""); - } - if (vulnRequirements.contains("epss_score")) { - sqlSelectColumns.add("e.\"SCORE\" AS \"epss_score\""); - } - if (vulnRequirements.contains("epss_percentile")) { - sqlSelectColumns.add("e.\"PERCENTILE\" AS \"epss_percentile\""); - } + final boolean needsEpss = vulnRequirements.contains("epss_score") + || vulnRequirements.contains("epss_percentile"); + + final var vulnRowMapper = new CelPolicyVulnerabilityRowMapper(); - return getVulnerabilities(sqlSelectColumns, vulnIds); + return jdbiHandle.createQuery(/* language=InjectedFreeMarker */ """ + <#-- @ftlvariable name="fetchColumns" type="java.util.Collection" --> + <#-- @ftlvariable name="needsEpss" type="boolean" --> + SELECT v."ID" AS db_id + <#if fetchColumns?size gt 0> + , ${fetchColumns?join(", ")} + + FROM "VULNERABILITY" AS v + <#if needsEpss!false> + LEFT JOIN "EPSS" AS ep + ON v."VULNID" = ep."CVE" + + WHERE v."ID" = ANY(:ids) + """) + .define("fetchColumns", fetchColumns) + .define("needsEpss", needsEpss) + .bindArray("ids", Long.class, vulnIds) + .reduceResultSet( + new HashMap<>(), + (accumulator, rs, ctx) -> { + final long dbId = rs.getLong("db_id"); + accumulator.put(dbId, vulnRowMapper.map(rs, ctx)); + return accumulator; + }); } } diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyFieldMappingRegistry.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyFieldMappingRegistry.java new file mode 100644 index 0000000000..336ee2e3da --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyFieldMappingRegistry.java @@ -0,0 +1,128 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.policy.cel.persistence; + +import java.util.Collection; +import java.util.List; + +public final class CelPolicyFieldMappingRegistry { + + private CelPolicyFieldMappingRegistry() { + } + + public record FieldMapping(String protoFieldName, String sqlExpression) { + } + + static final List COMPONENT_FIELDS = List.of( + new FieldMapping("uuid", "c.\"UUID\""), + new FieldMapping("group", "c.\"GROUP\""), + new FieldMapping("name", "c.\"NAME\""), + new FieldMapping("version", "c.\"VERSION\""), + new FieldMapping("classifier", "c.\"CLASSIFIER\""), + new FieldMapping("cpe", "c.\"CPE\""), + new FieldMapping("purl", "c.\"PURL\""), + new FieldMapping("swid_tag_id", "c.\"SWIDTAGID\""), + new FieldMapping("is_internal", "c.\"INTERNAL\""), + new FieldMapping("md5", "c.\"MD5\""), + new FieldMapping("sha1", "c.\"SHA1\""), + new FieldMapping("sha256", "c.\"SHA_256\""), + new FieldMapping("sha384", "c.\"SHA_384\""), + new FieldMapping("sha512", "c.\"SHA_512\""), + new FieldMapping("sha3_256", "c.\"SHA3_256\""), + new FieldMapping("sha3_384", "c.\"SHA3_384\""), + new FieldMapping("sha3_512", "c.\"SHA3_512\""), + new FieldMapping("blake2b_256", "c.\"BLAKE2B_256\""), + new FieldMapping("blake2b_384", "c.\"BLAKE2B_384\""), + new FieldMapping("blake2b_512", "c.\"BLAKE2B_512\""), + new FieldMapping("blake3", "c.\"BLAKE3\""), + new FieldMapping("license_name", "c.\"LICENSE\""), + new FieldMapping("license_expression", "c.\"LICENSE_EXPRESSION\""), + new FieldMapping("published_at", "pam.\"PUBLISHED_AT\""), + new FieldMapping("latest_version", "pm.\"LATEST_VERSION\"")); + + static final List VULNERABILITY_FIELDS = List.of( + new FieldMapping("uuid", "v.\"UUID\""), + new FieldMapping("id", "v.\"VULNID\""), + new FieldMapping("source", "v.\"SOURCE\""), + new FieldMapping("created", "v.\"CREATED\""), + new FieldMapping("published", "v.\"PUBLISHED\""), + new FieldMapping("updated", "v.\"UPDATED\""), + new FieldMapping("severity", "v.\"SEVERITY\""), + new FieldMapping("cvssv2_base_score", "v.\"CVSSV2BASESCORE\""), + new FieldMapping("cvssv2_impact_subscore", "v.\"CVSSV2IMPACTSCORE\""), + new FieldMapping("cvssv2_exploitability_subscore", "v.\"CVSSV2EXPLOITSCORE\""), + new FieldMapping("cvssv2_vector", "v.\"CVSSV2VECTOR\""), + new FieldMapping("cvssv3_base_score", "v.\"CVSSV3BASESCORE\""), + new FieldMapping("cvssv3_impact_subscore", "v.\"CVSSV3IMPACTSCORE\""), + new FieldMapping("cvssv3_exploitability_subscore", "v.\"CVSSV3EXPLOITSCORE\""), + new FieldMapping("cvssv3_vector", "v.\"CVSSV3VECTOR\""), + new FieldMapping("cvssv4_score", "v.\"CVSSV4SCORE\""), + new FieldMapping("cvssv4_vector", "v.\"CVSSV4VECTOR\""), + new FieldMapping("owasp_rr_likelihood_score", "v.\"OWASPRRLIKELIHOODSCORE\""), + new FieldMapping("owasp_rr_technical_impact_score", "v.\"OWASPRRTECHNICALIMPACTSCORE\""), + new FieldMapping("owasp_rr_business_impact_score", "v.\"OWASPRRBUSINESSIMPACTSCORE\""), + new FieldMapping("owasp_rr_vector", "v.\"OWASPRRVECTOR\""), + new FieldMapping("cwes", "STRING_TO_ARRAY(v.\"CWES\", ',')"), + new FieldMapping("aliases", "CAST(JSONB_VULN_ALIASES(v.\"SOURCE\", v.\"VULNID\") AS TEXT)"), + new FieldMapping("epss_score", "ep.\"SCORE\""), + new FieldMapping("epss_percentile", "ep.\"PERCENTILE\"")); + + static final List LICENSE_FIELDS = List.of( + new FieldMapping("uuid", "l.\"UUID\""), + new FieldMapping("id", "l.\"LICENSEID\""), + new FieldMapping("name", "l.\"NAME\""), + new FieldMapping("is_osi_approved", "l.\"ISOSIAPPROVED\""), + new FieldMapping("is_fsf_libre", "l.\"FSFLIBRE\""), + new FieldMapping("is_deprecated_id", "l.\"ISDEPRECATED\""), + new FieldMapping("is_custom", "l.\"ISCUSTOMLICENSE\"")); + + static final List LICENSE_GROUP_FIELDS = List.of( + new FieldMapping("uuid", "lg.\"UUID\""), + new FieldMapping("name", "lg.\"NAME\"")); + + static final List PROJECT_FIELDS = List.of( + new FieldMapping("uuid", "p.\"UUID\""), + new FieldMapping("group", "p.\"GROUP\""), + new FieldMapping("name", "p.\"NAME\""), + new FieldMapping("version", "p.\"VERSION\""), + new FieldMapping("classifier", "p.\"CLASSIFIER\""), + new FieldMapping("cpe", "p.\"CPE\""), + new FieldMapping("purl", "p.\"PURL\""), + new FieldMapping("swid_tag_id", "p.\"SWIDTAGID\""), + new FieldMapping("last_bom_import", "p.\"LAST_BOM_IMPORTED\""), + new FieldMapping("metadata_tools", "pm.\"TOOLS\""), + new FieldMapping("bom_generated", "b.\"GENERATED\""), + new FieldMapping("inactive_since", "p.\"INACTIVE_SINCE\"")); + + static final List PROJECT_PROPERTY_FIELDS = List.of( + new FieldMapping("group", "pp.\"GROUPNAME\""), + new FieldMapping("name", "pp.\"PROPERTYNAME\""), + new FieldMapping("value", "pp.\"PROPERTYVALUE\""), + new FieldMapping("type", "pp.\"PROPERTYTYPE\"")); + + static List selectColumns( + List fields, + Collection requiredProtoFields) { + return fields.stream() + .filter(fieldMapping -> requiredProtoFields.contains(fieldMapping.protoFieldName())) + .map(fieldMapping -> fieldMapping.sqlExpression() + " AS \"" + fieldMapping.protoFieldName() + "\"") + .toList(); + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyLicenseRowMapper.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyLicenseRowMapper.java new file mode 100644 index 0000000000..815fbac54f --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyLicenseRowMapper.java @@ -0,0 +1,72 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.policy.cel.persistence; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.dependencytrack.common.Mappers; +import org.dependencytrack.proto.policy.v1.License; +import org.jdbi.v3.core.mapper.RowMapper; +import org.jdbi.v3.core.statement.StatementContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; + +import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.maybeSet; + +public final class CelPolicyLicenseRowMapper implements RowMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(CelPolicyLicenseRowMapper.class); + + @Override + public License map(ResultSet rs, StatementContext ctx) throws SQLException { + final License.Builder builder = License.newBuilder(); + maybeSet(rs, "uuid", ResultSet::getString, builder::setUuid); + maybeSet(rs, "id", ResultSet::getString, builder::setId); + maybeSet(rs, "name", ResultSet::getString, builder::setName); + maybeSet(rs, "is_osi_approved", ResultSet::getBoolean, builder::setIsOsiApproved); + maybeSet(rs, "is_fsf_libre", ResultSet::getBoolean, builder::setIsFsfLibre); + maybeSet(rs, "is_deprecated_id", ResultSet::getBoolean, builder::setIsDeprecatedId); + maybeSet(rs, "is_custom", ResultSet::getBoolean, builder::setIsCustom); + maybeSet(rs, "groups_json", ResultSet::getString, jsonString -> parseLicenseGroups(builder, jsonString)); + return builder.build(); + } + + private static void parseLicenseGroups(License.Builder builder, String jsonString) { + if (jsonString == null) { + return; + } + + try { + final ArrayNode groupsArray = Mappers.jsonMapper().readValue(jsonString, ArrayNode.class); + for (final JsonNode groupNode : groupsArray) { + builder.addGroups(License.Group.newBuilder() + .setUuid(Optional.ofNullable(groupNode.get("uuid")).map(JsonNode::asText).orElse("")) + .setName(Optional.ofNullable(groupNode.get("name")).map(JsonNode::asText).orElse("")) + .build()); + } + } catch (JacksonException e) { + LOGGER.warn("Failed to parse license groups from {}", jsonString, e); + } + } +} diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyProjectRowMapper.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyProjectRowMapper.java index ce4810ab6b..f03a07d795 100644 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyProjectRowMapper.java +++ b/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyProjectRowMapper.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.JsonNode; import com.google.protobuf.util.JsonFormat; import org.dependencytrack.common.Mappers; import org.dependencytrack.model.mapping.PolicyProtoMapper; @@ -38,10 +39,12 @@ import java.util.List; import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.hasColumn; import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.maybeSet; -public class CelPolicyProjectRowMapper implements RowMapper { +public final class CelPolicyProjectRowMapper implements RowMapper { + + private static final JsonFormat.Parser PROPERTY_JSON_PARSER = + JsonFormat.parser().ignoringUnknownFields(); @Override public Project map(final ResultSet rs, final StatementContext ctx) throws SQLException { @@ -59,28 +62,20 @@ public Project map(final ResultSet rs, final StatementContext ctx) throws SQLExc maybeSet(rs, "properties", CelPolicyProjectRowMapper::maybeConvertProperties, builder::addAllProperties); final Project.Metadata.Builder metadataBuilder = Project.Metadata.newBuilder(); - if (hasColumn(rs, "metadata_tools")) { - metadataBuilder.setTools(convertMetadataTools(rs)); - } - if (hasColumn(rs, "inactive_since")) { - builder.setIsActive(convertInactiveSince(rs)); - } + maybeSet(rs, "metadata_tools", CelPolicyProjectRowMapper::convertMetadataTools, metadataBuilder::setTools); + maybeSet(rs, "inactive_since", CelPolicyProjectRowMapper::convertInactiveSince, builder::setIsActive); maybeSet(rs, "bom_generated", RowMapperUtil::nullableTimestamp, metadataBuilder::setBomGenerated); builder.setMetadata(metadataBuilder.build()); return builder.build(); } - private static boolean convertInactiveSince(final ResultSet rs) throws SQLException { - final var jsonInactiveSince = rs.getTimestamp("inactive_since"); - if (jsonInactiveSince == null) { - return true; - } - return false; + private static Boolean convertInactiveSince(ResultSet rs, String columnName) throws SQLException { + return rs.getTimestamp(columnName) == null; } - private static Tools convertMetadataTools(final ResultSet rs) throws SQLException { - final String jsonString = rs.getString("metadata_tools"); + private static Tools convertMetadataTools(ResultSet rs, String columnName) throws SQLException { + final String jsonString = rs.getString(columnName); if (isBlank(jsonString)) { return Tools.getDefaultInstance(); } @@ -117,16 +112,15 @@ private static List maybeConvertProperties(final ResultSet rs, // Instead, use Jackson's streaming API to iterate over the array, and deserialize individual objects. final var properties = new ArrayList(); try (final JsonParser jsonParser = Mappers.jsonMapper().createParser(jsonString)) { - JsonToken currentToken = jsonParser.nextToken(); // Position cursor at first token. - if (currentToken != JsonToken.START_ARRAY) { + if (jsonParser.nextToken() != JsonToken.START_ARRAY) { return Collections.emptyList(); } while (jsonParser.nextToken() != JsonToken.END_ARRAY) { - currentToken = jsonParser.nextToken(); - if (currentToken == JsonToken.START_OBJECT) { + if (jsonParser.currentToken() == JsonToken.START_OBJECT) { + final JsonNode objectNode = jsonParser.readValueAsTree(); final var builder = Project.Property.newBuilder(); - JsonFormat.parser().merge(jsonParser.getValueAsString(), builder); + PROPERTY_JSON_PARSER.merge(objectNode.toString(), builder); properties.add(builder.build()); } else { jsonParser.skipChildren(); diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyVulnerabilityRowMapper.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyVulnerabilityRowMapper.java index 517164cefa..0cd74d74ae 100644 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyVulnerabilityRowMapper.java +++ b/apiserver/src/main/java/org/dependencytrack/policy/cel/persistence/CelPolicyVulnerabilityRowMapper.java @@ -37,13 +37,13 @@ import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.maybeSet; import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.stringArray; -public class CelPolicyVulnerabilityRowMapper implements RowMapper { +public final class CelPolicyVulnerabilityRowMapper implements RowMapper { private static final TypeReference> VULNERABILITY_ALIASES_TYPE_REF = new TypeReference<>() { }; @Override - public Vulnerability map(final ResultSet rs, final StatementContext ctx) throws SQLException { + public Vulnerability map(ResultSet rs, StatementContext ctx) throws SQLException { final Vulnerability.Builder builder = Vulnerability.newBuilder(); maybeSet(rs, "uuid", ResultSet::getString, builder::setUuid); maybeSet(rs, "id", ResultSet::getString, builder::setId); @@ -70,29 +70,43 @@ public Vulnerability map(final ResultSet rs, final StatementContext ctx) throws maybeSet(rs, "epss_percentile", RowMapperUtil::nullableDouble, builder::setEpssPercentile); maybeSet(rs, "cwes", CelPolicyVulnerabilityRowMapper::maybeConvertCwes, builder::addAllCwes); maybeSet(rs, "aliases", CelPolicyVulnerabilityRowMapper::maybeConvertAliases, builder::addAllAliases); - - // Workaround for https://github.com/DependencyTrack/dependency-track/issues/2474. - if (builder.getSeverity().isBlank() && hasAnyRiskScore(builder)) { - final Severity severity = VulnerabilityUtil.getSeverity(null, - builder.hasCvssv2BaseScore() ? BigDecimal.valueOf(builder.getCvssv2BaseScore()) : null, - builder.hasCvssv3BaseScore() ? BigDecimal.valueOf(builder.getCvssv3BaseScore()) : null, - builder.hasOwaspRrLikelihoodScore() ? BigDecimal.valueOf(builder.getOwaspRrLikelihoodScore()) : null, - builder.hasOwaspRrTechnicalImpactScore() ? BigDecimal.valueOf(builder.getOwaspRrTechnicalImpactScore()) : null, - builder.hasOwaspRrBusinessImpactScore() ? BigDecimal.valueOf(builder.getOwaspRrBusinessImpactScore()) : null - ); - builder.setSeverity(severity.name()); - } + builder.setSeverity(computeSeverity(builder).name()); return builder.build(); } - private static List maybeConvertCwes(final ResultSet rs, final String columnName) throws SQLException { + private static List maybeConvertCwes(ResultSet rs, String columnName) throws SQLException { return stringArray(rs, columnName).stream() .map(Integer::parseInt) .toList(); } - private static List maybeConvertAliases(final ResultSet rs, final String columnName) throws SQLException { + private static Severity computeSeverity(Vulnerability.Builder builder) { + return VulnerabilityUtil.getSeverity( + !builder.getSeverity().isBlank() + ? Severity.valueOf(builder.getSeverity()) + : null, + builder.hasCvssv2BaseScore() + ? BigDecimal.valueOf(builder.getCvssv2BaseScore()) + : null, + builder.hasCvssv3BaseScore() + ? BigDecimal.valueOf(builder.getCvssv3BaseScore()) + : null, + builder.hasCvssv4Score() + ? BigDecimal.valueOf(builder.getCvssv4Score()) + : null, + builder.hasOwaspRrLikelihoodScore() + ? BigDecimal.valueOf(builder.getOwaspRrLikelihoodScore()) + : null, + builder.hasOwaspRrTechnicalImpactScore() + ? BigDecimal.valueOf(builder.getOwaspRrTechnicalImpactScore()) + : null, + builder.hasOwaspRrBusinessImpactScore() + ? BigDecimal.valueOf(builder.getOwaspRrBusinessImpactScore()) + : null); + } + + private static List maybeConvertAliases(ResultSet rs, String columnName) throws SQLException { final List aliases = deserializeJson(rs, columnName, VULNERABILITY_ALIASES_TYPE_REF); if (aliases == null) { return Collections.emptyList(); @@ -108,10 +122,4 @@ private static List maybeConvertAliases(final ResultSet rs, .toList(); } - private boolean hasAnyRiskScore(final Vulnerability.Builder builder) { - return builder.hasCvssv2BaseScore() - || builder.hasCvssv3BaseScore() - || (builder.hasOwaspRrBusinessImpactScore() && builder.hasOwaspRrTechnicalImpactScore() && builder.hasOwaspRrLikelihoodScore()); - } - } diff --git a/apiserver/src/main/java/org/dependencytrack/util/NotificationUtil.java b/apiserver/src/main/java/org/dependencytrack/util/NotificationUtil.java deleted file mode 100644 index 043d5e728f..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/util/NotificationUtil.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.util; - -import org.apache.commons.lang3.StringUtils; -import org.dependencytrack.model.Component; -import org.dependencytrack.model.Policy; -import org.dependencytrack.model.PolicyCondition; -import org.dependencytrack.model.PolicyViolation; -import org.dependencytrack.model.Project; -import org.dependencytrack.model.Tag; -import org.dependencytrack.model.ViolationAnalysisState; -import org.dependencytrack.notification.JdoNotificationEmitter; -import org.dependencytrack.persistence.QueryManager; - -import javax.jdo.Query; -import java.util.Arrays; -import java.util.Date; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; -import java.util.stream.Collectors; - -import static org.dependencytrack.notification.NotificationModelConverter.convert; -import static org.dependencytrack.notification.api.NotificationFactory.createPolicyViolationNotification; - -public final class NotificationUtil { - - /** - * Private constructor. - */ - private NotificationUtil() { - } - - public static void analyzeNotificationCriteria(final QueryManager qm, final Long violationId) { - final Query query = qm.getPersistenceManager().newQuery(Query.SQL, """ - SELECT - "PV"."UUID" AS "violationUuid", - "PV"."TYPE" AS "violationType", - "PV"."TIMESTAMP" AS "violationTimestamp", - "PC"."UUID" AS "conditionUuid", - "PC"."SUBJECT" AS "conditionSubject", - "PC"."OPERATOR" AS "conditionOperator", - "PC"."VALUE" AS "conditionValue", - "P"."UUID" AS "policyUuid", - "P"."NAME" AS "policyName", - "P"."VIOLATIONSTATE" AS "policyViolationState", - "VA"."SUPPRESSED" AS "analysisSuppressed", - "VA"."STATE" AS "analysisState", - "C"."UUID" AS "componentUuid", - "C"."GROUP" AS "componentGroup", - "C"."NAME" AS "componentName", - "C"."VERSION" AS "componentVersion", - "C"."PURL" AS "componentPurl", - "C"."MD5" AS "componentMd5", - "C"."SHA1" AS "componentSha1", - "C"."SHA_256" AS "componentSha256", - "C"."SHA_512" AS "componentSha512", - "PR"."UUID" AS "projectUuid", - "PR"."NAME" AS "projectName", - "PR"."VERSION" AS "projectVersion", - "PR"."DESCRIPTION" AS "projectDescription", - "PR"."PURL" AS "projectPurl", - (SELECT - STRING_AGG("T"."NAME", ',') - FROM - "TAG" AS "T" - INNER JOIN - "PROJECTS_TAGS" AS "PT" ON "PT"."TAG_ID" = "T"."ID" - WHERE - "PT"."PROJECT_ID" = "PR"."ID" - ) AS "projectTags" - FROM - "POLICYVIOLATION" AS "PV" - INNER JOIN - "POLICYCONDITION" AS "PC" ON "PC"."ID" = "PV"."POLICYCONDITION_ID" - INNER JOIN - "POLICY" AS "P" ON "P"."ID" = "PC"."POLICY_ID" - INNER JOIN - "COMPONENT" AS "C" ON "C"."ID" = "PV"."COMPONENT_ID" - INNER JOIN - "PROJECT" AS "PR" ON "PR"."ID" = "PV"."PROJECT_ID" - LEFT JOIN - "VIOLATIONANALYSIS" AS "VA" ON "VA"."POLICYVIOLATION_ID" = "PV"."ID" - WHERE - "PV"."ID" = ? - """); - query.setParameters(violationId); - final PolicyViolationNotificationProjection projection; - try { - projection = query.executeResultUnique(PolicyViolationNotificationProjection.class); - } finally { - query.closeAll(); - } - - if (projection == null) { - return; - } - - if ((projection.analysisSuppressed != null && projection.analysisSuppressed) - || ViolationAnalysisState.APPROVED.name().equals(projection.analysisState)) { - return; - } - - final var project = new Project(); - project.setUuid(UUID.fromString(projection.projectUuid)); - project.setName(projection.projectName); - project.setVersion(projection.projectVersion); - project.setDescription(projection.projectDescription); - project.setPurl(projection.projectPurl); - project.setTags(Optional.ofNullable(projection.projectTags).stream() - .flatMap(tagNames -> Arrays.stream(tagNames.split(","))) - .map(StringUtils::trimToNull) - .filter(Objects::nonNull) - .map(tagName -> { - final var tag = new Tag(); - tag.setName(tagName); - return tag; - }) - .collect(Collectors.toSet())); - - final var component = new Component(); - component.setUuid(UUID.fromString(projection.componentUuid)); - component.setGroup(projection.componentGroup); - component.setName(projection.componentName); - component.setVersion(projection.componentVersion); - component.setPurl(projection.componentPurl); - component.setMd5(projection.componentMd5); - component.setSha1(projection.componentSha1); - component.setSha256(projection.componentSha256); - component.setSha512(projection.componentSha512); - - final var policy = new Policy(); - policy.setUuid(UUID.fromString(projection.policyUuid)); - policy.setName(projection.policyName); - policy.setViolationState(Policy.ViolationState.valueOf(projection.policyViolationState)); - - final var policyCondition = new PolicyCondition(); - policyCondition.setPolicy(policy); - policyCondition.setUuid(UUID.fromString(projection.conditionUuid)); - policyCondition.setSubject(PolicyCondition.Subject.valueOf(projection.conditionSubject)); - policyCondition.setOperator(PolicyCondition.Operator.valueOf(projection.conditionOperator)); - policyCondition.setValue(projection.conditionValue); - - final var violation = new PolicyViolation(); - violation.setPolicyCondition(policyCondition); - violation.setUuid(UUID.fromString(projection.violationUuid)); - violation.setType(PolicyViolation.Type.valueOf(projection.violationType)); - violation.setTimestamp(projection.violationTimestamp); - - new JdoNotificationEmitter(qm).emit( - createPolicyViolationNotification( - convert(project), - convert(component), - convert(violation))); - } - - public static class PolicyViolationNotificationProjection { - public String projectUuid; - public String projectName; - public String projectVersion; - public String projectDescription; - public String projectPurl; - public String projectTags; - public String componentUuid; - public String componentGroup; - public String componentName; - public String componentVersion; - public String componentPurl; - public String componentMd5; - public String componentSha1; - public String componentSha256; - public String componentSha512; - public String violationUuid; - public String violationType; - public Date violationTimestamp; - public String conditionUuid; - public String conditionSubject; - public String conditionOperator; - public String conditionValue; - public String policyUuid; - public String policyName; - public String policyViolationState; - public Boolean analysisSuppressed; - public String analysisState; - } - -} diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDaoTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDaoTest.java index aa4b5fdc09..5edf4ecc14 100644 --- a/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDaoTest.java +++ b/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDaoTest.java @@ -23,19 +23,27 @@ import org.dependencytrack.model.Analysis; import org.dependencytrack.model.AnalysisState; import org.dependencytrack.model.Component; +import org.dependencytrack.model.Policy; +import org.dependencytrack.model.PolicyCondition.Operator; +import org.dependencytrack.model.PolicyCondition.Subject; +import org.dependencytrack.model.PolicyViolation; import org.dependencytrack.model.Project; import org.dependencytrack.model.Severity; +import org.dependencytrack.model.ViolationAnalysisState; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerabilityKey; import org.dependencytrack.notification.proto.v1.ComponentVulnAnalysisCompleteSubject; import org.dependencytrack.notification.proto.v1.NewVulnerabilitySubject; import org.dependencytrack.notification.proto.v1.NewVulnerableDependencySubject; +import org.dependencytrack.notification.proto.v1.PolicyViolationSubject; import org.dependencytrack.notification.proto.v1.VulnerabilityAnalysisDecisionChangeSubject; import org.dependencytrack.persistence.command.MakeAnalysisCommand; +import org.dependencytrack.persistence.command.MakeViolationAnalysisCommand; import org.dependencytrack.persistence.jdbi.query.GetProjectAuditChangeNotificationSubjectQuery; import org.junit.jupiter.api.Test; import java.math.BigDecimal; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; @@ -883,4 +891,189 @@ public void testGetForProjectVulnAnalysisCompleteWithAnalysisOverrides() { assertThat(vulnerability.getCvssV3()).isEqualTo(9.8); assertThat(vulnerability.getCvssV3Vector()).isEqualTo("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"); } + + @Test + public void shouldGetForNewPolicyViolations() { + final var project = new Project(); + project.setName("projectName"); + project.setVersion("projectVersion"); + project.setDescription("projectDescription"); + project.setPurl("projectPurl"); + qm.persist(project); + qm.bind(project, List.of( + qm.createTag("projectTagA"), + qm.createTag("projectTagB") + )); + + final var component = new Component(); + component.setProject(project); + component.setGroup("componentGroup"); + component.setName("componentName"); + component.setVersion("componentVersion"); + component.setPurl("componentPurl"); + component.setMd5("componentMd5"); + component.setSha1("componentSha1"); + component.setSha256("componentSha256"); + component.setSha512("componentSha512"); + qm.persist(component); + + final var policy = qm.createPolicy("testPolicy", Policy.Operator.ALL, Policy.ViolationState.FAIL); + final var condition = qm.createPolicyCondition(policy, Subject.VERSION, Operator.NUMERIC_EQUAL, "1.0"); + + final var violation = new PolicyViolation(); + violation.setType(PolicyViolation.Type.OPERATIONAL); + violation.setComponent(component); + violation.setPolicyCondition(condition); + violation.setTimestamp(new Date()); + qm.persist(violation); + + final List subjects = withJdbiHandle(handle -> handle + .attach(NotificationSubjectDao.class) + .getForNewPolicyViolations(List.of(violation.getId()))); + + assertThat(subjects).satisfiesExactly(subject -> + assertThatJson(JsonFormat.printer().print(subject)) + .withMatcher("projectUuid", equalTo(project.getUuid().toString())) + .withMatcher("componentUuid", equalTo(component.getUuid().toString())) + .withMatcher("violationUuid", equalTo(violation.getUuid().toString())) + .withMatcher("conditionUuid", equalTo(condition.getUuid().toString())) + .withMatcher("policyUuid", equalTo(policy.getUuid().toString())) + .isEqualTo(/* language=JSON */ """ + { + "project": { + "uuid": "${json-unit.matches:projectUuid}", + "name": "projectName", + "version": "projectVersion", + "description": "projectDescription", + "purl": "projectPurl", + "isActive": true, + "tags": [ + "projecttaga", + "projecttagb" + ] + }, + "component": { + "uuid": "${json-unit.matches:componentUuid}", + "group": "componentGroup", + "name": "componentName", + "version": "componentVersion", + "purl": "componentPurl", + "md5": "componentmd5", + "sha1": "componentsha1", + "sha256": "componentsha256", + "sha512": "componentsha512" + }, + "policyViolation": { + "uuid": "${json-unit.matches:violationUuid}", + "type": "OPERATIONAL", + "timestamp": "${json-unit.any-string}", + "condition": { + "uuid": "${json-unit.matches:conditionUuid}", + "subject": "VERSION", + "operator": "NUMERIC_EQUAL", + "value": "1.0", + "policy": { + "uuid": "${json-unit.matches:policyUuid}", + "name": "testPolicy", + "violationState": "FAIL" + } + } + } + } + """)); + } + + @Test + public void shouldFilterSuppressedViolationsForNewPolicyViolations() { + final var project = new Project(); + project.setName("projectName"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("componentName"); + qm.persist(component); + + final var policy = qm.createPolicy("testPolicy", Policy.Operator.ALL, Policy.ViolationState.FAIL); + final var conditionA = qm.createPolicyCondition(policy, Subject.VERSION, Operator.NUMERIC_EQUAL, "1.0"); + final var conditionB = qm.createPolicyCondition(policy, Subject.VERSION, Operator.NUMERIC_EQUAL, "2.0"); + + final var violationA = new PolicyViolation(); + violationA.setType(PolicyViolation.Type.OPERATIONAL); + violationA.setComponent(component); + violationA.setPolicyCondition(conditionA); + violationA.setTimestamp(new Date()); + qm.persist(violationA); + + final var violationB = new PolicyViolation(); + violationB.setType(PolicyViolation.Type.OPERATIONAL); + violationB.setComponent(component); + violationB.setPolicyCondition(conditionB); + violationB.setTimestamp(new Date()); + qm.persist(violationB); + + qm.makeViolationAnalysis( + new MakeViolationAnalysisCommand(component, violationB) + .withState(ViolationAnalysisState.REJECTED) + .withSuppress(true)); + + final List subjects = withJdbiHandle(handle -> handle + .attach(NotificationSubjectDao.class) + .getForNewPolicyViolations(List.of(violationA.getId(), violationB.getId()))); + + assertThat(subjects).singleElement() + .extracting(s -> s.getPolicyViolation().getUuid()) + .isEqualTo(violationA.getUuid().toString()); + } + + @Test + public void shouldFilterApprovedViolationsForNewPolicyViolations() { + final var project = new Project(); + project.setName("projectName"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("componentName"); + qm.persist(component); + + final var policy = qm.createPolicy("testPolicy", Policy.Operator.ALL, Policy.ViolationState.FAIL); + final var conditionA = qm.createPolicyCondition(policy, Subject.VERSION, Operator.NUMERIC_EQUAL, "1.0"); + final var conditionB = qm.createPolicyCondition(policy, Subject.VERSION, Operator.NUMERIC_EQUAL, "2.0"); + + final var violationA = new PolicyViolation(); + violationA.setType(PolicyViolation.Type.OPERATIONAL); + violationA.setComponent(component); + violationA.setPolicyCondition(conditionA); + violationA.setTimestamp(new Date()); + qm.persist(violationA); + + final var violationB = new PolicyViolation(); + violationB.setType(PolicyViolation.Type.OPERATIONAL); + violationB.setComponent(component); + violationB.setPolicyCondition(conditionB); + violationB.setTimestamp(new Date()); + qm.persist(violationB); + + qm.makeViolationAnalysis( + new MakeViolationAnalysisCommand(component, violationB) + .withState(ViolationAnalysisState.APPROVED)); + + final List subjects = withJdbiHandle(handle -> handle + .attach(NotificationSubjectDao.class) + .getForNewPolicyViolations(List.of(violationA.getId(), violationB.getId()))); + + assertThat(subjects).singleElement() + .extracting(s -> s.getPolicyViolation().getUuid()) + .isEqualTo(violationA.getUuid().toString()); + } + + @Test + public void shouldReturnEmptyForNewPolicyViolationsWithEmptyInput() { + final List subjects = withJdbiHandle(handle -> handle + .attach(NotificationSubjectDao.class) + .getForNewPolicyViolations(List.of())); + + assertThat(subjects).isEmpty(); + } } \ No newline at end of file diff --git a/apiserver/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java b/apiserver/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java index 6225d46a3a..4a128d8303 100644 --- a/apiserver/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java +++ b/apiserver/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java @@ -1826,31 +1826,6 @@ void testEvaluateProjectWhenProjectDoesNotExist() { assertThatNoException().isThrownBy(() -> new CelPolicyEngine().evaluateProject(UUID.randomUUID())); } - @Test - void testEvaluateComponent() { - final var policy = qm.createPolicy("policy", Policy.Operator.ANY, Policy.ViolationState.FAIL); - qm.createPolicyCondition(policy, PolicyCondition.Subject.EXPRESSION, PolicyCondition.Operator.MATCHES, """ - component.name == "acme-lib" - """, PolicyViolation.Type.OPERATIONAL); - - final var project = new Project(); - project.setName("acme-app"); - qm.persist(project); - - final var component = new Component(); - component.setProject(project); - component.setName("acme-lib"); - qm.persist(component); - - new CelPolicyEngine().evaluateComponent(component.getUuid()); - assertThat(qm.getAllPolicyViolations(component)).hasSize(1); - } - - @Test - void testEvaluateComponentWhenComponentDoesNotExist() { - assertThatNoException().isThrownBy(() -> new CelPolicyEngine().evaluateComponent(UUID.randomUUID())); - } - @Test void issue1924() { Policy policy = qm.createPolicy("Policy 1924", Policy.Operator.ALL, Policy.ViolationState.INFO); @@ -2183,13 +2158,72 @@ void testEvaluateProjectWithFuncComponentIsDirectDependencyOfComponentWithInMemo assertThat(qm.getAllPolicyViolations(componentB)).isEmpty(); condition.setValue(""" - component.is_direct_dependency_of(v1.Component{ - name: "acme-lib-a", - version: "vers:golang/>=v1.0.0|=v1.0.0| projectionClazz, - final Descriptor protoDescriptor, - final Class persistenceClass) { - assertThat(FieldMappingUtil.getFieldMappings(projectionClazz)).allSatisfy( - fieldMapping -> { - assertHasProtoField(protoDescriptor, fieldMapping.protoFieldName()); - assertHasSqlColumn(persistenceClass, fieldMapping.sqlColumnName()); - - } - ); - } - - private void assertHasProtoField(final Descriptor protoDescriptor, final String fieldName) { - assertThat(protoDescriptor.findFieldByName(fieldName)).isNotNull(); - } - - private void assertHasSqlColumn(final Class clazz, final String columnName) { - final PersistenceManagerFactory pmf = qm.getPersistenceManager().getPersistenceManagerFactory(); - - final TypeMetadata typeMetadata = pmf.getMetadata(clazz.getName()); - assertThat(typeMetadata).isNotNull(); - - var found = false; - for (final MemberMetadata memberMetadata : typeMetadata.getMembers()) { - if (memberMetadata.getColumns() == null) { - continue; - } - - for (final ColumnMetadata columnMetadata : memberMetadata.getColumns()) { - if (columnName.equals(columnMetadata.getName())) { - found = true; - break; - } - } - } - - assertThat(found).isTrue(); - } - -} \ No newline at end of file diff --git a/apiserver/src/test/java/org/dependencytrack/policy/cel/persistence/CelPolicyDaoTest.java b/apiserver/src/test/java/org/dependencytrack/policy/cel/persistence/CelPolicyDaoTest.java index 5330368dff..b7afbc4200 100644 --- a/apiserver/src/test/java/org/dependencytrack/policy/cel/persistence/CelPolicyDaoTest.java +++ b/apiserver/src/test/java/org/dependencytrack/policy/cel/persistence/CelPolicyDaoTest.java @@ -100,8 +100,8 @@ public void testLoadRequiredFieldsForProject() throws Exception { .toList()); requirements.put(TYPE_PROJECT_METADATA, "bom_generated"); - final org.dependencytrack.proto.policy.v1.Project enrichedProject = withJdbiHandle(handle -> handle - .attach(CelPolicyDao.class).loadRequiredFields(project.getId(), requirements)); + final org.dependencytrack.proto.policy.v1.Project enrichedProject = withJdbiHandle(handle -> + new CelPolicyDao(handle).loadRequiredFields(project.getId(), requirements)); assertThatJson(JsonFormat.printer().print(enrichedProject)) .withMatcher("uuid", equalTo(project.getUuid().toString())) @@ -117,6 +117,14 @@ public void testLoadRequiredFieldsForProject() throws Exception { "projecttaga", "projecttagb" ], + "properties": [ + { + "group": "propertyGroup", + "name": "propertyName", + "value": "propertyValue", + "type": "STRING" + } + ], "cpe": "projectCpe", "purl": "projectPurl", "swidTagId": "projectSwidTagId", @@ -208,8 +216,8 @@ public void testLoadRequiredFieldsForComponent() throws Exception { .map(Descriptors.FieldDescriptor::getName) .toList()); - final org.dependencytrack.proto.policy.v1.Component enrichedComponent = withJdbiHandle(handle -> handle - .attach(CelPolicyDao.class).loadRequiredComponentFields(List.of(component.getId()), requirements)) + final org.dependencytrack.proto.policy.v1.Component enrichedComponent = withJdbiHandle(handle -> + new CelPolicyDao(handle).loadRequiredComponentFields(List.of(component.getId()), requirements)) .get(component.getId()); assertThatJson(JsonFormat.printer().print(enrichedComponent)) @@ -233,6 +241,10 @@ public void testLoadRequiredFieldsForComponent() throws Exception { "sha3256": "componentsha3_256", "sha3384": "componentsha3_384", "sha3512": "componentsha3_512", + "blake2b256": "componentBlake2b_256", + "blake2b384": "componentBlake2b_384", + "blake2b512": "componentBlake2b_512", + "blake3": "componentBlake3", "licenseName": "componentLicenseName", "licenseExpression": "componentLicenseExpression", "latestVersion": "1.0.0", @@ -285,8 +297,8 @@ public void testLoadRequiredFieldsForVulnerability() throws Exception { .map(Descriptors.FieldDescriptor::getName) .toList()); - final org.dependencytrack.proto.policy.v1.Vulnerability enrichedVuln = withJdbiHandle(handle -> handle - .attach(CelPolicyDao.class).loadRequiredVulnerabilityFields(List.of(vuln.getId()), requirements)) + final org.dependencytrack.proto.policy.v1.Vulnerability enrichedVuln = withJdbiHandle(handle -> + new CelPolicyDao(handle).loadRequiredVulnerabilityFields(List.of(vuln.getId()), requirements)) .get(vuln.getId()); assertThatJson(JsonFormat.printer().print(enrichedVuln))