admission-gate 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.
- admission_gate-0.1.0/LICENSE +21 -0
- admission_gate-0.1.0/PKG-INFO +194 -0
- admission_gate-0.1.0/README.md +180 -0
- admission_gate-0.1.0/pyproject.toml +21 -0
- admission_gate-0.1.0/setup.cfg +4 -0
- admission_gate-0.1.0/src/admission_gate/__init__.py +13 -0
- admission_gate-0.1.0/src/admission_gate/gate.py +249 -0
- admission_gate-0.1.0/src/admission_gate/verify.py +74 -0
- admission_gate-0.1.0/src/admission_gate.egg-info/PKG-INFO +194 -0
- admission_gate-0.1.0/src/admission_gate.egg-info/SOURCES.txt +12 -0
- admission_gate-0.1.0/src/admission_gate.egg-info/dependency_links.txt +1 -0
- admission_gate-0.1.0/src/admission_gate.egg-info/entry_points.txt +3 -0
- admission_gate-0.1.0/src/admission_gate.egg-info/top_level.txt +1 -0
- admission_gate-0.1.0/tests/test_gate.py +55 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ak-skwaa-mahawk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: admission-gate
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deterministic admission gate with human review and cryptographic audit logging for CLI agents.
|
|
5
|
+
Author: ak-skwaa-mahawk
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# admission-gate
|
|
16
|
+
|
|
17
|
+
[](https://github.com/ak-skwaa-mahawk/admission-gate/actions/workflows/ci.yml)
|
|
18
|
+
[](https://opensource.org/licenses/MIT)
|
|
19
|
+
[](https://github.com/ak-skwaa-mahawk/admission-gate/releases)
|
|
20
|
+
|
|
21
|
+
A lightweight admission gatekeeper and cryptographic audit logger for autonomous CLI agents.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
name: Path bypass / policy edge case
|
|
25
|
+
about: Report a path canonicalization escape or policy engine loophole
|
|
26
|
+
title: "[BYPASS]: "
|
|
27
|
+
labels: bug, security
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
### Environment
|
|
31
|
+
- OS: [e.g. Windows 11, macOS Sonoma, Ubuntu 24.04, Android Termux]
|
|
32
|
+
- Python Version: [e.g. 3.11]
|
|
33
|
+
|
|
34
|
+
### Proposed Action
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"action_id": "test_bypass",
|
|
38
|
+
"command": "...",
|
|
39
|
+
"target_path": "...",
|
|
40
|
+
"risk_tier": 1
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### 3. Add an Integration Example Script
|
|
47
|
+
|
|
48
|
+
Include a short example script under `examples/agent_tool_example.py` demonstrating how an LLM tool loop hooks into `gated_shell()`:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
"""
|
|
52
|
+
Example: Hooking admission-gate into an agent tool execution loop.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from admission_gate import ActionProposal, gated_shell
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def agent_shell_tool(command: str, target: str) -> str:
|
|
59
|
+
proposal = ActionProposal(
|
|
60
|
+
action_id="agent_call_001",
|
|
61
|
+
command=command,
|
|
62
|
+
target_path=target,
|
|
63
|
+
risk_tier=2,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
executed, output, code = gated_shell(
|
|
67
|
+
proposal,
|
|
68
|
+
allowed_roots=["./workspace"],
|
|
69
|
+
log_path="audit_log.jsonl",
|
|
70
|
+
require_confirm=True,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if not executed:
|
|
74
|
+
return f"Refused by gate: {output}"
|
|
75
|
+
|
|
76
|
+
return f"Success (exit code {code}):\n{output}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
result = agent_shell_tool("ls -la", "./workspace")
|
|
81
|
+
print(result)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# 1. Create the issue template directory and file
|
|
87
|
+
mkdir -p .github/ISSUE_TEMPLATE
|
|
88
|
+
cat << 'EOF' > .github/ISSUE_TEMPLATE/path_bypass.md
|
|
89
|
+
---
|
|
90
|
+
name: Path bypass / policy edge case
|
|
91
|
+
about: Report a path canonicalization escape or policy engine loophole
|
|
92
|
+
title: "[BYPASS]: "
|
|
93
|
+
labels: bug, security
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### Environment
|
|
97
|
+
- OS: [e.g. Windows 11, macOS Sonoma, Ubuntu 24.04, Android Termux]
|
|
98
|
+
- Python Version: [e.g. 3.11]
|
|
99
|
+
|
|
100
|
+
### Proposed Action
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"action_id": "test_bypass",
|
|
104
|
+
"command": "...",
|
|
105
|
+
"target_path": "...",
|
|
106
|
+
"risk_tier": 1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# admission-gate
|
|
112
|
+
|
|
113
|
+
A lightweight admission gatekeeper and cryptographic audit logger for autonomous CLI agents.
|
|
114
|
+
|
|
115
|
+
When local scripts or LLMs execute commands in a subshell, developers are forced between two extremes: fully autonomous execution that risks destructive operations, or prompt fatigue from micro-approving dozens of benign commands. `admission-gate` sits between the agent and your shell:
|
|
116
|
+
|
|
117
|
+
1. **Deterministic Filter**: Blocks dangerous patterns (`rm -rf /`) and restricts filesystem access to configured `allowed_roots` using strict path canonicalization.
|
|
118
|
+
2. **Interactive TTY Confirmation**: Halts for human authorization (`[y/N]`) only on proposals that pass static policy checks.
|
|
119
|
+
3. **SHA-256 Audit Trail**: Commits all proposals, policy outcomes, and approval decisions to an append-only, hash-chained log.
|
|
120
|
+
|
|
121
|
+
Requires Python 3.8+ with zero third-party dependencies.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Quickstart
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
git clone [https://github.com/ak-skwaa-mahawk/admission-gate.git](https://github.com/ak-skwaa-mahawk/admission-gate.git)
|
|
129
|
+
cd admission-gate
|
|
130
|
+
pip install .
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Library Usage
|
|
136
|
+
|
|
137
|
+
Wrap your agent's shell execution tool so rejected actions never hit the subshell:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from admission_gate import ActionProposal, gated_shell
|
|
141
|
+
|
|
142
|
+
proposal = ActionProposal(
|
|
143
|
+
action_id="task_101",
|
|
144
|
+
command="rm scratch.tmp",
|
|
145
|
+
target_path="./workspace/scratch.tmp",
|
|
146
|
+
risk_tier=2,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Returns (executed: bool, output: str, exit_code: int)
|
|
150
|
+
executed, output, code = gated_shell(
|
|
151
|
+
proposal,
|
|
152
|
+
allowed_roots=["./workspace"],
|
|
153
|
+
log_path="audit_log.jsonl",
|
|
154
|
+
require_confirm=True,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if not executed:
|
|
158
|
+
print(f"Action refused: {output}")
|
|
159
|
+
else:
|
|
160
|
+
print(f"Command succeeded:\n{output}")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Standalone Policy Checks
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from admission_gate import ActionProposal, evaluate
|
|
167
|
+
|
|
168
|
+
p = ActionProposal("chk_1", "cat /etc/shadow", "/etc/shadow", 1)
|
|
169
|
+
passed, reason = evaluate(p)
|
|
170
|
+
# passed -> False
|
|
171
|
+
# reason -> "Blocked: path resolves to protected directory '/etc'"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Verifying Audit Log Integrity
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
python3 src/admission_gate/verify.py audit_log.jsonl
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Running Tests
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
PYTHONPATH=src python3 -m unittest discover -s tests
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# admission-gate
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ak-skwaa-mahawk/admission-gate/actions/workflows/ci.yml)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://github.com/ak-skwaa-mahawk/admission-gate/releases)
|
|
6
|
+
|
|
7
|
+
A lightweight admission gatekeeper and cryptographic audit logger for autonomous CLI agents.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
name: Path bypass / policy edge case
|
|
11
|
+
about: Report a path canonicalization escape or policy engine loophole
|
|
12
|
+
title: "[BYPASS]: "
|
|
13
|
+
labels: bug, security
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
### Environment
|
|
17
|
+
- OS: [e.g. Windows 11, macOS Sonoma, Ubuntu 24.04, Android Termux]
|
|
18
|
+
- Python Version: [e.g. 3.11]
|
|
19
|
+
|
|
20
|
+
### Proposed Action
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"action_id": "test_bypass",
|
|
24
|
+
"command": "...",
|
|
25
|
+
"target_path": "...",
|
|
26
|
+
"risk_tier": 1
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
### 3. Add an Integration Example Script
|
|
33
|
+
|
|
34
|
+
Include a short example script under `examples/agent_tool_example.py` demonstrating how an LLM tool loop hooks into `gated_shell()`:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
"""
|
|
38
|
+
Example: Hooking admission-gate into an agent tool execution loop.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from admission_gate import ActionProposal, gated_shell
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def agent_shell_tool(command: str, target: str) -> str:
|
|
45
|
+
proposal = ActionProposal(
|
|
46
|
+
action_id="agent_call_001",
|
|
47
|
+
command=command,
|
|
48
|
+
target_path=target,
|
|
49
|
+
risk_tier=2,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
executed, output, code = gated_shell(
|
|
53
|
+
proposal,
|
|
54
|
+
allowed_roots=["./workspace"],
|
|
55
|
+
log_path="audit_log.jsonl",
|
|
56
|
+
require_confirm=True,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if not executed:
|
|
60
|
+
return f"Refused by gate: {output}"
|
|
61
|
+
|
|
62
|
+
return f"Success (exit code {code}):\n{output}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
result = agent_shell_tool("ls -la", "./workspace")
|
|
67
|
+
print(result)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# 1. Create the issue template directory and file
|
|
73
|
+
mkdir -p .github/ISSUE_TEMPLATE
|
|
74
|
+
cat << 'EOF' > .github/ISSUE_TEMPLATE/path_bypass.md
|
|
75
|
+
---
|
|
76
|
+
name: Path bypass / policy edge case
|
|
77
|
+
about: Report a path canonicalization escape or policy engine loophole
|
|
78
|
+
title: "[BYPASS]: "
|
|
79
|
+
labels: bug, security
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### Environment
|
|
83
|
+
- OS: [e.g. Windows 11, macOS Sonoma, Ubuntu 24.04, Android Termux]
|
|
84
|
+
- Python Version: [e.g. 3.11]
|
|
85
|
+
|
|
86
|
+
### Proposed Action
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"action_id": "test_bypass",
|
|
90
|
+
"command": "...",
|
|
91
|
+
"target_path": "...",
|
|
92
|
+
"risk_tier": 1
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# admission-gate
|
|
98
|
+
|
|
99
|
+
A lightweight admission gatekeeper and cryptographic audit logger for autonomous CLI agents.
|
|
100
|
+
|
|
101
|
+
When local scripts or LLMs execute commands in a subshell, developers are forced between two extremes: fully autonomous execution that risks destructive operations, or prompt fatigue from micro-approving dozens of benign commands. `admission-gate` sits between the agent and your shell:
|
|
102
|
+
|
|
103
|
+
1. **Deterministic Filter**: Blocks dangerous patterns (`rm -rf /`) and restricts filesystem access to configured `allowed_roots` using strict path canonicalization.
|
|
104
|
+
2. **Interactive TTY Confirmation**: Halts for human authorization (`[y/N]`) only on proposals that pass static policy checks.
|
|
105
|
+
3. **SHA-256 Audit Trail**: Commits all proposals, policy outcomes, and approval decisions to an append-only, hash-chained log.
|
|
106
|
+
|
|
107
|
+
Requires Python 3.8+ with zero third-party dependencies.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Quickstart
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
git clone [https://github.com/ak-skwaa-mahawk/admission-gate.git](https://github.com/ak-skwaa-mahawk/admission-gate.git)
|
|
115
|
+
cd admission-gate
|
|
116
|
+
pip install .
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Library Usage
|
|
122
|
+
|
|
123
|
+
Wrap your agent's shell execution tool so rejected actions never hit the subshell:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from admission_gate import ActionProposal, gated_shell
|
|
127
|
+
|
|
128
|
+
proposal = ActionProposal(
|
|
129
|
+
action_id="task_101",
|
|
130
|
+
command="rm scratch.tmp",
|
|
131
|
+
target_path="./workspace/scratch.tmp",
|
|
132
|
+
risk_tier=2,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Returns (executed: bool, output: str, exit_code: int)
|
|
136
|
+
executed, output, code = gated_shell(
|
|
137
|
+
proposal,
|
|
138
|
+
allowed_roots=["./workspace"],
|
|
139
|
+
log_path="audit_log.jsonl",
|
|
140
|
+
require_confirm=True,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if not executed:
|
|
144
|
+
print(f"Action refused: {output}")
|
|
145
|
+
else:
|
|
146
|
+
print(f"Command succeeded:\n{output}")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Standalone Policy Checks
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from admission_gate import ActionProposal, evaluate
|
|
153
|
+
|
|
154
|
+
p = ActionProposal("chk_1", "cat /etc/shadow", "/etc/shadow", 1)
|
|
155
|
+
passed, reason = evaluate(p)
|
|
156
|
+
# passed -> False
|
|
157
|
+
# reason -> "Blocked: path resolves to protected directory '/etc'"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Verifying Audit Log Integrity
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
python3 src/admission_gate/verify.py audit_log.jsonl
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Running Tests
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
PYTHONPATH=src python3 -m unittest discover -s tests
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "admission-gate"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Deterministic admission gate with human review and cryptographic audit logging for CLI agents."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "ak-skwaa-mahawk" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
admission-gate = "admission_gate.gate:main"
|
|
21
|
+
verify-audit = "admission_gate.verify:main"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Deterministic admission gate for CLI agent actions."""
|
|
2
|
+
|
|
3
|
+
from .gate import ActionProposal, PolicyEngine, AuditLogger, evaluate, gated_shell
|
|
4
|
+
from .verify import verify_log
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"ActionProposal",
|
|
8
|
+
"PolicyEngine",
|
|
9
|
+
"AuditLogger",
|
|
10
|
+
"evaluate",
|
|
11
|
+
"gated_shell",
|
|
12
|
+
"verify_log",
|
|
13
|
+
]
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
admission_gate.gate - Library interface and execution wrapper for CLI agent actions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import asdict, dataclass
|
|
14
|
+
from typing import List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ActionProposal:
|
|
19
|
+
action_id: str
|
|
20
|
+
command: str
|
|
21
|
+
target_path: str
|
|
22
|
+
risk_tier: int = 1 # 1 = Low, 2 = Medium, 3 = High
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PolicyEngine:
|
|
26
|
+
BLOCKED_PATTERNS = ["rm -rf /", ":(){ :|:& };:", "/dev/sd", "> /dev/null"]
|
|
27
|
+
|
|
28
|
+
PROTECTED_PATHS = [
|
|
29
|
+
"/etc",
|
|
30
|
+
"/boot",
|
|
31
|
+
"/sys",
|
|
32
|
+
"/dev",
|
|
33
|
+
"/proc",
|
|
34
|
+
"C:\\Windows",
|
|
35
|
+
"C:\\Windows\\System32",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def _canonicalize(cls, path: str) -> str:
|
|
40
|
+
expanded = os.path.expanduser(path)
|
|
41
|
+
if not os.path.exists(expanded):
|
|
42
|
+
parent = os.path.dirname(expanded) or "."
|
|
43
|
+
real_parent = os.path.realpath(parent)
|
|
44
|
+
norm = os.path.join(real_parent, os.path.basename(expanded))
|
|
45
|
+
else:
|
|
46
|
+
norm = os.path.realpath(expanded)
|
|
47
|
+
|
|
48
|
+
norm = os.path.normcase(norm)
|
|
49
|
+
if norm.startswith("\\\\?\\"):
|
|
50
|
+
norm = norm[4:]
|
|
51
|
+
return os.path.abspath(norm)
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def _is_within(cls, child: str, parent: str) -> bool:
|
|
55
|
+
parent_dir = parent if parent.endswith(os.sep) else parent + os.sep
|
|
56
|
+
return child == parent or child.startswith(parent_dir)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def evaluate(
|
|
60
|
+
cls,
|
|
61
|
+
proposal: ActionProposal,
|
|
62
|
+
allowed_roots: Optional[List[str]] = None,
|
|
63
|
+
) -> Tuple[bool, str]:
|
|
64
|
+
for pattern in cls.BLOCKED_PATTERNS:
|
|
65
|
+
if pattern in proposal.command:
|
|
66
|
+
return False, f"Blocked: matched hazardous pattern '{pattern}'"
|
|
67
|
+
|
|
68
|
+
target_norm = cls._canonicalize(proposal.target_path)
|
|
69
|
+
|
|
70
|
+
for raw_protected in cls.PROTECTED_PATHS:
|
|
71
|
+
protected_norm = cls._canonicalize(raw_protected)
|
|
72
|
+
if cls._is_within(target_norm, protected_norm):
|
|
73
|
+
return (
|
|
74
|
+
False,
|
|
75
|
+
f"Blocked: path resolves to protected directory '{raw_protected}'",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if allowed_roots:
|
|
79
|
+
in_allowed = False
|
|
80
|
+
for raw_allowed in allowed_roots:
|
|
81
|
+
allowed_norm = cls._canonicalize(raw_allowed)
|
|
82
|
+
if cls._is_within(target_norm, allowed_norm):
|
|
83
|
+
in_allowed = True
|
|
84
|
+
break
|
|
85
|
+
if not in_allowed:
|
|
86
|
+
allowed_str = ", ".join(allowed_roots)
|
|
87
|
+
return False, f"Blocked: path escapes allowed roots ({allowed_str})"
|
|
88
|
+
|
|
89
|
+
if proposal.risk_tier not in (1, 2, 3):
|
|
90
|
+
return False, "Blocked: invalid risk tier (must be 1, 2, or 3)"
|
|
91
|
+
|
|
92
|
+
return True, "Passed automated policy checks."
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AuditLogger:
|
|
96
|
+
def __init__(self, log_path: str = "audit_log.jsonl"):
|
|
97
|
+
self.log_path = log_path
|
|
98
|
+
self.last_hash = self._recover_tip_hash()
|
|
99
|
+
|
|
100
|
+
def _recover_tip_hash(self) -> str:
|
|
101
|
+
if not os.path.exists(self.log_path):
|
|
102
|
+
return "0" * 64
|
|
103
|
+
last_line = ""
|
|
104
|
+
with open(self.log_path, "r", encoding="utf-8") as f:
|
|
105
|
+
for line in f:
|
|
106
|
+
if line.strip():
|
|
107
|
+
last_line = line
|
|
108
|
+
if not last_line:
|
|
109
|
+
return "0" * 64
|
|
110
|
+
try:
|
|
111
|
+
return json.loads(last_line).get("entry_hash", "0" * 64)
|
|
112
|
+
except json.JSONDecodeError:
|
|
113
|
+
return "0" * 64
|
|
114
|
+
|
|
115
|
+
def commit(
|
|
116
|
+
self,
|
|
117
|
+
proposal: ActionProposal,
|
|
118
|
+
passed: bool,
|
|
119
|
+
reason: str,
|
|
120
|
+
human_decision: Optional[bool],
|
|
121
|
+
) -> str:
|
|
122
|
+
payload = {
|
|
123
|
+
"prev_hash": self.last_hash,
|
|
124
|
+
"timestamp_ns": time.time_ns(),
|
|
125
|
+
"proposal": asdict(proposal),
|
|
126
|
+
"policy_passed": passed,
|
|
127
|
+
"policy_reason": reason,
|
|
128
|
+
"human_accepted": human_decision,
|
|
129
|
+
}
|
|
130
|
+
serialized = json.dumps(payload, sort_keys=True)
|
|
131
|
+
entry_hash = hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
|
132
|
+
record = {**payload, "entry_hash": entry_hash}
|
|
133
|
+
|
|
134
|
+
with open(self.log_path, "a", encoding="utf-8") as f:
|
|
135
|
+
f.write(json.dumps(record) + "\n")
|
|
136
|
+
|
|
137
|
+
self.last_hash = entry_hash
|
|
138
|
+
return entry_hash
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _prompt_tty(message: str) -> bool:
|
|
142
|
+
try:
|
|
143
|
+
tty_path = "CON:" if os.name == "nt" else "/dev/tty"
|
|
144
|
+
with open(tty_path, "r") as t_in, open(tty_path, "w") as t_out:
|
|
145
|
+
t_out.write(message)
|
|
146
|
+
t_out.flush()
|
|
147
|
+
res = t_in.readline().strip().lower()
|
|
148
|
+
return res in ("y", "yes")
|
|
149
|
+
except (OSError, IOError):
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def evaluate(
|
|
154
|
+
proposal: ActionProposal, allowed_roots: Optional[List[str]] = None
|
|
155
|
+
) -> Tuple[bool, str]:
|
|
156
|
+
return PolicyEngine.evaluate(proposal, allowed_roots=allowed_roots)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def gated_shell(
|
|
160
|
+
proposal: ActionProposal,
|
|
161
|
+
allowed_roots: Optional[List[str]] = None,
|
|
162
|
+
log_path: str = "audit_log.jsonl",
|
|
163
|
+
require_confirm: bool = True,
|
|
164
|
+
) -> Tuple[bool, str, int]:
|
|
165
|
+
logger = AuditLogger(log_path=log_path)
|
|
166
|
+
passed, reason = evaluate(proposal, allowed_roots=allowed_roots)
|
|
167
|
+
|
|
168
|
+
decision = None
|
|
169
|
+
if passed:
|
|
170
|
+
if require_confirm:
|
|
171
|
+
msg = f"[Agent Gate] Authorize command '{proposal.command}' on target '{proposal.target_path}'? [y/N]: "
|
|
172
|
+
decision = _prompt_tty(msg)
|
|
173
|
+
else:
|
|
174
|
+
decision = True
|
|
175
|
+
else:
|
|
176
|
+
decision = False
|
|
177
|
+
|
|
178
|
+
logger.commit(proposal, passed, reason, decision)
|
|
179
|
+
|
|
180
|
+
if not (passed and decision):
|
|
181
|
+
err_msg = reason if not passed else "Execution rejected by operator."
|
|
182
|
+
return False, err_msg, -1
|
|
183
|
+
|
|
184
|
+
res = subprocess.run(
|
|
185
|
+
proposal.command,
|
|
186
|
+
shell=True,
|
|
187
|
+
stdout=subprocess.PIPE,
|
|
188
|
+
stderr=subprocess.PIPE,
|
|
189
|
+
text=True,
|
|
190
|
+
)
|
|
191
|
+
output = res.stdout if res.returncode == 0 else res.stderr
|
|
192
|
+
return True, output, res.returncode
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def main():
|
|
196
|
+
parser = argparse.ArgumentParser(
|
|
197
|
+
description="Deterministic admission gate for CLI agent actions."
|
|
198
|
+
)
|
|
199
|
+
parser.add_argument(
|
|
200
|
+
"--allow-root",
|
|
201
|
+
action="append",
|
|
202
|
+
dest="allowed_roots",
|
|
203
|
+
help="Allowed filesystem boundary (can be specified multiple times)",
|
|
204
|
+
)
|
|
205
|
+
parser.add_argument(
|
|
206
|
+
"--log-file",
|
|
207
|
+
default="audit_log.jsonl",
|
|
208
|
+
help="Path to write the audit trail (default: audit_log.jsonl)",
|
|
209
|
+
)
|
|
210
|
+
parser.add_argument(
|
|
211
|
+
"--no-confirm",
|
|
212
|
+
action="store_true",
|
|
213
|
+
help="Bypass interactive TTY confirmation (policy checks still enforced)",
|
|
214
|
+
)
|
|
215
|
+
args = parser.parse_args()
|
|
216
|
+
|
|
217
|
+
logger = AuditLogger(log_path=args.log_file)
|
|
218
|
+
print(f"[Agent Gate] Online. Log: {args.log_file} (Tip: {logger.last_hash[:16]}...)")
|
|
219
|
+
|
|
220
|
+
if not sys.stdin.isatty():
|
|
221
|
+
for line in sys.stdin:
|
|
222
|
+
raw = line.strip()
|
|
223
|
+
if not raw:
|
|
224
|
+
continue
|
|
225
|
+
try:
|
|
226
|
+
data = json.loads(raw)
|
|
227
|
+
proposal = ActionProposal(
|
|
228
|
+
action_id=str(data["action_id"]),
|
|
229
|
+
command=str(data["command"]),
|
|
230
|
+
target_path=str(data["target_path"]),
|
|
231
|
+
risk_tier=int(data.get("risk_tier", 1)),
|
|
232
|
+
)
|
|
233
|
+
executed, out, code = gated_shell(
|
|
234
|
+
proposal,
|
|
235
|
+
allowed_roots=args.allowed_roots,
|
|
236
|
+
log_path=args.log_file,
|
|
237
|
+
require_confirm=not args.no_confirm,
|
|
238
|
+
)
|
|
239
|
+
status = f"Code {code}" if executed else "Blocked"
|
|
240
|
+
print(f"Result [{proposal.action_id}]: {status} - {out.strip()}")
|
|
241
|
+
except Exception as e:
|
|
242
|
+
print(f"Error parsing line: {e}", file=sys.stderr)
|
|
243
|
+
else:
|
|
244
|
+
print("Reading stdin for JSON proposals. Example:")
|
|
245
|
+
print('{"action_id": "1", "command": "echo test", "target_path": "./workspace", "risk_tier": 1}')
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
if __name__ == "__main__":
|
|
249
|
+
main()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Audit log verification CLI."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def verify_log(log_path: str) -> Tuple[bool, int, str]:
|
|
12
|
+
expected_prev = "0" * 64
|
|
13
|
+
count = 0
|
|
14
|
+
|
|
15
|
+
with open(log_path, "r", encoding="utf-8") as f:
|
|
16
|
+
for idx, line in enumerate(f, start=1):
|
|
17
|
+
raw = line.strip()
|
|
18
|
+
if not raw:
|
|
19
|
+
continue
|
|
20
|
+
|
|
21
|
+
entry = json.loads(raw)
|
|
22
|
+
recorded_hash = entry.get("entry_hash")
|
|
23
|
+
|
|
24
|
+
if entry.get("prev_hash") != expected_prev:
|
|
25
|
+
return (
|
|
26
|
+
False,
|
|
27
|
+
idx,
|
|
28
|
+
f"Chain break at line {idx}: expected prev_hash {expected_prev[:16]}, got {entry.get('prev_hash', '')[:16]}",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
payload = {k: v for k, v in entry.items() if k != "entry_hash"}
|
|
32
|
+
serialized = json.dumps(payload, sort_keys=True)
|
|
33
|
+
recalculated = hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
|
34
|
+
|
|
35
|
+
if recorded_hash != recalculated:
|
|
36
|
+
return (
|
|
37
|
+
False,
|
|
38
|
+
idx,
|
|
39
|
+
f"Tamper detected at line {idx}: hash mismatch",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
expected_prev = recorded_hash
|
|
43
|
+
count += 1
|
|
44
|
+
|
|
45
|
+
return True, count, expected_prev
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main():
|
|
49
|
+
parser = argparse.ArgumentParser(
|
|
50
|
+
description="Verify SHA-256 hash-chain integrity of an admission-gate audit log."
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"log_path",
|
|
54
|
+
nargs="?",
|
|
55
|
+
default="audit_log.jsonl",
|
|
56
|
+
help="Path to the audit log JSONL file (default: audit_log.jsonl)",
|
|
57
|
+
)
|
|
58
|
+
args = parser.parse_args()
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
ok, count, detail = verify_log(args.log_path)
|
|
62
|
+
if ok:
|
|
63
|
+
print(f"[PASS] Audit log intact. Verified {count} records. Tip: {detail[:16]}...")
|
|
64
|
+
sys.exit(0)
|
|
65
|
+
else:
|
|
66
|
+
print(f"[FAIL] {detail}", file=sys.stderr)
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
except FileNotFoundError:
|
|
69
|
+
print(f"[ERROR] Log file not found: {args.log_path}", file=sys.stderr)
|
|
70
|
+
sys.exit(2)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: admission-gate
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deterministic admission gate with human review and cryptographic audit logging for CLI agents.
|
|
5
|
+
Author: ak-skwaa-mahawk
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# admission-gate
|
|
16
|
+
|
|
17
|
+
[](https://github.com/ak-skwaa-mahawk/admission-gate/actions/workflows/ci.yml)
|
|
18
|
+
[](https://opensource.org/licenses/MIT)
|
|
19
|
+
[](https://github.com/ak-skwaa-mahawk/admission-gate/releases)
|
|
20
|
+
|
|
21
|
+
A lightweight admission gatekeeper and cryptographic audit logger for autonomous CLI agents.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
name: Path bypass / policy edge case
|
|
25
|
+
about: Report a path canonicalization escape or policy engine loophole
|
|
26
|
+
title: "[BYPASS]: "
|
|
27
|
+
labels: bug, security
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
### Environment
|
|
31
|
+
- OS: [e.g. Windows 11, macOS Sonoma, Ubuntu 24.04, Android Termux]
|
|
32
|
+
- Python Version: [e.g. 3.11]
|
|
33
|
+
|
|
34
|
+
### Proposed Action
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"action_id": "test_bypass",
|
|
38
|
+
"command": "...",
|
|
39
|
+
"target_path": "...",
|
|
40
|
+
"risk_tier": 1
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### 3. Add an Integration Example Script
|
|
47
|
+
|
|
48
|
+
Include a short example script under `examples/agent_tool_example.py` demonstrating how an LLM tool loop hooks into `gated_shell()`:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
"""
|
|
52
|
+
Example: Hooking admission-gate into an agent tool execution loop.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from admission_gate import ActionProposal, gated_shell
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def agent_shell_tool(command: str, target: str) -> str:
|
|
59
|
+
proposal = ActionProposal(
|
|
60
|
+
action_id="agent_call_001",
|
|
61
|
+
command=command,
|
|
62
|
+
target_path=target,
|
|
63
|
+
risk_tier=2,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
executed, output, code = gated_shell(
|
|
67
|
+
proposal,
|
|
68
|
+
allowed_roots=["./workspace"],
|
|
69
|
+
log_path="audit_log.jsonl",
|
|
70
|
+
require_confirm=True,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if not executed:
|
|
74
|
+
return f"Refused by gate: {output}"
|
|
75
|
+
|
|
76
|
+
return f"Success (exit code {code}):\n{output}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
result = agent_shell_tool("ls -la", "./workspace")
|
|
81
|
+
print(result)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# 1. Create the issue template directory and file
|
|
87
|
+
mkdir -p .github/ISSUE_TEMPLATE
|
|
88
|
+
cat << 'EOF' > .github/ISSUE_TEMPLATE/path_bypass.md
|
|
89
|
+
---
|
|
90
|
+
name: Path bypass / policy edge case
|
|
91
|
+
about: Report a path canonicalization escape or policy engine loophole
|
|
92
|
+
title: "[BYPASS]: "
|
|
93
|
+
labels: bug, security
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### Environment
|
|
97
|
+
- OS: [e.g. Windows 11, macOS Sonoma, Ubuntu 24.04, Android Termux]
|
|
98
|
+
- Python Version: [e.g. 3.11]
|
|
99
|
+
|
|
100
|
+
### Proposed Action
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"action_id": "test_bypass",
|
|
104
|
+
"command": "...",
|
|
105
|
+
"target_path": "...",
|
|
106
|
+
"risk_tier": 1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# admission-gate
|
|
112
|
+
|
|
113
|
+
A lightweight admission gatekeeper and cryptographic audit logger for autonomous CLI agents.
|
|
114
|
+
|
|
115
|
+
When local scripts or LLMs execute commands in a subshell, developers are forced between two extremes: fully autonomous execution that risks destructive operations, or prompt fatigue from micro-approving dozens of benign commands. `admission-gate` sits between the agent and your shell:
|
|
116
|
+
|
|
117
|
+
1. **Deterministic Filter**: Blocks dangerous patterns (`rm -rf /`) and restricts filesystem access to configured `allowed_roots` using strict path canonicalization.
|
|
118
|
+
2. **Interactive TTY Confirmation**: Halts for human authorization (`[y/N]`) only on proposals that pass static policy checks.
|
|
119
|
+
3. **SHA-256 Audit Trail**: Commits all proposals, policy outcomes, and approval decisions to an append-only, hash-chained log.
|
|
120
|
+
|
|
121
|
+
Requires Python 3.8+ with zero third-party dependencies.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Quickstart
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
git clone [https://github.com/ak-skwaa-mahawk/admission-gate.git](https://github.com/ak-skwaa-mahawk/admission-gate.git)
|
|
129
|
+
cd admission-gate
|
|
130
|
+
pip install .
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Library Usage
|
|
136
|
+
|
|
137
|
+
Wrap your agent's shell execution tool so rejected actions never hit the subshell:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from admission_gate import ActionProposal, gated_shell
|
|
141
|
+
|
|
142
|
+
proposal = ActionProposal(
|
|
143
|
+
action_id="task_101",
|
|
144
|
+
command="rm scratch.tmp",
|
|
145
|
+
target_path="./workspace/scratch.tmp",
|
|
146
|
+
risk_tier=2,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Returns (executed: bool, output: str, exit_code: int)
|
|
150
|
+
executed, output, code = gated_shell(
|
|
151
|
+
proposal,
|
|
152
|
+
allowed_roots=["./workspace"],
|
|
153
|
+
log_path="audit_log.jsonl",
|
|
154
|
+
require_confirm=True,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if not executed:
|
|
158
|
+
print(f"Action refused: {output}")
|
|
159
|
+
else:
|
|
160
|
+
print(f"Command succeeded:\n{output}")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Standalone Policy Checks
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from admission_gate import ActionProposal, evaluate
|
|
167
|
+
|
|
168
|
+
p = ActionProposal("chk_1", "cat /etc/shadow", "/etc/shadow", 1)
|
|
169
|
+
passed, reason = evaluate(p)
|
|
170
|
+
# passed -> False
|
|
171
|
+
# reason -> "Blocked: path resolves to protected directory '/etc'"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Verifying Audit Log Integrity
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
python3 src/admission_gate/verify.py audit_log.jsonl
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Running Tests
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
PYTHONPATH=src python3 -m unittest discover -s tests
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/admission_gate/__init__.py
|
|
5
|
+
src/admission_gate/gate.py
|
|
6
|
+
src/admission_gate/verify.py
|
|
7
|
+
src/admission_gate.egg-info/PKG-INFO
|
|
8
|
+
src/admission_gate.egg-info/SOURCES.txt
|
|
9
|
+
src/admission_gate.egg-info/dependency_links.txt
|
|
10
|
+
src/admission_gate.egg-info/entry_points.txt
|
|
11
|
+
src/admission_gate.egg-info/top_level.txt
|
|
12
|
+
tests/test_gate.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
admission_gate
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
import unittest
|
|
4
|
+
from admission_gate.gate import ActionProposal, PolicyEngine, AuditLogger, evaluate
|
|
5
|
+
from admission_gate.verify import verify_log
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestPolicyEngine(unittest.TestCase):
|
|
9
|
+
def test_blocks_dangerous_commands(self):
|
|
10
|
+
p = ActionProposal("1", "rm -rf /", "./workspace", 1)
|
|
11
|
+
passed, _ = evaluate(p)
|
|
12
|
+
self.assertFalse(passed)
|
|
13
|
+
|
|
14
|
+
def test_blocks_protected_directories(self):
|
|
15
|
+
p = ActionProposal("2", "touch /etc/config", "/etc/config", 1)
|
|
16
|
+
passed, _ = evaluate(p)
|
|
17
|
+
self.assertFalse(passed)
|
|
18
|
+
|
|
19
|
+
def test_enforces_allowed_roots(self):
|
|
20
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
21
|
+
allowed = os.path.join(tmp, "allowed")
|
|
22
|
+
os.makedirs(allowed, exist_ok=True)
|
|
23
|
+
|
|
24
|
+
good_p = ActionProposal("3", "touch test", os.path.join(allowed, "file.txt"), 1)
|
|
25
|
+
passed, _ = evaluate(good_p, allowed_roots=[allowed])
|
|
26
|
+
self.assertTrue(passed)
|
|
27
|
+
|
|
28
|
+
bad_p = ActionProposal("4", "touch test", os.path.join(tmp, "outside.txt"), 1)
|
|
29
|
+
passed, _ = evaluate(bad_p, allowed_roots=[allowed])
|
|
30
|
+
self.assertFalse(passed)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestAuditChain(unittest.TestCase):
|
|
34
|
+
def test_log_and_verification(self):
|
|
35
|
+
with tempfile.NamedTemporaryFile(suffix=".jsonl", delete=False) as f:
|
|
36
|
+
log_path = f.name
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
logger = AuditLogger(log_path=log_path)
|
|
40
|
+
p1 = ActionProposal("1", "echo hi", "./workspace", 1)
|
|
41
|
+
p2 = ActionProposal("2", "ls", "./workspace", 1)
|
|
42
|
+
|
|
43
|
+
logger.commit(p1, True, "passed", True)
|
|
44
|
+
logger.commit(p2, True, "passed", False)
|
|
45
|
+
|
|
46
|
+
ok, count, _ = verify_log(log_path)
|
|
47
|
+
self.assertTrue(ok)
|
|
48
|
+
self.assertEqual(count, 2)
|
|
49
|
+
finally:
|
|
50
|
+
if os.path.exists(log_path):
|
|
51
|
+
os.remove(log_path)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
unittest.main()
|