diff --git a/rules/java/feature-gate-check.toml b/rules/java/feature-gate-check.toml index 017ec39..5b0e0be 100644 --- a/rules/java/feature-gate-check.toml +++ b/rules/java/feature-gate-check.toml @@ -4,12 +4,17 @@ languages = ["java"] category = "feature_gate" confidence = "medium" description = "Feature flag or plan-based gating in Java" +# Accept string literals as well as field accesses and bare identifiers +# as the feature key; non-literal stubs need manual rego review. query = """ (method_invocation name: (identifier) @method_name arguments: (argument_list - (string_literal - (string_fragment) @feature)) + [ + (string_literal (string_fragment) @feature) + (field_access) @feature + (identifier) @feature + ]) ) @match """ @@ -46,6 +51,26 @@ public class Service { """ expect_match = true +[[rule.tests]] +input = """ +public class Service { + public void check(String featureKey) { + if (featureFlags.hasFeature(featureKey)) { enable(); } + } +} +""" +expect_match = true + +[[rule.tests]] +input = """ +public class Service { + public void check() { + if (featureFlags.isFeatureEnabled(Features.BETA_DASHBOARD)) { enable(); } + } +} +""" +expect_match = true + [[rule.tests]] input = """ public class Service { diff --git a/rules/java/has-role-call.toml b/rules/java/has-role-call.toml index 28787f1..3ac861f 100644 --- a/rules/java/has-role-call.toml +++ b/rules/java/has-role-call.toml @@ -4,12 +4,19 @@ languages = ["java"] category = "rbac" confidence = "high" description = "Spring Security hasRole/hasAuthority method call" +# Accept string literals as well as field accesses (e.g. `Role.ADMIN`) and +# bare identifiers (e.g. `role`) as the role argument. The rego stub treats +# whatever text is captured as the role name; for non-literal expressions +# the stub will need manual review, but the finding is still recorded. query = """ (method_invocation name: (identifier) @method_name arguments: (argument_list - (string_literal - (string_fragment) @role_value)) + [ + (string_literal (string_fragment) @role_value) + (field_access) @role_value + (identifier) @role_value + ]) ) @match """ @@ -68,6 +75,26 @@ public class SecurityConfig { """ expect_match = true +[[rule.tests]] +input = """ +public class Service { + public void check(Account acct) { + if (!acct.hasRole(Role.ADMIN)) { throw new ForbiddenException(); } + } +} +""" +expect_match = true + +[[rule.tests]] +input = """ +public class Service { + public void check(Account acct, String role) { + if (acct.hasRole(role)) { allow(); } + } +} +""" +expect_match = true + [[rule.tests]] input = """ public class SecurityConfig { diff --git a/rules/java/is-user-in-role.toml b/rules/java/is-user-in-role.toml index d1b5ce9..885404b 100644 --- a/rules/java/is-user-in-role.toml +++ b/rules/java/is-user-in-role.toml @@ -4,12 +4,17 @@ languages = ["java"] category = "rbac" confidence = "high" description = "Servlet API isUserInRole() call" +# Accept string literals as well as field accesses and bare identifiers +# as the role argument; non-literal stubs need manual rego review. query = """ (method_invocation name: (identifier) @method_name arguments: (argument_list - (string_literal - (string_fragment) @role_value)) + [ + (string_literal (string_fragment) @role_value) + (field_access) @role_value + (identifier) @role_value + ]) ) @match """ @@ -35,6 +40,26 @@ public class MyServlet { """ expect_match = true +[[rule.tests]] +input = """ +public class MyServlet { + public void doGet() { + if (request.isUserInRole(Role.ADMIN)) { doSomething(); } + } +} +""" +expect_match = true + +[[rule.tests]] +input = """ +public class MyServlet { + public void doGet(String roleVar) { + if (request.isUserInRole(roleVar)) { doSomething(); } + } +} +""" +expect_match = true + [[rule.tests]] input = """ public class MyServlet { diff --git a/rules/java/role-equals-check.toml b/rules/java/role-equals-check.toml index c9d3112..3ff7dbc 100644 --- a/rules/java/role-equals-check.toml +++ b/rules/java/role-equals-check.toml @@ -4,14 +4,19 @@ languages = ["java"] category = "rbac" confidence = "medium" description = "Role comparison via .equals() or string equality" +# Accept string literals as well as field accesses (e.g. `Role.ADMIN`) and +# bare identifiers (e.g. `roleVar`) as the comparison argument. query = """ (method_invocation object: (method_invocation name: (identifier) @getter) name: (identifier) @method_name arguments: (argument_list - (string_literal - (string_fragment) @role_value)) + [ + (string_literal (string_fragment) @role_value) + (field_access) @role_value + (identifier) @role_value + ]) ) @match """ @@ -50,6 +55,26 @@ public class Service { """ expect_match = false +[[rule.tests]] +input = """ +public class Service { + public void check() { + if (user.getRole().equals(Role.ADMIN)) { doSomething(); } + } +} +""" +expect_match = true + +[[rule.tests]] +input = """ +public class Service { + public void check(String roleName) { + if (user.getRole().equals(roleName)) { doSomething(); } + } +} +""" +expect_match = true + [[rule.tests]] input = """ public class Service { diff --git a/rules/java/shiro-is-permitted.toml b/rules/java/shiro-is-permitted.toml index 2f224fc..d099611 100644 --- a/rules/java/shiro-is-permitted.toml +++ b/rules/java/shiro-is-permitted.toml @@ -4,12 +4,17 @@ languages = ["java"] category = "rbac" confidence = "high" description = "Apache Shiro Subject.isPermitted() call" +# Accept string literals as well as field accesses and bare identifiers +# as the permission argument; non-literal stubs need manual rego review. query = """ (method_invocation name: (identifier) @method_name arguments: (argument_list - (string_literal - (string_fragment) @perm_value)) + [ + (string_literal (string_fragment) @perm_value) + (field_access) @perm_value + (identifier) @perm_value + ]) ) @match """ @@ -45,6 +50,26 @@ public class Service { """ expect_match = true +[[rule.tests]] +input = """ +public class Service { + public void delete() { + if (subject.isPermitted(Permissions.USER_DELETE)) { doIt(); } + } +} +""" +expect_match = true + +[[rule.tests]] +input = """ +public class Service { + public void delete(String permVar) { + if (subject.isPermitted(permVar)) { doIt(); } + } +} +""" +expect_match = true + [[rule.tests]] input = """ public class Service { diff --git a/src/scanner/matcher.rs b/src/scanner/matcher.rs index a5dc0ea..88a8885 100644 --- a/src/scanner/matcher.rs +++ b/src/scanner/matcher.rs @@ -378,6 +378,30 @@ public class Ctrl { assert!(!findings.is_empty(), "should match isUserInRole"); } + #[test] + fn java_is_user_in_role_non_literal_arg_matches() { + let findings = parse_and_match_java( + r#"request.isUserInRole(Role.ADMIN);"#, + include_str!("../../rules/java/is-user-in-role.toml"), + ); + assert!( + !findings.is_empty(), + "should match isUserInRole with field-access arg" + ); + } + + #[test] + fn java_is_user_in_role_identifier_arg_matches() { + let findings = parse_and_match_java( + r#"request.isUserInRole(roleVar);"#, + include_str!("../../rules/java/is-user-in-role.toml"), + ); + assert!( + !findings.is_empty(), + "should match isUserInRole with identifier arg (variable role name)" + ); + } + #[test] fn java_has_role_call_matches() { let findings = parse_and_match_java( @@ -387,6 +411,30 @@ public class Ctrl { assert!(!findings.is_empty(), "should match hasRole"); } + #[test] + fn java_has_role_call_field_access_arg_matches() { + let findings = parse_and_match_java( + r#"if (!acct.hasRole(Role.ADMIN)) { throw new ForbiddenException(); }"#, + include_str!("../../rules/java/has-role-call.toml"), + ); + assert!( + !findings.is_empty(), + "should match hasRole with field-access arg (e.g., Role.ADMIN)" + ); + } + + #[test] + fn java_has_role_call_identifier_arg_matches() { + let findings = parse_and_match_java( + r#"if (acct.hasRole(roleName)) { allow(); }"#, + include_str!("../../rules/java/has-role-call.toml"), + ); + assert!( + !findings.is_empty(), + "should match hasRole with identifier arg (variable role name)" + ); + } + #[test] fn java_shiro_requires_permissions_matches() { let findings = parse_and_match_java( @@ -410,6 +458,30 @@ public class Ctrl { assert!(!findings.is_empty(), "should match isPermitted"); } + #[test] + fn java_shiro_is_permitted_non_literal_arg_matches() { + let findings = parse_and_match_java( + r#"subject.isPermitted(Permissions.USER_DELETE);"#, + include_str!("../../rules/java/shiro-is-permitted.toml"), + ); + assert!( + !findings.is_empty(), + "should match isPermitted with field-access arg" + ); + } + + #[test] + fn java_shiro_is_permitted_identifier_arg_matches() { + let findings = parse_and_match_java( + r#"subject.isPermitted(permVar);"#, + include_str!("../../rules/java/shiro-is-permitted.toml"), + ); + assert!( + !findings.is_empty(), + "should match isPermitted with identifier arg (variable permission)" + ); + } + #[test] fn java_marker_annotation_matches() { let findings = parse_and_match_java( @@ -450,6 +522,30 @@ public class Ctrl { assert!(!findings.is_empty(), "should match getRole().equals()"); } + #[test] + fn java_role_equals_check_field_access_arg_matches() { + let findings = parse_and_match_java( + r#"user.getRole().equals(Role.ADMIN);"#, + include_str!("../../rules/java/role-equals-check.toml"), + ); + assert!( + !findings.is_empty(), + "should match getRole().equals(field-access)" + ); + } + + #[test] + fn java_role_equals_check_identifier_arg_matches() { + let findings = parse_and_match_java( + r#"user.getRole().equals(roleVar);"#, + include_str!("../../rules/java/role-equals-check.toml"), + ); + assert!( + !findings.is_empty(), + "should match getRole().equals(identifier)" + ); + } + #[test] fn java_role_equals_check_no_false_positive() { let findings = parse_and_match_java( @@ -486,6 +582,30 @@ public class Ctrl { assert!(!findings.is_empty(), "should match hasFeature()"); } + #[test] + fn java_feature_gate_non_literal_arg_matches() { + let findings = parse_and_match_java( + r#"featureFlags.hasFeature(Features.BETA_DASHBOARD);"#, + include_str!("../../rules/java/feature-gate-check.toml"), + ); + assert!( + !findings.is_empty(), + "should match hasFeature with field-access arg" + ); + } + + #[test] + fn java_feature_gate_identifier_arg_matches() { + let findings = parse_and_match_java( + r#"featureFlags.hasFeature(featureKey);"#, + include_str!("../../rules/java/feature-gate-check.toml"), + ); + assert!( + !findings.is_empty(), + "should match hasFeature with identifier arg (variable feature key)" + ); + } + #[test] fn dedup_removes_duplicates() { let f = Finding {