@sanctix/client 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/LICENSE +21 -0
- package/README.md +25 -0
- package/bin/sanctix.js +48 -0
- package/cli/init.js +278 -0
- package/cli/runtime.js +85 -0
- package/cli/start.js +165 -0
- package/cli/status.js +103 -0
- package/cli/stop.js +134 -0
- package/cli/uninstall.js +195 -0
- package/hooks/.gitkeep +0 -0
- package/hooks/check-agent-spawn-result.cjs +60 -0
- package/hooks/check-agent-spawn.cjs +60 -0
- package/hooks/check-bash.cjs +75 -0
- package/hooks/check-file-edit-result.cjs +63 -0
- package/hooks/check-file-edit.cjs +63 -0
- package/hooks/check-subagent-start.cjs +59 -0
- package/hooks/post-audit-event.cjs +50 -0
- package/hooks/tool-capture.cjs +138 -0
- package/package.json +37 -0
- package/templates/.gitkeep +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
const { captureToolContext } = require(path.join(__dirname, 'tool-capture.cjs'));
|
|
9
|
+
const { postAuditEvent } = require(path.join(__dirname, 'post-audit-event.cjs'));
|
|
10
|
+
|
|
11
|
+
const AUDIT_URL = process.env.SANCTIX_AUDIT_URL || 'http://localhost:8787';
|
|
12
|
+
const API_KEY = process.env.SANCTIX_API_KEY || null;
|
|
13
|
+
const CORRELATION_ID = process.env.AWF_CORRELATION_ID || crypto.randomUUID();
|
|
14
|
+
const USER_ID = process.env.AWF_USER_ID || 'user';
|
|
15
|
+
|
|
16
|
+
const LOG_PATH = '/tmp/sanctix-audit.log';
|
|
17
|
+
|
|
18
|
+
let input = {};
|
|
19
|
+
try {
|
|
20
|
+
input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
21
|
+
} catch (_e) {
|
|
22
|
+
input = {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const toolName = (input && input.tool_name) || '';
|
|
26
|
+
const filePath = (input && input.tool_input && (input.tool_input.path || input.tool_input.file_path)) || 'unknown';
|
|
27
|
+
const timestamp = new Date().toISOString();
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
fs.appendFileSync(LOG_PATH, JSON.stringify({
|
|
31
|
+
timestamp,
|
|
32
|
+
tool: toolName,
|
|
33
|
+
file: filePath,
|
|
34
|
+
hook: 'pre-tool-use',
|
|
35
|
+
decision: 'allow',
|
|
36
|
+
}) + '\n');
|
|
37
|
+
} catch (_e) {}
|
|
38
|
+
|
|
39
|
+
(async () => {
|
|
40
|
+
let toolCtx = null;
|
|
41
|
+
try { toolCtx = captureToolContext(input); } catch (_e) {}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await postAuditEvent({
|
|
45
|
+
actor_type: 'agent',
|
|
46
|
+
event_type: 'hook.file_edit.pre_tool_use',
|
|
47
|
+
timestamp_utc: timestamp,
|
|
48
|
+
correlation_id: CORRELATION_ID,
|
|
49
|
+
user_id: USER_ID,
|
|
50
|
+
runtime_provider: 'claude_code',
|
|
51
|
+
event_data: {
|
|
52
|
+
tool_name: toolCtx && toolCtx.tool_name,
|
|
53
|
+
file_path: toolCtx && toolCtx.file_path,
|
|
54
|
+
command: toolCtx && toolCtx.command,
|
|
55
|
+
args_redacted: toolCtx && toolCtx.args_redacted,
|
|
56
|
+
},
|
|
57
|
+
before_state: toolCtx && toolCtx.before_state,
|
|
58
|
+
}, { url: AUDIT_URL, apiKey: API_KEY, timeoutMs: 500 });
|
|
59
|
+
} catch (_e) {}
|
|
60
|
+
|
|
61
|
+
process.stdout.write(JSON.stringify({ decision: 'allow' }));
|
|
62
|
+
process.exit(0);
|
|
63
|
+
})();
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
const { captureToolContext } = require(path.join(__dirname, 'tool-capture.cjs'));
|
|
9
|
+
const { postAuditEvent } = require(path.join(__dirname, 'post-audit-event.cjs'));
|
|
10
|
+
|
|
11
|
+
const AUDIT_URL = process.env.SANCTIX_AUDIT_URL || 'http://localhost:8787';
|
|
12
|
+
const API_KEY = process.env.SANCTIX_API_KEY || null;
|
|
13
|
+
const CORRELATION_ID = process.env.AWF_CORRELATION_ID || crypto.randomUUID();
|
|
14
|
+
const USER_ID = process.env.AWF_USER_ID || 'user';
|
|
15
|
+
|
|
16
|
+
const LOG_PATH = '/tmp/sanctix-audit.log';
|
|
17
|
+
|
|
18
|
+
let input = {};
|
|
19
|
+
try {
|
|
20
|
+
input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
21
|
+
} catch (_e) {
|
|
22
|
+
input = {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const timestamp = new Date().toISOString();
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
fs.appendFileSync(LOG_PATH, JSON.stringify({
|
|
29
|
+
timestamp,
|
|
30
|
+
hook: 'sub-agent-start',
|
|
31
|
+
decision: 'allow',
|
|
32
|
+
}) + '\n');
|
|
33
|
+
} catch (_e) {}
|
|
34
|
+
|
|
35
|
+
(async () => {
|
|
36
|
+
let toolCtx = null;
|
|
37
|
+
try { toolCtx = captureToolContext(input); } catch (_e) {}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await postAuditEvent({
|
|
41
|
+
actor_type: 'agent',
|
|
42
|
+
event_type: 'hook.subagent.start',
|
|
43
|
+
timestamp_utc: timestamp,
|
|
44
|
+
correlation_id: CORRELATION_ID,
|
|
45
|
+
user_id: USER_ID,
|
|
46
|
+
runtime_provider: 'claude_code',
|
|
47
|
+
event_data: {
|
|
48
|
+
tool_name: toolCtx && toolCtx.tool_name,
|
|
49
|
+
file_path: toolCtx && toolCtx.file_path,
|
|
50
|
+
command: toolCtx && toolCtx.command,
|
|
51
|
+
args_redacted: toolCtx && toolCtx.args_redacted,
|
|
52
|
+
},
|
|
53
|
+
before_state: toolCtx && toolCtx.before_state,
|
|
54
|
+
}, { url: AUDIT_URL, apiKey: API_KEY, timeoutMs: 500 });
|
|
55
|
+
} catch (_e) {}
|
|
56
|
+
|
|
57
|
+
process.stdout.write(JSON.stringify({ decision: 'allow' }));
|
|
58
|
+
process.exit(0);
|
|
59
|
+
})();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Self-contained audit POST helper for all client-side hook shims.
|
|
4
|
+
// Uses Node http/https only. Never throws.
|
|
5
|
+
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const https = require('https');
|
|
8
|
+
|
|
9
|
+
async function postAuditEvent(payload, opts) {
|
|
10
|
+
const url = (opts && opts.url) || 'http://localhost:8787';
|
|
11
|
+
const apiKey = (opts && opts.apiKey) || null;
|
|
12
|
+
const timeoutMs = (opts && opts.timeoutMs) || 500;
|
|
13
|
+
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
try {
|
|
16
|
+
const parsed = new URL('/events', url);
|
|
17
|
+
const isHttps = parsed.protocol === 'https:';
|
|
18
|
+
const lib = isHttps ? https : http;
|
|
19
|
+
const data = Buffer.from(JSON.stringify(payload));
|
|
20
|
+
const headers = {
|
|
21
|
+
'content-type': 'application/json',
|
|
22
|
+
'content-length': data.length,
|
|
23
|
+
};
|
|
24
|
+
if (apiKey) {
|
|
25
|
+
headers['X-Sanctix-API-Key'] = apiKey;
|
|
26
|
+
}
|
|
27
|
+
const req = lib.request({
|
|
28
|
+
host: parsed.hostname,
|
|
29
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
30
|
+
path: parsed.pathname,
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: headers,
|
|
33
|
+
}, (res) => {
|
|
34
|
+
res.on('data', () => {});
|
|
35
|
+
res.on('end', resolve);
|
|
36
|
+
});
|
|
37
|
+
req.on('error', () => resolve());
|
|
38
|
+
req.setTimeout(timeoutMs, () => {
|
|
39
|
+
try { req.destroy(); } catch (_e) {}
|
|
40
|
+
resolve();
|
|
41
|
+
});
|
|
42
|
+
req.write(data);
|
|
43
|
+
req.end();
|
|
44
|
+
} catch (_e) {
|
|
45
|
+
resolve();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { postAuditEvent };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Self-contained tool-input capture for client-side hook audit payloads.
|
|
4
|
+
// CommonJS so it can be require()d from .cjs hooks.
|
|
5
|
+
// Best-effort: every field is wrapped in try/catch and falls back to null.
|
|
6
|
+
// captureToolContext never throws.
|
|
7
|
+
|
|
8
|
+
const { execFileSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const SECRET_KEY_PATTERNS = [
|
|
11
|
+
'password',
|
|
12
|
+
'secret',
|
|
13
|
+
'key',
|
|
14
|
+
'token',
|
|
15
|
+
'api_key',
|
|
16
|
+
'apikey',
|
|
17
|
+
'auth',
|
|
18
|
+
'credential',
|
|
19
|
+
'database_url',
|
|
20
|
+
'private',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const BEARER_TOKEN_RE = /^(Bearer\s+|sk-|ghp_|xoxb-)/i;
|
|
24
|
+
|
|
25
|
+
const COMMAND_MAX_LEN = 500;
|
|
26
|
+
|
|
27
|
+
function isSecretKey(key) {
|
|
28
|
+
if (typeof key !== 'string') return false;
|
|
29
|
+
const lower = key.toLowerCase();
|
|
30
|
+
return SECRET_KEY_PATTERNS.some((p) => lower.includes(p));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isSecretValue(value) {
|
|
34
|
+
if (typeof value !== 'string') return false;
|
|
35
|
+
if (BEARER_TOKEN_RE.test(value)) return true;
|
|
36
|
+
if (value.length > 200 && !/\s/.test(value)) return true;
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseArgsRedacted(command) {
|
|
41
|
+
try {
|
|
42
|
+
if (typeof command !== 'string') return null;
|
|
43
|
+
const result = {};
|
|
44
|
+
const tokens = command.split(/\s+/);
|
|
45
|
+
for (const tok of tokens) {
|
|
46
|
+
if (!tok) continue;
|
|
47
|
+
const eqIdx = tok.indexOf('=');
|
|
48
|
+
if (eqIdx <= 0) continue;
|
|
49
|
+
const key = tok.slice(0, eqIdx);
|
|
50
|
+
const value = tok.slice(eqIdx + 1);
|
|
51
|
+
if (isSecretKey(key) || isSecretValue(value)) {
|
|
52
|
+
result[key] = '[REDACTED]';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Object.keys(result).length ? result : null;
|
|
56
|
+
} catch (_e) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parsePorcelain(output) {
|
|
62
|
+
const staged = [];
|
|
63
|
+
const unstaged = [];
|
|
64
|
+
const untracked = [];
|
|
65
|
+
if (!output) return { staged, unstaged, untracked };
|
|
66
|
+
const lines = output.split('\n');
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
if (!line || line.length < 3) continue;
|
|
69
|
+
const X = line[0];
|
|
70
|
+
const Y = line[1];
|
|
71
|
+
const path = line.slice(3);
|
|
72
|
+
if (X === '?' && Y === '?') {
|
|
73
|
+
untracked.push({ status: '??', path });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (X !== ' ' && X !== '?') {
|
|
77
|
+
staged.push({ status: X, path });
|
|
78
|
+
}
|
|
79
|
+
if (Y !== ' ' && Y !== '?') {
|
|
80
|
+
unstaged.push({ status: Y, path });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { staged, unstaged, untracked };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function captureGitState() {
|
|
87
|
+
try {
|
|
88
|
+
const out = execFileSync('git', ['status', '--porcelain'], {
|
|
89
|
+
cwd: process.cwd(),
|
|
90
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
91
|
+
encoding: 'utf8',
|
|
92
|
+
timeout: 2000,
|
|
93
|
+
});
|
|
94
|
+
return parsePorcelain(out);
|
|
95
|
+
} catch (_e) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function captureToolContext(input) {
|
|
101
|
+
const ctx = {
|
|
102
|
+
tool_name: null,
|
|
103
|
+
file_path: null,
|
|
104
|
+
command: null,
|
|
105
|
+
args_redacted: null,
|
|
106
|
+
before_state: null,
|
|
107
|
+
};
|
|
108
|
+
try {
|
|
109
|
+
if (!input || typeof input !== 'object') return ctx;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
ctx.tool_name = input.tool_name || null;
|
|
113
|
+
} catch (_e) {}
|
|
114
|
+
|
|
115
|
+
const toolInput = (input && input.tool_input) || {};
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
ctx.file_path = toolInput.path || toolInput.file_path || null;
|
|
119
|
+
} catch (_e) {}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
if (ctx.tool_name === 'Bash' && typeof toolInput.command === 'string') {
|
|
123
|
+
ctx.command = toolInput.command.slice(0, COMMAND_MAX_LEN);
|
|
124
|
+
ctx.args_redacted = parseArgsRedacted(toolInput.command);
|
|
125
|
+
}
|
|
126
|
+
} catch (_e) {}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
ctx.before_state = captureGitState();
|
|
130
|
+
} catch (_e) {}
|
|
131
|
+
|
|
132
|
+
return ctx;
|
|
133
|
+
} catch (_e) {
|
|
134
|
+
return ctx;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { captureToolContext };
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sanctix/client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sanctix governed edge client. Installs governance hooks for Claude Code, Codex and Cursor. Connects to the Sanctix control plane.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sanctix": "bin/sanctix.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"cli/",
|
|
12
|
+
"hooks/",
|
|
13
|
+
"templates/",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"commander": "^12.0.0",
|
|
19
|
+
"inquirer": "^9.0.0",
|
|
20
|
+
"chalk": "^5.0.0",
|
|
21
|
+
"fs-extra": "^11.0.0"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"agent-governance",
|
|
28
|
+
"ai-agents",
|
|
29
|
+
"trust",
|
|
30
|
+
"audit",
|
|
31
|
+
"claude-code",
|
|
32
|
+
"codex",
|
|
33
|
+
"cursor"
|
|
34
|
+
],
|
|
35
|
+
"author": "Ruvoni Inc.",
|
|
36
|
+
"license": "MIT"
|
|
37
|
+
}
|
|
File without changes
|