diff --git a/rules/002_domain_model/002_0010_access_rules_with_read_write_in_xpath.rego b/rules/002_domain_model/002_0010_access_rules_with_read_write_in_xpath.rego new file mode 100644 index 0000000..2adbf8d --- /dev/null +++ b/rules/002_domain_model/002_0010_access_rules_with_read_write_in_xpath.rego @@ -0,0 +1,90 @@ +# METADATA +# scope: package +# title: Access rules using XPath should only contain attributes and associations with Read access +# description: If an XPath uses attributes or associations with read and write access, it may cause a security breach +# authors: +# - Bart Zantingh +# custom: +# category: Security +# rulename: AccessRuleXpathConstraintsWithReadWrite +# severity: HIGH +# rulenumber: "002_0010" +# remediation: Ensure that all attributes and associations used in XPath constraints have read-only access +# input: "*/DomainModels$DomainModel.yaml" +package app.mendix.domain_model.access_rules_with_read_write_in_xpath + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +default allow := false + +allow if count(errors) == 0 + +# check XPaths for attributes with ReadWrite access +errors contains error if { + some entity in input.Entities + entity_name := entity.Name + + attributes := [attribute | + some attribute in entity.Attributes + attribute["$Type"] == "DomainModels$Attribute" + ] + + some attribute in attributes + has_read_write_access_to_attribute(entity, attribute.Name) + used_in_xpath(entity, attribute.Name) + + error := sprintf( + "[%v, %v, %v] Entity %v has an XPath constraint that uses one or more attributes with read/write access", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + entity_name, + ], + ) +} + +# check XPaths for associations with ReadWrite access +errors contains error if { + some association in input.Associations + + some entity in input.Entities + has_read_write_access_to_association(entity, association.Name) + used_in_xpath(entity, association.Name) + + entity_name := entity.Name + + error := sprintf( + "[%v, %v, %v] Entity %v has an XPath constraint that uses one or more assocations with read/write access", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + entity_name, + ], + ) +} + +has_read_write_access_to_attribute(entity, attribute_name) if { + some access_rule in entity.AccessRules + + some member_access in access_rule.MemberAccesses + member_access.AccessRights == "ReadWrite" + contains(member_access.Attribute, attribute_name) +} + +has_read_write_access_to_association(entity, association_name) if { + some access_rule in entity.AccessRules + + some member_access in access_rule.MemberAccesses + member_access.AccessRights == "ReadWrite" + contains(member_access.Association, association_name) +} + +# check if there are XPath constraints that contain the name of the attribute or association +used_in_xpath(entity, search_term) if { + some access_rule in entity.AccessRules + contains(access_rule.XPathConstraint, search_term) +} diff --git a/rules/002_domain_model/002_0010_access_rules_with_read_write_in_xpath_test.rego b/rules/002_domain_model/002_0010_access_rules_with_read_write_in_xpath_test.rego new file mode 100644 index 0000000..15aa7bd --- /dev/null +++ b/rules/002_domain_model/002_0010_access_rules_with_read_write_in_xpath_test.rego @@ -0,0 +1,81 @@ +package app.mendix.domain_model.access_rules_with_read_write_in_xpath_test + +import data.app.mendix.domain_model.access_rules_with_read_write_in_xpath +import rego.v1 + +# Test data +attribute_with_read_used_in_xpath := {"Entities": [{ + "AccessRules": [{ + "MemberAccesses": [{ + "AccessRights": "Read", + "Attribute": "MxLintTest.TestEntity.Attribute1", + "Association": "", + }], + "XPathConstraint": "[Attribute1 = true]", + }], + "Attributes": [{ + "$Type": "DomainModels$Attribute", + "Name": "Attribute1", + }], + "Name": "TestEntity", +}]} + +attribute_with_read_write_used_in_xpath := {"Entities": [{ + "AccessRules": [{ + "MemberAccesses": [{ + "AccessRights": "ReadWrite", + "Attribute": "MxLintTest.TestEntity.Attribute1", + "Association": "", + }], + "XPathConstraint": "[Attribute1 = true]", + }], + "Attributes": [{ + "$Type": "DomainModels$Attribute", + "Name": "Attribute1", + }], + "Name": "TestEntity", +}]} + +association_with_read_used_in_xpath := { + "Associations": [{"Name": "TestEntity_Entity"}], + "Entities": [{ + "AccessRules": [{ + "MemberAccesses": [{ + "AccessRights": "Read", + "Attribute": "", + "Association": "MxLintTest.TestEntity_Entity", + }], + "XPathConstraint": "[MxLintTest.TestEntity_Entity/MxLintTest.Entity]", + }], + "Name": "TestEntity", + }], +} + +association_with_read_write_used_in_xpath := { + "Associations": [{"Name": "TestEntity_Entity"}], + "Entities": [{ + "AccessRules": [{ + "MemberAccesses": [{ + "AccessRights": "ReadWrite", + "Attribute": "", + "Association": "MxLintTest.TestEntity_Entity", + }], + "XPathConstraint": "[MxLintTest.TestEntity_Entity/MxLintTest.Entity]", + }], + "Name": "TestEntity", + }], +} + +# Test cases +test_should_allow_when_default_access_rights_none_or_read if { + access_rules_with_read_write_in_xpath.allow with input as attribute_with_read_used_in_xpath + access_rules_with_read_write_in_xpath.allow with input as association_with_read_used_in_xpath +} + +test_should_deny_when_attribute_with_read_write_in_xpath if { + not access_rules_with_read_write_in_xpath.allow with input as attribute_with_read_write_used_in_xpath +} + +test_should_deny_when_association_with_read_write_in_xpath if { + not access_rules_with_read_write_in_xpath.allow with input as association_with_read_write_used_in_xpath +} diff --git a/rules/002_domain_model/002_0011_unlimited_length_attributes_editable_by_anonymous.rego b/rules/002_domain_model/002_0011_unlimited_length_attributes_editable_by_anonymous.rego new file mode 100644 index 0000000..91a1077 --- /dev/null +++ b/rules/002_domain_model/002_0011_unlimited_length_attributes_editable_by_anonymous.rego @@ -0,0 +1,59 @@ +# METADATA +# scope: package +# title: Unlimited length string attributes should not be editable by anonymous users +# description: A malicious agent could set a very long value for the attribute causing the database to run out of space +# authors: +# - Bart Zantingh +# custom: +# category: Security +# rulename: UnlimitedLengthAttributesEditableByAnonymous +# severity: CRITICAL +# rulenumber: "002_0011" +# remediation: Ensure that anonymous users have, at most, only read access to attributes with unlimited length +# input: "*/DomainModels$DomainModel.yaml" +package app.mendix.domain_model.unlimited_length_attributes_editable_by_anonymous + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + some entity in input.Entities + + anon_has_access(entity) + + attributes := [attribute | + some attribute in entity.Attributes + attribute["$Type"] == "DomainModels$Attribute" + attribute.NewType["$Type"] == "DomainModels$StringAttributeType" + attribute.NewType.Length == 0 + ] + + some access_rule in entity.AccessRules + some member_access in access_rule.MemberAccesses + member_access.AccessRights == "ReadWrite" + + some attribute in attributes + contains(member_access.Attribute, attribute.Name) + + error := sprintf( + "[%v, %v, %v] String attribute %v in entity %v has unlimited length and seems to be editable by anonymous users", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + attribute.Name, + entity.Name, + ], + ) +} + +anon_has_access(entity) if { + some access_rule in entity.AccessRules + some module_role in access_rule.AllowedModuleRoles + contains(lower(module_role), "anon") +} diff --git a/rules/002_domain_model/002_0011_unlimited_length_attributes_editable_by_anonymous_test.rego b/rules/002_domain_model/002_0011_unlimited_length_attributes_editable_by_anonymous_test.rego new file mode 100644 index 0000000..c8fc59c --- /dev/null +++ b/rules/002_domain_model/002_0011_unlimited_length_attributes_editable_by_anonymous_test.rego @@ -0,0 +1,54 @@ +package app.mendix.domain_model.unlimited_length_attributes_editable_by_anonymous_test + +import data.app.mendix.domain_model.unlimited_length_attributes_editable_by_anonymous +import rego.v1 + +# Test data +anon_with_readonly_access := {"Entities": [{ + "AccessRules": [{ + "AllowedModuleRoles": ["MxLintTest.Anonymous"], + "MemberAccesses": [{ + "$Type": "DomainModels$MemberAccess", + "AccessRights": "ReadOnly", + "Attribute": "MxLintTest.TestEntity.Attribute", + }], + }], + "Attributes": [{ + "$Type": "DomainModels$Attribute", + "Name": "Attribute", + "NewType": { + "$Type": "DomainModels$StringAttributeType", + "Length": 0, + }, + }], + "Name": "TestEntity", +}]} + +anon_with_readwrite_access := {"Entities": [{ + "AccessRules": [{ + "AllowedModuleRoles": ["MxLintTest.Anonymous"], + "MemberAccesses": [{ + "$Type": "DomainModels$MemberAccess", + "AccessRights": "ReadWrite", + "Attribute": "MxLintTest.TestEntity.Attribute", + }], + }], + "Attributes": [{ + "$Type": "DomainModels$Attribute", + "Name": "Attribute", + "NewType": { + "$Type": "DomainModels$StringAttributeType", + "Length": 0, + }, + }], + "Name": "TestEntity", +}]} + +# Test cases +test_should_allow_when_anon_has_readonly_access if { + unlimited_length_attributes_editable_by_anonymous.allow with input as anon_with_readonly_access +} + +test_should_deny_when_anon_has_readwrite_access if { + not unlimited_length_attributes_editable_by_anonymous.allow with input as anon_with_readwrite_access +} diff --git a/rules/002_domain_model/002_0012_anonymous_users_with_create_access.rego b/rules/002_domain_model/002_0012_anonymous_users_with_create_access.rego new file mode 100644 index 0000000..08dcc61 --- /dev/null +++ b/rules/002_domain_model/002_0012_anonymous_users_with_create_access.rego @@ -0,0 +1,49 @@ +# METADATA +# scope: package +# title: Anonymous users should not be allowed to create persistent entities +# description: Anonymous users with create access to persistent entities can pose security risks +# authors: +# - Bart Zantingh +# custom: +# category: Security +# rulename: AnonymousUsersWithCreateAccess +# severity: HIGH +# rulenumber: "002_0012" +# remediation: Remove create access or make entity non-persistent +# input: "*/DomainModels$DomainModel.yaml" +package app.mendix.domain_model.anonymous_users_with_create_access + +import rego.v1 + +annotation := rego.metadata.chain()[1].annotations + +default allow := false + +allow if count(errors) == 0 + +errors contains error if { + some entity in input.Entities + entity.MaybeGeneralization.Persistable == true + entity_name := entity.Name + + some access_rule in entity.AccessRules + access_rule.AllowCreate == true + + some module_role in access_rule.AllowedModuleRoles + contains(lower(module_role), "anon") # converts module role name to lower case so it catches all variants of spelling + + module_name := split(module_role, ".")[0] + module_role_name := split(module_role, ".")[1] + + error := sprintf( + "[%v, %v, %v] Module role %v in module %v seems to be for anonymous users, but has Create access to persistent entity %v", + [ + annotation.custom.severity, + annotation.custom.category, + annotation.custom.rulenumber, + module_role_name, + module_name, + entity_name, + ], + ) +} diff --git a/rules/002_domain_model/002_0012_anonymous_users_with_create_access_test.rego b/rules/002_domain_model/002_0012_anonymous_users_with_create_access_test.rego new file mode 100644 index 0000000..e8a786a --- /dev/null +++ b/rules/002_domain_model/002_0012_anonymous_users_with_create_access_test.rego @@ -0,0 +1,54 @@ +package app.mendix.domain_model.anonymous_users_with_create_access_test + +import data.app.mendix.domain_model.anonymous_users_with_create_access +import rego.v1 + +# Test data +anon_role_with_create_access_to_persistent := { + "$Type": "DomainModels$DomainModel", + "Entities": [{ + "AccessRules": [{ + "AllowCreate": true, + "AllowedModuleRoles": ["MyFirstModule.Anonymous"], + }], + "MaybeGeneralization": {"Persistable": true}, + "Name": "Entity", + }], +} + +anon_role_without_create_access_to_persistent := { + "$Type": "DomainModels$DomainModel", + "Entities": [{ + "AccessRules": [{ + "AllowCreate": false, + "AllowedModuleRoles": ["MyFirstModule.Anonymous"], + }], + "MaybeGeneralization": {"Persistable": true}, + "Name": "Entity", + }], +} + +anon_role_with_create_access_to_nonpersistent := { + "$Type": "DomainModels$DomainModel", + "Entities": [{ + "AccessRules": [{ + "AllowCreate": true, + "AllowedModuleRoles": ["MyFirstModule.Anonymous"], + }], + "MaybeGeneralization": {"Persistable": false}, + "Name": "Entity", + }], +} + +# Test cases +test_should_allow_when_anon_role_has_no_create_access_to_persistent_entity if { + anonymous_users_with_create_access.allow with input as anon_role_without_create_access_to_persistent +} + +test_should_allow_when_anon_role_has_create_access_to_nonpersistent_entity if { + anonymous_users_with_create_access.allow with input as anon_role_with_create_access_to_nonpersistent +} + +test_should_deny_when_anon_role_has_create_access_to_persistent_entity if { + not anonymous_users_with_create_access.allow with input as anon_role_with_create_access_to_persistent +}