Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions rules/go/permission-predicate-call.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
[rule]
id = "go-permission-predicate-call"
languages = ["go"]
category = "rbac"
confidence = "medium"
description = "Boolean permission predicate (e.g. IsAdmin(), IsOwner(), CanRead(unit), HasAnyUnitAccess())"
# Sister rule to `go-has-role-call` and `go-permission-check-call`. Those
# require a *string-literal* argument so the rego template can substitute
# the role/permission name. This rule covers the very common Go shape
# where the call is a boolean predicate with no string-literal argument:
#
# user.IsAdmin()
# repo.IsOwner(account)
# perm.CanRead(unit)
# perm.HasAnyUnitAccess()
#
# Surfaced by the corpus shakedown on Gitea, where the entire permission
# model is hand-rolled around predicates of this shape. None matched any
# string-literal-arg rule.
#
# No rego template: the role/permission being checked isn't recoverable
# from the AST, so any generated stub would be misleading. The finding
# itself is still valuable as a policy-decision marker. Confidence is
# `medium` because predicate-style names like `IsActive` or `HasMembers`
# can also describe non-authz state — but the anchored alternation below
# is conservative (admin/owner/access/membership/permission keywords).
#
# Predicate names are explicit alternations rather than a broad
# `Has[A-Z][A-Za-z]+` to avoid duplicate findings with the existing
# string-literal rules (which would also match `HasRole`/`HasPermission`).
query = """
(call_expression
function: [
(identifier) @fn_name
(selector_expression field: (field_identifier) @fn_name)
]
) @match
"""

[rule.predicates.fn_name]
match = "^(IsAdmin|IsOwner|Is[A-Z][A-Za-z]+Admin|Is[A-Z][A-Za-z]+Owner|Can[A-Z][A-Za-z]+|HasAnyUnitAccess|HasUnitAccess|HasAccess|HasMembership|HasOwnership|Owns[A-Z][A-Za-z]+)$"

# -- Positive: Gitea-shape predicates --

[[rule.tests]]
input = """
package main

func handle(repo *Repository) {
if repo.IsAdmin(user) {
deleteRepo()
}
}
"""
expect_match = true

[[rule.tests]]
input = """
package main

func handle(perm *Permission) {
if perm.IsOwner() {
manage()
}
}
"""
expect_match = true

[[rule.tests]]
input = """
package main

func handle(perm *Permission) {
if perm.HasAnyUnitAccess() {
list()
}
}
"""
expect_match = true

[[rule.tests]]
input = """
package main

func handle(perm *Permission, unit Unit) {
if perm.CanRead(unit) {
read()
}
}
"""
expect_match = true

[[rule.tests]]
input = """
package main

func handle(repo *Repository) {
if IsRepoAdmin(repo, user) {
approve()
}
}
"""
expect_match = true

# -- Negative: not authz-shaped --

[[rule.tests]]
input = """
package main

func handle(s *Server) {
if s.IsRunning() {
serve()
}
}
"""
expect_match = false

[[rule.tests]]
input = """
package main

func handle(c *Cache) {
if c.HasValue("foo") {
useIt()
}
}
"""
expect_match = false

# -- Negative: covered by go-has-role-call (kept narrow to avoid duplicates) --

[[rule.tests]]
input = """
package main

func handle() {
if HasRole("admin") {
deleteAll()
}
}
"""
expect_match = false
76 changes: 76 additions & 0 deletions rules/java/authorized-annotation.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
[rule]
id = "java-authorized-annotation"
languages = ["java"]
category = "rbac"
confidence = "high"
description = "OpenMRS-style @Authorized({\"PRIV\"}) annotation"
# OpenMRS's primary policy surface is `@Authorized({"Manage Users"})`
# applied to service/method calls. Surfaced by the corpus shakedown:
# 24 files use it; before this rule we matched zero.
#
# Two query patterns cover the common forms:
# 1. Single string: @Authorized("Manage Users")
# 2. Array of strings: @Authorized({"Manage Users", "Edit Users"})
# Both feed the same {{role_value}} capture so the rego template emits
# a single privilege-check shape.
query = """
[
(annotation
name: (identifier) @anno_name
arguments: (annotation_argument_list
(string_literal (string_fragment) @role_value)))
(annotation
name: (identifier) @anno_name
arguments: (annotation_argument_list
(element_value_array_initializer
(string_literal (string_fragment) @role_value))))
] @match
"""

[rule.predicates.anno_name]
eq = "Authorized"

[rule.rego_template]
template = """
default allow := false

allow if {
"{{role_value}}" in input.user.privileges
}
"""

[[rule.tests]]
input = """
public class UserService {
@Authorized("Manage Users")
public void delete(User u) { }
}
"""
expect_match = true

[[rule.tests]]
input = """
public class UserService {
@Authorized({"Manage Users", "Edit Users"})
public void update(User u) { }
}
"""
expect_match = true

[[rule.tests]]
input = """
public class UserService {
@Override
public void delete(User u) { }
}
"""
expect_match = false

[[rule.tests]]
input = """
public class UserService {
@Transactional
public void update(User u) { }
}
"""
expect_match = false
87 changes: 87 additions & 0 deletions rules/python/check-helper-call.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
[rule]
id = "py-check-helper-call"
languages = ["python"]
category = "abac"
confidence = "medium"
description = "Permission check_* helper call (e.g. check_can_invite_users(user), check_basic_stream_access(...))"
# Surfaced by the corpus shakedown on Zulip, where the actual policy
# decision points are spelled `check_can_*`, `check_basic_*`,
# `check_message_*`, `check_stream_*`, `check_*_access` — bare module
# functions invoked from views/lib code. Sister rule to
# `py-permission-check-call` (which only matches *attribute* calls like
# `user.can("delete")`).
#
# No rego template: the policy intent is encoded in the function name
# itself, not a string argument. Confidence is medium because some
# `check_*` helpers are validation rather than authz (e.g.
# `check_message_format`); the anchored alternation keeps it tight by
# requiring a permission/access keyword in the suffix.
query = """
(call
function: (identifier) @fn_name
) @match
"""

[rule.predicates.fn_name]
match = "^(check_can_[a-z_]+|check_basic_[a-z_]+|check_message_[a-z_]+access|check_stream_[a-z_]+access|check_[a-z_]+_access|check_[a-z_]+_permission|check_[a-z_]+_authz)$"

# -- Positive: Zulip-shape policy helpers --

[[rule.tests]]
input = """
def maybe_invite():
check_can_invite_users(user_profile, invite_as)
invite()
"""
expect_match = true

[[rule.tests]]
input = """
def read_stream(stream):
check_basic_stream_access(user_profile, stream)
return stream.messages.all()
"""
expect_match = true

[[rule.tests]]
input = """
def edit(message):
check_message_edit_access(user_profile, message)
do_edit()
"""
expect_match = true

[[rule.tests]]
input = """
def view(stream_id):
check_stream_access(user_profile, stream_id)
return render()
"""
expect_match = true

# -- Negative: validation helpers, not authz --

[[rule.tests]]
input = """
def parse(payload):
check_format(payload)
return payload
"""
expect_match = false

[[rule.tests]]
input = """
def post(message):
check_message_format(message)
deliver()
"""
expect_match = false

# -- Negative: covered by py-permission-check-call --

[[rule.tests]]
input = """
if user.can("delete"):
delete_resource()
"""
expect_match = false
Loading