@guardion/guardion 0.2.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/config.yaml.example +26 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +289 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +63 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +26 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +44 -0
- package/dist/constants.js.map +1 -0
- package/dist/installer.d.ts +13 -0
- package/dist/installer.d.ts.map +1 -0
- package/dist/installer.js +137 -0
- package/dist/installer.js.map +1 -0
- package/dist/keychain.d.ts +4 -0
- package/dist/keychain.d.ts.map +1 -0
- package/dist/keychain.js +117 -0
- package/dist/keychain.js.map +1 -0
- package/dist/mock-server.d.ts +2 -0
- package/dist/mock-server.d.ts.map +1 -0
- package/dist/mock-server.js +172 -0
- package/dist/mock-server.js.map +1 -0
- package/dist/scanner.d.ts +24 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +23 -0
- package/dist/scanner.js.map +1 -0
- package/hooks/guardion-hook.cjs +202 -0
- package/hooks/metadata.cjs +86 -0
- package/package.json +67 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Guardion Claude Code hook — monitoring-only event forwarder.
|
|
4
|
+
*
|
|
5
|
+
* Reads hook event JSON from stdin, resolves config + token, posts to Guard API.
|
|
6
|
+
* Always exits 0 — never blocks Claude Code.
|
|
7
|
+
*
|
|
8
|
+
* Config (in priority order):
|
|
9
|
+
* ~/.guardion/config.json written by `npx guardion init`
|
|
10
|
+
* GUARDION_API_URL env override for CI / testing
|
|
11
|
+
* GUARDION_POLICY env override for CI / testing
|
|
12
|
+
*
|
|
13
|
+
* Token (first match wins):
|
|
14
|
+
* GUARDION_TOKEN env CI / testing
|
|
15
|
+
* macOS Keychain service=guardion, account=token
|
|
16
|
+
* /etc/guardion/token enterprise MDM, root-owned
|
|
17
|
+
* ~/.guardion/token user-level fallback
|
|
18
|
+
*/
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
const http = require('http');
|
|
25
|
+
const https = require('https');
|
|
26
|
+
const { execFileSync } = require('child_process');
|
|
27
|
+
|
|
28
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const HOOK_TIMEOUT_MS = 3000;
|
|
31
|
+
const KEYCHAIN_TIMEOUT = 1000;
|
|
32
|
+
const GUARDION_DIR = path.join(os.homedir(), '.guardion');
|
|
33
|
+
const CONFIG_JSON_PATH = path.join(GUARDION_DIR, 'config.json');
|
|
34
|
+
const SESSION_DIR = path.join(GUARDION_DIR, 'sessions');
|
|
35
|
+
const CURRENT_SESSION = path.join(GUARDION_DIR, 'current-session');
|
|
36
|
+
const HOOK_EVENTS_PATH = '/v1/hooks/events';
|
|
37
|
+
|
|
38
|
+
// ── Config ───────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function loadConfig() {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(fs.readFileSync(CONFIG_JSON_PATH, 'utf8'));
|
|
43
|
+
} catch {
|
|
44
|
+
return {
|
|
45
|
+
api_url: process.env.GUARDION_API_URL || 'https://api.guardion.ai',
|
|
46
|
+
policy: process.env.GUARDION_POLICY || '',
|
|
47
|
+
application: process.env.GUARDION_APPLICATION || 'claude-code',
|
|
48
|
+
tier: process.env.GUARDION_TIER || 'hooks',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Token ────────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
function resolveToken() {
|
|
56
|
+
if (process.env.GUARDION_TOKEN) return process.env.GUARDION_TOKEN.trim();
|
|
57
|
+
|
|
58
|
+
if (process.platform === 'darwin') {
|
|
59
|
+
try {
|
|
60
|
+
const t = execFileSync(
|
|
61
|
+
'security',
|
|
62
|
+
['find-generic-password', '-s', 'guardion', '-a', 'token', '-w'],
|
|
63
|
+
{ timeout: KEYCHAIN_TIMEOUT, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
64
|
+
).trim();
|
|
65
|
+
if (t) return t;
|
|
66
|
+
} catch { /* not in keychain */ }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const p of ['/etc/guardion/token', path.join(GUARDION_DIR, 'token')]) {
|
|
70
|
+
try {
|
|
71
|
+
const t = fs.readFileSync(p, 'utf8').trim();
|
|
72
|
+
if (t) return t;
|
|
73
|
+
} catch { /* file absent */ }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Session persistence ───────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function writeCurrentSession(sessionId) {
|
|
82
|
+
try {
|
|
83
|
+
fs.mkdirSync(GUARDION_DIR, { recursive: true });
|
|
84
|
+
const tmp = CURRENT_SESSION + '.tmp';
|
|
85
|
+
fs.writeFileSync(tmp, sessionId, 'utf8');
|
|
86
|
+
fs.renameSync(tmp, CURRENT_SESSION);
|
|
87
|
+
} catch { /* non-critical */ }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readCurrentSession() {
|
|
91
|
+
try { return fs.readFileSync(CURRENT_SESSION, 'utf8').trim(); } catch { return ''; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function deleteCurrentSession() {
|
|
95
|
+
try { fs.unlinkSync(CURRENT_SESSION); } catch { /* already gone */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function writeSessionMeta(sessionId, meta) {
|
|
99
|
+
try {
|
|
100
|
+
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
101
|
+
fs.writeFileSync(
|
|
102
|
+
path.join(SESSION_DIR, `${sessionId}.json`),
|
|
103
|
+
JSON.stringify(meta, null, 2),
|
|
104
|
+
'utf8'
|
|
105
|
+
);
|
|
106
|
+
} catch { /* non-critical */ }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function deleteSessionMeta(sessionId) {
|
|
110
|
+
if (!sessionId) return;
|
|
111
|
+
try { fs.unlinkSync(path.join(SESSION_DIR, `${sessionId}.json`)); } catch { /* gone */ }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── HTTP POST ─────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
function postEvent(apiUrl, token, payload) {
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
let url;
|
|
119
|
+
try { url = new URL(HOOK_EVENTS_PATH, apiUrl); } catch {
|
|
120
|
+
return resolve();
|
|
121
|
+
}
|
|
122
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
123
|
+
const body = JSON.stringify(payload);
|
|
124
|
+
|
|
125
|
+
const req = transport.request(
|
|
126
|
+
{
|
|
127
|
+
hostname: url.hostname,
|
|
128
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
129
|
+
path: url.pathname + url.search,
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: {
|
|
132
|
+
'Content-Type': 'application/json',
|
|
133
|
+
'Authorization': `Bearer ${token}`,
|
|
134
|
+
'Content-Length': Buffer.byteLength(body),
|
|
135
|
+
},
|
|
136
|
+
timeout: HOOK_TIMEOUT_MS,
|
|
137
|
+
},
|
|
138
|
+
(res) => { res.resume(); res.on('end', resolve); }
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
req.on('error', resolve);
|
|
142
|
+
req.on('timeout', () => { req.destroy(); resolve(); });
|
|
143
|
+
req.write(body);
|
|
144
|
+
req.end();
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Metadata ──────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
function collectMetadata(payload) {
|
|
151
|
+
try {
|
|
152
|
+
const { collectMetadata: collect } = require('./metadata.cjs');
|
|
153
|
+
return collect(payload);
|
|
154
|
+
} catch {
|
|
155
|
+
return { session_id: payload.session_id || null, cwd: payload.cwd || process.cwd() };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
// Safety: hard exit after timeout regardless of network state
|
|
162
|
+
setTimeout(() => process.exit(0), HOOK_TIMEOUT_MS + 1500);
|
|
163
|
+
|
|
164
|
+
let rawInput = '';
|
|
165
|
+
process.stdin.setEncoding('utf8');
|
|
166
|
+
process.stdin.on('data', (chunk) => { rawInput += chunk; });
|
|
167
|
+
process.stdin.on('end', async () => {
|
|
168
|
+
let payload;
|
|
169
|
+
try { payload = JSON.parse(rawInput); } catch { return process.exit(0); }
|
|
170
|
+
|
|
171
|
+
const config = loadConfig();
|
|
172
|
+
const token = resolveToken();
|
|
173
|
+
if (!token) return process.exit(0);
|
|
174
|
+
|
|
175
|
+
const event = payload.hook_event_name || payload.event || '';
|
|
176
|
+
|
|
177
|
+
// Enrich payload with config context
|
|
178
|
+
if (config.policy) payload.policy = config.policy;
|
|
179
|
+
if (config.application) payload.application = config.application;
|
|
180
|
+
|
|
181
|
+
if (event === 'SessionStart') {
|
|
182
|
+
const meta = collectMetadata(payload);
|
|
183
|
+
const sessionId = payload.session_id || meta.session_id || `gs-${Date.now()}`;
|
|
184
|
+
|
|
185
|
+
payload.trace_id = sessionId;
|
|
186
|
+
payload.metadata = meta;
|
|
187
|
+
|
|
188
|
+
writeCurrentSession(sessionId);
|
|
189
|
+
writeSessionMeta(sessionId, meta);
|
|
190
|
+
} else if (event === 'SessionEnd') {
|
|
191
|
+
const sessionId = payload.session_id || readCurrentSession();
|
|
192
|
+
payload.trace_id = sessionId;
|
|
193
|
+
deleteCurrentSession();
|
|
194
|
+
deleteSessionMeta(sessionId);
|
|
195
|
+
} else {
|
|
196
|
+
const traceId = process.env.GUARDION_TRACE_ID || readCurrentSession();
|
|
197
|
+
if (traceId) payload.trace_id = traceId;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await postEvent(config.api_url, token, payload);
|
|
201
|
+
process.exit(0);
|
|
202
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const GIT_TIMEOUT_MS = 2000;
|
|
9
|
+
const SESSION_FILE_DIR = path.join(os.homedir(), '.claude', 'sessions');
|
|
10
|
+
|
|
11
|
+
function gitCmd(args, cwd) {
|
|
12
|
+
try {
|
|
13
|
+
return execFileSync('git', args, {
|
|
14
|
+
cwd: cwd || process.cwd(),
|
|
15
|
+
timeout: GIT_TIMEOUT_MS,
|
|
16
|
+
encoding: 'utf8',
|
|
17
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
18
|
+
}).trim();
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readSessionFile() {
|
|
25
|
+
const pids = [process.pid, process.ppid];
|
|
26
|
+
for (const pid of pids) {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(fs.readFileSync(path.join(SESSION_FILE_DIR, `${pid}.json`), 'utf8'));
|
|
29
|
+
} catch { /* try next */ }
|
|
30
|
+
}
|
|
31
|
+
// Fallback: most recent session file
|
|
32
|
+
try {
|
|
33
|
+
const files = fs.readdirSync(SESSION_FILE_DIR)
|
|
34
|
+
.filter(f => f.endsWith('.json'))
|
|
35
|
+
.map(f => ({ name: f, mtime: fs.statSync(path.join(SESSION_FILE_DIR, f)).mtimeMs }))
|
|
36
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
37
|
+
if (files.length > 0) {
|
|
38
|
+
return JSON.parse(fs.readFileSync(path.join(SESSION_FILE_DIR, files[0].name), 'utf8'));
|
|
39
|
+
}
|
|
40
|
+
} catch { /* dir absent or empty */ }
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function collectMetadata(hookPayload) {
|
|
45
|
+
const cwd = hookPayload.cwd || process.cwd();
|
|
46
|
+
const sessionFile = readSessionFile();
|
|
47
|
+
|
|
48
|
+
const meta = {
|
|
49
|
+
session_id: hookPayload.session_id || sessionFile?.sessionId || null,
|
|
50
|
+
pid: sessionFile?.pid || process.ppid || null,
|
|
51
|
+
cwd,
|
|
52
|
+
started_at: sessionFile?.startedAt || null,
|
|
53
|
+
kind: sessionFile?.kind || null,
|
|
54
|
+
entrypoint: sessionFile?.entrypoint || null,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
try { meta.os_user = os.userInfo().username; } catch {
|
|
58
|
+
meta.os_user = process.env.USER || process.env.USERNAME || null;
|
|
59
|
+
}
|
|
60
|
+
try { meta.os_uid = process.getuid ? process.getuid() : null; } catch { meta.os_uid = null; }
|
|
61
|
+
|
|
62
|
+
meta.git_user_name = gitCmd(['config', 'user.name'], cwd);
|
|
63
|
+
meta.git_user_email = gitCmd(['config', 'user.email'], cwd);
|
|
64
|
+
|
|
65
|
+
try { meta.hostname = os.hostname(); } catch { meta.hostname = null; }
|
|
66
|
+
meta.platform = process.platform;
|
|
67
|
+
meta.arch = process.arch;
|
|
68
|
+
try { meta.os_version = os.release(); } catch { meta.os_version = null; }
|
|
69
|
+
meta.node_version = process.version;
|
|
70
|
+
meta.shell = process.env.SHELL || null;
|
|
71
|
+
|
|
72
|
+
meta.git_remote = gitCmd(['remote', 'get-url', 'origin'], cwd);
|
|
73
|
+
meta.git_branch = gitCmd(['branch', '--show-current'], cwd);
|
|
74
|
+
meta.git_commit = gitCmd(['rev-parse', '--short', 'HEAD'], cwd);
|
|
75
|
+
const porcelain = gitCmd(['status', '--porcelain'], cwd);
|
|
76
|
+
meta.git_dirty = porcelain !== null ? porcelain.length > 0 : null;
|
|
77
|
+
|
|
78
|
+
// Strip null/undefined to keep payload compact
|
|
79
|
+
for (const k of Object.keys(meta)) {
|
|
80
|
+
if (meta[k] === null || meta[k] === undefined) delete meta[k];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return meta;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { collectMetadata, readSessionFile, gitCmd };
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@guardion/guardion",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "AI security monitoring for Claude Code — full agent observability via hooks",
|
|
6
|
+
"bin": {
|
|
7
|
+
"guardion": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/GuardionAI/guardion.git"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://guardion.ai",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"security",
|
|
19
|
+
"monitoring",
|
|
20
|
+
"ai",
|
|
21
|
+
"claude",
|
|
22
|
+
"claude-code",
|
|
23
|
+
"guardrails",
|
|
24
|
+
"hooks",
|
|
25
|
+
"observability",
|
|
26
|
+
"llm"
|
|
27
|
+
],
|
|
28
|
+
"author": {
|
|
29
|
+
"name": "Guardion AI",
|
|
30
|
+
"email": "rafael@guardion.ai",
|
|
31
|
+
"url": "https://guardion.ai"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc",
|
|
35
|
+
"dev": "tsx src/cli.ts",
|
|
36
|
+
"hook": "tsx src/cli.ts hook",
|
|
37
|
+
"mock": "tsx src/cli.ts mock",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest",
|
|
40
|
+
"test:integration": "vitest run --reporter=verbose __tests__/integration"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"chalk": "^5.3.0",
|
|
44
|
+
"commander": "^12.1.0",
|
|
45
|
+
"express": "^4.21.0",
|
|
46
|
+
"js-yaml": "^4.1.0",
|
|
47
|
+
"zod": "^3.23.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/express": "^4.17.21",
|
|
51
|
+
"@types/js-yaml": "^4.0.9",
|
|
52
|
+
"@types/node": "^22.0.0",
|
|
53
|
+
"tsx": "^4.19.0",
|
|
54
|
+
"typescript": "^5.6.0",
|
|
55
|
+
"vitest": "^4.1.4"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18"
|
|
59
|
+
},
|
|
60
|
+
"files": [
|
|
61
|
+
"dist",
|
|
62
|
+
"hooks",
|
|
63
|
+
"plugin.json",
|
|
64
|
+
"config.yaml.example"
|
|
65
|
+
],
|
|
66
|
+
"license": "MIT"
|
|
67
|
+
}
|