diff --git a/phpstan.neon b/phpstan.neon index 0021dc712..58d51a908 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -58,7 +58,7 @@ parameters: PackageUpdateJob: 'array{id: int, update_equal_refs: bool, delete_before: bool, force_dump: bool, source: string}' GitHubUserMigrateJob: 'array{id: int, old_scope: string, new_scope: string}' SecurityAdvisoryJob: 'array{source: string}' - FilterListJob: 'array{list: string}' + FilterListJob: 'array{list: string, source: string}' JobResult: 'array{status: \App\Entity\Job::STATUS_*, message: string, vendor?: string, details?: string, exceptionMsg?: string, exceptionClass?: class-string<\Throwable>, results?: array{hooks_setup: int, hooks_failed: array, hooks_ok_unchanged: int}}' ErroredResult: 'array{status: \App\Entity\Job::STATUS_ERRORED, message: string, exception: \Throwable}' diff --git a/src/Audit/Display/AuditLogDisplayFactory.php b/src/Audit/Display/AuditLogDisplayFactory.php index a1cb9aa76..b2a7a3101 100644 --- a/src/Audit/Display/AuditLogDisplayFactory.php +++ b/src/Audit/Display/AuditLogDisplayFactory.php @@ -17,6 +17,7 @@ use App\Entity\AuditRecord; use App\Entity\User; use App\FilterList\FilterLists; +use App\FilterList\FilterSources; use Symfony\Bundle\SecurityBundle\Security; class AuditLogDisplayFactory @@ -222,6 +223,7 @@ public function buildSingle(AuditRecord $record): AuditLogDisplayInterface $record->attributes['entry']['package_name'], $record->attributes['entry']['version'], FilterLists::from($record->attributes['entry']['list']), + FilterSources::from($record->attributes['entry']['source']), $record->attributes['entry']['reason'] ?? $record->attributes['entry']['category'], $this->buildActor($record->attributes['actor'] ?? null), $record->ip @@ -231,6 +233,7 @@ public function buildSingle(AuditRecord $record): AuditLogDisplayInterface $record->attributes['entry']['package_name'], $record->attributes['entry']['version'], FilterLists::from($record->attributes['entry']['list']), + FilterSources::from($record->attributes['entry']['source']), $record->attributes['entry']['reason'] ?? $record->attributes['entry']['category'], $this->buildActor($record->attributes['actor'] ?? null), $record->ip diff --git a/src/Audit/Display/FilterListEntryAddedDisplay.php b/src/Audit/Display/FilterListEntryAddedDisplay.php index 4a58a3d60..089000f54 100644 --- a/src/Audit/Display/FilterListEntryAddedDisplay.php +++ b/src/Audit/Display/FilterListEntryAddedDisplay.php @@ -14,6 +14,7 @@ use App\Audit\AuditRecordType; use App\FilterList\FilterLists; +use App\FilterList\FilterSources; readonly class FilterListEntryAddedDisplay extends AbstractAuditLogDisplay { @@ -22,6 +23,7 @@ public function __construct( public string $packageName, public string $version, public FilterLists $list, + public FilterSources $source, public string $reason, ActorDisplay $actor, ?string $ip, diff --git a/src/Audit/Display/FilterListEntryDeletedDisplay.php b/src/Audit/Display/FilterListEntryDeletedDisplay.php index 08afd450d..8deba7526 100644 --- a/src/Audit/Display/FilterListEntryDeletedDisplay.php +++ b/src/Audit/Display/FilterListEntryDeletedDisplay.php @@ -14,6 +14,7 @@ use App\Audit\AuditRecordType; use App\FilterList\FilterLists; +use App\FilterList\FilterSources; readonly class FilterListEntryDeletedDisplay extends AbstractAuditLogDisplay { @@ -22,6 +23,7 @@ public function __construct( public string $packageName, public string $version, public FilterLists $list, + public FilterSources $source, public string $reason, ActorDisplay $actor, ?string $ip, diff --git a/src/Command/UpdateFilterListCommand.php b/src/Command/UpdateFilterListCommand.php index 164fd4b91..6ab7cfc99 100644 --- a/src/Command/UpdateFilterListCommand.php +++ b/src/Command/UpdateFilterListCommand.php @@ -13,6 +13,7 @@ namespace App\Command; use App\FilterList\FilterLists; +use App\FilterList\FilterSources; use App\Service\Locker; use App\Service\Scheduler; use Symfony\Component\Console\Command\Command; @@ -35,8 +36,9 @@ protected function configure(): void ->setName('packagist:filter-list') ->setDefinition([ new InputArgument('list', InputArgument::REQUIRED, 'The name of the filter list', null, FilterLists::cases()), + new InputArgument('source', InputArgument::REQUIRED, 'The name of the filter source', null, FilterSources::cases()), ]) - ->setDescription('Updates all entries for a single filter list') + ->setDescription('Updates all entries for a single filter list source') ; } @@ -50,12 +52,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int return self::INVALID; } + try { + $source = FilterSources::from($input->getArgument('source')); + } catch (\ValueError) { + $output->writeln('source must be one of '.implode(', ', array_map(fn (FilterSources $source) => $source->value, FilterSources::cases()))); + + return self::INVALID; + } + $lockAcquired = $this->locker->lockFilterList($list->value); if (!$lockAcquired) { return 0; } - $this->scheduler->scheduleFilterList($list, 0); + $this->scheduler->scheduleFilterList($list, $source, 0); sleep(2); // sleep to prevent running the same command on multiple machines at around the same time via cron $this->locker->unlockFilterList($list->value); diff --git a/src/Controller/PackageController.php b/src/Controller/PackageController.php index 5b49c5d13..337112f1c 100644 --- a/src/Controller/PackageController.php +++ b/src/Controller/PackageController.php @@ -31,6 +31,7 @@ use App\Entity\Version; use App\Event\PackageAbandonedEvent; use App\Event\PackageUnabandonedEvent; +use App\FilterList\FilterLists; use App\Form\Model\MaintainerRequest; use App\Form\Model\TransferPackageRequest; use App\Form\Type\AbandonedType; @@ -74,6 +75,7 @@ use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Routing\Requirement\EnumRequirement; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; @@ -681,7 +683,7 @@ public function viewPackageAction(Request $req, string $name, CsrfTokenManagerIn foreach ($versions as $version) { if ($version->getNormalizedVersion() === $normalizedVersion) { $data['hasVersionsFlaggedAsMalware'][$version->getId()] = true; - $data['listsFlaggingVersionsAsMalware'][$packageVersionFlaggedAsMalware->getList()->value] = $packageVersionFlaggedAsMalware->getList(); + $data['listsFlaggingVersionsAsMalware'][$packageVersionFlaggedAsMalware->getSource()->value] = $packageVersionFlaggedAsMalware->getSource(); } } } @@ -1656,12 +1658,12 @@ public function securityAdvisoryAction(Request $request, string $id): Response return $this->render('package/security_advisory.html.twig', ['securityAdvisories' => $securityAdvisories, 'id' => $id]); } - #[Route(path: '/packages/{name}/filter-lists/', name: 'view_package_filter_lists', requirements: ['name' => Package::PACKAGE_NAME_OR_EXT_REGEX])] - public function filterListsAction(Request $request, string $name): Response + #[Route(path: '/packages/{name}/filter-lists/{list}/', name: 'view_package_filter_lists', requirements: ['name' => Package::PACKAGE_NAME_OR_EXT_REGEX, 'list' => new EnumRequirement(FilterLists::class)])] + public function filterListsAction(Request $request, string $name, FilterLists $list): Response { /** @var FilterListEntryRepository $repo */ $repo = $this->getEM()->getRepository(FilterListEntry::class); - $entries = $repo->getPackageEntries($name); + $entries = $repo->getPackageEntries($name, $list); $data = []; $data['name'] = $name; @@ -1693,6 +1695,7 @@ public function filterListsAction(Request $request, string $name): Response $data['entries'] = $entries; $data['count'] = \count($entries); + $data['list'] = $list; return $this->render('package/filter_list_entries.html.twig', $data); } diff --git a/src/Entity/FilterListEntry.php b/src/Entity/FilterListEntry.php index db6a752d3..155032a1e 100644 --- a/src/Entity/FilterListEntry.php +++ b/src/Entity/FilterListEntry.php @@ -13,6 +13,7 @@ namespace App\Entity; use App\FilterList\FilterLists; +use App\FilterList\FilterSources; use App\FilterList\RemoteFilterListEntry; use App\Service\IdGenerator; use Doctrine\ORM\Mapping as ORM; @@ -48,8 +49,11 @@ class FilterListEntry #[ORM\Column] private \DateTimeImmutable $updatedAt; - #[ORM\Column(nullable: true)] - private ?string $publicId; + #[ORM\Column] + private string $publicId; + + #[ORM\Column] + private FilterSources $source; public function __construct(RemoteFilterListEntry $remote) { @@ -59,6 +63,7 @@ public function __construct(RemoteFilterListEntry $remote) $this->link = $remote->link; $this->list = $remote->list; $this->reason = $remote->reason; + $this->source = $remote->source; $this->createdAt = $this->updatedAt = new \DateTimeImmutable(); } @@ -93,7 +98,12 @@ public function getPublicId(): ?string return $this->publicId; } - public function assignPublicId(): void + public function getSource(): FilterSources + { + return $this->source; + } + + private function assignPublicId(): void { $this->publicId = IdGenerator::generateFilterListEntry(); } diff --git a/src/Entity/FilterListEntryRepository.php b/src/Entity/FilterListEntryRepository.php index 0596a85d0..2f82b5db4 100644 --- a/src/Entity/FilterListEntryRepository.php +++ b/src/Entity/FilterListEntryRepository.php @@ -13,6 +13,7 @@ namespace App\Entity; use App\FilterList\FilterLists; +use App\FilterList\FilterSources; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\DBAL\ArrayParameterType; use Doctrine\Persistence\ManagerRegistry; @@ -31,11 +32,13 @@ public function __construct( /** * @return list */ - public function getEntriesInList(FilterLists $list): array + public function getEntriesInList(FilterLists $list, FilterSources $source): array { return $this->createQueryBuilder('fl') ->where('fl.list = :list') + ->andWhere('fl.source = :source') ->setParameter('list', $list) + ->setParameter('source', $source) ->getQuery() ->getResult(); } @@ -47,9 +50,9 @@ public function getPackageVersionsFlaggedAsMalwareForPackage(Package $package): { return $this->createQueryBuilder('fl') ->where('fl.packageName = :packageName') - ->andWhere('fl.list IN (:lists)') + ->andWhere('fl.list = :malware') ->setParameter('packageName', $package->getName()) - ->setParameter('lists', FilterLists::malwareListsValues(), ArrayParameterType::STRING) + ->setParameter('malware', FilterLists::MALWARE) ->getQuery() ->getResult(); } @@ -57,11 +60,13 @@ public function getPackageVersionsFlaggedAsMalwareForPackage(Package $package): /** * @return list */ - public function getPackageEntries(string $packageName): array + public function getPackageEntries(string $packageName, FilterLists $list): array { return $this->createQueryBuilder('fl') ->where('fl.packageName = :packageName') + ->andWhere('fl.list = :list') ->setParameter('packageName', $packageName) + ->setParameter('list', $list) ->getQuery() ->getResult(); } @@ -69,7 +74,7 @@ public function getPackageEntries(string $packageName): array /** * @param array $packageNames * - * @return array> + * @return array> */ public function getAllPackageEntriesMap(array $packageNames): array { @@ -86,6 +91,7 @@ public function getAllPackageEntriesMap(array $packageNames): array 'list' => $entry->getList()->value, 'reason' => $entry->getReason(), 'publicId' => $entry->getPublicId(), + 'source' => $entry->getSource()->value, ]; } diff --git a/src/FilterList/Dump/DumpableFilterList.php b/src/FilterList/Dump/DumpableFilterList.php index 1ed2971e3..2928c7cdb 100644 --- a/src/FilterList/Dump/DumpableFilterList.php +++ b/src/FilterList/Dump/DumpableFilterList.php @@ -12,6 +12,8 @@ namespace App\FilterList\Dump; +use App\FilterList\FilterSources; + final readonly class DumpableFilterList { public function __construct( @@ -19,6 +21,7 @@ public function __construct( public string $url, public ?string $reason, public ?string $id, + public string $source, ) { } } diff --git a/src/FilterList/Dump/FilterListDumperProvider.php b/src/FilterList/Dump/FilterListDumperProvider.php index b3efc7154..acc519d89 100644 --- a/src/FilterList/Dump/FilterListDumperProvider.php +++ b/src/FilterList/Dump/FilterListDumperProvider.php @@ -41,9 +41,10 @@ public function getEntriesForDump(array $packageNames): array foreach ($entries as $entry) { $groupedEntries[$packageName][$entry['list']][] = new DumpableFilterList( $entry['version'], - $this->urlGenerator->generate('view_package_filter_lists', ['name' => $packageName], UrlGeneratorInterface::ABSOLUTE_URL), + $this->urlGenerator->generate('view_package_filter_lists', ['name' => $packageName, 'list' => $entry['list']], UrlGeneratorInterface::ABSOLUTE_URL), $entry['reason'], $entry['publicId'], + $entry['source'], ); } } diff --git a/src/FilterList/FilterListResolver.php b/src/FilterList/FilterListResolver.php index 60c94dfd3..925aa422d 100644 --- a/src/FilterList/FilterListResolver.php +++ b/src/FilterList/FilterListResolver.php @@ -20,18 +20,12 @@ class FilterListResolver * @param array $existingEntries * @param array $remoteEntries * - * @return array{list, list, bool} + * @return array{list, list} */ public function resolve(array $existingEntries, array $remoteEntries): array { - $modifiedExisting = false; $existingMap = []; foreach ($existingEntries as $existing) { - if (!$existing->getPublicId()) { - $existing->assignPublicId(); - $modifiedExisting = true; - } - $existingMap[$existing->getPackageName()][$existing->getVersion()] = $existing; } @@ -58,7 +52,6 @@ public function resolve(array $existingEntries, array $remoteEntries): array return [ $new, $unmatched, - $modifiedExisting, ]; } } diff --git a/src/FilterList/FilterLists.php b/src/FilterList/FilterLists.php index be1977dd8..686891930 100644 --- a/src/FilterList/FilterLists.php +++ b/src/FilterList/FilterLists.php @@ -14,50 +14,5 @@ enum FilterLists: string { - case AIKIDO_MALWARE = 'aikido-malware'; - - public function logo(): string - { - return match ($this) { - self::AIKIDO_MALWARE => 'img/aikido-dark.svg', - }; - } - - public function displayName(): string - { - return match ($this) { - self::AIKIDO_MALWARE => 'Aikido', - }; - } - - public function url(): string - { - return match ($this) { - self::AIKIDO_MALWARE => 'https://aikido.dev/', - }; - } - - /** - * @return list - */ - public static function defaultLists(): array - { - return [self::AIKIDO_MALWARE]; - } - - /** - * @return list - */ - public static function malwareLists(): array - { - return [self::AIKIDO_MALWARE]; - } - - /** - * @return list - */ - public static function malwareListsValues(): array - { - return array_map(fn (FilterLists $list) => $list->value, self::malwareLists()); - } + case MALWARE = 'malware'; } diff --git a/src/FilterList/FilterSources.php b/src/FilterList/FilterSources.php new file mode 100644 index 000000000..12c91a6f4 --- /dev/null +++ b/src/FilterList/FilterSources.php @@ -0,0 +1,39 @@ + + * Nils Adermann + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\FilterList; + +enum FilterSources: string +{ + case AIKIDO = 'aikido'; + + public function logo(): string + { + return match ($this) { + self::AIKIDO => 'img/aikido-dark.svg', + }; + } + + public function displayName(): string + { + return match ($this) { + self::AIKIDO => 'Aikido', + }; + } + + public function url(): string + { + return match ($this) { + self::AIKIDO => 'https://aikido.dev/', + }; + } +} diff --git a/src/FilterList/List/AikidoMalwareFilterList.php b/src/FilterList/List/AikidoMalwareFilterList.php index 88f8da9da..7f790ed85 100644 --- a/src/FilterList/List/AikidoMalwareFilterList.php +++ b/src/FilterList/List/AikidoMalwareFilterList.php @@ -13,6 +13,7 @@ namespace App\FilterList\List; use App\FilterList\FilterLists; +use App\FilterList\FilterSources; use App\FilterList\RemoteFilterListEntry; use Psr\Log\LoggerInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -55,9 +56,10 @@ public function getListEntries(): ?array $entries[] = new RemoteFilterListEntry( $packageName, $entry['version'], - FilterLists::AIKIDO_MALWARE, + FilterLists::MALWARE, \sprintf('https://app.aikido.dev/reports/malware/software-supply-chain-attacks?search=%s&ecosystem=packagist', urlencode($packageName)), 'malware', + FilterSources::AIKIDO, ); } diff --git a/src/FilterList/RemoteFilterListEntry.php b/src/FilterList/RemoteFilterListEntry.php index b6f4586aa..65fc196f1 100644 --- a/src/FilterList/RemoteFilterListEntry.php +++ b/src/FilterList/RemoteFilterListEntry.php @@ -20,6 +20,7 @@ public function __construct( public FilterLists $list, public ?string $link, public string $reason, + public FilterSources $source, ) { } } diff --git a/src/Package/V2Dumper.php b/src/Package/V2Dumper.php index 50d4c3892..56ecef590 100644 --- a/src/Package/V2Dumper.php +++ b/src/Package/V2Dumper.php @@ -98,7 +98,6 @@ public function dumpRoot(bool $verbose = false): void $rootFileContents['filter'] = [ 'metadata' => true, 'lists' => array_map(fn (FilterLists $list) => $list->value, FilterLists::cases()), - 'default-lists' => array_map(fn (FilterLists $list) => $list->value, FilterLists::defaultLists()), ]; if ($verbose) { diff --git a/src/Service/FilterListWorker.php b/src/Service/FilterListWorker.php index 6db8c9e11..1872e9bcf 100644 --- a/src/Service/FilterListWorker.php +++ b/src/Service/FilterListWorker.php @@ -18,6 +18,7 @@ use App\FilterList\FilterListEntryUpdateListener; use App\FilterList\FilterListResolver; use App\FilterList\FilterLists; +use App\FilterList\FilterSources; use App\FilterList\List\FilterListInterface; use App\Model\DownloadManager; use Doctrine\Persistence\ManagerRegistry; @@ -58,24 +59,28 @@ public function __construct( public function process(Job $job, SignalHandler $signal): array { $list = FilterLists::from($job->getPayload()['list']); + $source = FilterSources::from($job->getPayload()['source']); $lockAcquired = $this->locker->lockFilterList(self::FILTER_LIST_WORKER_RUN); if (!$lockAcquired) { return ['status' => Job::STATUS_RESCHEDULE, 'after' => new \DateTimeImmutable('+2 minutes'), 'message' => 'Could not acquire lock']; } - $source = $this->filterLists[$list->value]; - $remoteEntries = $source->getListEntries(); + $filterList = $this->filterLists[$source->value . '-' . $list->value]; + $remoteEntries = $filterList->getListEntries(); if (null === $remoteEntries) { - $this->logger->info('Filter list update failed, skipping', ['list' => $list]); + $this->logger->info('Filter list update failed, skipping', [ + 'list' => $list, + 'source' => $source, + ]); $this->locker->unlockFilterList(self::FILTER_LIST_WORKER_RUN); return ['status' => Job::STATUS_ERRORED, 'message' => 'Filter list update failed, skipped']; } /** @var FilterListEntry[] $existingEntries */ - $existingEntries = $this->doctrine->getRepository(FilterListEntry::class)->getEntriesInList($list); - [$new, $removed, $modifiedExisting] = $this->malwareFeedResolver->resolve($existingEntries, $remoteEntries); + $existingEntries = $this->doctrine->getRepository(FilterListEntry::class)->getEntriesInList($list, $source); + [$new, $removed] = $this->malwareFeedResolver->resolve($existingEntries, $remoteEntries); foreach ($new as $entry) { $this->doctrine->getManager()->persist($entry); @@ -85,7 +90,7 @@ public function process(Job $job, SignalHandler $signal): array $this->doctrine->getManager()->remove($entry); } - if ($new !== [] || $removed !== [] || $modifiedExisting) { + if ($new !== [] || $removed !== []) { $this->doctrine->getManager()->flush(); } diff --git a/src/Service/Scheduler.php b/src/Service/Scheduler.php index bc8bba6d8..4b675ad99 100644 --- a/src/Service/Scheduler.php +++ b/src/Service/Scheduler.php @@ -15,6 +15,7 @@ use App\Entity\Job; use App\Entity\Package; use App\FilterList\FilterLists; +use App\FilterList\FilterSources; use Doctrine\Persistence\ManagerRegistry; use Predis\Client as RedisClient; @@ -80,9 +81,9 @@ public function scheduleSecurityAdvisory(string $source, int $packageId, ?\DateT /** * @return Job */ - public function scheduleFilterList(FilterLists $list, int $packageId, ?\DateTimeImmutable $executeAfter = null): Job + public function scheduleFilterList(FilterLists $list, FilterSources $source, int $packageId, ?\DateTimeImmutable $executeAfter = null): Job { - return $this->createJob('filter:update', ['list' => $list->value], $packageId, $executeAfter); + return $this->createJob('filter:update', ['list' => $list->value, 'source' => $source->value], $packageId, $executeAfter); } private function getPendingUpdateJob(int $packageId, bool $updateEqualRefs = false, bool $deleteBefore = false): ?string diff --git a/templates/audit_log/display/filter_list_entry_added.html.twig b/templates/audit_log/display/filter_list_entry_added.html.twig index f3fc0037e..17460ccc9 100644 --- a/templates/audit_log/display/filter_list_entry_added.html.twig +++ b/templates/audit_log/display/filter_list_entry_added.html.twig @@ -1,4 +1,4 @@ {% import 'audit_log/macros.html.twig' as auditLog %} {{ auditLog.packageLink(display.packageName) }}
-Versions {{ display.version}} were flagged by {{ display.list.displayName }}. Reason: {{ display.reason }}. +Versions {{ display.version}} were flagged as {{ display.list.value }} by {{ display.source.displayName }}. Reason: {{ display.reason }}. diff --git a/templates/audit_log/display/filter_list_entry_deleted.html.twig b/templates/audit_log/display/filter_list_entry_deleted.html.twig index 7a67e0202..4cc103e8f 100644 --- a/templates/audit_log/display/filter_list_entry_deleted.html.twig +++ b/templates/audit_log/display/filter_list_entry_deleted.html.twig @@ -1,4 +1,4 @@ {% import 'audit_log/macros.html.twig' as auditLog %} {{ auditLog.packageLink(display.packageName) }}
-Versions {{ display.version}} were unflagged by {{ display.list}}. +Versions {{ display.version }} were unflagged by {{ display.source.displayName }} as {{ display.list.value }}. diff --git a/templates/package/_filter_list_entries_list.html.twig b/templates/package/_filter_list_entries_list.html.twig index d3fc03897..84aef43f0 100644 --- a/templates/package/_filter_list_entries_list.html.twig +++ b/templates/package/_filter_list_entries_list.html.twig @@ -13,13 +13,13 @@ Reason: {{ entry.reason }} {% if entry.link %} - View details on {{ entry.list.displayName }} + View details on {{ entry.source.displayName }} {% endif %}

-

Reported by: {{ entry.list.displayName }}

+

Reported by: {{ entry.source.displayName }}

diff --git a/templates/package/filter_list_entries.html.twig b/templates/package/filter_list_entries.html.twig index 5f342aa7c..e473c03cb 100644 --- a/templates/package/filter_list_entries.html.twig +++ b/templates/package/filter_list_entries.html.twig @@ -4,7 +4,7 @@ {% block head_additions %}{% endblock %} -{% block title %}{{ 'packages.filter_lists'|trans }} - {{ name }} - {{ parent() }}{% endblock %} +{% block title %}{{ ('packages.filter_lists.' ~ list.value)|trans }} - {{ name }} - {{ parent() }}{% endblock %} {% block content %}
@@ -12,6 +12,7 @@

{{ name }} + {{ ('packages.filter_lists.' ~ list.value)|trans }} {% if version is defined %} for {{ version }} {% endif %} diff --git a/templates/package/version_list.html.twig b/templates/package/version_list.html.twig index a186d17eb..cff73ea08 100644 --- a/templates/package/version_list.html.twig +++ b/templates/package/version_list.html.twig @@ -17,7 +17,7 @@ {% endif %} {% if hasVersionsFlaggedAsMalware[version.id]|default(false) %} - + {% endif %} diff --git a/templates/package/view_package.html.twig b/templates/package/view_package.html.twig index a0c002ee6..7079ce01b 100644 --- a/templates/package/view_package.html.twig +++ b/templates/package/view_package.html.twig @@ -49,7 +49,7 @@

{% if hasVersionsFlaggedAsMalware %} - + {% endif %} {% if not package.isAutoUpdated() and is_granted('update', package) %} diff --git a/tests/Audit/Display/AuditLogDisplayFactoryTest.php b/tests/Audit/Display/AuditLogDisplayFactoryTest.php index ee8d7938d..e0422da23 100644 --- a/tests/Audit/Display/AuditLogDisplayFactoryTest.php +++ b/tests/Audit/Display/AuditLogDisplayFactoryTest.php @@ -32,6 +32,7 @@ use App\Entity\AuditRecord; use App\Entity\User; use App\FilterList\FilterLists; +use App\FilterList\FilterSources; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\MockObject\Stub; use PHPUnit\Framework\TestCase; @@ -602,7 +603,7 @@ public function testBuildFilterListEntryAdded(): void $auditRecord = $this->createAuditRecord( AuditRecordType::FilterListEntryAdded, [ - 'entry' => ['package_name' => 'acme/package', 'version' => '<1.0', 'list' => FilterLists::AIKIDO_MALWARE->value, 'reason' => 'malware'], + 'entry' => ['package_name' => 'acme/package', 'version' => '<1.0', 'list' => FilterLists::MALWARE->value, 'reason' => 'malware', 'source' => FilterSources::AIKIDO->value], ] ); @@ -612,9 +613,10 @@ public function testBuildFilterListEntryAdded(): void self::assertSame('acme/package', $display->packageName); self::assertSame('<1.0', $display->version); self::assertSame('malware', $display->reason); - self::assertSame(FilterLists::AIKIDO_MALWARE, $display->list); + self::assertSame(FilterLists::MALWARE, $display->list); self::assertNull($display->actor->id); self::assertSame('unknown', $display->actor->username); + self::assertSame(FilterSources::AIKIDO, $display->source); self::assertSame(AuditRecordType::FilterListEntryAdded, $display->getType()); self::assertSame('audit_log/display/filter_list_entry_added.html.twig', $display->getTemplateName()); } @@ -624,7 +626,7 @@ public function testBuildFilterListEntryDeleted(): void $auditRecord = $this->createAuditRecord( AuditRecordType::FilterListEntryDeleted, [ - 'entry' => ['package_name' => 'acme/package', 'version' => '<1.0', 'list' => FilterLists::AIKIDO_MALWARE->value, 'reason' => 'malware'], + 'entry' => ['package_name' => 'acme/package', 'version' => '<1.0', 'list' => FilterLists::MALWARE->value, 'reason' => 'malware', 'source' => FilterSources::AIKIDO->value], ] ); @@ -634,9 +636,10 @@ public function testBuildFilterListEntryDeleted(): void self::assertSame('acme/package', $display->packageName); self::assertSame('<1.0', $display->version); self::assertSame('malware', $display->reason); - self::assertSame(FilterLists::AIKIDO_MALWARE, $display->list); + self::assertSame(FilterLists::MALWARE, $display->list); self::assertNull($display->actor->id); self::assertSame('unknown', $display->actor->username); + self::assertSame(FilterSources::AIKIDO, $display->source); self::assertSame(AuditRecordType::FilterListEntryDeleted, $display->getType()); self::assertSame('audit_log/display/filter_list_entry_deleted.html.twig', $display->getTemplateName()); } diff --git a/tests/FilterList/Dump/FilterListDumperProviderTest.php b/tests/FilterList/Dump/FilterListDumperProviderTest.php index 510043151..21a2ac5f9 100644 --- a/tests/FilterList/Dump/FilterListDumperProviderTest.php +++ b/tests/FilterList/Dump/FilterListDumperProviderTest.php @@ -16,6 +16,7 @@ use App\Entity\FilterListEntryRepository; use App\FilterList\Dump\DumpableFilterList; use App\FilterList\Dump\FilterListDumperProvider; +use App\FilterList\FilterSources; use Doctrine\ORM\EntityManager; use Doctrine\Persistence\ManagerRegistry; use PHPUnit\Framework\MockObject\MockObject; @@ -59,16 +60,16 @@ public function testGetMalwareDataForDump(): void ->with(['acme/package']) ->willReturn([ 'acme/package' => [ - ['version' => '1.0.0', 'reason' => 'malware', 'list' => 'test', 'publicId' => 'PKFE-test1'], - ['version' => '2.0.0', 'reason' => 'malware', 'list' => 'test', 'publicId' => 'PKFE-test2'], + ['version' => '1.0.0', 'reason' => 'malware', 'list' => 'test', 'publicId' => 'PKFE-test1', 'source' => 'aikido'], + ['version' => '2.0.0', 'reason' => 'malware', 'list' => 'test', 'publicId' => 'PKFE-test2', 'source' => 'aikido'], ], ]); $this->assertEquals([ 'acme/package' => [ 'test' => [ - new DumpableFilterList('1.0.0', '', 'malware', 'PKFE-test1'), - new DumpableFilterList('2.0.0', '', 'malware', 'PKFE-test2'), + new DumpableFilterList('1.0.0', '', 'malware', 'PKFE-test1', 'aikido'), + new DumpableFilterList('2.0.0', '', 'malware', 'PKFE-test2', 'aikido'), ], ], ], $this->filterListDumperProvider->getEntriesForDump(['acme/package'])); diff --git a/tests/FilterList/FilterListResolverTest.php b/tests/FilterList/FilterListResolverTest.php index 5530cf3ce..b2a725285 100644 --- a/tests/FilterList/FilterListResolverTest.php +++ b/tests/FilterList/FilterListResolverTest.php @@ -15,6 +15,7 @@ use App\Entity\FilterListEntry; use App\FilterList\FilterListResolver; use App\FilterList\FilterLists; +use App\FilterList\FilterSources; use App\FilterList\RemoteFilterListEntry; use Doctrine\Persistence\Reflection\TypedNoDefaultReflectionProperty; use PHPUnit\Framework\TestCase; @@ -43,19 +44,7 @@ public function testExistingEntryWithTwoRemoteEntriesResultsInNoChange(): void $remote = $this->createRemoteFilterListEntry('vendor/package', '1.0.0'); $result = $this->resolver->resolve([$existing], [$remote, $remote]); - $this->assertSame([[], [], false], $result); - } - - public function testExistingWithoutIdAssignsNewPublicId(): void - { - $existing = new FilterListEntry($this->createRemoteFilterListEntry('vendor/package', '1.0.0')); - $this->unsetPublicId($existing); - $this->assertNull($existing->getPublicId()); - $remote = $this->createRemoteFilterListEntry('vendor/package', '1.0.0'); - $result = $this->resolver->resolve([$existing], [$remote, $remote]); - - $this->assertSame([[], [], true], $result); - $this->assertNotNull($existing->getPublicId()); + $this->assertSame([[], []], $result); } public function testResolveRemoveOldEntry(): void @@ -98,7 +87,7 @@ public function testResolveEmpty(): void { $result = $this->resolver->resolve([], []); - $this->assertSame([[], [], false], $result); + $this->assertSame([[], []], $result); } public function testResolveMultipleVersionsSamePackage(): void @@ -131,9 +120,10 @@ private function createRemoteFilterListEntry(string $packageName, string $versio return new RemoteFilterListEntry( $packageName, $version, - FilterLists::AIKIDO_MALWARE, + FilterLists::MALWARE, 'https://example.com/'.$packageName, 'malware', + FilterSources::AIKIDO, ); } diff --git a/tests/FilterList/List/AikidoMalwareFilterListTest.php b/tests/FilterList/List/AikidoMalwareFilterListTest.php index f38e261fc..b569b6d9b 100644 --- a/tests/FilterList/List/AikidoMalwareFilterListTest.php +++ b/tests/FilterList/List/AikidoMalwareFilterListTest.php @@ -38,7 +38,7 @@ public function testGetListEntriesReturnsEntries(): void $this->assertInstanceOf(RemoteFilterListEntry::class, $result[0]); $this->assertSame('vendor/malware', $result[0]->packageName); $this->assertSame('1.0.0', $result[0]->version); - $this->assertSame(FilterLists::AIKIDO_MALWARE, $result[0]->list); + $this->assertSame(FilterLists::MALWARE, $result[0]->list); } public function testGetListEntriesFiltersNonMalwareEntries(): void diff --git a/tests/FilterListWorkerTest.php b/tests/FilterListWorkerTest.php index 37a0e2131..fcdcf84fc 100644 --- a/tests/FilterListWorkerTest.php +++ b/tests/FilterListWorkerTest.php @@ -20,6 +20,7 @@ use App\FilterList\FilterListEntryUpdateListener; use App\FilterList\FilterListResolver; use App\FilterList\FilterLists; +use App\FilterList\FilterSources; use App\FilterList\List\FilterListInterface; use App\FilterList\RemoteFilterListEntry; use App\Model\DownloadManager; @@ -55,7 +56,7 @@ protected function setUp(): void $this->downloadManager = $this->createStub(DownloadManager::class); $this->urlGenerator = $this->createStub(UrlGeneratorInterface::class); $doctrine = $this->createStub(ManagerRegistry::class); - $this->worker = new FilterListWorker($this->locker, new NullLogger(), $doctrine, [FilterLists::AIKIDO_MALWARE->value => $this->filterList], new FilterListResolver(), new FilterListEntryUpdateListener($doctrine), $this->mailer, $this->downloadManager, 'test@example.com', $this->urlGenerator, 'packagist.org'); + $this->worker = new FilterListWorker($this->locker, new NullLogger(), $doctrine, [FilterSources::AIKIDO->value . '-' . FilterLists::MALWARE->value => $this->filterList], new FilterListResolver(), new FilterListEntryUpdateListener($doctrine), $this->mailer, $this->downloadManager, 'test@example.com', $this->urlGenerator, 'packagist.org'); $this->em = $this->createMock(EntityManager::class); @@ -96,7 +97,7 @@ public function testProcess(): void $this->filterListEntryRepository ->expects($this->once()) ->method('getEntriesInList') - ->with(FilterLists::AIKIDO_MALWARE) + ->with(FilterLists::MALWARE) ->willReturn([$existingEntry, $existingEntryToBeDeleted]); $this->em @@ -133,7 +134,7 @@ public function testProcessNoEntries(): void $this->filterListEntryRepository ->expects($this->once()) ->method('getEntriesInList') - ->with(FilterLists::AIKIDO_MALWARE) + ->with(FilterLists::MALWARE) ->willReturn([]); $this->expectNoPersistAndRemove(); @@ -191,15 +192,16 @@ private function createRemoteFilterListEntry(string $packageName, string $versio return new RemoteFilterListEntry( $packageName, $version, - FilterLists::AIKIDO_MALWARE, + FilterLists::MALWARE, 'https://example.com/'.$packageName, 'malware', + FilterSources::AIKIDO, ); } private function createJob(): Job { - return new Job('job', 'filter:update', ['list' => FilterLists::AIKIDO_MALWARE->value]); + return new Job('job', 'filter:update', ['list' => FilterLists::MALWARE->value, 'source' => FilterSources::AIKIDO->value]); } private function expectLock(): void diff --git a/tests/Package/V2DumperTest.php b/tests/Package/V2DumperTest.php index b10d0eed1..d3545dad8 100644 --- a/tests/Package/V2DumperTest.php +++ b/tests/Package/V2DumperTest.php @@ -19,6 +19,7 @@ use App\Entity\Version; use App\FilterList\Dump\FilterListDumperProvider; use App\FilterList\FilterLists; +use App\FilterList\FilterSources; use App\FilterList\RemoteFilterListEntry; use App\Model\ProviderManager; use App\Package\V2Dumper; @@ -150,9 +151,10 @@ public function testDumpFilterEntries(): void $remote = new RemoteFilterListEntry( 'acme/package', '1.0.0', - FilterLists::AIKIDO_MALWARE, + FilterLists::MALWARE, null, 'malware', + FilterSources::AIKIDO, ); $filterEntry = new FilterListEntry($remote); $this->store($filterEntry); @@ -164,11 +166,12 @@ public function testDumpFilterEntries(): void $data = json_decode((string) file_get_contents($releaseFile), true); $this->assertArrayHasKey('filter', $data); - $this->assertArrayHasKey('aikido-malware', $data['filter']); - $filterList = $data['filter']['aikido-malware']; + $this->assertArrayHasKey('malware', $data['filter']); + $filterList = $data['filter']['malware']; $this->assertNotEmpty($filterList); $this->assertSame('1.0.0', $filterList[0]['constraint']); $this->assertSame('malware', $filterList[0]['reason']); + $this->assertSame(FilterSources::AIKIDO->value, $filterList[0]['source']); } public function testSpamFrozenPackageIsSkipped(): void diff --git a/translations/messages.en.yml b/translations/messages.en.yml index 25cc5b1fd..bd0d0123d 100644 --- a/translations/messages.en.yml +++ b/translations/messages.en.yml @@ -68,8 +68,8 @@ packages: from: "Packages from %vendor%" security_advisory_title: Security Advisories security_advisories: Security Advisories - filter_lists_title: Filters - filter_lists: Filters + filter_lists: + malware: Malware Reports browse: packages: Packages