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
98 changes: 98 additions & 0 deletions rules/go/access-descriptor-builder.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
[rule]
id = "go-access-descriptor-builder"
languages = ["go"]
category = "abac"
confidence = "high"
description = "ABAC access-descriptor builder call (e.g. .WithPrincipal(...), .WithPermission(...))"
# Captures the builder/fluent shape commonly used to assemble an
# authorization input for a policy engine — e.g. ocp's
# `NewAccess().WithPrincipal(p).WithResource(r).WithPermission(perm)
# .WithTenant(t)`. Each builder call is a separate
# `call_expression`, so we surface a finding per chain link rather than
# trying to recognise the whole chain — the model (or a human reviewer)
# coalesces them downstream.
#
# The method-name allowlist is intentionally narrow: it sticks to
# attribute names that mean "I'm assembling an authz subject/object/
# action tuple" — `Principal`, `Subject`, `Resource`, `Permission`,
# `Role`, `Tenant`, `Action`. Generic `WithName`, `WithID`, etc. are
# excluded to keep this from firing on every builder in the codebase.
# Confidence is `high` because the combination of the `With…` prefix
# and an authz-flavored attribute is rarely incidental.
query = """
(call_expression
function: (selector_expression
field: (field_identifier) @attr_method)
) @match
"""

[rule.predicates.attr_method]
match = "^With(Principal|Subject|Resource|Permission|Role|Tenant|Action)$"

[rule.rego_template]
template = """
# Builder calls assemble an ABAC input — `principal`, `resource`,
# `permission`, etc. The actual decision usually happens elsewhere
# (an OPA `.Eval(ctx)` or a custom evaluator). Adapt this stub to
# match the attributes your builder actually accumulates.
default allow := false

allow if {
input.principal == input.resource.owner
}

allow if {
input.permission in data.permissions[input.principal]
}
"""

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

func mk() {
a := NewAccess().WithPrincipal("bob").WithResource("sources").WithPermission("sources.view")
_ = a
}
"""
expect_match = true

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

func mk(b *AccessBuilder) {
b.WithRole("admin")
}
"""
expect_match = true

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

func mk(b *AccessBuilder) {
b.WithTenant("acme")
}
"""
expect_match = true

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

func mk(b *Builder) {
b.WithName("foo")
}
"""
expect_match = false

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

func mk(opt *Options) {
opt.WithTimeout(5)
}
"""
expect_match = false
128 changes: 128 additions & 0 deletions rules/go/opa-rego-eval.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
[rule]
id = "go-opa-rego-eval"
languages = ["go"]
category = "custom"
confidence = "high"
description = "OPA Rego evaluation in Go (rego.New(...) entry point)"
# OPA's Go SDK (`github.com/open-policy-agent/opa/rego`) exposes a small,
# distinctive entry-point: `rego.New(opts...)`. Anchoring on the
# constructor catches both straight-line evaluation (`rego.New(...)
# .Eval(ctx)`) and partial-evaluation flows (`rego.New(...).Partial(ctx)`,
# as in ocp's `internal/authz/authz.go`) — every call site of OPA from
# Go has to go through `rego.New` once. We deliberately don't add a
# second rule for `.Eval` / `.Partial` directly: those method names are
# generic enough (lots of unrelated APIs use them) that without the
# `rego` package qualifier they'd be noisy. Confidence is `high` because
# applications don't typically embed OPA for non-policy reasons.
#
# Known limitation: aliased imports (e.g. `import opa "…/opa/rego"`)
# won't match — the `pkg eq "rego"` predicate keys on the source
# identifier, not the imported path. Aliasing this package is rare in
# practice; widening to `opa` would risk false positives on unrelated
# `opa.New` symbols. If a real codebase shows up using an alias, prefer
# adding a second rule keyed on that alias rather than relaxing this one.
query = """
(call_expression
function: (selector_expression
operand: (identifier) @pkg
field: (field_identifier) @ctor)
) @match
"""

[rule.predicates.pkg]
eq = "rego"

[rule.predicates.ctor]
eq = "New"

[rule.rego_template]
template = """
# OPA is already the policy engine here — the embedded Go code IS the
# enforcement point. This stub is a starter scaffold; the real policy
# almost certainly lives in a separate `.rego` file already (and the
# Go code passes it via `rego.Module(...)` or `rego.Load(...)`). Lift
# that policy into your bundle rather than duplicating logic here.
default allow := false

allow if {
input.user.permissions[_] == input.required_permission
}
"""

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

import "github.com/open-policy-agent/opa/rego"

func decide(ctx context.Context) {
r := rego.New(
rego.Query("data.authz.allow"),
rego.Module("authz.rego", src),
)
_ = r
}
"""
expect_match = true

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

func decide(ctx context.Context) {
pqs, err := rego.New(
rego.Query("data.authz.allow = true"),
rego.Module("authz.rego", src),
rego.Unknowns(unknowns),
rego.ParsedInput(access.ToValue()),
).Partial(ctx)
_ = pqs
_ = err
}
"""
expect_match = true

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

func decide(ctx context.Context) {
res, _ := rego.New(rego.Query("data.x")).Eval(ctx)
_ = res
}
"""
expect_match = true

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

func decide() {
foo.New("bar")
}
"""
expect_match = false

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

func decide(svc *Service) {
svc.Process(input)
}
"""
expect_match = false

# We deliberately do NOT match a bare `q.Eval(ctx)` without a preceding
# `rego.New` capture in scope — too generic. The companion test pins
# this behaviour so a future widening of the rule has to confront the
# false-positive trade-off explicitly.
[[rule.tests]]
input = """
package main

func decide(ctx context.Context, q *rego.Rego) {
_, _ = q.Eval(ctx)
}
"""
expect_match = false
Loading