Files
gstack/test/audit-compliance.test.ts
Garry Tan 3cda8deec9 fix: security audit round 2 (v0.13.4.0) (#640)
* fix: chrome-cdp localhost-only binding

Restrict Chrome CDP to localhost by adding --remote-debugging-address=127.0.0.1
and --remote-allow-origins to prevent network-accessible debugging sessions.

Clears 1 Socket anomaly (Chrome CDP session exposure).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: extension sender validation + message type allowlist

Add sender.id check and ALLOWED_TYPES allowlist to the Chrome extension's
message handler. Defense-in-depth against message spoofing from external
extensions or future externally_connectable changes.

Clears 2 Socket anomalies (extension permissions).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: checksum-verified bun install

Replace unverified curl|bash bun installation with checksum-verified
download-then-execute pattern. The install script is downloaded, sha256
verified against a known hash, then executed. Preserves the Bun-native
install path without adding a Node/npm dependency.

Clears Snyk W012 + 3 Socket anomalies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: content trust boundary markers in browse output

Wrap page-content commands (text, html, links, forms, accessibility,
console, dialog, snapshot) with --- BEGIN/END UNTRUSTED EXTERNAL CONTENT ---
markers. Covers direct commands (server.ts), chain sub-commands, and
snapshot output (meta-commands.ts).

Adds PAGE_CONTENT_COMMANDS set and wrapUntrustedContent() helper in
commands.ts (single source of truth, DRY). Expands the SKILL.md trust
warning with explicit processing rules for agents.

Clears Snyk W011 (third-party content exposure).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: harden trust boundary markers against escape attacks

- Sanitize URLs in markers (remove newlines, cap at 200 chars) to prevent
  marker injection via history.pushState
- Escape marker strings in content (zero-width space) so malicious pages
  can't forge the END marker to break out of the untrusted block
- Wrap resume command snapshot with trust boundary markers
- Wrap diff command output with trust boundary markers
- Wrap watch stop last snapshot with trust boundary markers

Found by cross-model adversarial review (Claude + Codex).

* chore: bump version and changelog (v0.13.4.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: gitignore .factory/ and remove from tracking

Factory Droid support was removed in this branch. The .factory/ directory
was re-added by merging main (which had v0.13.5.0 Factory support).
Gitignore it so it stays out.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:46:33 -06:00

116 lines
5.2 KiB
TypeScript

import { describe, test, expect } from 'bun:test';
import { readFileSync, readdirSync, existsSync } from 'fs';
import { join } from 'path';
const ROOT = join(import.meta.dir, '..');
function getAllSkillMds(): Array<{ name: string; content: string }> {
const results: Array<{ name: string; content: string }> = [];
const rootPath = join(ROOT, 'SKILL.md');
if (existsSync(rootPath)) {
results.push({ name: 'root', content: readFileSync(rootPath, 'utf-8') });
}
for (const entry of readdirSync(ROOT, { withFileTypes: true })) {
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
const skillPath = join(ROOT, entry.name, 'SKILL.md');
if (existsSync(skillPath)) {
results.push({ name: entry.name, content: readFileSync(skillPath, 'utf-8') });
}
}
return results;
}
describe('Audit compliance', () => {
// Fix 1: W007 — No hardcoded credentials in documentation
test('no hardcoded credential patterns in SKILL.md.tmpl', () => {
const tmpl = readFileSync(join(ROOT, 'SKILL.md.tmpl'), 'utf-8');
expect(tmpl).not.toContain('"password123"');
expect(tmpl).not.toContain('"test@example.com"');
expect(tmpl).not.toContain('"test@test.com"');
expect(tmpl).toContain('$TEST_EMAIL');
expect(tmpl).toContain('$TEST_PASSWORD');
});
// Fix 2: Conditional telemetry — binary calls wrapped with existence check
test('preamble telemetry calls are conditional on _TEL and binary existence', () => {
const preamble = readFileSync(join(ROOT, 'scripts/resolvers/preamble.ts'), 'utf-8');
// Pending finalization must check _TEL and binary existence
expect(preamble).toContain('_TEL" != "off"');
expect(preamble).toContain('-x ');
expect(preamble).toContain('gstack-telemetry-log');
// End-of-skill telemetry must also be conditional
const completionIdx = preamble.indexOf('Telemetry (run last)');
expect(completionIdx).toBeGreaterThan(-1);
const completionSection = preamble.slice(completionIdx);
expect(completionSection).toContain('_TEL" != "off"');
});
// Round 2 Fix 1: W012 — Bun install uses checksum verification
test('bun install uses checksum-verified method', () => {
const browseResolver = readFileSync(join(ROOT, 'scripts/resolvers/browse.ts'), 'utf-8');
expect(browseResolver).toContain('shasum -a 256');
expect(browseResolver).toContain('BUN_INSTALL_SHA');
const setup = readFileSync(join(ROOT, 'setup'), 'utf-8');
// Setup error message should not have unverified curl|bash
const lines = setup.split('\n');
for (const line of lines) {
if (line.includes('bun.sh/install') && line.includes('| bash') && !line.includes('shasum')) {
throw new Error(`Unverified bun install found: ${line.trim()}`);
}
}
});
// Fix 4: W011 — Untrusted content warning in command reference
test('command reference includes untrusted content warning after Navigation', () => {
const rootSkill = readFileSync(join(ROOT, 'SKILL.md'), 'utf-8');
const navIdx = rootSkill.indexOf('### Navigation');
const readingIdx = rootSkill.indexOf('### Reading');
expect(navIdx).toBeGreaterThan(-1);
expect(readingIdx).toBeGreaterThan(navIdx);
const between = rootSkill.slice(navIdx, readingIdx);
expect(between.toLowerCase()).toContain('untrusted');
});
// Round 2 Fix 2: Trust boundary markers + helper + wrapping in all paths
test('browse wraps untrusted content with trust boundary markers', () => {
const commands = readFileSync(join(ROOT, 'browse/src/commands.ts'), 'utf-8');
expect(commands).toContain('PAGE_CONTENT_COMMANDS');
expect(commands).toContain('wrapUntrustedContent');
const server = readFileSync(join(ROOT, 'browse/src/server.ts'), 'utf-8');
expect(server).toContain('wrapUntrustedContent');
const meta = readFileSync(join(ROOT, 'browse/src/meta-commands.ts'), 'utf-8');
expect(meta).toContain('wrapUntrustedContent');
});
// Fix 5: Data flow documentation in review.ts
test('review.ts has data flow documentation', () => {
const review = readFileSync(join(ROOT, 'scripts/resolvers/review.ts'), 'utf-8');
expect(review).toContain('Data sent');
expect(review).toContain('Data NOT sent');
});
// Round 2 Fix 3: Extension sender validation + message type allowlist
test('extension background.js validates message sender', () => {
const bg = readFileSync(join(ROOT, 'extension/background.js'), 'utf-8');
expect(bg).toContain('sender.id !== chrome.runtime.id');
expect(bg).toContain('ALLOWED_TYPES');
});
// Round 2 Fix 4: Chrome CDP binds to localhost only
test('chrome-cdp binds to localhost only', () => {
const cdp = readFileSync(join(ROOT, 'bin/chrome-cdp'), 'utf-8');
expect(cdp).toContain('--remote-debugging-address=127.0.0.1');
expect(cdp).toContain('--remote-allow-origins=');
});
// Fix 2+6: All generated SKILL.md files with telemetry are conditional
test('all generated SKILL.md files with telemetry calls use conditional pattern', () => {
const skills = getAllSkillMds();
for (const { name, content } of skills) {
if (content.includes('gstack-telemetry-log')) {
expect(content).toContain('_TEL" != "off"');
}
}
});
});