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
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ tree-sitter = "0.24"
tree-sitter-typescript = "0.23"
tree-sitter-java = "0.23"
tree-sitter-javascript = "0.23"
tree-sitter-python = "0.23"
ignore = "0.4"
sha2 = "0.10"
regex = "1"
Expand Down
58 changes: 29 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Sift through your codebase for embedded authorization logic. Extract it into Policy as Code (PaC) — [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) for [OPA](https://www.openpolicyagent.org/) today, with other engines (e.g. Cedar) on the roadmap.

> **Status:** v0.1 — structural scanning ready for TypeScript, JavaScript, and Java. `--deep` (LLM-assisted) mode functional via any OpenAI-compatible endpoint or MCP-capable agent host.
> **Status:** v0.1 — structural scanning ready for TypeScript, JavaScript, Java, and Python. `--deep` (LLM-assisted) mode functional via any OpenAI-compatible endpoint or MCP-capable agent host.

## What is zift?

Expand All @@ -23,7 +23,34 @@ zift report . # detailed findings report

1. **Structural scan** (tree-sitter) — fast, deterministic, zero-cost. Finds known authorization patterns: role checks, permission guards, auth middleware, security annotations.

2. **Semantic scan** (`--deep`, opt-in) — sends candidate code regions to an LLM that classifies authorization logic the structural pass missed or misjudged. Useful for business rules that implicitly encode access control, and for languages where structural support hasn't shipped yet (Python, Go, etc.).
2. **Semantic scan** (`--deep`, opt-in) — sends candidate code regions to an LLM that classifies authorization logic the structural pass missed or misjudged. Useful for business rules that implicitly encode access control, and for languages where structural support hasn't shipped yet (Go, etc.).

## Supported languages

| Language | Structural | Deep (cold-region) | Framework hints (deep) |
|----------|-----------|---------------------|------------------------|
| TypeScript / JavaScript | yes (v0.1) | yes (v0.1) | Express, NestJS, Next.js |
| Java | yes (v0.1) | yes (v0.1) | Spring Security, Jakarta Security |
| Python | yes (v0.1) | yes (v0.1) | Django, Flask, FastAPI |
| Go | planned (v0.2) | yes (v0.1) | Gin, Echo |
| C# | planned (v0.3) | yes (v0.1) | ASP.NET Core |
| Kotlin | planned (v0.3) | yes (v0.1) | Spring (Kotlin) |
| Ruby | planned (v0.3) | yes (v0.1) | Rails |
| PHP | planned (v0.3) | yes (v0.1) | Laravel |

Deep mode walks the full source tree by extension and detects auth-y function names with regex — so it produces useful results in any language well before structural support lands.

## Installation

### Cargo

```bash
cargo install --git https://github.com/EnforceAuth/zift
```

### Binary download

Prebuilt binaries for Linux (x86_64), macOS (x86_64 and arm64), and Windows (x86_64) are available from [Releases](https://github.com/EnforceAuth/zift/releases).

## Deep mode (`--deep`)

Expand Down Expand Up @@ -189,33 +216,6 @@ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":
You should see a single line back with `serverInfo.name == "zift"` and capability flags for tools/resources.
Then call `tools/list` to see the seven tool descriptors.

## Supported languages

| Language | Structural | Deep (cold-region) | Framework hints (deep) |
|----------|-----------|---------------------|------------------------|
| TypeScript / JavaScript | yes (v0.1) | yes (v0.1) | Express, NestJS, Next.js |
| Java | yes (v0.1) | yes (v0.1) | Spring Security, Jakarta Security |
| Python | planned (v0.2) | yes (v0.1) | Django, Flask, FastAPI |
| Go | planned (v0.2) | yes (v0.1) | Gin, Echo |
| C# | planned (v0.3) | yes (v0.1) | ASP.NET Core |
| Kotlin | planned (v0.3) | yes (v0.1) | Spring (Kotlin) |
| Ruby | planned (v0.3) | yes (v0.1) | Rails |
| PHP | planned (v0.3) | yes (v0.1) | Laravel |

Deep mode walks the full source tree by extension and detects auth-y function names with regex — so it produces useful results in any language well before structural support lands.

## Installation

### Cargo

```bash
cargo install --git https://github.com/EnforceAuth/zift
```

### Binary download

Prebuilt binaries for Linux (x86_64), macOS (x86_64 and arm64), and Windows (x86_64) are available from [Releases](https://github.com/EnforceAuth/zift/releases).

## License

Apache-2.0
58 changes: 58 additions & 0 deletions rules/python/django-permission-required.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
[rule]
id = "py-django-permission-required"
languages = ["python"]
category = "middleware"
confidence = "high"
description = "Django @permission_required decorator (with permission codename argument)"
# Matches both bare and module-qualified forms of the decorator:
# @permission_required('app.delete_user')
# @django.contrib.auth.decorators.permission_required('app.delete_user')
# The decorator's call function is captured at the rightmost identifier, so
# arbitrarily deep import paths still bind to `decorator_name`.
query = """
(decorator
(call
function: [
(identifier) @decorator_name
(attribute attribute: (identifier) @decorator_name)
]
arguments: (argument_list
(string (string_content) @perm_name)))
) @match
"""

[rule.predicates.decorator_name]
eq = "permission_required"

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

allow if {
"{{perm_name}}" in input.user.permissions
}
"""

[[rule.tests]]
input = """
@permission_required('app.delete_user')
def delete_user(request, id):
pass
"""
expect_match = true

[[rule.tests]]
input = """
@django.contrib.auth.decorators.permission_required('app.delete_user')
def delete_user(request, id):
pass
"""
expect_match = true

[[rule.tests]]
input = """
@cache_page(60)
def index(request):
pass
"""
expect_match = false
45 changes: 45 additions & 0 deletions rules/python/django-user-passes-test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[rule]
id = "py-django-user-passes-test"
languages = ["python"]
category = "middleware"
confidence = "medium"
description = "Django @user_passes_test decorator (custom predicate gate)"
# The decorator wraps a predicate function/lambda — we can detect the
# decorator but the actual rule encoded inside the predicate needs human
# review or deep-mode analysis. Confidence is `medium` for that reason.
query = """
(decorator
(call
function: [
(identifier) @decorator_name
(attribute attribute: (identifier) @decorator_name)
])
) @match
"""

[rule.predicates.decorator_name]
eq = "user_passes_test"

[[rule.tests]]
input = """
@user_passes_test(lambda u: u.is_admin)
def view(request):
pass
"""
expect_match = true

[[rule.tests]]
input = """
@django.contrib.auth.decorators.user_passes_test(is_staff_check)
def admin_view(request):
pass
"""
expect_match = true

[[rule.tests]]
input = """
@cache_control(max_age=60)
def view(request):
pass
"""
expect_match = false
53 changes: 53 additions & 0 deletions rules/python/fastapi-depends.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
[rule]
id = "py-fastapi-depends"
languages = ["python"]
category = "middleware"
confidence = "medium"
description = "FastAPI Depends(...) used as a parameter default (dependency-injection auth gate)"
# Matches both `token: str = Depends(...)` (typed) and `token = Depends(...)`
# (untyped) parameter defaults. `Depends` is FastAPI's idiomatic way to wire
# auth dependencies (`oauth2_scheme`, `get_current_user`, `require_role`),
# but it's also used for non-auth dependency injection — confidence is
# `medium` and we intentionally emit no rego template (the wrapped callable
# is what encodes the policy, and that needs human or deep-mode review).
query = """
[
(typed_default_parameter
value: (call
function: (identifier) @fn_name)) @match
(default_parameter
value: (call
function: (identifier) @fn_name)) @match
]
"""

[rule.predicates.fn_name]
eq = "Depends"

[[rule.tests]]
input = """
def read_items(token: str = Depends(oauth2_scheme)):
pass
"""
expect_match = true

[[rule.tests]]
input = """
def read_items(token = Depends(get_current_user)):
pass
"""
expect_match = true

[[rule.tests]]
input = """
def read_items(token: str = "default"):
pass
"""
expect_match = false

[[rule.tests]]
input = """
def factory(builder = Builder()):
pass
"""
expect_match = false
73 changes: 73 additions & 0 deletions rules/python/feature-gate-check.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
[rule]
id = "py-feature-gate-check"
languages = ["python"]
category = "feature_gate"
confidence = "medium"
description = "Feature flag or plan-based gating in Python"
# Covers two shapes:
# 1. method call: `flags.has_feature("X")` (mirrors the Java rule)
# 2. property comparison: `user.plan == "enterprise"` (mirrors the TS rule)
# Both branches share the captures `@gate_key` (method or property name)
# and `@gate_value` (feature/plan literal) so a single anchored regex
# in the predicate keeps the call and comparison shapes selective.
# Only the literal-string form is captured; dynamic feature keys
# (Features.BETA_DASHBOARD or a variable) can be added later if false
# negatives surface.
query = """
[
(call
function: (attribute
attribute: (identifier) @gate_key)
arguments: (argument_list
(string (string_content) @gate_value))) @match
(comparison_operator
(attribute attribute: (identifier) @gate_key)
operators: ["==" "is"]
(string (string_content) @gate_value)) @match
]
"""

[rule.predicates.gate_key]
match = "^(has_feature|is_feature_enabled|check_feature|has_plan|is_plan_active|plan|tier|subscription|license|edition|feature_flag|feature)$"

[[rule.tests]]
input = """
if feature_flags.has_feature("advanced-analytics"):
enable()
"""
expect_match = true

[[rule.tests]]
input = """
if subscription.has_plan("pro"):
enable()
"""
expect_match = true

[[rule.tests]]
input = """
if user.plan == "enterprise":
enable_advanced_feature()
"""
expect_match = true

[[rule.tests]]
input = """
if account.tier == "pro":
upgrade()
"""
expect_match = true

[[rule.tests]]
input = """
if validator.is_enabled("field"):
validate()
"""
expect_match = false

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