diff --git a/CHANGELOG.md b/CHANGELOG.md index 26724c5..fc6e61e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ CHANGELOG =================== +## v2.14.0 - (2026-10-16) +### Changed +- `HealthCheckController::getJsVulnerabilities` switch front package manager to audit javacscript CVE from yarn to npm. + ## v2.13.0 - (2026-09-23) ### Added - `smart_sonata.documentation` config diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index dec8798..aacd1a0 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -276,12 +276,6 @@ parameters: count: 1 path: src/Controller/Admin/Monitoring/HealthCheckController.php - - - message: '#^Cannot access offset ''advisory'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Controller/Admin/Monitoring/HealthCheckController.php - - message: '#^Cannot access offset ''current'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible @@ -294,48 +288,18 @@ parameters: count: 1 path: src/Controller/Admin/Monitoring/HealthCheckController.php - - - message: '#^Cannot access offset ''data'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Controller/Admin/Monitoring/HealthCheckController.php - - message: '#^Cannot access offset ''description'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 1 path: src/Controller/Admin/Monitoring/HealthCheckController.php - - - message: '#^Cannot access offset ''findings'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Controller/Admin/Monitoring/HealthCheckController.php - - message: '#^Cannot access offset ''last'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 1 path: src/Controller/Admin/Monitoring/HealthCheckController.php - - - message: '#^Cannot access offset ''module_name'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Controller/Admin/Monitoring/HealthCheckController.php - - - - message: '#^Cannot access offset ''patched_versions'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Controller/Admin/Monitoring/HealthCheckController.php - - - - message: '#^Cannot access offset ''paths'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Controller/Admin/Monitoring/HealthCheckController.php - - message: '#^Cannot access offset ''release'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible @@ -351,7 +315,7 @@ parameters: - message: '#^Cannot access offset ''severity'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible - count: 4 + count: 3 path: src/Controller/Admin/Monitoring/HealthCheckController.php - @@ -360,12 +324,6 @@ parameters: count: 2 path: src/Controller/Admin/Monitoring/HealthCheckController.php - - - message: '#^Cannot access offset ''type'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Controller/Admin/Monitoring/HealthCheckController.php - - message: '#^Cannot access offset ''value'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible @@ -381,7 +339,7 @@ parameters: - message: '#^Cannot access offset 0 on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible - count: 5 + count: 3 path: src/Controller/Admin/Monitoring/HealthCheckController.php - @@ -447,23 +405,11 @@ parameters: - message: '#^Parameter \#1 \$advisory of method Smart\\SonataBundle\\Controller\\Admin\\Monitoring\\HealthCheckController\:\:getAdvisoryVulnerability\(\) expects array, mixed given\.$#' identifier: argument.type - count: 2 - path: src/Controller/Admin/Monitoring/HealthCheckController.php - - - - message: '#^Parameter \#1 \$haystack of function str_contains expects string, mixed given\.$#' - identifier: argument.type count: 1 path: src/Controller/Admin/Monitoring/HealthCheckController.php - - message: '#^Parameter \#1 \$haystack of function strpos expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Controller/Admin/Monitoring/HealthCheckController.php - - - - message: '#^Parameter \#1 \$haystack of function strrpos expects string, mixed given\.$#' + message: '#^Parameter \#1 \$haystack of function str_contains expects string, mixed given\.$#' identifier: argument.type count: 1 path: src/Controller/Admin/Monitoring/HealthCheckController.php @@ -471,19 +417,7 @@ parameters: - message: '#^Parameter \#1 \$string of function strtolower expects string, mixed given\.$#' identifier: argument.type - count: 4 - path: src/Controller/Admin/Monitoring/HealthCheckController.php - - - - message: '#^Parameter \#1 \$string of function substr expects string, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Controller/Admin/Monitoring/HealthCheckController.php - - - - message: '#^Parameter \#2 \$packageName of method Smart\\SonataBundle\\Controller\\Admin\\Monitoring\\HealthCheckController\:\:getAdvisoryVulnerability\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 + count: 3 path: src/Controller/Admin/Monitoring/HealthCheckController.php - diff --git a/phpstan.neon b/phpstan.neon index 0b8ba37..0609973 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -10,6 +10,7 @@ includes: - phpstan-baseline.neon parameters: + level: 10 disallowedFunctionCalls: - function: 'exec()' diff --git a/src/Controller/Admin/Monitoring/HealthCheckController.php b/src/Controller/Admin/Monitoring/HealthCheckController.php index ee61f43..15c420d 100644 --- a/src/Controller/Admin/Monitoring/HealthCheckController.php +++ b/src/Controller/Admin/Monitoring/HealthCheckController.php @@ -11,6 +11,10 @@ use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Environment; +/** + * @phpstan-type NpmAuditAdvisory array{title?: string, url?: string, severity?: string, range?: string} + * @phpstan-type NpmAuditVulnerability array{via?: list, effects?: list, nodes?: list, isDirect?: bool} + */ class HealthCheckController extends AbstractController { protected array $currentVersions = []; @@ -137,7 +141,7 @@ private function getPhpVulnerabilities(): array private function getJsVulnerabilities(): array { - $process = new Process(['yarn', 'audit', '--json']); + $process = new Process(['npm', 'audit', '--json']); $process->setWorkingDirectory($this->projectDir); $process->run(); @@ -147,6 +151,15 @@ private function getJsVulnerabilities(): array } try { + /** @var array{error?: array{summary?: string}, vulnerabilities?: array} $auditOutput */ + $auditOutput = json_decode($output, true); + if (isset($auditOutput['error'])) { + return ['error' => $auditOutput['error']['summary'] ?? $output]; + } + if (!isset($auditOutput['vulnerabilities'])) { + return ['severityCount' => []]; + } + $severityCount = [ 'critical' => 0, 'high' => 0, @@ -157,24 +170,34 @@ private function getJsVulnerabilities(): array ]; $vulnerabilities = []; - // Yarn audit --json retourne une ligne JSON par événement (auditAdvisory) - foreach (explode("\n", trim($output)) as $line) { - $data = json_decode($line, true); - if (isset($data['type']) && $data['type'] === 'auditAdvisory') { - $advisory = $data['data']['advisory']; + // npm audit --json (auditReportVersion 2) groupe par package vulnérable : dans "via", les objets sont les + // advisories propres au package, les chaînes ne font que pointer vers un autre package vulnérable + foreach ($auditOutput['vulnerabilities'] as $packageName => $vulnerability) { + foreach ($vulnerability['via'] ?? [] as $advisory) { + if (!is_array($advisory)) { + continue; + } // Compter les CVE par sévérité $severityValue = strtolower($advisory['severity'] ?? 'unknown'); + if (!isset($severityCount[$severityValue])) { + $severityValue = 'unknown'; // ex: info + } $severityCount[$severityValue]++; - $firstDependent = $advisory['findings'][0]['paths'][0] ?? null; + $dependencyPath = $this->getNpmDependencyPath($auditOutput['vulnerabilities'], $packageName); $vulnerabilities[] = [ - ...$this->getAdvisoryVulnerability($advisory, $advisory['module_name'], $severityValue), - 'dependent' => $firstDependent !== null ? [ - 'parent' => substr($firstDependent, 0, (int) strpos($firstDependent, '>')), - 'chain' => substr($firstDependent, 0, (int) strrpos($firstDependent, '>')), - ] : null, - 'patched_versions' => $advisory['patched_versions'] ?? null, + ...$this->getAdvisoryVulnerability([ + ...$advisory, + // npm ne fournit pas de CVE, on affiche l'identifiant GHSA de l'url de l'advisory + 'cve' => basename($advisory['url'] ?? '') ?: null, + 'findings' => [['version' => $this->getNpmInstalledVersion($vulnerability['nodes'][0] ?? null)]], + ], $packageName, $severityValue), + 'dependent' => [ + 'parent' => count($dependencyPath) > 1 ? $dependencyPath[0] : '', + 'chain' => implode('>', array_slice($dependencyPath, 0, -1)), + ], + 'vulnerable_versions' => $advisory['range'] ?? null, ]; } } @@ -185,6 +208,44 @@ private function getJsVulnerabilities(): array } } + /** + * Remonte les "effects" (packages qui dépendent du package vulnérable) jusqu'à une dépendance directe du projet + * + * @param array $auditVulnerabilities + * @return list chemin de la dépendance directe jusqu'au package vulnérable (ex: ['tailwindcss', 'chokidar', 'braces']) + */ + private function getNpmDependencyPath(array $auditVulnerabilities, string $packageName): array + { + $queue = [[$packageName]]; + $visited = [$packageName => true]; + while (($path = array_shift($queue)) !== null) { + $current = $path[0]; + if ($auditVulnerabilities[$current]['isDirect'] ?? false) { + return $path; + } + foreach ($auditVulnerabilities[$current]['effects'] ?? [] as $effect) { + if (!isset($visited[$effect])) { + $visited[$effect] = true; + $queue[] = [$effect, ...$path]; + } + } + } + + // Le parent direct n'est pas lui-même vulnérable donc absent du rapport npm audit + return [$packageName]; + } + + private function getNpmInstalledVersion(?string $nodePath): string + { + if ($nodePath === null || !is_file($this->projectDir . '/' . $nodePath . '/package.json')) { + return 'unknown'; + } + /** @var array{version?: string} $package */ + $package = json_decode((string) file_get_contents($this->projectDir . '/' . $nodePath . '/package.json'), true); + + return $package['version'] ?? 'unknown'; + } + private function getAdvisoryVulnerability(array $advisory, string $packageName, string $severityValue): array { return [ diff --git a/templates/admin/monitoring/health_check.html.twig b/templates/admin/monitoring/health_check.html.twig index 51c0c08..34973ad 100644 --- a/templates/admin/monitoring/health_check.html.twig +++ b/templates/admin/monitoring/health_check.html.twig @@ -192,12 +192,12 @@ + {{ row.package }} {% if row.dependent.parent != '' %}
Dépendance de : @@ -206,7 +206,7 @@
{% endif %}
Version courante affecté : {{ row.version }}
-
Patché pour les versions : {{ row.patched_versions }}
+ {% if row.vulnerable_versions %}
Versions vulnérables : {{ row.vulnerable_versions }}
{% endif %}
Voir le rapport