SuperCharge Claude Code v1.0.0 - Complete Customization Package
Features: - 30+ Custom Skills (cognitive, development, UI/UX, autonomous agents) - RalphLoop autonomous agent integration - Multi-AI consultation (Qwen) - Agent management system with sync capabilities - Custom hooks for session management - MCP servers integration - Plugin marketplace setup - Comprehensive installation script Components: - Skills: always-use-superpowers, ralph, brainstorming, ui-ux-pro-max, etc. - Agents: 100+ agents across engineering, marketing, product, etc. - Hooks: session-start-superpowers, qwen-consult, ralph-auto-trigger - Commands: /brainstorm, /write-plan, /execute-plan - MCP Servers: zai-mcp-server, web-search-prime, web-reader, zread - Binaries: ralphloop wrapper Installation: ./supercharge.sh
This commit is contained in:
276
plugins/claude-code-safety-net/tests/audit.test.ts
Normal file
276
plugins/claude-code-safety-net/tests/audit.test.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { redactSecrets, sanitizeSessionIdForFilename, writeAuditLog } from '../src/core/audit.ts';
|
||||
import type { AuditLogEntry } from '../src/types.ts';
|
||||
|
||||
describe('sanitizeSessionIdForFilename', () => {
|
||||
test('returns valid session id unchanged', () => {
|
||||
expect(sanitizeSessionIdForFilename('test-session-123')).toBe('test-session-123');
|
||||
});
|
||||
|
||||
test('replaces invalid characters with underscores', () => {
|
||||
expect(sanitizeSessionIdForFilename('test/session')).toBe('test_session');
|
||||
expect(sanitizeSessionIdForFilename('test\\session')).toBe('test_session');
|
||||
expect(sanitizeSessionIdForFilename('test:session')).toBe('test_session');
|
||||
});
|
||||
|
||||
test('strips leading/trailing special chars', () => {
|
||||
expect(sanitizeSessionIdForFilename('.session')).toBe('session');
|
||||
expect(sanitizeSessionIdForFilename('session.')).toBe('session');
|
||||
expect(sanitizeSessionIdForFilename('-session-')).toBe('session');
|
||||
expect(sanitizeSessionIdForFilename('_session_')).toBe('session');
|
||||
});
|
||||
|
||||
test('returns null for empty or invalid input', () => {
|
||||
expect(sanitizeSessionIdForFilename('')).toBeNull();
|
||||
expect(sanitizeSessionIdForFilename(' ')).toBeNull();
|
||||
expect(sanitizeSessionIdForFilename('...')).toBeNull();
|
||||
expect(sanitizeSessionIdForFilename('..')).toBeNull();
|
||||
expect(sanitizeSessionIdForFilename('.')).toBeNull();
|
||||
});
|
||||
|
||||
test('truncates long session ids', () => {
|
||||
const longId = 'a'.repeat(200);
|
||||
const result = sanitizeSessionIdForFilename(longId);
|
||||
expect(result?.length).toBeLessThanOrEqual(128);
|
||||
});
|
||||
|
||||
test('handles path traversal attempts', () => {
|
||||
const result = sanitizeSessionIdForFilename('../../etc/passwd');
|
||||
expect(result).not.toContain('/');
|
||||
expect(result).not.toContain('..');
|
||||
});
|
||||
});
|
||||
|
||||
describe('redactSecrets', () => {
|
||||
test('redacts TOKEN=value patterns', () => {
|
||||
const result = redactSecrets('TOKEN=secret123 git reset --hard');
|
||||
expect(result).toContain('<redacted>');
|
||||
expect(result).not.toContain('secret123');
|
||||
});
|
||||
|
||||
test('redacts API_KEY patterns', () => {
|
||||
const result = redactSecrets('API_KEY=mysecretkey');
|
||||
expect(result).toContain('<redacted>');
|
||||
expect(result).not.toContain('mysecretkey');
|
||||
});
|
||||
|
||||
test('redacts GitHub tokens', () => {
|
||||
const result = redactSecrets('ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
|
||||
expect(result).toBe('<redacted>');
|
||||
});
|
||||
|
||||
test('redacts URL credentials', () => {
|
||||
const result = redactSecrets('https://user:password@example.com');
|
||||
expect(result).not.toContain('password');
|
||||
expect(result).toContain('<redacted>');
|
||||
});
|
||||
|
||||
test('preserves non-secret content', () => {
|
||||
const result = redactSecrets('git reset --hard');
|
||||
expect(result).toBe('git reset --hard');
|
||||
});
|
||||
|
||||
test('redacts Authorization Bearer token', () => {
|
||||
const result = redactSecrets('curl -H "Authorization: Bearer abc123" https://example.com');
|
||||
expect(result).not.toContain('abc123');
|
||||
expect(result).toContain('<redacted>');
|
||||
});
|
||||
|
||||
test('redacts Authorization Basic token', () => {
|
||||
const result = redactSecrets("curl -H 'Authorization: Basic abc123' https://example.com");
|
||||
expect(result).not.toContain('abc123');
|
||||
expect(result).toContain('<redacted>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeAuditLog', () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = join(
|
||||
tmpdir(),
|
||||
`safety-net-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
);
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (existsSync(testDir)) {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function getLogFile(sessionId: string): string {
|
||||
return join(testDir, '.cc-safety-net', 'logs', `${sessionId}.jsonl`);
|
||||
}
|
||||
|
||||
function readLogEntries(sessionId: string): AuditLogEntry[] {
|
||||
const logFile = getLogFile(sessionId);
|
||||
if (!existsSync(logFile)) {
|
||||
return [];
|
||||
}
|
||||
const content = readFileSync(logFile, 'utf-8');
|
||||
return content
|
||||
.split('\n')
|
||||
.filter((line) => line.trim())
|
||||
.map((line) => JSON.parse(line) as AuditLogEntry);
|
||||
}
|
||||
|
||||
test('denied command creates log entry', () => {
|
||||
const sessionId = 'test-session-123';
|
||||
writeAuditLog(
|
||||
sessionId,
|
||||
'git reset --hard',
|
||||
'git reset --hard',
|
||||
'git reset --hard destroys uncommitted changes',
|
||||
'/home/user/project',
|
||||
{ homeDir: testDir },
|
||||
);
|
||||
|
||||
const entries = readLogEntries(sessionId);
|
||||
expect(entries.length).toBe(1);
|
||||
expect(entries[0]?.command).toContain('git reset --hard');
|
||||
});
|
||||
|
||||
test('log format has correct fields', () => {
|
||||
const sessionId = 'test-session-789';
|
||||
writeAuditLog(
|
||||
sessionId,
|
||||
'git reset --hard',
|
||||
'git reset --hard',
|
||||
'git reset --hard destroys uncommitted changes',
|
||||
'/home/user/project',
|
||||
{ homeDir: testDir },
|
||||
);
|
||||
|
||||
const entries = readLogEntries(sessionId);
|
||||
expect(entries.length).toBe(1);
|
||||
|
||||
expect(entries[0]).toHaveProperty('ts');
|
||||
expect(entries[0]).toHaveProperty('command');
|
||||
expect(entries[0]).toHaveProperty('segment');
|
||||
expect(entries[0]).toHaveProperty('reason');
|
||||
expect(entries[0]).toHaveProperty('cwd');
|
||||
|
||||
expect(entries[0]?.cwd).toBe('/home/user/project');
|
||||
expect(entries[0]?.reason).toContain('git reset --hard');
|
||||
});
|
||||
|
||||
test('log redacts secrets', () => {
|
||||
const sessionId = 'test-session-redact';
|
||||
writeAuditLog(
|
||||
sessionId,
|
||||
'TOKEN=secret123 git reset --hard',
|
||||
'TOKEN=secret123 git reset --hard',
|
||||
'git reset --hard destroys uncommitted changes',
|
||||
null,
|
||||
{ homeDir: testDir },
|
||||
);
|
||||
|
||||
const entries = readLogEntries(sessionId);
|
||||
expect(entries.length).toBe(1);
|
||||
expect(entries[0]?.command).not.toContain('secret123');
|
||||
expect(entries[0]?.command).toContain('<redacted>');
|
||||
});
|
||||
|
||||
test('missing session id creates no log', () => {
|
||||
// Empty session ID
|
||||
writeAuditLog('', 'git reset --hard', 'git reset --hard', 'reason', null, {
|
||||
homeDir: testDir,
|
||||
});
|
||||
|
||||
const logsDir = join(testDir, '.cc-safety-net', 'logs');
|
||||
if (existsSync(logsDir)) {
|
||||
const files = readdirSync(logsDir);
|
||||
expect(files.length).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('multiple denials append to same log', () => {
|
||||
const sessionId = 'test-session-multi';
|
||||
writeAuditLog(sessionId, 'git reset --hard', 'git reset --hard', 'reason1', null, {
|
||||
homeDir: testDir,
|
||||
});
|
||||
writeAuditLog(sessionId, 'git clean -f', 'git clean -f', 'reason2', null, {
|
||||
homeDir: testDir,
|
||||
});
|
||||
writeAuditLog(sessionId, 'rm -rf /', 'rm -rf /', 'reason3', null, {
|
||||
homeDir: testDir,
|
||||
});
|
||||
|
||||
const entries = readLogEntries(sessionId);
|
||||
expect(entries.length).toBe(3);
|
||||
expect(entries[0]?.command).toContain('git reset --hard');
|
||||
expect(entries[1]?.command).toContain('git clean -f');
|
||||
expect(entries[2]?.command).toContain('rm -rf /');
|
||||
});
|
||||
|
||||
test('session id path traversal does not escape logs dir', () => {
|
||||
const sessionId = '../../outside';
|
||||
writeAuditLog(sessionId, 'git reset --hard', 'git reset --hard', 'reason', null, {
|
||||
homeDir: testDir,
|
||||
});
|
||||
|
||||
// Verify no file was created outside the logs dir
|
||||
expect(existsSync(join(testDir, 'outside.jsonl'))).toBe(false);
|
||||
|
||||
// Verify log was created in the correct location
|
||||
const logsDir = join(testDir, '.cc-safety-net', 'logs');
|
||||
if (existsSync(logsDir)) {
|
||||
const files = readdirSync(logsDir).filter((f) => f.endsWith('.jsonl'));
|
||||
expect(files.length).toBe(1);
|
||||
// The file should be inside logs dir
|
||||
for (const file of files) {
|
||||
const fullPath = join(logsDir, file);
|
||||
expect(fullPath.startsWith(logsDir)).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('session id absolute path does not escape logs dir', () => {
|
||||
const sessionId = join(testDir, 'escaped');
|
||||
writeAuditLog(sessionId, 'git reset --hard', 'git reset --hard', 'reason', null, {
|
||||
homeDir: testDir,
|
||||
});
|
||||
|
||||
// Verify no file was created at the escaped location
|
||||
expect(existsSync(join(testDir, 'escaped.jsonl'))).toBe(false);
|
||||
|
||||
// Verify log was created in the correct location
|
||||
const logsDir = join(testDir, '.cc-safety-net', 'logs');
|
||||
if (existsSync(logsDir)) {
|
||||
const files = readdirSync(logsDir).filter((f) => f.endsWith('.jsonl'));
|
||||
expect(files.length).toBe(1);
|
||||
for (const file of files) {
|
||||
const fullPath = join(logsDir, file);
|
||||
expect(fullPath.startsWith(logsDir)).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('cwd null when not provided', () => {
|
||||
const sessionId = 'test-session-no-cwd';
|
||||
writeAuditLog(sessionId, 'git reset --hard', 'git reset --hard', 'reason', null, {
|
||||
homeDir: testDir,
|
||||
});
|
||||
|
||||
const entries = readLogEntries(sessionId);
|
||||
expect(entries.length).toBe(1);
|
||||
expect(entries[0]?.cwd).toBeNull();
|
||||
});
|
||||
|
||||
test('truncates long commands', () => {
|
||||
const sessionId = 'test-session-long';
|
||||
const longCommand = `git reset --hard ${'x'.repeat(500)}`;
|
||||
writeAuditLog(sessionId, longCommand, longCommand, 'reason', null, {
|
||||
homeDir: testDir,
|
||||
});
|
||||
|
||||
const entries = readLogEntries(sessionId);
|
||||
expect(entries.length).toBe(1);
|
||||
expect(entries[0]?.command.length).toBeLessThanOrEqual(300);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user