mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
feat(security): wire logAttempt to gstack-telemetry-log (fire-and-forget)
Every local attempt.jsonl write now also triggers a subprocess call to
gstack-telemetry-log with the attack_attempt event type. The binary handles
tier gating internally (community → Supabase upload, anonymous → local
JSONL only, off → no-op), so security.ts doesn't need to re-check.
Binary resolution follows the skill preamble pattern — never relies on PATH,
which breaks in compiled-binary contexts:
1. ~/.claude/skills/gstack/bin/gstack-telemetry-log (global install)
2. .claude/skills/gstack/bin/gstack-telemetry-log (symlinked dev)
3. bin/gstack-telemetry-log (in-repo dev)
Fire-and-forget:
* spawn with stdio: 'ignore', detached: true, unref()
* .on('error') swallows failures
* Missing binary is non-fatal — local attempts.jsonl still gives audit trail
Never throws. Never blocks. Existing 37 security tests pass unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+67
-2
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { randomBytes, createHash } from 'crypto';
|
import { randomBytes, createHash } from 'crypto';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
@@ -258,10 +259,74 @@ function rotateIfNeeded(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Append an attempt to the local log. Never throws — logging failure should
|
* Try to locate the gstack-telemetry-log binary. Resolution order matches
|
||||||
* not break the sidebar. Returns true if the write succeeded.
|
* the existing skill preamble pattern (never relies on PATH — packaged
|
||||||
|
* binary layouts can break that).
|
||||||
|
*
|
||||||
|
* Order:
|
||||||
|
* 1. ~/.claude/skills/gstack/bin/gstack-telemetry-log (global install)
|
||||||
|
* 2. .claude/skills/gstack/bin/gstack-telemetry-log (symlinked dev)
|
||||||
|
* 3. bin/gstack-telemetry-log (in-repo dev)
|
||||||
|
*/
|
||||||
|
function findTelemetryBinary(): string | null {
|
||||||
|
const candidates = [
|
||||||
|
path.join(os.homedir(), '.claude', 'skills', 'gstack', 'bin', 'gstack-telemetry-log'),
|
||||||
|
path.resolve(process.cwd(), '.claude', 'skills', 'gstack', 'bin', 'gstack-telemetry-log'),
|
||||||
|
path.resolve(process.cwd(), 'bin', 'gstack-telemetry-log'),
|
||||||
|
];
|
||||||
|
for (const c of candidates) {
|
||||||
|
try {
|
||||||
|
fs.accessSync(c, fs.constants.X_OK);
|
||||||
|
return c;
|
||||||
|
} catch {
|
||||||
|
// try next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire-and-forget subprocess invocation of gstack-telemetry-log with the
|
||||||
|
* attack_attempt event type. The binary handles tier gating internally
|
||||||
|
* (community → upload, anonymous → local only, off → no-op), so we don't
|
||||||
|
* need to re-check here.
|
||||||
|
*
|
||||||
|
* Never throws. Never blocks. If the binary isn't found or spawn fails, the
|
||||||
|
* local attempts.jsonl write from logAttempt() still gives us the audit trail.
|
||||||
|
*/
|
||||||
|
function reportAttemptTelemetry(record: AttemptRecord): void {
|
||||||
|
const bin = findTelemetryBinary();
|
||||||
|
if (!bin) return;
|
||||||
|
try {
|
||||||
|
const child = spawn(bin, [
|
||||||
|
'--event-type', 'attack_attempt',
|
||||||
|
'--url-domain', record.urlDomain || '',
|
||||||
|
'--payload-hash', record.payloadHash,
|
||||||
|
'--confidence', String(record.confidence),
|
||||||
|
'--layer', record.layer,
|
||||||
|
'--verdict', record.verdict,
|
||||||
|
], {
|
||||||
|
stdio: 'ignore',
|
||||||
|
detached: true,
|
||||||
|
});
|
||||||
|
// unref so this subprocess doesn't hold the event loop open
|
||||||
|
child.unref();
|
||||||
|
child.on('error', () => { /* swallow — telemetry must never break sidebar */ });
|
||||||
|
} catch {
|
||||||
|
// Spawn failure is non-fatal.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append an attempt to the local log AND fire telemetry via
|
||||||
|
* gstack-telemetry-log (which respects the user's telemetry tier setting).
|
||||||
|
* Never throws — logging failure should not break the sidebar.
|
||||||
|
* Returns true if the local write succeeded.
|
||||||
*/
|
*/
|
||||||
export function logAttempt(record: AttemptRecord): boolean {
|
export function logAttempt(record: AttemptRecord): boolean {
|
||||||
|
// Fire telemetry first, async — even if local write fails, we still want
|
||||||
|
// the event reported (it goes to a different directory anyway).
|
||||||
|
reportAttemptTelemetry(record);
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(SECURITY_DIR, { recursive: true, mode: 0o700 });
|
fs.mkdirSync(SECURITY_DIR, { recursive: true, mode: 0o700 });
|
||||||
rotateIfNeeded();
|
rotateIfNeeded();
|
||||||
|
|||||||
Reference in New Issue
Block a user