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
4 changes: 2 additions & 2 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ echo "Running cargo fmt check..."
cargo fmt -- --check

echo "Running cargo clippy..."
cargo clippy -- -D warnings
cargo clippy --all-features -- -D warnings

echo "Running cargo test..."
cargo test
cargo test --all-features

echo "Pre-commit checks passed."
4 changes: 2 additions & 2 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ Thanks for opening a PR! A few quick notes:
<!-- How did you verify this works? Commands, fixtures, manual steps. -->

- [ ] `cargo fmt`
- [ ] `cargo clippy -- -D warnings`
- [ ] `cargo test`
- [ ] `cargo clippy --all-features -- -D warnings`
- [ ] `cargo test --all-features`

## Checklist

Expand Down
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ definitions.
```bash
cargo build
cargo build --release
cargo test
cargo test --all-features
cargo fmt # required before committing
cargo clippy -- -D warnings
cargo clippy --all-features -- -D warnings
```

## Architecture
Expand All @@ -46,8 +46,8 @@ cargo clippy -- -D warnings

### Language support

- v0.1: TypeScript, JavaScript, Java, Python, Go
- v0.2 (planned): C#, Kotlin, Ruby, PHP
- v0.1: TypeScript, JavaScript, Java, Python, Go, C#
- v0.2 (planned): Kotlin, Ruby, PHP

## Conventional Commits & Versioning

Expand Down
8 changes: 4 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ cd zift
git config core.hooksPath .githooks # enables the pre-commit hook

cargo build
cargo test
cargo test --all-features
```

CI runs `rustfmt`, `clippy`, and the test suite. Please run all three locally
before pushing:

```bash
cargo fmt
cargo clippy -- -D warnings
cargo test
cargo clippy --all-features -- -D warnings
cargo test --all-features
```

## Reporting bugs and asking questions
Expand All @@ -37,7 +37,7 @@ cargo test

1. Fork the repo and create a topic branch from `main`.
2. Make your changes. Add tests where it makes sense.
3. Run `cargo fmt`, `cargo clippy -- -D warnings`, and `cargo test`.
3. Run `cargo fmt`, `cargo clippy --all-features -- -D warnings`, and `cargo test --all-features`.
4. Sign off your commits — see [DCO](#developer-certificate-of-origin) below.
5. Use a [Conventional Commits](https://www.conventionalcommits.org/) prefix in the PR title.
6. Open the PR. Be ready to iterate on review feedback.
Expand Down
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.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ tree-sitter-java = "0.23"
tree-sitter-javascript = "0.25"
tree-sitter-python = "0.25"
tree-sitter-go = "0.25"
tree-sitter-c-sharp = "0.23"
ignore = "0.4"
sha2 = "0.11"
regex = "1"
Expand All @@ -48,8 +49,8 @@ mockito = "1"

# Integration tests in `tests/` reach into modules gated behind `unstable`
# (deep, mcp, scanner, config). Mark them as requiring the feature so
# default `cargo test` skips them cleanly; CI runs `cargo test --features
# unstable` to exercise them.
# default `cargo test` skips them cleanly; CI runs `cargo test --all-features`
# to exercise them.
[[test]]
name = "deep_http_integration"
required-features = ["unstable"]
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,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, Java, Python, and Go. `--deep` (LLM-assisted) mode functional via any OpenAI-compatible endpoint or MCP-capable agent host.
> **Status:** v0.2 — structural scanning ready for TypeScript, JavaScript, Java, Python, Go, and C#. `--deep` (LLM-assisted) mode functional via any OpenAI-compatible endpoint or MCP-capable agent host.

## What is zift?

Expand All @@ -27,7 +27,7 @@ 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 (C#, Kotlin, 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 (Kotlin, Ruby, PHP, etc.).

## Supported languages

Expand All @@ -37,7 +37,7 @@ zift report . # detailed findings report
| Java | yes (v0.1) | yes (v0.1) | Spring Security, Jakarta Security |
| Python | yes (v0.1) | yes (v0.1) | Django, Flask, FastAPI |
| Go | yes (v0.1) | yes (v0.1) | Gin, Echo |
| C# | planned (v0.2) | yes (v0.1) | ASP.NET Core |
| C# | yes (v0.2) | yes (v0.1) | ASP.NET Core |
| Kotlin | planned (v0.2) | yes (v0.1) | Spring (Kotlin) |
| Ruby | planned (v0.2) | yes (v0.1) | Rails |
| PHP | planned (v0.2) | yes (v0.1) | Laravel |
Expand Down
4 changes: 3 additions & 1 deletion docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ allow if {

## Language support

These priorities describe release milestones. C# ships in the v0.2 milestone.

### Priority 1 (v0.1)

| Language | Key frameworks / patterns |
Expand All @@ -214,12 +216,12 @@ allow if {
|----------|--------------------------|
| Python | Django (`@permission_required`, `has_perm()`), Flask-Login, FastAPI `Depends()` |
| Go | Custom middleware, Casbin, chi/gorilla middleware chains, `if claims.Role` |
| C# | ASP.NET Core `[Authorize]`, policy-based authorization, `ClaimsPrincipal` checks |
Comment thread
boorad marked this conversation as resolved.

### Priority 3 (v0.3)

| Language | Key frameworks / patterns |
|----------|--------------------------|
| C# | ASP.NET `[Authorize]`, policy-based authorization, `ClaimsPrincipal` checks |
| Kotlin | Spring Security (same patterns as Java), Ktor auth plugins |
| Ruby | Pundit, CanCanCan, Devise, `before_action` guards |
| PHP | Laravel Gates/Policies, Symfony Voters |
Expand Down
1 change: 1 addition & 0 deletions docs/corpus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ We are **not** shipping policies for these projects. The runs exist to stress-te
| Java | [openmrs/openmrs-core](https://github.com/openmrs/openmrs-core) | 76 | 20 (context subset) | Deep surfaces a daemon-thread privilege bypass — a real authz decision invisible to any pattern rule. See [java.md](java.md). |
| Python | [zulip/zulip](https://github.com/zulip/zulip) | 39 | 28 (decorator + lib/users subset) | 14 of 25 deep findings in `decorator.py` are the `@require_*` family — adding one rule converts them all to structural. See [python.md](python.md). |
| Go | [go-gitea/gitea](https://github.com/go-gitea/gitea) | 18 | 23 (perm subset) | Deep surfaces the entire `IsAdmin`/`IsOwner`/`Has*` family the structural pass missed; one predicate widening on `go-has-role-call` closes most of the gap. See [go.md](go.md). |
| C# | [bitwarden/server](https://github.com/bitwarden/server) | 318 | 88 (AdminConsole subset) | ASP.NET Core resource authorization dominates structurally; deep surfaces generic `[Authorize<TRequirement>]`, ownership checks, and helper gates. See [csharp.md](csharp.md). |

> The "deep" column is intentionally a **scoped subset** rather than the whole repo — running deep against 5,000+ files per language is neither cheap nor necessary to surface gaps. Each per-language doc explains the subset and why.
>
Expand Down
147 changes: 147 additions & 0 deletions docs/corpus/csharp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# C# — Bitwarden Server

Real-world results from running Zift against [bitwarden/server](https://github.com/bitwarden/server), Bitwarden's server-side application.

## Why this target

Bitwarden is a production ASP.NET Core codebase with a mature authorization model. It uses controller attributes, policy names, anonymous endpoint overrides, and resource-based checks through `IAuthorizationService`. That makes it a strong first C# corpus target: it exercises framework-level middleware and service-layer authorization, not just toy `[Authorize]` examples.

## Target metadata

| | |
|---|---|
| Repo | [bitwarden/server](https://github.com/bitwarden/server) |
| Commit | `99923132` |
| C# files (excl. `bin/`, `obj/`) | 4,534 |
| LOC (C#) | 1,540,794 |
| Externalized PaC | None observed |
| Zift version | 0.1.9 |

## Structural pass

```bash
zift scan ~/zift-corpus/csharp/server --format json -o structural.json
```

| | |
|---|---|
| Wall time | 111.35s |
| Peak RSS | ~32 MB |
| Total findings | **318** |
| Files with findings | 116 |
| Externalized % | 0% (no policy-import enforcement points emitted) |

The full-repo result includes tests because the corpus methodology scans the shallow clone as-is. Production paths account for 172 findings under `src/` plus 4 under `bitwarden_license/src/`; tests account for 143 findings.

**Findings per rule**

| Rule | Count |
|------|------:|
| `csharp-authorization-service-authorize-async` | 186 |
| `csharp-aspnet-authorize-policy-shorthand` | 82 |
| `csharp-aspnet-allow-anonymous` | 34 |
| `csharp-aspnet-authorize-attribute` | 16 |

**Findings per category**

| Category | Count |
|----------|------:|
| `abac` | 268 |
| `middleware` | 50 |

**Top findings (sample)**

| File | Line | Snippet |
|------|-----:|---------|
| `src/Api/AdminConsole/Controllers/CollectionsController.cs` | 23 | `Authorize("Application")` |
| `src/Api/AdminConsole/Controllers/CollectionsController.cs` | 62 | `_authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Read)` |
| `src/Api/AdminConsole/Controllers/CollectionsController.cs` | 118 | `_authorizationService.AuthorizeAsync(User, CollectionOperations.ReadAll(orgId))` |
| `src/Api/AdminConsole/Controllers/GroupsController.cs` | 23 | `Authorize("Application")` |
| `src/Api/AdminConsole/Controllers/GroupsController.cs` | 134 | `_authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.ModifyGroupAccess)` |
| `src/Api/AdminConsole/Controllers/OrganizationUsersController.cs` | 206 | `_authorizationService.AuthorizeAsync(User, new ManageUsersRequirement())` |
| `src/Api/Controllers/DevicesController.cs` | 243 | `AllowAnonymous` |
| `src/Admin/Controllers/UsersController.cs` | 19 | `Authorize` |

## Gaps & follow-ups

**FN: generic Bitwarden authorization attributes.**

Bitwarden uses a custom generic attribute form extensively:

```csharp
[Authorize<ManageUsersRequirement>]
```

A quick grep found 108 `Authorize<TRequirement>` occurrences in the shallow clone. The current C# rules catch the ASP.NET built-in `[Authorize]`, `[Authorize("Policy")]`, `[Authorize(Policy = "...")]`, and `[Authorize(Roles = "...")]` shapes, but not the generic Bitwarden helper. A dedicated `csharp-bitwarden-authorize-requirement` rule, or a more general generic-attribute rule, would close a large chunk of structural coverage.

**FN: `AuthorizeOrThrowAsync`.**

Bitwarden wraps authorization in `AuthorizeOrThrowAsync(...)` at command/service boundaries. A grep found 21 occurrences. The current `AuthorizeAsync` rule catches the standard ASP.NET method name but misses this wrapper. This is a good candidate for either widening the method predicate or adding a separate high-confidence C# rule.

**FN: policy-builder lambdas.**

Startup code contains policy assertions such as `policy.RequireAssertion(ctx => ctx.User.HasClaim(...))`. The direct `HasClaim(...)` call shape can be detected, but policy-builder context and lambda-based registration deserve their own rule if they show up across more ASP.NET projects.

**FP risk: low for the main structural hits.** The sampled `AuthorizeAsync`, `[Authorize("Application")]`, and `[AllowAnonymous]` findings are real authorization sites. Test files add noise to the whole-repo total, but they are still useful for rule-shape validation because they exercise the same authorization APIs through mocks and assertions.

## Deep pass

Run scoped to the Admin Console controllers — 14 C# files, ~3,400 LOC, concentrated around organization and collection management:

```bash
zift scan ~/zift-corpus/csharp/server/src/Api/AdminConsole/Controllers/ \
--deep --agent-cmd "claude -p --output-format json" \
--format json -o deep.json
```

| | |
|---|---|
| Transport | subprocess (`claude -p --output-format json`) |
| Wall time | 177.98s |
| Total findings | 88 (29 structural retained + 59 semantic) |
| Files with findings | 13 |
| Cost reported | $0.00 |

One candidate was skipped because the agent returned non-JSON for that prompt; Zift treated it as a per-candidate bad response and continued.

**Findings by pass**

| Pass | Count |
|------|------:|
| `semantic` | 59 |
| `structural` | 29 |

**Deep findings by category**

| Category | Count |
|----------|------:|
| `custom` | 23 |
| `middleware` | 22 |
| `ownership` | 5 |
| `abac` | 3 |
| `feature_gate` | 3 |
| `rbac` | 2 |
| `business_rule` | 1 |

**Notable deep-only findings**

| File | Line | Category | Description |
|------|-----:|----------|-------------|
| `GroupsController.cs` | 68 | middleware | `[Authorize<ManageGroupsRequirement>]` gates the endpoint |
| `GroupsController.cs` | 232 | ownership | `group.OrganizationId != orgId` tenant ownership check |
| `GroupsController.cs` | 254 | ownership | Bulk delete validates every group's `OrganizationId` |
| `OrganizationConnectionsController.cs` | 60 | custom | `HasPermissionAsync(...)` gates connection creation |
| `OrganizationConnectionsController.cs` | 178 | rbac | `HasPermissionAsync` chooses `ManageScim` vs `OrganizationOwner` |
| `OrganizationInviteLinksController.cs` | 15 | feature_gate | `[RequireFeature(FeatureFlagKeys.GenerateInviteLink)]` gates the controller |

## Diff structural ↔ deep

The subset had 36 structural findings in the whole-repo structural report. The deep run retained 29 structural findings and added 59 semantic findings.

| Bucket | Count | Notes |
|--------|------:|-------|
| Structural retained | 29 | Mostly `AuthorizeAsync`, `[Authorize("Application")]`, and `[AllowAnonymous]` |
| Deep-only | 59 | Generic authorization attributes, tenant ownership checks, helper gates, and feature flags |
| Structural not retained | 7 | Mostly lower-context structural candidates that deep did not echo back, plus one skipped malformed agent response |

The biggest actionable rule gap is the generic attribute family. In this subset alone, deep surfaced 19 `[Authorize<TRequirement>]` findings; across the full shallow clone, grep found 108 occurrences.
33 changes: 33 additions & 0 deletions rules/csharp/aspnet-allow-anonymous.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[rule]
id = "csharp-aspnet-allow-anonymous"
languages = ["csharp"]
category = "middleware"
confidence = "high"
description = "ASP.NET Core [AllowAnonymous] attribute"
query = """
(attribute
name: [
(identifier) @attribute_name
(qualified_name) @attribute_name
]
) @match
"""

[rule.predicates.attribute_name]
match = "(^|\\.)AllowAnonymous(Attribute)?$"

[[rule.tests]]
input = """
using Microsoft.AspNetCore.Authorization;

[AllowAnonymous]
public IActionResult Health() => Ok();
"""
expect_match = true

[[rule.tests]]
input = """
[Authorize]
public IActionResult Index() => Ok();
"""
expect_match = false
Loading