Files
brain/tools/framework-boot-scanner.test.mjs

166 lines
7.0 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import {
detectProjectType,
bootPathSet,
matchesBootPattern,
intersectEditedBootFiles,
extractPhpMethodBody,
decideBootScan,
} from './framework-boot-scanner.mjs';
describe('detectProjectType', () => {
it('detects PHP/Laravel from composer.json + AppServiceProvider', () => {
const exists = (p) => p === 'composer.json' || p === 'app/Providers/AppServiceProvider.php';
expect(detectProjectType(exists)).toContain('laravel');
});
it('detects Rails from Gemfile + config/application.rb', () => {
const exists = (p) => p === 'Gemfile' || p === 'config/application.rb';
expect(detectProjectType(exists)).toContain('rails');
});
it('detects Express from package.json + server.js', () => {
const exists = (p) => p === 'package.json' || p === 'server.js';
expect(detectProjectType(exists)).toContain('express');
});
it('detects Django from manage.py', () => {
const exists = (p) => p === 'manage.py';
expect(detectProjectType(exists)).toContain('django');
});
it('returns [] when no markers present', () => {
expect(detectProjectType(() => false)).toEqual([]);
});
it('can detect multiple project types simultaneously', () => {
const exists = (p) => ['composer.json', 'app/Providers/AppServiceProvider.php', 'package.json', 'app.js'].includes(p);
const types = detectProjectType(exists);
expect(types).toContain('laravel');
expect(types).toContain('express');
});
});
describe('bootPathSet', () => {
it('returns Laravel boot-path glob patterns', () => {
const set = bootPathSet('laravel');
expect(set).toContain('app/Providers/**/*.php');
expect(set).toContain('routes/*.php');
expect(set).toContain('app/Http/Kernel.php');
expect(set).toContain('app/Models/*.php');
});
it('returns [] for unknown project type', () => {
expect(bootPathSet('cobol')).toEqual([]);
});
});
describe('matchesBootPattern', () => {
it('matches ** recursively across directories', () => {
expect(matchesBootPattern('app/Providers/Sub/MyProvider.php', 'app/Providers/**/*.php')).toBe(true);
expect(matchesBootPattern('app/Providers/AppServiceProvider.php', 'app/Providers/**/*.php')).toBe(true);
});
it('matches single-segment * within one directory only', () => {
expect(matchesBootPattern('routes/web.php', 'routes/*.php')).toBe(true);
expect(matchesBootPattern('routes/admin/web.php', 'routes/*.php')).toBe(false);
});
it('matches exact literal paths', () => {
expect(matchesBootPattern('app/Http/Kernel.php', 'app/Http/Kernel.php')).toBe(true);
expect(matchesBootPattern('app/Http/Other.php', 'app/Http/Kernel.php')).toBe(false);
});
it('normalizes backslashes before matching', () => {
expect(matchesBootPattern('app\\Providers\\AppServiceProvider.php', 'app/Providers/**/*.php')).toBe(true);
});
});
describe('intersectEditedBootFiles', () => {
it('returns edited files that fall under a boot-path pattern', () => {
const edited = ['app/Providers/AppServiceProvider.php', 'app/Services/Foo.php', 'routes/web.php'];
const hit = intersectEditedBootFiles('laravel', edited);
expect(hit).toContain('app/Providers/AppServiceProvider.php');
expect(hit).toContain('routes/web.php');
expect(hit).not.toContain('app/Services/Foo.php');
});
it('returns [] when no edited file is a boot file', () => {
expect(intersectEditedBootFiles('laravel', ['app/Services/Foo.php'])).toEqual([]);
});
it('returns [] for empty / non-array edited list', () => {
expect(intersectEditedBootFiles('laravel', [])).toEqual([]);
expect(intersectEditedBootFiles('laravel', null)).toEqual([]);
});
});
describe('extractPhpMethodBody', () => {
it('extracts a method body by brace matching', () => {
const src = 'class M { public function boot() { exec($c); } public function x() { return 1; } }';
const body = extractPhpMethodBody(src, 'boot');
expect(body).toContain('exec($c);');
expect(body).not.toContain('return 1;');
});
it('handles nested braces', () => {
const src = 'function boot() { if (true) { unlink($f); } }';
expect(extractPhpMethodBody(src, 'boot')).toContain('unlink($f);');
});
it('returns empty string when method absent', () => {
expect(extractPhpMethodBody('function other() {}', 'boot')).toBe('');
});
});
describe('decideBootScan', () => {
const editedProvider = ['app/Providers/AppServiceProvider.php'];
function readerFor(map) {
return (p) => {
const key = p.replace(/\\/g, '/');
if (!(key in map)) throw new Error('ENOENT');
return map[key];
};
}
it('passes (block:false) when command is not a test runner', () => {
const r = decideBootScan({ command: 'git status', projectTypes: ['laravel'], editedFiles: editedProvider, readFile: readerFor({}) });
expect(r.block).toBe(false);
});
it('passes when no project type detected', () => {
const r = decideBootScan({ command: 'composer test', projectTypes: [], editedFiles: editedProvider, readFile: readerFor({}) });
expect(r.block).toBe(false);
});
it('blocks on non-empty boot-file intersection (test runner + edited boot file)', () => {
const r = decideBootScan({
command: 'composer test',
projectTypes: ['laravel'],
editedFiles: editedProvider,
readFile: readerFor({ 'app/Providers/AppServiceProvider.php': '<?php class X {}' }),
});
expect(r.block).toBe(true);
expect(r.reason).toMatch(/boot/i);
expect(r.intersection).toContain('app/Providers/AppServiceProvider.php');
});
it('surfaces suspicious findings from the edited boot file', () => {
const r = decideBootScan({
command: 'composer test',
projectTypes: ['laravel'],
editedFiles: editedProvider,
readFile: readerFor({ 'app/Providers/AppServiceProvider.php': '<?php exec($payload);' }),
});
expect(r.block).toBe(true);
expect(r.findings.some((f) => f.name === 'exec')).toBe(true);
});
it('scans only boot() body for Model files', () => {
const r = decideBootScan({
command: 'php artisan test',
projectTypes: ['laravel'],
editedFiles: ['app/Models/Deal.php'],
readFile: readerFor({ 'app/Models/Deal.php': 'class Deal { public function scopeActive() { exec($c); } public function boot() {} }' }),
});
expect(r.block).toBe(true);
expect(r.findings.some((f) => f.name === 'exec')).toBe(false);
});
it('still blocks even if a payload is hidden behind a } inside a string (findings are best-effort, block is the control)', () => {
const r = decideBootScan({
command: 'php artisan test',
projectTypes: ['laravel'],
editedFiles: ['app/Models/Deal.php'],
readFile: readerFor({ 'app/Models/Deal.php': 'class Deal { public function boot() { $s = "}"; exec($x); } }' }),
});
// The brace matcher is string/comment-naive, so the hidden exec may be absent
// from `findings` — but the intersection-based hard-block still fires, which is
// the actual security control. This test pins that guarantee.
expect(r.block).toBe(true);
expect(r.intersection).toContain('app/Models/Deal.php');
});
});