getOption('format') ?? 'table';
$noDevDeps = $input->hasOption('no-dev');
$failOnVulnerabilities = $input->hasOption('fail-on-vulnerabilities');
$output->writeLine("Running Security Audit...\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("\nAudit completed in " . number_format($duration, 2) . "s");
// Return appropriate exit code
if ($returnCode === 0) {
$output->writeLine("\n✓ No security vulnerabilities found!");
return ExitCode::SUCCESS;
}
$output->writeLine("\n✗ Security vulnerabilities detected!");
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("Security Audit Summary\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('Failed to parse audit results');
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' => "{$severityCounts['critical']}",
'High' => "{$severityCounts['high']}",
'Medium' => "{$severityCounts['medium']}",
'Low' => "{$severityCounts['low']}",
'Affected Packages' => count($affectedPackages),
'Abandoned Packages' => count($auditData['abandoned'] ?? []),
];
$responsiveOutput->writeKeyValue($summary);
if ($totalVulnerabilities > 0) {
$output->writeLine("\n⚠ Security vulnerabilities detected!");
$output->writeLine("Run php console.php security:audit for details");
} else {
$output->writeLine("\n✓ No security vulnerabilities found!");
}
if (!empty($auditData['abandoned'])) {
$output->writeLine("\nAbandoned Packages:");
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('Package name is required');
$output->writeLine('Usage: php console.php security:audit-details ');
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('Failed to parse audit results');
return ExitCode::FAILURE;
}
// Find package advisories
$advisories = $auditData['advisories'][$packageName] ?? null;
if (!$advisories) {
$output->writeLine("No vulnerabilities found for package: {$packageName}");
return ExitCode::SUCCESS;
}
$output->writeLine("Vulnerabilities for {$packageName}\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('✓ No security issues found!');
return;
}
if (!empty($advisories)) {
$output->writeLine("Security Vulnerabilities:\n");
foreach ($advisories as $packageName => $packageAdvisories) {
$output->writeLine("{$packageName}");
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}] {$cve}: {$title}");
if (!empty($advisory['link'])) {
$output->writeLine(" Link: {$advisory['link']}");
}
}
$output->writeLine('');
}
}
if (!empty($abandoned)) {
$output->writeLine("Abandoned Packages:\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("\nMore Info: {$advisory['link']}");
}
$output->writeLine('');
}
private function getSeverityColor(string $severity): string
{
return match (strtolower($severity)) {
'critical' => '',
'high' => '',
'medium' => '',
'low' => '',
default => '',
};
}
private function formatSeverity(string $severity): string
{
$color = $this->getSeverityColor($severity);
return "{$color}" . strtoupper($severity) . '';
}
private function determineExitCode(array $auditData, bool $failOnVulnerabilities): ExitCode
{
$hasVulnerabilities = !empty($auditData['advisories']);
if ($hasVulnerabilities && $failOnVulnerabilities) {
return ExitCode::FAILURE;
}
return ExitCode::SUCCESS;
}
}