Skip to content
Open
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
8 changes: 8 additions & 0 deletions c/include/dd/policies/eval_ctx.h
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,11 @@ plcs_errors plcs_eval_ctx_get_unum_eval_error(plcs_numeric_evaluators ix);
plcs_eval_ctx_register_str_evaluator(function_ptr, id); \
plcs_eval_ctx_set_str_eval_param(id, param); \
} while (0)

/**
* @brief Returns the description of the rule that triggered the match during the most recent
* policy evaluation. Must be read inside an action callback when using plcs_evaluate_buffer,
* as it is reset at the start of each policy evaluation.
* @return const char* NULL if no match occurred or the matching node has no description.
*/
const char *plcs_eval_ctx_get_matched_description(void);
18 changes: 10 additions & 8 deletions c/src/eval_ctx.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "eval_ctx.h"

#include <stdbool.h>
#include <stdio.h>

static plcs_eval_ctx ctx;
static bool plcs_eval_ctx_initialized = false;
Expand Down Expand Up @@ -162,13 +163,6 @@ void plcs_eval_ctx_set_error(plcs_errors error) {
ctx.error = error;
}

// TODO: consider implementing it as a stack to preserve error history
void plcs_eval_ctx_set_action_error(plcs_actions ix, plcs_errors error) {
if (ix >= 0 && ix < PLCS_ACTIONS__COUNT) {
ctx.actions[ix].error = error;
}
}

void plcs_eval_ctx_set_str_eval_error(plcs_string_evaluators ix, plcs_errors error) {
if (ix >= 0 && ix < PLCS_STR_EVAL__COUNT) {
ctx.string_evaluators[ix].error = error;
Expand Down Expand Up @@ -208,6 +202,14 @@ plcs_errors plcs_eval_ctx_get_unum_eval_error(plcs_numeric_evaluators ix) {
return PLCS_EIX_OVERFLOW;
}

const char *plcs_eval_ctx_get_matched_description(void) {
return ctx.matched_description;
}

void plcs_eval_ctx_set_matched_description(const char *description) {
ctx.matched_description = description;
}

plcs_errors plcs_eval_ctx_peek_last_error(void) {
return ctx.error;
}
Expand Down Expand Up @@ -240,10 +242,10 @@ void plcs_eval_ctx_reset(void) {

for (int i = 0; i < PLCS_ACTIONS__COUNT; ++i) {
ctx.actions[i].function_ptr = NULL;
ctx.actions[i].error = PLCS_ESUCCESS;
}

ctx.error = PLCS_ESUCCESS;
ctx.matched_description = NULL;
}

plcs_errors plcs_eval_ctx_init(void) {
Expand Down
27 changes: 18 additions & 9 deletions c/src/eval_ctx.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ typedef struct unumeric_evaluator_entry {
typedef struct action_entry {
/**< Function pointer to the action function */
plcs_action_function_ptr function_ptr;
/**< Error code if the action is not registered or fails !NEEDS */
plcs_errors error;
} action_entry;

/**
Expand Down Expand Up @@ -111,6 +109,12 @@ typedef struct plcs_eval_ctx {
/**< TODO: consider implementing this as a stack to preserve history of errors */
plcs_errors error;

/**< Description of the rule that triggered the action during the most recent
* policy evaluation. NULL if no match occurred. Read via
* plcs_eval_ctx_get_matched_description().
*/
const char *matched_description;

} plcs_eval_ctx;

/**
Expand All @@ -119,13 +123,6 @@ typedef struct plcs_eval_ctx {
*/
void plcs_eval_ctx_set_error(plcs_errors error);

/**
* @brief Sets an error code for an action
* @param ix An action ID from plcs_actions enum
* @param error plcs_errors enum
*/
void plcs_eval_ctx_set_action_error(plcs_actions ix, plcs_errors error);

/**
* @brief Sets an error code for a string evaluator
* @param ix A plcs_string_evaluators enum ID
Expand All @@ -146,3 +143,15 @@ void plcs_eval_ctx_set_num_eval_error(plcs_numeric_evaluators id, plcs_errors er
* @param error plcs_errors enum
*/
void plcs_eval_ctx_set_unum_eval_error(plcs_numeric_evaluators ix, plcs_errors error);

/**
* @brief Returns the description of the rule that triggered the action during the most recent policy evaluation.
* @return const char* NULL if no match occurred.
*/
const char *plcs_eval_ctx_get_matched_description(void);

/**
* @brief Sets the description of the rule that triggered the action during the most recent policy evaluation.
* @param description const char* description of the rule that triggered the action.
*/
void plcs_eval_ctx_set_matched_description(const char *description);
206 changes: 201 additions & 5 deletions c/src/evaluator.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
#include "wire/evaluation_result.h"
#define PLCS_MAX_EVAL_DEPTH 64

static char synth_buf[512];

plcs_evaluation_result evaluate_rules(dd_ns(NodeTypeWrapper_table_t) node, int depth);

plcs_evaluation_result evaluate_string(dd_ns(StrEvaluator_table_t) eval_str, const char *description) {
Expand Down Expand Up @@ -168,6 +170,166 @@ plcs_evaluation_result DoOper(dd_ns(BoolOperation_enum_t) oper, plcs_evaluation_
}
}

static const char *cmp_op_label(dd_ns(CmpTypeSTR_enum_t) cmp) {
switch (cmp) {
case dd_ns(CmpTypeSTR_CMP_EXACT):
return "matches";
case dd_ns(CmpTypeSTR_CMP_PREFIX):
return "starts with";
case dd_ns(CmpTypeSTR_CMP_SUFFIX):
return "ends with";
case dd_ns(CmpTypeSTR_CMP_CONTAINS):
return "contains";
case dd_ns(CmpTypeSTR_CMP_WILDCARD):
return "matches (wildcard)";
default:
return "?";
}
}

static const char *str_eval_field_label(dd_ns(StringEvaluators_enum_t) id) {
switch (id) {
case dd_ns(StringEvaluators_COMPONENT):
return "component";
case dd_ns(StringEvaluators_PROCESS_EXE):
return "executable";
case dd_ns(StringEvaluators_PROCESS_EXE_FULL_PATH):
return "executable full path";
case dd_ns(StringEvaluators_PROCESS_BASEDIR_PATH):
return "process base directory";
case dd_ns(StringEvaluators_PROCESS_ARGV):
return "command-line argument";
case dd_ns(StringEvaluators_PROCESS_CWD):
return "process working directory";
case dd_ns(StringEvaluators_RUNTIME_LANGUAGE):
return "language";
case dd_ns(StringEvaluators_RUNTIME_ENTRY_POINT_FILE):
return "entry point file";
case dd_ns(StringEvaluators_RUNTIME_ENTRY_POINT_JAR):
return "entry point JAR";
case dd_ns(StringEvaluators_RUNTIME_ENTRY_POINT_CLASS):
return "entry point class";
case dd_ns(StringEvaluators_RUNTIME_ENTRY_POINT_PACKAGE):
return "entry point package";
case dd_ns(StringEvaluators_RUNTIME_ENTRY_POINT_MODULE):
return "entry point module";
case dd_ns(StringEvaluators_RUNTIME_ENTRY_POINT_SOURCE):
return "entry point source file";
case dd_ns(StringEvaluators_RUNTIME_DOPTION):
return "runtime -D option";
case dd_ns(StringEvaluators_RUNTIME_VERSION):
return "runtime version";
case dd_ns(StringEvaluators_LIBC_FLAVOR):
return "libc flavor";
case dd_ns(StringEvaluators_LIBC_VERSION):
return "libc version";
case dd_ns(StringEvaluators_MACHINE_ARCHITECTURE):
return "machine architecture";
case dd_ns(StringEvaluators_HOST_NAME):
return "host name";
case dd_ns(StringEvaluators_HOST_IP):
return "host IP";
case dd_ns(StringEvaluators_OS):
return "operating system";
case dd_ns(StringEvaluators_OS_DISTRO):
return "OS distribution";
case dd_ns(StringEvaluators_OS_DISTRO_VERSION):
return "OS distribution version";
case dd_ns(StringEvaluators_OS_DISTRO_CODENAME):
return "OS distribution codename";
case dd_ns(StringEvaluators_OS_KERNEL_VERSION):
return "OS kernel version";
case dd_ns(StringEvaluators_OS_KERNEL_NAME):
return "OS kernel name";
case dd_ns(StringEvaluators_OS_USER):
return "OS user";
case dd_ns(StringEvaluators_OS_USER_GROUP):
return "OS user group";
case dd_ns(StringEvaluators_CONTAINER_IMAGE):
return "container image";
case dd_ns(StringEvaluators_CONTAINER_ID):
return "container ID";
case dd_ns(StringEvaluators_IIS_APPLICATION_POOL):
return "IIS application pool";
case dd_ns(StringEvaluators_PROCESS_ARGV_0):
return "command-line argument 0";
case dd_ns(StringEvaluators_PROCESS_ARGV_1):
return "command-line argument 1";
case dd_ns(StringEvaluators_PROCESS_ARGV_2):
return "command-line argument 2";
case dd_ns(StringEvaluators_PROCESS_ARGV_3):
return "command-line argument 3";
case dd_ns(StringEvaluators_PROCESS_ARGV_4):
return "command-line argument 4";
case dd_ns(StringEvaluators_PROCESS_ARGV_5):
return "command-line argument 5";
case dd_ns(StringEvaluators_PROCESS_ARGV_N):
return "last command-line argument";
case dd_ns(StringEvaluators_PROCESS_ARGV_N_2):
return "second to last command-line argument";
case dd_ns(StringEvaluators_PROCESS_ARGV_N_3):
return "third to last command-line argument";
case dd_ns(StringEvaluators_PROCESS_ARGV_N_4):
return "fourth to last command-line argument";
case dd_ns(StringEvaluators_PROCESS_ARGV_N_5):
return "fifth to last command-line argument";
case dd_ns(StringEvaluators_PROCESS_ARGV_N_6):
return "sixth to last command-line argument";
case dd_ns(StringEvaluators_PROCESS_ENVAR):
return "process environment variable";
default:
return dd_ns(StringEvaluators_name)(id);
}
}

static inline void capture_matched_description(dd_ns(NodeTypeWrapper_table_t) node) {
if (!node) {
return;
}
// A more specific description from a deeper node takes priority.
const char *existing = plcs_eval_ctx_get_matched_description();
if (existing && existing[0] != '\0') {
return;
}
const char *description = NULL;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you discussed the possibility of generating the description in the backend instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the concern was that generating on the backend would make it harder to include runtime context (which arg matched, what value triggered the rule, etc.), since that information is only available at injection time. It would also require the backend to maintain an enum -> description mapping, meaning policy changes would need to be coordinated across repos. I'd lean toward keeping it in dd-policy-engine for those reasons, but if you have other thoughts let me know!

switch (dd_ns(NodeTypeWrapper_node_type)(node)) {
case dd_ns(NodeType_CompositeNode): {
dd_ns(CompositeNode_table_t) comp = dd_ns(NodeTypeWrapper_node)(node);
description = dd_ns(CompositeNode_description)(comp);
if ((!description || description[0] == '\0') && dd_ns(CompositeNode_op)(comp) == dd_ns(BoolOperation_BOOL_NOT)) {
dd_ns(NodeTypeWrapper_vec_t) children = dd_ns(CompositeNode_children)(comp);
if (children && dd_ns(NodeTypeWrapper_vec_len)(children) == 1) {
capture_matched_description(dd_ns(NodeTypeWrapper_vec_at)(children, 0));
}
return;
}
break;
}
case dd_ns(NodeType_EvaluatorNode): {
dd_ns(EvaluatorNode_table_t) eval_node = dd_ns(NodeTypeWrapper_node)(node);
description = dd_ns(EvaluatorNode_description)(eval_node);
if (!description || description[0] == '\0') {
dd_ns(EvaluatorType_union_t) eval = dd_ns(EvaluatorNode_eval_union)(eval_node);
if (eval.type == dd_ns(EvaluatorType_StrEvaluator)) {
dd_ns(StrEvaluator_table_t) str_eval = eval.value;
plcs_string_evaluators eval_id = dd_ns(StrEvaluator_id)(str_eval);
const char *field = str_eval_field_label(eval_id);
const char *op = cmp_op_label(dd_ns(StrEvaluator_cmp)(str_eval));
const char *value = dd_ns(StrEvaluator_value)(str_eval);
const char *ctx = plcs_eval_ctx_get_string_param(eval_id);
snprintf(synth_buf, sizeof(synth_buf), "%s '%s' %s '%s'", field ? field : "?", ctx ? ctx : "?", op, value ? value : "");
plcs_eval_ctx_set_matched_description(synth_buf);
return;
}
}
break;
}
}
if (description && description[0] != '\0') {
plcs_eval_ctx_set_matched_description(description);
}
}

plcs_evaluation_result composite_evaluator(dd_ns(CompositeNode_table_t) node, int depth) {
if (!node) {
return PLCS_EVAL_RESULT_ABSTAIN;
Expand Down Expand Up @@ -204,20 +366,34 @@ plcs_evaluation_result composite_evaluator(dd_ns(CompositeNode_table_t) node, in
}

// keep iterating recursively over the tree
dd_ns(NodeTypeWrapper_table_t) last_true_child = NULL;
for (size_t ix = 0; ix < children_len; ++ix) {
res = DoOper(oper, res, evaluate_rules(dd_ns(NodeTypeWrapper_vec_at)(children, ix), depth + 1));
dd_ns(NodeTypeWrapper_table_t) child = dd_ns(NodeTypeWrapper_vec_at)(children, ix);
plcs_evaluation_result child_res = evaluate_rules(child, depth + 1);
res = DoOper(oper, res, child_res);

// short circuit
// short circuit OR: capture the child that caused TRUE
if (oper == dd_ns(BoolOperation_BOOL_OR) && res == PLCS_EVAL_RESULT_TRUE) {
capture_matched_description(child);
return res;
}

// short circuit
// track last TRUE child for AND description capture
if (oper == dd_ns(BoolOperation_BOOL_AND) && child_res == PLCS_EVAL_RESULT_TRUE) {
last_true_child = child;
}

// short circuit AND
if (oper == dd_ns(BoolOperation_BOOL_AND) && res == PLCS_EVAL_RESULT_FALSE) {
return res;
}
}

// AND succeeded: capture description of last TRUE child (no-op if OR already set one)
if (oper == dd_ns(BoolOperation_BOOL_AND) && last_true_child) {
capture_matched_description(last_true_child);
}

return res;
}

Expand Down Expand Up @@ -270,7 +446,6 @@ static inline plcs_errors perform_actions(plcs_evaluation_result eval_res, dd_ns
plcs_action_function_ptr action_function = plcs_eval_ctx_get_action(action_id);
if (action_function) {
res = action_function(eval_res, values, values_len, dd_ns(Action_description)(action), action_id);
plcs_eval_ctx_set_action_error(action_id, res);
} else {
res = PLCS_EACTIONS_EVAL;
}
Expand All @@ -286,9 +461,30 @@ plcs_errors evaluate_policy(dd_ns(Policy_table_t) policy) {
// extract rules
dd_ns(NodeTypeWrapper_table_t) rules = dd_ns(Policy_rules)(policy);

// // evaluate rules if they exist, otherwise return EVAL_RESULT_ABSTAIN
// reset matched description
plcs_eval_ctx_set_matched_description(NULL);

// evaluate rules if they exist, otherwise return EVAL_RESULT_ABSTAIN
plcs_evaluation_result eval_res = rules ? evaluate_rules(rules, 0) : PLCS_EVAL_RESULT_ABSTAIN;

if (eval_res == PLCS_EVAL_RESULT_TRUE &&
(plcs_eval_ctx_get_matched_description() == NULL || plcs_eval_ctx_get_matched_description()[0] == '\0')) {
capture_matched_description(rules);
}

// Prepend [skip]/[allow] prefix from the first action when no prefix is present yet.
const char *desc = plcs_eval_ctx_get_matched_description();
if (desc && desc[0] != '\0' && desc[0] != '[' && actions && dd_ns(Action_vec_len)(actions) > 0) {
dd_ns(Action_table_t) first_action = dd_ns(Action_vec_at)(actions, 0);
if (first_action) {
const char *prefix = dd_ns(Action_action)(first_action) == dd_ns(ActionId_INJECT_DENY) ? "[skip] " : "[allow] ";
char temp[512];
snprintf(temp, sizeof(temp), "%s%s", prefix, desc);
snprintf(synth_buf, sizeof(synth_buf), "%s", temp);
plcs_eval_ctx_set_matched_description(synth_buf);
}
}

// perform actions given evaluation result
return perform_actions(eval_res, actions);
}
Expand Down
6 changes: 0 additions & 6 deletions c/src/test/test_eval_ctx.c
Original file line number Diff line number Diff line change
Expand Up @@ -298,12 +298,6 @@ UTEST(eval_ctx, last_error_set_and_get) {
ASSERT_EQ(plcs_eval_ctx_peek_last_error(), (plcs_errors)PLCS_ESUCCESS);
}

UTEST(eval_ctx, set_error_out_of_bound) {
plcs_eval_ctx_set_action_error(PLCS_ACTIONS__COUNT, 0);
int err = plcs_eval_ctx_get_last_error();
ASSERT_EQ(err, PLCS_ESUCCESS);
}

/* -------------------------------------------------------------------------- */
/* Default evaluator sanity checks */
/* -------------------------------------------------------------------------- */
Expand Down
Loading
Loading