- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
271 lines
9.6 KiB
PHP
271 lines
9.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Console\Security\Commands;
|
|
|
|
use App\Framework\Console\ConsoleCommand;
|
|
use App\Framework\Console\ExitCode;
|
|
use App\Framework\Console\Input\ConsoleInput;
|
|
use App\Framework\Console\Layout\ResponsiveOutput;
|
|
use App\Framework\Console\Output\ConsoleOutput;
|
|
|
|
final readonly class DependencyAuditCommand
|
|
{
|
|
#[ConsoleCommand(
|
|
name: 'security:audit',
|
|
description: 'Audit dependencies for known security vulnerabilities'
|
|
)]
|
|
public function audit(ConsoleInput $input, ConsoleOutput $output): ExitCode
|
|
{
|
|
$format = $input->getOption('format') ?? 'table';
|
|
$noDevDeps = $input->hasOption('no-dev');
|
|
$failOnVulnerabilities = $input->hasOption('fail-on-vulnerabilities');
|
|
|
|
$output->writeLine("<yellow>Running Security Audit...</yellow>\n");
|
|
|
|
// Build composer audit command
|
|
$command = 'composer audit --format=' . escapeshellarg($format);
|
|
if ($noDevDeps) {
|
|
$command .= ' --no-dev';
|
|
}
|
|
|
|
// Execute composer audit
|
|
$startTime = microtime(true);
|
|
exec($command . ' 2>&1', $outputLines, $returnCode);
|
|
$duration = microtime(true) - $startTime;
|
|
|
|
$outputText = implode("\n", $outputLines);
|
|
|
|
// Parse JSON output if format is JSON
|
|
if ($format === 'json') {
|
|
$auditData = json_decode($outputText, true);
|
|
|
|
if (json_last_error() === JSON_ERROR_NONE) {
|
|
$this->displayJsonResults($auditData, $output);
|
|
|
|
return $this->determineExitCode(
|
|
$auditData,
|
|
$failOnVulnerabilities
|
|
);
|
|
}
|
|
}
|
|
|
|
// Display raw output for non-JSON formats
|
|
$output->writeLine($outputText);
|
|
$output->writeLine("\n<gray>Audit completed in " . number_format($duration, 2) . "s</gray>");
|
|
|
|
// Return appropriate exit code
|
|
if ($returnCode === 0) {
|
|
$output->writeLine("\n<green>✓ No security vulnerabilities found!</green>");
|
|
return ExitCode::SUCCESS;
|
|
}
|
|
|
|
$output->writeLine("\n<red>✗ Security vulnerabilities detected!</red>");
|
|
return $failOnVulnerabilities ? ExitCode::FAILURE : ExitCode::SUCCESS;
|
|
}
|
|
|
|
#[ConsoleCommand(
|
|
name: 'security:audit-summary',
|
|
description: 'Show summary of dependency security audit'
|
|
)]
|
|
public function auditSummary(ConsoleInput $input, ConsoleOutput $output): ExitCode
|
|
{
|
|
$output->writeLine("<yellow>Security Audit Summary</yellow>\n");
|
|
|
|
// Execute composer audit with JSON format
|
|
exec('composer audit --format=json 2>&1', $outputLines, $returnCode);
|
|
$auditData = json_decode(implode("\n", $outputLines), true);
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
$output->writeLine('<red>Failed to parse audit results</red>');
|
|
return ExitCode::FAILURE;
|
|
}
|
|
|
|
$responsiveOutput = ResponsiveOutput::create($output);
|
|
|
|
// Count vulnerabilities by severity
|
|
$severityCounts = [
|
|
'critical' => 0,
|
|
'high' => 0,
|
|
'medium' => 0,
|
|
'low' => 0,
|
|
];
|
|
|
|
$totalVulnerabilities = 0;
|
|
$affectedPackages = [];
|
|
|
|
foreach ($auditData['advisories'] ?? [] as $packageName => $advisories) {
|
|
$affectedPackages[] = $packageName;
|
|
|
|
foreach ($advisories as $advisory) {
|
|
$severity = strtolower($advisory['severity'] ?? 'unknown');
|
|
if (isset($severityCounts[$severity])) {
|
|
$severityCounts[$severity]++;
|
|
}
|
|
$totalVulnerabilities++;
|
|
}
|
|
}
|
|
|
|
// Display summary
|
|
$summary = [
|
|
'Total Vulnerabilities' => $totalVulnerabilities,
|
|
'Critical' => "<red>{$severityCounts['critical']}</red>",
|
|
'High' => "<yellow>{$severityCounts['high']}</yellow>",
|
|
'Medium' => "<cyan>{$severityCounts['medium']}</cyan>",
|
|
'Low' => "<gray>{$severityCounts['low']}</gray>",
|
|
'Affected Packages' => count($affectedPackages),
|
|
'Abandoned Packages' => count($auditData['abandoned'] ?? []),
|
|
];
|
|
|
|
$responsiveOutput->writeKeyValue($summary);
|
|
|
|
if ($totalVulnerabilities > 0) {
|
|
$output->writeLine("\n<red>⚠ Security vulnerabilities detected!</red>");
|
|
$output->writeLine("Run <cyan>php console.php security:audit</cyan> for details");
|
|
} else {
|
|
$output->writeLine("\n<green>✓ No security vulnerabilities found!</green>");
|
|
}
|
|
|
|
if (!empty($auditData['abandoned'])) {
|
|
$output->writeLine("\n<yellow>Abandoned Packages:</yellow>");
|
|
foreach ($auditData['abandoned'] as $package => $replacement) {
|
|
$replacementText = $replacement ? " (use {$replacement} instead)" : '';
|
|
$output->writeLine(" • {$package}{$replacementText}");
|
|
}
|
|
}
|
|
|
|
return $totalVulnerabilities > 0 ? ExitCode::FAILURE : ExitCode::SUCCESS;
|
|
}
|
|
|
|
#[ConsoleCommand(
|
|
name: 'security:audit-details',
|
|
description: 'Show detailed information about a specific package vulnerability'
|
|
)]
|
|
public function auditDetails(ConsoleInput $input, ConsoleOutput $output): ExitCode
|
|
{
|
|
$packageName = $input->getArgument('package');
|
|
|
|
if (!$packageName) {
|
|
$output->writeLine('<red>Package name is required</red>');
|
|
$output->writeLine('Usage: php console.php security:audit-details <package>');
|
|
return ExitCode::INVALID_ARGUMENTS;
|
|
}
|
|
|
|
// Execute composer audit with JSON format
|
|
exec('composer audit --format=json 2>&1', $outputLines);
|
|
$auditData = json_decode(implode("\n", $outputLines), true);
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
$output->writeLine('<red>Failed to parse audit results</red>');
|
|
return ExitCode::FAILURE;
|
|
}
|
|
|
|
// Find package advisories
|
|
$advisories = $auditData['advisories'][$packageName] ?? null;
|
|
|
|
if (!$advisories) {
|
|
$output->writeLine("<yellow>No vulnerabilities found for package: {$packageName}</yellow>");
|
|
return ExitCode::SUCCESS;
|
|
}
|
|
|
|
$output->writeLine("<yellow>Vulnerabilities for {$packageName}</yellow>\n");
|
|
|
|
foreach ($advisories as $advisory) {
|
|
$this->displayAdvisoryDetails($advisory, $output);
|
|
$output->writeLine(str_repeat('─', 80));
|
|
}
|
|
|
|
return ExitCode::SUCCESS;
|
|
}
|
|
|
|
private function displayJsonResults(array $auditData, ConsoleOutput $output): void
|
|
{
|
|
$advisories = $auditData['advisories'] ?? [];
|
|
$abandoned = $auditData['abandoned'] ?? [];
|
|
|
|
if (empty($advisories) && empty($abandoned)) {
|
|
$output->writeLine('<green>✓ No security issues found!</green>');
|
|
return;
|
|
}
|
|
|
|
if (!empty($advisories)) {
|
|
$output->writeLine("<red>Security Vulnerabilities:</red>\n");
|
|
|
|
foreach ($advisories as $packageName => $packageAdvisories) {
|
|
$output->writeLine("<yellow>{$packageName}</yellow>");
|
|
|
|
foreach ($packageAdvisories as $advisory) {
|
|
$severity = $advisory['severity'] ?? 'unknown';
|
|
$severityColor = $this->getSeverityColor($severity);
|
|
$cve = $advisory['cve'] ?? 'N/A';
|
|
$title = $advisory['title'] ?? 'No title';
|
|
|
|
$output->writeLine(" • [{$severityColor}{$severity}</color>] {$cve}: {$title}");
|
|
if (!empty($advisory['link'])) {
|
|
$output->writeLine(" Link: <cyan>{$advisory['link']}</cyan>");
|
|
}
|
|
}
|
|
$output->writeLine('');
|
|
}
|
|
}
|
|
|
|
if (!empty($abandoned)) {
|
|
$output->writeLine("<yellow>Abandoned Packages:</yellow>\n");
|
|
foreach ($abandoned as $package => $replacement) {
|
|
$replacementText = $replacement ? " (use {$replacement})" : '';
|
|
$output->writeLine(" • {$package}{$replacementText}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private function displayAdvisoryDetails(array $advisory, ConsoleOutput $output): void
|
|
{
|
|
$responsiveOutput = ResponsiveOutput::create($output);
|
|
|
|
$details = [
|
|
'Title' => $advisory['title'] ?? 'Unknown',
|
|
'CVE' => $advisory['cve'] ?? 'N/A',
|
|
'Severity' => $this->formatSeverity($advisory['severity'] ?? 'unknown'),
|
|
'Affected Versions' => $advisory['affectedVersions'] ?? 'N/A',
|
|
'Source' => $advisory['sources'][0]['remoteId'] ?? 'N/A',
|
|
];
|
|
|
|
$responsiveOutput->writeKeyValue($details);
|
|
|
|
if (!empty($advisory['link'])) {
|
|
$output->writeLine("\n<cyan>More Info:</cyan> {$advisory['link']}");
|
|
}
|
|
|
|
$output->writeLine('');
|
|
}
|
|
|
|
private function getSeverityColor(string $severity): string
|
|
{
|
|
return match (strtolower($severity)) {
|
|
'critical' => '<red>',
|
|
'high' => '<yellow>',
|
|
'medium' => '<cyan>',
|
|
'low' => '<gray>',
|
|
default => '<white>',
|
|
};
|
|
}
|
|
|
|
private function formatSeverity(string $severity): string
|
|
{
|
|
$color = $this->getSeverityColor($severity);
|
|
return "{$color}" . strtoupper($severity) . '</color>';
|
|
}
|
|
|
|
private function determineExitCode(array $auditData, bool $failOnVulnerabilities): ExitCode
|
|
{
|
|
$hasVulnerabilities = !empty($auditData['advisories']);
|
|
|
|
if ($hasVulnerabilities && $failOnVulnerabilities) {
|
|
return ExitCode::FAILURE;
|
|
}
|
|
|
|
return ExitCode::SUCCESS;
|
|
}
|
|
}
|