143 lines
4.1 KiB
PHP
143 lines
4.1 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace SecurityLinter\Analyzer;
|
||
|
|
|
||
|
|
use PhpParser\ParserFactory;
|
||
|
|
use PhpParser\Parser;
|
||
|
|
use PhpParser\NodeTraverser;
|
||
|
|
use PhpParser\NodeVisitor\NameResolver;
|
||
|
|
use PhpParser\Error;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Parses PHP files into AST
|
||
|
|
*/
|
||
|
|
class FileAnalyzer
|
||
|
|
{
|
||
|
|
private Parser $parser;
|
||
|
|
private array $errors = [];
|
||
|
|
|
||
|
|
public function __construct()
|
||
|
|
{
|
||
|
|
$this->parser = (new ParserFactory())->createForNewestSupportedVersion();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Parse PHP code into AST
|
||
|
|
*/
|
||
|
|
public function parse(string $code, string $filePath = ''): ?array
|
||
|
|
{
|
||
|
|
try {
|
||
|
|
$ast = $this->parser->parse($code);
|
||
|
|
|
||
|
|
if ($ast === null) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Resolve names to fully qualified
|
||
|
|
$traverser = new NodeTraverser();
|
||
|
|
$traverser->addVisitor(new NameResolver());
|
||
|
|
$ast = $traverser->traverse($ast);
|
||
|
|
|
||
|
|
return $ast;
|
||
|
|
} catch (Error $e) {
|
||
|
|
$this->errors[] = [
|
||
|
|
'file' => $filePath,
|
||
|
|
'message' => $e->getMessage(),
|
||
|
|
'line' => $e->getStartLine(),
|
||
|
|
];
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Parse a Blade template
|
||
|
|
*/
|
||
|
|
public function parseBlade(string $code, string $filePath): array
|
||
|
|
{
|
||
|
|
$result = [
|
||
|
|
'raw_outputs' => [], // {!! $var !!}
|
||
|
|
'escaped_outputs' => [], // {{ $var }}
|
||
|
|
'php_blocks' => [], // @php ... @endphp
|
||
|
|
'includes' => [], // @include, @extends, @component
|
||
|
|
'forms' => [], // <form> tags
|
||
|
|
'csrf_tokens' => [], // @csrf directives
|
||
|
|
];
|
||
|
|
|
||
|
|
// Find raw (unescaped) outputs - security risk
|
||
|
|
preg_match_all('/\{!!\s*(.+?)\s*!!\}/s', $code, $matches, PREG_OFFSET_CAPTURE);
|
||
|
|
foreach ($matches[1] as $match) {
|
||
|
|
$result['raw_outputs'][] = [
|
||
|
|
'expression' => $match[0],
|
||
|
|
'line' => $this->getLineNumber($code, $match[1]),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find escaped outputs
|
||
|
|
preg_match_all('/\{\{\s*(.+?)\s*\}\}/s', $code, $matches, PREG_OFFSET_CAPTURE);
|
||
|
|
foreach ($matches[1] as $match) {
|
||
|
|
$result['escaped_outputs'][] = [
|
||
|
|
'expression' => $match[0],
|
||
|
|
'line' => $this->getLineNumber($code, $match[1]),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find @php blocks
|
||
|
|
preg_match_all('/@php\s*(.*?)\s*@endphp/s', $code, $matches, PREG_OFFSET_CAPTURE);
|
||
|
|
foreach ($matches[1] as $match) {
|
||
|
|
$result['php_blocks'][] = [
|
||
|
|
'code' => $match[0],
|
||
|
|
'line' => $this->getLineNumber($code, $match[1]),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find includes
|
||
|
|
preg_match_all('/@(include|extends|component)\s*\(\s*[\'"](.+?)[\'"]/s', $code, $matches, PREG_OFFSET_CAPTURE);
|
||
|
|
foreach ($matches[2] as $i => $match) {
|
||
|
|
$result['includes'][] = [
|
||
|
|
'type' => $matches[1][$i][0],
|
||
|
|
'path' => $match[0],
|
||
|
|
'line' => $this->getLineNumber($code, $match[1]),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find forms
|
||
|
|
preg_match_all('/<form[^>]*>/i', $code, $matches, PREG_OFFSET_CAPTURE);
|
||
|
|
foreach ($matches[0] as $match) {
|
||
|
|
$line = $this->getLineNumber($code, $match[1]);
|
||
|
|
$result['forms'][] = [
|
||
|
|
'tag' => $match[0],
|
||
|
|
'line' => $line,
|
||
|
|
'has_method' => stripos($match[0], 'method=') !== false,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find CSRF tokens
|
||
|
|
preg_match_all('/@csrf/i', $code, $matches, PREG_OFFSET_CAPTURE);
|
||
|
|
foreach ($matches[0] as $match) {
|
||
|
|
$result['csrf_tokens'][] = [
|
||
|
|
'line' => $this->getLineNumber($code, $match[1]),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get line number from offset
|
||
|
|
*/
|
||
|
|
private function getLineNumber(string $code, int $offset): int
|
||
|
|
{
|
||
|
|
return substr_count(substr($code, 0, $offset), "\n") + 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get parse errors
|
||
|
|
*/
|
||
|
|
public function getErrors(): array
|
||
|
|
{
|
||
|
|
return $this->errors;
|
||
|
|
}
|
||
|
|
}
|