permissiondiff 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- permissiondiff-0.1.0/LICENSE +13 -0
- permissiondiff-0.1.0/PKG-INFO +244 -0
- permissiondiff-0.1.0/README.md +212 -0
- permissiondiff-0.1.0/pyproject.toml +86 -0
- permissiondiff-0.1.0/pyproject.toml.orig +76 -0
- permissiondiff-0.1.0/src/permissiondiff/__init__.py +23 -0
- permissiondiff-0.1.0/src/permissiondiff/__main__.py +6 -0
- permissiondiff-0.1.0/src/permissiondiff/application.py +102 -0
- permissiondiff-0.1.0/src/permissiondiff/cli.py +265 -0
- permissiondiff-0.1.0/src/permissiondiff/config.py +163 -0
- permissiondiff-0.1.0/src/permissiondiff/engine.py +155 -0
- permissiondiff-0.1.0/src/permissiondiff/errors.py +25 -0
- permissiondiff-0.1.0/src/permissiondiff/evaluator.py +86 -0
- permissiondiff-0.1.0/src/permissiondiff/generator.py +148 -0
- permissiondiff-0.1.0/src/permissiondiff/invariants.py +193 -0
- permissiondiff-0.1.0/src/permissiondiff/loader.py +27 -0
- permissiondiff-0.1.0/src/permissiondiff/models.py +278 -0
- permissiondiff-0.1.0/src/permissiondiff/py.typed +1 -0
- permissiondiff-0.1.0/src/permissiondiff/report.py +121 -0
- permissiondiff-0.1.0/src/permissiondiff/snapshot.py +138 -0
- permissiondiff-0.1.0/src/permissiondiff/worker.py +48 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright 2026 PermissionDiff contributors
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: permissiondiff
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deterministic differential authorization regression testing
|
|
5
|
+
Keywords: authorization,ci,differential-testing,property-based-testing,security-testing
|
|
6
|
+
Author: Abishek Giri
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Dist: hypothesis>=6.100
|
|
21
|
+
Requires-Dist: pydantic>=2.7
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Requires-Dist: rich>=13.7
|
|
24
|
+
Requires-Dist: typer>=0.12
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Project-URL: Homepage, https://github.com/abishekgiri/permissiondiff
|
|
27
|
+
Project-URL: Repository, https://github.com/abishekgiri/permissiondiff
|
|
28
|
+
Project-URL: Issues, https://github.com/abishekgiri/permissiondiff/issues
|
|
29
|
+
Project-URL: Changelog, https://github.com/abishekgiri/permissiondiff/blob/main/CHANGELOG.md
|
|
30
|
+
Project-URL: Security, https://github.com/abishekgiri/permissiondiff/security/policy
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# PermissionDiff
|
|
34
|
+
|
|
35
|
+
> **Prove your code didn't just hand the wrong person the keys.**
|
|
36
|
+
|
|
37
|
+
[](https://github.com/abishekgiri/permissiondiff/actions/workflows/ci.yml)
|
|
38
|
+

|
|
39
|
+

|
|
40
|
+
[](https://github.com/astral-sh/ruff)
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
**PermissionDiff is a CI-native tool that shows exactly how a code change alters effective authorization.** It answers: “Did this pull request accidentally give a user, tenant, role, or service access it did not have before?”
|
|
44
|
+
|
|
45
|
+
PermissionDiff generates meaningful authorization cases from subjects, resources, actions, and context values you declare. It checks explicit invariants and records baseline cases so a candidate authorizer evaluates the **same exact corpus**. Security verdicts are deterministic Python decisions—never LLM judgments.
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
CRITICAL — Cross-tenant access must be denied
|
|
49
|
+
support(acme) → read_invoice → invoice(globex)
|
|
50
|
+
actual: ALLOW
|
|
51
|
+
repro: .permissiondiff/failures/PD-0001.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
PermissionDiff is at **v0/alpha** maturity. The core workflow is tested and usable, but public
|
|
55
|
+
interfaces may evolve before 1.0. It supports Python 3.12, 3.13, and 3.14 and is licensed under
|
|
56
|
+
[Apache-2.0](https://github.com/abishekgiri/permissiondiff/blob/main/LICENSE).
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
Install the CLI with uv:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
uv tool install permissiondiff
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or install it in an active virtual environment with pip:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python -m pip install permissiondiff
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
To add PermissionDiff to an existing uv project instead, run `uv add permissiondiff`.
|
|
73
|
+
|
|
74
|
+
## Five-minute quickstart
|
|
75
|
+
|
|
76
|
+
After installation:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
permissiondiff init demo
|
|
80
|
+
cd demo
|
|
81
|
+
permissiondiff test
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`init` creates a runnable config and a deliberately vulnerable authorizer. The test reports a minimized cross-tenant reproduction under `.permissiondiff/failures/` and exits `1` because the tenant-isolation invariant fails.
|
|
85
|
+
|
|
86
|
+
## Authorizer interface
|
|
87
|
+
|
|
88
|
+
Point PermissionDiff at one ordinary decision function:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from permissiondiff import Action, Context, Decision, Resource, Subject
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def authorize(
|
|
95
|
+
subject: Subject,
|
|
96
|
+
action: Action,
|
|
97
|
+
resource: Resource,
|
|
98
|
+
context: Context,
|
|
99
|
+
) -> Decision:
|
|
100
|
+
if subject.tenant != resource.tenant:
|
|
101
|
+
return Decision.DENY
|
|
102
|
+
if action.name == "read_invoice" and subject.role in {"support", "admin"}:
|
|
103
|
+
return Decision.ALLOW
|
|
104
|
+
return Decision.DENY
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The result must be exactly `Decision.ALLOW` or `Decision.DENY`. Crashes, hangs, and invalid return values are explicit evaluation errors and exit `3`; PermissionDiff never fails open.
|
|
108
|
+
|
|
109
|
+
## Configuration
|
|
110
|
+
|
|
111
|
+
The YAML describes real entities rather than asking a fuzzer to invent arbitrary application objects:
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
authorizer: auth:authorize
|
|
115
|
+
|
|
116
|
+
subjects:
|
|
117
|
+
- {id: alice, tenant: acme, role: support}
|
|
118
|
+
- {id: bob, tenant: globex, role: support}
|
|
119
|
+
|
|
120
|
+
resources:
|
|
121
|
+
- {id: invoice-acme, type: invoice, tenant: acme, owner_id: alice}
|
|
122
|
+
- {id: invoice-globex, type: invoice, tenant: globex, owner_id: bob}
|
|
123
|
+
|
|
124
|
+
actions: [read_invoice, refund]
|
|
125
|
+
contexts:
|
|
126
|
+
amounts: {min: 0, max: 1000, boundaries: [499, 500, 501]}
|
|
127
|
+
|
|
128
|
+
invariants:
|
|
129
|
+
- tenant_isolation
|
|
130
|
+
- role_boundary: {action: refund, allowed_roles: [admin]}
|
|
131
|
+
- ownership: {actions: [read_invoice]}
|
|
132
|
+
|
|
133
|
+
fail_on:
|
|
134
|
+
invariant_violation: true
|
|
135
|
+
newly_allowed: true
|
|
136
|
+
newly_denied: false
|
|
137
|
+
|
|
138
|
+
generation: {max_examples: 64}
|
|
139
|
+
execution: {timeout_seconds: 2}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Hypothesis combines declared entities and explores numeric boundaries. With the same PermissionDiff and Python versions, configuration, seed, exact corpus, and deterministic authorizer, findings are semantically reproducible. Canonical JSON, stable case fingerprints, stable finding ordering, and fixed IDs make review practical. A nondeterministic authorizer remains nondeterministic; PermissionDiff does not conceal that.
|
|
143
|
+
|
|
144
|
+
## Test and invariant workflow
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
uv run permissiondiff test --config permissiondiff.yaml --seed 42
|
|
148
|
+
uv run permissiondiff explain PD-0001
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Built-in invariants cover tenant isolation, action-specific role boundaries, and ownership. A custom deterministic invariant can be declared as:
|
|
152
|
+
|
|
153
|
+
```yaml
|
|
154
|
+
invariants:
|
|
155
|
+
- custom: {name: refund_limit, callable: invariants:refund_limit}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The callable receives `(AuthorizationCase, Decision)` and returns `bool` or `InvariantResult`.
|
|
159
|
+
|
|
160
|
+
## Baseline and diff workflow
|
|
161
|
+
|
|
162
|
+
Create a baseline on trusted code:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
uv run permissiondiff snapshot --config permissiondiff.yaml \
|
|
166
|
+
--output .permissiondiff/main.json --seed 42
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The snapshot stores every exact input case, its baseline decision, a stable fingerprint, the corpus seed, and schema/package versions. On candidate code, replay it:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
uv run permissiondiff diff --config permissiondiff.yaml \
|
|
173
|
+
--baseline .permissiondiff/main.json
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Classification is exhaustive:
|
|
177
|
+
|
|
178
|
+
| Baseline | Candidate | Result |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| DENY | DENY | `unchanged_denied` |
|
|
181
|
+
| ALLOW | ALLOW | `unchanged_allowed` |
|
|
182
|
+
| DENY | ALLOW | `newly_allowed` |
|
|
183
|
+
| ALLOW | DENY | `newly_denied` |
|
|
184
|
+
|
|
185
|
+
A newly allowed path is security-sensitive, not automatically a vulnerability. `fail_on` decides what blocks CI. Invariants can still catch a vulnerability already present in the baseline.
|
|
186
|
+
|
|
187
|
+
The complete machine report is `.permissiondiff/report.json` by default.
|
|
188
|
+
|
|
189
|
+
## CI
|
|
190
|
+
|
|
191
|
+
```yaml
|
|
192
|
+
name: permissiondiff
|
|
193
|
+
on: [pull_request]
|
|
194
|
+
jobs:
|
|
195
|
+
authorization:
|
|
196
|
+
runs-on: ubuntu-latest
|
|
197
|
+
steps:
|
|
198
|
+
- uses: actions/checkout@v4
|
|
199
|
+
- uses: astral-sh/setup-uv@v6
|
|
200
|
+
- run: uv sync --all-groups
|
|
201
|
+
- run: uv run permissiondiff diff --baseline .permissiondiff/main.json
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Exit codes are stable: `0` pass, `1` configured policy/invariant failure, `2` configuration or snapshot error, `3` evaluation/runtime error.
|
|
205
|
+
|
|
206
|
+
The repository's thin composite action invokes the same CLI without duplicating its logic:
|
|
207
|
+
|
|
208
|
+
```yaml
|
|
209
|
+
- uses: abishekgiri/permissiondiff@v0
|
|
210
|
+
with:
|
|
211
|
+
config: permissiondiff.yaml
|
|
212
|
+
baseline: .permissiondiff/main.json
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Security and limitations
|
|
216
|
+
|
|
217
|
+
**Only point PermissionDiff at decision logic with no live side effects. Never use an authorizer that issues refunds, deletes data, sends messages, or contacts production.**
|
|
218
|
+
|
|
219
|
+
Authorizers run in a timeout-controlled subprocess to isolate crashes and hangs from the main CLI. **This subprocess is not a security sandbox.** Imported Python code has the operating-system permissions of the user running PermissionDiff and may be malicious. Use trusted code and isolated CI environments.
|
|
220
|
+
|
|
221
|
+
Snapshots and reproductions may contain sensitive identifiers. Treat them accordingly. v0 supports local Python authorizers and explicit domains; it does not include Git worktree orchestration, remote policy engines, live agent/tool interception, a dashboard, or least-privilege mining.
|
|
222
|
+
|
|
223
|
+
Report vulnerabilities privately as described in the
|
|
224
|
+
[security policy](https://github.com/abishekgiri/permissiondiff/security/policy). For bugs and
|
|
225
|
+
feature requests, use [GitHub Issues](https://github.com/abishekgiri/permissiondiff/issues).
|
|
226
|
+
|
|
227
|
+
## Development
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
uv sync --all-groups
|
|
231
|
+
uv run ruff check .
|
|
232
|
+
uv run ruff format --check .
|
|
233
|
+
uv run mypy src tests
|
|
234
|
+
uv run pytest
|
|
235
|
+
uv run pytest --cov=permissiondiff --cov-report=term-missing
|
|
236
|
+
uv build
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The roadmap is deliberately short: prove authorization diffs are useful, then consider policy-engine adapters, Git-aware orchestration, agent principals/delegation, safe shadow interception, and least-privilege reduction with proof.
|
|
240
|
+
|
|
241
|
+
See the [contribution guide](https://github.com/abishekgiri/permissiondiff/blob/main/CONTRIBUTING.md)
|
|
242
|
+
for the contribution workflow and the
|
|
243
|
+
[changelog](https://github.com/abishekgiri/permissiondiff/blob/main/CHANGELOG.md) for release
|
|
244
|
+
history.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# PermissionDiff
|
|
2
|
+
|
|
3
|
+
> **Prove your code didn't just hand the wrong person the keys.**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/abishekgiri/permissiondiff/actions/workflows/ci.yml)
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
[](https://github.com/astral-sh/ruff)
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
**PermissionDiff is a CI-native tool that shows exactly how a code change alters effective authorization.** It answers: “Did this pull request accidentally give a user, tenant, role, or service access it did not have before?”
|
|
12
|
+
|
|
13
|
+
PermissionDiff generates meaningful authorization cases from subjects, resources, actions, and context values you declare. It checks explicit invariants and records baseline cases so a candidate authorizer evaluates the **same exact corpus**. Security verdicts are deterministic Python decisions—never LLM judgments.
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
CRITICAL — Cross-tenant access must be denied
|
|
17
|
+
support(acme) → read_invoice → invoice(globex)
|
|
18
|
+
actual: ALLOW
|
|
19
|
+
repro: .permissiondiff/failures/PD-0001.json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
PermissionDiff is at **v0/alpha** maturity. The core workflow is tested and usable, but public
|
|
23
|
+
interfaces may evolve before 1.0. It supports Python 3.12, 3.13, and 3.14 and is licensed under
|
|
24
|
+
[Apache-2.0](https://github.com/abishekgiri/permissiondiff/blob/main/LICENSE).
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Install the CLI with uv:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv tool install permissiondiff
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or install it in an active virtual environment with pip:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python -m pip install permissiondiff
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
To add PermissionDiff to an existing uv project instead, run `uv add permissiondiff`.
|
|
41
|
+
|
|
42
|
+
## Five-minute quickstart
|
|
43
|
+
|
|
44
|
+
After installation:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
permissiondiff init demo
|
|
48
|
+
cd demo
|
|
49
|
+
permissiondiff test
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`init` creates a runnable config and a deliberately vulnerable authorizer. The test reports a minimized cross-tenant reproduction under `.permissiondiff/failures/` and exits `1` because the tenant-isolation invariant fails.
|
|
53
|
+
|
|
54
|
+
## Authorizer interface
|
|
55
|
+
|
|
56
|
+
Point PermissionDiff at one ordinary decision function:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from permissiondiff import Action, Context, Decision, Resource, Subject
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def authorize(
|
|
63
|
+
subject: Subject,
|
|
64
|
+
action: Action,
|
|
65
|
+
resource: Resource,
|
|
66
|
+
context: Context,
|
|
67
|
+
) -> Decision:
|
|
68
|
+
if subject.tenant != resource.tenant:
|
|
69
|
+
return Decision.DENY
|
|
70
|
+
if action.name == "read_invoice" and subject.role in {"support", "admin"}:
|
|
71
|
+
return Decision.ALLOW
|
|
72
|
+
return Decision.DENY
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The result must be exactly `Decision.ALLOW` or `Decision.DENY`. Crashes, hangs, and invalid return values are explicit evaluation errors and exit `3`; PermissionDiff never fails open.
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
The YAML describes real entities rather than asking a fuzzer to invent arbitrary application objects:
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
authorizer: auth:authorize
|
|
83
|
+
|
|
84
|
+
subjects:
|
|
85
|
+
- {id: alice, tenant: acme, role: support}
|
|
86
|
+
- {id: bob, tenant: globex, role: support}
|
|
87
|
+
|
|
88
|
+
resources:
|
|
89
|
+
- {id: invoice-acme, type: invoice, tenant: acme, owner_id: alice}
|
|
90
|
+
- {id: invoice-globex, type: invoice, tenant: globex, owner_id: bob}
|
|
91
|
+
|
|
92
|
+
actions: [read_invoice, refund]
|
|
93
|
+
contexts:
|
|
94
|
+
amounts: {min: 0, max: 1000, boundaries: [499, 500, 501]}
|
|
95
|
+
|
|
96
|
+
invariants:
|
|
97
|
+
- tenant_isolation
|
|
98
|
+
- role_boundary: {action: refund, allowed_roles: [admin]}
|
|
99
|
+
- ownership: {actions: [read_invoice]}
|
|
100
|
+
|
|
101
|
+
fail_on:
|
|
102
|
+
invariant_violation: true
|
|
103
|
+
newly_allowed: true
|
|
104
|
+
newly_denied: false
|
|
105
|
+
|
|
106
|
+
generation: {max_examples: 64}
|
|
107
|
+
execution: {timeout_seconds: 2}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Hypothesis combines declared entities and explores numeric boundaries. With the same PermissionDiff and Python versions, configuration, seed, exact corpus, and deterministic authorizer, findings are semantically reproducible. Canonical JSON, stable case fingerprints, stable finding ordering, and fixed IDs make review practical. A nondeterministic authorizer remains nondeterministic; PermissionDiff does not conceal that.
|
|
111
|
+
|
|
112
|
+
## Test and invariant workflow
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
uv run permissiondiff test --config permissiondiff.yaml --seed 42
|
|
116
|
+
uv run permissiondiff explain PD-0001
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Built-in invariants cover tenant isolation, action-specific role boundaries, and ownership. A custom deterministic invariant can be declared as:
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
invariants:
|
|
123
|
+
- custom: {name: refund_limit, callable: invariants:refund_limit}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The callable receives `(AuthorizationCase, Decision)` and returns `bool` or `InvariantResult`.
|
|
127
|
+
|
|
128
|
+
## Baseline and diff workflow
|
|
129
|
+
|
|
130
|
+
Create a baseline on trusted code:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
uv run permissiondiff snapshot --config permissiondiff.yaml \
|
|
134
|
+
--output .permissiondiff/main.json --seed 42
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The snapshot stores every exact input case, its baseline decision, a stable fingerprint, the corpus seed, and schema/package versions. On candidate code, replay it:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
uv run permissiondiff diff --config permissiondiff.yaml \
|
|
141
|
+
--baseline .permissiondiff/main.json
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Classification is exhaustive:
|
|
145
|
+
|
|
146
|
+
| Baseline | Candidate | Result |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| DENY | DENY | `unchanged_denied` |
|
|
149
|
+
| ALLOW | ALLOW | `unchanged_allowed` |
|
|
150
|
+
| DENY | ALLOW | `newly_allowed` |
|
|
151
|
+
| ALLOW | DENY | `newly_denied` |
|
|
152
|
+
|
|
153
|
+
A newly allowed path is security-sensitive, not automatically a vulnerability. `fail_on` decides what blocks CI. Invariants can still catch a vulnerability already present in the baseline.
|
|
154
|
+
|
|
155
|
+
The complete machine report is `.permissiondiff/report.json` by default.
|
|
156
|
+
|
|
157
|
+
## CI
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
name: permissiondiff
|
|
161
|
+
on: [pull_request]
|
|
162
|
+
jobs:
|
|
163
|
+
authorization:
|
|
164
|
+
runs-on: ubuntu-latest
|
|
165
|
+
steps:
|
|
166
|
+
- uses: actions/checkout@v4
|
|
167
|
+
- uses: astral-sh/setup-uv@v6
|
|
168
|
+
- run: uv sync --all-groups
|
|
169
|
+
- run: uv run permissiondiff diff --baseline .permissiondiff/main.json
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Exit codes are stable: `0` pass, `1` configured policy/invariant failure, `2` configuration or snapshot error, `3` evaluation/runtime error.
|
|
173
|
+
|
|
174
|
+
The repository's thin composite action invokes the same CLI without duplicating its logic:
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
- uses: abishekgiri/permissiondiff@v0
|
|
178
|
+
with:
|
|
179
|
+
config: permissiondiff.yaml
|
|
180
|
+
baseline: .permissiondiff/main.json
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Security and limitations
|
|
184
|
+
|
|
185
|
+
**Only point PermissionDiff at decision logic with no live side effects. Never use an authorizer that issues refunds, deletes data, sends messages, or contacts production.**
|
|
186
|
+
|
|
187
|
+
Authorizers run in a timeout-controlled subprocess to isolate crashes and hangs from the main CLI. **This subprocess is not a security sandbox.** Imported Python code has the operating-system permissions of the user running PermissionDiff and may be malicious. Use trusted code and isolated CI environments.
|
|
188
|
+
|
|
189
|
+
Snapshots and reproductions may contain sensitive identifiers. Treat them accordingly. v0 supports local Python authorizers and explicit domains; it does not include Git worktree orchestration, remote policy engines, live agent/tool interception, a dashboard, or least-privilege mining.
|
|
190
|
+
|
|
191
|
+
Report vulnerabilities privately as described in the
|
|
192
|
+
[security policy](https://github.com/abishekgiri/permissiondiff/security/policy). For bugs and
|
|
193
|
+
feature requests, use [GitHub Issues](https://github.com/abishekgiri/permissiondiff/issues).
|
|
194
|
+
|
|
195
|
+
## Development
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
uv sync --all-groups
|
|
199
|
+
uv run ruff check .
|
|
200
|
+
uv run ruff format --check .
|
|
201
|
+
uv run mypy src tests
|
|
202
|
+
uv run pytest
|
|
203
|
+
uv run pytest --cov=permissiondiff --cov-report=term-missing
|
|
204
|
+
uv build
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The roadmap is deliberately short: prove authorization diffs are useful, then consider policy-engine adapters, Git-aware orchestration, agent principals/delegation, safe shadow interception, and least-privilege reduction with proof.
|
|
208
|
+
|
|
209
|
+
See the [contribution guide](https://github.com/abishekgiri/permissiondiff/blob/main/CONTRIBUTING.md)
|
|
210
|
+
for the contribution workflow and the
|
|
211
|
+
[changelog](https://github.com/abishekgiri/permissiondiff/blob/main/CHANGELOG.md) for release
|
|
212
|
+
history.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "permissiondiff"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Deterministic differential authorization regression testing"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"authorization",
|
|
11
|
+
"ci",
|
|
12
|
+
"differential-testing",
|
|
13
|
+
"property-based-testing",
|
|
14
|
+
"security-testing",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Topic :: Security",
|
|
26
|
+
"Topic :: Software Development :: Testing",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"hypothesis>=6.100",
|
|
31
|
+
"pydantic>=2.7",
|
|
32
|
+
"pyyaml>=6.0",
|
|
33
|
+
"rich>=13.7",
|
|
34
|
+
"typer>=0.12",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[[project.authors]]
|
|
38
|
+
name = "Abishek Giri"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/abishekgiri/permissiondiff"
|
|
42
|
+
Repository = "https://github.com/abishekgiri/permissiondiff"
|
|
43
|
+
Issues = "https://github.com/abishekgiri/permissiondiff/issues"
|
|
44
|
+
Changelog = "https://github.com/abishekgiri/permissiondiff/blob/main/CHANGELOG.md"
|
|
45
|
+
Security = "https://github.com/abishekgiri/permissiondiff/security/policy"
|
|
46
|
+
|
|
47
|
+
[project.scripts]
|
|
48
|
+
permissiondiff = "permissiondiff.cli:app"
|
|
49
|
+
|
|
50
|
+
[dependency-groups]
|
|
51
|
+
dev = [
|
|
52
|
+
"mypy>=1.10",
|
|
53
|
+
"pytest>=8.2",
|
|
54
|
+
"pytest-cov>=5.0",
|
|
55
|
+
"ruff>=0.5",
|
|
56
|
+
"types-pyyaml>=6.0",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[build-system]
|
|
60
|
+
requires = ["uv_build>=0.11.1,<0.12.0"]
|
|
61
|
+
build-backend = "uv_build"
|
|
62
|
+
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
line-length = 100
|
|
65
|
+
target-version = "py312"
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint]
|
|
68
|
+
select = [
|
|
69
|
+
"E",
|
|
70
|
+
"F",
|
|
71
|
+
"I",
|
|
72
|
+
"UP",
|
|
73
|
+
"B",
|
|
74
|
+
"SIM",
|
|
75
|
+
"RUF",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[tool.mypy]
|
|
79
|
+
python_version = "3.12"
|
|
80
|
+
strict = true
|
|
81
|
+
packages = ["permissiondiff"]
|
|
82
|
+
mypy_path = "src"
|
|
83
|
+
|
|
84
|
+
[tool.pytest.ini_options]
|
|
85
|
+
addopts = "-ra"
|
|
86
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "permissiondiff"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Deterministic differential authorization regression testing"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Abishek Giri" }]
|
|
10
|
+
keywords = [
|
|
11
|
+
"authorization",
|
|
12
|
+
"ci",
|
|
13
|
+
"differential-testing",
|
|
14
|
+
"property-based-testing",
|
|
15
|
+
"security-testing",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Topic :: Security",
|
|
27
|
+
"Topic :: Software Development :: Testing",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"hypothesis>=6.100",
|
|
32
|
+
"pydantic>=2.7",
|
|
33
|
+
"pyyaml>=6.0",
|
|
34
|
+
"rich>=13.7",
|
|
35
|
+
"typer>=0.12",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/abishekgiri/permissiondiff"
|
|
40
|
+
Repository = "https://github.com/abishekgiri/permissiondiff"
|
|
41
|
+
Issues = "https://github.com/abishekgiri/permissiondiff/issues"
|
|
42
|
+
Changelog = "https://github.com/abishekgiri/permissiondiff/blob/main/CHANGELOG.md"
|
|
43
|
+
Security = "https://github.com/abishekgiri/permissiondiff/security/policy"
|
|
44
|
+
|
|
45
|
+
[project.scripts]
|
|
46
|
+
permissiondiff = "permissiondiff.cli:app"
|
|
47
|
+
|
|
48
|
+
[dependency-groups]
|
|
49
|
+
dev = [
|
|
50
|
+
"mypy>=1.10",
|
|
51
|
+
"pytest>=8.2",
|
|
52
|
+
"pytest-cov>=5.0",
|
|
53
|
+
"ruff>=0.5",
|
|
54
|
+
"types-pyyaml>=6.0",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[build-system]
|
|
58
|
+
requires = ["uv_build>=0.11.1,<0.12.0"]
|
|
59
|
+
build-backend = "uv_build"
|
|
60
|
+
|
|
61
|
+
[tool.ruff]
|
|
62
|
+
line-length = 100
|
|
63
|
+
target-version = "py312"
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint]
|
|
66
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
67
|
+
|
|
68
|
+
[tool.mypy]
|
|
69
|
+
python_version = "3.12"
|
|
70
|
+
strict = true
|
|
71
|
+
packages = ["permissiondiff"]
|
|
72
|
+
mypy_path = "src"
|
|
73
|
+
|
|
74
|
+
[tool.pytest.ini_options]
|
|
75
|
+
addopts = "-ra"
|
|
76
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Public PermissionDiff API."""
|
|
2
|
+
|
|
3
|
+
from permissiondiff.models import (
|
|
4
|
+
Action,
|
|
5
|
+
AuthorizationCase,
|
|
6
|
+
ChangeType,
|
|
7
|
+
Context,
|
|
8
|
+
Decision,
|
|
9
|
+
Resource,
|
|
10
|
+
Subject,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Action",
|
|
15
|
+
"AuthorizationCase",
|
|
16
|
+
"ChangeType",
|
|
17
|
+
"Context",
|
|
18
|
+
"Decision",
|
|
19
|
+
"Resource",
|
|
20
|
+
"Subject",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|