collectFilesByExtension('pages', ['php', 'phtml']), $this->collectFilesByExtension('templates', ['php', 'phtml']), $this->collectModuleFilesByExtension('pages', ['php', 'phtml']), $this->collectModuleFilesByExtension('templates', ['php', 'phtml']) ); $pattern = '/<(?:script|link)\b[^>]*\b(?:src|href)\s*=\s*["\']\s*(?:https?:)?\/\/(?!localhost|127\.0\.0\.1)/i'; $violations = $this->findPatternViolations($files, $pattern); self::assertSame( [], $violations, "Remote script/link asset references found (GR-SEC-004):\n" . implode("\n", $violations) ); } public function testNoRemoteCssImportsOrFontUrls(): void { $files = array_merge( $this->collectFilesByExtension('web/css', ['css']), $this->collectModuleFilesByExtension('web/css', ['css']) ); $importPattern = '/@import\s+(?:url\()?["\']?\s*(?:https?:)?\/\/(?!localhost|127\.0\.0\.1)/i'; $fontPattern = '/url\(\s*["\']?\s*(?:https?:)?\/\/(?!localhost|127\.0\.0\.1).*?\.(?:woff2?|woff|ttf|otf|eot)\b/i'; $violations = array_merge( $this->findPatternViolations($files, $importPattern), $this->findPatternViolations($files, $fontPattern) ); $violations = array_values(array_unique($violations)); sort($violations); self::assertSame( [], $violations, "Remote CSS imports/font URLs found (GR-SEC-004):\n" . implode("\n", $violations) ); } /** * @param list $extensions * @return list */ private function collectFilesByExtension(string $relativeDir, array $extensions): array { $root = $this->projectRootPath(); $dir = $root . '/' . $relativeDir; if (!is_dir($dir)) { return []; } $result = []; $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS) ); foreach ($iterator as $file) { if (!$file->isFile()) { continue; } if (!in_array(strtolower($file->getExtension()), $extensions, true)) { continue; } $result[] = str_replace($root . '/', '', $file->getPathname()); } sort($result); return $result; } /** * @param list $extensions * @return list */ private function collectModuleFilesByExtension(string $moduleSubDir, array $extensions): array { $root = $this->projectRootPath(); $modulesDir = $root . '/modules'; if (!is_dir($modulesDir)) { return []; } $result = []; $entries = scandir($modulesDir) ?: []; foreach ($entries as $entry) { if ($entry === '.' || $entry === '..') { continue; } $dir = $modulesDir . '/' . $entry . '/' . $moduleSubDir; if (!is_dir($dir)) { continue; } $result = array_merge($result, $this->collectFilesByExtension('modules/' . $entry . '/' . $moduleSubDir, $extensions)); } $result = array_values(array_unique($result)); sort($result); return $result; } /** * @param list $files * @return list */ private function findPatternViolations(array $files, string $pattern): array { $violations = []; foreach ($files as $relativePath) { $content = $this->readProjectFile($relativePath); if (preg_match($pattern, $content)) { $violations[] = $relativePath; } } sort($violations); return $violations; } }