@solongate/proxy 0.82.55 → 0.82.57
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/hooks/audit.mjs +14 -1
- package/hooks/guard.bundled.mjs +73 -4
- package/hooks/guard.mjs +59 -4
- package/package.json +1 -1
package/hooks/audit.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { homedir } from 'node:os';
|
|
|
11
11
|
// Bump on every audit hook change. The cloud serves the newest version; the guard
|
|
12
12
|
// hook installs it on its next run (no re-login needed). See guard.mjs
|
|
13
13
|
// fetchAndInstallHook / maybeSelfUpdate.
|
|
14
|
-
const HOOK_VERSION =
|
|
14
|
+
const HOOK_VERSION = 17;
|
|
15
15
|
|
|
16
16
|
function loadEnvKey(dir) {
|
|
17
17
|
try {
|
|
@@ -131,6 +131,15 @@ function ghostMatch(targetPath, patterns) {
|
|
|
131
131
|
continue;
|
|
132
132
|
}
|
|
133
133
|
if (re.test(norm)) return true;
|
|
134
|
+
// A path ghost must also hide the file when a tool lists it by BARE name —
|
|
135
|
+
// `ls dir/` prints just "secret-plan.txt" (no slash), which a slash-pattern
|
|
136
|
+
// like `*/secret-plan.txt` would otherwise miss. Match the pattern's last
|
|
137
|
+
// segment against the target's basename.
|
|
138
|
+
const patBase = pat.slice(pat.lastIndexOf('/') + 1);
|
|
139
|
+
if (patBase && patBase !== pat) {
|
|
140
|
+
const reBase = ghostGlobToRegExp(patBase);
|
|
141
|
+
if (reBase && reBase.test(base)) return true;
|
|
142
|
+
}
|
|
134
143
|
}
|
|
135
144
|
return false;
|
|
136
145
|
}
|
|
@@ -151,6 +160,10 @@ function ghostStripLines(text, pats) {
|
|
|
151
160
|
const trimmed = line.trim();
|
|
152
161
|
if (!trimmed) { kept.push(line); continue; }
|
|
153
162
|
if (ghostMatch(trimmed, pats)) continue; // full-path listing line
|
|
163
|
+
// grep -n / ripgrep output is `path:line:content` (or `path:content`) — the
|
|
164
|
+
// path is the first colon field. Drop the whole match line if it's a ghost.
|
|
165
|
+
const firstField = trimmed.split(':')[0];
|
|
166
|
+
if (firstField && firstField !== trimmed && ghostMatch(firstField, pats)) continue;
|
|
154
167
|
const toks = trimmed.split(/\s+/);
|
|
155
168
|
const anyHit = toks.some((t) => ghostMatch(ghostCleanToken(t), pats));
|
|
156
169
|
if (!anyHit) { kept.push(line); continue; }
|
package/hooks/guard.bundled.mjs
CHANGED
|
@@ -6536,7 +6536,7 @@ import { resolve, join, dirname, isAbsolute } from "node:path";
|
|
|
6536
6536
|
import { homedir } from "node:os";
|
|
6537
6537
|
import { gunzipSync } from "node:zlib";
|
|
6538
6538
|
import { createHash } from "node:crypto";
|
|
6539
|
-
var HOOK_VERSION =
|
|
6539
|
+
var HOOK_VERSION = 48;
|
|
6540
6540
|
function localLogsOnly(security) {
|
|
6541
6541
|
if (security && typeof security === "object") {
|
|
6542
6542
|
const l = security.localLogs;
|
|
@@ -7161,6 +7161,73 @@ function extractTargetPaths(args) {
|
|
|
7161
7161
|
}
|
|
7162
7162
|
return out;
|
|
7163
7163
|
}
|
|
7164
|
+
function ghostGlobToRe(glob) {
|
|
7165
|
+
let re = "";
|
|
7166
|
+
for (let i = 0; i < glob.length; i++) {
|
|
7167
|
+
const c = glob[i];
|
|
7168
|
+
if (c === "*") {
|
|
7169
|
+
if (glob[i + 1] === "*") {
|
|
7170
|
+
re += ".*";
|
|
7171
|
+
i++;
|
|
7172
|
+
} else
|
|
7173
|
+
re += "[^/]*";
|
|
7174
|
+
} else if (c === "?")
|
|
7175
|
+
re += "[^/]";
|
|
7176
|
+
else if ("\\^$.|+()[]{}".indexOf(c) !== -1)
|
|
7177
|
+
re += "\\" + c;
|
|
7178
|
+
else
|
|
7179
|
+
re += c;
|
|
7180
|
+
}
|
|
7181
|
+
try {
|
|
7182
|
+
return new RegExp("^" + re + "$");
|
|
7183
|
+
} catch {
|
|
7184
|
+
return null;
|
|
7185
|
+
}
|
|
7186
|
+
}
|
|
7187
|
+
function ghostPathMatch(p, patterns) {
|
|
7188
|
+
if (!p)
|
|
7189
|
+
return false;
|
|
7190
|
+
const norm = String(p).replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
7191
|
+
if (!norm)
|
|
7192
|
+
return false;
|
|
7193
|
+
const base = norm.split("/").filter(Boolean).pop() || norm;
|
|
7194
|
+
for (let pat of patterns) {
|
|
7195
|
+
pat = String(pat || "").trim().toLowerCase();
|
|
7196
|
+
if (!pat)
|
|
7197
|
+
continue;
|
|
7198
|
+
const re = ghostGlobToRe(pat);
|
|
7199
|
+
if (re && (re.test(norm) || re.test(base)))
|
|
7200
|
+
return true;
|
|
7201
|
+
const patBase = pat.slice(pat.lastIndexOf("/") + 1);
|
|
7202
|
+
if (patBase && patBase !== pat) {
|
|
7203
|
+
const rb = ghostGlobToRe(patBase);
|
|
7204
|
+
if (rb && rb.test(base))
|
|
7205
|
+
return true;
|
|
7206
|
+
}
|
|
7207
|
+
}
|
|
7208
|
+
return false;
|
|
7209
|
+
}
|
|
7210
|
+
function ghostCheck(toolName, args, sec) {
|
|
7211
|
+
try {
|
|
7212
|
+
const pats = sec && sec.ghost && Array.isArray(sec.ghost.patterns) ? sec.ghost.patterns : [];
|
|
7213
|
+
if (!pats.length)
|
|
7214
|
+
return null;
|
|
7215
|
+
for (const p of extractTargetPaths(args)) {
|
|
7216
|
+
if (ghostPathMatch(p, pats))
|
|
7217
|
+
return String(p) + ": No such file or directory";
|
|
7218
|
+
}
|
|
7219
|
+
for (const cmd of extractCommands(args)) {
|
|
7220
|
+
const c = String(cmd || "").toLowerCase();
|
|
7221
|
+
for (let pat of pats) {
|
|
7222
|
+
const patBase = String(pat || "").trim().toLowerCase().slice(String(pat).lastIndexOf("/") + 1).replace(/[*?]/g, "");
|
|
7223
|
+
if (patBase && patBase.length > 2 && c.includes(patBase))
|
|
7224
|
+
return patBase + ": No such file or directory";
|
|
7225
|
+
}
|
|
7226
|
+
}
|
|
7227
|
+
} catch {
|
|
7228
|
+
}
|
|
7229
|
+
return null;
|
|
7230
|
+
}
|
|
7164
7231
|
function tamperCheck(toolName, args) {
|
|
7165
7232
|
const tn = String(toolName || "").toLowerCase();
|
|
7166
7233
|
const isExec = TAMPER_GUARD_TOOLS_EXEC.has(tn) || /bash|shell|exec|powershell|cmd|run|eval/.test(tn);
|
|
@@ -7830,12 +7897,14 @@ if (!REFRESH_MODE) {
|
|
|
7830
7897
|
} catch {
|
|
7831
7898
|
}
|
|
7832
7899
|
const _tr = _cacheOk && _selfProt ? tamperCheck(toolName, args) : null;
|
|
7833
|
-
|
|
7900
|
+
const _gr = !_tr && _cacheOk ? ghostCheck(toolName, args, _sec) : null;
|
|
7901
|
+
const _deny = _tr || _gr;
|
|
7902
|
+
if (_deny) {
|
|
7834
7903
|
const _logEntry = {
|
|
7835
7904
|
tool: toolName,
|
|
7836
7905
|
arguments: args,
|
|
7837
7906
|
decision: "DENY",
|
|
7838
|
-
reason:
|
|
7907
|
+
reason: _deny,
|
|
7839
7908
|
permission: guessPermission(toolName),
|
|
7840
7909
|
source: `${AGENT_TYPE}-guard`,
|
|
7841
7910
|
agent_id: AGENT_TYPE,
|
|
@@ -7860,7 +7929,7 @@ if (!REFRESH_MODE) {
|
|
|
7860
7929
|
process.stderr.write(`[SolonGate ROUTE] BLACK (block)
|
|
7861
7930
|
`);
|
|
7862
7931
|
writeDenyFlag(toolName);
|
|
7863
|
-
blockTool(
|
|
7932
|
+
blockTool(_deny);
|
|
7864
7933
|
}
|
|
7865
7934
|
} catch (_e) {
|
|
7866
7935
|
if (_e === SG_DONE)
|
package/hooks/guard.mjs
CHANGED
|
@@ -32,7 +32,7 @@ import { createHash } from 'node:crypto';
|
|
|
32
32
|
// the installed hook self-updates when the cloud version is higher (see
|
|
33
33
|
// maybeSelfUpdate). This is what makes guard fixes propagate without a manual
|
|
34
34
|
// reinstall — the same trust model as the OPA WASM this hook already runs.
|
|
35
|
-
const HOOK_VERSION =
|
|
35
|
+
const HOOK_VERSION = 48;
|
|
36
36
|
|
|
37
37
|
// True when local log storage is ON. In that mode logs are kept LOCAL ONLY and
|
|
38
38
|
// nothing is sent to the cloud audit log.
|
|
@@ -874,6 +874,57 @@ function extractTargetPaths(args) {
|
|
|
874
874
|
return out;
|
|
875
875
|
}
|
|
876
876
|
|
|
877
|
+
// Ghost paths at PreToolUse: deny any tool call that DIRECTLY targets a hidden
|
|
878
|
+
// path (read/cat/edit/open it). This is the only ghost enforcement possible
|
|
879
|
+
// without rewriting tool OUTPUT — which needs a PostToolUse hook that Antigravity
|
|
880
|
+
// does not run — so a plain `ls dir/` listing (which never names the file) can't
|
|
881
|
+
// be hidden here; only direct access is. On Claude Code the PostToolUse audit
|
|
882
|
+
// additionally strips listings. Deny reason mimics a missing file.
|
|
883
|
+
function ghostGlobToRe(glob) {
|
|
884
|
+
let re = '';
|
|
885
|
+
for (let i = 0; i < glob.length; i++) {
|
|
886
|
+
const c = glob[i];
|
|
887
|
+
if (c === '*') { if (glob[i + 1] === '*') { re += '.*'; i++; } else re += '[^/]*'; }
|
|
888
|
+
else if (c === '?') re += '[^/]';
|
|
889
|
+
else if ('\\^$.|+()[]{}'.indexOf(c) !== -1) re += '\\' + c;
|
|
890
|
+
else re += c;
|
|
891
|
+
}
|
|
892
|
+
try { return new RegExp('^' + re + '$'); } catch { return null; }
|
|
893
|
+
}
|
|
894
|
+
function ghostPathMatch(p, patterns) {
|
|
895
|
+
if (!p) return false;
|
|
896
|
+
const norm = String(p).replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
897
|
+
if (!norm) return false;
|
|
898
|
+
const base = norm.split('/').filter(Boolean).pop() || norm;
|
|
899
|
+
for (let pat of patterns) {
|
|
900
|
+
pat = String(pat || '').trim().toLowerCase();
|
|
901
|
+
if (!pat) continue;
|
|
902
|
+
const re = ghostGlobToRe(pat);
|
|
903
|
+
if (re && (re.test(norm) || re.test(base))) return true;
|
|
904
|
+
const patBase = pat.slice(pat.lastIndexOf('/') + 1);
|
|
905
|
+
if (patBase && patBase !== pat) { const rb = ghostGlobToRe(patBase); if (rb && rb.test(base)) return true; }
|
|
906
|
+
}
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
function ghostCheck(toolName, args, sec) {
|
|
910
|
+
try {
|
|
911
|
+
const pats = sec && sec.ghost && Array.isArray(sec.ghost.patterns) ? sec.ghost.patterns : [];
|
|
912
|
+
if (!pats.length) return null;
|
|
913
|
+
for (const p of extractTargetPaths(args)) {
|
|
914
|
+
if (ghostPathMatch(p, pats)) return String(p) + ': No such file or directory';
|
|
915
|
+
}
|
|
916
|
+
// A shell command that names the hidden file (cat/less/head/open <path>).
|
|
917
|
+
for (const cmd of extractCommands(args)) {
|
|
918
|
+
const c = String(cmd || '').toLowerCase();
|
|
919
|
+
for (let pat of pats) {
|
|
920
|
+
const patBase = String(pat || '').trim().toLowerCase().slice(String(pat).lastIndexOf('/') + 1).replace(/[*?]/g, '');
|
|
921
|
+
if (patBase && patBase.length > 2 && c.includes(patBase)) return patBase + ': No such file or directory';
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
} catch {}
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
|
|
877
928
|
function tamperCheck(toolName, args) {
|
|
878
929
|
const tn = String(toolName || '').toLowerCase();
|
|
879
930
|
const isExec = TAMPER_GUARD_TOOLS_EXEC.has(tn) || /bash|shell|exec|powershell|cmd|run|eval/.test(tn);
|
|
@@ -1675,10 +1726,14 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
|
|
|
1675
1726
|
// localLogs config). On a cache miss, fall through to the full flow, which
|
|
1676
1727
|
// resolves the policy properly and still blocks tamper.
|
|
1677
1728
|
const _tr = (_cacheOk && _selfProt) ? tamperCheck(toolName, args) : null;
|
|
1678
|
-
|
|
1729
|
+
// Ghost is a separate layer (not gated by selfProtect): deny direct access
|
|
1730
|
+
// to a hidden path here so it works even where PostToolUse never runs (agy).
|
|
1731
|
+
const _gr = (!_tr && _cacheOk) ? ghostCheck(toolName, args, _sec) : null;
|
|
1732
|
+
const _deny = _tr || _gr;
|
|
1733
|
+
if (_deny) {
|
|
1679
1734
|
const _logEntry = {
|
|
1680
1735
|
tool: toolName, arguments: args,
|
|
1681
|
-
decision: 'DENY', reason:
|
|
1736
|
+
decision: 'DENY', reason: _deny,
|
|
1682
1737
|
permission: guessPermission(toolName),
|
|
1683
1738
|
source: `${AGENT_TYPE}-guard`,
|
|
1684
1739
|
agent_id: AGENT_TYPE, agent_name: AGENT_NAME,
|
|
@@ -1696,7 +1751,7 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
|
|
|
1696
1751
|
} catch {}
|
|
1697
1752
|
process.stderr.write(`[SolonGate ROUTE] BLACK (block)\n`);
|
|
1698
1753
|
writeDenyFlag(toolName);
|
|
1699
|
-
blockTool(
|
|
1754
|
+
blockTool(_deny); // throws SG_DONE — skips the slow policy path entirely
|
|
1700
1755
|
}
|
|
1701
1756
|
} catch (_e) { if (_e === SG_DONE) throw _e; /* else: fall through to full flow */ }
|
|
1702
1757
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.82.
|
|
3
|
+
"version": "0.82.57",
|
|
4
4
|
"description": "AI tool security proxy: protect any AI tool server with customizable policies, path/command constraints, rate limiting, and audit logging. No code changes required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|