Initial commit: PHP/Laravel Security Linter v1.0.0
A static security analysis tool for PHP and Laravel applications with recursive taint analysis capabilities. Features: - Comprehensive vulnerability detection (XSS, SQL Injection, Command Injection, Path Traversal, CSRF, Authentication issues) - Recursive taint analysis across function calls - Blade template analysis with context-aware XSS detection - Smart escape detection and escape bypass detection - Syntax highlighting in terminal output - Multi-language support (Japanese/English) - Docker support for easy deployment - Multiple output formats (text, JSON, HTML, SARIF, Markdown) - CI/CD integration ready (GitHub Actions, GitLab CI) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
142
src/Analyzer/FileAnalyzer.php
Normal file
142
src/Analyzer/FileAnalyzer.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user