diff --git a/src/Controller/PackageController.php b/src/Controller/PackageController.php index c8058d6cd..2cca9f966 100644 --- a/src/Controller/PackageController.php +++ b/src/Controller/PackageController.php @@ -1476,7 +1476,7 @@ public function securityAdvisoriesAction(Request $request, string $name): Respon { /** @var SecurityAdvisoryRepository $repo */ $repo = $this->getEM()->getRepository(SecurityAdvisory::class); - $securityAdvisories = $repo->getPackageSecurityAdvisories($name); + $securityAdvisories = $repo->findByPackageName($name); $data = []; $data['name'] = $name; @@ -1491,7 +1491,7 @@ public function securityAdvisoriesAction(Request $request, string $name): Respon $versionParser = new VersionParser(); foreach ($securityAdvisories as $advisory) { try { - $affectedVersionConstraint = $versionParser->parseConstraints($advisory['affectedVersions']); + $affectedVersionConstraint = $versionParser->parseConstraints($advisory->getAffectedVersions()); } catch (UnexpectedValueException) { // ignore parsing errors, advisory must be invalid continue; @@ -1528,7 +1528,7 @@ public function securityAdvisoryAction(Request $request, string $id): Response throw new NotFoundHttpException(); } - return $this->render('package/security_advisory.html.twig', ['advisories' => $securityAdvisories, 'id' => $id]); + return $this->render('package/security_advisory.html.twig', ['securityAdvisories' => $securityAdvisories, 'id' => $id]); } private function createAddMaintainerForm(Package $package): FormInterface diff --git a/src/Entity/SecurityAdvisory.php b/src/Entity/SecurityAdvisory.php index 13aba8009..69b3991b5 100644 --- a/src/Entity/SecurityAdvisory.php +++ b/src/Entity/SecurityAdvisory.php @@ -15,6 +15,7 @@ use App\SecurityAdvisory\AdvisoryIdGenerator; use App\SecurityAdvisory\AdvisoryParser; use App\SecurityAdvisory\FriendsOfPhpSecurityAdvisoriesSource; +use App\SecurityAdvisory\Severity; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -70,6 +71,9 @@ class SecurityAdvisory #[ORM\Column(type: 'string', nullable: true)] private string|null $composerRepository = null; + #[ORM\Column(nullable: true)] + private Severity|null $severity = null; + /** * @var Collection&Selectable */ @@ -86,26 +90,35 @@ public function __construct(RemoteSecurityAdvisory $advisory, string $source) $this->updatedAt = new DateTimeImmutable(); $this->copyAdvisory($advisory, true); - $this->addSource($this->remoteId, $source); + $this->addSource($advisory->id, $source, $advisory->severity); } public function updateAdvisory(RemoteSecurityAdvisory $advisory): void { - if (!in_array($advisory->getSource(), [null, $this->source], true)) { + $this->findSecurityAdvisorySource($advisory->source)?->update($advisory); + + $now = new DateTimeImmutable(); + if (!$this->severity && $advisory->severity) { + $this->updatedAt = $now; + $this->severity = $advisory->severity; + } + + if (!in_array($advisory->source, [null, $this->source], true)) { return; } if ( - $this->remoteId !== $advisory->getId() || - $this->packageName !== $advisory->getPackageName() || - $this->title !== $advisory->getTitle() || - $this->link !== $advisory->getLink() || - $this->cve !== $advisory->getCve() || - $this->affectedVersions !== $advisory->getAffectedVersions() || - $this->reportedAt != $advisory->getDate() || - $this->composerRepository !== $advisory->getComposerRepository() + $this->remoteId !== $advisory->id || + $this->packageName !== $advisory->packageName || + $this->title !== $advisory->title || + $this->link !== $advisory->link || + $this->cve !== $advisory->cve || + $this->affectedVersions !== $advisory->affectedVersions || + $this->reportedAt != $advisory->date || + $this->composerRepository !== $advisory->composerRepository || + ($this->severity !== $advisory->severity && $advisory->severity) ) { - $this->updatedAt = new DateTimeImmutable(); + $this->updatedAt = $now; } $this->copyAdvisory($advisory, false); @@ -113,17 +126,21 @@ public function updateAdvisory(RemoteSecurityAdvisory $advisory): void private function copyAdvisory(RemoteSecurityAdvisory $advisory, bool $initialCopy): void { - $this->remoteId = $advisory->getId(); - $this->packageName = $advisory->getPackageName(); - $this->title = $advisory->getTitle(); - $this->link = $advisory->getLink(); - $this->cve = $advisory->getCve(); - $this->affectedVersions = $advisory->getAffectedVersions(); - $this->composerRepository = $advisory->getComposerRepository(); + $this->remoteId = $advisory->id; + $this->packageName = $advisory->packageName; + $this->title = $advisory->title; + $this->link = $advisory->link; + $this->cve = $advisory->cve; + $this->affectedVersions = $advisory->affectedVersions; + $this->composerRepository = $advisory->composerRepository; // only update if the date is different to avoid ending up with a new datetime object which doctrine will want to update in the DB for nothing - if ($initialCopy || $this->reportedAt != $advisory->getDate()) { - $this->reportedAt = $advisory->getDate(); + if ($initialCopy || $this->reportedAt != $advisory->date) { + $this->reportedAt = $advisory->date; + } + + if ($initialCopy && $advisory->severity) { + $this->severity = $advisory->severity; } } @@ -179,52 +196,52 @@ public function getSource(): string public function calculateDifferenceScore(RemoteSecurityAdvisory $advisory): int { // Regard advisories where CVE + package name match as identical as the remaining data on GitHub and FriendsOfPhp can be quite different - if ($advisory->getCve() === $this->getCve() && $advisory->getPackageName() === $this->getPackageName()) { + if ($advisory->cve === $this->getCve() && $advisory->packageName === $this->getPackageName()) { return 0; } $score = 0; - if ($advisory->getId() !== $this->getRemoteId() && $this->getSource() === $advisory->getSource()) { + if ($advisory->id !== $this->getRemoteId() && $this->getSource() === $advisory->source) { $score++; } - if ($advisory->getPackageName() !== $this->getPackageName()) { + if ($advisory->packageName !== $this->getPackageName()) { $score += 99; } - if ($advisory->getTitle() !== $this->getTitle()) { + if ($advisory->title !== $this->getTitle()) { $increase = 1; // Do not increase the score if the title was just renamed to add a CVE e.g. from CVE-2022-xxx to CVE-2022-99999999 - if (AdvisoryParser::titleWithoutCve($this->getTitle()) === AdvisoryParser::titleWithoutCve($advisory->getTitle())) { + if (AdvisoryParser::titleWithoutCve($this->getTitle()) === AdvisoryParser::titleWithoutCve($advisory->title)) { $increase = 0; } $score += $increase; } - if ($advisory->getLink() !== $this->getLink() && !in_array($this->getLink(), $advisory->getReferences(), true)) { + if ($advisory->link !== $this->getLink() && !in_array($this->getLink(), $advisory->references, true)) { $score++; } - if ($advisory->getCve() !== $this->getCve()) { + if ($advisory->cve !== $this->getCve()) { $score++; // CVE ID changed from not null to different not-null value - if ($advisory->getCve() !== null && $this->getCve() !== null) { + if ($advisory->cve !== null && $this->getCve() !== null) { $score += 99; } } - if ($advisory->getAffectedVersions() !== $this->getAffectedVersions()) { + if ($advisory->affectedVersions !== $this->getAffectedVersions()) { $score++; } - if ($advisory->getComposerRepository() !== $this->composerRepository) { + if ($advisory->composerRepository !== $this->composerRepository) { $score++; } - if ($advisory->getDate() != $this->reportedAt) { + if ($advisory->date != $this->reportedAt) { $score++; } @@ -241,15 +258,20 @@ private function assignPackagistAdvisoryId(): void $this->packagistAdvisoryId = AdvisoryIdGenerator::generate(); } + public function getSeverity(): ?Severity + { + return $this->severity; + } + public function hasSources(): bool { return !$this->sources->isEmpty(); } - public function addSource(string $remoteId, string $source): void + public function addSource(string $remoteId, string $source, Severity|null $severity): void { if (null === $this->getSourceRemoteId($source)) { - $this->sources->add(new SecurityAdvisorySource($this, $remoteId, $source)); + $this->sources->add(new SecurityAdvisorySource($this, $remoteId, $source, $severity)); // FriendsOfPhp source is curated by PHP developer, trust that data over data from GitHub if ($source === FriendsOfPhpSecurityAdvisoriesSource::SOURCE_NAME) { @@ -300,7 +322,18 @@ public function getSourceRemoteId(string $source): ?string public function setupSource(): void { if (!$this->getSourceRemoteId($this->source)) { - $this->addSource($this->remoteId, $this->source); + $this->addSource($this->remoteId, $this->source, null); } } + + public function findSecurityAdvisorySource(string $search): ?SecurityAdvisorySource + { + foreach ($this->sources as $source) { + if ($source->getSource() === $search) { + return $source; + } + } + + return null; + } } diff --git a/src/Entity/SecurityAdvisoryRepository.php b/src/Entity/SecurityAdvisoryRepository.php index 90130ef0d..2b5918f9a 100644 --- a/src/Entity/SecurityAdvisoryRepository.php +++ b/src/Entity/SecurityAdvisoryRepository.php @@ -97,6 +97,22 @@ public function findByRemoteId(string $source, string $id): array ->getResult(); } + /** + * @return list + */ + public function findByPackageName(string $packageName): array + { + return $this + ->createQueryBuilder('a') + ->addSelect('s') + ->leftJoin('a.sources', 's') + ->where('a.packageName = :packageName') + ->orderBy('a.reportedAt', 'DESC') + ->setParameters(['packageName' => $packageName]) + ->getQuery() + ->getResult(); + } + /** * @return array, reportedAt: string, composerRepository: string|null}> */ @@ -136,7 +152,7 @@ public function searchSecurityAdvisories(array $packageNames, int $updatedSince) } if (!$useCache || $filterByNames) { - $sql = 'SELECT s.packagistAdvisoryId as advisoryId, s.packageName, s.remoteId, s.title, s.link, s.cve, s.affectedVersions, s.source, s.reportedAt, s.composerRepository, sa.source sourceSource, sa.remoteId sourceRemoteId + $sql = 'SELECT s.packagistAdvisoryId as advisoryId, s.packageName, s.remoteId, s.title, s.link, s.cve, s.affectedVersions, s.source, s.reportedAt, s.composerRepository, sa.source sourceSource, sa.remoteId sourceRemoteId, s.severity FROM security_advisory s INNER JOIN security_advisory_source sa ON sa.securityAdvisory_id=s.id WHERE s.updatedAt >= :updatedSince '. diff --git a/src/Entity/SecurityAdvisorySource.php b/src/Entity/SecurityAdvisorySource.php index 285ea412d..c8baa7065 100644 --- a/src/Entity/SecurityAdvisorySource.php +++ b/src/Entity/SecurityAdvisorySource.php @@ -12,6 +12,8 @@ namespace App\Entity; +use App\SecurityAdvisory\RemoteSecurityAdvisory; +use App\SecurityAdvisory\Severity; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] @@ -32,11 +34,15 @@ class SecurityAdvisorySource #[ORM\Column(type: 'string')] private string $source; - public function __construct(SecurityAdvisory $securityAdvisory, string $remoteId, string $source) + #[ORM\Column(nullable: true)] + private Severity|null $severity; + + public function __construct(SecurityAdvisory $securityAdvisory, string $remoteId, string $source, Severity|null $severity) { $this->securityAdvisory = $securityAdvisory; $this->remoteId = $remoteId; $this->source = $source; + $this->severity = $severity; } public function getRemoteId(): string @@ -48,4 +54,14 @@ public function getSource(): string { return $this->source; } + + public function getSeverity(): ?Severity + { + return $this->severity; + } + + public function update(RemoteSecurityAdvisory $advisory): void + { + $this->severity = $advisory->severity; + } } diff --git a/src/SecurityAdvisory/GitHubSecurityAdvisoriesSource.php b/src/SecurityAdvisory/GitHubSecurityAdvisoriesSource.php index 0ce5173bb..c3a2bdf8c 100644 --- a/src/SecurityAdvisory/GitHubSecurityAdvisoriesSource.php +++ b/src/SecurityAdvisory/GitHubSecurityAdvisoriesSource.php @@ -125,6 +125,7 @@ public function getAdvisories(ConsoleIO $io): ?RemoteSecurityAdvisoryCollection $this->providerManager->packageExists($packageName) ? SecurityAdvisory::PACKAGIST_ORG : null, $references, self::SOURCE_NAME, + Severity::fromGitHub($node['advisory']['severity']), ); } @@ -157,6 +158,7 @@ private function getQuery(string $after = ''): string permalink, publishedAt, withdrawnAt, + severity, identifiers { type, value diff --git a/src/SecurityAdvisory/RemoteSecurityAdvisory.php b/src/SecurityAdvisory/RemoteSecurityAdvisory.php index 2e12b714c..4e7762258 100644 --- a/src/SecurityAdvisory/RemoteSecurityAdvisory.php +++ b/src/SecurityAdvisory/RemoteSecurityAdvisory.php @@ -35,85 +35,33 @@ class RemoteSecurityAdvisory * @param list $references */ public function __construct( - private string $id, - private string $title, - private string $packageName, - private string $affectedVersions, - private string $link, - private ?string $cve, - private DateTimeImmutable $date, - private ?string $composerRepository, - private array $references, - private string $source, + public readonly string $id, + public readonly string $title, + public readonly string $packageName, + public readonly string $affectedVersions, + public readonly string $link, + public readonly ?string $cve, + public readonly DateTimeImmutable $date, + public readonly ?string $composerRepository, + public readonly array $references, + public readonly string $source, + public readonly ?Severity $severity, ) { } - - public function getId(): string - { - return $this->id; - } - - public function getTitle(): string - { - return $this->title; - } - - public function getPackageName(): string - { - return $this->packageName; - } - - public function getAffectedVersions(): string - { - return $this->affectedVersions; - } - - public function getLink(): string - { - return $this->link; - } - - public function getCve(): ?string - { - return $this->cve; - } - - public function getDate(): DateTimeImmutable - { - return $this->date; - } - - public function getComposerRepository(): ?string - { - return $this->composerRepository; - } - - /** - * @return list - */ - public function getReferences(): array - { - return $this->references; - } - - public function getSource(): string - { - return $this->source; - } - public function withAddedAffectedVersion(string $version): self { return new self( - $this->getId(), - $this->getTitle(), - $this->getPackageName(), - implode('|', [$this->getAffectedVersions(), $version]), - $this->getLink(), - $this->getCve(), - $this->getDate(), - $this->getComposerRepository(), - $this->getReferences(), - $this->getSource(), + $this->id, + $this->title, + $this->packageName, + implode('|', [$this->affectedVersions, $version]), + $this->link, + $this->cve, + $this->date, + $this->composerRepository, + $this->references, + $this->source, + $this->severity, ); } @@ -186,7 +134,8 @@ public static function createFromFriendsOfPhp(string $fileNameWithPath, array $i $date, $composerRepository, [], - FriendsOfPhpSecurityAdvisoriesSource::SOURCE_NAME + FriendsOfPhpSecurityAdvisoriesSource::SOURCE_NAME, + null, // The FriendsOfPHP database doesn't contain severity values ); } } diff --git a/src/SecurityAdvisory/RemoteSecurityAdvisoryCollection.php b/src/SecurityAdvisory/RemoteSecurityAdvisoryCollection.php index c79918230..0ce074dbe 100644 --- a/src/SecurityAdvisory/RemoteSecurityAdvisoryCollection.php +++ b/src/SecurityAdvisory/RemoteSecurityAdvisoryCollection.php @@ -23,7 +23,7 @@ class RemoteSecurityAdvisoryCollection public function __construct(array $advisories) { foreach ($advisories as $advisory) { - $this->groupedSecurityAdvisories[$advisory->getPackageName()][] = $advisory; + $this->groupedSecurityAdvisories[$advisory->packageName][] = $advisory; } } diff --git a/src/SecurityAdvisory/SecurityAdvisoryResolver.php b/src/SecurityAdvisory/SecurityAdvisoryResolver.php index 9a0fb0f44..af2218e07 100644 --- a/src/SecurityAdvisory/SecurityAdvisoryResolver.php +++ b/src/SecurityAdvisory/SecurityAdvisoryResolver.php @@ -42,9 +42,9 @@ public function resolve(array $existingAdvisories, RemoteSecurityAdvisoryCollect $unmatchedRemoteAdvisories = []; foreach ($remoteAdvisories->getPackageNames() as $packageName) { foreach ($remoteAdvisories->getAdvisoriesForPackageName($packageName) as $remoteAdvisory) { - if (isset($existingSourceAdvisoryMap[$packageName][$remoteAdvisory->getId()])) { - $existingSourceAdvisoryMap[$packageName][$remoteAdvisory->getId()]->updateAdvisory($remoteAdvisory); - unset($existingSourceAdvisoryMap[$packageName][$remoteAdvisory->getId()]); + if (isset($existingSourceAdvisoryMap[$packageName][$remoteAdvisory->id])) { + $existingSourceAdvisoryMap[$packageName][$remoteAdvisory->id]->updateAdvisory($remoteAdvisory); + unset($existingSourceAdvisoryMap[$packageName][$remoteAdvisory->id]); } else { $unmatchedRemoteAdvisories[$packageName][] = $remoteAdvisory; } @@ -79,7 +79,7 @@ public function resolve(array $existingAdvisories, RemoteSecurityAdvisoryCollect $newAdvisories[] = new SecurityAdvisory($remoteAdvisory, $sourceName); } else { // Update advisory and make sure the new source is added - $matchedAdvisory->addSource($remoteAdvisory->getId(), $sourceName); + $matchedAdvisory->addSource($remoteAdvisory->id, $sourceName, $remoteAdvisory->severity); $matchedAdvisory->updateAdvisory($remoteAdvisory); unset($unmatchedExistingAdvisories[$packageName][$matchedAdvisory->getPackagistAdvisoryId()]); } diff --git a/src/SecurityAdvisory/Severity.php b/src/SecurityAdvisory/Severity.php new file mode 100644 index 000000000..5cada0b12 --- /dev/null +++ b/src/SecurityAdvisory/Severity.php @@ -0,0 +1,50 @@ + + * Nils Adermann + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Common Vulnerability Scoring System v3 + * + * - None: 0.0 + * - Low: 0.1 - 3.9 + * - Medium: 4.0 - 6.9 + * - High: 7.0 - 8.9 + * - Critical: 9.0 - 10.0 + * + * @see https://www.first.org/cvss/specification-document + */ +enum Severity: string +{ + case NONE = 'none'; + case LOW = 'low'; + case MEDIUM = 'medium'; + case HIGH = 'high'; + case CRITICAL = 'critical'; + + /** + * @see https://docs.github.com/en/code-security/security-advisories/working-with-global-security-advisories-from-the-github-advisory-database/about-the-github-advisory-database#about-cvss-levels + */ + public static function fromGitHub(?string $githubSeverity): ?Severity + { + if (!$githubSeverity) { + return null; + } + + // GitHub uses moderate instead of medium + if (strtolower($githubSeverity) === 'moderate') { + return self::MEDIUM; + } + + return Severity::tryFrom(strtolower($githubSeverity)); + } +} diff --git a/templates/api_doc/index.html.twig b/templates/api_doc/index.html.twig index 28151ebb2..b20a0509f 100644 --- a/templates/api_doc/index.html.twig +++ b/templates/api_doc/index.html.twig @@ -466,7 +466,8 @@ GET https://{{ packagist_host }}/api/security-advisories/?updatedSince=[timestam } ], "reportedAt": "[date the issue was reported]", - "composerRepository": "[composer repository the package can be found in]" + "composerRepository": "[composer repository the package can be found in]", + "severity": [severity if available, following the CVSS3 spec] } ] } diff --git a/templates/package/_security_advisory_list.html.twig b/templates/package/_security_advisory_list.html.twig new file mode 100644 index 000000000..95573589e --- /dev/null +++ b/templates/package/_security_advisory_list.html.twig @@ -0,0 +1,41 @@ +
+
+ {% if securityAdvisories|length %} +
    + {% for advisory in securityAdvisories %} +
  • +
    +
    +
    +

    + {% if advisory.severity %}[{{ advisory.severity.value|upper }}]{% endif %} + {{ advisory.title }} +

    +

    + {{ advisory.packagistAdvisoryId }} + {% if advisory.cve %} + {{ advisory.cve }} + {% endif %} + {% for source in advisory.sources %} + {% if source.source == 'GitHub' and source.remoteId is not empty %} + {{ source.remoteId }} + {% endif %} + {% endfor %} +

    +

    Affected version: {{ advisory.affectedVersions }}

    +
    +
    +

    Reported by:
    {% for source in advisory.sources %}{{ source.source }}{% if not loop.last %}, {% endif %}{% endfor %}

    +
    +
    +
    +
  • + {% endfor %} +
+ {% else %} +
+

{{ 'listing.no_security_advisories'|trans }}

+
+ {% endif %} +
+
diff --git a/templates/package/security_advisories.html.twig b/templates/package/security_advisories.html.twig index 5538e1393..78a58e97d 100644 --- a/templates/package/security_advisories.html.twig +++ b/templates/package/security_advisories.html.twig @@ -22,44 +22,5 @@ -
-
- {% if securityAdvisories|length %} -
    - {% for advisory in securityAdvisories %} -
  • -
    -
    -
    -

    - {{ advisory.title }} -

    -

    - {{ advisory.advisoryId }} - {% if advisory.cve %} - {{ advisory.cve }} - {% endif %} - {% for source in advisory.sources %} - {% if source.name == 'GitHub' and source.remoteId is not empty %} - {{ source.remoteId }} - {% endif %} - {% endfor %} -

    -

    Affected version: {{ advisory.affectedVersions }}

    -
    -
    -

    Reported by:
    {% for source in advisory.sources %}{{ source.name }}{% if not loop.last %}, {% endif %}{% endfor %}

    -
    -
    -
    -
  • - {% endfor %} -
- {% else %} -
-

{{ 'listing.no_security_advisories'|trans }}

-
- {% endif %} -
-
+ {% include 'package/_security_advisory_list.html.twig' %} {% endblock %} diff --git a/templates/package/security_advisory.html.twig b/templates/package/security_advisory.html.twig index b939a5555..1947fda24 100644 --- a/templates/package/security_advisory.html.twig +++ b/templates/package/security_advisory.html.twig @@ -17,39 +17,5 @@ -
-
-
    - {% for advisory in advisories %} -
  • -
    -
    -
    -

    - {{ advisory.title }} -

    -

    - {{ advisory.packagistAdvisoryId }} - {% if advisory.cve %} - {{ advisory.cve }} - {% endif %} - {% for source in advisory.sources %} - {% if source.source == 'GitHub' and source.remoteId is not empty %} - {{ source.remoteId }} - {% endif %} - {% endfor %} -

    -

    Affected package: {% if advisory.packageName is existing_package %}{{ advisory.packageName }}{% else %}{{ advisory.packageName }}{% endif %}

    -

    Affected version: {{ advisory.affectedVersions }}

    -
    -
    -

    Reported by:
    {% for source in advisory.sources %}{{ source.source }}{% if not loop.last %}, {% endif %}{% endfor %}

    -
    -
    -
    -
  • - {% endfor %} -
-
-
+ {% include 'package/_security_advisory_list.html.twig' %} {% endblock %} diff --git a/tests/Controller/ApiControllerTest.php b/tests/Controller/ApiControllerTest.php index 3e48de315..6dec80155 100644 --- a/tests/Controller/ApiControllerTest.php +++ b/tests/Controller/ApiControllerTest.php @@ -12,6 +12,10 @@ namespace App\Tests\Controller; +use App\Entity\SecurityAdvisory; +use App\SecurityAdvisory\GitHubSecurityAdvisoriesSource; +use App\SecurityAdvisory\RemoteSecurityAdvisory; +use App\SecurityAdvisory\Severity; use Doctrine\DBAL\Connection; use Doctrine\Persistence\ManagerRegistry; use Exception; @@ -160,4 +164,31 @@ public static function urlProvider(): array ['update-package', 'ssh://ghe.example.org/user/jjjjj.git', false], ]; } + + public function testSecurityAdvisories(): void + { + $advisory = new SecurityAdvisory(new RemoteSecurityAdvisory( + 'GHSA-1234-1234-1234', + 'Advisory Title', + 'acme/package', + '<1.0.1', + 'https://example.org', + 'CVE-12345', + new \DateTimeImmutable(), + SecurityAdvisory::PACKAGIST_ORG, + [], + GitHubSecurityAdvisoriesSource::SOURCE_NAME, + Severity::MEDIUM, + ), GitHubSecurityAdvisoriesSource::SOURCE_NAME); + $em = static::getContainer()->get(ManagerRegistry::class)->getManager(); + $em->persist($advisory); + $em->flush(); + + $this->client->request('GET', '/api/security-advisories/?packages[]=acme/package'); + $this->assertEquals(200, $this->client->getResponse()->getStatusCode(), $this->client->getResponse()->getContent()); + + $content = json_decode($this->client->getResponse()->getContent(), true, flags: JSON_THROW_ON_ERROR); + $this->assertArrayHasKey('acme/package', $content['advisories']); + $this->assertCount(1, $content['advisories']['acme/package']); + } } diff --git a/tests/Entity/SecurityAdvisoryTest.php b/tests/Entity/SecurityAdvisoryTest.php index d599e7cc9..5d910e700 100644 --- a/tests/Entity/SecurityAdvisoryTest.php +++ b/tests/Entity/SecurityAdvisoryTest.php @@ -14,7 +14,9 @@ use App\Entity\SecurityAdvisory; use App\SecurityAdvisory\FriendsOfPhpSecurityAdvisoriesSource; +use App\SecurityAdvisory\GitHubSecurityAdvisoriesSource; use App\SecurityAdvisory\RemoteSecurityAdvisory; +use App\SecurityAdvisory\Severity; use PHPUnit\Framework\TestCase; class SecurityAdvisoryTest extends TestCase @@ -85,33 +87,54 @@ public function testCalculateDifferenceScoreChangeNameAndCVE(): void public function testCalculateDifferenceScoreCveXXXX(): void { - $remoteAdvisory = RemoteSecurityAdvisory::createFromFriendsOfPhp('symfony/framework-bundle/CVE-2022-xxxx.yaml', [ - 'title' => 'CVE-2022-xxxx: CSRF token missing in forms', - 'link' => 'https://symfony.com/cve-2022-xxxx', - 'cve' => 'CVE-2022-xxxx', - 'branches' => [ - '5.3.x' => [ - 'time' => '2022-01-29 12:00:00', - 'versions' => ['>=5.3.14', '<=5.3.14'], - ], - '5.4.x' => [ - 'time' => '2022-01-29 12:00:00', - 'versions' => ['>=5.4.3', '<=5.4.3'], - ], - '6.0.x' => [ - 'time' => '2022-01-29 12:00:00', - 'versions' => ['>=6.0.3', '<=6.0.3'], - ], - ], - 'reference' => 'composer://symfony/framework-bundle', - ]); + $remoteAdvisory = $this->generateFriendsOfPhpRemoteAdvisory('CVE-2022-xxxx: CSRF token missing in forms', 'https://symfony.com/cve-2022-xxxx', 'CVE-2022-xxxx'); $advisory = new SecurityAdvisory($remoteAdvisory, FriendsOfPhpSecurityAdvisoriesSource::SOURCE_NAME); - $updatedRemoteAdvisory = RemoteSecurityAdvisory::createFromFriendsOfPhp('symfony/framework-bundle/CVE-2022-99999999999.yaml', [ - 'title' => 'CVE-2022-99999999999: CSRF token missing in forms', - 'link' => 'https://symfony.com/cve-2022-99999999999', - 'cve' => 'CVE-2022-99999999999', + $updatedRemoteAdvisory = $this->generateFriendsOfPhpRemoteAdvisory('CVE-2022-99999999999: CSRF token missing in forms', 'https://symfony.com/cve-2022-99999999999', 'CVE-2022-99999999999'); + + $this->assertSame(3, $advisory->calculateDifferenceScore($updatedRemoteAdvisory)); + } + + public function testStoreSeverity(): void + { + $friendsOfPhpRemoteAdvisory = $this->generateFriendsOfPhpRemoteAdvisory('CVE-2022-xxxx: CSRF token missing in forms', 'https://symfony.com/cve-2022-xxxx', 'CVE-2022-xxxx'); + $gitHubRemoteAdvisor = $this->generateGitHubAdvisory(null); + $advisory = new SecurityAdvisory($friendsOfPhpRemoteAdvisory, $friendsOfPhpRemoteAdvisory->source); + + $this->assertNull($advisory->getSeverity(), "FriendsOfPHP doesn't provide severity information"); + $advisory->addSource($gitHubRemoteAdvisor->id, GitHubSecurityAdvisoriesSource::SOURCE_NAME, null); + $advisory->updateAdvisory($this->generateGitHubAdvisory(Severity::HIGH)); + $this->assertSame(Severity::HIGH, $advisory->getSeverity(), "GitHub should update the severity severity"); + $this->assertSame(Severity::HIGH, $advisory->findSecurityAdvisorySource(GitHubSecurityAdvisoriesSource::SOURCE_NAME)?->getSeverity(), 'GitHub should update the source data'); + + $advisory->updateAdvisory($friendsOfPhpRemoteAdvisory); + $this->assertSame(Severity::HIGH, $advisory->getSeverity(), "FriendsOfPHP shouldn't reset the severity information"); + } + + private function generateGitHubAdvisory(Severity|null $severity): RemoteSecurityAdvisory + { + return new RemoteSecurityAdvisory( + 'GHSA-1234-1234-1234', + 'Tile', + 'symfony/framework-bundle', + '', + 'https://github.com/advisories/GHSA-1234-1234-1234', + null, + new \DateTimeImmutable(), + null, + [], + GitHubSecurityAdvisoriesSource::SOURCE_NAME, + $severity, + ); + } + + private function generateFriendsOfPhpRemoteAdvisory(string $title, string $link, string $cve): RemoteSecurityAdvisory + { + return RemoteSecurityAdvisory::createFromFriendsOfPhp(sprintf('symfony/framework-bundle/%s.yaml', $cve), [ + 'title' => $title, + 'link' => $link, + 'cve' => $cve, 'branches' => [ '5.3.x' => [ 'time' => '2022-01-29 12:00:00', @@ -128,7 +151,5 @@ public function testCalculateDifferenceScoreCveXXXX(): void ], 'reference' => 'composer://symfony/framework-bundle', ]); - - $this->assertSame(3, $advisory->calculateDifferenceScore($updatedRemoteAdvisory)); } } diff --git a/tests/SecurityAdvisory/GitHubSecurityAdvisoriesSourceTest.php b/tests/SecurityAdvisory/GitHubSecurityAdvisoriesSourceTest.php index f7907e952..3adef444a 100644 --- a/tests/SecurityAdvisory/GitHubSecurityAdvisoriesSourceTest.php +++ b/tests/SecurityAdvisory/GitHubSecurityAdvisoriesSourceTest.php @@ -16,6 +16,7 @@ use App\Entity\SecurityAdvisory; use App\Model\ProviderManager; use App\SecurityAdvisory\GitHubSecurityAdvisoriesSource; +use App\SecurityAdvisory\Severity; use Composer\IO\BufferIO; use Doctrine\Persistence\ManagerRegistry; use PHPUnit\Framework\MockObject\MockObject; @@ -29,8 +30,8 @@ */ class GitHubSecurityAdvisoriesSourceTest extends TestCase { - private ProviderManager|MockObject $providerManager; - private ManagerRegistry|MockObject $doctrine; + private ProviderManager&MockObject $providerManager; + private ManagerRegistry&MockObject $doctrine; protected function setUp(): void { @@ -43,7 +44,7 @@ public function testWithoutPagination(): void $responseFactory = function (string $method, string $url, array $options) { $this->assertSame('POST', $method); $this->assertSame('https://api.github.com/graphql', $url); - $this->assertSame('{"query":"query{securityVulnerabilities(ecosystem:COMPOSER,first:100){nodes{advisory{summary,permalink,publishedAt,withdrawnAt,identifiers{type,value},references{url}},vulnerableVersionRange,package{name}},pageInfo{hasNextPage,endCursor}}}"}', $options['body']); + $this->assertSame('{"query":"query{securityVulnerabilities(ecosystem:COMPOSER,first:100){nodes{advisory{summary,permalink,publishedAt,withdrawnAt,severity,identifiers{type,value},references{url}},vulnerableVersionRange,package{name}},pageInfo{hasNextPage,endCursor}}}"}', $options['body']); return new MockResponse(json_encode($this->getGraphQLResultFirstPage(false)), ['http_code' => 200, 'response_headers' => ['Content-Type' => 'application/json; charset=utf-8']]); }; @@ -59,23 +60,25 @@ public function testWithoutPagination(): void $advisories = $advisoryCollection->getAdvisoriesForPackageName($package->getName()); $this->assertCount(2, $advisories); - $this->assertSame('GHSA-h58v-c6rf-g9f7', $advisories[0]->getId()); - $this->assertSame('Cross site scripting in the system log', $advisories[0]->getTitle()); - $this->assertSame('vendor/package', $advisories[0]->getPackageName()); - $this->assertSame('>=4.10.0,<4.11.5|>=4.5.0,<4.9.16', $advisories[0]->getAffectedVersions()); - $this->assertSame('https://github.com/advisories/GHSA-h58v-c6rf-g9f7', $advisories[0]->getLink()); - $this->assertSame('CVE-2021-35210', $advisories[0]->getCve()); - $this->assertSame('2021-07-01T17:00:04+0000', $advisories[0]->getDate()->format(\DateTimeInterface::ISO8601)); - $this->assertNull($advisories[0]->getComposerRepository()); - - $this->assertSame('GHSA-f7wm-x4gw-6m23', $advisories[1]->getId()); - $this->assertSame('Insert tag injection in forms', $advisories[1]->getTitle()); - $this->assertSame('vendor/package', $advisories[1]->getPackageName()); - $this->assertSame('=4.10.0', $advisories[1]->getAffectedVersions()); - $this->assertSame('https://github.com/advisories/GHSA-f7wm-x4gw-6m23', $advisories[1]->getLink()); - $this->assertSame('CVE-2020-25768', $advisories[1]->getCve()); - $this->assertSame('2020-09-24T16:23:54+0000', $advisories[1]->getDate()->format(\DateTimeInterface::ISO8601)); - $this->assertNull($advisories[1]->getComposerRepository()); + $this->assertSame('GHSA-h58v-c6rf-g9f7', $advisories[0]->id); + $this->assertSame('Cross site scripting in the system log', $advisories[0]->title); + $this->assertSame('vendor/package', $advisories[0]->packageName); + $this->assertSame('>=4.10.0,<4.11.5|>=4.5.0,<4.9.16', $advisories[0]->affectedVersions); + $this->assertSame('https://github.com/advisories/GHSA-h58v-c6rf-g9f7', $advisories[0]->link); + $this->assertSame('CVE-2021-35210', $advisories[0]->cve); + $this->assertSame('2021-07-01T17:00:04+0000', $advisories[0]->date->format(\DateTimeInterface::ISO8601)); + $this->assertNull($advisories[0]->composerRepository); + $this->assertSame(Severity::MEDIUM, $advisories[0]->severity); + + $this->assertSame('GHSA-f7wm-x4gw-6m23', $advisories[1]->id); + $this->assertSame('Insert tag injection in forms', $advisories[1]->title); + $this->assertSame('vendor/package', $advisories[1]->packageName); + $this->assertSame('=4.10.0', $advisories[1]->affectedVersions); + $this->assertSame('https://github.com/advisories/GHSA-f7wm-x4gw-6m23', $advisories[1]->link); + $this->assertSame('CVE-2020-25768', $advisories[1]->cve); + $this->assertSame('2020-09-24T16:23:54+0000', $advisories[1]->date->format(\DateTimeInterface::ISO8601)); + $this->assertNull($advisories[1]->composerRepository); + $this->assertSame(Severity::MEDIUM, $advisories[1]->severity); } public function testWithPagination(): void @@ -95,11 +98,11 @@ public function testWithPagination(): void $counter++; switch ($counter) { case 1: - $this->assertSame('{"query":"query{securityVulnerabilities(ecosystem:COMPOSER,first:100){nodes{advisory{summary,permalink,publishedAt,withdrawnAt,identifiers{type,value},references{url}},vulnerableVersionRange,package{name}},pageInfo{hasNextPage,endCursor}}}"}', $options['body']); + $this->assertSame('{"query":"query{securityVulnerabilities(ecosystem:COMPOSER,first:100){nodes{advisory{summary,permalink,publishedAt,withdrawnAt,severity,identifiers{type,value},references{url}},vulnerableVersionRange,package{name}},pageInfo{hasNextPage,endCursor}}}"}', $options['body']); return new MockResponse(json_encode($this->getGraphQLResultFirstPage(true)), ['http_code' => 200, 'response_headers' => ['Content-Type' => 'application/json; charset=utf-8']]); case 2: - $this->assertSame('{"query":"query{securityVulnerabilities(ecosystem:COMPOSER,first:100,after:\u0022Y3Vyc29yOnYyOpK5MjAyMC0wOS0yNFQxODoyMzo0MyswMjowMM0T9A==\u0022){nodes{advisory{summary,permalink,publishedAt,withdrawnAt,identifiers{type,value},references{url}},vulnerableVersionRange,package{name}},pageInfo{hasNextPage,endCursor}}}"}', $options['body']); + $this->assertSame('{"query":"query{securityVulnerabilities(ecosystem:COMPOSER,first:100,after:\u0022Y3Vyc29yOnYyOpK5MjAyMC0wOS0yNFQxODoyMzo0MyswMjowMM0T9A==\u0022){nodes{advisory{summary,permalink,publishedAt,withdrawnAt,severity,identifiers{type,value},references{url}},vulnerableVersionRange,package{name}},pageInfo{hasNextPage,endCursor}}}"}', $options['body']); return new MockResponse(json_encode($this->getGraphQLResultSecondPage()), ['http_code' => 200, 'response_headers' => ['Content-Type' => 'application/json; charset=utf-8']]); } @@ -117,28 +120,30 @@ public function testWithPagination(): void $otherAdvisories = $advisoryCollection->getAdvisoriesForPackageName('vendor/other-package'); $this->assertCount(1, $otherAdvisories); - $this->assertSame('<5.11.0', $otherAdvisories[0]->getAffectedVersions()); + $this->assertSame('<5.11.0', $otherAdvisories[0]->affectedVersions); $advisories = $advisoryCollection->getAdvisoriesForPackageName($package->getName()); $this->assertCount(2, $advisories); - $this->assertSame('GHSA-h58v-c6rf-g9f7', $advisories[0]->getId()); - $this->assertSame('Cross site scripting in the system log', $advisories[0]->getTitle()); - $this->assertSame('vendor/package', $advisories[0]->getPackageName()); - $this->assertSame('>=4.10.0,<4.11.5|>=4.5.0,<4.9.16', $advisories[0]->getAffectedVersions()); - $this->assertSame('https://github.com/advisories/GHSA-h58v-c6rf-g9f7', $advisories[0]->getLink()); - $this->assertSame('CVE-2021-35210', $advisories[0]->getCve()); - $this->assertSame('2021-07-01T17:00:04+0000', $advisories[0]->getDate()->format(\DateTimeInterface::ISO8601)); - $this->assertSame(SecurityAdvisory::PACKAGIST_ORG, $advisories[0]->getComposerRepository()); - - $this->assertSame('GHSA-f7wm-x4gw-6m23', $advisories[1]->getId()); - $this->assertSame('Insert tag injection in forms', $advisories[1]->getTitle()); - $this->assertSame('vendor/package', $advisories[1]->getPackageName()); - $this->assertSame('=4.10.0|<4.11.0', $advisories[1]->getAffectedVersions()); - $this->assertSame('https://github.com/advisories/GHSA-f7wm-x4gw-6m23', $advisories[1]->getLink()); - $this->assertSame('CVE-2020-25768', $advisories[1]->getCve()); - $this->assertSame('2020-09-24T16:23:54+0000', $advisories[1]->getDate()->format(\DateTimeInterface::ISO8601)); - $this->assertSame(SecurityAdvisory::PACKAGIST_ORG, $advisories[1]->getComposerRepository()); + $this->assertSame('GHSA-h58v-c6rf-g9f7', $advisories[0]->id); + $this->assertSame('Cross site scripting in the system log', $advisories[0]->title); + $this->assertSame('vendor/package', $advisories[0]->packageName); + $this->assertSame('>=4.10.0,<4.11.5|>=4.5.0,<4.9.16', $advisories[0]->affectedVersions); + $this->assertSame('https://github.com/advisories/GHSA-h58v-c6rf-g9f7', $advisories[0]->link); + $this->assertSame('CVE-2021-35210', $advisories[0]->cve); + $this->assertSame('2021-07-01T17:00:04+0000', $advisories[0]->date->format(\DateTimeInterface::ISO8601)); + $this->assertSame(SecurityAdvisory::PACKAGIST_ORG, $advisories[0]->composerRepository); + $this->assertSame(Severity::MEDIUM, $advisories[0]->severity); + + $this->assertSame('GHSA-f7wm-x4gw-6m23', $advisories[1]->id); + $this->assertSame('Insert tag injection in forms', $advisories[1]->title); + $this->assertSame('vendor/package', $advisories[1]->packageName); + $this->assertSame('=4.10.0|<4.11.0', $advisories[1]->affectedVersions); + $this->assertSame('https://github.com/advisories/GHSA-f7wm-x4gw-6m23', $advisories[1]->link); + $this->assertSame('CVE-2020-25768', $advisories[1]->cve); + $this->assertSame('2020-09-24T16:23:54+0000', $advisories[1]->date->format(\DateTimeInterface::ISO8601)); + $this->assertSame(SecurityAdvisory::PACKAGIST_ORG, $advisories[1]->composerRepository); + $this->assertSame(Severity::MEDIUM, $advisories[1]->severity); } private function getPackage(): Package @@ -193,6 +198,8 @@ private function graphQlPackageNode(string $advisoryId, string $packageName, str 'summary' => $summary, 'permalink' => 'https://github.com/advisories/' . $advisoryId, 'publishedAt' => $publishedAt, + 'withdrawnAt' => null, + 'severity' => 'MODERATE', 'identifiers' => [ ['type' => 'GHSA', 'value' => $advisoryId], ['type' => 'CVE', 'value' => $cve], diff --git a/tests/SecurityAdvisory/RemoteSecurityAdvisoryTest.php b/tests/SecurityAdvisory/RemoteSecurityAdvisoryTest.php index b10e30408..6ec1dd0f8 100644 --- a/tests/SecurityAdvisory/RemoteSecurityAdvisoryTest.php +++ b/tests/SecurityAdvisory/RemoteSecurityAdvisoryTest.php @@ -33,14 +33,15 @@ public function testCreateFromFriendsOfPhp(): void 'reference' => 'composer://3f/pygmentize', ]); - $this->assertSame('3f/pygmentize/2017-05-15.yaml', $advisory->getId()); - $this->assertSame('Remote Code Execution', $advisory->getTitle()); - $this->assertSame('https://github.com/dedalozzo/pygmentize/issues/1', $advisory->getLink()); - $this->assertNull($advisory->getCve()); - $this->assertSame('<1.2', $advisory->getAffectedVersions()); - $this->assertSame('3f/pygmentize', $advisory->getPackageName()); - $this->assertSame('2017-05-15 00:00:00', $advisory->getDate()->format('Y-m-d H:i:s')); - $this->assertSame(SecurityAdvisory::PACKAGIST_ORG, $advisory->getComposerRepository()); + $this->assertSame('3f/pygmentize/2017-05-15.yaml', $advisory->id); + $this->assertSame('Remote Code Execution', $advisory->title); + $this->assertSame('https://github.com/dedalozzo/pygmentize/issues/1', $advisory->link); + $this->assertNull($advisory->cve); + $this->assertSame('<1.2', $advisory->affectedVersions); + $this->assertSame('3f/pygmentize', $advisory->packageName); + $this->assertSame('2017-05-15 00:00:00', $advisory->date->format('Y-m-d H:i:s')); + $this->assertSame(SecurityAdvisory::PACKAGIST_ORG, $advisory->composerRepository); + $this->assertNull($advisory->severity); } public function testCreateFromFriendsOfPhpOnlyYearAvailable(): void @@ -59,7 +60,7 @@ public function testCreateFromFriendsOfPhpOnlyYearAvailable(): void 'reference' => 'composer://erusev/parsedown', ]); - $this->assertSame('2019-01-01 00:00:00', $advisory->getDate()->format('Y-m-d H:i:s')); + $this->assertSame('2019-01-01 00:00:00', $advisory->date->format('Y-m-d H:i:s')); } public function testCreateFromFriendsOfPhpOnlyYearButBranchDatesAvailable(): void @@ -79,7 +80,7 @@ public function testCreateFromFriendsOfPhpOnlyYearButBranchDatesAvailable(): voi 'composer-repository' => false, ]); - $this->assertSame('2019-10-08 00:00:00', $advisory->getDate()->format('Y-m-d H:i:s')); + $this->assertSame('2019-10-08 00:00:00', $advisory->date->format('Y-m-d H:i:s')); } public function testCreateFromFriendsOfPhpCVEXXXX(): void @@ -105,15 +106,15 @@ public function testCreateFromFriendsOfPhpCVEXXXX(): void 'reference' => 'composer://symfony/framework-bundle', ]); - $this->assertSame('symfony/framework-bundle/CVE-2022-xxxx.yaml', $advisory->getId()); - $this->assertNull($advisory->getCve()); + $this->assertSame('symfony/framework-bundle/CVE-2022-xxxx.yaml', $advisory->id); + $this->assertNull($advisory->cve); } public function testWithAddedAffectedVersion(): void { - $advisory = new RemoteSecurityAdvisory('id', 'foobar', 'foo/bar', '>=1', 'https://foobar.com', null, new \DateTimeImmutable(), null, [], 'test'); + $advisory = new RemoteSecurityAdvisory('id', 'foobar', 'foo/bar', '>=1', 'https://foobar.com', null, new \DateTimeImmutable(), null, [], 'test', null); $advisory = $advisory->withAddedAffectedVersion('<2'); - $this->assertSame('>=1|<2', $advisory->getAffectedVersions()); + $this->assertSame('>=1|<2', $advisory->affectedVersions); } } diff --git a/tests/SecurityAdvisory/SecurityAdvisoryResolverTest.php b/tests/SecurityAdvisory/SecurityAdvisoryResolverTest.php index c4255221c..db8bb7f74 100644 --- a/tests/SecurityAdvisory/SecurityAdvisoryResolverTest.php +++ b/tests/SecurityAdvisory/SecurityAdvisoryResolverTest.php @@ -76,7 +76,7 @@ public function testResolveDontRemoveAdvisoryFromOtherSource(): void public function testResolveDontRemoveAdvisoryWithMultipleSources(): void { $advisory = new SecurityAdvisory($this->createRemoteAdvisory('test'), 'test'); - $advisory->addSource('other-id', 'other'); + $advisory->addSource('other-id', 'other', null); [$new, $removed] = $this->resolver->resolve([$advisory], new RemoteSecurityAdvisoryCollection([]), 'test'); $this->assertSame([], $new); @@ -108,6 +108,18 @@ public function testResolveEmpty(): void private function createRemoteAdvisory(string $source, string $packageName = 'acme/package', ?string $cve = null): RemoteSecurityAdvisory { - return new RemoteSecurityAdvisory(uniqid('id-'), 'Security Advisory', $packageName, '^1.0', 'https://example.org', $cve, new \DateTimeImmutable(), null, [], $source); + return new RemoteSecurityAdvisory( + uniqid('id-'), + 'Security Advisory', + $packageName, + '^1.0', + 'https://example.org', + $cve, + new \DateTimeImmutable(), + null, + [], + $source, + null, + ); } } diff --git a/tests/SecurityAdvisory/SeverityTest.php b/tests/SecurityAdvisory/SeverityTest.php new file mode 100644 index 000000000..c29c5a49e --- /dev/null +++ b/tests/SecurityAdvisory/SeverityTest.php @@ -0,0 +1,36 @@ + + * Nils Adermann + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use App\SecurityAdvisory\Severity; +use PHPUnit\Framework\TestCase; + +class SeverityTest extends TestCase +{ + /** + * @dataProvider gitHubSeverityProvider + */ + public function testFromGitHub(?string $gitHubSeverity, ?Severity $expected): void + { + $this->assertSame($expected, Severity::fromGitHub($gitHubSeverity)); + } + + public static function gitHubSeverityProvider(): iterable + { + yield ['CRITICAL', Severity::CRITICAL]; + yield ['HIGH', Severity::HIGH]; + yield ['MODERATE', Severity::MEDIUM]; + yield ['LOW', Severity::LOW]; + yield [null, null]; + } +} diff --git a/tests/SecurityAdvisoryWorkerTest.php b/tests/SecurityAdvisoryWorkerTest.php index 1869bb696..8db9f8561 100644 --- a/tests/SecurityAdvisoryWorkerTest.php +++ b/tests/SecurityAdvisoryWorkerTest.php @@ -168,6 +168,18 @@ public function testProcessAdvisoryFailed(): void private function createRemoteAdvisory(string $packageName, string $remoteId): RemoteSecurityAdvisory { - return new RemoteSecurityAdvisory($remoteId, 'Advisory' . $packageName, $packageName, '^1.0', 'https://example/' . $packageName, null, new \DateTimeImmutable(), null, [], 'test'); + return new RemoteSecurityAdvisory( + $remoteId, + 'Advisory' . $packageName, + $packageName, + '^1.0', + 'https://example/' . $packageName, + null, + new \DateTimeImmutable(), + null, + [], + 'test', + null, + ); } }