397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
153 lines
6.7 KiB
JavaScript
153 lines
6.7 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
TEST_RUNNERS,
|
|
isTestRunner,
|
|
detectLanguage,
|
|
scanContent,
|
|
scanFileWith,
|
|
} from './static-content-scanner.mjs';
|
|
|
|
describe('isTestRunner', () => {
|
|
it('matches PHP/Laravel test runners', () => {
|
|
expect(isTestRunner('composer test')).toBe(true);
|
|
expect(isTestRunner('php artisan test --parallel')).toBe(true);
|
|
expect(isTestRunner('vendor/bin/pest')).toBe(true);
|
|
expect(isTestRunner('vendor/bin/phpunit tests/Unit')).toBe(true);
|
|
});
|
|
it('matches Ruby / Go / Java / Rust / .NET / JS runners', () => {
|
|
expect(isTestRunner('bundle exec rspec')).toBe(true);
|
|
expect(isTestRunner('go test ./...')).toBe(true);
|
|
expect(isTestRunner('mvn test')).toBe(true);
|
|
expect(isTestRunner('gradle build')).toBe(true);
|
|
expect(isTestRunner('cargo test')).toBe(true);
|
|
expect(isTestRunner('dotnet test')).toBe(true);
|
|
expect(isTestRunner('npx vitest run')).toBe(true);
|
|
expect(isTestRunner('npm run test')).toBe(true);
|
|
});
|
|
it('does not match unrelated commands', () => {
|
|
expect(isTestRunner('git status')).toBe(false);
|
|
expect(isTestRunner('ls -la')).toBe(false);
|
|
expect(isTestRunner('composer install')).toBe(false);
|
|
expect(isTestRunner('')).toBe(false);
|
|
expect(isTestRunner(null)).toBe(false);
|
|
});
|
|
it('exposes TEST_RUNNERS as a non-empty array', () => {
|
|
expect(Array.isArray(TEST_RUNNERS)).toBe(true);
|
|
expect(TEST_RUNNERS.length).toBeGreaterThanOrEqual(11);
|
|
});
|
|
});
|
|
|
|
describe('detectLanguage', () => {
|
|
it('maps extensions to language keys', () => {
|
|
expect(detectLanguage('app/Providers/AppServiceProvider.php')).toBe('php');
|
|
expect(detectLanguage('config/routes.rb')).toBe('ruby');
|
|
expect(detectLanguage('main.go')).toBe('go');
|
|
expect(detectLanguage('src/Main.java')).toBe('java');
|
|
expect(detectLanguage('src/Config.kt')).toBe('java'); // JVM
|
|
expect(detectLanguage('src/lib.rs')).toBe('rust');
|
|
expect(detectLanguage('Program.cs')).toBe('dotnet');
|
|
});
|
|
it('is case-insensitive on extension', () => {
|
|
expect(detectLanguage('Foo.PHP')).toBe('php');
|
|
});
|
|
it('returns null for unknown / extensionless / non-string', () => {
|
|
expect(detectLanguage('notes.txt')).toBeNull();
|
|
expect(detectLanguage('Makefile')).toBeNull();
|
|
expect(detectLanguage(42)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('scanContent — always-suspicious patterns', () => {
|
|
it('flags PHP code-execution sinks', () => {
|
|
const names = scanContent('<?php exec($cmd); eval($x);', 'php').map((f) => f.name);
|
|
expect(names).toContain('exec');
|
|
expect(names).toContain('eval');
|
|
});
|
|
it('flags PHP shell_exec / proc_open / pcntl / backticks', () => {
|
|
const names = scanContent('<?php $o = shell_exec($c); proc_open($c); $r = `whoami`;', 'php').map((f) => f.name);
|
|
expect(names).toContain('shell_exec');
|
|
expect(names).toContain('proc_open');
|
|
expect(names).toContain('backtick');
|
|
});
|
|
it('flags Ruby eval/system/popen/backticks', () => {
|
|
const names = scanContent('Kernel.eval(x); system("rm"); IO.popen(c); y = `ls`', 'ruby').map((f) => f.name);
|
|
expect(names).toContain('Kernel.eval');
|
|
expect(names).toContain('system');
|
|
expect(names).toContain('IO.popen');
|
|
expect(names).toContain('backtick');
|
|
});
|
|
it('flags Go exec + reflect co-occurrence', () => {
|
|
const src = 'import "os/exec"\nexec.Command("sh")\nv := reflect.ValueOf(f); v.Call(args)';
|
|
const names = scanContent(src, 'go').map((f) => f.name);
|
|
expect(names).toContain('exec.Command');
|
|
expect(names).toContain('reflect.Call');
|
|
});
|
|
it('does NOT flag Go reflect.Call when only ValueOf present (co-occurrence requires both)', () => {
|
|
const names = scanContent('v := reflect.ValueOf(f)', 'go').map((f) => f.name);
|
|
expect(names).not.toContain('reflect.Call');
|
|
});
|
|
it('flags Java Runtime.exec / ProcessBuilder / reflective invoke', () => {
|
|
const src = 'Runtime.getRuntime().exec(c); new ProcessBuilder(c); m = cls.getMethod("x"); m.invoke(o);';
|
|
const names = scanContent(src, 'java').map((f) => f.name);
|
|
expect(names).toContain('Runtime.exec');
|
|
expect(names).toContain('ProcessBuilder');
|
|
expect(names).toContain('Method.invoke');
|
|
});
|
|
it('flags Rust process::Command', () => {
|
|
const names = scanContent('let o = std::process::Command::new("sh");', 'rust').map((f) => f.name);
|
|
expect(names).toContain('process.Command');
|
|
});
|
|
it('flags .NET Process.Start + Assembly.Load reflective invoke', () => {
|
|
const src = 'Process.Start(p); var a = Assembly.Load(b); mi.Invoke(o, null);';
|
|
const names = scanContent(src, 'dotnet').map((f) => f.name);
|
|
expect(names).toContain('Process.Start');
|
|
expect(names).toContain('Assembly.Load');
|
|
});
|
|
});
|
|
|
|
describe('scanContent — protected-sensitive (file deletion / write)', () => {
|
|
it('includes protected-sensitive findings by default', () => {
|
|
const findings = scanContent('<?php unlink($f); file_put_contents($p, $d);', 'php');
|
|
const prot = findings.filter((f) => f.category === 'protected_sensitive').map((f) => f.name);
|
|
expect(prot).toContain('unlink');
|
|
expect(prot).toContain('file_put_contents');
|
|
});
|
|
it('omits protected-sensitive findings when includeProtectedSensitive: false', () => {
|
|
const findings = scanContent('<?php unlink($f);', 'php', { includeProtectedSensitive: false });
|
|
expect(findings.find((f) => f.category === 'protected_sensitive')).toBeUndefined();
|
|
});
|
|
it('still flags always-suspicious even with includeProtectedSensitive: false', () => {
|
|
const findings = scanContent('<?php exec($c); unlink($f);', 'php', { includeProtectedSensitive: false });
|
|
expect(findings.map((f) => f.name)).toContain('exec');
|
|
});
|
|
});
|
|
|
|
describe('scanContent — guards', () => {
|
|
it('returns [] for non-string source or unknown lang', () => {
|
|
expect(scanContent(null, 'php')).toEqual([]);
|
|
expect(scanContent('exec(', 'cobol')).toEqual([]);
|
|
expect(scanContent('exec(', null)).toEqual([]);
|
|
});
|
|
it('returns [] for clean source', () => {
|
|
expect(scanContent('<?php class Foo { public function bar() { return 1; } }', 'php')).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('scanFileWith', () => {
|
|
it('detects language from path and scans injected content', () => {
|
|
const reader = (p) => (p.endsWith('.php') ? '<?php system($c);' : '');
|
|
const findings = scanFileWith('app/Foo.php', reader);
|
|
expect(findings.map((f) => f.name)).toContain('system');
|
|
});
|
|
it('returns [] for unknown language path without calling reader', () => {
|
|
let called = false;
|
|
const reader = () => { called = true; return ''; };
|
|
expect(scanFileWith('notes.txt', reader)).toEqual([]);
|
|
expect(called).toBe(false);
|
|
});
|
|
it('returns [] when reader throws (unreadable file)', () => {
|
|
const reader = () => { throw new Error('ENOENT'); };
|
|
expect(scanFileWith('app/Foo.php', reader)).toEqual([]);
|
|
});
|
|
});
|