From 174b8d90f37ca215b4ab3b5048bff8015ca49958 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 10:07:35 +0000 Subject: [PATCH 1/9] Add ACR-based rules for microflows and domain model Implement six new rules derived from the CLEVR ACR (Application Consistency Rules) catalog, mapped onto the Mendix model export formats that mxlint already parses. Each rule ships with a passing _test.yaml covering compliant and non-compliant cases. Microflows (005): - 005_0006 MicroflowCallsSelf: detect direct recursion (self-call), including calls nested inside loops. - 005_0007 LoopInLoop: detect loops nested inside another loop. - 005_0008 CommitWithoutEvents: detect commit/change actions that commit without events. - 005_0009 MicroflowDocumentation: require documentation on microflows above a minimum number of actions. Domain model (002): - 002_0010 AssociationOwnerBoth: discourage associations owned by Both. - 002_0011 EntityDocumentation: require documentation on entities above a minimum number of attributes. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018DNq3pmSsKsrV77qQ6tUcW --- .../002_0010_association_owner_both.rego | 54 +++++++++++++++ .../002_0010_association_owner_both_test.yaml | 37 ++++++++++ .../002_0011_entity_documentation.rego | 45 ++++++++++++ .../002_0011_entity_documentation_test.yaml | 49 +++++++++++++ .../005_0006_microflow_calls_self.rego | 43 ++++++++++++ .../005_0006_microflow_calls_self_test.yaml | 61 ++++++++++++++++ .../005_microflows/005_0007_loop_in_loop.rego | 45 ++++++++++++ .../005_0007_loop_in_loop_test.yaml | 51 ++++++++++++++ .../005_0008_commit_without_events.rego | 60 ++++++++++++++++ .../005_0008_commit_without_events_test.yaml | 65 +++++++++++++++++ .../005_0009_microflow_documentation.rego | 49 +++++++++++++ ...005_0009_microflow_documentation_test.yaml | 69 +++++++++++++++++++ 12 files changed, 628 insertions(+) create mode 100644 rules/002_domain_model/002_0010_association_owner_both.rego create mode 100644 rules/002_domain_model/002_0010_association_owner_both_test.yaml create mode 100644 rules/002_domain_model/002_0011_entity_documentation.rego create mode 100644 rules/002_domain_model/002_0011_entity_documentation_test.yaml create mode 100644 rules/005_microflows/005_0006_microflow_calls_self.rego create mode 100644 rules/005_microflows/005_0006_microflow_calls_self_test.yaml create mode 100644 rules/005_microflows/005_0007_loop_in_loop.rego create mode 100644 rules/005_microflows/005_0007_loop_in_loop_test.yaml create mode 100644 rules/005_microflows/005_0008_commit_without_events.rego create mode 100644 rules/005_microflows/005_0008_commit_without_events_test.yaml create mode 100644 rules/005_microflows/005_0009_microflow_documentation.rego create mode 100644 rules/005_microflows/005_0009_microflow_documentation_test.yaml diff --git a/rules/002_domain_model/002_0010_association_owner_both.rego b/rules/002_domain_model/002_0010_association_owner_both.rego new file mode 100644 index 0000000..ed0b02d --- /dev/null +++ b/rules/002_domain_model/002_0010_association_owner_both.rego @@ -0,0 +1,54 @@ +# METADATA +# scope: package +# title: Avoid many-to-many associations owned by Both +# description: Using owner "Both" does not change the ability to navigate an association (that is always possible from both ends), but it does introduce overhead. Many-to-many associations should be owned by the Default entity unless there is a strong business reason. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/performance/associationownerboth +# custom: +# category: Performance +# rulename: AssociationOwnerBoth +# severity: LOW +# rulenumber: "002_0010" +# remediation: Change the association owner to Default unless both-side ownership is genuinely required. +# input: .*/DomainModels\$DomainModel\.yaml +package app.mendix.domain_model.association_owner_both + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + some association in input.Associations + association.Owner == "Both" + + error := sprintf( + "[%v, %v, %v] Association %v is owned by Both, which introduces unnecessary overhead", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + association.Name, + ], + ) +} + +errors contains error if { + some association in input.CrossAssociations + association.Owner == "Both" + + error := sprintf( + "[%v, %v, %v] Association %v is owned by Both, which introduces unnecessary overhead", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + association.Name, + ], + ) +} diff --git a/rules/002_domain_model/002_0010_association_owner_both_test.yaml b/rules/002_domain_model/002_0010_association_owner_both_test.yaml new file mode 100644 index 0000000..f8c3bea --- /dev/null +++ b/rules/002_domain_model/002_0010_association_owner_both_test.yaml @@ -0,0 +1,37 @@ +TestCases: + - name: allow domain model without associations + allow: true + input: + Associations: null + CrossAssociations: null + - name: allow association owned by Default + allow: true + input: + Associations: + - $Type: DomainModels$Association + Name: Order_Customer + Owner: Default + - name: do not allow association owned by Both + allow: false + input: + Associations: + - $Type: DomainModels$Association + Name: Order_Product + Owner: Both + - name: do not allow cross association owned by Both + allow: false + input: + CrossAssociations: + - $Type: DomainModels$CrossAssociation + Name: Order_ExternalTag + Owner: Both + - name: allow mix with only Default owners + allow: true + input: + Associations: + - $Type: DomainModels$Association + Name: Order_Customer + Owner: Default + - $Type: DomainModels$Association + Name: Order_Line + Owner: Default diff --git a/rules/002_domain_model/002_0011_entity_documentation.rego b/rules/002_domain_model/002_0011_entity_documentation.rego new file mode 100644 index 0000000..9412c32 --- /dev/null +++ b/rules/002_domain_model/002_0011_entity_documentation.rego @@ -0,0 +1,45 @@ +# METADATA +# scope: package +# title: Large entities should be documented +# description: When an entity has many attributes, documentation (for example the reasoning behind associations or attribute choices) improves maintainability. Another developer can then quickly see what the entity is used for. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/project-hygiene/entitydocumentation +# custom: +# category: Maintainability +# rulename: EntityDocumentation +# severity: LOW +# rulenumber: "002_0011" +# remediation: Add documentation to the entity describing its purpose. +# input: .*/DomainModels\$DomainModel\.yaml +package app.mendix.domain_model.entity_documentation + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +# Minimum number of attributes before documentation is required. +min_attributes := 10 + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + some entity in input.Entities + count(entity.Attributes) >= min_attributes + documentation := trim_space(object.get(entity, "Documentation", "")) + count(documentation) == 0 + + error := sprintf( + "[%v, %v, %v] Entity %v has %v attributes but no documentation", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + entity.Name, + count(entity.Attributes), + ], + ) +} diff --git a/rules/002_domain_model/002_0011_entity_documentation_test.yaml b/rules/002_domain_model/002_0011_entity_documentation_test.yaml new file mode 100644 index 0000000..9f12142 --- /dev/null +++ b/rules/002_domain_model/002_0011_entity_documentation_test.yaml @@ -0,0 +1,49 @@ +TestCases: + - name: allow small entity without documentation + allow: true + input: + Entities: + - $Type: DomainModels$EntityImpl + Name: SmallEntity + Documentation: "" + Attributes: + - $Type: DomainModels$Attribute + Name: Attr1 + - $Type: DomainModels$Attribute + Name: Attr2 + - name: allow large entity with documentation + allow: true + input: + Entities: + - $Type: DomainModels$EntityImpl + Name: BigDocumentedEntity + Documentation: "Holds customer master data." + Attributes: + - {Name: A1} + - {Name: A2} + - {Name: A3} + - {Name: A4} + - {Name: A5} + - {Name: A6} + - {Name: A7} + - {Name: A8} + - {Name: A9} + - {Name: A10} + - name: do not allow large entity without documentation + allow: false + input: + Entities: + - $Type: DomainModels$EntityImpl + Name: BigUndocumentedEntity + Documentation: "" + Attributes: + - {Name: A1} + - {Name: A2} + - {Name: A3} + - {Name: A4} + - {Name: A5} + - {Name: A6} + - {Name: A7} + - {Name: A8} + - {Name: A9} + - {Name: A10} diff --git a/rules/005_microflows/005_0006_microflow_calls_self.rego b/rules/005_microflows/005_0006_microflow_calls_self.rego new file mode 100644 index 0000000..9e74834 --- /dev/null +++ b/rules/005_microflows/005_0006_microflow_calls_self.rego @@ -0,0 +1,43 @@ +# METADATA +# scope: package +# title: Microflow should not call itself (recursion) +# description: Recursion is a complex structure and Mendix has no built-in fail-safe. A wrong exit condition or bad data can keep the recursive calls going and crash the app. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/architecture/microflowcallsself +# custom: +# category: Architecture +# rulename: MicroflowCallsSelf +# severity: MEDIUM +# rulenumber: "005_0006" +# remediation: Avoid recursion. If it is truly needed, add an emergency brake that stops execution after a maximum number of iterations. +# input: "**/*$Microflow.yaml" +package app.mendix.microflows.microflow_calls_self + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + # A microflow call anywhere in the flow (including inside loops) that + # targets a microflow with the same short name is a self-call / recursion. + some obj + walk(input, [_, obj]) + obj["$Type"] == "Microflows$MicroflowCallAction" + endswith(obj.MicroflowCall.Microflow, sprintf(".%v", [input.Name])) + + error := sprintf( + "[%v, %v, %v] Microflow %v calls itself (recursion)", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + input.Name, + ], + ) +} diff --git a/rules/005_microflows/005_0006_microflow_calls_self_test.yaml b/rules/005_microflows/005_0006_microflow_calls_self_test.yaml new file mode 100644 index 0000000..e8ccd6f --- /dev/null +++ b/rules/005_microflows/005_0006_microflow_calls_self_test.yaml @@ -0,0 +1,61 @@ +TestCases: + - name: allow microflow calling a different microflow + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$MicroflowCallAction + MicroflowCall: + $Type: Microflows$MicroflowCall + Microflow: MyModule.OtherFlow + - name: allow microflow with a call sharing a suffix but not the full name + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$MicroflowCallAction + MicroflowCall: + $Type: Microflows$MicroflowCall + Microflow: MyModule.DoMyFlow + - name: do not allow microflow that calls itself directly + allow: false + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$MicroflowCallAction + MicroflowCall: + $Type: Microflows$MicroflowCall + Microflow: MyModule.MyFlow + - name: do not allow microflow that calls itself inside a loop + allow: false + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$MicroflowCallAction + MicroflowCall: + $Type: Microflows$MicroflowCall + Microflow: MyModule.MyFlow diff --git a/rules/005_microflows/005_0007_loop_in_loop.rego b/rules/005_microflows/005_0007_loop_in_loop.rego new file mode 100644 index 0000000..c836359 --- /dev/null +++ b/rules/005_microflows/005_0007_loop_in_loop.rego @@ -0,0 +1,45 @@ +# METADATA +# scope: package +# title: Avoid nested loops in a microflow +# description: A loop can potentially be executed hundreds or thousands of times. A loop inside a loop multiplies that cost and quickly leads to performance problems. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/performance/loopinloop +# custom: +# category: Performance +# rulename: LoopInLoop +# severity: MEDIUM +# rulenumber: "005_0007" +# remediation: Restructure the logic to avoid nesting loops, for example by retrieving the required data once before the outer loop. +# input: "**/*$Microflow.yaml" +package app.mendix.microflows.loop_in_loop + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + # Find a loop that contains another loop anywhere within its body. + some outer + walk(input, [_, outer]) + outer["$Type"] == "Microflows$LoopedActivity" + + some inner + walk(outer.ObjectCollection, [_, inner]) + inner["$Type"] == "Microflows$LoopedActivity" + + error := sprintf( + "[%v, %v, %v] Microflow %v contains a loop nested inside another loop", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + input.Name, + ], + ) +} diff --git a/rules/005_microflows/005_0007_loop_in_loop_test.yaml b/rules/005_microflows/005_0007_loop_in_loop_test.yaml new file mode 100644 index 0000000..827dbe8 --- /dev/null +++ b/rules/005_microflows/005_0007_loop_in_loop_test.yaml @@ -0,0 +1,51 @@ +TestCases: + - name: allow microflow with a single loop + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$ChangeAction + - name: allow microflow with two sequential (non-nested) loops + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + - $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + - name: do not allow a loop nested inside another loop + allow: false + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity diff --git a/rules/005_microflows/005_0008_commit_without_events.rego b/rules/005_microflows/005_0008_commit_without_events.rego new file mode 100644 index 0000000..a95a84e --- /dev/null +++ b/rules/005_microflows/005_0008_commit_without_events.rego @@ -0,0 +1,60 @@ +# METADATA +# scope: package +# title: Avoid committing without events +# description: There is rarely a reason to disable events during a commit. Disabling them silently skips before/after commit logic and should only be done with a very good justification. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/reliability/commitwithoutevents +# custom: +# category: Reliability +# rulename: CommitWithoutEvents +# severity: MEDIUM +# rulenumber: "005_0008" +# remediation: Commit with events enabled, or document why the events must be skipped. +# input: "**/*$Microflow.yaml" +package app.mendix.microflows.commit_without_events + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +default allow := false + +allow if count(errors) == 0 + +# Dedicated Commit action with events explicitly disabled. +errors contains error if { + some obj + walk(input, [_, obj]) + obj["$Type"] == "Microflows$CommitAction" + obj.WithEvents == false + + error := sprintf( + "[%v, %v, %v] Commit action in microflow %v is configured without events", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + input.Name, + ], + ) +} + +# Change action that commits without events. +errors contains error if { + some obj + walk(input, [_, obj]) + obj["$Type"] == "Microflows$ChangeAction" + obj.Commit == "YesWithoutEvents" + + error := sprintf( + "[%v, %v, %v] Change action in microflow %v commits without events", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + input.Name, + ], + ) +} diff --git a/rules/005_microflows/005_0008_commit_without_events_test.yaml b/rules/005_microflows/005_0008_commit_without_events_test.yaml new file mode 100644 index 0000000..15a63b9 --- /dev/null +++ b/rules/005_microflows/005_0008_commit_without_events_test.yaml @@ -0,0 +1,65 @@ +TestCases: + - name: allow commit action with events + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$CommitAction + WithEvents: true + - name: allow change action committing with events + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$ChangeAction + Commit: "Yes" + - name: do not allow commit action without events + allow: false + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$CommitAction + WithEvents: false + - name: do not allow change action committing without events + allow: false + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$ChangeAction + Commit: "YesWithoutEvents" + - name: do not allow commit without events inside a loop + allow: false + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$CommitAction + WithEvents: false diff --git a/rules/005_microflows/005_0009_microflow_documentation.rego b/rules/005_microflows/005_0009_microflow_documentation.rego new file mode 100644 index 0000000..ff42cc8 --- /dev/null +++ b/rules/005_microflows/005_0009_microflow_documentation.rego @@ -0,0 +1,49 @@ +# METADATA +# scope: package +# title: Complex microflows should be documented +# description: With difficult microflows it is very helpful for other developers to see documentation about decisions or modeling choices. Microflows above a minimum number of actions should carry documentation. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/project-hygiene/microflowdocumentation +# custom: +# category: Maintainability +# rulename: MicroflowDocumentation +# severity: LOW +# rulenumber: "005_0009" +# remediation: Add documentation to the microflow describing its purpose and any important modeling choices. +# input: "**/*$Microflow.yaml" +package app.mendix.microflows.microflow_documentation + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +# Minimum number of action activities before documentation is required. +min_actions := 10 + +default allow := false + +allow if count(errors) == 0 + +action_count := count([obj | + walk(input, [_, obj]) + obj["$Type"] == "Microflows$ActionActivity" +]) + +errors contains error if { + action_count >= min_actions + documentation := trim_space(object.get(input, "Documentation", "")) + count(documentation) == 0 + + error := sprintf( + "[%v, %v, %v] Microflow %v has %v actions but no documentation", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + input.Name, + action_count, + ], + ) +} diff --git a/rules/005_microflows/005_0009_microflow_documentation_test.yaml b/rules/005_microflows/005_0009_microflow_documentation_test.yaml new file mode 100644 index 0000000..8ba70fd --- /dev/null +++ b/rules/005_microflows/005_0009_microflow_documentation_test.yaml @@ -0,0 +1,69 @@ +TestCases: + - name: allow simple microflow without documentation + allow: true + input: + $Type: Microflows$Microflow + Name: SmallFlow + Documentation: "" + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - name: allow complex microflow with documentation + allow: true + input: + $Type: Microflows$Microflow + Name: BigDocumentedFlow + Documentation: "This flow orchestrates the order processing." + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - name: do not allow complex microflow without documentation + allow: false + input: + $Type: Microflows$Microflow + Name: BigUndocumentedFlow + Documentation: "" + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - name: do not allow complex microflow with whitespace-only documentation + allow: false + input: + $Type: Microflows$Microflow + Name: WhitespaceDocFlow + Documentation: " " + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity + - $Type: Microflows$ActionActivity From 24deddcaa957aab9c31deeae47edee7fa2f2473e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 10:15:35 +0000 Subject: [PATCH 2/9] Add cross-file inheritance rule and System-entity creation rule Add two more ACR-derived rules, including the first cross-file rule that reads related model files via mxlint.io.readYaml. - 002_0012 MultiLevelInheritance (JavaScript, cross-file): walks the generalization chain across module domain models and flags entities that inherit through more than two levels. Ships with a fixture domain model (Inheritance/) that the test resolves via mxlint.io.readYaml. - 005_0010 CreateSystemEntity: flags microflows that instantiate System entities (e.g. System.User, System.Session) directly instead of using Administration.Account. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018DNq3pmSsKsrV77qQ6tUcW --- .../002_0012_multilevel_inheritance.js | 85 +++++++++++++++++++ .../002_0012_multilevel_inheritance_test.yaml | 40 +++++++++ .../Inheritance/DomainModels$DomainModel.yaml | 23 +++++ .../005_0010_create_system_entity.rego | 43 ++++++++++ .../005_0010_create_system_entity_test.yaml | 55 ++++++++++++ 5 files changed, 246 insertions(+) create mode 100644 rules/002_domain_model/002_0012_multilevel_inheritance.js create mode 100644 rules/002_domain_model/002_0012_multilevel_inheritance_test.yaml create mode 100644 rules/002_domain_model/Inheritance/DomainModels$DomainModel.yaml create mode 100644 rules/005_microflows/005_0010_create_system_entity.rego create mode 100644 rules/005_microflows/005_0010_create_system_entity_test.yaml diff --git a/rules/002_domain_model/002_0012_multilevel_inheritance.js b/rules/002_domain_model/002_0012_multilevel_inheritance.js new file mode 100644 index 0000000..8b0d2b3 --- /dev/null +++ b/rules/002_domain_model/002_0012_multilevel_inheritance.js @@ -0,0 +1,85 @@ +const metadata = { + scope: "package", + title: "Inheritance should be limited to 2 levels", + description: "Deep generalization hierarchies are hard to maintain and have a negative performance impact. Inheritance should be limited to at most two levels.", + authors: ["Xiwen Cheng "], + related_resources: ["https://sdf-docs.clevr.com/?docs=acr-rules/architecture/multilevelinheritance"], + custom: { + category: "Architecture", + rulename: "MultiLevelInheritance", + severity: "MEDIUM", + rulenumber: "002_0012", + remediation: "Flatten the hierarchy. Instead of deep inheritance, associate objects or move attributes up into the super entity with an ObjectType enumeration.", + input: ".*/DomainModels\\$DomainModel\\.yaml" + } +}; + +// Maximum number of ancestors an entity may have through inheritance. +const MAX_LEVELS = 2; + +// Resolve a fully qualified entity name (e.g. "Module.Entity") to its entity +// definition by reading the domain model of the referenced module. +function resolveEntity(qualifiedName) { + const parts = qualifiedName.split("."); + const moduleName = parts[0]; + const entityName = parts.slice(1).join("."); + try { + const domainModel = mxlint.io.readYaml(moduleName + "/DomainModels$DomainModel.yaml"); + if (!domainModel || !domainModel.Entities) { + return undefined; + } + return domainModel.Entities.find(e => e.Name === entityName); + } catch (e) { + // Domain model not available (e.g. platform System module) - stop here. + return undefined; + } +} + +// Extract the generalization reference of an entity, or undefined if it does +// not inherit from another entity. +function generalizationOf(entity) { + if (!entity || !entity.MaybeGeneralization) { + return undefined; + } + return entity.MaybeGeneralization.Generalization; +} + +// Count how many ancestors an entity has by walking up the generalization chain. +function ancestorCount(generalization) { + let count = 0; + let current = generalization; + while (current) { + count++; + if (count > 50) { + // Safety brake against cyclic definitions. + break; + } + const moduleName = current.split(".")[0]; + if (moduleName === "System") { + // The platform System module is the base of the hierarchy. + break; + } + const parent = resolveEntity(current); + current = generalizationOf(parent); + } + return count; +} + +function rule(input = {}) { + const errors = []; + const entities = input.Entities || []; + + for (const entity of entities) { + const generalization = generalizationOf(entity); + if (!generalization) { + continue; + } + const levels = ancestorCount(generalization); + if (levels > MAX_LEVELS) { + errors.push(`[${metadata.custom.severity}, ${metadata.custom.category}, ${metadata.custom.rulenumber}] Entity ${entity.Name} inherits through ${levels} levels, which exceeds the maximum of ${MAX_LEVELS}.`); + } + } + + const allow = errors.length === 0; + return { allow, errors }; +} diff --git a/rules/002_domain_model/002_0012_multilevel_inheritance_test.yaml b/rules/002_domain_model/002_0012_multilevel_inheritance_test.yaml new file mode 100644 index 0000000..f4b6186 --- /dev/null +++ b/rules/002_domain_model/002_0012_multilevel_inheritance_test.yaml @@ -0,0 +1,40 @@ +TestCases: + - name: allow no entities + allow: true + input: + Entities: null + - name: allow entity without generalization + allow: true + input: + Entities: + - $Type: DomainModels$EntityImpl + Name: Standalone + MaybeGeneralization: + $Type: DomainModels$NoGeneralization + - name: allow entity inheriting directly from System + allow: true + input: + Entities: + - $Type: DomainModels$EntityImpl + Name: MyDocument + MaybeGeneralization: + $Type: DomainModels$Generalization + Generalization: System.FileDocument + - name: allow inheritance of two levels + allow: true + input: + Entities: + - $Type: DomainModels$EntityImpl + Name: TwoLevels + MaybeGeneralization: + $Type: DomainModels$Generalization + Generalization: Inheritance.Middle + - name: do not allow inheritance of three levels + allow: false + input: + Entities: + - $Type: DomainModels$EntityImpl + Name: ThreeLevels + MaybeGeneralization: + $Type: DomainModels$Generalization + Generalization: Inheritance.Leaf diff --git a/rules/002_domain_model/Inheritance/DomainModels$DomainModel.yaml b/rules/002_domain_model/Inheritance/DomainModels$DomainModel.yaml new file mode 100644 index 0000000..1b7b8a0 --- /dev/null +++ b/rules/002_domain_model/Inheritance/DomainModels$DomainModel.yaml @@ -0,0 +1,23 @@ +# Fixture domain model used by 002_0012_multilevel_inheritance_test.yaml. +# It is intentionally NOT a rule file, so the rule scanner ignores it. +# It defines an inheritance chain: Base <- Middle <- Leaf. +$Type: DomainModels$DomainModel +Associations: [] +CrossAssociations: [] +Documentation: "" +Entities: + - $Type: DomainModels$EntityImpl + Name: Base + MaybeGeneralization: + $Type: DomainModels$NoGeneralization + Persistable: true + - $Type: DomainModels$EntityImpl + Name: Middle + MaybeGeneralization: + $Type: DomainModels$Generalization + Generalization: Inheritance.Base + - $Type: DomainModels$EntityImpl + Name: Leaf + MaybeGeneralization: + $Type: DomainModels$Generalization + Generalization: Inheritance.Middle diff --git a/rules/005_microflows/005_0010_create_system_entity.rego b/rules/005_microflows/005_0010_create_system_entity.rego new file mode 100644 index 0000000..c0479df --- /dev/null +++ b/rules/005_microflows/005_0010_create_system_entity.rego @@ -0,0 +1,43 @@ +# METADATA +# scope: package +# title: Avoid creating System entities directly +# description: System entities such as User or Session should not be instantiated directly in a microflow. Create an Administration.Account instead and let Mendix manage sessions. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/reliability/createsystementity +# custom: +# category: Reliability +# rulename: CreateSystemEntity +# severity: MEDIUM +# rulenumber: "005_0010" +# remediation: Create an Administration.Account (which specializes System.User) instead of creating a System entity directly, and let Mendix handle session creation. +# input: "**/*$Microflow.yaml" +package app.mendix.microflows.create_system_entity + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + # Find any create action (anywhere in the flow) that instantiates a System entity. + some obj + walk(input, [_, obj]) + startswith(object.get(obj, "$Type", ""), "Microflows$Create") + startswith(object.get(obj, "Entity", ""), "System.") + + error := sprintf( + "[%v, %v, %v] Microflow %v creates System entity %v directly", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + input.Name, + obj.Entity, + ], + ) +} diff --git a/rules/005_microflows/005_0010_create_system_entity_test.yaml b/rules/005_microflows/005_0010_create_system_entity_test.yaml new file mode 100644 index 0000000..0b85981 --- /dev/null +++ b/rules/005_microflows/005_0010_create_system_entity_test.yaml @@ -0,0 +1,55 @@ +TestCases: + - name: allow creating a non-system entity + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$CreateChangeAction + Entity: MyModule.Customer + - name: allow retrieving a system entity + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$RetrieveAction + RetrieveSource: + $Type: Microflows$DatabaseRetrieveSource + Entity: System.User + - name: do not allow creating System.User + allow: false + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$CreateChangeAction + Entity: System.User + - name: do not allow creating System.Session inside a loop + allow: false + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$CreateChangeAction + Entity: System.Session From 4b3d65d04ad7566067770a884d7db1de2b1789c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 10:51:06 +0000 Subject: [PATCH 3/9] Add ACR settings, naming, documentation and TODO rules Add six more ACR-derived rules across project settings, domain model, pages and microflows. Project settings (001): - 001_0010 SecurityLevelCheckEverything: require SecurityLevel = CheckEverything. - 001_0011 StrictPageUrlCheck: require strict page URL check enabled. - 001_0012 AllowOneSessionPerUser: discourage multiple sessions per user. Domain model (002): - 002_0013 AttributeNaming: attributes should be PascalCase. Pages (004): - 004_0005 PageDocumentation: pages should have documentation. Microflows (005): - 005_0011 TodoAnnotations: flag leftover TODO/FIXME annotations. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018DNq3pmSsKsrV77qQ6tUcW --- ..._0010_security_level_check_everything.rego | 38 +++++++++++++++ ..._security_level_check_everything_test.yaml | 16 +++++++ .../001_0011_strict_page_url_check.rego | 37 +++++++++++++++ .../001_0011_strict_page_url_check_test.yaml | 11 +++++ .../001_0012_allow_one_session_per_user.rego | 39 ++++++++++++++++ ..._0012_allow_one_session_per_user_test.yaml | 26 +++++++++++ .../002_0013_attribute_naming.rego | 43 +++++++++++++++++ .../002_0013_attribute_naming_test.yaml | 46 +++++++++++++++++++ .../004_0005_page_documentation.rego | 40 ++++++++++++++++ .../004_0005_page_documentation_test.yaml | 19 ++++++++ .../005_0011_todo_annotations.rego | 44 ++++++++++++++++++ .../005_0011_todo_annotations_test.yaml | 40 ++++++++++++++++ 12 files changed, 399 insertions(+) create mode 100644 rules/001_project_settings/001_0010_security_level_check_everything.rego create mode 100644 rules/001_project_settings/001_0010_security_level_check_everything_test.yaml create mode 100644 rules/001_project_settings/001_0011_strict_page_url_check.rego create mode 100644 rules/001_project_settings/001_0011_strict_page_url_check_test.yaml create mode 100644 rules/001_project_settings/001_0012_allow_one_session_per_user.rego create mode 100644 rules/001_project_settings/001_0012_allow_one_session_per_user_test.yaml create mode 100644 rules/002_domain_model/002_0013_attribute_naming.rego create mode 100644 rules/002_domain_model/002_0013_attribute_naming_test.yaml create mode 100644 rules/004_pages/004_0005_page_documentation.rego create mode 100644 rules/004_pages/004_0005_page_documentation_test.yaml create mode 100644 rules/005_microflows/005_0011_todo_annotations.rego create mode 100644 rules/005_microflows/005_0011_todo_annotations_test.yaml diff --git a/rules/001_project_settings/001_0010_security_level_check_everything.rego b/rules/001_project_settings/001_0010_security_level_check_everything.rego new file mode 100644 index 0000000..774ba12 --- /dev/null +++ b/rules/001_project_settings/001_0010_security_level_check_everything.rego @@ -0,0 +1,38 @@ +# METADATA +# scope: package +# title: Security level should be set to production +# description: The project security level should be "CheckEverything" so that both page and microflow access as well as entity access are enforced. Lower levels leave parts of the app unprotected. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/security/securitylevel +# custom: +# category: Security +# rulename: SecurityLevelCheckEverything +# severity: HIGH +# rulenumber: "001_0010" +# remediation: Set the security level to "CheckEverything" in the project security settings. +# input: .*Security\$ProjectSecurity\.yaml +package app.mendix.project_settings.security_level_check_everything + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + input.SecurityLevel != "CheckEverything" + + error := sprintf( + "[%v, %v, %v] Security level is %v, expected CheckEverything", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + input.SecurityLevel, + ], + ) +} diff --git a/rules/001_project_settings/001_0010_security_level_check_everything_test.yaml b/rules/001_project_settings/001_0010_security_level_check_everything_test.yaml new file mode 100644 index 0000000..4adf380 --- /dev/null +++ b/rules/001_project_settings/001_0010_security_level_check_everything_test.yaml @@ -0,0 +1,16 @@ +TestCases: + - name: allow CheckEverything + allow: true + input: + $Type: Security$ProjectSecurity + SecurityLevel: CheckEverything + - name: do not allow CheckNothing + allow: false + input: + $Type: Security$ProjectSecurity + SecurityLevel: CheckNothing + - name: do not allow CheckSecurity + allow: false + input: + $Type: Security$ProjectSecurity + SecurityLevel: CheckSecurity diff --git a/rules/001_project_settings/001_0011_strict_page_url_check.rego b/rules/001_project_settings/001_0011_strict_page_url_check.rego new file mode 100644 index 0000000..c13685e --- /dev/null +++ b/rules/001_project_settings/001_0011_strict_page_url_check.rego @@ -0,0 +1,37 @@ +# METADATA +# scope: package +# title: Strict page URL check should be enabled +# description: With strict page URL check enabled, pages can only be opened through a valid navigation path, which prevents users from reaching pages they should not have direct access to. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/security/securitystrictpageurl +# custom: +# category: Security +# rulename: StrictPageUrlCheck +# severity: MEDIUM +# rulenumber: "001_0011" +# remediation: Enable "Strict page URL check" in the project security settings. +# input: .*Security\$ProjectSecurity\.yaml +package app.mendix.project_settings.strict_page_url_check + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + input.StrictPageUrlCheck == false + + error := sprintf( + "[%v, %v, %v] Strict page URL check is disabled", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + ], + ) +} diff --git a/rules/001_project_settings/001_0011_strict_page_url_check_test.yaml b/rules/001_project_settings/001_0011_strict_page_url_check_test.yaml new file mode 100644 index 0000000..ce17753 --- /dev/null +++ b/rules/001_project_settings/001_0011_strict_page_url_check_test.yaml @@ -0,0 +1,11 @@ +TestCases: + - name: allow when strict page url check enabled + allow: true + input: + $Type: Security$ProjectSecurity + StrictPageUrlCheck: true + - name: do not allow when strict page url check disabled + allow: false + input: + $Type: Security$ProjectSecurity + StrictPageUrlCheck: false diff --git a/rules/001_project_settings/001_0012_allow_one_session_per_user.rego b/rules/001_project_settings/001_0012_allow_one_session_per_user.rego new file mode 100644 index 0000000..6314dc7 --- /dev/null +++ b/rules/001_project_settings/001_0012_allow_one_session_per_user.rego @@ -0,0 +1,39 @@ +# METADATA +# scope: package +# title: Allow only one session per user +# description: Allowing multiple concurrent sessions per user increases the attack surface and makes it harder to invalidate a session. Restrict users to a single active session unless there is a clear business reason. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/security/securityallowonesessionperuser +# custom: +# category: Security +# rulename: AllowOneSessionPerUser +# severity: MEDIUM +# rulenumber: "001_0012" +# remediation: Disable "Allow multiple sessions per user" in the project runtime settings. +# input: .*Settings\$ProjectSettings\.yaml +package app.mendix.project_settings.allow_one_session_per_user + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + some part in input.Settings + part["$Type"] == "Settings$ModelSettings" + part.AllowUserMultipleSessions == true + + error := sprintf( + "[%v, %v, %v] Multiple sessions per user are allowed", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + ], + ) +} diff --git a/rules/001_project_settings/001_0012_allow_one_session_per_user_test.yaml b/rules/001_project_settings/001_0012_allow_one_session_per_user_test.yaml new file mode 100644 index 0000000..acccad8 --- /dev/null +++ b/rules/001_project_settings/001_0012_allow_one_session_per_user_test.yaml @@ -0,0 +1,26 @@ +TestCases: + - name: allow single session per user + allow: true + input: + $Type: Settings$ProjectSettings + Settings: + - $Type: Settings$ModelSettings + AllowUserMultipleSessions: false + HashAlgorithm: BCrypt + - name: do not allow multiple sessions per user + allow: false + input: + $Type: Settings$ProjectSettings + Settings: + - $Type: Settings$ModelSettings + AllowUserMultipleSessions: true + HashAlgorithm: BCrypt + - name: allow when other settings parts present but model settings ok + allow: true + input: + $Type: Settings$ProjectSettings + Settings: + - $Type: Forms$WebUIProjectSettingsPart + UseOptimizedClient: "No" + - $Type: Settings$ModelSettings + AllowUserMultipleSessions: false diff --git a/rules/002_domain_model/002_0013_attribute_naming.rego b/rules/002_domain_model/002_0013_attribute_naming.rego new file mode 100644 index 0000000..951a04e --- /dev/null +++ b/rules/002_domain_model/002_0013_attribute_naming.rego @@ -0,0 +1,43 @@ +# METADATA +# scope: package +# title: Attributes should be named in PascalCase +# description: Consistent PascalCase attribute names (starting with an uppercase letter, letters and digits only) improve readability and maintainability across the domain model. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/project-hygiene/attributenameentity +# custom: +# category: ProjectHygiene +# rulename: AttributeNaming +# severity: LOW +# rulenumber: "002_0013" +# remediation: Rename the attribute to PascalCase, e.g. "OrderDate" instead of "order_date". +# input: .*/DomainModels\$DomainModel\.yaml +package app.mendix.domain_model.attribute_naming + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +pascal_case := `^[A-Z][a-zA-Z0-9]*$` + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + some entity in input.Entities + some attribute in entity.Attributes + not regex.match(pascal_case, attribute.Name) + + error := sprintf( + "[%v, %v, %v] Attribute %v.%v is not PascalCase", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + entity.Name, + attribute.Name, + ], + ) +} diff --git a/rules/002_domain_model/002_0013_attribute_naming_test.yaml b/rules/002_domain_model/002_0013_attribute_naming_test.yaml new file mode 100644 index 0000000..540acfc --- /dev/null +++ b/rules/002_domain_model/002_0013_attribute_naming_test.yaml @@ -0,0 +1,46 @@ +TestCases: + - name: allow PascalCase attributes + allow: true + input: + Entities: + - $Type: DomainModels$EntityImpl + Name: Order + Attributes: + - $Type: DomainModels$Attribute + Name: OrderDate + - $Type: DomainModels$Attribute + Name: TotalAmount + - name: allow entity without attributes + allow: true + input: + Entities: + - $Type: DomainModels$EntityImpl + Name: Empty + Attributes: [] + - name: do not allow snake_case attribute + allow: false + input: + Entities: + - $Type: DomainModels$EntityImpl + Name: Order + Attributes: + - $Type: DomainModels$Attribute + Name: order_date + - name: do not allow attribute starting with lowercase + allow: false + input: + Entities: + - $Type: DomainModels$EntityImpl + Name: Order + Attributes: + - $Type: DomainModels$Attribute + Name: totalAmount + - name: do not allow attribute with leading underscore + allow: false + input: + Entities: + - $Type: DomainModels$EntityImpl + Name: Order + Attributes: + - $Type: DomainModels$Attribute + Name: _Internal diff --git a/rules/004_pages/004_0005_page_documentation.rego b/rules/004_pages/004_0005_page_documentation.rego new file mode 100644 index 0000000..67f9aa2 --- /dev/null +++ b/rules/004_pages/004_0005_page_documentation.rego @@ -0,0 +1,40 @@ +# METADATA +# scope: package +# title: Pages should be documented +# description: Documentation on a page helps other developers understand its purpose and intended use. Pages without any documentation are harder to maintain. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/project-hygiene/pagedocumentation +# custom: +# category: ProjectHygiene +# rulename: PageDocumentation +# severity: LOW +# rulenumber: "004_0005" +# remediation: Add documentation to the page describing its purpose. +# input: .*\.Forms\$Page\.yaml +package app.mendix.pages.page_documentation + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + input["$Type"] == "Forms$Page" + documentation := trim_space(object.get(input, "Documentation", "")) + count(documentation) == 0 + + error := sprintf( + "[%v, %v, %v] Page %v has no documentation", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + object.get(input, "Name", ""), + ], + ) +} diff --git a/rules/004_pages/004_0005_page_documentation_test.yaml b/rules/004_pages/004_0005_page_documentation_test.yaml new file mode 100644 index 0000000..64b5656 --- /dev/null +++ b/rules/004_pages/004_0005_page_documentation_test.yaml @@ -0,0 +1,19 @@ +TestCases: + - name: allow documented page + allow: true + input: + $Type: Forms$Page + Name: Order_Overview + Documentation: "Shows all open orders for the current user." + - name: do not allow page without documentation + allow: false + input: + $Type: Forms$Page + Name: Order_Overview + Documentation: "" + - name: do not allow page with whitespace-only documentation + allow: false + input: + $Type: Forms$Page + Name: Order_Detail + Documentation: " " diff --git a/rules/005_microflows/005_0011_todo_annotations.rego b/rules/005_microflows/005_0011_todo_annotations.rego new file mode 100644 index 0000000..6c72f2c --- /dev/null +++ b/rules/005_microflows/005_0011_todo_annotations.rego @@ -0,0 +1,44 @@ +# METADATA +# scope: package +# title: Avoid leftover TODO annotations +# description: TODO or FIXME notes in microflow annotations signal unfinished work. They should be resolved and removed before the microflow is considered complete. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/project-hygiene/todoannotations +# custom: +# category: ProjectHygiene +# rulename: TodoAnnotations +# severity: LOW +# rulenumber: "005_0011" +# remediation: Resolve the outstanding work and remove the TODO/FIXME annotation. +# input: "**/*$Microflow.yaml" +package app.mendix.microflows.todo_annotations + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +todo_pattern := `(?i)\b(todo|fixme)\b` + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + some obj + walk(input, [_, obj]) + obj["$Type"] == "Microflows$Annotation" + regex.match(todo_pattern, object.get(obj, "Caption", "")) + + error := sprintf( + "[%v, %v, %v] Microflow %v contains a TODO/FIXME annotation: %v", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + input.Name, + obj.Caption, + ], + ) +} diff --git a/rules/005_microflows/005_0011_todo_annotations_test.yaml b/rules/005_microflows/005_0011_todo_annotations_test.yaml new file mode 100644 index 0000000..ef75720 --- /dev/null +++ b/rules/005_microflows/005_0011_todo_annotations_test.yaml @@ -0,0 +1,40 @@ +TestCases: + - name: allow microflow with a normal annotation + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$Annotation + Caption: "This flow processes orders." + - name: allow microflow without annotations + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + - name: do not allow TODO annotation + allow: false + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$Annotation + Caption: "TODO: handle the error case" + - name: do not allow lowercase fixme annotation + allow: false + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$Annotation + Caption: "fixme before release" From 7fbfd2687de0b81af7af668dca8b38e20c560d9b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 10:52:13 +0000 Subject: [PATCH 4/9] Add cross-file UnassignedModuleRole rule 001_0013 UnassignedModuleRole (JavaScript, cross-file): lists every module's Security$ModuleSecurity via mxlint.io.listdir/isdir/readYaml and flags module roles that are not assigned to any user role in the project security. Ships with ModuleA/ModuleB fixtures for the test. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018DNq3pmSsKsrV77qQ6tUcW --- .../001_0013_unassigned_module_role.js | 67 +++++++++++++++++++ .../001_0013_unassigned_module_role_test.yaml | 26 +++++++ .../ModuleA/Security$ModuleSecurity.yaml | 7 ++ .../ModuleB/Security$ModuleSecurity.yaml | 5 ++ 4 files changed, 105 insertions(+) create mode 100644 rules/001_project_settings/001_0013_unassigned_module_role.js create mode 100644 rules/001_project_settings/001_0013_unassigned_module_role_test.yaml create mode 100644 rules/001_project_settings/ModuleA/Security$ModuleSecurity.yaml create mode 100644 rules/001_project_settings/ModuleB/Security$ModuleSecurity.yaml diff --git a/rules/001_project_settings/001_0013_unassigned_module_role.js b/rules/001_project_settings/001_0013_unassigned_module_role.js new file mode 100644 index 0000000..dd591be --- /dev/null +++ b/rules/001_project_settings/001_0013_unassigned_module_role.js @@ -0,0 +1,67 @@ +const metadata = { + scope: "package", + title: "Module roles should be assigned to a user role", + description: "A module role that is not connected to any user role can never be granted to a user. It is usually a leftover or a modeling mistake and should be assigned or removed.", + authors: ["Xiwen Cheng "], + related_resources: ["https://sdf-docs.clevr.com/?docs=acr-rules/reliability/unassignedmodulerole"], + custom: { + category: "Reliability", + rulename: "UnassignedModuleRole", + severity: "LOW", + rulenumber: "001_0013", + remediation: "Assign the module role to at least one user role, or remove it from the module security.", + input: ".*Security\\$ProjectSecurity\\.yaml" + } +}; + +// Collect every module role defined across all modules by reading each +// module's Security$ModuleSecurity.yaml. Returns a set of "Module.Role". +function definedModuleRoles() { + const defined = new Set(); + let entries; + try { + entries = mxlint.io.listdir("."); + } catch (e) { + return defined; + } + for (const entry of entries) { + try { + if (!mxlint.io.isdir(entry)) { + continue; + } + const moduleSecurity = mxlint.io.readYaml(entry + "/Security$ModuleSecurity.yaml"); + if (!moduleSecurity || !moduleSecurity.ModuleRoles) { + continue; + } + for (const role of moduleSecurity.ModuleRoles) { + defined.add(entry + "." + role.Name); + } + } catch (e) { + // Not a module directory (no module security) - skip. + } + } + return defined; +} + +function rule(input = {}) { + const errors = []; + + // Module roles that are assigned to at least one user role. + const assigned = new Set(); + const userRoles = input.UserRoles || []; + for (const userRole of userRoles) { + for (const moduleRole of (userRole.ModuleRoles || [])) { + assigned.add(moduleRole); + } + } + + const defined = definedModuleRoles(); + for (const moduleRole of defined) { + if (!assigned.has(moduleRole)) { + errors.push(`[${metadata.custom.severity}, ${metadata.custom.category}, ${metadata.custom.rulenumber}] Module role ${moduleRole} is not assigned to any user role.`); + } + } + + const allow = errors.length === 0; + return { allow, errors }; +} diff --git a/rules/001_project_settings/001_0013_unassigned_module_role_test.yaml b/rules/001_project_settings/001_0013_unassigned_module_role_test.yaml new file mode 100644 index 0000000..6fa2289 --- /dev/null +++ b/rules/001_project_settings/001_0013_unassigned_module_role_test.yaml @@ -0,0 +1,26 @@ +TestCases: + - name: allow when all defined module roles are assigned + allow: true + input: + $Type: Security$ProjectSecurity + UserRoles: + - $Type: Security$UserRole + Name: Administrator + ModuleRoles: + - ModuleA.User + - ModuleA.Admin + - ModuleB.Manager + - name: do not allow when a module role is unassigned + allow: false + input: + $Type: Security$ProjectSecurity + UserRoles: + - $Type: Security$UserRole + Name: User + ModuleRoles: + - ModuleA.User + - name: do not allow when there are no user roles at all + allow: false + input: + $Type: Security$ProjectSecurity + UserRoles: [] diff --git a/rules/001_project_settings/ModuleA/Security$ModuleSecurity.yaml b/rules/001_project_settings/ModuleA/Security$ModuleSecurity.yaml new file mode 100644 index 0000000..96da061 --- /dev/null +++ b/rules/001_project_settings/ModuleA/Security$ModuleSecurity.yaml @@ -0,0 +1,7 @@ +# Fixture for 001_0013_unassigned_module_role_test.yaml (ignored by rule scanner). +$Type: Security$ModuleSecurity +ModuleRoles: + - $Type: Security$ModuleRole + Name: User + - $Type: Security$ModuleRole + Name: Admin diff --git a/rules/001_project_settings/ModuleB/Security$ModuleSecurity.yaml b/rules/001_project_settings/ModuleB/Security$ModuleSecurity.yaml new file mode 100644 index 0000000..4b5b361 --- /dev/null +++ b/rules/001_project_settings/ModuleB/Security$ModuleSecurity.yaml @@ -0,0 +1,5 @@ +# Fixture for 001_0013_unassigned_module_role_test.yaml (ignored by rule scanner). +$Type: Security$ModuleSecurity +ModuleRoles: + - $Type: Security$ModuleRole + Name: Manager From 24d471f18215103dd2c7a73d75767682e164d03b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 10:54:09 +0000 Subject: [PATCH 5/9] Add count-based ACR maintainability rules - 002_0014 AmountAccessRules: limit access rules per entity. - 005_0012 ParameterAmount: limit microflow parameters. - 006_0002 ModuleRolesAmount: limit module roles per module. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018DNq3pmSsKsrV77qQ6tUcW --- .../002_0014_amount_access_rules.rego | 44 ++++++++++++++++ .../002_0014_amount_access_rules_test.yaml | 50 +++++++++++++++++++ .../005_0012_parameter_amount.rego | 47 +++++++++++++++++ .../005_0012_parameter_amount_test.yaml | 46 +++++++++++++++++ .../006_0002_module_roles_amount.rego | 43 ++++++++++++++++ .../006_0002_module_roles_amount_test.yaml | 26 ++++++++++ 6 files changed, 256 insertions(+) create mode 100644 rules/002_domain_model/002_0014_amount_access_rules.rego create mode 100644 rules/002_domain_model/002_0014_amount_access_rules_test.yaml create mode 100644 rules/005_microflows/005_0012_parameter_amount.rego create mode 100644 rules/005_microflows/005_0012_parameter_amount_test.yaml create mode 100644 rules/006_security/006_0002_module_roles_amount.rego create mode 100644 rules/006_security/006_0002_module_roles_amount_test.yaml diff --git a/rules/002_domain_model/002_0014_amount_access_rules.rego b/rules/002_domain_model/002_0014_amount_access_rules.rego new file mode 100644 index 0000000..5b62d52 --- /dev/null +++ b/rules/002_domain_model/002_0014_amount_access_rules.rego @@ -0,0 +1,44 @@ +# METADATA +# scope: package +# title: Limit the number of access rules per entity +# description: An entity with a large number of access rules is hard to reason about and increases the risk of misconfigured security. Keep the number of access rules per entity manageable. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/maintainability/amountaccessrules +# custom: +# category: Maintainability +# rulename: AmountAccessRules +# severity: LOW +# rulenumber: "002_0014" +# remediation: Consolidate access rules or split the entity so that each entity has a manageable number of access rules. +# input: .*/DomainModels\$DomainModel\.yaml +package app.mendix.domain_model.amount_access_rules + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +max_access_rules := 10 + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + some entity in input.Entities + rule_count := count(entity.AccessRules) + rule_count > max_access_rules + + error := sprintf( + "[%v, %v, %v] Entity %v has %v access rules, which exceeds the maximum of %v", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + entity.Name, + rule_count, + max_access_rules, + ], + ) +} diff --git a/rules/002_domain_model/002_0014_amount_access_rules_test.yaml b/rules/002_domain_model/002_0014_amount_access_rules_test.yaml new file mode 100644 index 0000000..20500d9 --- /dev/null +++ b/rules/002_domain_model/002_0014_amount_access_rules_test.yaml @@ -0,0 +1,50 @@ +TestCases: + - name: allow entity with few access rules + allow: true + input: + Entities: + - $Type: DomainModels$EntityImpl + Name: SmallEntity + AccessRules: + - $Type: DomainModels$AccessRule + DefaultMemberAccessRights: ReadOnly + - $Type: DomainModels$AccessRule + DefaultMemberAccessRights: ReadOnly + - $Type: DomainModels$AccessRule + DefaultMemberAccessRights: ReadOnly + - name: allow entity with no access rules + allow: true + input: + Entities: + - $Type: DomainModels$EntityImpl + Name: NoRules + AccessRules: [] + - name: do not allow entity with too many access rules + allow: false + input: + Entities: + - $Type: DomainModels$EntityImpl + Name: BigEntity + AccessRules: + - $Type: DomainModels$AccessRule + DefaultMemberAccessRights: ReadOnly + - $Type: DomainModels$AccessRule + DefaultMemberAccessRights: ReadOnly + - $Type: DomainModels$AccessRule + DefaultMemberAccessRights: ReadOnly + - $Type: DomainModels$AccessRule + DefaultMemberAccessRights: ReadOnly + - $Type: DomainModels$AccessRule + DefaultMemberAccessRights: ReadOnly + - $Type: DomainModels$AccessRule + DefaultMemberAccessRights: ReadOnly + - $Type: DomainModels$AccessRule + DefaultMemberAccessRights: ReadOnly + - $Type: DomainModels$AccessRule + DefaultMemberAccessRights: ReadOnly + - $Type: DomainModels$AccessRule + DefaultMemberAccessRights: ReadOnly + - $Type: DomainModels$AccessRule + DefaultMemberAccessRights: ReadOnly + - $Type: DomainModels$AccessRule + DefaultMemberAccessRights: ReadOnly diff --git a/rules/005_microflows/005_0012_parameter_amount.rego b/rules/005_microflows/005_0012_parameter_amount.rego new file mode 100644 index 0000000..8cd4cf8 --- /dev/null +++ b/rules/005_microflows/005_0012_parameter_amount.rego @@ -0,0 +1,47 @@ +# METADATA +# scope: package +# title: Limit the number of microflow parameters +# description: A microflow with many parameters is harder to understand, reuse and test. Consider passing an object or splitting the logic when the number of parameters grows too large. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/maintainability/parameteramount +# custom: +# category: Maintainability +# rulename: ParameterAmount +# severity: LOW +# rulenumber: "005_0012" +# remediation: Reduce the number of parameters, for example by passing a single object that groups related values. +# input: "**/*$Microflow.yaml" +package app.mendix.microflows.parameter_amount + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +max_parameters := 5 + +default allow := false + +allow if count(errors) == 0 + +parameter_count := count([obj | + some obj in input.ObjectCollection.Objects + obj["$Type"] == "Microflows$MicroflowParameter" +]) + +errors contains error if { + parameter_count > max_parameters + + error := sprintf( + "[%v, %v, %v] Microflow %v has %v parameters, which exceeds the maximum of %v", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + input.Name, + parameter_count, + max_parameters, + ], + ) +} diff --git a/rules/005_microflows/005_0012_parameter_amount_test.yaml b/rules/005_microflows/005_0012_parameter_amount_test.yaml new file mode 100644 index 0000000..1e753d0 --- /dev/null +++ b/rules/005_microflows/005_0012_parameter_amount_test.yaml @@ -0,0 +1,46 @@ +TestCases: + - name: allow microflow with few parameters + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$StartEvent + - $Type: Microflows$MicroflowParameter + Name: Param0 + - $Type: Microflows$MicroflowParameter + Name: Param1 + - $Type: Microflows$MicroflowParameter + Name: Param2 + - name: allow microflow with no parameters + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$StartEvent + - name: do not allow microflow with too many parameters + allow: false + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$StartEvent + - $Type: Microflows$MicroflowParameter + Name: Param0 + - $Type: Microflows$MicroflowParameter + Name: Param1 + - $Type: Microflows$MicroflowParameter + Name: Param2 + - $Type: Microflows$MicroflowParameter + Name: Param3 + - $Type: Microflows$MicroflowParameter + Name: Param4 + - $Type: Microflows$MicroflowParameter + Name: Param5 diff --git a/rules/006_security/006_0002_module_roles_amount.rego b/rules/006_security/006_0002_module_roles_amount.rego new file mode 100644 index 0000000..5398a91 --- /dev/null +++ b/rules/006_security/006_0002_module_roles_amount.rego @@ -0,0 +1,43 @@ +# METADATA +# scope: package +# title: Limit the number of module roles per module +# description: A module with many module roles increases the complexity of the security model. Keep the number of module roles per module small and map them to user roles at the project level. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/maintainability/modulerolesamount +# custom: +# category: Maintainability +# rulename: ModuleRolesAmount +# severity: LOW +# rulenumber: "006_0002" +# remediation: Reduce the number of module roles by combining roles that share the same access, or move variation to the user-role level. +# input: .*Security\$ModuleSecurity\.yaml +package app.mendix.security.module_roles_amount + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +max_module_roles := 5 + +default allow := false + +allow if count(errors) == 0 + +module_role_count := count(object.get(input, "ModuleRoles", [])) + +errors contains error if { + module_role_count > max_module_roles + + error := sprintf( + "[%v, %v, %v] Module has %v module roles, which exceeds the maximum of %v", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + module_role_count, + max_module_roles, + ], + ) +} diff --git a/rules/006_security/006_0002_module_roles_amount_test.yaml b/rules/006_security/006_0002_module_roles_amount_test.yaml new file mode 100644 index 0000000..1077420 --- /dev/null +++ b/rules/006_security/006_0002_module_roles_amount_test.yaml @@ -0,0 +1,26 @@ +TestCases: + - name: allow module with few roles + allow: true + input: + $Type: Security$ModuleSecurity + ModuleRoles: + - $Type: Security$ModuleRole + Name: User + - $Type: Security$ModuleRole + Name: Admin + - name: allow module with no roles + allow: true + input: + $Type: Security$ModuleSecurity + ModuleRoles: [] + - name: do not allow module with too many roles + allow: false + input: + $Type: Security$ModuleSecurity + ModuleRoles: + - {Name: Role1} + - {Name: Role2} + - {Name: Role3} + - {Name: Role4} + - {Name: Role5} + - {Name: Role6} From a9698e731aa115aebd4295e515f07bea52556053 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 10:55:37 +0000 Subject: [PATCH 6/9] Add microflow entity-access and split-caption rules - 006_0003 MicroflowEntityAccess: exposed microflows performing database operations should apply entity access. - 005_0013 SplitCaption: exclusive/inclusive splits should have a caption. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018DNq3pmSsKsrV77qQ6tUcW --- .../005_0013_split_caption.rego | 44 +++++++++++++ .../005_0013_split_caption_test.yaml | 40 ++++++++++++ .../006_0003_microflow_entity_access.rego | 61 +++++++++++++++++++ ...006_0003_microflow_entity_access_test.yaml | 56 +++++++++++++++++ 4 files changed, 201 insertions(+) create mode 100644 rules/005_microflows/005_0013_split_caption.rego create mode 100644 rules/005_microflows/005_0013_split_caption_test.yaml create mode 100644 rules/006_security/006_0003_microflow_entity_access.rego create mode 100644 rules/006_security/006_0003_microflow_entity_access_test.yaml diff --git a/rules/005_microflows/005_0013_split_caption.rego b/rules/005_microflows/005_0013_split_caption.rego new file mode 100644 index 0000000..2135129 --- /dev/null +++ b/rules/005_microflows/005_0013_split_caption.rego @@ -0,0 +1,44 @@ +# METADATA +# scope: package +# title: Splits should have a descriptive caption +# description: A caption on an exclusive or inclusive split documents the decision being made and makes the microflow much easier to read. Splits without a caption hide their intent. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/maintainability/splitcaption +# custom: +# category: Maintainability +# rulename: SplitCaption +# severity: LOW +# rulenumber: "005_0013" +# remediation: Give each split a short caption describing the decision it represents. +# input: "**/*$Microflow.yaml" +package app.mendix.microflows.split_caption + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +split_types := {"Microflows$ExclusiveSplit", "Microflows$InclusiveSplit"} + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + some obj + walk(input, [_, obj]) + split_types[object.get(obj, "$Type", "")] + caption := trim_space(object.get(obj, "Caption", "")) + count(caption) == 0 + + error := sprintf( + "[%v, %v, %v] Microflow %v has a split without a caption", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + input.Name, + ], + ) +} diff --git a/rules/005_microflows/005_0013_split_caption_test.yaml b/rules/005_microflows/005_0013_split_caption_test.yaml new file mode 100644 index 0000000..5c506b6 --- /dev/null +++ b/rules/005_microflows/005_0013_split_caption_test.yaml @@ -0,0 +1,40 @@ +TestCases: + - name: allow split with a caption + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ExclusiveSplit + Caption: "Is blocked?" + - name: allow microflow without splits + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + - name: do not allow split without caption + allow: false + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ExclusiveSplit + Caption: "" + - name: do not allow inclusive split with whitespace caption + allow: false + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$InclusiveSplit + Caption: " " diff --git a/rules/006_security/006_0003_microflow_entity_access.rego b/rules/006_security/006_0003_microflow_entity_access.rego new file mode 100644 index 0000000..f876d67 --- /dev/null +++ b/rules/006_security/006_0003_microflow_entity_access.rego @@ -0,0 +1,61 @@ +# METADATA +# scope: package +# title: Exposed microflows should apply entity access +# description: A microflow that is directly accessible by end users (it has allowed module roles) and performs database operations should apply entity access, otherwise it can bypass the entity security rules. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/security/microflowentityaccess +# custom: +# category: Security +# rulename: MicroflowEntityAccess +# severity: HIGH +# rulenumber: "006_0003" +# remediation: Enable "Apply entity access" on the microflow, or restrict who can call it directly. +# input: "**/*$Microflow.yaml" +package app.mendix.security.microflow_entity_access + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +database_actions := { + "Microflows$RetrieveAction", + "Microflows$CreateChangeAction", + "Microflows$ChangeAction", + "Microflows$DeleteAction", + "Microflows$CommitAction", + "Microflows$RollbackAction", + "Microflows$AggregateListAction", +} + +default allow := false + +allow if count(errors) == 0 + +has_database_action if { + some obj + walk(input, [_, obj]) + database_actions[object.get(obj, "$Type", "")] +} + +errors contains error if { + # Directly callable by end users. + count(object.get(input, "AllowedModuleRoles", [])) > 0 + + # Entity access is not applied. + input.ApplyEntityAccess == false + + # And the microflow touches the database. + has_database_action + + error := sprintf( + "[%v, %v, %v] Microflow %v is exposed to module roles but does not apply entity access", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + input.Name, + ], + ) +} diff --git a/rules/006_security/006_0003_microflow_entity_access_test.yaml b/rules/006_security/006_0003_microflow_entity_access_test.yaml new file mode 100644 index 0000000..013dad1 --- /dev/null +++ b/rules/006_security/006_0003_microflow_entity_access_test.yaml @@ -0,0 +1,56 @@ +TestCases: + - name: allow exposed microflow that applies entity access + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + AllowedModuleRoles: + - MyModule.User + ApplyEntityAccess: true + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$RetrieveAction + - name: allow non-exposed microflow without entity access + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + AllowedModuleRoles: [] + ApplyEntityAccess: false + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$RetrieveAction + - name: allow exposed microflow without database operations + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + AllowedModuleRoles: + - MyModule.User + ApplyEntityAccess: false + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$LogMessageAction + - name: do not allow exposed microflow with db action and no entity access + allow: false + input: + $Type: Microflows$Microflow + Name: MyFlow + AllowedModuleRoles: + - MyModule.User + ApplyEntityAccess: false + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$RetrieveAction From 93b50e349af045982253965230632c3ed9f3f9ef Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 10:57:03 +0000 Subject: [PATCH 7/9] Add enumeration prefix and Mendix version rules - 007_0001 EnumerationPrefix (new enumerations category): enumerations should start with a recognizable prefix (ENU). - 003_0002 MinimumMendixVersion: flag unsupported Mendix major versions read from Metadata ProductVersion. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018DNq3pmSsKsrV77qQ6tUcW --- .../003_0002_minimum_mendix_version.rego | 43 +++++++++++++++++++ .../003_0002_minimum_mendix_version_test.yaml | 17 ++++++++ .../007_0001_enumeration_prefix.rego | 42 ++++++++++++++++++ .../007_0001_enumeration_prefix_test.yaml | 21 +++++++++ 4 files changed, 123 insertions(+) create mode 100644 rules/003_modules/003_0002_minimum_mendix_version.rego create mode 100644 rules/003_modules/003_0002_minimum_mendix_version_test.yaml create mode 100644 rules/007_enumerations/007_0001_enumeration_prefix.rego create mode 100644 rules/007_enumerations/007_0001_enumeration_prefix_test.yaml diff --git a/rules/003_modules/003_0002_minimum_mendix_version.rego b/rules/003_modules/003_0002_minimum_mendix_version.rego new file mode 100644 index 0000000..9b1d93f --- /dev/null +++ b/rules/003_modules/003_0002_minimum_mendix_version.rego @@ -0,0 +1,43 @@ +# METADATA +# scope: package +# title: Use a supported Mendix major version +# description: Older Mendix major versions no longer receive security patches or bug fixes. Running on an unsupported major version (for example 5, 6 or 7) is a maintainability and security risk. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/maintainability/latestmendixverion +# custom: +# category: Maintainability +# rulename: MinimumMendixVersion +# severity: MEDIUM +# rulenumber: "003_0002" +# remediation: Upgrade the app to a currently supported Mendix major version. +# input: .*Metadata\.yaml +package app.mendix.modules.minimum_mendix_version + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +minimum_major := 9 + +default allow := false + +allow if count(errors) == 0 + +major_version := to_number(split(input.ProductVersion, ".")[0]) + +errors contains error if { + major_version < minimum_major + + error := sprintf( + "[%v, %v, %v] Mendix major version %v is below the minimum supported version %v", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + major_version, + minimum_major, + ], + ) +} diff --git a/rules/003_modules/003_0002_minimum_mendix_version_test.yaml b/rules/003_modules/003_0002_minimum_mendix_version_test.yaml new file mode 100644 index 0000000..1721754 --- /dev/null +++ b/rules/003_modules/003_0002_minimum_mendix_version_test.yaml @@ -0,0 +1,17 @@ +TestCases: + - name: allow current Mendix version + allow: true + input: + ProductVersion: 10.24.16.96987 + - name: allow minimum supported version + allow: true + input: + ProductVersion: 9.24.2.1234 + - name: do not allow Mendix 7 + allow: false + input: + ProductVersion: 7.23.5.100 + - name: do not allow Mendix 6 + allow: false + input: + ProductVersion: 6.10.0.500 diff --git a/rules/007_enumerations/007_0001_enumeration_prefix.rego b/rules/007_enumerations/007_0001_enumeration_prefix.rego new file mode 100644 index 0000000..5091458 --- /dev/null +++ b/rules/007_enumerations/007_0001_enumeration_prefix.rego @@ -0,0 +1,42 @@ +# METADATA +# scope: package +# title: Enumerations should have a recognizable prefix +# description: Prefixing enumerations with a short string such as ENU makes it much easier to find all enumerations, for example in microflow expressions. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/project-hygiene/enumprefix +# custom: +# category: ProjectHygiene +# rulename: EnumerationPrefix +# severity: LOW +# rulenumber: "007_0001" +# remediation: Rename the enumeration so that it starts with the agreed prefix, for example ENU_Status. +# input: .*\.Enumerations\$Enumeration\.yaml +package app.mendix.enumerations.enumeration_prefix + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +required_prefix := "ENU" + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + input["$Type"] == "Enumerations$Enumeration" + not startswith(input.Name, required_prefix) + + error := sprintf( + "[%v, %v, %v] Enumeration %v does not start with the required prefix %v", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + input.Name, + required_prefix, + ], + ) +} diff --git a/rules/007_enumerations/007_0001_enumeration_prefix_test.yaml b/rules/007_enumerations/007_0001_enumeration_prefix_test.yaml new file mode 100644 index 0000000..7b7ab94 --- /dev/null +++ b/rules/007_enumerations/007_0001_enumeration_prefix_test.yaml @@ -0,0 +1,21 @@ +TestCases: + - name: allow enumeration with ENU prefix + allow: true + input: + $Type: Enumerations$Enumeration + Name: ENU_Status + - name: allow enumeration named ENUM prefix + allow: true + input: + $Type: Enumerations$Enumeration + Name: ENUMStatus + - name: do not allow enumeration without prefix + allow: false + input: + $Type: Enumerations$Enumeration + Name: EnumerationStatus + - name: do not allow enumeration with different prefix + allow: false + input: + $Type: Enumerations$Enumeration + Name: E_Status From 93c725397e8a4ce8e268502b24a1e1e64ce46608 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 10:58:40 +0000 Subject: [PATCH 8/9] Add no-continue and loop-heavy-action microflow rules - 005_0014 NoContinueErrorHandling: flag activities that continue on error without handling it. - 005_0015 LoopHeavyAction: flag heavy actions (retrieve, microflow call, web service, mapping, etc.) inside a loop. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018DNq3pmSsKsrV77qQ6tUcW --- .../005_0014_no_continue_error_handling.rego | 41 +++++++++++++ ..._0014_no_continue_error_handling_test.yaml | 37 ++++++++++++ .../005_0015_loop_heavy_action.rego | 56 ++++++++++++++++++ .../005_0015_loop_heavy_action_test.yaml | 58 +++++++++++++++++++ 4 files changed, 192 insertions(+) create mode 100644 rules/005_microflows/005_0014_no_continue_error_handling.rego create mode 100644 rules/005_microflows/005_0014_no_continue_error_handling_test.yaml create mode 100644 rules/005_microflows/005_0015_loop_heavy_action.rego create mode 100644 rules/005_microflows/005_0015_loop_heavy_action_test.yaml diff --git a/rules/005_microflows/005_0014_no_continue_error_handling.rego b/rules/005_microflows/005_0014_no_continue_error_handling.rego new file mode 100644 index 0000000..1208212 --- /dev/null +++ b/rules/005_microflows/005_0014_no_continue_error_handling.rego @@ -0,0 +1,41 @@ +# METADATA +# scope: package +# title: Do not silently continue on errors +# description: An activity with "Continue" error handling completely ignores the error. At the very least the error should be logged and handled, otherwise failures pass by unnoticed. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/maintainability/nocontinues +# - https://docs.mendix.com/howto/logic-business-rules/set-up-error-handling +# custom: +# category: Maintainability +# rulename: NoContinueErrorHandling +# severity: MEDIUM +# rulenumber: "005_0014" +# remediation: Use custom error handling that at least logs the error, instead of continuing silently. +# input: "**/*$Microflow.yaml" +package app.mendix.microflows.no_continue_error_handling + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + some obj + walk(input, [_, obj]) + object.get(obj, "ErrorHandlingType", "") == "Continue" + + error := sprintf( + "[%v, %v, %v] Microflow %v has an activity that continues on error without handling it", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + input.Name, + ], + ) +} diff --git a/rules/005_microflows/005_0014_no_continue_error_handling_test.yaml b/rules/005_microflows/005_0014_no_continue_error_handling_test.yaml new file mode 100644 index 0000000..2f08177 --- /dev/null +++ b/rules/005_microflows/005_0014_no_continue_error_handling_test.yaml @@ -0,0 +1,37 @@ +TestCases: + - name: allow rollback error handling + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$RetrieveAction + ErrorHandlingType: Rollback + - name: allow custom error handling + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$RetrieveAction + ErrorHandlingType: Custom + - name: do not allow continue error handling + allow: false + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$RetrieveAction + ErrorHandlingType: Continue diff --git a/rules/005_microflows/005_0015_loop_heavy_action.rego b/rules/005_microflows/005_0015_loop_heavy_action.rego new file mode 100644 index 0000000..e47c14d --- /dev/null +++ b/rules/005_microflows/005_0015_loop_heavy_action.rego @@ -0,0 +1,56 @@ +# METADATA +# scope: package +# title: Avoid heavy actions inside a loop +# description: A loop can run hundreds or thousands of times. Performing heavy actions such as database retrieves, microflow calls or web service calls inside a loop multiplies their cost and quickly leads to performance problems. +# authors: +# - Xiwen Cheng +# related_resources: +# - https://sdf-docs.clevr.com/?docs=acr-rules/performance/loopheavyaction +# custom: +# category: Performance +# rulename: LoopHeavyAction +# severity: MEDIUM +# rulenumber: "005_0015" +# remediation: Move the heavy action out of the loop, for example by retrieving all required data once before the loop. +# input: "**/*$Microflow.yaml" +package app.mendix.microflows.loop_heavy_action + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +heavy_actions := { + "Microflows$RetrieveAction", + "Microflows$MicroflowCallAction", + "Microflows$DeleteAction", + "Microflows$AggregateListAction", + "Microflows$RestCallAction", + "Microflows$WebServiceCallAction", + "Microflows$ImportMappingAction", + "Microflows$ExportMappingAction", +} + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + some loop + walk(input, [_, loop]) + loop["$Type"] == "Microflows$LoopedActivity" + + some obj + walk(loop.ObjectCollection, [_, obj]) + heavy_actions[object.get(obj, "$Type", "")] + + error := sprintf( + "[%v, %v, %v] Microflow %v performs a heavy action (%v) inside a loop", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + input.Name, + obj["$Type"], + ], + ) +} diff --git a/rules/005_microflows/005_0015_loop_heavy_action_test.yaml b/rules/005_microflows/005_0015_loop_heavy_action_test.yaml new file mode 100644 index 0000000..18b3325 --- /dev/null +++ b/rules/005_microflows/005_0015_loop_heavy_action_test.yaml @@ -0,0 +1,58 @@ +TestCases: + - name: allow retrieve outside of a loop + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$RetrieveAction + - name: allow loop with only lightweight actions + allow: true + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$ChangeAction + Commit: "No" + - name: do not allow retrieve inside a loop + allow: false + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$RetrieveAction + - name: do not allow microflow call inside a loop + allow: false + input: + $Type: Microflows$Microflow + Name: MyFlow + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$LoopedActivity + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ActionActivity + Action: + $Type: Microflows$MicroflowCallAction From 9233998cbd1e097892870e9746be28dc0c29cd3d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 11:55:47 +0000 Subject: [PATCH 9/9] Fix pre-existing rule tests and make CI actually run them The three rego rules 001_0007, 001_0008 and 005_0005 shipped OPA-native _test.rego files, which `mxlint test-rules` does not execute (it looks for _test.yaml). Convert them to _test.yaml so they are actually exercised. The 001_0009 strict-mode JavaScript rule reads Security$ProjectSecurity via mxlint.io; its test used an unsupported `files:` key and the wrong input shape, so it never passed. Rewrite the test to use the real ProjectSettings shape plus a Security$ProjectSecurity.yaml fixture. Also fix scripts/run-policy-tests.sh: newer mxlint-cli releases removed the `--rules` flag, so `test-rules --rules ./rules` errored out and the script reported success without running any tests. Point test-rules at a generated config instead so the suite actually runs (176 test cases pass). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018DNq3pmSsKsrV77qQ6tUcW --- .../001_0007_hash_algorithm_test.rego | 36 --------- .../001_0007_hash_algorithm_test.yaml | 25 ++++++ ...008_check_security_on_user_roles_test.rego | 24 ------ ...008_check_security_on_user_roles_test.yaml | 15 ++++ ...09_strict_mode_with_react_client_test.yaml | 40 +++++----- .../Security$ProjectSecurity.yaml | 3 + .../005_0005_nested_if_statements_test.rego | 76 ------------------- .../005_0005_nested_if_statements_test.yaml | 59 ++++++++++++++ scripts/run-policy-tests.sh | 12 ++- 9 files changed, 132 insertions(+), 158 deletions(-) delete mode 100644 rules/001_project_settings/001_0007_hash_algorithm_test.rego create mode 100644 rules/001_project_settings/001_0007_hash_algorithm_test.yaml delete mode 100644 rules/001_project_settings/001_0008_check_security_on_user_roles_test.rego create mode 100644 rules/001_project_settings/001_0008_check_security_on_user_roles_test.yaml create mode 100644 rules/001_project_settings/Security$ProjectSecurity.yaml delete mode 100644 rules/005_microflows/005_0005_nested_if_statements_test.rego create mode 100644 rules/005_microflows/005_0005_nested_if_statements_test.yaml diff --git a/rules/001_project_settings/001_0007_hash_algorithm_test.rego b/rules/001_project_settings/001_0007_hash_algorithm_test.rego deleted file mode 100644 index 5c2ed2f..0000000 --- a/rules/001_project_settings/001_0007_hash_algorithm_test.rego +++ /dev/null @@ -1,36 +0,0 @@ -package app.mendix.project_settings.hash_algorithm_test - -import data.app.mendix.project_settings.hash_algorithm -import rego.v1 - -# Test data -bcrypt := {"Settings": { - "$Type": "Settings$ModelSettings", - "HashAlgorithm": "BCrypt", -}} - -ssha256 := {"Settings": { - "$Type": "Settings$ModelSettings", - "HashAlgorithm": "SSHA256", -}} - -sha256 := {"Settings": { - "$Type": "Settings$ModelSettings", - "HashAlgorithm": "SHA256", -}} - -md5 := {"Settings": { - "$Type": "Settings$ModelSettings", - "HashAlgorithm": "MD5", -}} - -# Test cases -test_should_allow_when_build_version_in_allowed_list if { - hash_algorithm.allow with input as bcrypt - hash_algorithm.allow with input as ssha256 -} - -test_should_deny_when_build_version_not_in_allowed_list if { - not hash_algorithm.allow with input as sha256 - not hash_algorithm.allow with input as md5 -} diff --git a/rules/001_project_settings/001_0007_hash_algorithm_test.yaml b/rules/001_project_settings/001_0007_hash_algorithm_test.yaml new file mode 100644 index 0000000..5fcf602 --- /dev/null +++ b/rules/001_project_settings/001_0007_hash_algorithm_test.yaml @@ -0,0 +1,25 @@ +TestCases: + - name: allow BCrypt hash algorithm + allow: true + input: + Settings: + $Type: Settings$ModelSettings + HashAlgorithm: BCrypt + - name: allow SSHA256 hash algorithm + allow: true + input: + Settings: + $Type: Settings$ModelSettings + HashAlgorithm: SSHA256 + - name: do not allow SHA256 hash algorithm + allow: false + input: + Settings: + $Type: Settings$ModelSettings + HashAlgorithm: SHA256 + - name: do not allow MD5 hash algorithm + allow: false + input: + Settings: + $Type: Settings$ModelSettings + HashAlgorithm: MD5 diff --git a/rules/001_project_settings/001_0008_check_security_on_user_roles_test.rego b/rules/001_project_settings/001_0008_check_security_on_user_roles_test.rego deleted file mode 100644 index ca72a4f..0000000 --- a/rules/001_project_settings/001_0008_check_security_on_user_roles_test.rego +++ /dev/null @@ -1,24 +0,0 @@ -package app.mendix.project_settings.check_security_on_user_roles_test - -import data.app.mendix.project_settings.check_security_on_user_roles -import rego.v1 - -# Test data -check_for_security := {"UserRoles": [{ - "CheckSecurity": true, - "Name": "Administrator", -}]} - -not_check_for_security := {"UserRoles": [{ - "CheckSecurity": false, - "Name": "Administrator", -}]} - -# Test cases -test_should_allow_when_checking_user_roles_for_security if { - check_security_on_user_roles.allow with input as check_for_security -} - -test_should_deny_when_not_checking_user_roles_for_security if { - not check_security_on_user_roles.allow with input as not_check_for_security -} diff --git a/rules/001_project_settings/001_0008_check_security_on_user_roles_test.yaml b/rules/001_project_settings/001_0008_check_security_on_user_roles_test.yaml new file mode 100644 index 0000000..cc9f72e --- /dev/null +++ b/rules/001_project_settings/001_0008_check_security_on_user_roles_test.yaml @@ -0,0 +1,15 @@ +TestCases: + - name: allow when user role is checked for security + allow: true + input: + UserRoles: + - $Type: Security$UserRole + Name: Administrator + CheckSecurity: true + - name: do not allow when user role is not checked for security + allow: false + input: + UserRoles: + - $Type: Security$UserRole + Name: Administrator + CheckSecurity: false diff --git a/rules/001_project_settings/001_0009_strict_mode_with_react_client_test.yaml b/rules/001_project_settings/001_0009_strict_mode_with_react_client_test.yaml index 3219eb3..2e2c279 100644 --- a/rules/001_project_settings/001_0009_strict_mode_with_react_client_test.yaml +++ b/rules/001_project_settings/001_0009_strict_mode_with_react_client_test.yaml @@ -1,28 +1,26 @@ +# The rule reads Security$ProjectSecurity.yaml from the working directory. +# During tests the working directory is this rule's folder, which contains a +# fixture Security$ProjectSecurity.yaml with StrictMode disabled. Cases that +# enable the optimized (React) client therefore expect a violation. TestCases: - - name: allow when UseOptimizedClient is false + - name: allow when optimized client is disabled allow: true input: - UseOptimizedClient: false - files: - "Security$ProjectSecurity.yaml": - StrictMode: false - - name: allow when both UseOptimizedClient and StrictMode are true + $Type: Settings$ProjectSettings + Settings: + - $Type: Forms$WebUIProjectSettingsPart + UseOptimizedClient: "No" + - name: allow when optimized client setting is absent allow: true input: - UseOptimizedClient: true - files: - "Security$ProjectSecurity.yaml": - StrictMode: true - - name: do not allow UseOptimizedClient true but StrictMode false + $Type: Settings$ProjectSettings + Settings: + - $Type: Settings$ModelSettings + HashAlgorithm: BCrypt + - name: do not allow optimized client enabled while strict mode is disabled allow: false input: - UseOptimizedClient: true - files: - "Security$ProjectSecurity.yaml": - StrictMode: false - - name: do not allow UseOptimizedClient true but StrictMode missing - allow: false - input: - UseOptimizedClient: true - files: - "Security$ProjectSecurity.yaml": {} + $Type: Settings$ProjectSettings + Settings: + - $Type: Forms$WebUIProjectSettingsPart + UseOptimizedClient: "Yes" diff --git a/rules/001_project_settings/Security$ProjectSecurity.yaml b/rules/001_project_settings/Security$ProjectSecurity.yaml new file mode 100644 index 0000000..fd45bd1 --- /dev/null +++ b/rules/001_project_settings/Security$ProjectSecurity.yaml @@ -0,0 +1,3 @@ +# Fixture for 001_0009_strict_mode_with_react_client_test.yaml (ignored by rule scanner). +$Type: Security$ProjectSecurity +StrictMode: false diff --git a/rules/005_microflows/005_0005_nested_if_statements_test.rego b/rules/005_microflows/005_0005_nested_if_statements_test.rego deleted file mode 100644 index 00362f0..0000000 --- a/rules/005_microflows/005_0005_nested_if_statements_test.rego +++ /dev/null @@ -1,76 +0,0 @@ -package app.mendix.microflows.nested_if_statements_test - -import data.app.mendix.microflows.nested_if_statements -import rego.v1 - -# Test data -one_exclusive_split_with_no_nested_ifs := {"ObjectCollection": { - "$Type": "Microflows$MicroflowObjectCollection", - "Objects": [{ - "$Type": "Microflows$ExpressionSplitCondition", - "Caption": "no nested ifs", - "SplitCondition": {"Expression": "if $Variable then a else b"}, - }], -}} - -multiple_exclusive_splits_with_no_nested_ifs := {"ObjectCollection": { - "$Type": "Microflows$MicroflowObjectCollection", - "Objects": [ - { - "$Type": "Microflows$ExpressionSplitCondition", - "Caption": "ex spit 1 with no nested ifs", - "SplitCondition": {"Expression": "if $Variable then a else b"}, - }, - { - "$Type": "Microflows$ExpressionSplitCondition", - "Caption": "ex split 2 with no nested ifs", - "SplitCondition": {"Expression": "if $Variable then c else d"}, - }, - ], -}} - -one_exclusive_split_with_else_if := {"ObjectCollection": { - "$Type": "Microflows$MicroflowObjectCollection", - "Objects": [{ - "$Type": "Microflows$ExclusiveSplit", - "Caption": "ex split with else-if", - "SplitCondition": {"Expression": "if true then\n\tfalse\nelse if false then\n\ttrue\nelse false"}, - }], -}} - -one_exclusive_split_with_then_if := {"ObjectCollection": { - "$Type": "Microflows$MicroflowObjectCollection", - "Objects": [{ - "$Type": "Microflows$ExclusiveSplit", - "Caption": "ex split with then-if", - "SplitCondition": {"Expression": "if a then if b then c else d else e"}, - }], -}} - -multiple_exclusive_splits_with_one_nested_if := {"ObjectCollection": { - "$Type": "Microflows$MicroflowObjectCollection", - "Objects": [ - { - "$Type": "Microflows$ExclusiveSplit", - "Caption": "no nested ifs", - "SplitCondition": {"Expression": "if $Variable then a else b"}, - }, - { - "$Type": "Microflows$ExclusiveSplit", - "Caption": "ex split with then-if", - "SplitCondition": {"Expression": "if a then b else if c then d else e"}, - }, - ], -}} - -# Test cases -test_should_allow_when_no_exclusive_splits_with_nested_ifs if { - nested_if_statements.allow with input as one_exclusive_split_with_no_nested_ifs - nested_if_statements.allow with input as multiple_exclusive_splits_with_no_nested_ifs -} - -test_should_deny_when_exclusive_splits_with_nested_ifs if { - not nested_if_statements.allow with input as one_exclusive_split_with_else_if - not nested_if_statements.allow with input as one_exclusive_split_with_then_if - not nested_if_statements.allow with input as multiple_exclusive_splits_with_one_nested_if -} diff --git a/rules/005_microflows/005_0005_nested_if_statements_test.yaml b/rules/005_microflows/005_0005_nested_if_statements_test.yaml new file mode 100644 index 0000000..d29c3dd --- /dev/null +++ b/rules/005_microflows/005_0005_nested_if_statements_test.yaml @@ -0,0 +1,59 @@ +TestCases: + - name: allow one exclusive split with no nested ifs + allow: true + input: + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ExpressionSplitCondition + Caption: no nested ifs + SplitCondition: + Expression: "if $Variable then a else b" + - name: allow multiple exclusive splits with no nested ifs + allow: true + input: + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ExpressionSplitCondition + Caption: ex split 1 with no nested ifs + SplitCondition: + Expression: "if $Variable then a else b" + - $Type: Microflows$ExpressionSplitCondition + Caption: ex split 2 with no nested ifs + SplitCondition: + Expression: "if $Variable then c else d" + - name: do not allow exclusive split with else-if + allow: false + input: + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ExclusiveSplit + Caption: ex split with else-if + SplitCondition: + Expression: "if true then\n\tfalse\nelse if false then\n\ttrue\nelse false" + - name: do not allow exclusive split with then-if + allow: false + input: + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ExclusiveSplit + Caption: ex split with then-if + SplitCondition: + Expression: "if a then if b then c else d else e" + - name: do not allow multiple exclusive splits with one nested if + allow: false + input: + ObjectCollection: + $Type: Microflows$MicroflowObjectCollection + Objects: + - $Type: Microflows$ExclusiveSplit + Caption: no nested ifs + SplitCondition: + Expression: "if $Variable then a else b" + - $Type: Microflows$ExclusiveSplit + Caption: ex split with then-if + SplitCondition: + Expression: "if a then b else if c then d else e" diff --git a/scripts/run-policy-tests.sh b/scripts/run-policy-tests.sh index 0a84b25..bd16d55 100755 --- a/scripts/run-policy-tests.sh +++ b/scripts/run-policy-tests.sh @@ -28,8 +28,18 @@ if [ ! -f "$MXLINT" ]; then fi +# test-rules reads the rules path from the config (the --rules flag was removed +# in newer mxlint-cli releases), so point it at ./rules via a temporary config. +CONFIG_FILE="$(mktemp)" +cat > "$CONFIG_FILE" << 'EOF' +rules: + path: ./rules +EOF + # capture all output to a file with tee -$MXLINT test-rules --rules ./rules 2>&1 | tee /tmp/mxlint-test-rules.log +$MXLINT test-rules --config "$CONFIG_FILE" 2>&1 | tee /tmp/mxlint-test-rules.log + +rm -f "$CONFIG_FILE" # grep for FAIL in the log file if grep -q "FAIL" /tmp/mxlint-test-rules.log; then