forked from fa/breadcrumb-the-shire
139 lines
4.2 KiB
PHP
139 lines
4.2 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Tests\Architecture;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* GR-SEC-004: No external CDN/remote assets in app templates/pages/styles.
|
|
*/
|
|
final class SecurityRemoteAssetContractTest extends TestCase
|
|
{
|
|
use ProjectFileAssertionSupport;
|
|
|
|
public function testNoRemoteScriptOrStylesheetTagsInAppTemplatesAndPages(): void
|
|
{
|
|
$files = array_merge(
|
|
$this->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<string> $extensions
|
|
* @return list<string>
|
|
*/
|
|
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<string> $extensions
|
|
* @return list<string>
|
|
*/
|
|
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<string> $files
|
|
* @return list<string>
|
|
*/
|
|
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;
|
|
}
|
|
}
|