Files
php-security-linter/src/Analyzer/FileAnalyzer.php
Yutaka Kurosaki 6280290898 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>
2026-01-31 15:18:53 +09:00

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;
}
}