瀏覽代碼

SecurityAdvisory add api endpoint

* store updatedAt for every advisory
* fix naming of advisory repository
Stephan Vock 5 年之前
父節點
當前提交
d50909e2eb

+ 27 - 0
src/Packagist/WebBundle/Controller/ApiController.php

@@ -13,6 +13,7 @@
 namespace Packagist\WebBundle\Controller;
 
 use Packagist\WebBundle\Entity\Package;
+use Packagist\WebBundle\Entity\SecurityAdvisory;
 use Packagist\WebBundle\Entity\User;
 use Packagist\WebBundle\Util\UserAgentParser;
 use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
@@ -246,6 +247,32 @@ class ApiController extends Controller
         return new JsonResponse(array('status' => 'success'), 201);
     }
 
+    /**
+     * @Route(
+     *     "/api/security-advisories/",
+     *     name="api_security_adivosries",
+     *     defaults={"_format" = "json"},
+     *     methods={"GET", "POST"}
+     * )
+     */
+    public function securityAdvisoryAction(Request $request)
+    {
+        $packageNames = array_values(array_filter(array_map(function (string $packageName) {
+            return trim($packageName);
+        }, explode(',', $request->get('packages')))));
+        $updatedSince = $request->query->get('updatedSince', 0);
+
+        /** @var array[] $advisories */
+        $advisories = $this->getDoctrine()->getRepository(SecurityAdvisory::class)->searchSecurityAdvisories($packageNames, $updatedSince);
+
+        $response = [];
+        foreach ($advisories as $advisory) {
+            $response[$advisory['packageName']][] = $advisory;
+        }
+
+        return new JsonResponse($response, 200);
+    }
+
     /**
      * @param string $name
      * @param string $version

+ 2 - 2
src/Packagist/WebBundle/Controller/PackageController.php

@@ -1129,8 +1129,8 @@ class PackageController extends Controller
 
     /**
      * @Route(
-     *      "/packages/{name}/security_advisories",
-     *      name="view_package_security_advisories",
+     *      "/packages/{name}/advisories",
+     *      name="view_package_advisories",
      *      requirements={"name"="([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?|ext-[A-Za-z0-9_.-]+?)"}
      * )
      */

+ 49 - 2
src/Packagist/WebBundle/Entity/SecurityAdvisory.php

@@ -9,9 +9,10 @@ use Packagist\WebBundle\SecurityAdvisory\RemoteSecurityAdvisory;
  * @ORM\Entity(repositoryClass="Packagist\WebBundle\Entity\SecurityAdvisoryRepository")
  * @ORM\Table(
  *     name="security_advisory",
- *     uniqueConstraints={@ORM\UniqueConstraint(name="source_packagename_idx", columns={"source","packageName"})},
+ *     uniqueConstraints={@ORM\UniqueConstraint(name="source_remoteid_idx", columns={"source","remoteId"})},
  *     indexes={
- *         @ORM\Index(name="package_name_idx",columns={"packageName"})
+ *         @ORM\Index(name="package_name_idx",columns={"packageName"}),
+ *         @ORM\Index(name="updated_at_idx",columns={"updatedAt"})
  *     }
  * )
  */
@@ -59,6 +60,11 @@ class SecurityAdvisory
      */
     private $source;
 
+    /**
+     * @ORM\Column(type="datetime")
+     */
+    private $updatedAt;
+
     public function __construct(RemoteSecurityAdvisory $advisory, string $source)
     {
         $this->source = $source;
@@ -67,6 +73,17 @@ class SecurityAdvisory
 
     public function updateAdvisory(RemoteSecurityAdvisory $advisory): void
     {
+        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->updatedAt = new \DateTime();
+        }
+
         $this->remoteId = $advisory->getId();
         $this->packageName = $advisory->getPackageName();
         $this->title = $advisory->getTitle();
@@ -79,4 +96,34 @@ class SecurityAdvisory
     {
         return $this->remoteId;
     }
+
+    public function getPackageName(): string
+    {
+        return $this->packageName;
+    }
+
+    public function getTitle(): string
+    {
+        return $this->title;
+    }
+
+    public function getLink(): ?string
+    {
+        return $this->link;
+    }
+
+    public function getCve(): ?string
+    {
+        return $this->cve;
+    }
+
+    public function getAffectedVersions(): string
+    {
+        return $this->affectedVersions;
+    }
+
+    public function getSource(): string
+    {
+        return $this->source;
+    }
 }

+ 25 - 3
src/Packagist/WebBundle/Entity/SecurityAdvisoryRepository.php

@@ -3,7 +3,7 @@
 namespace Packagist\WebBundle\Entity;
 
 use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
-use Doctrine\DBAL\Cache\QueryCacheProfile;
+use Doctrine\DBAL\Connection;
 use Symfony\Bridge\Doctrine\RegistryInterface;
 
 class SecurityAdvisoryRepository extends ServiceEntityRepository
@@ -13,7 +13,7 @@ class SecurityAdvisoryRepository extends ServiceEntityRepository
         parent::__construct($registry, SecurityAdvisory::class);
     }
 
-    public function getPackageSecurityAdvisories($name)
+    public function getPackageSecurityAdvisories(string $name): array
     {
         $sql = 'SELECT s.*
             FROM security_advisory s
@@ -25,7 +25,29 @@ class SecurityAdvisoryRepository extends ServiceEntityRepository
                 $sql,
                 ['name' => $name],
                 []
-//                new QueryCacheProfile(7*86400, 'security_advisories_'.$name.'_'.$offset.'_'.$limit, $this->getEntityManager()->getConfiguration()->getResultCacheImpl())
+            );
+        $result = $stmt->fetchAll();
+        $stmt->closeCursor();
+
+        return $result;
+    }
+
+    public function searchSecurityAdvisories(array $packageNames, int $updatedSince): array
+    {
+        $sql = 'SELECT s.packageName, s.remoteId, s.title, s.link, s.cve, s.affectedVersions, s.source
+            FROM security_advisory s
+            WHERE s.updatedAt >= :updatedSince ' .
+            (count($packageNames) > 0 ? ' AND s.packageName IN (:packageNames)' : '')
+            .' ORDER BY s.id DESC';
+
+        $stmt = $this->getEntityManager()->getConnection()
+            ->executeQuery(
+                $sql,
+                [
+                    'packageNames' => $packageNames,
+                    'updatedSince' => (new \DateTime('@' . $updatedSince))->format('Y-m-d H:i:s'),
+                ],
+                ['packageNames' => Connection::PARAM_STR_ARRAY]
             );
         $result = $stmt->fetchAll();
         $stmt->closeCursor();

+ 1 - 1
src/Packagist/WebBundle/Resources/views/package/view_package.html.twig

@@ -200,7 +200,7 @@
                             {% if securityAdvisories is defined %}
                                 <p>
                                     <span>
-                                        <a href="{{ path('view_package_security_advisories', {name: package.name}) }}" rel="nofollow">Security Advisories</a>:
+                                        <a href="{{ path('view_package_advisories', {name: package.name}) }}" rel="nofollow">Security</a>:
                                     </span>
                                     {{ securityAdvisories|number_format(0, '.', '&#8201;')|raw }}
                                 </p>

+ 3 - 3
src/Packagist/WebBundle/SecurityAdvisory/FriendsOfPhpSecurityAdvisoriesSource.php

@@ -17,7 +17,7 @@ use Symfony\Component\Yaml\Yaml;
 
 class FriendsOfPhpSecurityAdvisoriesSource implements SecurityAdvisorySourceInterface
 {
-    public const SOURCE_NAME = self::SECURITY_PACKAGE;
+    public const SOURCE_NAME = 'FriendsOfPHP/security-advisories';
     public const SECURITY_PACKAGE = 'sensiolabs/security-advisories';
 
     /** @var RegistryInterface */
@@ -50,7 +50,7 @@ class FriendsOfPhpSecurityAdvisoriesSource implements SecurityAdvisorySourceInte
             $rfs = Factory::createRemoteFilesystem($io, $config, []);
             $downloader = new ZipDownloader($io, $config, null, null, null, $rfs);
             $downloader->setOutputProgress(false);
-            $localDir = sys_get_temp_dir() . '/' . uniqid('friends-of-php-advisories', true);
+            $localDir = sys_get_temp_dir() . '/' . uniqid(self::SOURCE_NAME, true);
             $downloader->download($composerPackage, $localDir);
 
             $finder = new Finder();
@@ -62,7 +62,7 @@ class FriendsOfPhpSecurityAdvisoriesSource implements SecurityAdvisorySourceInte
                 $advisories[] = RemoteSecurityAdvisory::createFromFriendsOfPhp($file->getRelativePathname(), $content);
             }
         } catch (TransportException $e) {
-            $this->logger->error('Failed to download "sensiolabs/security-advisories" zip file', [
+            $this->logger->error(sprintf('Failed to download "%s" zip file', self::SECURITY_PACKAGE), [
                 'exception' => $e,
             ]);
         } finally {

+ 0 - 2
src/Packagist/WebBundle/Tests/SecurityAdvisory/RemoteSecurityAdvisoryTest.php

@@ -28,7 +28,5 @@ class RemoteSecurityAdvisoryTest extends TestCase
         $this->assertNull($advisory->getCve());
         $this->assertSame('<1.2', $advisory->getAffectedVersions());
         $this->assertSame('3f/pygmentize', $advisory->getPackageName());
-
-
     }
 }