From 7cda25aec708b0260f825f6a49e216db44db4a8b Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Wed, 6 May 2026 12:57:58 -0400 Subject: [PATCH 1/2] feat: detect bare identifier role checks --- .../typescript/identifier-includes-check.toml | 92 +++++++++++++++++++ src/rules/embedded.rs | 4 + src/scanner/matcher.rs | 30 ++++++ 3 files changed, 126 insertions(+) create mode 100644 rules/typescript/identifier-includes-check.toml diff --git a/rules/typescript/identifier-includes-check.toml b/rules/typescript/identifier-includes-check.toml new file mode 100644 index 0000000..a744da4 --- /dev/null +++ b/rules/typescript/identifier-includes-check.toml @@ -0,0 +1,92 @@ +[rule] +id = "ts-identifier-includes-check" +languages = ["typescript", "javascript"] +category = "rbac" +confidence = "high" +description = "Role or group collection inclusion check on a bare identifier (e.g., userGroups.includes(\"admin\"))" +query = """ +(call_expression + function: (member_expression + object: (identifier) @collection + property: (property_identifier) @method) + arguments: (arguments + (string) @role_value) +) @match +""" + +[rule.predicates.collection] +match = "^(roles?|groups?|userRoles|userGroups)$" + +[rule.predicates.method] +match = "^(includes|indexOf|contains|some|has)$" + +[rule.rego_template] +template = """ +default allow := false + +allow if { + "{{role_value}}" in input.user.roles +} +""" + + +[rule.cedar_template] +template = """ +permit ( + principal, + action, + resource +) +when { + principal.roles.contains("{{role_value}}") +}; +""" + +[[rule.tests]] +input = """ +if (userGroups.includes("manager")) { + allow(); +} +""" +expect_match = true + +[[rule.tests]] +input = """ +if (groups.indexOf("admin") > -1) { + allow(); +} +""" +expect_match = true + +[[rule.tests]] +input = """ +if (permissions.includes("write")) { + allow(); +} +""" +expect_match = false + +[[rule.tests]] +input = """ +if (claims.has("customer")) { + allow(); +} +""" +expect_match = false +language = "javascript" + +[[rule.tests]] +input = """ +if (cache.has("key")) { + hit(); +} +""" +expect_match = false + +[[rule.tests]] +input = """ +if (tags.includes("foo")) { + filter(); +} +""" +expect_match = false diff --git a/src/rules/embedded.rs b/src/rules/embedded.rs index ef50d17..1400d83 100644 --- a/src/rules/embedded.rs +++ b/src/rules/embedded.rs @@ -12,6 +12,10 @@ const EMBEDDED_RULES: &[(&str, &str)] = &[ "role-includes-check", include_str!("../../rules/typescript/role-includes-check.toml"), ), + ( + "identifier-includes-check", + include_str!("../../rules/typescript/identifier-includes-check.toml"), + ), ( "has-role-call", include_str!("../../rules/typescript/has-role-call.toml"), diff --git a/src/scanner/matcher.rs b/src/scanner/matcher.rs index 1cfad9e..0f753d7 100644 --- a/src/scanner/matcher.rs +++ b/src/scanner/matcher.rs @@ -483,6 +483,36 @@ public IActionResult Reports() => Ok();"#, assert!(!findings.is_empty()); } + #[test] + fn identifier_includes_check_is_role_shaped_only() { + let findings = parse_and_match( + r#"if (userGroups.includes("manager")) { allow(); }"#, + include_str!("../../rules/typescript/identifier-includes-check.toml"), + ); + + assert_eq!(findings.len(), 1); + assert_eq!(findings[0].category, crate::types::AuthCategory::Rbac); + let rego = findings[0].rego_stub.as_deref().unwrap(); + assert!( + rego.contains(r#""manager" in input.user.roles"#), + "rego should use a role membership check; got: {rego}" + ); + let cedar = findings[0].cedar_stub.as_deref().unwrap(); + assert!( + cedar.contains(r#"principal.roles.contains("manager")"#), + "cedar should use a role membership check; got: {cedar}" + ); + + let permission_findings = parse_and_match( + r#"if (permissions.includes("write")) { allow(); }"#, + include_str!("../../rules/typescript/identifier-includes-check.toml"), + ); + assert!( + permission_findings.is_empty(), + "bare permission collections need a permission-shaped rule" + ); + } + // -- Java rule tests -- fn parse_and_match_java(source: &str, rule_toml: &str) -> Vec { From 3687057d476e4330a6869eccb73796d748271dd7 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Wed, 6 May 2026 13:15:32 -0400 Subject: [PATCH 2/2] fix: address PR review feedback --- .../typescript/identifier-includes-check.toml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/rules/typescript/identifier-includes-check.toml b/rules/typescript/identifier-includes-check.toml index a744da4..3605a8c 100644 --- a/rules/typescript/identifier-includes-check.toml +++ b/rules/typescript/identifier-includes-check.toml @@ -75,6 +75,24 @@ if (claims.has("customer")) { expect_match = false language = "javascript" +[[rule.tests]] +input = """ +if (scope.includes("read:billing")) { + allow(); +} +""" +expect_match = false +language = "javascript" + +[[rule.tests]] +input = """ +if (scopes.has("write:billing")) { + allow(); +} +""" +expect_match = false +language = "javascript" + [[rule.tests]] input = """ if (cache.has("key")) {