From fe50197d1ee862827d54ecb046c587c1e9e38e91 Mon Sep 17 00:00:00 2001 From: Anna Cai Date: Tue, 26 May 2026 09:01:03 -0400 Subject: [PATCH 1/2] Expose matched rule id to action callbacks --- c/include/dd/policies/eval_ctx.h | 21 +++ c/src/eval_ctx.c | 9 ++ c/src/eval_ctx.h | 24 +++ c/src/evaluator.c | 46 +++++- c/src/schema/nodes_builder.h | 20 ++- c/src/schema/nodes_reader.h | 11 +- c/src/schema/nodes_verifier.h | 2 + c/src/test/test_evaluator.c | 35 +++- fbs-schema/nodes.fbs | 11 +- .../converter/arguments.go | 4 +- .../converter/commands.go | 2 +- .../converter/deny.go | 32 ++-- .../converter/libc.go | 47 +++--- .../converter/requirements.go | 51 ++++-- go/cmd/dd-requirements-converter/main_test.go | 151 +++++++++++++----- go/cmd/dd-rules-converter/main.go | 4 +- .../example_generate_c_header_buffer/main.go | 2 +- .../converter/condition.go | 2 +- .../converter/path_values.go | 2 +- .../converter/policy.go | 6 +- go/examples/example_writer/main.go | 4 +- go/schema/Creators.go | 24 ++- go/schema/dd/wls/CompositeNode.go | 25 ++- go/schema/dd/wls/EvaluatorNode.go | 19 ++- 24 files changed, 432 insertions(+), 122 deletions(-) diff --git a/c/include/dd/policies/eval_ctx.h b/c/include/dd/policies/eval_ctx.h index e4316ab..960838b 100644 --- a/c/include/dd/policies/eval_ctx.h +++ b/c/include/dd/policies/eval_ctx.h @@ -160,6 +160,27 @@ unsigned long plcs_eval_ctx_get_unumeric_param(plcs_numeric_evaluators id); */ plcs_action_function_ptr plcs_eval_ctx_get_action(plcs_actions ix); +/** + * @brief Returns the rule id that produced the current policy's TRUE verdict, + * or NULL if no rule-id-bearing top-level OR child matched. + * + * Set by the evaluator when composite_evaluator at depth 0 sees an OR child + * return TRUE; the description of that child is captured as the rule id. This + * lets action callbacks identify *which* rule fired in policies that bundle + * many rules under one Action (e.g. the requirements.bin produced by + * dd-requirements-converter, where each top-level OR child is wrapped in a + * composite whose description is the rule's stable identifier). + * + * For single-rule policies (rules tree root is not an OR), this returns NULL + * and callers should fall back to Action.description, which by convention + * carries the rule id in that shape (see dd-rules-converter). + * + * @note Reset to NULL at the start of each evaluate_policy. The returned + * pointer is owned by the FlatBuffer passed to plcs_evaluate_buffer and is + * only valid until that buffer is freed. + */ +const char *plcs_eval_ctx_get_matched_rule_id(void); + /** * @brief An accessor for the last error * @return the last error as a plcs_errors enum, NOTE: This will RESET the error! diff --git a/c/src/eval_ctx.c b/c/src/eval_ctx.c index a49b6c9..f4afa1d 100644 --- a/c/src/eval_ctx.c +++ b/c/src/eval_ctx.c @@ -208,6 +208,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_rule_id(void) { + return ctx.matched_rule_id; +} + +void plcs_eval_ctx_set_matched_rule_id(const char *id) { + ctx.matched_rule_id = id; +} + plcs_errors plcs_eval_ctx_peek_last_error(void) { return ctx.error; } @@ -244,6 +252,7 @@ void plcs_eval_ctx_reset(void) { } ctx.error = PLCS_ESUCCESS; + ctx.matched_rule_id = NULL; } plcs_errors plcs_eval_ctx_init(void) { diff --git a/c/src/eval_ctx.h b/c/src/eval_ctx.h index f4ff4a5..92f96c1 100644 --- a/c/src/eval_ctx.h +++ b/c/src/eval_ctx.h @@ -111,6 +111,17 @@ typedef struct plcs_eval_ctx { /**< TODO: consider implementing this as a stack to preserve history of errors */ plcs_errors error; + /** + * @brief Description of the top-level OR child whose evaluation returned + * TRUE within the policy currently being evaluated, or NULL if no such + * match occurred (e.g. single-rule policies whose top of the rules tree is + * not an OR). Set by composite_evaluator at depth 0, read by action + * callbacks via plcs_eval_ctx_get_matched_rule_id(). Points into the + * caller-owned FlatBuffer; lifetime is tied to plcs_evaluate_buffer's + * buffer argument. + */ + const char *matched_rule_id; + } plcs_eval_ctx; /** @@ -146,3 +157,16 @@ 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 Set the matched rule id captured during policy evaluation. + * + * Internal — called by composite_evaluator when a top-level OR child returns + * TRUE. The pointer is stored verbatim (no copy), so it must reference memory + * with a lifetime at least as long as the next plcs_eval_ctx_reset() or + * plcs_evaluate_buffer() call — in practice, a string inside the FlatBuffer + * being evaluated. + * + * @param id Rule id string, or NULL to clear. + */ +void plcs_eval_ctx_set_matched_rule_id(const char *id); diff --git a/c/src/evaluator.c b/c/src/evaluator.c index a2875ed..a307780 100644 --- a/c/src/evaluator.c +++ b/c/src/evaluator.c @@ -168,6 +168,35 @@ plcs_evaluation_result DoOper(dd_ns(BoolOperation_enum_t) oper, plcs_evaluation_ } } +// Reads the rule_id field from a NodeTypeWrapper's inner node (composite or +// evaluator) and records it as the policy's matched rule id. Used at depth 0 +// in OR composites to identify which top-level rule fired. The field is empty +// on nodes that aren't rule roots; the engine treats empty as "no rule id." +static inline void capture_matched_rule_id(dd_ns(NodeTypeWrapper_table_t) wrapper) { + if (!wrapper) { + return; + } + switch (dd_ns(NodeTypeWrapper_node_type)(wrapper)) { + case dd_ns(NodeType_CompositeNode): { + dd_ns(CompositeNode_table_t) c = dd_ns(NodeTypeWrapper_node)(wrapper); + if (c) { + plcs_eval_ctx_set_matched_rule_id(dd_ns(CompositeNode_rule_id)(c)); + } + break; + } + case dd_ns(NodeType_EvaluatorNode): { + dd_ns(EvaluatorNode_table_t) e = dd_ns(NodeTypeWrapper_node)(wrapper); + if (e) { + plcs_eval_ctx_set_matched_rule_id(dd_ns(EvaluatorNode_rule_id)(e)); + } + break; + } + default: + // unknown node type — leave matched_rule_id unchanged + break; + } +} + plcs_evaluation_result composite_evaluator(dd_ns(CompositeNode_table_t) node, int depth) { if (!node) { return PLCS_EVAL_RESULT_ABSTAIN; @@ -205,10 +234,20 @@ plcs_evaluation_result composite_evaluator(dd_ns(CompositeNode_table_t) node, in // keep iterating recursively over the tree 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); + res = DoOper(oper, res, evaluate_rules(child, depth + 1)); // short circuit if (oper == dd_ns(BoolOperation_BOOL_OR) && res == PLCS_EVAL_RESULT_TRUE) { + // When the top-level rules tree is an OR (depth == 0), the matching + // child's description is the rule's stable identifier (see + // dd-requirements-converter, which wraps each rule in such a composite). + // Capture it here so action callbacks can surface "which rule fired" in + // telemetry. Nested ORs (depth > 0) are part of a single rule's + // sub-tree (e.g. "match any cmd pattern") and must not overwrite. + if (depth == 0) { + capture_matched_rule_id(child); + } return res; } @@ -286,6 +325,11 @@ plcs_errors evaluate_policy(dd_ns(Policy_table_t) policy) { // extract rules dd_ns(NodeTypeWrapper_table_t) rules = dd_ns(Policy_rules)(policy); + // Clear any matched-rule-id residue from the previous policy. composite_evaluator + // sets this if/when a top-level OR child returns TRUE; if none does, it stays NULL + // and action callbacks fall back to Action.description. + plcs_eval_ctx_set_matched_rule_id(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; diff --git a/c/src/schema/nodes_builder.h b/c/src/schema/nodes_builder.h index ab6cb21..3815e35 100644 --- a/c/src/schema/nodes_builder.h +++ b/c/src/schema/nodes_builder.h @@ -35,25 +35,25 @@ __flatbuffers_build_table(flatbuffers_, dd_wls_NodeTypeWrapper, 2) static const flatbuffers_voffset_t __dd_wls_CompositeNode_required[] = { 0 }; typedef flatbuffers_ref_t dd_wls_CompositeNode_ref_t; static dd_wls_CompositeNode_ref_t dd_wls_CompositeNode_clone(flatbuffers_builder_t *B, dd_wls_CompositeNode_table_t t); -__flatbuffers_build_table(flatbuffers_, dd_wls_CompositeNode, 3) +__flatbuffers_build_table(flatbuffers_, dd_wls_CompositeNode, 4) static const flatbuffers_voffset_t __dd_wls_EvaluatorNode_required[] = { 0 }; typedef flatbuffers_ref_t dd_wls_EvaluatorNode_ref_t; static dd_wls_EvaluatorNode_ref_t dd_wls_EvaluatorNode_clone(flatbuffers_builder_t *B, dd_wls_EvaluatorNode_table_t t); -__flatbuffers_build_table(flatbuffers_, dd_wls_EvaluatorNode, 3) +__flatbuffers_build_table(flatbuffers_, dd_wls_EvaluatorNode, 4) #define __dd_wls_NodeTypeWrapper_formal_args , dd_wls_NodeType_union_ref_t v1 #define __dd_wls_NodeTypeWrapper_call_args , v1 static inline dd_wls_NodeTypeWrapper_ref_t dd_wls_NodeTypeWrapper_create(flatbuffers_builder_t *B __dd_wls_NodeTypeWrapper_formal_args); __flatbuffers_build_table_prolog(flatbuffers_, dd_wls_NodeTypeWrapper, dd_wls_NodeTypeWrapper_file_identifier, dd_wls_NodeTypeWrapper_type_identifier) -#define __dd_wls_CompositeNode_formal_args , flatbuffers_string_ref_t v0, dd_wls_BoolOperation_enum_t v1, dd_wls_NodeTypeWrapper_vec_ref_t v2 -#define __dd_wls_CompositeNode_call_args , v0, v1, v2 +#define __dd_wls_CompositeNode_formal_args , flatbuffers_string_ref_t v0, dd_wls_BoolOperation_enum_t v1, dd_wls_NodeTypeWrapper_vec_ref_t v2, flatbuffers_string_ref_t v3 +#define __dd_wls_CompositeNode_call_args , v0, v1, v2, v3 static inline dd_wls_CompositeNode_ref_t dd_wls_CompositeNode_create(flatbuffers_builder_t *B __dd_wls_CompositeNode_formal_args); __flatbuffers_build_table_prolog(flatbuffers_, dd_wls_CompositeNode, dd_wls_CompositeNode_file_identifier, dd_wls_CompositeNode_type_identifier) -#define __dd_wls_EvaluatorNode_formal_args , flatbuffers_string_ref_t v0, dd_wls_EvaluatorType_union_ref_t v2 -#define __dd_wls_EvaluatorNode_call_args , v0, v2 +#define __dd_wls_EvaluatorNode_formal_args , flatbuffers_string_ref_t v0, dd_wls_EvaluatorType_union_ref_t v2, flatbuffers_string_ref_t v3 +#define __dd_wls_EvaluatorNode_call_args , v0, v2, v3 static inline dd_wls_EvaluatorNode_ref_t dd_wls_EvaluatorNode_create(flatbuffers_builder_t *B __dd_wls_EvaluatorNode_formal_args); __flatbuffers_build_table_prolog(flatbuffers_, dd_wls_EvaluatorNode, dd_wls_EvaluatorNode_file_identifier, dd_wls_EvaluatorNode_type_identifier) @@ -101,12 +101,14 @@ static dd_wls_NodeTypeWrapper_ref_t dd_wls_NodeTypeWrapper_clone(flatbuffers_bui __flatbuffers_build_string_field(0, flatbuffers_, dd_wls_CompositeNode_description, dd_wls_CompositeNode) __flatbuffers_build_scalar_field(1, flatbuffers_, dd_wls_CompositeNode_op, dd_wls_BoolOperation, dd_wls_BoolOperation_enum_t, 1, 1, INT8_C(0), dd_wls_CompositeNode) __flatbuffers_build_table_vector_field(2, flatbuffers_, dd_wls_CompositeNode_children, dd_wls_NodeTypeWrapper, dd_wls_CompositeNode) +__flatbuffers_build_string_field(3, flatbuffers_, dd_wls_CompositeNode_rule_id, dd_wls_CompositeNode) static inline dd_wls_CompositeNode_ref_t dd_wls_CompositeNode_create(flatbuffers_builder_t *B __dd_wls_CompositeNode_formal_args) { if (dd_wls_CompositeNode_start(B) || dd_wls_CompositeNode_description_add(B, v0) || dd_wls_CompositeNode_children_add(B, v2) + || dd_wls_CompositeNode_rule_id_add(B, v3) || dd_wls_CompositeNode_op_add(B, v1)) { return 0; } @@ -119,6 +121,7 @@ static dd_wls_CompositeNode_ref_t dd_wls_CompositeNode_clone(flatbuffers_builder if (dd_wls_CompositeNode_start(B) || dd_wls_CompositeNode_description_pick(B, t) || dd_wls_CompositeNode_children_pick(B, t) + || dd_wls_CompositeNode_rule_id_pick(B, t) || dd_wls_CompositeNode_op_pick(B, t)) { return 0; } @@ -130,12 +133,14 @@ __flatbuffers_build_union_field(2, flatbuffers_, dd_wls_EvaluatorNode_eval, dd_w __flatbuffers_build_union_table_value_field(flatbuffers_, dd_wls_EvaluatorNode_eval, dd_wls_EvaluatorType, StrEvaluator, dd_wls_StrEvaluator) __flatbuffers_build_union_table_value_field(flatbuffers_, dd_wls_EvaluatorNode_eval, dd_wls_EvaluatorType, NumEvaluator, dd_wls_NumEvaluator) __flatbuffers_build_union_table_value_field(flatbuffers_, dd_wls_EvaluatorNode_eval, dd_wls_EvaluatorType, UNumEvaluator, dd_wls_UNumEvaluator) +__flatbuffers_build_string_field(3, flatbuffers_, dd_wls_EvaluatorNode_rule_id, dd_wls_EvaluatorNode) static inline dd_wls_EvaluatorNode_ref_t dd_wls_EvaluatorNode_create(flatbuffers_builder_t *B __dd_wls_EvaluatorNode_formal_args) { if (dd_wls_EvaluatorNode_start(B) || dd_wls_EvaluatorNode_description_add(B, v0) || dd_wls_EvaluatorNode_eval_add_value(B, v2) + || dd_wls_EvaluatorNode_rule_id_add(B, v3) || dd_wls_EvaluatorNode_eval_add_type(B, v2.type)) { return 0; } @@ -147,7 +152,8 @@ static dd_wls_EvaluatorNode_ref_t dd_wls_EvaluatorNode_clone(flatbuffers_builder __flatbuffers_memoize_begin(B, t); if (dd_wls_EvaluatorNode_start(B) || dd_wls_EvaluatorNode_description_pick(B, t) - || dd_wls_EvaluatorNode_eval_pick(B, t)) { + || dd_wls_EvaluatorNode_eval_pick(B, t) + || dd_wls_EvaluatorNode_rule_id_pick(B, t)) { return 0; } __flatbuffers_memoize_end(B, t, dd_wls_EvaluatorNode_end(B)); diff --git a/c/src/schema/nodes_reader.h b/c/src/schema/nodes_reader.h index 4ca7532..b2e6616 100644 --- a/c/src/schema/nodes_reader.h +++ b/c/src/schema/nodes_reader.h @@ -131,9 +131,14 @@ __flatbuffers_table_as_root(dd_wls_CompositeNode) __flatbuffers_define_string_field(0, dd_wls_CompositeNode, description, 0) __flatbuffers_define_scalar_field(1, dd_wls_CompositeNode, op, dd_wls_BoolOperation, dd_wls_BoolOperation_enum_t, INT8_C(0)) -/** At some point we will switch back to; children: [NodeType]; +/** At some point we will switch back to; children: [NodeType]; * (union vectors are not supported in GO so we are wrapping the table in a table) */ __flatbuffers_define_vector_field(2, dd_wls_CompositeNode, children, dd_wls_NodeTypeWrapper_vec_t, 0) +/** Stable identifier for the rule this node is the root of. Set only on + * nodes that sit directly under a policy's top-level OR (one per rule); + * empty/unset on all other nodes. Read by the engine to surface "which + * rule fired" to action callbacks via plcs_eval_ctx_get_matched_rule_id(). */ +__flatbuffers_define_string_field(3, dd_wls_CompositeNode, rule_id, 0) /** Represents a leaf node in the policy tree. * It contains a description and an evaluator. */ @@ -149,6 +154,10 @@ __flatbuffers_table_as_root(dd_wls_EvaluatorNode) __flatbuffers_define_string_field(0, dd_wls_EvaluatorNode, description, 0) /** The evaluator is a union of different evaluator types (String, Numeric, etc.). */ __flatbuffers_define_union_field(flatbuffers_, 2, dd_wls_EvaluatorNode, eval, dd_wls_EvaluatorType, 0) +/** Stable identifier for the rule this node is the root of. Same semantics + * as CompositeNode.rule_id: set only when this evaluator is itself the + * root of a rule (single-condition rule), empty otherwise. */ +__flatbuffers_define_string_field(3, dd_wls_EvaluatorNode, rule_id, 0) #include "flatcc/flatcc_epilogue.h" diff --git a/c/src/schema/nodes_verifier.h b/c/src/schema/nodes_verifier.h index db76356..54d9e5d 100644 --- a/c/src/schema/nodes_verifier.h +++ b/c/src/schema/nodes_verifier.h @@ -81,6 +81,7 @@ static int dd_wls_CompositeNode_verify_table(flatcc_table_verifier_descriptor_t if ((ret = flatcc_verify_string_field(td, 0, 0) /* description */)) return ret; if ((ret = flatcc_verify_field(td, 1, 1, 1) /* op */)) return ret; if ((ret = flatcc_verify_table_vector_field(td, 2, 0, &dd_wls_NodeTypeWrapper_verify_table) /* children */)) return ret; + if ((ret = flatcc_verify_string_field(td, 3, 0) /* rule_id */)) return ret; return flatcc_verify_ok; } @@ -129,6 +130,7 @@ static int dd_wls_EvaluatorNode_verify_table(flatcc_table_verifier_descriptor_t int ret; if ((ret = flatcc_verify_string_field(td, 0, 0) /* description */)) return ret; if ((ret = flatcc_verify_union_field(td, 2, 0, &dd_wls_EvaluatorType_union_verifier) /* eval */)) return ret; + if ((ret = flatcc_verify_string_field(td, 3, 0) /* rule_id */)) return ret; return flatcc_verify_ok; } diff --git a/c/src/test/test_evaluator.c b/c/src/test/test_evaluator.c index c4c3a1c..93cb552 100644 --- a/c/src/test/test_evaluator.c +++ b/c/src/test/test_evaluator.c @@ -614,7 +614,9 @@ UTEST(evaluator, test_node_evaluator_basic_functionality) { &b, dd_wls_StringEvaluators_RUNTIME_ENTRY_POINT_JAR, dd_wls_CmpTypeSTR_CMP_EXACT, flatbuffers_string_create_str(&b, "test.jar") ); - dd_wls_EvaluatorNode_create_as_root(&b, str, dd_wls_EvaluatorType_as_StrEvaluator(str)); + dd_wls_EvaluatorNode_create_as_root( + &b, str, dd_wls_EvaluatorType_as_StrEvaluator(str), flatbuffers_string_create_str(&b, "") + ); void *buf = flatcc_builder_finalize_buffer(&b, &sz); dd_wls_EvaluatorNode_table_t eval = dd_wls_EvaluatorNode_as_root(buf); @@ -634,7 +636,9 @@ UTEST(evaluator, test_node_evaluator_basic_functionality) { dd_wls_NumEvaluator_ref_t num = dd_wls_NumEvaluator_create(&b, dd_wls_NumericEvaluators_JAVA_HEAP, dd_wls_CmpTypeNUM_CMP_EQ, 100); - dd_wls_EvaluatorNode_create_as_root(&b, num, dd_wls_EvaluatorType_as_NumEvaluator(num)); + dd_wls_EvaluatorNode_create_as_root( + &b, num, dd_wls_EvaluatorType_as_NumEvaluator(num), flatbuffers_string_create_str(&b, "") + ); buf = flatcc_builder_finalize_buffer(&b, &sz); eval = dd_wls_EvaluatorNode_as_root(buf); @@ -653,7 +657,9 @@ UTEST(evaluator, test_node_evaluator_basic_functionality) { dd_wls_UNumEvaluator_ref_t unum = dd_wls_UNumEvaluator_create(&b, dd_wls_NumericEvaluators_RUNTIME_VERSION_MINOR, dd_wls_CmpTypeNUM_CMP_EQ, 4); - dd_wls_EvaluatorNode_create_as_root(&b, unum, dd_wls_EvaluatorType_as_UNumEvaluator(unum)); + dd_wls_EvaluatorNode_create_as_root( + &b, unum, dd_wls_EvaluatorType_as_UNumEvaluator(unum), flatbuffers_string_create_str(&b, "") + ); buf = flatcc_builder_finalize_buffer(&b, &sz); eval = dd_wls_EvaluatorNode_as_root(buf); @@ -940,6 +946,29 @@ UTEST(evaluator, test_boolean_operations_distributivity) { ASSERT_EQ(left, right); } +UTEST(evaluator, matched_rule_id_starts_null_and_round_trips) { + /* Newly-initialized contexts must have no matched rule. */ + int rc = plcs_eval_ctx_init(); + ASSERT_TRUE(rc == PLCS_ESUCCESS || rc == PLCS_EINITIZLIED); + plcs_eval_ctx_reset(); + ASSERT_TRUE(plcs_eval_ctx_get_matched_rule_id() == NULL); + + /* Setter stores by pointer (no copy). */ + static const char rule[] = "java8_version"; + plcs_eval_ctx_set_matched_rule_id(rule); + ASSERT_STREQ(rule, plcs_eval_ctx_get_matched_rule_id()); + + /* Setting NULL clears it (so callers can detect "no rule matched"). */ + plcs_eval_ctx_set_matched_rule_id(NULL); + ASSERT_TRUE(plcs_eval_ctx_get_matched_rule_id() == NULL); + + /* plcs_eval_ctx_reset must also clear it so a fresh evaluation cycle + never inherits a stale id from a previous run. */ + plcs_eval_ctx_set_matched_rule_id(rule); + plcs_eval_ctx_reset(); + ASSERT_TRUE(plcs_eval_ctx_get_matched_rule_id() == NULL); +} + UTEST(evaluator, test_extern_declarations_working) { /* Simple test to verify that extern declarations are working */ /* This test calls the functions to ensure they can be linked */ diff --git a/fbs-schema/nodes.fbs b/fbs-schema/nodes.fbs index ae3f362..41887ff 100644 --- a/fbs-schema/nodes.fbs +++ b/fbs-schema/nodes.fbs @@ -29,9 +29,14 @@ table NodeTypeWrapper { table CompositeNode { description: string; op: BoolOperation = BOOL_UNKNOWN; - /// At some point we will switch back to; children: [NodeType]; + /// At some point we will switch back to; children: [NodeType]; /// (union vectors are not supported in GO so we are wrapping the table in a table) children: [NodeTypeWrapper]; + /// Stable identifier for the rule this node is the root of. Set only on + /// nodes that sit directly under a policy's top-level OR (one per rule); + /// empty/unset on all other nodes. Read by the engine to surface "which + /// rule fired" to action callbacks via plcs_eval_ctx_get_matched_rule_id(). + rule_id: string; } /// Represents a leaf node in the policy tree. @@ -41,4 +46,8 @@ table EvaluatorNode { description: string; /// The evaluator is a union of different evaluator types (String, Numeric, etc.). eval: EvaluatorType; + /// Stable identifier for the rule this node is the root of. Same semantics + /// as CompositeNode.rule_id: set only when this evaluator is itself the + /// root of a rule (single-condition rule), empty otherwise. + rule_id: string; } diff --git a/go/cmd/dd-requirements-converter/converter/arguments.go b/go/cmd/dd-requirements-converter/converter/arguments.go index cb0722f..61ef670 100644 --- a/go/cmd/dd-requirements-converter/converter/arguments.go +++ b/go/cmd/dd-requirements-converter/converter/arguments.go @@ -66,7 +66,7 @@ func wildcardMatchToEvaluators(builder *flatbuffers.Builder, pattern string, pos } strEvaluator := schema.StrEvaluatorCreate(builder, ev, pattern, cmp) - node := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, "Argument matching: "+pattern, strEvaluator) + node := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, "Argument matching: "+pattern, strEvaluator, "") return schema.NodeTypeWrapperCreate(builder, node, wls.NodeTypeEvaluatorNode), nil } @@ -117,6 +117,6 @@ func (a ArgumentList) ConvertToWLS(builder *flatbuffers.Builder) (flatbuffers.UO // EvaluatorNode: arg matches "-version" at position 1 // EvaluatorNode: arg matches "1.*" at position 2 // ) - andNode := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, "Match argument pattern: "+strings.Join(a.Arguments, " "), nodes) + andNode := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, "Match argument pattern: "+strings.Join(a.Arguments, " "), nodes, "") return schema.NodeTypeWrapperCreate(builder, andNode, wls.NodeTypeCompositeNode), nil } diff --git a/go/cmd/dd-requirements-converter/converter/commands.go b/go/cmd/dd-requirements-converter/converter/commands.go index b38b048..0fd9ce5 100644 --- a/go/cmd/dd-requirements-converter/converter/commands.go +++ b/go/cmd/dd-requirements-converter/converter/commands.go @@ -24,6 +24,6 @@ func (c CmdPattern) ConvertToWLS(builder *flatbuffers.Builder) (flatbuffers.UOff matcher = wls.CmpTypeSTRCMP_EXACT } strEvaluator := schema.StrEvaluatorCreate(builder, wls.StringEvaluatorsPROCESS_EXE_FULL_PATH, pattern, matcher) - node := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, "Path matching: "+pattern, strEvaluator) + node := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, "Path matching: "+pattern, strEvaluator, "") return schema.NodeTypeWrapperCreate(builder, node, wls.NodeTypeEvaluatorNode), nil } diff --git a/go/cmd/dd-requirements-converter/converter/deny.go b/go/cmd/dd-requirements-converter/converter/deny.go index 844cff9..09b5c19 100644 --- a/go/cmd/dd-requirements-converter/converter/deny.go +++ b/go/cmd/dd-requirements-converter/converter/deny.go @@ -23,7 +23,12 @@ func isValidOS(os string) bool { return os == "windows" || os == "linux" || os == "darwin" } -func (d JSONDeny) ConvertToWLS(builder *flatbuffers.Builder) (flatbuffers.UOffsetT, error) { +// ConvertToWLS builds the rule's node tree. ruleID is the stable identifier +// for this deny rule; it's stamped on the rule's root composite so the engine +// can surface it via plcs_eval_ctx_get_matched_rule_id(). The root is always +// a BOOL_AND composite (even for single-condition rules) so the rule_id has +// a uniform place to live without depending on the rule's internal shape. +func (d JSONDeny) ConvertToWLS(builder *flatbuffers.Builder, ruleID string) (flatbuffers.UOffsetT, error) { var nodes []flatbuffers.UOffsetT if d.Os == "" && len(d.Cmds) == 0 && len(d.Args) == 0 && len(d.Envs) == 0 { @@ -32,7 +37,7 @@ func (d JSONDeny) ConvertToWLS(builder *flatbuffers.Builder) (flatbuffers.UOffse if d.Os != "" && isValidOS(d.Os) { osEval := schema.StrEvaluatorCreate(builder, wls.StringEvaluatorsOS, d.Os, wls.CmpTypeSTRCMP_EXACT) - osNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, "OS matching", osEval) + osNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, "OS matching", osEval, "") nodes = append(nodes, schema.NodeTypeWrapperCreate(builder, osNode, wls.NodeTypeEvaluatorNode)) } @@ -49,7 +54,7 @@ func (d JSONDeny) ConvertToWLS(builder *flatbuffers.Builder) (flatbuffers.UOffse if len(cmdNodes) == 1 { nodes = append(nodes, cmdNodes[0]) } else if len(cmdNodes) > 1 { - orNode := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_OR, "Match any cmd pattern", cmdNodes) + orNode := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_OR, "Match any cmd pattern", cmdNodes, "") nodes = append(nodes, schema.NodeTypeWrapperCreate(builder, orNode, wls.NodeTypeCompositeNode)) } @@ -67,7 +72,7 @@ func (d JSONDeny) ConvertToWLS(builder *flatbuffers.Builder) (flatbuffers.UOffse if len(argNodes) == 1 { nodes = append(nodes, argNodes[0]) } else if len(argNodes) > 1 { - andNode := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, "Match all argument patterns", argNodes) + andNode := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, "Match all argument patterns", argNodes, "") nodes = append(nodes, schema.NodeTypeWrapperCreate(builder, andNode, wls.NodeTypeCompositeNode)) } @@ -89,25 +94,20 @@ func (d JSONDeny) ConvertToWLS(builder *flatbuffers.Builder) (flatbuffers.UOffse } strEvaluator := schema.StrEvaluatorCreate(builder, wls.StringEvaluatorsPROCESS_ENVAR, kv, comparator) - node := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, "Environment variable matching: "+kv, strEvaluator) + node := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, "Environment variable matching: "+kv, strEvaluator, "") envNodes = append(envNodes, schema.NodeTypeWrapperCreate(builder, node, wls.NodeTypeEvaluatorNode)) } if len(envNodes) == 1 { nodes = append(nodes, envNodes[0]) } else if len(envNodes) > 1 { - andNode := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, d.Description, envNodes) + andNode := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, d.Description, envNodes, "") nodes = append(nodes, schema.NodeTypeWrapperCreate(builder, andNode, wls.NodeTypeCompositeNode)) } - var root flatbuffers.UOffsetT - // if there is only one node, use it directly (it's already a NodeTypeWrapper) - if len(nodes) == 1 { - root = nodes[0] - } else if len(nodes) > 1 { - andNode := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, d.Description, nodes) - root = schema.NodeTypeWrapperCreate(builder, andNode, wls.NodeTypeCompositeNode) - } - - return root, nil + // Always emit an outer AND composite as the rule root. For multi-condition + // rules this is the natural shape; for single-condition rules it's a + // one-child AND (semantic no-op) that gives the rule_id a uniform home. + andNode := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, d.Description, nodes, ruleID) + return schema.NodeTypeWrapperCreate(builder, andNode, wls.NodeTypeCompositeNode), nil } diff --git a/go/cmd/dd-requirements-converter/converter/libc.go b/go/cmd/dd-requirements-converter/converter/libc.go index 4a46224..7214072 100644 --- a/go/cmd/dd-requirements-converter/converter/libc.go +++ b/go/cmd/dd-requirements-converter/converter/libc.go @@ -50,7 +50,12 @@ func (rv *RequiredVersion) UnmarshalJSON(data []byte) error { // - supported: true + version → DENY if arch+flavor match AND version < min // - supported: false + no version → DENY if arch+flavor match // - supported: false + version → DENY if arch+flavor match AND version >= min -func (l JSONlibc) ConvertToWLS(builder *flatbuffers.Builder, flavor string) (flatbuffers.UOffsetT, error) { +// ConvertToWLS builds the rule's node tree. ruleID is the stable identifier +// for this libc rule; it's stamped on the rule's root composite so the engine +// can surface it via plcs_eval_ctx_get_matched_rule_id(). Pass "" if this is +// being built as a sub-tree (no rule root), though in practice every libc +// rule's tree is a rule root. +func (l JSONlibc) ConvertToWLS(builder *flatbuffers.Builder, flavor string, ruleID string) (flatbuffers.UOffsetT, error) { // If supported and no version requirement, no policy needed (allowed by default) if l.IsSupported && l.RequiredMinVersion == nil { return 0, nil @@ -64,11 +69,11 @@ func (l JSONlibc) ConvertToWLS(builder *flatbuffers.Builder, flavor string) (fla return 0, errors.New("unknown architecture") } archEval := schema.StrEvaluatorCreate(builder, wls.StringEvaluatorsMACHINE_ARCHITECTURE, schema.MachineArchitectureToString[archEnum], wls.CmpTypeSTRCMP_EXACT) - archNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, "architecture matching", archEval) + archNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, "architecture matching", archEval, "") nodes = append(nodes, schema.NodeTypeWrapperCreate(builder, archNode, wls.NodeTypeEvaluatorNode)) flavorEval := schema.StrEvaluatorCreate(builder, wls.StringEvaluatorsLIBC_FLAVOR, flavor, wls.CmpTypeSTRCMP_EXACT) - flavorNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, "flavor matching", flavorEval) + flavorNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, "flavor matching", flavorEval, "") nodes = append(nodes, schema.NodeTypeWrapperCreate(builder, flavorNode, wls.NodeTypeEvaluatorNode)) if l.RequiredMinVersion != nil { @@ -103,8 +108,8 @@ func (l JSONlibc) ConvertToWLS(builder *flatbuffers.Builder, flavor string) (fla } } - // combine all evaluators with AND - composite := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, l.Description, nodes) + // combine all evaluators with AND; the rule root carries the stable ruleID + composite := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, l.Description, nodes, ruleID) return schema.NodeTypeWrapperCreate(builder, composite, wls.NodeTypeCompositeNode), nil } @@ -112,36 +117,36 @@ func (l JSONlibc) ConvertToWLS(builder *flatbuffers.Builder, flavor string) (fla func buildVersionLessThanOrEqual(builder *flatbuffers.Builder, major, minor, patch int) flatbuffers.UOffsetT { // case 1: major < minMajor majorLtEval := schema.NumEvaluatorCreate(builder, wls.NumericEvaluatorsLIBC_VERSION_MAJOR, int64(major), wls.CmpTypeNUMCMP_LT) - majorLtNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "major version <", majorLtEval) + majorLtNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "major version <", majorLtEval, "") case1 := schema.NodeTypeWrapperCreate(builder, majorLtNode, wls.NodeTypeEvaluatorNode) // major == minMajor (used in Case 2 and Case 3) majorEqEval := schema.NumEvaluatorCreate(builder, wls.NumericEvaluatorsLIBC_VERSION_MAJOR, int64(major), wls.CmpTypeNUMCMP_EQ) - majorEqNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "major version ==", majorEqEval) + majorEqNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "major version ==", majorEqEval, "") majorEqWrapper := schema.NodeTypeWrapperCreate(builder, majorEqNode, wls.NodeTypeEvaluatorNode) // case 2: major == minMajor AND minor < minMinor minorLteEval := schema.NumEvaluatorCreate(builder, wls.NumericEvaluatorsLIBC_VERSION_MINOR, int64(minor), wls.CmpTypeNUMCMP_LTE) - minorLteNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "minor version <=", minorLteEval) + minorLteNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "minor version <=", minorLteEval, "") minorLteWrapper := schema.NodeTypeWrapperCreate(builder, minorLteNode, wls.NodeTypeEvaluatorNode) - case2And := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, "major == && minor <=", []flatbuffers.UOffsetT{majorEqWrapper, minorLteWrapper}) + case2And := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, "major == && minor <=", []flatbuffers.UOffsetT{majorEqWrapper, minorLteWrapper}, "") case2 := schema.NodeTypeWrapperCreate(builder, case2And, wls.NodeTypeCompositeNode) // case 3: major == minMajor AND minor == minMinor AND patch < minPatch minorEqEval := schema.NumEvaluatorCreate(builder, wls.NumericEvaluatorsLIBC_VERSION_MINOR, int64(minor), wls.CmpTypeNUMCMP_EQ) - minorEqNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "minor version ==", minorEqEval) + minorEqNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "minor version ==", minorEqEval, "") minorEqWrapper := schema.NodeTypeWrapperCreate(builder, minorEqNode, wls.NodeTypeEvaluatorNode) patchLteEval := schema.NumEvaluatorCreate(builder, wls.NumericEvaluatorsLIBC_VERSION_PATCH, int64(patch), wls.CmpTypeNUMCMP_LTE) - patchLteNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "patch version <=", patchLteEval) + patchLteNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "patch version <=", patchLteEval, "") patchLteWrapper := schema.NodeTypeWrapperCreate(builder, patchLteNode, wls.NodeTypeEvaluatorNode) - case3And := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, "major == && minor == && patch <=", []flatbuffers.UOffsetT{majorEqWrapper, minorEqWrapper, patchLteWrapper}) + case3And := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, "major == && minor == && patch <=", []flatbuffers.UOffsetT{majorEqWrapper, minorEqWrapper, patchLteWrapper}, "") case3 := schema.NodeTypeWrapperCreate(builder, case3And, wls.NodeTypeCompositeNode) // combine all cases with OR - versionOr := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_OR, "version <= min", []flatbuffers.UOffsetT{case1, case2, case3}) + versionOr := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_OR, "version <= min", []flatbuffers.UOffsetT{case1, case2, case3}, "") return schema.NodeTypeWrapperCreate(builder, versionOr, wls.NodeTypeCompositeNode) } @@ -149,35 +154,35 @@ func buildVersionLessThanOrEqual(builder *flatbuffers.Builder, major, minor, pat func buildVersionGreaterThan(builder *flatbuffers.Builder, major, minor, patch int) flatbuffers.UOffsetT { // case 1: major > minMajor majorGtEval := schema.NumEvaluatorCreate(builder, wls.NumericEvaluatorsLIBC_VERSION_MAJOR, int64(major), wls.CmpTypeNUMCMP_GT) - majorGtNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "major version >", majorGtEval) + majorGtNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "major version >", majorGtEval, "") case1 := schema.NodeTypeWrapperCreate(builder, majorGtNode, wls.NodeTypeEvaluatorNode) // major == minMajor (used in Case 2 and Case 3) majorEqEval := schema.NumEvaluatorCreate(builder, wls.NumericEvaluatorsLIBC_VERSION_MAJOR, int64(major), wls.CmpTypeNUMCMP_EQ) - majorEqNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "major version ==", majorEqEval) + majorEqNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "major version ==", majorEqEval, "") majorEqWrapper := schema.NodeTypeWrapperCreate(builder, majorEqNode, wls.NodeTypeEvaluatorNode) // case 2: major == minMajor AND minor > minMinor minorGtEval := schema.NumEvaluatorCreate(builder, wls.NumericEvaluatorsLIBC_VERSION_MINOR, int64(minor), wls.CmpTypeNUMCMP_GT) - minorGtNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "minor version >", minorGtEval) + minorGtNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "minor version >", minorGtEval, "") minorGtWrapper := schema.NodeTypeWrapperCreate(builder, minorGtNode, wls.NodeTypeEvaluatorNode) - case2And := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, "major == && minor >", []flatbuffers.UOffsetT{majorEqWrapper, minorGtWrapper}) + case2And := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, "major == && minor >", []flatbuffers.UOffsetT{majorEqWrapper, minorGtWrapper}, "") case2 := schema.NodeTypeWrapperCreate(builder, case2And, wls.NodeTypeCompositeNode) // case 3: major == minMajor AND minor == minMinor AND patch >= minPatch minorEqEval := schema.NumEvaluatorCreate(builder, wls.NumericEvaluatorsLIBC_VERSION_MINOR, int64(minor), wls.CmpTypeNUMCMP_EQ) - minorEqNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "minor version ==", minorEqEval) + minorEqNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "minor version ==", minorEqEval, "") minorEqWrapper := schema.NodeTypeWrapperCreate(builder, minorEqNode, wls.NodeTypeEvaluatorNode) patchGtEval := schema.NumEvaluatorCreate(builder, wls.NumericEvaluatorsLIBC_VERSION_PATCH, int64(patch), wls.CmpTypeNUMCMP_GT) - patchGtNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "patch version >", patchGtEval) + patchGtNode := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeNumEvaluator, "patch version >", patchGtEval, "") patchGtWrapper := schema.NodeTypeWrapperCreate(builder, patchGtNode, wls.NodeTypeEvaluatorNode) - case3And := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, "major == && minor == && patch >", []flatbuffers.UOffsetT{majorEqWrapper, minorEqWrapper, patchGtWrapper}) + case3And := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, "major == && minor == && patch >", []flatbuffers.UOffsetT{majorEqWrapper, minorEqWrapper, patchGtWrapper}, "") case3 := schema.NodeTypeWrapperCreate(builder, case3And, wls.NodeTypeCompositeNode) // combine all cases with OR - versionOr := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_OR, "version > min", []flatbuffers.UOffsetT{case1, case2, case3}) + versionOr := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_OR, "version > min", []flatbuffers.UOffsetT{case1, case2, case3}, "") return schema.NodeTypeWrapperCreate(builder, versionOr, wls.NodeTypeCompositeNode) } diff --git a/go/cmd/dd-requirements-converter/converter/requirements.go b/go/cmd/dd-requirements-converter/converter/requirements.go index 5fca9e9..b0a752e 100644 --- a/go/cmd/dd-requirements-converter/converter/requirements.go +++ b/go/cmd/dd-requirements-converter/converter/requirements.go @@ -21,44 +21,75 @@ type JSONNativeDeps struct { Musl []JSONlibc `json:"musl"` } +// ConvertToWLS produces a single Policy whose rules tree is a top-level OR over +// all source rules — preserving the engine's short-circuit on OR so that +// languages with many rules (Ruby has 251) don't pay an O(n) cost on every +// process startup. +// +// Each rule's root node carries a stable rule_id (see CompositeNode.rule_id / +// EvaluatorNode.rule_id in nodes.fbs). When the top-level OR matches a rule, +// the engine reads that rule_id (see composite_evaluator in +// dd-policy-engine/c/src/evaluator.c) and exposes it via +// plcs_eval_ctx_get_matched_rule_id, so the consuming action callback can +// surface "which rule fired" in telemetry. func (r JSONRequirements) ConvertToWLS(builder *flatbuffers.Builder) (flatbuffers.UOffsetT, error) { - var rules []flatbuffers.UOffsetT + var ruleSubtrees []flatbuffers.UOffsetT fmt.Printf("Converting %d deny rules\n", len(r.Deny)) for _, denyRule := range r.Deny { - denyNode, err := denyRule.ConvertToWLS(builder) + if denyRule.Id == "" { + return 0, fmt.Errorf("deny rule has no id; cannot identify it in telemetry") + } + denyNode, err := denyRule.ConvertToWLS(builder, denyRule.Id) if err != nil { return 0, err } - rules = append(rules, denyNode) + ruleSubtrees = append(ruleSubtrees, denyNode) } fmt.Printf("Converting %d glibc requirements\n", len(r.NativeDeps.Glibc)) for _, glibc := range r.NativeDeps.Glibc { - glibcNode, err := glibc.ConvertToWLS(builder, "glibc") + glibcNode, err := glibc.ConvertToWLS(builder, "glibc", libcRuleID("glibc", glibc)) if err != nil { return 0, err } - if glibcNode != 0 { - rules = append(rules, glibcNode) + if glibcNode == 0 { + continue } + ruleSubtrees = append(ruleSubtrees, glibcNode) } fmt.Printf("Converting %d musl requirements\n", len(r.NativeDeps.Musl)) for _, musl := range r.NativeDeps.Musl { - muslNode, err := musl.ConvertToWLS(builder, "musl") + muslNode, err := musl.ConvertToWLS(builder, "musl", libcRuleID("musl", musl)) if err != nil { return 0, err } - if muslNode != 0 { - rules = append(rules, muslNode) + if muslNode == 0 { + continue } + ruleSubtrees = append(ruleSubtrees, muslNode) } - composite := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_OR, "requirements", rules) + composite := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_OR, "requirements", ruleSubtrees, "") compositeNode := schema.NodeTypeWrapperCreate(builder, composite, wls.NodeTypeCompositeNode) action := schema.ActionCreate(builder, wls.ActionIdINJECT_DENY, "requirements", nil) policy := schema.PolicyCreate(builder, "All requirements", compositeNode, []flatbuffers.UOffsetT{action}) return schema.PoliciesCreate(builder, []flatbuffers.UOffsetT{policy}), nil } + +// libcRuleID synthesizes a stable, bounded-cardinality identifier for a libc +// requirement (JSONlibc has no `id` field). The shape is +// "libc__[_min_|_unsupported[_above_]]" so downstream +// telemetry can distinguish e.g. "glibc x86_64 < 2.17" from "musl arm64 +// entirely unsupported" without parsing the rule body. +func libcRuleID(flavor string, l JSONlibc) string { + if l.RequiredMinVersion != nil { + if l.IsSupported { + return fmt.Sprintf("libc_%s_%s_below_min_%s", flavor, l.Arch, l.RequiredMinVersion.String()) + } + return fmt.Sprintf("libc_%s_%s_unsupported_at_or_above_%s", flavor, l.Arch, l.RequiredMinVersion.String()) + } + return fmt.Sprintf("libc_%s_%s_unsupported", flavor, l.Arch) +} diff --git a/go/cmd/dd-requirements-converter/main_test.go b/go/cmd/dd-requirements-converter/main_test.go index c66112f..cf2f2d3 100644 --- a/go/cmd/dd-requirements-converter/main_test.go +++ b/go/cmd/dd-requirements-converter/main_test.go @@ -274,7 +274,7 @@ func TestJSONlibc_ConvertToWLS(t *testing.T) { } builder := flatbuffers.NewBuilder(1024) - offset, err := libc.ConvertToWLS(builder, tt.flavor) + offset, err := libc.ConvertToWLS(builder, tt.flavor, "test_rule_id") if err != nil { t.Fatalf("ConvertToWLS failed: %v", err) } @@ -454,28 +454,34 @@ func TestJSONDeny_ConvertToWLS(t *testing.T) { inputJSON string expectedRoot expectedNode }{ + // JSONDeny.ConvertToWLS now always emits a BOOL_AND composite as the + // rule root (so the rule_id has a uniform home regardless of how many + // conditions are present). Even single-condition rules get wrapped in + // a one-child AND. Tests below reflect that shape. { // OS only: {"os": "linux"} - // → StrEvaluator(OS, EXACT, "linux") + // → AND(StrEvaluator(OS, EXACT, "linux")) name: "os only", inputJSON: `{"os": "linux", "description": "deny linux"}`, - expectedRoot: strEval(wls.StringEvaluatorsOS, "linux"), + expectedRoot: andNode(strEval(wls.StringEvaluatorsOS, "linux")), }, { // Single cmd: {"cmds": ["/usr/bin/curl"]} - // → StrEvaluator(PROCESS_EXE_FULL_PATH, EXACT, "/usr/bin/curl") + // → AND(StrEvaluator(PROCESS_EXE_FULL_PATH, EXACT, "/usr/bin/curl")) name: "single exact command", inputJSON: `{"cmds": ["/usr/bin/curl"], "description": "deny curl"}`, - expectedRoot: strEval(wls.StringEvaluatorsPROCESS_EXE_FULL_PATH, "/usr/bin/curl"), + expectedRoot: andNode(strEval(wls.StringEvaluatorsPROCESS_EXE_FULL_PATH, "/usr/bin/curl")), }, { // Multiple cmds: {"cmds": ["/usr/bin/curl", "/usr/bin/wget"]} - // → OR(cmd1, cmd2) + // → AND(OR(cmd1, cmd2)) name: "multiple commands - OR", inputJSON: `{"cmds": ["/usr/bin/curl", "/usr/bin/wget"], "description": "deny download tools"}`, - expectedRoot: orNode( - strEval(wls.StringEvaluatorsPROCESS_EXE_FULL_PATH, "/usr/bin/curl"), - strEval(wls.StringEvaluatorsPROCESS_EXE_FULL_PATH, "/usr/bin/wget"), + expectedRoot: andNode( + orNode( + strEval(wls.StringEvaluatorsPROCESS_EXE_FULL_PATH, "/usr/bin/curl"), + strEval(wls.StringEvaluatorsPROCESS_EXE_FULL_PATH, "/usr/bin/wget"), + ), ), }, { @@ -490,33 +496,33 @@ func TestJSONDeny_ConvertToWLS(t *testing.T) { }, { // Single env var: {"envars": {"DEBUG": "1"}} - // → StrEvaluator(PROCESS_ENVAR, EXACT, "DEBUG=1") + // → AND(StrEvaluator(PROCESS_ENVAR, EXACT, "DEBUG=1")) name: "single environment variable", inputJSON: `{"envars": {"DEBUG": "1"}, "description": "deny debug mode"}`, - expectedRoot: strEval(wls.StringEvaluatorsPROCESS_ENVAR, "DEBUG=1"), + expectedRoot: andNode(strEval(wls.StringEvaluatorsPROCESS_ENVAR, "DEBUG=1")), }, { name: "environment variable wildcard asterisk in value", inputJSON: `{"envars": {"PATH": "/usr/*/bin"}, "description": "deny path pattern"}`, - expectedRoot: strEval(wls.StringEvaluatorsPROCESS_ENVAR, "PATH=/usr/*/bin", wls.CmpTypeSTRCMP_WILDCARD), + expectedRoot: andNode(strEval(wls.StringEvaluatorsPROCESS_ENVAR, "PATH=/usr/*/bin", wls.CmpTypeSTRCMP_WILDCARD)), }, { name: "environment variable wildcard question in value", inputJSON: `{"envars": {"TERM": "xterm-?56color"}, "description": "deny term pattern"}`, - expectedRoot: strEval(wls.StringEvaluatorsPROCESS_ENVAR, "TERM=xterm-?56color", wls.CmpTypeSTRCMP_WILDCARD), + expectedRoot: andNode(strEval(wls.StringEvaluatorsPROCESS_ENVAR, "TERM=xterm-?56color", wls.CmpTypeSTRCMP_WILDCARD)), }, { // JSON null value → KEY=*? + CMP_WILDCARD ("any non-empty value" for KEY=) name: "environment variable null value matches non-empty only", inputJSON: `{"envars": {"FOO": null}, "description": "deny when FOO set to any non-empty value"}`, - expectedRoot: strEval(wls.StringEvaluatorsPROCESS_ENVAR, "FOO=*?", wls.CmpTypeSTRCMP_WILDCARD), + expectedRoot: andNode(strEval(wls.StringEvaluatorsPROCESS_ENVAR, "FOO=*?", wls.CmpTypeSTRCMP_WILDCARD)), }, { // Single arg: {"args": [{"args": ["-rf"]}]} - // → StrEvaluator(PROCESS_ARGV, EXACT, "-rf") + // → AND(StrEvaluator(PROCESS_ARGV, EXACT, "-rf")) name: "single argument", inputJSON: `{"args": [{"args": ["-rf"]}], "description": "deny -rf flag"}`, - expectedRoot: strEval(wls.StringEvaluatorsPROCESS_ARGV, "-rf"), + expectedRoot: andNode(strEval(wls.StringEvaluatorsPROCESS_ARGV, "-rf")), }, } @@ -528,7 +534,7 @@ func TestJSONDeny_ConvertToWLS(t *testing.T) { } builder := flatbuffers.NewBuilder(1024) - offset, err := deny.ConvertToWLS(builder) + offset, err := deny.ConvertToWLS(builder, "test_rule_id") if err != nil { t.Fatalf("ConvertToWLS failed: %v", err) } @@ -598,16 +604,19 @@ func TestParseRequirementsJSON(t *testing.T) { func TestJSONRequirements_ConvertToWLS(t *testing.T) { tests := []struct { - name string - inputJSON string - wantUnmarshalErr bool - wantVersionErr bool - expectedRuleCount int // when conversion runs: rules ORed together in the single policy + name string + inputJSON string + wantUnmarshalErr bool + wantVersionErr bool + wantConvertErr bool + // Expected rule-id wrappers (description of each top-level OR child), + // in conversion order: deny rules first, then glibc, then musl. + expectedRuleIDs []string }{ { - name: "version one only", - inputJSON: `{"version":1}`, - expectedRuleCount: 0, + name: "version one only", + inputJSON: `{"version":1}`, + expectedRuleIDs: nil, }, { name: "version zero", @@ -630,26 +639,40 @@ func TestJSONRequirements_ConvertToWLS(t *testing.T) { wantVersionErr: true, }, { - name: "empty requirements", - inputJSON: `{}`, + name: "empty requirements", + inputJSON: `{}`, wantVersionErr: true, }, { - name: "no native_deps and no deny", - inputJSON: `{"version":1,"native_deps":{},"deny":[]}`, - expectedRuleCount: 0, + name: "no native_deps and no deny", + inputJSON: `{"version":1,"native_deps":{},"deny":[]}`, + expectedRuleIDs: nil, }, { name: "glibc + musl + deny combined", inputJSON: `{ "version": 1, - "deny": [{"os": "windows", "description": "no windows"}], + "deny": [{"id": "no_windows", "os": "windows", "description": "no windows"}], "native_deps": { "glibc": [{"arch": "x64", "supported": true, "min": "2.17"}], "musl": [{"arch": "arm64", "supported": false}] } }`, - expectedRuleCount: 3, // 1 deny + 1 glibc + 1 musl ORed together + expectedRuleIDs: []string{ + "no_windows", + // hashicorp/go-version normalizes "2.17" → "2.17.0"; this is fine for + // telemetry as long as the string is deterministic. + "libc_glibc_x64_below_min_2.17.0", + "libc_musl_arm64_unsupported", + }, + }, + { + name: "deny rule without id is rejected", + inputJSON: `{ + "version": 1, + "deny": [{"os": "windows", "description": "no windows"}] + }`, + wantConvertErr: true, }, } @@ -680,6 +703,12 @@ func TestJSONRequirements_ConvertToWLS(t *testing.T) { builder := flatbuffers.NewBuilder(1024) offset, err := req.ConvertToWLS(builder) + if tt.wantConvertErr { + if err == nil { + t.Fatal("expected ConvertToWLS error, got nil") + } + return + } if err != nil { t.Fatalf("ConvertToWLS failed: %v", err) } @@ -687,10 +716,10 @@ func TestJSONRequirements_ConvertToWLS(t *testing.T) { builder.Finish(offset) policies := wls.GetRootAsPolicies(builder.FinishedBytes(), 0) - // Always 1 policy now (all rules ORed together) + // Still exactly one policy (the engine's OR short-circuit fires only + // inside a single Policy's rules tree, so we must keep this shape). if policies.PoliciesLength() != 1 { - t.Errorf("Expected 1 policy, got %d", policies.PoliciesLength()) - return + t.Fatalf("Expected 1 policy, got %d", policies.PoliciesLength()) } var policy wls.Policy @@ -700,10 +729,13 @@ func TestJSONRequirements_ConvertToWLS(t *testing.T) { rules := policy.Rules(nil) if rules == nil { - t.Fatal("Expected rules node, got nil") + if len(tt.expectedRuleIDs) > 0 { + t.Fatal("Expected rules node, got nil") + } + return } - // The root should be an OR node with the expected number of children + // Top-level rules tree is an OR composite of rule-id-bearing wrappers. if rules.NodeType() != wls.NodeTypeCompositeNode { t.Fatalf("Expected composite node, got %s", rules.NodeType().String()) } @@ -713,15 +745,48 @@ func TestJSONRequirements_ConvertToWLS(t *testing.T) { t.Fatal("Failed to get node table") } - var composite wls.CompositeNode - composite.Init(table.Bytes, table.Pos) + var topLevelOR wls.CompositeNode + topLevelOR.Init(table.Bytes, table.Pos) - if composite.Op() != wls.BoolOperationBOOL_OR { - t.Errorf("Expected OR operation, got %s", composite.Op().String()) + if topLevelOR.Op() != wls.BoolOperationBOOL_OR { + t.Errorf("Expected top-level OR, got %s", topLevelOR.Op().String()) } - if composite.ChildrenLength() != tt.expectedRuleCount { - t.Errorf("Expected %d rules, got %d", tt.expectedRuleCount, composite.ChildrenLength()) + if topLevelOR.ChildrenLength() != len(tt.expectedRuleIDs) { + t.Errorf("Expected %d rule wrappers, got %d", len(tt.expectedRuleIDs), topLevelOR.ChildrenLength()) + return + } + + // Each OR child is the rule's root node, carrying its stable + // rule_id on the schema's rule_id field (not on description). + for i, wantID := range tt.expectedRuleIDs { + var childWrapper wls.NodeTypeWrapper + if !topLevelOR.Children(&childWrapper, i) { + t.Errorf("OR child[%d]: failed to read", i) + continue + } + var childTable flatbuffers.Table + if !childWrapper.Node(&childTable) { + t.Errorf("OR child[%d]: failed to get table", i) + continue + } + var gotID string + switch childWrapper.NodeType() { + case wls.NodeTypeCompositeNode: + var c wls.CompositeNode + c.Init(childTable.Bytes, childTable.Pos) + gotID = string(c.RuleId()) + case wls.NodeTypeEvaluatorNode: + var e wls.EvaluatorNode + e.Init(childTable.Bytes, childTable.Pos) + gotID = string(e.RuleId()) + default: + t.Errorf("OR child[%d]: unexpected node type %s", i, childWrapper.NodeType().String()) + continue + } + if gotID != wantID { + t.Errorf("OR child[%d]: expected rule_id %q, got %q", i, wantID, gotID) + } } }) } diff --git a/go/cmd/dd-rules-converter/main.go b/go/cmd/dd-rules-converter/main.go index 6552142..3553603 100644 --- a/go/cmd/dd-rules-converter/main.go +++ b/go/cmd/dd-rules-converter/main.go @@ -136,7 +136,7 @@ func validateRule(meta toml.MetaData, ruleId string) error { func createStrEvaluatorNode(builder *flatbuffers.Builder, evaluatorId wls.StringEvaluators, value string, cmpp wls.CmpTypeSTR, description string) flatbuffers.UOffsetT { evaluator := schema.StrEvaluatorCreate(builder, evaluatorId, value, cmpp) - node := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, description, evaluator) + node := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, description, evaluator, "") return schema.NodeTypeWrapperCreate(builder, node, wls.NodeTypeEvaluatorNode) } @@ -172,7 +172,7 @@ func createNode(builder *flatbuffers.Builder, node *parser.TermNode) (flatbuffer } func createConditionalNode(builder *flatbuffers.Builder, oper wls.BoolOperation, description string, nodes []flatbuffers.UOffsetT) flatbuffers.UOffsetT { - nodeRoot := schema.CompositeNodeCreate(builder, oper, description, nodes) + nodeRoot := schema.CompositeNodeCreate(builder, oper, description, nodes, "") return schema.NodeTypeWrapperCreate(builder, nodeRoot, wls.NodeTypeCompositeNode) } diff --git a/go/examples/example_generate_c_header_buffer/main.go b/go/examples/example_generate_c_header_buffer/main.go index 1464ad7..d15bf2a 100644 --- a/go/examples/example_generate_c_header_buffer/main.go +++ b/go/examples/example_generate_c_header_buffer/main.go @@ -30,7 +30,7 @@ func writeBufferToFile(buffer []byte, fileName string) { func createStrEvaluatorNode(builder *flatbuffers.Builder, evaluatorId wls.StringEvaluators, value string, cmp wls.CmpTypeSTR, description string) flatbuffers.UOffsetT { evaluator := schema.StrEvaluatorCreate(builder, evaluatorId, value, cmp) - node := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, description, evaluator) + node := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, description, evaluator, "") return schema.NodeTypeWrapperCreate(builder, node, wls.NodeTypeEvaluatorNode) } diff --git a/go/examples/example_json_to_hardcoded_injector_policies/converter/condition.go b/go/examples/example_json_to_hardcoded_injector_policies/converter/condition.go index 100f3f1..7d9522e 100644 --- a/go/examples/example_json_to_hardcoded_injector_policies/converter/condition.go +++ b/go/examples/example_json_to_hardcoded_injector_policies/converter/condition.go @@ -33,6 +33,6 @@ func (c JSONCondition) ConvertToWLS(builder *flatbuffers.Builder) (flatbuffers.U nodes[i] = node } fmt.Printf("adding %d nodes\n", len(nodes)) - composite := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_OR, c.Description, nodes) + composite := schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_OR, c.Description, nodes, "") return schema.NodeTypeWrapperCreate(builder, composite, wls.NodeTypeCompositeNode), nil } diff --git a/go/examples/example_json_to_hardcoded_injector_policies/converter/path_values.go b/go/examples/example_json_to_hardcoded_injector_policies/converter/path_values.go index a1bd2e9..983e80a 100644 --- a/go/examples/example_json_to_hardcoded_injector_policies/converter/path_values.go +++ b/go/examples/example_json_to_hardcoded_injector_policies/converter/path_values.go @@ -57,7 +57,7 @@ func (v JSONPathValue) ConvertToWLS(builder *flatbuffers.Builder) (flatbuffers.U str_evaluator := schema.StrEvaluatorCreate(builder, JsonStrValueTypeToWls(v.Value_type), v.Value, JsonStrCompareStrategyToWls(v.Cmp_strategy)) - str_evaluator_node := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, strOrEmpty(&v.Description), str_evaluator) + str_evaluator_node := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, strOrEmpty(&v.Description), str_evaluator, "") return schema.NodeTypeWrapperCreate(builder, str_evaluator_node, wls.NodeTypeEvaluatorNode), nil diff --git a/go/examples/example_json_to_hardcoded_injector_policies/converter/policy.go b/go/examples/example_json_to_hardcoded_injector_policies/converter/policy.go index ff7e954..b648d03 100644 --- a/go/examples/example_json_to_hardcoded_injector_policies/converter/policy.go +++ b/go/examples/example_json_to_hardcoded_injector_policies/converter/policy.go @@ -36,11 +36,11 @@ func appendRuntimeRule(builder *flatbuffers.Builder, node flatbuffers.UOffsetT, str_evaluator := schema.StrEvaluatorCreate(builder, wls.StringEvaluatorsRUNTIME_LANGUAGE, runtime, wls.CmpTypeSTRCMP_EXACT) - str_evaluator_node := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, "Runtime matching", str_evaluator) + str_evaluator_node := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, "Runtime matching", str_evaluator, "") language_node := schema.NodeTypeWrapperCreate(builder, str_evaluator_node, wls.NodeTypeEvaluatorNode) - return schema.NodeTypeWrapperCreate(builder, schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, description, []flatbuffers.UOffsetT{language_node, node}), wls.NodeTypeCompositeNode), nil // Example for runtime condition, adjust as needed + return schema.NodeTypeWrapperCreate(builder, schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_AND, description, []flatbuffers.UOffsetT{language_node, node}, ""), wls.NodeTypeCompositeNode), nil // Example for runtime condition, adjust as needed } func (p JSONPolicy) ConvertToWLS(builder *flatbuffers.Builder) (flatbuffers.UOffsetT, error) { @@ -64,7 +64,7 @@ func (p JSONPolicy) ConvertToWLS(builder *flatbuffers.Builder) (flatbuffers.UOff } nodes = append(nodes, node) } - conditionsNode = schema.NodeTypeWrapperCreate(builder, schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_OR, p.Description, nodes), wls.NodeTypeCompositeNode) + conditionsNode = schema.NodeTypeWrapperCreate(builder, schema.CompositeNodeCreate(builder, wls.BoolOperationBOOL_OR, p.Description, nodes, ""), wls.NodeTypeCompositeNode) } if AddRuntimeCondition(p.Runtime) { diff --git a/go/examples/example_writer/main.go b/go/examples/example_writer/main.go index ae9862b..4557602 100644 --- a/go/examples/example_writer/main.go +++ b/go/examples/example_writer/main.go @@ -37,7 +37,7 @@ func writeBufferToFile(buffer []byte, fileName string) { func createStrEvaluatorNode(builder *flatbuffers.Builder, evaluatorId wls.StringEvaluators, value string, cmpp wls.CmpTypeSTR, description string) flatbuffers.UOffsetT { evaluator := schema.StrEvaluatorCreate(builder, evaluatorId, value, cmpp) - node := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, description, evaluator) + node := schema.EvaluatorNodeCreate(builder, wls.EvaluatorTypeStrEvaluator, description, evaluator, "") return schema.NodeTypeWrapperCreate(builder, node, wls.NodeTypeEvaluatorNode) } @@ -49,7 +49,7 @@ func createDenyByRuntimePolicy(builder *flatbuffers.Builder, runtime string) fla } func createRoot(builder *flatbuffers.Builder, oper wls.BoolOperation, description string, nodes []flatbuffers.UOffsetT) flatbuffers.UOffsetT { - nodeRoot := schema.CompositeNodeCreate(builder, oper, description, nodes) + nodeRoot := schema.CompositeNodeCreate(builder, oper, description, nodes, "") return schema.NodeTypeWrapperCreate(builder, nodeRoot, wls.NodeTypeCompositeNode) } diff --git a/go/schema/Creators.go b/go/schema/Creators.go index c592bf1..9538fd0 100644 --- a/go/schema/Creators.go +++ b/go/schema/Creators.go @@ -55,24 +55,44 @@ func UNumEvaluatorCreate(builder *flatbuffers.Builder, evaluator wls.NumericEval return wls.UNumEvaluatorEnd(builder) } -func EvaluatorNodeCreate(builder *flatbuffers.Builder, evaluatorType wls.EvaluatorType, description string, evalOffset flatbuffers.UOffsetT) flatbuffers.UOffsetT { +// EvaluatorNodeCreate builds an EvaluatorNode. Pass ruleID="" for nodes that +// are not the root of a rule (the common case); pass the rule's stable id +// only when this evaluator is itself a rule root (single-condition rule). +func EvaluatorNodeCreate(builder *flatbuffers.Builder, evaluatorType wls.EvaluatorType, description string, evalOffset flatbuffers.UOffsetT, ruleID string) flatbuffers.UOffsetT { fbDescription := builder.CreateString(description) + var fbRuleID flatbuffers.UOffsetT + if ruleID != "" { + fbRuleID = builder.CreateString(ruleID) + } wls.EvaluatorNodeStart(builder) wls.EvaluatorNodeAddEvalType(builder, evaluatorType) wls.EvaluatorNodeAddDescription(builder, fbDescription) wls.EvaluatorNodeAddEval(builder, evalOffset) + if ruleID != "" { + wls.EvaluatorNodeAddRuleId(builder, fbRuleID) + } return wls.EvaluatorNodeEnd(builder) } -func CompositeNodeCreate(builder *flatbuffers.Builder, oper wls.BoolOperation, description string, nodes []flatbuffers.UOffsetT) flatbuffers.UOffsetT { +// CompositeNodeCreate builds a CompositeNode. Pass ruleID="" for nodes that +// are not the root of a rule (sub-rule structure, the top-level OR itself); +// pass the rule's stable id only when this composite is itself a rule root. +func CompositeNodeCreate(builder *flatbuffers.Builder, oper wls.BoolOperation, description string, nodes []flatbuffers.UOffsetT, ruleID string) flatbuffers.UOffsetT { fbDescription := builder.CreateString(description) + var fbRuleID flatbuffers.UOffsetT + if ruleID != "" { + fbRuleID = builder.CreateString(ruleID) + } childrenVector := builder.CreateVectorOfTables(nodes) wls.CompositeNodeStart(builder) wls.CompositeNodeAddDescription(builder, fbDescription) wls.CompositeNodeAddOp(builder, oper) wls.CompositeNodeAddChildren(builder, childrenVector) + if ruleID != "" { + wls.CompositeNodeAddRuleId(builder, fbRuleID) + } return wls.CompositeNodeEnd(builder) } diff --git a/go/schema/dd/wls/CompositeNode.go b/go/schema/dd/wls/CompositeNode.go index beae793..a333694 100644 --- a/go/schema/dd/wls/CompositeNode.go +++ b/go/schema/dd/wls/CompositeNode.go @@ -64,7 +64,7 @@ func (rcv *CompositeNode) MutateOp(n BoolOperation) bool { return rcv._tab.MutateInt8Slot(6, int8(n)) } -/// At some point we will switch back to; children: [NodeType]; +/// At some point we will switch back to; children: [NodeType]; /// (union vectors are not supported in GO so we are wrapping the table in a table) func (rcv *CompositeNode) Children(obj *NodeTypeWrapper, j int) bool { o := flatbuffers.UOffsetT(rcv._tab.Offset(8)) @@ -86,10 +86,26 @@ func (rcv *CompositeNode) ChildrenLength() int { return 0 } -/// At some point we will switch back to; children: [NodeType]; +/// At some point we will switch back to; children: [NodeType]; /// (union vectors are not supported in GO so we are wrapping the table in a table) +/// Stable identifier for the rule this node is the root of. Set only on +/// nodes that sit directly under a policy's top-level OR (one per rule); +/// empty/unset on all other nodes. Read by the engine to surface "which +/// rule fired" to action callbacks via plcs_eval_ctx_get_matched_rule_id(). +func (rcv *CompositeNode) RuleId() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(10)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +/// Stable identifier for the rule this node is the root of. Set only on +/// nodes that sit directly under a policy's top-level OR (one per rule); +/// empty/unset on all other nodes. Read by the engine to surface "which +/// rule fired" to action callbacks via plcs_eval_ctx_get_matched_rule_id(). func CompositeNodeStart(builder *flatbuffers.Builder) { - builder.StartObject(3) + builder.StartObject(4) } func CompositeNodeAddDescription(builder *flatbuffers.Builder, description flatbuffers.UOffsetT) { builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(description), 0) @@ -103,6 +119,9 @@ func CompositeNodeAddChildren(builder *flatbuffers.Builder, children flatbuffers func CompositeNodeStartChildrenVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT { return builder.StartVector(4, numElems, 4) } +func CompositeNodeAddRuleId(builder *flatbuffers.Builder, ruleId flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(3, flatbuffers.UOffsetT(ruleId), 0) +} func CompositeNodeEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { return builder.EndObject() } diff --git a/go/schema/dd/wls/EvaluatorNode.go b/go/schema/dd/wls/EvaluatorNode.go index 6a599ad..a50d8c0 100644 --- a/go/schema/dd/wls/EvaluatorNode.go +++ b/go/schema/dd/wls/EvaluatorNode.go @@ -76,8 +76,22 @@ func (rcv *EvaluatorNode) Eval(obj *flatbuffers.Table) bool { } /// The evaluator is a union of different evaluator types (String, Numeric, etc.). +/// Stable identifier for the rule this node is the root of. Same semantics +/// as CompositeNode.rule_id: set only when this evaluator is itself the +/// root of a rule (single-condition rule), empty otherwise. +func (rcv *EvaluatorNode) RuleId() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(10)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +/// Stable identifier for the rule this node is the root of. Same semantics +/// as CompositeNode.rule_id: set only when this evaluator is itself the +/// root of a rule (single-condition rule), empty otherwise. func EvaluatorNodeStart(builder *flatbuffers.Builder) { - builder.StartObject(3) + builder.StartObject(4) } func EvaluatorNodeAddDescription(builder *flatbuffers.Builder, description flatbuffers.UOffsetT) { builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(description), 0) @@ -88,6 +102,9 @@ func EvaluatorNodeAddEvalType(builder *flatbuffers.Builder, evalType EvaluatorTy func EvaluatorNodeAddEval(builder *flatbuffers.Builder, eval flatbuffers.UOffsetT) { builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(eval), 0) } +func EvaluatorNodeAddRuleId(builder *flatbuffers.Builder, ruleId flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(3, flatbuffers.UOffsetT(ruleId), 0) +} func EvaluatorNodeEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { return builder.EndObject() } From 4ab22f555ab5afb68d6701c4727df5f83a72a4e3 Mon Sep 17 00:00:00 2001 From: Anna Cai Date: Thu, 28 May 2026 15:12:14 -0400 Subject: [PATCH 2/2] remove comments --- fbs-schema/nodes.fbs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/fbs-schema/nodes.fbs b/fbs-schema/nodes.fbs index 41887ff..3be2a0b 100644 --- a/fbs-schema/nodes.fbs +++ b/fbs-schema/nodes.fbs @@ -32,10 +32,6 @@ table CompositeNode { /// At some point we will switch back to; children: [NodeType]; /// (union vectors are not supported in GO so we are wrapping the table in a table) children: [NodeTypeWrapper]; - /// Stable identifier for the rule this node is the root of. Set only on - /// nodes that sit directly under a policy's top-level OR (one per rule); - /// empty/unset on all other nodes. Read by the engine to surface "which - /// rule fired" to action callbacks via plcs_eval_ctx_get_matched_rule_id(). rule_id: string; } @@ -46,8 +42,5 @@ table EvaluatorNode { description: string; /// The evaluator is a union of different evaluator types (String, Numeric, etc.). eval: EvaluatorType; - /// Stable identifier for the rule this node is the root of. Same semantics - /// as CompositeNode.rule_id: set only when this evaluator is itself the - /// root of a rule (single-condition rule), empty otherwise. rule_id: string; }