@musashimiyamoto/agent-guard 0.1.0
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.
- package/README.md +93 -0
- package/package.json +46 -0
- package/src/cli.js +252 -0
- package/src/proxy/egress.js +112 -0
- package/src/proxy/index.js +132 -0
- package/src/proxy/logger.js +117 -0
- package/src/proxy/policy.js +157 -0
- package/src/rules/index.js +271 -0
- package/src/scanner.js +242 -0
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# π‘ Agent Guard
|
|
2
|
+
|
|
3
|
+
Security scanner for AI agent configurations. Detects misconfigurations, exposed secrets, and unsafe skill patterns.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Scan current directory
|
|
9
|
+
npx agent-guard scan .
|
|
10
|
+
|
|
11
|
+
# Scan specific agent
|
|
12
|
+
npx agent-guard scan ./my-agent
|
|
13
|
+
|
|
14
|
+
# JSON output for CI/CD
|
|
15
|
+
npx agent-guard scan . --json
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## What It Finds
|
|
19
|
+
|
|
20
|
+
| Category | Examples |
|
|
21
|
+
|----------|----------|
|
|
22
|
+
| **Secrets** | OpenAI keys, GitHub tokens, AWS credentials, private keys |
|
|
23
|
+
| **Network** | Public binds (0.0.0.0), CORS misconfig, exfiltration URLs |
|
|
24
|
+
| **Auth** | Missing authentication, default credentials |
|
|
25
|
+
| **Skills** | Missing manifests, shell execution, eval usage |
|
|
26
|
+
| **Injection** | Hidden unicode characters (RTL attacks) |
|
|
27
|
+
|
|
28
|
+
## Security Score
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Score: 90/100 (A)
|
|
32
|
+
ββββββββββββββββββββ
|
|
33
|
+
|
|
34
|
+
Critical: 0
|
|
35
|
+
High: 1
|
|
36
|
+
Medium: 0
|
|
37
|
+
Low: 0
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- **A (90-100)**: Production ready
|
|
41
|
+
- **B (80-89)**: Minor issues
|
|
42
|
+
- **C (70-79)**: Needs attention
|
|
43
|
+
- **D (60-69)**: Significant issues
|
|
44
|
+
- **F (<60)**: Critical vulnerabilities
|
|
45
|
+
|
|
46
|
+
## Skill Manifests
|
|
47
|
+
|
|
48
|
+
Every skill should include `skill.manifest.json`:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"metadata": {
|
|
53
|
+
"name": "my-skill",
|
|
54
|
+
"version": "1.0.0",
|
|
55
|
+
"author": "agent:id",
|
|
56
|
+
"isnad_root": "human:owner"
|
|
57
|
+
},
|
|
58
|
+
"permissions": {
|
|
59
|
+
"network": {
|
|
60
|
+
"egress": [{"domain": "api.example.com", "purpose": "core"}],
|
|
61
|
+
"block_all_other": true
|
|
62
|
+
},
|
|
63
|
+
"filesystem": {
|
|
64
|
+
"read": ["./data/"],
|
|
65
|
+
"write": ["./output/"],
|
|
66
|
+
"deny": ["~/.env", "~/.ssh"]
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"runtime_policy": {
|
|
70
|
+
"enforcement": "hard_block",
|
|
71
|
+
"log_violations": true
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Trust Tiers (IsnΔd)
|
|
77
|
+
|
|
78
|
+
| Tier | Name | Meaning |
|
|
79
|
+
|------|------|---------|
|
|
80
|
+
| π₯ | Thiqah | Audited by 3+ trusted agents, signed |
|
|
81
|
+
| π₯ | Hasan | Reputable author, signed manifest |
|
|
82
|
+
| π₯ | Da'if | Unsigned/unaudited, sandbox required |
|
|
83
|
+
| π | Matruk | Confirmed malicious, blocked |
|
|
84
|
+
|
|
85
|
+
## Exit Codes
|
|
86
|
+
|
|
87
|
+
- `0` β No critical findings
|
|
88
|
+
- `1` β Critical findings detected
|
|
89
|
+
- `2` β Scan error
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@musashimiyamoto/agent-guard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Security scanner for AI agent configurations. Detects misconfigurations, exposed secrets, and unsafe skill patterns.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agent-guard": "./src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"scan": "node src/cli.js scan",
|
|
11
|
+
"test": "node src/cli.js scan test/fixtures/vulnerable-agent --json"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"ai",
|
|
15
|
+
"agent",
|
|
16
|
+
"security",
|
|
17
|
+
"scanner",
|
|
18
|
+
"openclaw",
|
|
19
|
+
"moltbook",
|
|
20
|
+
"llm",
|
|
21
|
+
"vulnerability",
|
|
22
|
+
"secrets",
|
|
23
|
+
"audit"
|
|
24
|
+
],
|
|
25
|
+
"author": "Agent Guard <security@agentguard.dev>",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.0.0"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/agentguard/agent-guard"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://agentguard.dev",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/agentguard/agent-guard/issues"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"src/",
|
|
41
|
+
"README.md"
|
|
42
|
+
],
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"yaml": "^2.3.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Agent Guard CLI
|
|
4
|
+
// Security scanner for AI agent configurations
|
|
5
|
+
|
|
6
|
+
import { scan } from './scanner.js';
|
|
7
|
+
import { resolve } from 'path';
|
|
8
|
+
|
|
9
|
+
import { createProxy } from './proxy/index.js';
|
|
10
|
+
|
|
11
|
+
const VERSION = '0.1.0';
|
|
12
|
+
|
|
13
|
+
const COLORS = {
|
|
14
|
+
reset: '\x1b[0m',
|
|
15
|
+
bold: '\x1b[1m',
|
|
16
|
+
red: '\x1b[31m',
|
|
17
|
+
green: '\x1b[32m',
|
|
18
|
+
yellow: '\x1b[33m',
|
|
19
|
+
blue: '\x1b[34m',
|
|
20
|
+
magenta: '\x1b[35m',
|
|
21
|
+
cyan: '\x1b[36m',
|
|
22
|
+
gray: '\x1b[90m'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const SEVERITY_COLORS = {
|
|
26
|
+
critical: COLORS.red,
|
|
27
|
+
high: COLORS.magenta,
|
|
28
|
+
medium: COLORS.yellow,
|
|
29
|
+
low: COLORS.cyan
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const SEVERITY_ICONS = {
|
|
33
|
+
critical: 'π¨',
|
|
34
|
+
high: 'β οΈ ',
|
|
35
|
+
medium: 'π',
|
|
36
|
+
low: 'βΉοΈ '
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function printBanner() {
|
|
40
|
+
console.log(`
|
|
41
|
+
${COLORS.cyan}βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
42
|
+
β β
|
|
43
|
+
β ${COLORS.bold}π‘ AGENT GUARD${COLORS.reset}${COLORS.cyan} v${VERSION} β
|
|
44
|
+
β Security Scanner for AI Agents β
|
|
45
|
+
β β
|
|
46
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββ${COLORS.reset}
|
|
47
|
+
`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function printHelp() {
|
|
51
|
+
console.log(`
|
|
52
|
+
${COLORS.bold}Usage:${COLORS.reset}
|
|
53
|
+
agent-guard scan [path] Scan directory for security issues
|
|
54
|
+
agent-guard proxy [port] Start runtime protection proxy
|
|
55
|
+
agent-guard --help Show this help message
|
|
56
|
+
agent-guard --version Show version
|
|
57
|
+
|
|
58
|
+
${COLORS.bold}Examples:${COLORS.reset}
|
|
59
|
+
npx agent-guard scan . Scan current directory
|
|
60
|
+
npx agent-guard scan ./my-agent Scan specific agent directory
|
|
61
|
+
npx agent-guard proxy Start proxy on port 18800
|
|
62
|
+
npx agent-guard proxy 8080 Start proxy on custom port
|
|
63
|
+
|
|
64
|
+
${COLORS.bold}Options:${COLORS.reset}
|
|
65
|
+
--json Output results as JSON
|
|
66
|
+
--quiet Only show findings (no banner)
|
|
67
|
+
--policy <file> Use custom policy file
|
|
68
|
+
|
|
69
|
+
${COLORS.bold}Environment:${COLORS.reset}
|
|
70
|
+
HTTP_PROXY=http://127.0.0.1:18800 Route agent through proxy
|
|
71
|
+
|
|
72
|
+
${COLORS.bold}Exit Codes:${COLORS.reset}
|
|
73
|
+
0 No critical findings
|
|
74
|
+
1 Critical findings detected
|
|
75
|
+
2 Error during scan
|
|
76
|
+
`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function printScore(report) {
|
|
80
|
+
const { score, grade } = report;
|
|
81
|
+
|
|
82
|
+
const gradeColor =
|
|
83
|
+
grade === 'A' ? COLORS.green :
|
|
84
|
+
grade === 'B' ? COLORS.cyan :
|
|
85
|
+
grade === 'C' ? COLORS.yellow :
|
|
86
|
+
grade === 'D' ? COLORS.magenta : COLORS.red;
|
|
87
|
+
|
|
88
|
+
const bar = 'β'.repeat(Math.floor(score / 5)) + 'β'.repeat(20 - Math.floor(score / 5));
|
|
89
|
+
|
|
90
|
+
console.log(`
|
|
91
|
+
${COLORS.bold}Security Score:${COLORS.reset} ${gradeColor}${score}/100 (${grade})${COLORS.reset}
|
|
92
|
+
|
|
93
|
+
${gradeColor}${bar}${COLORS.reset}
|
|
94
|
+
|
|
95
|
+
${COLORS.bold}Summary:${COLORS.reset}
|
|
96
|
+
${COLORS.red}Critical:${COLORS.reset} ${report.summary.critical}
|
|
97
|
+
${COLORS.magenta}High:${COLORS.reset} ${report.summary.high}
|
|
98
|
+
${COLORS.yellow}Medium:${COLORS.reset} ${report.summary.medium}
|
|
99
|
+
${COLORS.cyan}Low:${COLORS.reset} ${report.summary.low}
|
|
100
|
+
|
|
101
|
+
Files scanned: ${report.scannedFiles}
|
|
102
|
+
`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function printFindings(findings) {
|
|
106
|
+
if (findings.length === 0) {
|
|
107
|
+
console.log(`${COLORS.green}β No security issues found!${COLORS.reset}\n`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(`${COLORS.bold}Findings:${COLORS.reset}\n`);
|
|
112
|
+
|
|
113
|
+
// Group by severity
|
|
114
|
+
const grouped = {
|
|
115
|
+
critical: findings.filter(f => f.severity === 'critical'),
|
|
116
|
+
high: findings.filter(f => f.severity === 'high'),
|
|
117
|
+
medium: findings.filter(f => f.severity === 'medium'),
|
|
118
|
+
low: findings.filter(f => f.severity === 'low')
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
for (const severity of ['critical', 'high', 'medium', 'low']) {
|
|
122
|
+
const items = grouped[severity];
|
|
123
|
+
if (items.length === 0) continue;
|
|
124
|
+
|
|
125
|
+
const color = SEVERITY_COLORS[severity];
|
|
126
|
+
const icon = SEVERITY_ICONS[severity];
|
|
127
|
+
|
|
128
|
+
console.log(`${color}${COLORS.bold}${icon} ${severity.toUpperCase()} (${items.length})${COLORS.reset}\n`);
|
|
129
|
+
|
|
130
|
+
for (const finding of items) {
|
|
131
|
+
const lineInfo = finding.line ? `:${finding.line}` : '';
|
|
132
|
+
console.log(` ${color}[${finding.rule}]${COLORS.reset} ${finding.name}`);
|
|
133
|
+
console.log(` ${COLORS.gray}${finding.file}${lineInfo}${COLORS.reset}`);
|
|
134
|
+
console.log(` ${finding.description}`);
|
|
135
|
+
console.log(` Match: ${COLORS.yellow}${finding.match}${COLORS.reset}`);
|
|
136
|
+
console.log();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function printRecommendations(report) {
|
|
142
|
+
if (report.totalFindings === 0) return;
|
|
143
|
+
|
|
144
|
+
console.log(`${COLORS.bold}Recommendations:${COLORS.reset}\n`);
|
|
145
|
+
|
|
146
|
+
const hasSecrets = report.findings.some(f => f.rule.startsWith('SEC-'));
|
|
147
|
+
const hasNetwork = report.findings.some(f => f.rule.startsWith('NET-'));
|
|
148
|
+
const hasAuth = report.findings.some(f => f.rule.startsWith('AUTH-'));
|
|
149
|
+
const hasSkill = report.findings.some(f => f.rule.startsWith('SKILL-'));
|
|
150
|
+
|
|
151
|
+
if (hasSecrets) {
|
|
152
|
+
console.log(` ${COLORS.red}1.${COLORS.reset} Rotate exposed API keys immediately`);
|
|
153
|
+
console.log(` Move secrets to environment variables or a vault\n`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (hasNetwork) {
|
|
157
|
+
console.log(` ${COLORS.red}2.${COLORS.reset} Fix network exposure`);
|
|
158
|
+
console.log(` Bind to 127.0.0.1 instead of 0.0.0.0`);
|
|
159
|
+
console.log(` Use a reverse proxy with authentication\n`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (hasAuth) {
|
|
163
|
+
console.log(` ${COLORS.red}3.${COLORS.reset} Enable authentication`);
|
|
164
|
+
console.log(` Configure JWT or API key authentication\n`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (hasSkill) {
|
|
168
|
+
console.log(` ${COLORS.yellow}4.${COLORS.reset} Review skill permissions`);
|
|
169
|
+
console.log(` Add skill.manifest.json with explicit permissions`);
|
|
170
|
+
console.log(` Audit skills using shell execution\n`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function main() {
|
|
175
|
+
const args = process.argv.slice(2);
|
|
176
|
+
|
|
177
|
+
const jsonOutput = args.includes('--json');
|
|
178
|
+
const quiet = args.includes('--quiet');
|
|
179
|
+
const filteredArgs = args.filter(a => !a.startsWith('--'));
|
|
180
|
+
|
|
181
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
182
|
+
printBanner();
|
|
183
|
+
printHelp();
|
|
184
|
+
process.exit(0);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
188
|
+
console.log(`agent-guard v${VERSION}`);
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const command = filteredArgs[0];
|
|
193
|
+
|
|
194
|
+
if (command === 'proxy') {
|
|
195
|
+
const port = parseInt(filteredArgs[1]) || 18800;
|
|
196
|
+
if (!quiet) printBanner();
|
|
197
|
+
console.log(`${COLORS.cyan}Starting runtime protection proxy...${COLORS.reset}\n`);
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const proxy = createProxy({ port });
|
|
201
|
+
proxy.start();
|
|
202
|
+
|
|
203
|
+
console.log(`${COLORS.green}Proxy ready.${COLORS.reset}`);
|
|
204
|
+
console.log(`${COLORS.gray}Set HTTP_PROXY=http://127.0.0.1:${port} to route agent traffic${COLORS.reset}\n`);
|
|
205
|
+
|
|
206
|
+
// Keep running
|
|
207
|
+
process.on('SIGINT', () => {
|
|
208
|
+
console.log(`\n${COLORS.yellow}Shutting down...${COLORS.reset}`);
|
|
209
|
+
proxy.stop();
|
|
210
|
+
process.exit(0);
|
|
211
|
+
});
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.error(`${COLORS.red}Error: ${err.message}${COLORS.reset}`);
|
|
214
|
+
process.exit(2);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (command !== 'scan') {
|
|
220
|
+
if (!quiet) printBanner();
|
|
221
|
+
printHelp();
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const targetPath = resolve(filteredArgs[1] || '.');
|
|
226
|
+
|
|
227
|
+
if (!quiet && !jsonOutput) {
|
|
228
|
+
printBanner();
|
|
229
|
+
console.log(`${COLORS.gray}Scanning: ${targetPath}${COLORS.reset}\n`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const report = await scan(targetPath);
|
|
234
|
+
|
|
235
|
+
if (jsonOutput) {
|
|
236
|
+
console.log(JSON.stringify(report, null, 2));
|
|
237
|
+
} else {
|
|
238
|
+
printScore(report);
|
|
239
|
+
printFindings(report.findings);
|
|
240
|
+
printRecommendations(report);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Exit with 1 if critical findings
|
|
244
|
+
process.exit(report.summary.critical > 0 ? 1 : 0);
|
|
245
|
+
|
|
246
|
+
} catch (err) {
|
|
247
|
+
console.error(`${COLORS.red}Error: ${err.message}${COLORS.reset}`);
|
|
248
|
+
process.exit(2);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
main();
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Egress Control Module
|
|
2
|
+
// Checks if outbound requests are allowed by policy
|
|
3
|
+
|
|
4
|
+
// Known malicious/exfiltration domains
|
|
5
|
+
const BLOCKED_DOMAINS = [
|
|
6
|
+
'webhook.site',
|
|
7
|
+
'requestbin.com',
|
|
8
|
+
'ngrok.io',
|
|
9
|
+
'pipedream.net',
|
|
10
|
+
'hookbin.com',
|
|
11
|
+
'burpcollaborator.net',
|
|
12
|
+
'oastify.com',
|
|
13
|
+
'interact.sh'
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// Default allowed domains (LLM APIs)
|
|
17
|
+
const DEFAULT_ALLOWED = [
|
|
18
|
+
'api.openai.com',
|
|
19
|
+
'api.anthropic.com',
|
|
20
|
+
'generativelanguage.googleapis.com',
|
|
21
|
+
'api.mistral.ai',
|
|
22
|
+
'api.cohere.ai',
|
|
23
|
+
'api.together.xyz',
|
|
24
|
+
'openrouter.ai'
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function checkEgress(url, policy = {}) {
|
|
28
|
+
const hostname = url.hostname.toLowerCase();
|
|
29
|
+
|
|
30
|
+
// Check blocked domains first (always blocked)
|
|
31
|
+
for (const blocked of BLOCKED_DOMAINS) {
|
|
32
|
+
if (hostname === blocked || hostname.endsWith('.' + blocked)) {
|
|
33
|
+
return {
|
|
34
|
+
allowed: false,
|
|
35
|
+
reason: `Domain "${blocked}" is on the blocklist (known exfiltration endpoint)`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check policy-specific blocks
|
|
41
|
+
if (policy.egress?.rules) {
|
|
42
|
+
for (const rule of policy.egress.rules) {
|
|
43
|
+
if (matchDomain(hostname, rule.domain || rule.pattern)) {
|
|
44
|
+
if (rule.allow === false) {
|
|
45
|
+
return {
|
|
46
|
+
allowed: false,
|
|
47
|
+
reason: rule.reason || `Blocked by policy rule: ${rule.pattern || rule.domain}`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (rule.allow === true) {
|
|
51
|
+
return {
|
|
52
|
+
allowed: true,
|
|
53
|
+
reason: rule.purpose || 'Allowed by policy'
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check default allowed
|
|
61
|
+
for (const allowed of DEFAULT_ALLOWED) {
|
|
62
|
+
if (hostname === allowed || hostname.endsWith('.' + allowed)) {
|
|
63
|
+
return {
|
|
64
|
+
allowed: true,
|
|
65
|
+
reason: 'Default allowed (LLM API)'
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check policy default action
|
|
71
|
+
const defaultAction = policy.egress?.default || 'deny';
|
|
72
|
+
|
|
73
|
+
if (defaultAction === 'allow') {
|
|
74
|
+
return {
|
|
75
|
+
allowed: true,
|
|
76
|
+
reason: 'Allowed by default policy'
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
allowed: false,
|
|
82
|
+
reason: 'Not in allowlist (default deny)'
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function matchDomain(hostname, pattern) {
|
|
87
|
+
if (!pattern) return false;
|
|
88
|
+
|
|
89
|
+
// Exact match
|
|
90
|
+
if (hostname === pattern) return true;
|
|
91
|
+
|
|
92
|
+
// Wildcard match (*.example.com)
|
|
93
|
+
if (pattern.startsWith('*.')) {
|
|
94
|
+
const suffix = pattern.slice(2);
|
|
95
|
+
return hostname === suffix || hostname.endsWith('.' + suffix);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Subdomain match (matches example.com and *.example.com)
|
|
99
|
+
return hostname.endsWith('.' + pattern);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function addToBlocklist(domain) {
|
|
103
|
+
if (!BLOCKED_DOMAINS.includes(domain)) {
|
|
104
|
+
BLOCKED_DOMAINS.push(domain);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function addToAllowlist(domain) {
|
|
109
|
+
if (!DEFAULT_ALLOWED.includes(domain)) {
|
|
110
|
+
DEFAULT_ALLOWED.push(domain);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Agent Guard Runtime Proxy
|
|
2
|
+
// Phase 2: Inference-level protection
|
|
3
|
+
|
|
4
|
+
import { createServer } from 'http';
|
|
5
|
+
import { request as httpRequest } from 'http';
|
|
6
|
+
import { request as httpsRequest } from 'https';
|
|
7
|
+
import { URL } from 'url';
|
|
8
|
+
import { loadPolicy } from './policy.js';
|
|
9
|
+
import { checkEgress } from './egress.js';
|
|
10
|
+
import { logEvent } from './logger.js';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_PORT = 18800;
|
|
13
|
+
|
|
14
|
+
export class AgentGuardProxy {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.port = options.port || DEFAULT_PORT;
|
|
17
|
+
this.policy = options.policy || loadPolicy();
|
|
18
|
+
this.server = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
start() {
|
|
22
|
+
this.server = createServer((req, res) => {
|
|
23
|
+
this.handleRequest(req, res);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
this.server.listen(this.port, '127.0.0.1', () => {
|
|
27
|
+
console.log(`π‘ Agent Guard Proxy listening on http://127.0.0.1:${this.port}`);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
stop() {
|
|
34
|
+
if (this.server) {
|
|
35
|
+
this.server.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async handleRequest(clientReq, clientRes) {
|
|
40
|
+
const startTime = Date.now();
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Parse target URL from proxy request
|
|
44
|
+
const targetUrl = new URL(clientReq.url);
|
|
45
|
+
|
|
46
|
+
// Check egress policy
|
|
47
|
+
const egressResult = checkEgress(targetUrl, this.policy);
|
|
48
|
+
|
|
49
|
+
if (!egressResult.allowed) {
|
|
50
|
+
logEvent({
|
|
51
|
+
type: 'egress_blocked',
|
|
52
|
+
target: targetUrl.hostname,
|
|
53
|
+
reason: egressResult.reason,
|
|
54
|
+
violation: true
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
clientRes.writeHead(403, { 'Content-Type': 'application/json' });
|
|
58
|
+
clientRes.end(JSON.stringify({
|
|
59
|
+
error: 'EGRESS_DENIED',
|
|
60
|
+
message: `Access to ${targetUrl.hostname} blocked by security policy`,
|
|
61
|
+
reason: egressResult.reason
|
|
62
|
+
}));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
logEvent({
|
|
67
|
+
type: 'egress_allowed',
|
|
68
|
+
target: targetUrl.hostname,
|
|
69
|
+
violation: false
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Forward request to target
|
|
73
|
+
const response = await this.forwardRequest(clientReq, targetUrl);
|
|
74
|
+
|
|
75
|
+
// Log response (for intent verification later)
|
|
76
|
+
const latency = Date.now() - startTime;
|
|
77
|
+
logEvent({
|
|
78
|
+
type: 'request_complete',
|
|
79
|
+
target: targetUrl.hostname,
|
|
80
|
+
status: response.statusCode,
|
|
81
|
+
latency
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Forward response to client
|
|
85
|
+
clientRes.writeHead(response.statusCode, response.headers);
|
|
86
|
+
response.pipe(clientRes);
|
|
87
|
+
|
|
88
|
+
} catch (err) {
|
|
89
|
+
logEvent({
|
|
90
|
+
type: 'proxy_error',
|
|
91
|
+
error: err.message,
|
|
92
|
+
violation: false
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
clientRes.writeHead(502, { 'Content-Type': 'application/json' });
|
|
96
|
+
clientRes.end(JSON.stringify({
|
|
97
|
+
error: 'PROXY_ERROR',
|
|
98
|
+
message: err.message
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
forwardRequest(clientReq, targetUrl) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const requestFn = targetUrl.protocol === 'https:' ? httpsRequest : httpRequest;
|
|
106
|
+
|
|
107
|
+
const options = {
|
|
108
|
+
hostname: targetUrl.hostname,
|
|
109
|
+
port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
|
|
110
|
+
path: targetUrl.pathname + targetUrl.search,
|
|
111
|
+
method: clientReq.method,
|
|
112
|
+
headers: {
|
|
113
|
+
...clientReq.headers,
|
|
114
|
+
host: targetUrl.host
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const proxyReq = requestFn(options, (proxyRes) => {
|
|
119
|
+
resolve(proxyRes);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
proxyReq.on('error', reject);
|
|
123
|
+
|
|
124
|
+
// Forward request body
|
|
125
|
+
clientReq.pipe(proxyReq);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createProxy(options) {
|
|
131
|
+
return new AgentGuardProxy(options);
|
|
132
|
+
}
|