From 4b9c63369878afd67aa3ee0046c387cd76c0183a Mon Sep 17 00:00:00 2001 From: Xiwen Cheng Date: Thu, 22 Jan 2026 23:05:35 +0100 Subject: [PATCH 1/2] new rules --- .../002_0009_no_default_value.rego | 61 ++++++++++++++++ .../002_0009_no_default_value_test.yaml | 23 +++++++ .../006_0001_exposed_constants.rego | 69 +++++++++++++++++++ .../006_0001_exposed_constants_test.yaml | 16 +++++ 4 files changed, 169 insertions(+) create mode 100644 rules/002_domain_model/002_0009_no_default_value.rego create mode 100644 rules/002_domain_model/002_0009_no_default_value_test.yaml create mode 100644 rules/006_security/006_0001_exposed_constants.rego create mode 100644 rules/006_security/006_0001_exposed_constants_test.yaml diff --git a/rules/002_domain_model/002_0009_no_default_value.rego b/rules/002_domain_model/002_0009_no_default_value.rego new file mode 100644 index 0000000..3d04e64 --- /dev/null +++ b/rules/002_domain_model/002_0009_no_default_value.rego @@ -0,0 +1,61 @@ +# METADATA +# scope: package +# title: Do not use default values on attributes +# description: Avoid default values because it introduces hidden logic that is hard to detect via "find changes". +# authors: +# - Rick Schreuder +# custom: +# category: Maintainability +# rulename: NoDefaultValue +# severity: LOW +# rulenumber: 002_0009 +# remediation: Remove the attribute default value and set it explicitly in logic where needed. +# input: .*DomainModels\$DomainModel\.yaml + +package app.mendix.domain_model.no_default_value + +import rego.v1 + +# Load custom rule annotations (severity, category, rule number). +annotation := rego.metadata.chain()[1].annotations + +# The file is valid only if no rule violations are found. +default allow := false +allow if count(errors) == 0 + +# A default value is considered "set" if it exists AND it is not the empty string. +default_value_is_set(default_value) if { + default_value != null + not is_empty_string(default_value) +} + +# Checks whether a value is exactly an empty string. +is_empty_string(value) if { + is_string(value) + value == "" +} + +# Emit an error for each attribute that defines a DefaultValue. +errors contains error_message if { + some entity_index + some attribute_index + + entity := input.Entities[entity_index] + attribute := entity.Attributes[attribute_index] + + # In the DomainModel yaml, DefaultValue is nested under Value + stored_value := object.get(attribute, "Value", {}) + default_value := object.get(stored_value, "DefaultValue", null) + + default_value_is_set(default_value) + + error_message := sprintf("[%v, %v, %v] %v.%v has a default value set", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + entity.Name, + attribute.Name + ] + ) +} \ No newline at end of file diff --git a/rules/002_domain_model/002_0009_no_default_value_test.yaml b/rules/002_domain_model/002_0009_no_default_value_test.yaml new file mode 100644 index 0000000..148e366 --- /dev/null +++ b/rules/002_domain_model/002_0009_no_default_value_test.yaml @@ -0,0 +1,23 @@ +TestCases: +- name: allow attributes with no default value or empty/null default value + allow: true + input: + Entities: + - Name: Entity1 + Attributes: + - Name: Attr1 + - Name: Attr2 + Value: + DefaultValue: null + - Name: Attr3 + Value: + DefaultValue: "" +- name: deny attribute with a default value set + allow: false + input: + Entities: + - Name: Entity1 + Attributes: + - Name: Attr1 + Value: + DefaultValue: "0" \ No newline at end of file diff --git a/rules/006_security/006_0001_exposed_constants.rego b/rules/006_security/006_0001_exposed_constants.rego new file mode 100644 index 0000000..f5f8ae8 --- /dev/null +++ b/rules/006_security/006_0001_exposed_constants.rego @@ -0,0 +1,69 @@ +# METADATA +# scope: package +# title: Exposed constants with sensitive data +# description: Constants with potentially sensitive data should not be exposed to the client. +# authors: +# - Bart Zantingh +# custom: +# category: Security +# rulename: ExposedConstants +# severity: HIGH +# rulenumber: "006_0001" +# remediation: Set constant's 'Exposed to client' setting to false. +# input: "**/*$Constant.yaml" +package app.mendix.constants.exposed_constants + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +default allow := false + +allow if count(errors) == 0 + +exposed := input.ExposedToClient + +errors contains error if { + exposed + + name := input.Name + + error := sprintf( + "[%v, %v, %v] Constant %v is exposed to the client", + [ + "MEDIUM", + annotation.custom.category, + annotation.custom.rulenumber, + name, + ], + ) +} + +errors contains error if { + contains_sensitive_data + exposed + + name := input.Name + + error := sprintf( + "[%v, %v, %v] Constant %v is exposed to the client, and seems to contain sensitive data", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + name, + ], + ) +} + +sensitive_keywords := [ + "id", "ident", + "username", "user_name", "user", "usr", "uname", + "secret", "scrt", + "password", "pwd", "passwrd", +] + +contains_sensitive_data if { + some keyword in sensitive_keywords + contains(lower(input.Name), keyword) +} diff --git a/rules/006_security/006_0001_exposed_constants_test.yaml b/rules/006_security/006_0001_exposed_constants_test.yaml new file mode 100644 index 0000000..598f8cc --- /dev/null +++ b/rules/006_security/006_0001_exposed_constants_test.yaml @@ -0,0 +1,16 @@ +TestCases: +- name: allow not exposed with sensitive name + allow: true + input: + ExposedToClient: false + Name: CST_TestConstant_password +- name: allow not exposed without sensitive name + allow: true + input: + ExposedToClient: false + Name: CST_TestConstant +- name: deny exposed with sensitive name + allow: false + input: + ExposedToClient: true + Name: CST_TestConstant_Id From add571ec325e73cbcb0e434ae483d941bc0f9c0d Mon Sep 17 00:00:00 2001 From: Xiwen Cheng Date: Thu, 22 Jan 2026 23:06:58 +0100 Subject: [PATCH 2/2] Refactor following Bart's suggestion --- .../002_0009_no_default_value.rego | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/rules/002_domain_model/002_0009_no_default_value.rego b/rules/002_domain_model/002_0009_no_default_value.rego index 3d04e64..ea79a29 100644 --- a/rules/002_domain_model/002_0009_no_default_value.rego +++ b/rules/002_domain_model/002_0009_no_default_value.rego @@ -24,30 +24,17 @@ default allow := false allow if count(errors) == 0 # A default value is considered "set" if it exists AND it is not the empty string. -default_value_is_set(default_value) if { +has_default_value(default_value) if { default_value != null - not is_empty_string(default_value) -} - -# Checks whether a value is exactly an empty string. -is_empty_string(value) if { - is_string(value) - value == "" + default_value != "" } # Emit an error for each attribute that defines a DefaultValue. errors contains error_message if { - some entity_index - some attribute_index - - entity := input.Entities[entity_index] - attribute := entity.Attributes[attribute_index] - - # In the DomainModel yaml, DefaultValue is nested under Value - stored_value := object.get(attribute, "Value", {}) - default_value := object.get(stored_value, "DefaultValue", null) - default_value_is_set(default_value) + some entity in input.Entities + some attribute in entity.Attributes + has_default_value(attribute.Value.DefaultValue) error_message := sprintf("[%v, %v, %v] %v.%v has a default value set", [