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
10 changes: 9 additions & 1 deletion src/Package/V2Dumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,15 @@ private function writeV2File(Package $package, string $name, string $path, strin

// fetch the file to ensure the region we hit after has a hot cache so we can verify correctly that the purge worked
if (file_exists($path)) {
$this->fetchCdnFile($relativePath);
try {
$this->cdnClient->fetchPublicMetadata($relativePath);
} catch (\Exception $e) {
// ignore 404s as if the file is not there yet it means it's unlikely to be cached in a bad state on some other region
if ($e->getCode() !== 404) {
throw $e;
}
$this->httpClient->reset();
}
}

$this->filesystem->mkdir(\dirname($path));
Expand Down
45 changes: 45 additions & 0 deletions src/Service/FilterListWorker.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,20 @@
namespace App\Service;

use App\Entity\FilterListEntry;
use App\Entity\Package;
use App\FilterList\FilterLists;
use App\FilterList\List\FilterListInterface;
use App\FilterList\FilterListEntryUpdateListener;
use App\FilterList\FilterListResolver;
use App\Entity\Job;
use App\Model\DownloadManager;
use Psr\Log\LoggerInterface;
use Seld\Signal\SignalHandler;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

final readonly class FilterListWorker
{
Expand All @@ -36,6 +42,10 @@ public function __construct(
private array $filterLists,
private FilterListResolver $malwareFeedResolver,
private FilterListEntryUpdateListener $malwarePackageVersionUpdateListener,
private MailerInterface $mailer,
private DownloadManager $downloadManager,
private string $mailFromEmail,
private UrlGeneratorInterface $urlGenerator,
) {}

/**
Expand Down Expand Up @@ -78,6 +88,41 @@ public function process(Job $job, SignalHandler $signal): array

$this->malwarePackageVersionUpdateListener->flushChangesToPackages();

/** @var array<string, list<FilterListEntry>> $newEntriesByPackage */
$newEntriesByPackage = [];
foreach ($new as $entry) {
$newEntriesByPackage[$entry->getPackageName()][] = $entry;
}

foreach ($newEntriesByPackage as $packageName => $entries) {
$package = $this->doctrine->getRepository(Package::class)->findOneBy(['name' => $packageName]);
$downloads = $package ? $this->downloadManager->getTotalDownloads($package->getId()) : 0;
$packageUrl = $this->urlGenerator->generate('view_package', ['name' => $packageName], UrlGeneratorInterface::ABSOLUTE_URL);

if ($downloads >= 10_000) {
$subject = '[URGENT] Filter list entry added for high-download package ' . $packageName . ' (' . number_format($downloads) . ' downloads)';
$body = 'A new filter list entry has been added for ' . $packageName . ' which has ' . number_format($downloads) . " total downloads. This requires urgent attention.\n\n";
} else {
$subject = 'Filter list entry added for ' . $packageName;
$body = 'A new filter list entry has been added for ' . $packageName . ".\n\n";
}

$body .= 'Package: ' . $packageUrl . "\n"
. 'List: ' . $list->value . "\n"
. 'Versions: ' . implode(', ', array_map(fn (FilterListEntry $e) => $e->getVersion(), $entries)) . "\n"
. 'Reason: ' . ($entries[0]->getReason() ?? 'N/A') . "\n"
. 'Link: ' . ($entries[0]->getLink() ?? 'N/A') . "\n";

$message = (new Email())
->subject($subject)
->from(new Address($this->mailFromEmail))
->to($this->mailFromEmail)
->text($body)
;
$message->getHeaders()->addTextHeader('X-Auto-Response-Suppress', 'OOF, DR, RN, NRN, AutoReply');
$this->mailer->send($message);
}

$this->locker->unlockFilterList(self::FILTER_LIST_WORKER_RUN);

return [
Expand Down
28 changes: 27 additions & 1 deletion tests/FilterListWorkerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
use App\Entity\Job;
use App\Entity\FilterListEntry;
use App\Entity\FilterListEntryRepository;
use App\Entity\Package;
use App\Entity\PackageRepository;
use App\FilterList\FilterLists;
use App\FilterList\List\FilterListInterface;
use App\FilterList\FilterListEntryUpdateListener;
use App\FilterList\FilterListResolver;
use App\FilterList\RemoteFilterListEntry;
use App\Model\DownloadManager;
use App\Service\Locker;
use App\Service\FilterListWorker;
use Doctrine\DBAL\Connection;
Expand All @@ -29,21 +32,30 @@
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Seld\Signal\SignalHandler;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class FilterListWorkerTest extends TestCase
{
private FilterListWorker $worker;
private FilterListInterface&MockObject $filterList;
private EntityManager&MockObject $em;
private FilterListEntryRepository&MockObject $filterListEntryRepository;
private PackageRepository $packageRepository;
private Locker&MockObject $locker;
private MailerInterface&MockObject $mailer;
private DownloadManager $downloadManager;
private UrlGeneratorInterface $urlGenerator;

protected function setUp(): void
{
$this->filterList = $this->createMock(FilterListInterface::class);
$this->locker = $this->createMock(Locker::class);
$this->mailer = $this->createMock(MailerInterface::class);
$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->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);

$this->em = $this->createMock(EntityManager::class);

Expand All @@ -56,11 +68,13 @@ protected function setUp(): void
->willReturn($this->createStub(Connection::class));

$this->filterListEntryRepository = $this->createMock(FilterListEntryRepository::class);
$this->packageRepository = $this->createStub(PackageRepository::class);

$doctrine
->method('getRepository')
->willReturnMap([
[FilterListEntry::class, null, $this->filterListEntryRepository],
[Package::class, null, $this->packageRepository],
]);
}

Expand Down Expand Up @@ -94,6 +108,14 @@ public function testProcess(): void
->method('remove')
->with($this->identicalTo($existingEntryToBeDeleted));

$this->urlGenerator
->method('generate')
->willReturn('https://packagist.org/packages/vendor/new-malware');

$this->mailer
->expects($this->once())
->method('send');

$result = $this->worker->process($this->createJob(), SignalHandler::create());

$this->assertSame(Job::STATUS_COMPLETED, $result['status']);
Expand Down Expand Up @@ -205,5 +227,9 @@ private function expectNoPersistAndRemove(): void
$this->em
->expects($this->never())
->method('flush');

$this->mailer
->expects($this->never())
->method('send');
}
}