23f7624596
ADR-166 MCP Bridge Security Lock / Static-source security lock (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / Compose default binds loopback + Mongo has auth (push) Failing after 2s
CodeQL Advanced / Analyze (rust) (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / plugin-agent-federation bindHost default (push) Failing after 1s
ADR-166 MCP Bridge Security Lock / Runtime behavior — 401 + terminal gate + fail-closed (push) Failing after 4s
business-pods-smoke / smoke (push) Failing after 1s
all-plugins-smoke / smoke-all (push) Failing after 2s
CI/CD Pipeline / Security & Code Quality (push) Failing after 1s
CI/CD Pipeline / Test Suite (ubuntu-latest) (push) Failing after 1s
CI/CD Pipeline / Build & Package (macos-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (ubuntu-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (windows-latest) (push) Has been skipped
CI/CD Pipeline / Documentation & Examples (push) Failing after 1s
Clone Tracker (14-day rolling) / Snapshot clones for ruflo ecosystem (push) Failing after 1s
CodeQL Advanced / Analyze (actions) (push) Failing after 1s
CodeQL Advanced / Analyze (javascript-typescript) (push) Failing after 1s
federation-peer-rust / stable-noop (push) Failing after 1s
metaharness-ci / score (push) Failing after 1s
metaharness-ci / router-compat (push) Failing after 0s
metaharness-ci / similarity-tests (push) Failing after 0s
no-agentbbs-smoke / smoke-without-agentbbs (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (windows-latest) (push) Has been skipped
codex-integration-audit / Codex integration audit (push) Failing after 1s
helpers-manifest-guard / guard (push) Failing after 1s
🔗 Cross-Agent Integration Tests / 🤝 Agent Coordination Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🧠 Memory Sharing Integration (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🛡️ Fault Tolerance Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / ⚡ Performance Integration Tests (push) Has been skipped
metaharness-ci / mcp-scan (push) Failing after 1s
metaharness-ci / eject-dryrun (push) Failing after 1s
metaharness-ci / metaharness-real-data (push) Failing after 0s
no-cli-optdep-bloat-2561 / guard (push) Failing after 1s
no-metaharness-smoke / smoke-without-metaharness (push) Failing after 1s
no-phantom-agentic-flow-subpath / guard (push) Failing after 1s
🔄 Automated Rollback Manager / 🚨 Failure Detection (push) Failing after 1s
V3 CI/CD Pipeline / Plugin hooks smoke / ubuntu-latest / Node 22 (push) Failing after 1s
V3 CI/CD Pipeline / ruflo-graph-intelligence build + test smoke (#2044, ADR-123) (push) Failing after 1s
CVE Audit Gate / Audit root (critical-blocking) (push) Failing after 2s
cost-tracker-smoke / smoke (push) Failing after 3s
oia-audit-weekly / audit (push) Failing after 2s
ruflo-agent-smoke / ruflo-agent structural smoke (push) Failing after 1s
📊 Status Badges Update / 📊 Update Status Badges (push) Failing after 1s
V3 CI/CD Pipeline / Static regression guards (#2267 YAML + (push) Failing after 1s
V3 CI/CD Pipeline / Test V3 Packages (push) Failing after 0s
V3 CI/CD Pipeline / agent_execute provider routing smoke (#2042) (push) Failing after 0s
CVE Audit Gate / Audit v3 (critical-blocking) (push) Failing after 1s
federation-peer-rust / stable-native (push) Failing after 2s
🔗 Cross-Agent Integration Tests / 🚀 Integration Test Setup (push) Failing after 2s
neural-trader-smoke / runtime-smoke (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (macos-latest) (push) Has been skipped
V3 CI/CD Pipeline / Build V3 (ubuntu-latest) (push) Has been skipped
V3 CI/CD Pipeline / Type Check V3 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 24 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 22 (push) Failing after 2s
V3 CI/CD Pipeline / browser rvf create flag smoke (#2015) (push) Failing after 0s
V3 CI/CD Pipeline / Dependency review (#2046) (push) Has been skipped
V3 CI/CD Pipeline / Supply-chain audit (#2046) (push) Failing after 0s
V3 CI/CD Pipeline / witness marker drift smoke (#2021) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader portfolio CG smoke (#2068, ADR-126 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader backtest signing smoke (#2068, ADR-126 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / kg-extract type-import classification smoke (#2049) (push) Failing after 0s
V3 CI/CD Pipeline / witness verify precondition smoke (#1880) (push) Failing after 2s
V3 CI/CD Pipeline / neural-trader pipeline risk-gate smoke (#2068, ADR-126 Phase 5) (push) Failing after 0s
V3 CI/CD Pipeline / neural-trader feature attribution smoke (#2068, ADR-126 Phase 6) (push) Failing after 0s
V3 CI/CD Pipeline / plugin-registry signature verification smoke (#1922, CWE-347) (push) Failing after 4s
V3 CI/CD Pipeline / memory stats legacy-DB smoke (#2120) (push) Failing after 4s
V3 CI/CD Pipeline / github deprecated actions smoke (#2089, ADR-127 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / graph query + pathfinder smoke (ADR-130 P2+P5) (push) Has been skipped
V3 CI/CD Pipeline / graph trajectory hooks smoke (ADR-130 P3) (push) Has been skipped
V3 CI/CD Pipeline / graph plugin adapter smoke (ADR-130 P4) (push) Has been skipped
V3 CI/CD Pipeline / graph benchmark (ADR-130 P6) (push) Has been skipped
V3 CI/CD Pipeline / statusline generator delegation smoke (#2195) (push) Failing after 1s
V3 CI/CD Pipeline / wizard init regression guard (#2206 (push) Failing after 1s
V3 CI/CD Pipeline / memory no-stray-db smoke (ADR-125 P7) (push) Failing after 1s
V3 CI/CD Pipeline / github-safe injection smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github actions pin smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github attribution opt-in smoke (#2089, ADR-127 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / pre-bash hook safety smoke (#2017) (push) Failing after 1s
V3 CI/CD Pipeline / Memory import smoke / ubuntu-latest (push) Failing after 0s
V3 CI/CD Pipeline / MCP protocol smoke / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / ruvllm WASM auto-init smoke (#2086) (push) Failing after 4s
V3 CI/CD Pipeline / MCP paired-tool round-trip smoke (#1889) (push) Failing after 1s
V3 CI/CD Pipeline / Plugin package install-safety (#1902/#1903/#1904) (push) Failing after 1s
V3 CI/CD Pipeline / Tool description discoverability (ADR-112) (push) Failing after 3s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (22) (push) Failing after 1s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (24) (push) Failing after 1s
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Vector-index dimension audit (#1947) (push) Failing after 0s
V3 CI/CD Pipeline / Hook-command install safety (#1921) (push) Failing after 1s
V3 CI/CD Pipeline / ToolOutputGuardrail smoke (ADR-131, (push) Failing after 1s
V3 CI/CD Pipeline / init-bundle invariants smoke (#2095, ADR-128 Phase 5) (push) Failing after 1s
V3 CI/CD Pipeline / wasm provider bridge smoke (ADR-129 P1) (push) Failing after 2s
V3 CI/CD Pipeline / wasm gallery CRUD smoke (ADR-129 P3) (push) Failing after 1s
V3 CI/CD Pipeline / wasm plugin bridge smoke (ADR-129 P4) (push) Failing after 0s
V3 CI/CD Pipeline / wasm compose smoke (ADR-129 P2) (push) Failing after 4s
V3 CI/CD Pipeline / graph schema smoke (ADR-130 P1) (push) Failing after 0s
Validate Marketplace / validate (push) Failing after 1s
🔍 Verification Pipeline / 🚀 Setup Verification (push) Failing after 1s
🔍 Verification Pipeline / 🛡️ Security Verification (push) Has been skipped
🔍 Verification Pipeline / 📝 Code Quality (push) Has been skipped
🔍 Verification Pipeline / 🧪 Test Verification (${{ matrix.os }}, Node ${{ matrix.node }}) (push) Has been skipped
🔍 Verification Pipeline / 🏗️ Build Verification (push) Has been skipped
🔍 Verification Pipeline / 📚 Documentation Verification (push) Has been skipped
CVE Audit Gate / High-severity report (warn only) (push) Has been cancelled
🔄 Automated Rollback Manager / 🔄 Execute Rollback (push) Has been cancelled
🔄 Automated Rollback Manager / ✅ Post-Rollback Verification (push) Has been cancelled
🔄 Automated Rollback Manager / 📊 Rollback Monitoring (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / windows-latest (push) Has been cancelled
🔄 Automated Rollback Manager / ⏳ Manual Rollback Approval (push) Has been cancelled
V3 CI/CD Pipeline / MCP protocol smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Memory import smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / ubuntu-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Publish to npm (alpha) (push) Has been cancelled
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / macos-latest / Node 22 (push) Has been cancelled
V3 CI/CD Pipeline / Plugin hooks smoke / macos-latest / Node 22 (push) Has been cancelled
CI/CD Pipeline / Deploy & Release (push) Has been cancelled
CI/CD Pipeline / CI Status (push) Has been cancelled
🔗 Cross-Agent Integration Tests / 📊 Integration Test Report (push) Has been cancelled
🔄 Automated Rollback Manager / 🔍 Pre-Rollback Validation (push) Has been cancelled
🔍 Verification Pipeline / ⚡ Performance Verification (push) Has been cancelled
🔍 Verification Pipeline / 📊 Verification Report (push) Has been cancelled
549 lines
22 KiB
TypeScript
549 lines
22 KiB
TypeScript
/**
|
|
* Security & Performance Verification Tests
|
|
*
|
|
* Validates all 10 user-facing scenarios from the critical fixes implementation.
|
|
* Mix of:
|
|
* - Behavioral tests: exercise real code paths with hostile inputs
|
|
* - Source-guard tests: regression guards ensuring dangerous patterns don't return
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
// ============================================================================
|
|
// Scenario 1: SQL Injection in Memory Search (CRIT-01)
|
|
// ============================================================================
|
|
describe('Scenario 1: SQL injection in embeddings search', () => {
|
|
// Source-guard: ensure parameterized queries are used
|
|
it('should use parameterized queries for namespace filtering', async () => {
|
|
const { readFileSync } = await import('fs');
|
|
const source = readFileSync(
|
|
new URL('../src/commands/embeddings.ts', import.meta.url),
|
|
'utf-8'
|
|
);
|
|
|
|
// Must NOT have string interpolation in SQL for namespace
|
|
expect(source).not.toMatch(/WHERE.*namespace\s*=\s*'\$\{/);
|
|
expect(source).not.toMatch(/WHERE.*namespace\s*=\s*'"\s*\+/);
|
|
|
|
// Must use bind parameters
|
|
expect(source).toContain('.bind(');
|
|
expect(source).toContain('.prepare(');
|
|
|
|
// The old vulnerable pattern must be gone
|
|
expect(source).not.toMatch(/namespace\s*=\s*''\s*\+\s*namespace/);
|
|
});
|
|
|
|
it('should use parameterized queries for keyword search', async () => {
|
|
const { readFileSync } = await import('fs');
|
|
const source = readFileSync(
|
|
new URL('../src/commands/embeddings.ts', import.meta.url),
|
|
'utf-8'
|
|
);
|
|
|
|
// Keyword search must use LIKE with bind parameter, not string concat
|
|
expect(source).toContain('LIKE ?');
|
|
expect(source).not.toMatch(/LIKE\s*'%'\s*\+/);
|
|
});
|
|
|
|
// Behavioral: formatEmbedding rejects non-numeric array elements (Finding 2 fix)
|
|
it('formatEmbedding should reject non-numeric embedding values', async () => {
|
|
const { readFileSync } = await import('fs');
|
|
// We can't easily import the function directly (it's not exported),
|
|
// so we test the validation logic inline matching the implementation
|
|
function formatEmbedding(embedding: number[], dimensions: number = 384): string {
|
|
for (let i = 0; i < embedding.length; i++) {
|
|
if (typeof embedding[i] !== 'number' || !Number.isFinite(embedding[i])) {
|
|
throw new Error(`Invalid embedding value at index ${i}: expected finite number, got ${typeof embedding[i]}`);
|
|
}
|
|
}
|
|
const padded = [...embedding];
|
|
while (padded.length < dimensions) padded.push(0);
|
|
if (padded.length > dimensions) padded.length = dimensions;
|
|
return `'[${padded.join(',')}]'::ruvector(${dimensions})`;
|
|
}
|
|
|
|
// Valid embedding
|
|
expect(() => formatEmbedding([0.1, 0.2, 0.3], 3)).not.toThrow();
|
|
|
|
// SQL injection via crafted string element
|
|
const malicious = [1, 2] as unknown as number[];
|
|
(malicious as unknown[])[1] = '3); DROP TABLE users; --';
|
|
expect(() => formatEmbedding(malicious, 3)).toThrow('Invalid embedding value');
|
|
|
|
// NaN and Infinity should be rejected
|
|
expect(() => formatEmbedding([1, NaN, 3], 3)).toThrow('Invalid embedding value');
|
|
expect(() => formatEmbedding([1, Infinity, 3], 3)).toThrow('Invalid embedding value');
|
|
expect(() => formatEmbedding([1, -Infinity, 3], 3)).toThrow('Invalid embedding value');
|
|
});
|
|
|
|
// Behavioral: source file must contain the validation guard
|
|
it('import.ts formatEmbedding must validate array elements', async () => {
|
|
const { readFileSync } = await import('fs');
|
|
const source = readFileSync(
|
|
new URL('../src/commands/ruvector/import.ts', import.meta.url),
|
|
'utf-8'
|
|
);
|
|
|
|
// Must validate each element is a finite number
|
|
expect(source).toContain('Number.isFinite');
|
|
expect(source).toContain('Invalid embedding value');
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Scenario 2: Prototype Pollution via JSON (HIGH-03)
|
|
// ============================================================================
|
|
describe('Scenario 2: Prototype pollution prevention', () => {
|
|
it('safeJsonParse should strip __proto__ keys', async () => {
|
|
const { safeJsonParse } = await import(
|
|
'../../memory/src/json-security.js'
|
|
);
|
|
|
|
const malicious = '{"__proto__": {"polluted": true}, "safe": "value"}';
|
|
const result = safeJsonParse(malicious) as Record<string, unknown>;
|
|
|
|
expect(result.safe).toBe('value');
|
|
// __proto__ key should not exist as an own property
|
|
expect(Object.hasOwn(result, '__proto__')).toBe(false);
|
|
// Prototype chain should be untouched (standard Object.prototype)
|
|
expect(Object.getPrototypeOf(result)).toBe(Object.prototype);
|
|
// Verify prototype was NOT actually polluted on other objects
|
|
expect(({} as any).polluted).toBeUndefined();
|
|
});
|
|
|
|
it('safeJsonParse should strip constructor keys', async () => {
|
|
const { safeJsonParse } = await import(
|
|
'../../memory/src/json-security.js'
|
|
);
|
|
|
|
const malicious = '{"constructor": {"prototype": {"pwned": true}}}';
|
|
const result = safeJsonParse(malicious) as Record<string, unknown>;
|
|
|
|
// constructor key should not exist as own property
|
|
expect(Object.hasOwn(result, 'constructor')).toBe(false);
|
|
expect(({} as any).pwned).toBeUndefined();
|
|
});
|
|
|
|
it('safeJsonParse should handle nested __proto__', async () => {
|
|
const { safeJsonParse } = await import(
|
|
'../../memory/src/json-security.js'
|
|
);
|
|
|
|
const nested = '{"a": {"__proto__": {"x": 1}}, "b": 2}';
|
|
const result = safeJsonParse(nested) as any;
|
|
|
|
expect(result.b).toBe(2);
|
|
// Nested __proto__ key should not exist as own property
|
|
expect(Object.hasOwn(result.a, '__proto__')).toBe(false);
|
|
});
|
|
|
|
it('memory backends must use safeJsonParse, not JSON.parse for row data', async () => {
|
|
const { readFileSync } = await import('fs');
|
|
|
|
const backends = [
|
|
'../../memory/src/agentdb-backend.ts',
|
|
'../../memory/src/sqlite-backend.ts',
|
|
'../../memory/src/sqljs-backend.ts',
|
|
];
|
|
|
|
for (const backend of backends) {
|
|
const source = readFileSync(
|
|
new URL(backend, import.meta.url),
|
|
'utf-8'
|
|
);
|
|
|
|
// Must import safeJsonParse
|
|
expect(source).toContain('safeJsonParse');
|
|
|
|
// Must NOT use raw JSON.parse on row data (tags, metadata, references)
|
|
// Allow JSON.parse for other uses (like config), but row.tags/metadata must be safe
|
|
const dangerousPatterns = [
|
|
/JSON\.parse\(row\.tags/,
|
|
/JSON\.parse\(row\.metadata/,
|
|
/JSON\.parse\(row\.references/,
|
|
/JSON\.parse\(entry\.tags/,
|
|
];
|
|
|
|
for (const pattern of dangerousPatterns) {
|
|
expect(source).not.toMatch(pattern);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Scenario 3: Docker Container Name Injection (CRIT-02)
|
|
// ============================================================================
|
|
describe('Scenario 3: Container name injection prevention', () => {
|
|
// Behavioral: test execFileSync is used instead of execSync with shell
|
|
it('import.ts must not pass shell metacharacters to execSync', async () => {
|
|
const { readFileSync } = await import('fs');
|
|
const source = readFileSync(
|
|
new URL('../src/commands/ruvector/import.ts', import.meta.url),
|
|
'utf-8'
|
|
);
|
|
|
|
// execFileSync does NOT interpret shell metacharacters — this is the fix
|
|
expect(source).toContain('execFileSync');
|
|
// The container name is passed as an array element, not interpolated in a string
|
|
expect(source).toMatch(/execFileSync\s*\(\s*'docker'\s*,\s*\[/);
|
|
});
|
|
|
|
it('should validate container names with strict regex', () => {
|
|
const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
|
|
|
// Valid names
|
|
expect(validPattern.test('ruvector-postgres')).toBe(true);
|
|
expect(validPattern.test('my_container.v2')).toBe(true);
|
|
expect(validPattern.test('pg12')).toBe(true);
|
|
|
|
// Injection attempts
|
|
expect(validPattern.test('evil; rm -rf /')).toBe(false);
|
|
expect(validPattern.test('$(whoami)')).toBe(false);
|
|
expect(validPattern.test('`cat /etc/passwd`')).toBe(false);
|
|
expect(validPattern.test('name && echo pwned')).toBe(false);
|
|
expect(validPattern.test('name|cat /etc/shadow')).toBe(false);
|
|
expect(validPattern.test('')).toBe(false);
|
|
expect(validPattern.test('-invalid')).toBe(false);
|
|
});
|
|
|
|
it('import.ts must use execFileSync, not execSync', async () => {
|
|
const { readFileSync } = await import('fs');
|
|
const source = readFileSync(
|
|
new URL('../src/commands/ruvector/import.ts', import.meta.url),
|
|
'utf-8'
|
|
);
|
|
|
|
// Must use execFileSync (no shell interpretation)
|
|
expect(source).toContain('execFileSync');
|
|
|
|
// Must NOT use execSync with template literals for docker commands
|
|
expect(source).not.toMatch(/execSync\s*\(\s*`.*docker/);
|
|
|
|
// Must validate container name
|
|
expect(source).toContain('Invalid container name');
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Scenario 5: Embedding Model Name Injection (CRIT-02)
|
|
// ============================================================================
|
|
describe('Scenario 5: Embedding model name injection prevention', () => {
|
|
it('should validate model names with strict regex', () => {
|
|
const validPattern = /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9._-]+$/;
|
|
|
|
// Valid model names
|
|
expect(validPattern.test('sentence-transformers/all-MiniLM-L6-v2')).toBe(true);
|
|
expect(validPattern.test('BAAI/bge-small-en-v1.5')).toBe(true);
|
|
|
|
// Injection attempts
|
|
expect(validPattern.test('model; whoami')).toBe(false);
|
|
expect(validPattern.test('$(cat /etc/passwd)')).toBe(false);
|
|
expect(validPattern.test('model`rm -rf /`')).toBe(false);
|
|
expect(validPattern.test('model && echo pwned')).toBe(false);
|
|
expect(validPattern.test('model | cat /etc/shadow')).toBe(false);
|
|
});
|
|
|
|
it('init.ts must use execFileSync for embedding model commands', async () => {
|
|
const { readFileSync } = await import('fs');
|
|
const source = readFileSync(
|
|
new URL('../src/commands/init.ts', import.meta.url),
|
|
'utf-8'
|
|
);
|
|
|
|
// Must use execFileSync
|
|
expect(source).toContain('execFileSync');
|
|
|
|
// Must validate model name
|
|
expect(source).toContain('Invalid embedding model name');
|
|
|
|
// Must NOT have execSync with embeddingModel interpolation
|
|
expect(source).not.toMatch(/execSync\s*\(\s*`[^`]*\$\{embeddingModel\}/);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Scenario 6: Browser Eval Security (CRIT-03, defense-in-depth)
|
|
// NOTE: Pattern blocklist is a best-effort defense layer, not a sandbox.
|
|
// The primary defense is the browser sandbox itself.
|
|
// ============================================================================
|
|
describe('Scenario 6: Browser eval length limit and pattern blocking (defense-in-depth)', () => {
|
|
it('should enforce configurable max script length (default 20KB)', async () => {
|
|
const { readFileSync } = await import('fs');
|
|
const source = readFileSync(
|
|
new URL('../../browser/src/mcp-tools/browser-tools.ts', import.meta.url),
|
|
'utf-8'
|
|
);
|
|
|
|
// Default should be 20_000
|
|
expect(source).toContain('DEFAULT_MAX_EVAL_SCRIPT_LENGTH = 20_000');
|
|
|
|
// Should be configurable via env var
|
|
expect(source).toContain('CLAUDE_FLOW_MAX_EVAL_SCRIPT_LENGTH');
|
|
|
|
// Must check length before eval
|
|
expect(source).toContain('script.length > MAX_EVAL_SCRIPT_LENGTH');
|
|
});
|
|
|
|
it('should block all dangerous eval patterns', async () => {
|
|
const { readFileSync } = await import('fs');
|
|
const source = readFileSync(
|
|
new URL('../../browser/src/mcp-tools/browser-tools.ts', import.meta.url),
|
|
'utf-8'
|
|
);
|
|
|
|
const requiredPatterns = [
|
|
'process', // Node.js process access
|
|
'require', // CommonJS require
|
|
'__dirname', // Node path leaking
|
|
'__filename', // Node path leaking
|
|
'child_process', // Command execution
|
|
'global', // Global scope access
|
|
'globalThis', // Global scope bypass
|
|
'Function', // Dynamic function creation
|
|
'constructor', // Prototype chain bypass
|
|
'Reflect', // Reflection API
|
|
'import', // Dynamic import
|
|
'eval', // Direct eval recursion
|
|
];
|
|
|
|
for (const pattern of requiredPatterns) {
|
|
expect(source).toContain(pattern);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Scenario 7: BoundedSet Eviction (PERF-01)
|
|
// ============================================================================
|
|
describe('Scenario 7: BoundedSet eviction under load', () => {
|
|
// Source-guard: verify BoundedSet is used in gossip
|
|
it('gossip.ts must use exported BoundedSet for seenMessages', async () => {
|
|
const { readFileSync } = await import('fs');
|
|
const source = readFileSync(
|
|
new URL('../../swarm/src/consensus/gossip.ts', import.meta.url),
|
|
'utf-8'
|
|
);
|
|
|
|
// BoundedSet must be exported and use Map for insertion-order FIFO
|
|
expect(source).toContain('export class BoundedSet');
|
|
expect(source).toContain('new Map');
|
|
expect(source).toContain('this.maxSize');
|
|
|
|
// Must evict oldest when full (uses Map keys iterator)
|
|
expect(source).toContain('this.map.keys().next().value');
|
|
|
|
// GossipNode must use BoundedSet, not plain Set
|
|
expect(source).toContain('seenMessages: BoundedSet<string>');
|
|
});
|
|
|
|
// Behavioral: test with FIFO semantics matching production code
|
|
it('BoundedSet FIFO eviction should work correctly', () => {
|
|
// Replicate exact production FIFO semantics (duplicates are no-ops)
|
|
class BoundedSet<T> {
|
|
private map = new Map<T, true>();
|
|
constructor(private maxSize: number) {}
|
|
add(value: T): void {
|
|
if (this.map.has(value)) return; // FIFO: duplicate is no-op
|
|
if (this.map.size >= this.maxSize) {
|
|
const oldest = this.map.keys().next().value;
|
|
if (oldest !== undefined) this.map.delete(oldest);
|
|
}
|
|
this.map.set(value, true);
|
|
}
|
|
has(value: T): boolean { return this.map.has(value); }
|
|
get size(): number { return this.map.size; }
|
|
}
|
|
|
|
const set = new BoundedSet<number>(5);
|
|
|
|
// Fill to capacity
|
|
for (let i = 0; i < 5; i++) set.add(i);
|
|
expect(set.size).toBe(5);
|
|
|
|
// Add one more — oldest (0) should be evicted
|
|
set.add(99);
|
|
expect(set.size).toBe(5);
|
|
expect(set.has(0)).toBe(false);
|
|
expect(set.has(99)).toBe(true);
|
|
expect(set.has(1)).toBe(true);
|
|
|
|
// FIFO: re-adding existing value does NOT refresh its position
|
|
set.add(1); // 1 already exists, this is a no-op (stays at front)
|
|
set.add(100); // evicts 1 (still oldest in FIFO), unlike LRU which would keep 1
|
|
expect(set.has(1)).toBe(false); // FIFO evicted 1 because it was still oldest
|
|
expect(set.has(2)).toBe(true); // 2 survives because 1 was evicted first
|
|
|
|
// Add many more — should never exceed maxSize
|
|
for (let i = 200; i < 300; i++) set.add(i);
|
|
expect(set.size).toBe(5);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Scenario 8: Bulk Operations with Partial Failures (PERF-02)
|
|
// ============================================================================
|
|
describe('Scenario 8: Bulk operations with Promise.allSettled', () => {
|
|
it('agentdb-backend must use Promise.allSettled for bulk ops', async () => {
|
|
const { readFileSync } = await import('fs');
|
|
const source = readFileSync(
|
|
new URL('../../memory/src/agentdb-backend.ts', import.meta.url),
|
|
'utf-8'
|
|
);
|
|
|
|
// Must use Promise.allSettled (not Promise.all)
|
|
expect(source).toContain('Promise.allSettled');
|
|
|
|
// Must have BATCH_SIZE for bounded concurrency
|
|
expect(source).toContain('BATCH_SIZE');
|
|
|
|
// Must log failures
|
|
expect(source).toMatch(/failed.*bulkInsert|bulkInsert.*failed/i);
|
|
});
|
|
|
|
it('Promise.allSettled should not throw on partial failure', async () => {
|
|
// Simulate the pattern used in bulkInsert
|
|
const operations = [
|
|
Promise.resolve('ok1'),
|
|
Promise.reject(new Error('entry 2 failed')),
|
|
Promise.resolve('ok3'),
|
|
Promise.reject(new Error('entry 4 failed')),
|
|
Promise.resolve('ok5'),
|
|
];
|
|
|
|
const results = await Promise.allSettled(operations);
|
|
|
|
const fulfilled = results.filter(r => r.status === 'fulfilled');
|
|
const rejected = results.filter(r => r.status === 'rejected');
|
|
|
|
expect(fulfilled.length).toBe(3);
|
|
expect(rejected.length).toBe(2);
|
|
// Key: no exception thrown, all results available
|
|
expect(results.length).toBe(5);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Scenario 9: RuVector Schema Name & Timestamp Injection (DA-CRIT-2, DA-HIGH-3)
|
|
// ============================================================================
|
|
describe('Scenario 9: Schema name and timestamp validation', () => {
|
|
it('validateSchemaName should reject SQL injection payloads', async () => {
|
|
const { validateSchemaName } = await import(
|
|
'../src/commands/ruvector/pg-utils.js'
|
|
);
|
|
|
|
// Valid schema names
|
|
expect(validateSchemaName('claude_flow')).toBe('claude_flow');
|
|
expect(validateSchemaName('public')).toBe('public');
|
|
expect(validateSchemaName('my_schema_v2')).toBe('my_schema_v2');
|
|
|
|
// SQL injection attempts — must throw
|
|
expect(() => validateSchemaName("'; DROP TABLE users; --")).toThrow();
|
|
expect(() => validateSchemaName('schema; DELETE FROM')).toThrow();
|
|
expect(() => validateSchemaName('1invalid')).toThrow();
|
|
expect(() => validateSchemaName('')).toThrow();
|
|
expect(() => validateSchemaName('a'.repeat(64))).toThrow(); // >63 chars
|
|
expect(() => validateSchemaName('schema name')).toThrow(); // spaces
|
|
expect(() => validateSchemaName('schema$name')).toThrow(); // special chars
|
|
});
|
|
|
|
it('validateTimestamp should reject crafted timestamps', async () => {
|
|
const { validateTimestamp } = await import(
|
|
'../src/commands/ruvector/pg-utils.js'
|
|
);
|
|
|
|
// Valid ISO 8601 timestamps with time component
|
|
expect(validateTimestamp('2024-01-15T10:30:00Z')).toBe('2024-01-15T10:30:00Z');
|
|
expect(validateTimestamp('2024-01-15T10:30:00.000Z')).toBe('2024-01-15T10:30:00.000Z');
|
|
expect(validateTimestamp('2024-01-15 10:30:00')).toBe('2024-01-15 10:30:00');
|
|
expect(validateTimestamp('2024-01-15T10:30:00+05:30')).toBe('2024-01-15T10:30:00+05:30');
|
|
|
|
// Date-only is NOT valid for timestamptz — must require time component
|
|
expect(() => validateTimestamp('2024-01-15')).toThrow();
|
|
|
|
// SQL injection via timestamp — must throw
|
|
expect(() => validateTimestamp("2024'; DROP TABLE memory_entries; --")).toThrow();
|
|
expect(() => validateTimestamp('not-a-date')).toThrow();
|
|
expect(() => validateTimestamp('')).toThrow();
|
|
});
|
|
|
|
it('all ruvector commands must validate schema names', async () => {
|
|
const { readFileSync } = await import('fs');
|
|
const commands = [
|
|
'../src/commands/ruvector/init.ts',
|
|
'../src/commands/ruvector/backup.ts',
|
|
'../src/commands/ruvector/status.ts',
|
|
'../src/commands/ruvector/optimize.ts',
|
|
'../src/commands/ruvector/benchmark.ts',
|
|
'../src/commands/ruvector/migrate.ts',
|
|
];
|
|
|
|
for (const cmd of commands) {
|
|
const source = readFileSync(new URL(cmd, import.meta.url), 'utf-8');
|
|
expect(source).toContain('validateSchemaName');
|
|
expect(source).toContain("from './pg-utils.js'");
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Scenario 10: PID Injection in MCP Server (DA-CRIT-3)
|
|
// ============================================================================
|
|
describe('Scenario 10: PID injection prevention in mcp-server', () => {
|
|
it('isProcessRunning must not use execSync with PID interpolation', async () => {
|
|
const { readFileSync } = await import('fs');
|
|
const source = readFileSync(
|
|
new URL('../src/mcp-server.ts', import.meta.url),
|
|
'utf-8'
|
|
);
|
|
|
|
// Must NOT have execSync with PID template literal
|
|
expect(source).not.toMatch(/execSync\s*\(\s*`[^`]*\$\{.*pid/i);
|
|
|
|
// Must use safe /proc read or execFileSync
|
|
const usesProcRead = source.includes('/proc/') && source.includes('readFileSync');
|
|
const usesExecFileSync = source.includes('execFileSync');
|
|
expect(usesProcRead || usesExecFileSync).toBe(true);
|
|
|
|
// Must validate PID is numeric
|
|
expect(source).toMatch(/parseInt|Number\(|\/\\d|isNaN/);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Scenario: ESLint child_process restriction (HIGH-01)
|
|
// ============================================================================
|
|
describe('Bonus: ESLint child_process restriction', () => {
|
|
it('eslintrc should restrict both import and require of child_process', async () => {
|
|
const { readFileSync } = await import('fs');
|
|
const eslintConfig = readFileSync(
|
|
new URL('../.eslintrc.json', import.meta.url),
|
|
'utf-8'
|
|
);
|
|
|
|
// Must restrict ES module imports
|
|
expect(eslintConfig).toContain('no-restricted-imports');
|
|
expect(eslintConfig).toContain('child_process');
|
|
|
|
// Must restrict CommonJS require
|
|
expect(eslintConfig).toContain('no-restricted-modules');
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Scenario: Hive-mind truthiness fix (HIGH-02)
|
|
// ============================================================================
|
|
describe('Bonus: Hive-mind truthiness fix', () => {
|
|
it('should use === true, not !== false for Byzantine consensus', async () => {
|
|
const { readFileSync } = await import('fs');
|
|
const source = readFileSync(
|
|
new URL('../src/commands/hive-mind.ts', import.meta.url),
|
|
'utf-8'
|
|
);
|
|
|
|
// The specific fix: check for strict true, not loose "not false"
|
|
// The old pattern `!== false` would treat undefined/null as truthy
|
|
expect(source).toContain('=== true');
|
|
});
|
|
});
|