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