Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
74 changes: 4 additions & 70 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

-
Expand All @@ -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
Expand All @@ -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

-
Expand Down Expand Up @@ -447,43 +405,19 @@ 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

-
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

-
Expand Down
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ includes:
- phpstan-baseline.neon

parameters:
level: 10
disallowedFunctionCalls:
-
function: 'exec()'
Expand Down
87 changes: 74 additions & 13 deletions src/Controller/Admin/Monitoring/HealthCheckController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string|NpmAuditAdvisory>, effects?: list<string>, nodes?: list<string>, isDirect?: bool}
*/
class HealthCheckController extends AbstractController
{
protected array $currentVersions = [];
Expand Down Expand Up @@ -135,9 +139,9 @@
}
}

private function getJsVulnerabilities(): array

Check warning on line 142 in src/Controller/Admin/Monitoring/HealthCheckController.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This method has 5 returns, which is more than the 3 allowed.

See more on https://sonarcloud.io/project/issues?id=smartbooster_sonata-bundle&issues=AaFEL4nwpeEU4kzpt6AD&open=AaFEL4nwpeEU4kzpt6AD&pullRequest=88

Check failure on line 142 in src/Controller/Admin/Monitoring/HealthCheckController.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 19 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=smartbooster_sonata-bundle&issues=AaFEL4nwpeEU4kzpt6AE&open=AaFEL4nwpeEU4kzpt6AE&pullRequest=88
{
$process = new Process(['yarn', 'audit', '--json']);
$process = new Process(['npm', 'audit', '--json']);
$process->setWorkingDirectory($this->projectDir);
$process->run();

Expand All @@ -147,6 +151,15 @@
}

try {
/** @var array{error?: array{summary?: string}, vulnerabilities?: array<string, NpmAuditVulnerability>} $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,
Expand All @@ -157,24 +170,34 @@
];
$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,
];
}
}
Expand All @@ -185,6 +208,44 @@
}
}

/**
* Remonte les "effects" (packages qui dépendent du package vulnérable) jusqu'à une dépendance directe du projet
*
* @param array<string, NpmAuditVulnerability> $auditVulnerabilities
* @return list<string> 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 [
Expand Down
6 changes: 3 additions & 3 deletions templates/admin/monitoring/health_check.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,12 @@
</td>
<td>
<div>
<a href="https://classic.yarnpkg.com/en/package/{{ row.package }}#v{{ row.version }}" target="_blank">{{ row.package }}</a></div>
<a href="https://www.npmjs.com/package/{{ row.package }}/v/{{ row.version }}" target="_blank">{{ row.package }}</a></div>
{% if row.dependent.parent != '' %}
<div class="text-xs" >
Dépendance de :
<a
href="https://classic.yarnpkg.com/en/package/{{ row.dependent.parent }}"
href="https://www.npmjs.com/package/{{ row.dependent.parent }}"
title="{{ row.dependent.chain }}"
target="_blank"
>
Expand All @@ -206,7 +206,7 @@
</div>
{% endif %}
<div class="text-xs">Version courante affecté : {{ row.version }}</div>
<div v-if="row.patched_versions" class="text-xs">Patché pour les versions : {{ row.patched_versions }}</div>
{% if row.vulnerable_versions %}<div class="text-xs">Versions vulnérables : {{ row.vulnerable_versions }}</div>{% endif %}
</td>
<td><a target="_blank" href="{{ row.link }}">Voir le rapport</a></td>
</tr>
Expand Down
Loading