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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ public enum Type {
private Date timestamp;

@Persistent
@Column(name = "TEXT")
@Size(min = 1, max = 255)
@Column(name = "TEXT", jdbcType = "CLOB")
@Size(min = 1, max = 1024)
@Pattern(
regexp = RegexSequence.Definition.PRINTABLE_CHARS,
message = "The text may only contain printable characters")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,15 @@ public static PolicyCondition convert(org.dependencytrack.model.PolicyCondition
}

public static PolicyViolation convert(org.dependencytrack.model.PolicyViolation violation) {
return PolicyViolation.newBuilder()
final PolicyViolation.Builder builder = PolicyViolation.newBuilder()
.setUuid(violation.getUuid().toString())
.setCondition(convert(violation.getPolicyCondition()))
.setType(violation.getType().name())
.setTimestamp(Timestamps.fromDate(violation.getTimestamp()))
.build();
.setTimestamp(Timestamps.fromDate(violation.getTimestamp()));
if (violation.getText() != null) {
builder.setMessage(violation.getText());
}
return builder.build();
}

public static PolicyViolationAnalysis convert(ViolationAnalysis analysis) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ default List<PolicyViolationSubject> getForNewPolicyViolations(Collection<Long>
SELECT pv."UUID" AS "violationUuid"
, pv."TYPE" AS "violationType"
, pv."TIMESTAMP" AS "violationTimestamp"
, pv."TEXT" AS "violationMessage"
, pc."UUID" AS "conditionUuid"
, pc."SUBJECT" AS "conditionSubject"
, pc."OPERATOR" AS "conditionOperator"
Expand Down Expand Up @@ -743,12 +744,16 @@ FROM UNNEST(:violationIds) AS req(violation_id)
.setPolicy(policy)
.build();

final PolicyViolation violation = PolicyViolation.newBuilder()
final PolicyViolation.Builder violationBuilder = PolicyViolation.newBuilder()
.setUuid(row.getColumn("violationUuid", String.class))
.setType(row.getColumn("violationType", String.class))
.setTimestamp(Timestamps.fromDate(row.getColumn("violationTimestamp", Timestamp.class)))
.setCondition(condition)
.build();
.setCondition(condition);
final String violationMessage = row.getColumn("violationMessage", String.class);
if (violationMessage != null) {
violationBuilder.setMessage(violationMessage);
}
final PolicyViolation violation = violationBuilder.build();

return PolicyViolationSubject.newBuilder()
.setProject(project)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,9 @@ private void advanceSchedule(NotificationRule rule, Instant lastTriggeredAt) {
if (violation.analysisState() != null) {
entryBuilder.setAnalysisState(violation.analysisState());
}
if (violation.message() != null) {
entryBuilder.setMessage(violation.message());
}

violationsByProjectId
.computeIfAbsent(violation.projectId(), k -> new ArrayList<>())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ public record NewPolicyViolation(
String violationType,
Instant timestamp,
String analysisState,
boolean suppressed) {}
boolean suppressed,
@Nullable String message) {}

public List<NewPolicyViolation> getNewPolicyViolationsSince(
Collection<Long> projectIds, Instant sinceTimestamp, Instant beforeTimestamp) {
Expand All @@ -197,6 +198,7 @@ public List<NewPolicyViolation> getNewPolicyViolationsSince(
, pv."TIMESTAMP" AS timestamp
, va."STATE" AS analysis_state
, COALESCE(va."SUPPRESSED", FALSE) AS suppressed
, pv."TEXT" AS message
FROM "POLICYVIOLATION" pv
LEFT JOIN "VIOLATIONANALYSIS" va
ON va."POLICYVIOLATION_ID" = pv."ID"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ private CelPolicyProgram compile(String normalizedSrc, String scriptDigest, Cach
LOGGER.debug("Compiling expression: %s".formatted(normalizedSrc));
final CelAbstractSyntaxTree ast =
policyType.compiler().compile(normalizedSrc).getAst();
validateResultType(ast);

final CelRuntime.Program celProgram;
try {
Expand All @@ -124,6 +125,23 @@ private CelPolicyProgram compile(String normalizedSrc, String scriptDigest, Cach
return program;
}

private void validateResultType(CelAbstractSyntaxTree ast) throws CelValidationException {
final Set<CelType> allowedTypes = policyType.resultTypes();
if (allowedTypes.contains(ast.getResultType())) {
return;
}

final String allowedTypeNames =
allowedTypes.stream().map(CelType::name).sorted().collect(Collectors.joining(" or "));
throw new CelValidationException(
ast.getSource(),
List.of(CelIssue.formatError(
1,
0,
"Expression must return %s, but returns %s"
.formatted(allowedTypeNames, ast.getResultType().name()))));
}

private record AnalysisResult(Map<CelType, Set<String>> requirements, Set<String> usedFunctions) {}

private static AnalysisResult analyze(CelAbstractSyntaxTree ast) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import org.dependencytrack.proto.policy.v1.License;
import org.dependencytrack.proto.policy.v1.Project;
import org.dependencytrack.proto.policy.v1.Vulnerability;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -295,6 +296,9 @@ public void evaluateProject(UUID uuid) throws InterruptedException {

record ConditionScript(PolicyCondition condition, CelPolicyProgram script) {}

private record ViolatedCondition(
PolicyCondition condition, @Nullable String message) {}

record PolicyWithScripts(Policy policy, List<ConditionScript> conditionScripts) {}

private List<PolicyWithScripts> compilePoliciesScripts(List<Policy> policies) {
Expand Down Expand Up @@ -369,12 +373,13 @@ private void evaluateComponentAgainstPolicies(
Map<Long, List<PolicyViolation>> violationsByComponentId) {
for (final PolicyWithScripts pws : policiesWithScripts) {
final Policy policy = pws.policy();
final var violatedConditions = new ArrayList<PolicyCondition>();
final var violatedConditions = new ArrayList<ViolatedCondition>();

for (final ConditionScript cs : pws.conditionScripts()) {
try {
if (cs.script().execute(scriptArgs)) {
violatedConditions.add(cs.condition());
final CelPolicyProgram.Result result = cs.script().evaluate(scriptArgs);
if (result.matched()) {
violatedConditions.add(new ViolatedCondition(cs.condition(), result.message()));
}
} catch (CelEvaluationException e) {
LOGGER.warn(
Expand All @@ -393,11 +398,12 @@ private void evaluateComponentAgainstPolicies(
};

if (policyViolated) {
for (final PolicyCondition condition : violatedConditions) {
for (final ViolatedCondition violatedCondition : violatedConditions) {
final var violation = new PolicyViolation();
violation.setType(condition.getViolationType());
violation.setPolicyCondition(condition);
violation.setType(violatedCondition.condition().getViolationType());
violation.setPolicyCondition(violatedCondition.condition());
violation.setTimestamp(new Date());
violation.setText(violatedCondition.message());
violationsByComponentId
.computeIfAbsent(componentId, _ -> new ArrayList<>())
.add(violation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,28 @@
import dev.cel.common.types.CelType;
import dev.cel.runtime.CelEvaluationException;
import dev.cel.runtime.CelRuntime;
import org.jspecify.annotations.Nullable;

import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

public final class CelPolicyProgram {

/// Maximum length of a violation message produced by an expression.
/// Longer messages are truncated.
static final int MAX_MESSAGE_LENGTH = 1024;

/// Outcome of evaluating a policy expression.
///
/// @param matched Whether the expression matched
/// @param message Message produced by the expression, if it returned a string
record Result(boolean matched, @Nullable String message) {

private static final Result MATCHED = new Result(true, null);
private static final Result NOT_MATCHED = new Result(false, null);
}

private final CelRuntime.Program program;
private final Map<CelType, Set<String>> requirements;

Expand All @@ -41,7 +56,33 @@ Map<CelType, Set<String>> getRequirements() {
return requirements;
}

/// Evaluate the expression.
///
/// A boolean result is the match outcome. A string result means the expression
/// matched and the string is the violation message, unless the string is blank,
/// which is treated as no match. See ADR 043.
Result evaluate(final Map<String, Object> arguments) throws CelEvaluationException {
final Object result = program.eval(arguments);
return switch (result) {
case Boolean matched -> matched ? Result.MATCHED : Result.NOT_MATCHED;
case String message -> {
final String strippedMessage = message.strip();
if (strippedMessage.isEmpty()) {
yield Result.NOT_MATCHED;
}
yield new Result(
true,
strippedMessage.length() > MAX_MESSAGE_LENGTH
? strippedMessage.substring(0, MAX_MESSAGE_LENGTH)
: strippedMessage);
}
default ->
throw new IllegalStateException("Expression returned unexpected type %s"
.formatted(result.getClass().getName()));
};
}

boolean execute(final Map<String, Object> arguments) throws CelEvaluationException {
return (Boolean) program.eval(arguments);
return evaluate(arguments).matched();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

import com.google.protobuf.Descriptors.Descriptor;
import dev.cel.common.CelVarDecl;
import dev.cel.common.types.CelType;
import dev.cel.common.types.SimpleType;
import dev.cel.compiler.CelCompiler;
import dev.cel.compiler.CelCompilerFactory;
import dev.cel.extensions.CelExtensions;
Expand All @@ -33,30 +35,49 @@
import org.dependencytrack.proto.policy.v1.VersionDistance;
import org.dependencytrack.proto.policy.v1.Vulnerability;

import java.util.Set;

public enum CelPolicyType {
COMPONENT(CelPolicyVariable.COMPONENT, CelPolicyVariable.PROJECT, CelPolicyVariable.VULNS, CelPolicyVariable.NOW),
/// Component policy conditions may return a boolean, or a string
/// that doubles as violation message. See ADR 043.
COMPONENT(
Set.of(SimpleType.BOOL, SimpleType.STRING),
CelPolicyVariable.COMPONENT,
CelPolicyVariable.PROJECT,
CelPolicyVariable.VULNS,
CelPolicyVariable.NOW),
/// Vulnerability policy conditions must return a boolean. A match usually
/// applies a suppression, so a string result is rejected to prevent
/// accidental matches.
VULNERABILITY(
CelPolicyVariable.COMPONENT, CelPolicyVariable.PROJECT, CelPolicyVariable.VULN, CelPolicyVariable.NOW);
Set.of(SimpleType.BOOL),
CelPolicyVariable.COMPONENT,
CelPolicyVariable.PROJECT,
CelPolicyVariable.VULN,
CelPolicyVariable.NOW);

private final Set<CelType> resultTypes;
private final CelCompiler compiler;
private final CelRuntime runtime;

CelPolicyType(CelPolicyVariable... variables) {
CelPolicyType(Set<CelType> resultTypes, CelPolicyVariable... variables) {
this.resultTypes = resultTypes;
final var library = new CelPolicyLibrary();

// NB: Message types must be registered directly on the builders,
// not inside CelRuntimeLibrary#setRuntimeOptions, because the runtime's build()
// creates the descriptor pool before invoking library callbacks.
final var compilerBuilder = CelCompilerFactory.standardCelCompilerBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.addLibraries(CelExtensions.strings(), library)
.addLibraries(CelExtensions.strings(), CelExtensions.bindings(), library)
.addMessageTypes(messageTypes());
for (final CelPolicyVariable variable : variables) {
compilerBuilder.addVarDeclarations(
CelVarDecl.newVarDeclaration(variable.variableName(), variable.celType()));
}
this.compiler = compilerBuilder.build();

// NB: The bindings extension is a compile-time macro and has no runtime part.
this.runtime = CelRuntimeFactory.standardCelRuntimeBuilder()
.addLibraries(CelExtensions.strings(), library)
.addMessageTypes(messageTypes())
Expand All @@ -79,6 +100,11 @@ private static Descriptor[] messageTypes() {
};
}

/// Result types an expression of this type may have.
Set<CelType> resultTypes() {
return resultTypes;
}

CelCompiler compiler() {
return compiler;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ public Set<Long> reconcileViolations(
final var projIds = new Long[size];
final var condIds = new Long[size];
final var types = new String[size];
final var texts = new String[size];

int i = 0;
for (final var entry : reportedViolationsByComponentId.entrySet()) {
Expand All @@ -494,30 +495,36 @@ public Set<Long> reconcileViolations(
projIds[i] = projectId;
condIds[i] = violation.getPolicyCondition().getId();
types[i] = violation.getType().name();
texts[i] = violation.getText();
i++;
}
}

return jdbiHandle
.createQuery("""
WITH created AS (
WITH upserted AS (
INSERT INTO "POLICYVIOLATION" (
"UUID"
, "TIMESTAMP"
, "COMPONENT_ID"
, "PROJECT_ID"
, "POLICYCONDITION_ID"
, "TYPE"
, "TEXT"
)
SELECT GEN_RANDOM_UUID()
, t.*
FROM UNNEST(:timestamps, :componentIds, :projectIds, :policyConditionIds, :types)
AS t("TIMESTAMP", "COMPONENT_ID", "PROJECT_ID", "POLICYCONDITION_ID", "TYPE")
FROM UNNEST(:timestamps, :componentIds, :projectIds, :policyConditionIds, :types, :texts)
AS t("TIMESTAMP", "COMPONENT_ID", "PROJECT_ID", "POLICYCONDITION_ID", "TYPE", "TEXT")
ORDER BY t."PROJECT_ID"
, t."COMPONENT_ID"
, t."POLICYCONDITION_ID"
ON CONFLICT DO NOTHING
RETURNING "ID"
ON CONFLICT ("COMPONENT_ID", "PROJECT_ID", "POLICYCONDITION_ID") DO UPDATE
SET "TEXT" = EXCLUDED."TEXT"
WHERE "POLICYVIOLATION"."TEXT" IS DISTINCT FROM EXCLUDED."TEXT"
-- xmax is zero for rows created by this statement,
-- and non-zero for existing rows that were updated.
RETURNING "ID", (xmax = 0) AS "CREATED"
Comment thread
nscuro marked this conversation as resolved.
),
deleted AS (
DELETE FROM "POLICYVIOLATION"
Expand All @@ -532,13 +539,14 @@ deleted AS (
FOR UPDATE
)
)
SELECT "ID" FROM created
SELECT "ID" FROM upserted WHERE "CREATED"
""")
.bind("timestamps", timestamps)
.bind("componentIds", componentIds)
.bind("projectIds", projIds)
.bind("policyConditionIds", condIds)
.bind("types", types)
.bind("texts", texts)
.bind("projectId", projectId)
.mapTo(Long.class)
.set();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ public record CreatePolicyConditionRequest(
requiredMode = Schema.RequiredMode.REQUIRED)
PolicyCondition.Operator operator,

@NotBlank @Schema(description = "Value the subject is compared to", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank @Schema(description = """
Value the subject is compared to. For `EXPRESSION` subjects, a CEL expression \
that returns either a boolean, or a string. A non-blank string means the \
condition matched and the string is used as violation message. A blank string \
means the condition did not match.""", requiredMode = Schema.RequiredMode.REQUIRED)
String value,

@Schema(description = "Violation type produced when the condition matches. Required for `EXPRESSION` subjects")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ public record UpdatePolicyConditionRequest(
requiredMode = Schema.RequiredMode.REQUIRED)
PolicyCondition.Operator operator,

@NotBlank @Schema(description = "Value the subject is compared to", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank @Schema(description = """
Value the subject is compared to. For `EXPRESSION` subjects, a CEL expression \
that returns either a boolean, or a string. A non-blank string means the \
condition matched and the string is used as violation message. A blank string \
means the condition did not match.""", requiredMode = Schema.RequiredMode.REQUIRED)
String value,

@Schema(description = "Violation type produced when the condition matches. Required for `EXPRESSION` subjects")
Expand Down
Loading
Loading