agentsec-firewall 0.1.1__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.
- agentsec_firewall-0.1.1/PKG-INFO +171 -0
- agentsec_firewall-0.1.1/README.md +156 -0
- agentsec_firewall-0.1.1/agentfirewall/__init__.py +5 -0
- agentsec_firewall-0.1.1/agentfirewall/__main__.py +5 -0
- agentsec_firewall-0.1.1/agentfirewall/cli.py +236 -0
- agentsec_firewall-0.1.1/agentfirewall/hooks.py +106 -0
- agentsec_firewall-0.1.1/agentfirewall/interceptor.py +113 -0
- agentsec_firewall-0.1.1/agentsec_firewall.egg-info/PKG-INFO +171 -0
- agentsec_firewall-0.1.1/agentsec_firewall.egg-info/SOURCES.txt +16 -0
- agentsec_firewall-0.1.1/agentsec_firewall.egg-info/dependency_links.txt +1 -0
- agentsec_firewall-0.1.1/agentsec_firewall.egg-info/entry_points.txt +2 -0
- agentsec_firewall-0.1.1/agentsec_firewall.egg-info/requires.txt +8 -0
- agentsec_firewall-0.1.1/agentsec_firewall.egg-info/top_level.txt +1 -0
- agentsec_firewall-0.1.1/pyproject.toml +29 -0
- agentsec_firewall-0.1.1/setup.cfg +4 -0
- agentsec_firewall-0.1.1/tests/test_cli.py +151 -0
- agentsec_firewall-0.1.1/tests/test_hooks.py +156 -0
- agentsec_firewall-0.1.1/tests/test_interceptor.py +157 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentsec-firewall
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Policy-enforced firewall for AI agent tool calls
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: agentsec-core>=0.1.0
|
|
9
|
+
Requires-Dist: click>=8.0
|
|
10
|
+
Requires-Dist: pyyaml>=6.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
14
|
+
Requires-Dist: ruff>=0.3.0; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# agentfirewall
|
|
17
|
+
|
|
18
|
+
Policy-enforced firewall for AI agent tool calls. Intercepts, evaluates, and audits every tool invocation against a YAML security policy.
|
|
19
|
+
|
|
20
|
+
Part of the [AgentSec](https://github.com/agentsec) suite -- open-source security primitives for AI agents.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install agentsec-firewall
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
> **Premium:** Get enterprise security policies, advanced secret detection, and webhook alerting at [zazmatt.gumroad.com/l/kjwwhn](https://zazmatt.gumroad.com/l/kjwwhn)
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. Initialize policy and audit log
|
|
34
|
+
agentfirewall init
|
|
35
|
+
|
|
36
|
+
# 2. Install hooks into Claude Code
|
|
37
|
+
agentfirewall install
|
|
38
|
+
|
|
39
|
+
# 3. Done -- every tool call is now checked against .agentfirewall/policy.yaml
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## How It Works
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
Tool Call (e.g. Bash "rm -rf /")
|
|
46
|
+
|
|
|
47
|
+
v
|
|
48
|
+
+-----------+
|
|
49
|
+
| PreToolUse | <-- Claude Code hook reads stdin JSON
|
|
50
|
+
| Hook |
|
|
51
|
+
+-----+-----+
|
|
52
|
+
|
|
|
53
|
+
v
|
|
54
|
+
+-----------+
|
|
55
|
+
| Interceptor| <-- Evaluates against policy.yaml
|
|
56
|
+
| .check() | Scans params for secrets
|
|
57
|
+
+-----+-----+
|
|
58
|
+
|
|
|
59
|
+
+----+----+
|
|
60
|
+
| |
|
|
61
|
+
ALLOW DENY ---------> exit 2 (blocks tool call)
|
|
62
|
+
| + JSON reason to stdout
|
|
63
|
+
v
|
|
64
|
+
Tool Executes
|
|
65
|
+
|
|
|
66
|
+
v
|
|
67
|
+
+-----------+
|
|
68
|
+
| PostToolUse| <-- Logs execution to audit.jsonl
|
|
69
|
+
| Hook |
|
|
70
|
+
+-----------+
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Policy Reference
|
|
74
|
+
|
|
75
|
+
Policies are YAML files at `.agentfirewall/policy.yaml`:
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
version: "1.0"
|
|
79
|
+
name: "my-policy"
|
|
80
|
+
description: "Custom security policy"
|
|
81
|
+
default_action: log # allow | deny | log | alert
|
|
82
|
+
|
|
83
|
+
rules:
|
|
84
|
+
- name: allow-read-operations
|
|
85
|
+
tools: ["Read", "Glob", "Grep"] # fnmatch patterns
|
|
86
|
+
action: allow
|
|
87
|
+
reason: "Read operations are safe"
|
|
88
|
+
|
|
89
|
+
- name: block-dangerous-bash
|
|
90
|
+
tools: ["Bash"]
|
|
91
|
+
resources: ["rm -rf *", "sudo *"] # resource patterns
|
|
92
|
+
action: deny
|
|
93
|
+
reason: "Dangerous shell commands blocked"
|
|
94
|
+
|
|
95
|
+
- name: alert-mcp-tools
|
|
96
|
+
tools: ["mcp__*"] # wildcards supported
|
|
97
|
+
action: alert
|
|
98
|
+
reason: "MCP tool calls flagged for review"
|
|
99
|
+
|
|
100
|
+
- name: log-all-writes
|
|
101
|
+
tools: ["Write", "Edit"]
|
|
102
|
+
action: log
|
|
103
|
+
reason: "File modifications logged"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Actions:**
|
|
107
|
+
- `allow` -- permit the tool call
|
|
108
|
+
- `deny` -- block the tool call (exit code 2)
|
|
109
|
+
- `log` -- permit but log to audit trail
|
|
110
|
+
- `alert` -- permit, log, and fire webhook alert
|
|
111
|
+
|
|
112
|
+
## CLI Reference
|
|
113
|
+
|
|
114
|
+
| Command | Description |
|
|
115
|
+
|---------|-------------|
|
|
116
|
+
| `agentfirewall init` | Create `.agentfirewall/` with default policy and audit log |
|
|
117
|
+
| `agentfirewall install` | Add PreToolUse/PostToolUse hooks to `.claude/settings.local.json` |
|
|
118
|
+
| `agentfirewall uninstall` | Remove agentfirewall hooks from settings |
|
|
119
|
+
| `agentfirewall validate` | Validate policy YAML and print rule summary |
|
|
120
|
+
| `agentfirewall audit` | Query audit log (supports `--tail`, `--tool`, `--action` filters) |
|
|
121
|
+
| `agentfirewall scan [PATH]` | Run security scanner on project files |
|
|
122
|
+
|
|
123
|
+
## Python API
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from agentfirewall import Interceptor, PolicyViolationError
|
|
127
|
+
from agentsec_core.schemas import PolicyAction
|
|
128
|
+
|
|
129
|
+
# From a policy file
|
|
130
|
+
interceptor = Interceptor(policy_path=".agentfirewall/policy.yaml")
|
|
131
|
+
|
|
132
|
+
# Check a tool call (returns PolicyDecision, never raises)
|
|
133
|
+
decision = interceptor.check("Bash", {"command": "rm -rf /"})
|
|
134
|
+
if decision.action == PolicyAction.DENY:
|
|
135
|
+
print(f"Blocked: {decision.reason}")
|
|
136
|
+
|
|
137
|
+
# Check and raise on deny
|
|
138
|
+
try:
|
|
139
|
+
interceptor.check_or_raise("Bash", {"command": "rm -rf /"})
|
|
140
|
+
except PolicyViolationError as e:
|
|
141
|
+
print(f"Violation: {e.decision.reason}")
|
|
142
|
+
|
|
143
|
+
# Decorator pattern
|
|
144
|
+
@interceptor.wrap("data_export")
|
|
145
|
+
def export_data(**kwargs):
|
|
146
|
+
... # Only runs if policy allows
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Free vs Premium
|
|
150
|
+
|
|
151
|
+
| Feature | Free (OSS) | Premium |
|
|
152
|
+
|---------|:----------:|:-------:|
|
|
153
|
+
| YAML policy enforcement | Yes | Yes |
|
|
154
|
+
| PreToolUse / PostToolUse hooks | Yes | Yes |
|
|
155
|
+
| Audit log (JSONL) | Yes | Yes |
|
|
156
|
+
| Secret scanning (23 patterns) | Yes | Yes |
|
|
157
|
+
| Enterprise policy templates | -- | Yes |
|
|
158
|
+
| Webhook alerts (Slack, Discord) | -- | Yes |
|
|
159
|
+
| PII/GDPR filtering rules | -- | Yes |
|
|
160
|
+
| Priority support | -- | Yes |
|
|
161
|
+
|
|
162
|
+
[Get Premium -- $10](https://zazmatt.gumroad.com/l/kjwwhn)
|
|
163
|
+
|
|
164
|
+
## Requirements
|
|
165
|
+
|
|
166
|
+
- Python 3.11+
|
|
167
|
+
- [agentsec-core](../agentsec-core/) >= 0.1.0
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# agentfirewall
|
|
2
|
+
|
|
3
|
+
Policy-enforced firewall for AI agent tool calls. Intercepts, evaluates, and audits every tool invocation against a YAML security policy.
|
|
4
|
+
|
|
5
|
+
Part of the [AgentSec](https://github.com/agentsec) suite -- open-source security primitives for AI agents.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install agentsec-firewall
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
> **Premium:** Get enterprise security policies, advanced secret detection, and webhook alerting at [zazmatt.gumroad.com/l/kjwwhn](https://zazmatt.gumroad.com/l/kjwwhn)
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# 1. Initialize policy and audit log
|
|
19
|
+
agentfirewall init
|
|
20
|
+
|
|
21
|
+
# 2. Install hooks into Claude Code
|
|
22
|
+
agentfirewall install
|
|
23
|
+
|
|
24
|
+
# 3. Done -- every tool call is now checked against .agentfirewall/policy.yaml
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## How It Works
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
Tool Call (e.g. Bash "rm -rf /")
|
|
31
|
+
|
|
|
32
|
+
v
|
|
33
|
+
+-----------+
|
|
34
|
+
| PreToolUse | <-- Claude Code hook reads stdin JSON
|
|
35
|
+
| Hook |
|
|
36
|
+
+-----+-----+
|
|
37
|
+
|
|
|
38
|
+
v
|
|
39
|
+
+-----------+
|
|
40
|
+
| Interceptor| <-- Evaluates against policy.yaml
|
|
41
|
+
| .check() | Scans params for secrets
|
|
42
|
+
+-----+-----+
|
|
43
|
+
|
|
|
44
|
+
+----+----+
|
|
45
|
+
| |
|
|
46
|
+
ALLOW DENY ---------> exit 2 (blocks tool call)
|
|
47
|
+
| + JSON reason to stdout
|
|
48
|
+
v
|
|
49
|
+
Tool Executes
|
|
50
|
+
|
|
|
51
|
+
v
|
|
52
|
+
+-----------+
|
|
53
|
+
| PostToolUse| <-- Logs execution to audit.jsonl
|
|
54
|
+
| Hook |
|
|
55
|
+
+-----------+
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Policy Reference
|
|
59
|
+
|
|
60
|
+
Policies are YAML files at `.agentfirewall/policy.yaml`:
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
version: "1.0"
|
|
64
|
+
name: "my-policy"
|
|
65
|
+
description: "Custom security policy"
|
|
66
|
+
default_action: log # allow | deny | log | alert
|
|
67
|
+
|
|
68
|
+
rules:
|
|
69
|
+
- name: allow-read-operations
|
|
70
|
+
tools: ["Read", "Glob", "Grep"] # fnmatch patterns
|
|
71
|
+
action: allow
|
|
72
|
+
reason: "Read operations are safe"
|
|
73
|
+
|
|
74
|
+
- name: block-dangerous-bash
|
|
75
|
+
tools: ["Bash"]
|
|
76
|
+
resources: ["rm -rf *", "sudo *"] # resource patterns
|
|
77
|
+
action: deny
|
|
78
|
+
reason: "Dangerous shell commands blocked"
|
|
79
|
+
|
|
80
|
+
- name: alert-mcp-tools
|
|
81
|
+
tools: ["mcp__*"] # wildcards supported
|
|
82
|
+
action: alert
|
|
83
|
+
reason: "MCP tool calls flagged for review"
|
|
84
|
+
|
|
85
|
+
- name: log-all-writes
|
|
86
|
+
tools: ["Write", "Edit"]
|
|
87
|
+
action: log
|
|
88
|
+
reason: "File modifications logged"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Actions:**
|
|
92
|
+
- `allow` -- permit the tool call
|
|
93
|
+
- `deny` -- block the tool call (exit code 2)
|
|
94
|
+
- `log` -- permit but log to audit trail
|
|
95
|
+
- `alert` -- permit, log, and fire webhook alert
|
|
96
|
+
|
|
97
|
+
## CLI Reference
|
|
98
|
+
|
|
99
|
+
| Command | Description |
|
|
100
|
+
|---------|-------------|
|
|
101
|
+
| `agentfirewall init` | Create `.agentfirewall/` with default policy and audit log |
|
|
102
|
+
| `agentfirewall install` | Add PreToolUse/PostToolUse hooks to `.claude/settings.local.json` |
|
|
103
|
+
| `agentfirewall uninstall` | Remove agentfirewall hooks from settings |
|
|
104
|
+
| `agentfirewall validate` | Validate policy YAML and print rule summary |
|
|
105
|
+
| `agentfirewall audit` | Query audit log (supports `--tail`, `--tool`, `--action` filters) |
|
|
106
|
+
| `agentfirewall scan [PATH]` | Run security scanner on project files |
|
|
107
|
+
|
|
108
|
+
## Python API
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from agentfirewall import Interceptor, PolicyViolationError
|
|
112
|
+
from agentsec_core.schemas import PolicyAction
|
|
113
|
+
|
|
114
|
+
# From a policy file
|
|
115
|
+
interceptor = Interceptor(policy_path=".agentfirewall/policy.yaml")
|
|
116
|
+
|
|
117
|
+
# Check a tool call (returns PolicyDecision, never raises)
|
|
118
|
+
decision = interceptor.check("Bash", {"command": "rm -rf /"})
|
|
119
|
+
if decision.action == PolicyAction.DENY:
|
|
120
|
+
print(f"Blocked: {decision.reason}")
|
|
121
|
+
|
|
122
|
+
# Check and raise on deny
|
|
123
|
+
try:
|
|
124
|
+
interceptor.check_or_raise("Bash", {"command": "rm -rf /"})
|
|
125
|
+
except PolicyViolationError as e:
|
|
126
|
+
print(f"Violation: {e.decision.reason}")
|
|
127
|
+
|
|
128
|
+
# Decorator pattern
|
|
129
|
+
@interceptor.wrap("data_export")
|
|
130
|
+
def export_data(**kwargs):
|
|
131
|
+
... # Only runs if policy allows
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Free vs Premium
|
|
135
|
+
|
|
136
|
+
| Feature | Free (OSS) | Premium |
|
|
137
|
+
|---------|:----------:|:-------:|
|
|
138
|
+
| YAML policy enforcement | Yes | Yes |
|
|
139
|
+
| PreToolUse / PostToolUse hooks | Yes | Yes |
|
|
140
|
+
| Audit log (JSONL) | Yes | Yes |
|
|
141
|
+
| Secret scanning (23 patterns) | Yes | Yes |
|
|
142
|
+
| Enterprise policy templates | -- | Yes |
|
|
143
|
+
| Webhook alerts (Slack, Discord) | -- | Yes |
|
|
144
|
+
| PII/GDPR filtering rules | -- | Yes |
|
|
145
|
+
| Priority support | -- | Yes |
|
|
146
|
+
|
|
147
|
+
[Get Premium -- $10](https://zazmatt.gumroad.com/l/kjwwhn)
|
|
148
|
+
|
|
149
|
+
## Requirements
|
|
150
|
+
|
|
151
|
+
- Python 3.11+
|
|
152
|
+
- [agentsec-core](../agentsec-core/) >= 0.1.0
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Click-based CLI for agentfirewall."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
DEFAULT_POLICY_DIR = ".agentfirewall"
|
|
14
|
+
DEFAULT_POLICY_PATH = f"{DEFAULT_POLICY_DIR}/policy.yaml"
|
|
15
|
+
DEFAULT_LOG_PATH = f"{DEFAULT_POLICY_DIR}/audit.jsonl"
|
|
16
|
+
|
|
17
|
+
DEFAULT_POLICY_YAML = """\
|
|
18
|
+
version: "1.0"
|
|
19
|
+
name: "default-agent-policy"
|
|
20
|
+
description: "Default security policy -- blocks dangerous operations, logs everything"
|
|
21
|
+
default_action: log
|
|
22
|
+
rules:
|
|
23
|
+
- name: allow-read-operations
|
|
24
|
+
tools: ["Read", "Glob", "Grep"]
|
|
25
|
+
action: allow
|
|
26
|
+
reason: "Read operations are safe"
|
|
27
|
+
- name: block-dangerous-bash
|
|
28
|
+
tools: ["Bash"]
|
|
29
|
+
resources: ["rm -rf *", "sudo *", "chmod 777 *", "curl * | sh", "wget * | sh"]
|
|
30
|
+
action: deny
|
|
31
|
+
reason: "Dangerous shell commands blocked"
|
|
32
|
+
- name: alert-mcp-tools
|
|
33
|
+
tools: ["mcp__*"]
|
|
34
|
+
action: alert
|
|
35
|
+
reason: "MCP tool calls flagged for review"
|
|
36
|
+
- name: log-all-writes
|
|
37
|
+
tools: ["Write", "Edit"]
|
|
38
|
+
action: log
|
|
39
|
+
reason: "File modifications logged"
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
HOOK_PRETOOL = {
|
|
43
|
+
"type": "command",
|
|
44
|
+
"event": "PreToolUse",
|
|
45
|
+
"command": "python3 -c 'from agentfirewall.hooks import pretool_hook; pretool_hook()'",
|
|
46
|
+
}
|
|
47
|
+
HOOK_POSTTOOL = {
|
|
48
|
+
"type": "command",
|
|
49
|
+
"event": "PostToolUse",
|
|
50
|
+
"command": "python3 -c 'from agentfirewall.hooks import posttool_hook; posttool_hook()'",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
SETTINGS_PATH = ".claude/settings.local.json"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@click.group()
|
|
57
|
+
@click.version_option(package_name="agentfirewall")
|
|
58
|
+
def main() -> None:
|
|
59
|
+
"""agentfirewall -- Policy-enforced firewall for AI agent tool calls."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@main.command()
|
|
63
|
+
def init() -> None:
|
|
64
|
+
"""Initialize .agentfirewall/ with default policy and audit log."""
|
|
65
|
+
policy_dir = Path(DEFAULT_POLICY_DIR)
|
|
66
|
+
policy_dir.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
|
|
68
|
+
policy_file = Path(DEFAULT_POLICY_PATH)
|
|
69
|
+
if policy_file.exists():
|
|
70
|
+
click.echo(f"Policy already exists: {policy_file}")
|
|
71
|
+
else:
|
|
72
|
+
policy_file.write_text(DEFAULT_POLICY_YAML, encoding="utf-8")
|
|
73
|
+
click.echo(f"Created default policy: {policy_file}")
|
|
74
|
+
|
|
75
|
+
log_file = Path(DEFAULT_LOG_PATH)
|
|
76
|
+
if not log_file.exists():
|
|
77
|
+
log_file.touch()
|
|
78
|
+
click.echo(f"Created audit log: {log_file}")
|
|
79
|
+
|
|
80
|
+
click.echo("Initialization complete. Run `agentfirewall install` to add hooks.")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@main.command()
|
|
84
|
+
@click.option("--policy", default=DEFAULT_POLICY_PATH, help="Path to policy YAML file.")
|
|
85
|
+
def validate(policy: str) -> None:
|
|
86
|
+
"""Validate a policy YAML file."""
|
|
87
|
+
from agentsec_core.policy import load_policy
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
loaded = load_policy(policy)
|
|
91
|
+
except FileNotFoundError:
|
|
92
|
+
click.echo(f"Error: Policy file not found: {policy}", err=True)
|
|
93
|
+
raise SystemExit(1)
|
|
94
|
+
except ValueError as exc:
|
|
95
|
+
click.echo(f"Error: Invalid policy: {exc}", err=True)
|
|
96
|
+
raise SystemExit(1)
|
|
97
|
+
|
|
98
|
+
click.echo(f"Policy: {loaded.name} (v{loaded.version})")
|
|
99
|
+
click.echo(f"Description: {loaded.description}")
|
|
100
|
+
click.echo(f"Default action: {loaded.default_action.value}")
|
|
101
|
+
click.echo(f"Rules: {len(loaded.rules)}")
|
|
102
|
+
for rule in loaded.rules:
|
|
103
|
+
tools = ", ".join(rule.tools) if rule.tools else "*"
|
|
104
|
+
click.echo(f" - {rule.name}: {rule.action.value} [{tools}]")
|
|
105
|
+
click.echo("Policy is valid.")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@main.command()
|
|
109
|
+
@click.option("--tail", "tail_n", default=20, type=int, help="Number of recent events.")
|
|
110
|
+
@click.option("--tool", "tool_name", default=None, help="Filter by tool name.")
|
|
111
|
+
@click.option("--action", "action_filter", default=None, help="Filter by action (allow/deny/log/alert).")
|
|
112
|
+
def audit(tail_n: int, tool_name: str | None, action_filter: str | None) -> None:
|
|
113
|
+
"""Query the audit log."""
|
|
114
|
+
from agentsec_core.logger import AuditLogger
|
|
115
|
+
from agentsec_core.schemas import PolicyAction
|
|
116
|
+
|
|
117
|
+
log_path = Path(DEFAULT_LOG_PATH)
|
|
118
|
+
if not log_path.exists():
|
|
119
|
+
click.echo("No audit log found. Run `agentfirewall init` first.", err=True)
|
|
120
|
+
raise SystemExit(1)
|
|
121
|
+
|
|
122
|
+
audit_logger = AuditLogger(log_path)
|
|
123
|
+
|
|
124
|
+
action_enum = None
|
|
125
|
+
if action_filter:
|
|
126
|
+
try:
|
|
127
|
+
action_enum = PolicyAction(action_filter.lower())
|
|
128
|
+
except ValueError:
|
|
129
|
+
click.echo(f"Error: Invalid action '{action_filter}'. Use: allow/deny/log/alert", err=True)
|
|
130
|
+
raise SystemExit(1)
|
|
131
|
+
|
|
132
|
+
events = audit_logger.query(tool_name=tool_name, action=action_enum)
|
|
133
|
+
recent = events[-tail_n:]
|
|
134
|
+
|
|
135
|
+
if not recent:
|
|
136
|
+
click.echo("No matching events found.")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
click.echo(f"Showing {len(recent)} of {len(events)} events:")
|
|
140
|
+
for event in recent:
|
|
141
|
+
ts = event.timestamp.strftime("%Y-%m-%d %H:%M:%S") if event.timestamp else "N/A"
|
|
142
|
+
violations = f" [{len(event.violations)} violations]" if event.violations else ""
|
|
143
|
+
click.echo(f" {ts} | {event.action_taken.value:5s} | {event.tool_name}{violations}")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@main.command()
|
|
147
|
+
@click.argument("path", default=".")
|
|
148
|
+
def scan(path: str) -> None:
|
|
149
|
+
"""Run security scanner on project files."""
|
|
150
|
+
from agentsec_core.scanner import scan_file
|
|
151
|
+
|
|
152
|
+
target = Path(path)
|
|
153
|
+
if not target.exists():
|
|
154
|
+
click.echo(f"Error: Path not found: {path}", err=True)
|
|
155
|
+
raise SystemExit(1)
|
|
156
|
+
|
|
157
|
+
files = [target] if target.is_file() else sorted(target.rglob("*"))
|
|
158
|
+
total_violations = 0
|
|
159
|
+
|
|
160
|
+
for filepath in files:
|
|
161
|
+
if not filepath.is_file():
|
|
162
|
+
continue
|
|
163
|
+
violations = scan_file(str(filepath))
|
|
164
|
+
for v in violations:
|
|
165
|
+
total_violations += 1
|
|
166
|
+
click.echo(f" [{v.severity}] {v.source}:{v.line_number} - {v.message}")
|
|
167
|
+
|
|
168
|
+
if total_violations == 0:
|
|
169
|
+
click.echo("No security issues found.")
|
|
170
|
+
else:
|
|
171
|
+
click.echo(f"\nFound {total_violations} issue(s).")
|
|
172
|
+
raise SystemExit(1)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@main.command()
|
|
176
|
+
def install() -> None:
|
|
177
|
+
"""Install agentfirewall hooks into Claude Code settings."""
|
|
178
|
+
settings_path = Path(SETTINGS_PATH)
|
|
179
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
|
|
181
|
+
if settings_path.exists():
|
|
182
|
+
settings = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
183
|
+
else:
|
|
184
|
+
settings = {}
|
|
185
|
+
|
|
186
|
+
hooks = settings.get("hooks", [])
|
|
187
|
+
|
|
188
|
+
# Check for existing agentfirewall hooks
|
|
189
|
+
existing_commands = {h.get("command", "") for h in hooks}
|
|
190
|
+
added = 0
|
|
191
|
+
|
|
192
|
+
if HOOK_PRETOOL["command"] not in existing_commands:
|
|
193
|
+
hooks.append(HOOK_PRETOOL)
|
|
194
|
+
added += 1
|
|
195
|
+
|
|
196
|
+
if HOOK_POSTTOOL["command"] not in existing_commands:
|
|
197
|
+
hooks.append(HOOK_POSTTOOL)
|
|
198
|
+
added += 1
|
|
199
|
+
|
|
200
|
+
settings["hooks"] = hooks
|
|
201
|
+
settings_path.write_text(
|
|
202
|
+
json.dumps(settings, indent=2) + "\n", encoding="utf-8"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if added > 0:
|
|
206
|
+
click.echo(f"Installed {added} hook(s) into {settings_path}")
|
|
207
|
+
else:
|
|
208
|
+
click.echo("Hooks already installed.")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@main.command()
|
|
212
|
+
def uninstall() -> None:
|
|
213
|
+
"""Remove agentfirewall hooks from Claude Code settings."""
|
|
214
|
+
settings_path = Path(SETTINGS_PATH)
|
|
215
|
+
|
|
216
|
+
if not settings_path.exists():
|
|
217
|
+
click.echo("No settings file found. Nothing to uninstall.")
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
settings = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
221
|
+
hooks = settings.get("hooks", [])
|
|
222
|
+
|
|
223
|
+
firewall_commands = {HOOK_PRETOOL["command"], HOOK_POSTTOOL["command"]}
|
|
224
|
+
original_count = len(hooks)
|
|
225
|
+
hooks = [h for h in hooks if h.get("command", "") not in firewall_commands]
|
|
226
|
+
|
|
227
|
+
removed = original_count - len(hooks)
|
|
228
|
+
settings["hooks"] = hooks
|
|
229
|
+
settings_path.write_text(
|
|
230
|
+
json.dumps(settings, indent=2) + "\n", encoding="utf-8"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if removed > 0:
|
|
234
|
+
click.echo(f"Removed {removed} hook(s) from {settings_path}")
|
|
235
|
+
else:
|
|
236
|
+
click.echo("No agentfirewall hooks found to remove.")
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Claude Code hook integration for agentfirewall.
|
|
2
|
+
|
|
3
|
+
PreToolUse hook: reads tool call from stdin, checks policy, exits 2 to block.
|
|
4
|
+
PostToolUse hook: reads tool result from stdin, logs to audit trail.
|
|
5
|
+
|
|
6
|
+
Install via: agentfirewall install
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from agentsec_core.schemas import AuditEvent, PolicyAction
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Default paths (created by `agentfirewall init`)
|
|
21
|
+
DEFAULT_POLICY_PATH = ".agentfirewall/policy.yaml"
|
|
22
|
+
DEFAULT_LOG_PATH = ".agentfirewall/audit.jsonl"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _read_hook_input() -> dict:
|
|
26
|
+
"""Read JSON hook input from stdin. Returns empty dict on failure."""
|
|
27
|
+
try:
|
|
28
|
+
raw = sys.stdin.read()
|
|
29
|
+
return json.loads(raw) if raw.strip() else {}
|
|
30
|
+
except (json.JSONDecodeError, OSError):
|
|
31
|
+
return {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _parse_json_field(data: dict, field: str) -> dict:
|
|
35
|
+
"""Parse a possibly-stringified JSON field."""
|
|
36
|
+
value = data.get(field, "{}")
|
|
37
|
+
if isinstance(value, dict):
|
|
38
|
+
return value
|
|
39
|
+
try:
|
|
40
|
+
return json.loads(value)
|
|
41
|
+
except (json.JSONDecodeError, TypeError):
|
|
42
|
+
return {}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def pretool_hook() -> None:
|
|
46
|
+
"""PreToolUse hook entry point.
|
|
47
|
+
|
|
48
|
+
Reads stdin JSON with tool_name and tool_input.
|
|
49
|
+
Checks against policy. Exits 2 to block, 0 to allow.
|
|
50
|
+
Outputs JSON reason on block.
|
|
51
|
+
"""
|
|
52
|
+
if not Path(DEFAULT_POLICY_PATH).exists():
|
|
53
|
+
sys.exit(0) # No policy = allow all
|
|
54
|
+
|
|
55
|
+
from .interceptor import Interceptor
|
|
56
|
+
|
|
57
|
+
hook_data = _read_hook_input()
|
|
58
|
+
tool_name = hook_data.get("tool_name", "")
|
|
59
|
+
tool_input = _parse_json_field(hook_data, "tool_input")
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
interceptor = Interceptor(
|
|
63
|
+
policy_path=DEFAULT_POLICY_PATH,
|
|
64
|
+
log_path=DEFAULT_LOG_PATH,
|
|
65
|
+
)
|
|
66
|
+
decision = interceptor.check(tool_name, tool_input)
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
# Fail-open: don't break Claude Code if firewall crashes
|
|
69
|
+
sys.stderr.write(f"agentfirewall: error checking policy: {exc}\n")
|
|
70
|
+
sys.exit(0)
|
|
71
|
+
|
|
72
|
+
if decision.action == PolicyAction.DENY:
|
|
73
|
+
reason = decision.reason or "Blocked by agentfirewall policy"
|
|
74
|
+
output = json.dumps({"decision": "block", "reason": reason})
|
|
75
|
+
print(output) # noqa: T201 — CLI output
|
|
76
|
+
sys.exit(2)
|
|
77
|
+
|
|
78
|
+
sys.exit(0)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def posttool_hook() -> None:
|
|
82
|
+
"""PostToolUse hook entry point.
|
|
83
|
+
|
|
84
|
+
Reads stdin JSON with tool result. Logs to audit trail.
|
|
85
|
+
Always exits 0 (never blocks post-execution).
|
|
86
|
+
"""
|
|
87
|
+
if not Path(DEFAULT_LOG_PATH).parent.exists():
|
|
88
|
+
sys.exit(0)
|
|
89
|
+
|
|
90
|
+
from agentsec_core.logger import AuditLogger
|
|
91
|
+
|
|
92
|
+
hook_data = _read_hook_input()
|
|
93
|
+
tool_name = hook_data.get("tool_name", "")
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
audit_logger = AuditLogger(DEFAULT_LOG_PATH)
|
|
97
|
+
event = AuditEvent(
|
|
98
|
+
event_type="tool_execution",
|
|
99
|
+
tool_name=tool_name,
|
|
100
|
+
action_taken=PolicyAction.LOG,
|
|
101
|
+
)
|
|
102
|
+
audit_logger.log(event)
|
|
103
|
+
except Exception as exc:
|
|
104
|
+
sys.stderr.write(f"agentfirewall: error logging: {exc}\n")
|
|
105
|
+
|
|
106
|
+
sys.exit(0)
|