Skip to content

Commit

Permalink
Implement PackageScanner service (#170)
Browse files Browse the repository at this point in the history
* Implement PackageScanner service

* Remove php 7.4.3 constant
  • Loading branch information
karniv00l authored and akondas committed May 28, 2020
1 parent 57e599c commit f89cfbc
Show file tree
Hide file tree
Showing 47 changed files with 1,607 additions and 32 deletions.
1 change: 1 addition & 0 deletions config/packages/messenger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ framework:
'Buddy\Repman\Message\User\SendPasswordResetLink': async
'Buddy\Repman\Message\User\SendConfirmToken': async
'Buddy\Repman\Message\Proxy\AddDownloads': async
'Buddy\Repman\Message\Security\ScanPackage': async
1 change: 1 addition & 0 deletions config/packages/test/messenger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ framework:
routing:
'Buddy\Repman\Message\User\SendPasswordResetLink': sync
'Buddy\Repman\Message\User\SendConfirmToken': sync
'Buddy\Repman\Message\Organization\SynchronizePackage': sync
3 changes: 3 additions & 0 deletions config/services_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ services:
class: Buddy\Repman\Tests\Doubles\FakeBuddyApi

Buddy\Repman\Tests\Doubles\HttpClientStub:

Buddy\Repman\Service\Security\PackageScanner:
class: Buddy\Repman\Tests\Doubles\FakePackageScanner
34 changes: 34 additions & 0 deletions public/dist/css/dashboard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@charset "UTF-8";

.badge {
display: inline-block;
padding: 0.25em 0.4em;
font-size: 75%;
font-weight: 600;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 3px;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}

.badge-success {
color: #fff;
background-color: #5eba00;
}

.badge-info {
color: #fff;
background-color: #45aaf2;
}

.badge-warning {
color: #fff;
background-color: #f1c40f;
}

.badge-danger {
color: #fff;
background-color: #cd201f;
}
67 changes: 67 additions & 0 deletions src/Command/ScanAllPackagesCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace Buddy\Repman\Command;

use Buddy\Repman\Query\User\PackageQuery;
use Buddy\Repman\Repository\PackageRepository;
use Buddy\Repman\Service\Security\PackageScanner;
use Ramsey\Uuid\Uuid;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class ScanAllPackagesCommand extends Command
{
private PackageScanner $scanner;
private PackageQuery $packageQuery;
private PackageRepository $packageRepository;

public function __construct(PackageScanner $scanner, PackageQuery $packageQuery, PackageRepository $packageRepository)
{
$this->scanner = $scanner;
$this->packageQuery = $packageQuery;
$this->packageRepository = $packageRepository;

parent::__construct();
}

/**
* @return void
*/
protected function configure()
{
$this
->setName('repman:security:scan-all')
->setDescription('Scan all synchronized packages')
;
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$count = $this->packageQuery->getAllSynchronizedCount();
$limit = 50;
$offset = 0;

$progressBar = new ProgressBar($output, $count);
$progressBar->start();

for ($offset = 0; $offset <= $count; $offset = ($offset + 1) * $limit) {
$list = $this->packageQuery->getAllSynchronized($limit, $offset);

foreach ($list as $item) {
$this->scanner->scan(
$this->packageRepository->getById(Uuid::fromString($item->id()))
);
$progressBar->advance();
}
}

$progressBar->finish();
$output->writeln(sprintf("\nSuccessfully scanned %d packages", $count));

return 0;
}
}
26 changes: 26 additions & 0 deletions src/Controller/OrganizationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Buddy\Repman\Message\Organization\RemovePackage;
use Buddy\Repman\Message\Organization\RemoveToken;
use Buddy\Repman\Message\Organization\SynchronizePackage;
use Buddy\Repman\Message\Security\ScanPackage;
use Buddy\Repman\Query\User\Model\Installs\Day;
use Buddy\Repman\Query\User\Model\Organization;
use Buddy\Repman\Query\User\Model\Package;
Expand Down Expand Up @@ -321,6 +322,31 @@ public function stats(Organization $organization, Request $request): Response
]);
}

/**
* @Route("/organization/{organization}/package/{package}/scan", name="organization_package_scan", methods={"POST"}, requirements={"organization"="%organization_pattern%","package"="%uuid_pattern%"})
*/
public function scanPackage(Organization $organization, Package $package): Response
{
$this->dispatchMessage(new ScanPackage($package->id()));

$this->addFlash('success', 'Package will be scanned in the background');

return $this->redirectToRoute('organization_packages', ['organization' => $organization->alias()]);
}

/**
* @Route("/organization/{organization}/package/{package}/scan-results", name="organization_package_scan_results", methods={"GET","POST"}, requirements={"organization"="%organization_pattern%","package"="%uuid_pattern%"})
*/
public function packageScanResults(Organization $organization, Package $package, Request $request): Response
{
return $this->render('organization/package/scanResults.html.twig', [
'organization' => $organization,
'package' => $package,
'results' => $this->packageQuery->getScanResults($package->id(), 20, (int) $request->get('offset', 0)),
'count' => $this->packageQuery->getScanResultsCount($package->id()),
]);
}

protected function getUser(): User
{
/** @var User $user */
Expand Down
10 changes: 10 additions & 0 deletions src/Entity/Organization/Package.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ public function isSynchronized(): bool
return $this->name !== null;
}

public function isSynchronizedSuccessfully(): bool
{
return $this->isSynchronized() && $this->lastSyncError === null;
}

public function oauthToken(): string
{
$token = $this->organization->oauthToken(str_replace('-oauth', '', $this->type));
Expand Down Expand Up @@ -190,6 +195,11 @@ public function metadata(string $key)
return $this->metadata[$key];
}

public function latestReleasedVersion(): ?string
{
return $this->latestReleasedVersion;
}

private function setName(string $name): void
{
if (preg_match(self::NAME_PATTERN, $name, $matches) !== 1) {
Expand Down
73 changes: 73 additions & 0 deletions src/Entity/Organization/Package/ScanResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

namespace Buddy\Repman\Entity\Organization\Package;

use Buddy\Repman\Entity\Organization\Package;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Index;
use Ramsey\Uuid\UuidInterface;

/**
* @ORM\Entity(repositoryClass="Buddy\Repman\Repository\ScanResultRepository")
* @ORM\Table(
* name="organization_package_scan_result",
* indexes={
* @Index(name="date_idx", columns={"date"})
* }
* )
*/
class ScanResult
{
const STATUS_PENDING = 'pending';
const STATUS_OK = 'ok';
const STATUS_WARNING = 'warning';
const STATUS_ERROR = 'error';

/**
* @ORM\Id
* @ORM\Column(type="uuid", unique=true)
*/
private UuidInterface $id;

/**
* @ORM\ManyToOne(targetEntity="Buddy\Repman\Entity\Organization\Package")
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
private Package $package;

/**
* @ORM\Column(type="datetime_immutable")
*/
private \DateTimeImmutable $date;

/**
* @ORM\Column(type="string", length=7)
*/
private string $status;

/**
* @ORM\Column(type="string", length=255)
*/
private string $version;

/**
* @var array<string,array<string,string>|string>
* @ORM\Column(type="json")
*/
private array $content = [];

/**
* @param array<string,array<string,string>|string> $content
*/
public function __construct(UuidInterface $id, Package $package, \DateTimeImmutable $date, string $status, array $content)
{
$this->id = $id;
$this->package = $package;
$this->date = $date;
$this->status = $status;
$this->version = (string) $this->package->latestReleasedVersion();
$this->content = $content;
}
}
20 changes: 20 additions & 0 deletions src/Message/Security/ScanPackage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Buddy\Repman\Message\Security;

final class ScanPackage
{
private string $id;

public function __construct(string $id)
{
$this->id = $id;
}

public function id(): string
{
return $this->id;
}
}
15 changes: 14 additions & 1 deletion src/MessageHandler/Organization/SynchronizePackageHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,26 @@

use Buddy\Repman\Entity\Organization\Package;
use Buddy\Repman\Message\Organization\SynchronizePackage;
use Buddy\Repman\Message\Security\ScanPackage;
use Buddy\Repman\Repository\PackageRepository;
use Buddy\Repman\Service\PackageSynchronizer;
use Ramsey\Uuid\Uuid;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp;

final class SynchronizePackageHandler implements MessageHandlerInterface
{
private PackageSynchronizer $synchronizer;
private PackageRepository $packages;
private MessageBusInterface $messageBus;

public function __construct(PackageSynchronizer $synchronizer, PackageRepository $packages)
public function __construct(PackageSynchronizer $synchronizer, PackageRepository $packages, MessageBusInterface $messageBus)
{
$this->synchronizer = $synchronizer;
$this->packages = $packages;
$this->messageBus = $messageBus;
}

public function __invoke(SynchronizePackage $message): void
Expand All @@ -30,5 +36,12 @@ public function __invoke(SynchronizePackage $message): void
}

$this->synchronizer->synchronize($package);

if ($package->isSynchronizedSuccessfully()) {
$this->messageBus->dispatch(
(new Envelope(new ScanPackage($package->id()->toString())))
->with(new DispatchAfterCurrentBusStamp())
);
}
}
}
34 changes: 34 additions & 0 deletions src/MessageHandler/Security/ScanPackageHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Buddy\Repman\MessageHandler\Security;

use Buddy\Repman\Entity\Organization\Package;
use Buddy\Repman\Message\Security\ScanPackage;
use Buddy\Repman\Repository\PackageRepository;
use Buddy\Repman\Service\Security\PackageScanner;
use Ramsey\Uuid\Uuid;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;

final class ScanPackageHandler implements MessageHandlerInterface
{
private PackageScanner $scanner;
private PackageRepository $packages;

public function __construct(PackageScanner $scanner, PackageRepository $packages)
{
$this->scanner = $scanner;
$this->packages = $packages;
}

public function __invoke(ScanPackage $message): void
{
$package = $this->packages->find(Uuid::fromString($message->id()));
if (!$package instanceof Package) {
return;
}

$this->scanner->scan($package);
}
}
41 changes: 41 additions & 0 deletions src/Migrations/Version20200514173159.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Buddy\Repman\Migrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20200514173159 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');

$this->addSql('CREATE TABLE organization_package_scan_result (id UUID NOT NULL, package_id UUID NOT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, status VARCHAR(7) NOT NULL, version VARCHAR(255) NOT NULL, content JSON NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_9AB3F43AF44CABFF ON organization_package_scan_result (package_id)');
$this->addSql('CREATE INDEX date_idx ON organization_package_scan_result (date)');
$this->addSql('COMMENT ON COLUMN organization_package_scan_result.id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN organization_package_scan_result.package_id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN organization_package_scan_result.date IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE organization_package_scan_result ADD CONSTRAINT FK_9AB3F43AF44CABFF FOREIGN KEY (package_id) REFERENCES organization_package (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');

$this->addSql('DROP TABLE organization_package_scan_result');
}
}
Loading

0 comments on commit f89cfbc

Please sign in to comment.