瀏覽代碼

Package: display security advisories for individual packages

Stephan Vock 5 年之前
父節點
當前提交
8c895cef4f

+ 45 - 0
src/Packagist/WebBundle/Controller/PackageController.php

@@ -3,11 +3,14 @@
 namespace Packagist\WebBundle\Controller;
 
 use Composer\Package\Version\VersionParser;
+use Composer\Semver\Constraint\Constraint;
 use DateTimeImmutable;
 use Doctrine\ORM\NoResultException;
 use Packagist\WebBundle\Entity\Download;
 use Packagist\WebBundle\Entity\Package;
 use Packagist\WebBundle\Entity\PackageRepository;
+use Packagist\WebBundle\Entity\SecurityAdvisory;
+use Packagist\WebBundle\Entity\SecurityAdvisoryRepository;
 use Packagist\WebBundle\Entity\Version;
 use Packagist\WebBundle\Entity\Vendor;
 use Packagist\WebBundle\Entity\VersionRepository;
@@ -485,6 +488,21 @@ class PackageController extends Controller
         $data['dependents'] = $repo->getDependantCount($package->getName());
         $data['suggesters'] = $repo->getSuggestCount($package->getName());
 
+        /** @var SecurityAdvisoryRepository $securityAdvisoryRepository */
+        $securityAdvisoryRepository = $this->getDoctrine()->getRepository(SecurityAdvisory::class);
+        $securityAdvisories = $securityAdvisoryRepository->getPackageSecurityAdvisories($package->getName());
+        $data['securityAdvisories'] = count($securityAdvisories);
+        $data['hasVersionSecurityAdvisories'] = [];
+        foreach ($securityAdvisories as $advisory) {
+            $versionParser = new VersionParser();
+            $affectedVersionConstraint = $versionParser->parseConstraints($advisory['affectedVersions']);
+            foreach ($versions as $version) {
+                if (!isset($data['hasVersionSecurityAdvisories'][$version->getId()]) && $affectedVersionConstraint->matches(new Constraint('=', $version->getNormalizedVersion()))) {
+                    $data['hasVersionSecurityAdvisories'][$version->getId()] = true;
+                }
+            }
+        }
+
         if ($maintainerForm = $this->createAddMaintainerForm($package)) {
             $data['addMaintainerForm'] = $maintainerForm->createView();
         }
@@ -1109,6 +1127,33 @@ class PackageController extends Controller
         return $this->overallStatsAction($req, $package, $version);
     }
 
+    /**
+     * @Route(
+     *      "/packages/{name}/security_advisories",
+     *      name="view_package_security_advisories",
+     *      requirements={"name"="([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?|ext-[A-Za-z0-9_.-]+?)"}
+     * )
+     */
+    public function securityAdvisoriesAction(Request $req, $name)
+    {
+        $page = max(1, (int) $req->query->get('page', 1));
+
+        /** @var SecurityAdvisoryRepository $repo */
+        $repo = $this->getDoctrine()->getRepository(SecurityAdvisory::class);
+        $securityAdvisories = $repo->getPackageSecurityAdvisories($name);
+        $advisoryCount = count($securityAdvisories);
+
+        $paginator = new Pagerfanta(new FixedAdapter($advisoryCount, $securityAdvisories));
+        $paginator->setMaxPerPage(100);
+        $paginator->setCurrentPage($page, false, true);
+
+        $data['securityAdvisories'] = $paginator;
+        $data['count'] = $advisoryCount;
+        $data['name'] = $name;
+
+        return $this->render('PackagistWebBundle:package:security_advisories.html.twig', $data);
+    }
+
     private function createAddMaintainerForm(Package $package)
     {
         if (!$user = $this->getUser()) {

+ 1 - 1
src/Packagist/WebBundle/Entity/SecurityAdvisory.php

@@ -6,7 +6,7 @@ use Doctrine\ORM\Mapping as ORM;
 use Packagist\WebBundle\SecurityAdvisory\RemoteSecurityAdvisory;
 
 /**
- * @ORM\Entity()
+ * @ORM\Entity(repositoryClass="Packagist\WebBundle\Entity\SecurityAdvisoryRepository")
  * @ORM\Table(
  *     name="security_advisory",
  *     uniqueConstraints={@ORM\UniqueConstraint(name="source_packagename_idx", columns={"source","packageName"})},

+ 35 - 0
src/Packagist/WebBundle/Entity/SecurityAdvisoryRepository.php

@@ -0,0 +1,35 @@
+<?php declare(strict_types=1);
+
+namespace Packagist\WebBundle\Entity;
+
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\DBAL\Cache\QueryCacheProfile;
+use Symfony\Bridge\Doctrine\RegistryInterface;
+
+class SecurityAdvisoryRepository extends ServiceEntityRepository
+{
+    public function __construct(RegistryInterface $registry)
+    {
+        parent::__construct($registry, SecurityAdvisory::class);
+    }
+
+    public function getPackageSecurityAdvisories($name)
+    {
+        $sql = 'SELECT s.*
+            FROM security_advisory s
+            WHERE s.packageName = :name
+            ORDER BY s.id DESC';
+
+        $stmt = $this->getEntityManager()->getConnection()
+            ->executeQuery(
+                $sql,
+                ['name' => $name],
+                []
+//                new QueryCacheProfile(7*86400, 'security_advisories_'.$name.'_'.$offset.'_'.$limit, $this->getEntityManager()->getConfiguration()->getResultCacheImpl())
+            );
+        $result = $stmt->fetchAll();
+        $stmt->closeCursor();
+
+        return $result;
+    }
+}

+ 4 - 0
src/Packagist/WebBundle/Resources/public/css/main.css

@@ -1100,6 +1100,10 @@ input:focus:invalid:focus, textarea:focus:invalid:focus, select:focus:invalid:fo
 .package .package-aside i:hover {
   color: #a5aab0;
 }
+.package .package-aside i.advisory-alert {
+    color: #ff4533;
+    margin-left: -20px;
+}
 
 .package .details-toggler.open, .package .details-toggler.open a, .package .details-toggler.open i {
   background: #f28d1a;

+ 55 - 0
src/Packagist/WebBundle/Resources/views/package/security_advisories.html.twig

@@ -0,0 +1,55 @@
+{% extends "PackagistWebBundle::layout.html.twig" %}
+
+{% set showSearchDesc = 'hide' %}
+
+{% block head_additions %}<meta name="robots" content="noindex, nofollow">{% endblock %}
+
+{% block title %}{{ 'packages.security_advisory_title'|trans }} - {{ name }} - {{ parent() }}{% endblock %}
+
+{% block content %}
+    <div class="row">
+        <div class="col-xs-12 package">
+            <div class="package-header">
+                <h2 class="title">
+                    <a href="{{ path("view_package", {name: name}) }}">{{ name }}</a> {{ 'packages.security_advisories'|trans }}
+                    <small>({{ count }})</small>
+                </h2>
+            </div>
+        </div>
+    </div>
+
+    <section class="row">
+        <section class="col-md-12">
+            {% if securityAdvisories|length %}
+                <ul class="packages list-unstyled">
+                    {% for advisory in securityAdvisories %}
+                        <li class="row">
+                            <div class="col-xs-12 package-item">
+                                <div class="row">
+                                    <div class="col-sm-8 col-lg-9">
+                                        <h4 class="font-bold">
+                                            <a href="{{ advisory.link }}">{{ advisory.title }}</a>
+                                        </h4>
+                                        {% if advisory.cve %}
+                                            <p>
+                                                <a href="http://cve.mitre.org/cgi-bin/cvename.cgi?name={{ advisory.cve }}">{{ advisory.cve }}</a>
+                                            </p>
+                                        {% endif %}
+                                        <p>Affected version: {{ advisory.affectedVersions }}</p>
+                                    </div>
+                                    <div class="col-sm-4 col-lg-3">
+                                        <p>Reported by:<br/>{{ advisory.source }}</p>
+                                    </div>
+                                </div>
+                            </div>
+                        </li>
+                    {% endfor %}
+                </ul>
+            {% else %}
+                <div class="alert alert-danger">
+                    <p>{{ 'listing.no_security_advisories'|trans }}</p>
+                </div>
+            {% endif %}
+        </section>
+    </section>
+{% endblock %}

+ 4 - 0
src/Packagist/WebBundle/Resources/views/package/version_list.html.twig

@@ -10,6 +10,10 @@
                     {% endif -%}
                 </a>
 
+                {% if hasVersionSecurityAdvisories[version.id]|default(false) %}
+                    <i class="glyphicon glyphicon-alert advisory-alert" title="Version has security advisories"></i>
+                {% endif %}
+
                 {% if deleteVersionCsrfToken is defined and deleteVersionCsrfToken is not empty %}
                 <form class="delete-version" action="{{ path("delete_version", {"versionId": version.id}) }}" method="DELETE">
                     <input type="hidden" name="_token" value="{{ deleteVersionCsrfToken }}" />

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

@@ -197,6 +197,14 @@
                                     {{ suggesters|number_format(0, '.', '&#8201;')|raw }}
                                 </p>
                             {% endif %}
+                            {% if securityAdvisories is defined %}
+                                <p>
+                                    <span>
+                                        <a href="{{ path('view_package_security_advisories', {name: package.name}) }}" rel="nofollow">Security Advisories</a>:
+                                    </span>
+                                    {{ securityAdvisories|number_format(0, '.', '&#8201;')|raw }}
+                                </p>
+                            {% endif %}
                             {% if package.gitHubStars is not null %}
                                 <p>
                                     <span>
@@ -286,7 +294,7 @@
                         {% include 'PackagistWebBundle:package:version_details.html.twig' with {version: expandedVersion} %}
                     {% endif %}
                 </div>
-                {% include 'PackagistWebBundle:package:version_list.html.twig' with {versions: versions, expandedId: expandedVersion.id, deleteVersionCsrfToken: deleteVersionCsrfToken|default(null)} %}
+                {% include 'PackagistWebBundle:package:version_list.html.twig' with {versions: versions, expandedId: expandedVersion.id, deleteVersionCsrfToken: deleteVersionCsrfToken|default(null), hasVersionSecurityAdvisories: hasVersionSecurityAdvisories} %}
             {% elseif package.crawledAt is null %}
                 <p class="col-xs-12">This package has not been crawled yet, some information is missing.</p>
             {% else %}