mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-17 18:32:19 +02:00
fix(security): delete dead exports the ripped chat path left behind
Three-way split by importer class: (a) Zero importers, deleted: the whole attack-attempt logging cluster in security.ts (logAttempt, AttemptRecord, salted hashPayload + device-salt, attempts.jsonl rotation, telemetry spawn plumbing incl. buildTelemetrySpawnCommand/resolveBashBinary — the LIVE attempts.jsonl writer is tunnel-denial-log.ts with its own rotation); the decision-file handshake (writeDecision/readDecision/clearDecision/excerptForReview — written for sidebar-agent's poll loop, which no longer exists); sidebar-utils.ts (whole module — its sanitizeExtensionUrl 'sanitized before embedding in a prompt' for the deleted prompt builder); 8 dead server.ts imports (sanitizeExtensionUrl, generateCanary, injectCanary, writeDecision, rotateRoot, serializeRegistry, restoreRegistry, clearAgentRecord); buildPtyClearCookie + buildSseClearCookie; WEBDRIVER_MASK_SCRIPT (orphaned by the D7 stealth narrowing — applyStealth never used it). (b) Dead-pin tests edited with their exports: the 'still exported' pin in stealth-layer-c, the string-content describe in stealth-webdriver (its live applyStealth behavioral coverage untouched), the clear-cookie assertions, security-review-flow.test.ts deleted whole (all 4 describes exercised the dead decision mechanism, incl. a 'simulated sidebar-agent poll loop'). (c) KEPT deliberately: leaseCount (live behavioral coverage), extractPtyCookie + validatePtySessionToken (extractPtyCookie is adopted by the terminal-agent cookie-parse unification later in this wave), resetSessionMarker + clearContentFilters (test-support API for the live content-security layer). Also fixes two pre-existing red pins found while here, invisible until the free suite got a CI job: the v1.44 spawnClaude->maybeSpawnPty rename in terminal-agent.test.ts, and a cross-file test-isolation bug where content-security.test.ts's clearContentFilters() wiped the auto-registered url-blocklist filter for every later file in the same bun process (security-integration.test.ts failed on co-run; afterAll now restores it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
44d58aa6ee
commit
ef186cccb3
@@ -14,14 +14,10 @@ import {
|
||||
generateCanary,
|
||||
injectCanary,
|
||||
checkCanaryInStructure,
|
||||
hashPayload,
|
||||
logAttempt,
|
||||
writeSessionState,
|
||||
readSessionState,
|
||||
getStatus,
|
||||
extractDomain,
|
||||
buildTelemetrySpawnCommand,
|
||||
resolveBashBinary,
|
||||
type LayerSignal,
|
||||
} from '../src/security';
|
||||
|
||||
@@ -239,46 +235,9 @@ describe('canary', () => {
|
||||
|
||||
// ─── Payload hashing ─────────────────────────────────────────
|
||||
|
||||
describe('hashPayload', () => {
|
||||
test('same payload produces same hash (deterministic with persistent salt)', () => {
|
||||
const h1 = hashPayload('attack string');
|
||||
const h2 = hashPayload('attack string');
|
||||
expect(h1).toBe(h2);
|
||||
});
|
||||
|
||||
test('different payloads produce different hashes', () => {
|
||||
expect(hashPayload('a')).not.toBe(hashPayload('b'));
|
||||
});
|
||||
|
||||
test('hash is sha256 hex (64 chars)', () => {
|
||||
const h = hashPayload('test');
|
||||
expect(h).toMatch(/^[0-9a-f]{64}$/);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Attack log + rotation ───────────────────────────────────
|
||||
|
||||
describe('logAttempt', () => {
|
||||
test('writes attempts.jsonl with correct shape', () => {
|
||||
const ok = logAttempt({
|
||||
ts: '2026-04-19T12:34:56Z',
|
||||
urlDomain: 'example.com',
|
||||
payloadHash: 'deadbeef',
|
||||
confidence: 0.9,
|
||||
layer: 'testsavant_content',
|
||||
verdict: 'block',
|
||||
});
|
||||
expect(ok).toBe(true);
|
||||
|
||||
const logPath = path.join(os.homedir(), '.gstack', 'security', 'attempts.jsonl');
|
||||
const content = fs.readFileSync(logPath, 'utf8');
|
||||
const lines = content.split('\n').filter(Boolean);
|
||||
const last = JSON.parse(lines[lines.length - 1]);
|
||||
expect(last.urlDomain).toBe('example.com');
|
||||
expect(last.payloadHash).toBe('deadbeef');
|
||||
expect(last.verdict).toBe('block');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Session state (cross-process, atomic) ───────────────────
|
||||
|
||||
@@ -330,74 +289,6 @@ describe('extractDomain', () => {
|
||||
|
||||
// ─── Bash binary resolution (Windows shebang-script invocation) ─────
|
||||
|
||||
describe('resolveBashBinary', () => {
|
||||
test('on POSIX, returns the system bash via Bun.which', () => {
|
||||
if (process.platform === 'win32') return;
|
||||
const out = resolveBashBinary({ PATH: process.env.PATH ?? '' });
|
||||
expect(out).toBeTruthy();
|
||||
expect(out!.endsWith('bash')).toBe(true);
|
||||
});
|
||||
|
||||
test('honors GSTACK_BASH_BIN absolute-path override', () => {
|
||||
// Construct a synthetic absolute path; the helper short-circuits on
|
||||
// path.isAbsolute and never touches the filesystem, so this is portable.
|
||||
const fake = process.platform === 'win32' ? 'C:\\opt\\bash.exe' : '/opt/custom/bash';
|
||||
const out = resolveBashBinary({ GSTACK_BASH_BIN: fake, PATH: '' });
|
||||
expect(out).toBe(fake);
|
||||
});
|
||||
|
||||
test('strips wrapping double quotes from override values', () => {
|
||||
const fake = process.platform === 'win32' ? 'C:\\opt\\bash.exe' : '/opt/custom/bash';
|
||||
const out = resolveBashBinary({ GSTACK_BASH_BIN: `"${fake}"`, PATH: '' });
|
||||
expect(out).toBe(fake);
|
||||
});
|
||||
|
||||
test('BASH_BIN works as a fallback when GSTACK_BASH_BIN is unset', () => {
|
||||
const fake = process.platform === 'win32' ? 'C:\\opt\\bash.exe' : '/opt/custom/bash';
|
||||
const out = resolveBashBinary({ BASH_BIN: fake, PATH: '' });
|
||||
expect(out).toBe(fake);
|
||||
});
|
||||
|
||||
test('returns null when nothing resolves (override is unset and PATH is empty)', () => {
|
||||
// Empty PATH means Bun.which finds nothing.
|
||||
const out = resolveBashBinary({ PATH: '' });
|
||||
expect(out).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Telemetry spawn command (Windows bash wrapper, v1.24-aligned) ──
|
||||
|
||||
describe('buildTelemetrySpawnCommand', () => {
|
||||
const bin = '/home/user/.claude/skills/gstack/bin/gstack-telemetry-log';
|
||||
const args = ['--event-type', 'attack_attempt', '--confidence', '0.95'];
|
||||
|
||||
test('on POSIX, returns the binary path and args unchanged', () => {
|
||||
if (process.platform === 'win32') return;
|
||||
const out = buildTelemetrySpawnCommand(bin, args);
|
||||
expect(out).not.toBeNull();
|
||||
expect(out!.cmd).toBe(bin);
|
||||
expect(out!.cmdArgs).toEqual(args);
|
||||
});
|
||||
|
||||
test('on win32 with bash resolvable, wraps the call in bash with the script as first arg', () => {
|
||||
if (process.platform !== 'win32') return;
|
||||
const fakeBash = 'C:\\Program Files\\Git\\bin\\bash.exe';
|
||||
const out = buildTelemetrySpawnCommand(bin, args, { GSTACK_BASH_BIN: fakeBash, PATH: '' });
|
||||
expect(out).not.toBeNull();
|
||||
expect(out!.cmd).toBe(fakeBash);
|
||||
expect(out!.cmdArgs).toEqual([bin, ...args]);
|
||||
});
|
||||
|
||||
test('on win32 with bash unresolvable, returns null so caller skips spawn', () => {
|
||||
if (process.platform !== 'win32') return;
|
||||
// No override, empty PATH — Bun.which finds nothing on Windows.
|
||||
const out = buildTelemetrySpawnCommand(bin, args, { PATH: '' });
|
||||
expect(out).toBeNull();
|
||||
});
|
||||
|
||||
test('does not mutate the caller-supplied args array', () => {
|
||||
const originalArgs = [...args];
|
||||
buildTelemetrySpawnCommand(bin, args);
|
||||
expect(args).toEqual(originalArgs);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user