-
Notifications
You must be signed in to change notification settings - Fork 45
New: Add security advisories endpoint. #86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
require __DIR__ . '/../vendor/autoload.php'; | ||
|
||
$client = new Packagist\Api\Client(); | ||
|
||
// Get any advisories for the monolog/monolog package | ||
$advisories = $client->advisories(['monolog/monolog']); | ||
var_export($advisories); | ||
|
||
// Get any advisories for the monolog/monolog package which were modified after midnight 2022/07/2022. | ||
$advisories = $client->advisories(['monolog/monolog' => '1.8.1'], 1659052800); | ||
var_export($advisories); | ||
|
||
// Get any advisories for the monolog/monolog package which will affect version 1.8.1 of that package | ||
$advisories = $client->advisories(['monolog/monolog' => '1.8.1'], null, true); | ||
var_export($advisories); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace spec\Packagist\Api\Result\Advisory; | ||
|
||
use Packagist\Api\Result\Advisory\Source; | ||
use PhpSpec\ObjectBehavior; | ||
|
||
class SourceSpec extends ObjectBehavior | ||
{ | ||
public function let() | ||
{ | ||
$this->fromArray([ | ||
'name' => 'FriendsOfPHP/security-advisories', | ||
'remoteId' => 'monolog/monolog/2014-12-29-1.yaml', | ||
]); | ||
} | ||
|
||
public function it_is_initializable() | ||
{ | ||
$this->shouldHaveType(Source::class); | ||
} | ||
|
||
public function it_gets_name() | ||
{ | ||
$this->getName()->shouldReturn('FriendsOfPHP/security-advisories'); | ||
} | ||
|
||
public function it_gets_remote_id() | ||
{ | ||
$this->getRemoteId()->shouldReturn('monolog/monolog/2014-12-29-1.yaml'); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace spec\Packagist\Api\Result; | ||
|
||
use Packagist\Api\Result\AbstractResult; | ||
use Packagist\Api\Result\Advisory; | ||
use Packagist\Api\Result\Advisory\Source; | ||
use PhpSpec\ObjectBehavior; | ||
|
||
class AdvisorySpec extends ObjectBehavior | ||
{ | ||
private $source; | ||
|
||
private function data() | ||
{ | ||
return [ | ||
'advisoryId' => 'PKSA-dmw8-jd8k-q3c6', | ||
'packageName' => 'monolog/monolog', | ||
'remoteId' => 'monolog/monolog/2014-12-29-1.yaml', | ||
'title' => 'Header injection in NativeMailerHandler', | ||
'link' => 'https://github.com/Seldaek/monolog/pull/448#issuecomment-68208704', | ||
'cve' => 'test-value', | ||
'affectedVersions' => '>=1.8.0,<1.12.0', | ||
'sources' => [$this->source], | ||
'reportedAt' => '2014-12-29 00:00:00', | ||
'composerRepository' => 'https://packagist.org', | ||
]; | ||
} | ||
|
||
public function let(Source $source) | ||
{ | ||
$this->source = $source; | ||
$this->fromArray($this->data()); | ||
} | ||
|
||
public function it_is_initializable() | ||
{ | ||
$this->shouldHaveType(Advisory::class); | ||
} | ||
|
||
public function it_is_a_packagist_result() | ||
{ | ||
$this->shouldHaveType(AbstractResult::class); | ||
} | ||
|
||
public function it_gets_advisory_id() | ||
{ | ||
$this->getAdvisoryId()->shouldReturn($this->data()['advisoryId']); | ||
} | ||
|
||
public function it_gets_package_name() | ||
{ | ||
$this->getPackageName()->shouldReturn($this->data()['packageName']); | ||
} | ||
|
||
public function it_gets_remote_id() | ||
{ | ||
$this->getRemoteId()->shouldReturn($this->data()['remoteId']); | ||
} | ||
|
||
public function it_gets_title() | ||
{ | ||
$this->getTitle()->shouldReturn($this->data()['title']); | ||
} | ||
|
||
public function it_gets_link() | ||
{ | ||
$this->getLink()->shouldReturn($this->data()['link']); | ||
} | ||
|
||
public function it_gets_cve() | ||
{ | ||
$this->getCve()->shouldReturn($this->data()['cve']); | ||
} | ||
|
||
public function it_gets_affected_versions() | ||
{ | ||
$this->getAffectedVersions()->shouldReturn($this->data()['affectedVersions']); | ||
} | ||
|
||
public function it_gets_sources() | ||
{ | ||
$this->getSources()->shouldReturn($this->data()['sources']); | ||
} | ||
|
||
public function it_gets_reported_at() | ||
{ | ||
$this->getReportedAt()->shouldReturn($this->data()['reportedAt']); | ||
} | ||
|
||
public function it_gets_composer_repository() | ||
{ | ||
$this->getComposerRepository()->shouldReturn($this->data()['composerRepository']); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,9 +4,11 @@ | |
|
||
namespace Packagist\Api; | ||
|
||
use Composer\Semver\Semver; | ||
use GuzzleHttp\Client as HttpClient; | ||
use GuzzleHttp\ClientInterface; | ||
use GuzzleHttp\Exception\GuzzleException; | ||
use Packagist\Api\Result\Advisory; | ||
use Packagist\Api\Result\Factory; | ||
use Packagist\Api\Result\Package; | ||
|
||
|
@@ -187,6 +189,96 @@ public function popular(int $total): array | |
return array_slice($results, 0, $total); | ||
} | ||
|
||
/** | ||
* Get a list of known security vulnerability advisories | ||
* | ||
* $packages can be a simple array of package names, or an array with package names | ||
* as keys and version strings as values. | ||
* | ||
* If $filterByVersion is true, any packages which are not accompanied by a version | ||
* number will be ignored. | ||
* | ||
* @param array $packages | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My initial instinct when testing this was to call (Not asking for changes here, just voicing my thoughts while testing) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I thought about allowing Maybe a future enhancement could be to change all of those to accept |
||
* @param integer|null $updatedSince A unix timestamp. | ||
* Only advisories updated after this date/time will be included | ||
* @param boolean $filterByVersion If true, only advisories which affect the version of packages in the | ||
* $packages array will be included | ||
* @return Advisory[] | ||
*/ | ||
public function advisories(array $packages = [], ?int $updatedSince = null, bool $filterByVersion = false): array | ||
{ | ||
if (count($packages) === 0 && $updatedSince === null) { | ||
throw new \InvalidArgumentException( | ||
'At least one package or an $updatedSince timestamp must be passed in.' | ||
); | ||
} | ||
|
||
if (count($packages) === 0 && $filterByVersion) { | ||
return []; | ||
} | ||
|
||
// Add updatedSince to query if passed in | ||
$query = []; | ||
if ($updatedSince !== null) { | ||
$query['updatedSince'] = $updatedSince; | ||
} | ||
$options = [ | ||
'query' => array_filter($query), | ||
]; | ||
|
||
// Add packages if appropriate | ||
if (count($packages) > 0) { | ||
$content = ['packages' => []]; | ||
foreach ($packages as $package => $version) { | ||
if (is_numeric($package)) { | ||
$package = $version; | ||
} | ||
$content['packages'][] = $package; | ||
} | ||
$options['headers']['Content-type'] = 'application/x-www-form-urlencoded'; | ||
$options['body'] = http_build_query($content); | ||
} | ||
|
||
// Get advisories from API | ||
/** @var Advisory[] $advisories */ | ||
$advisories = $this->respondPost($this->url('/api/security-advisories/'), $options); | ||
|
||
// Filter advisories if necessary | ||
if (count($advisories) > 0 && $filterByVersion) { | ||
return $this->filterAdvisories($advisories, $packages); | ||
} | ||
|
||
return $advisories; | ||
} | ||
|
||
/** | ||
* Filter the advisories array to only include any advisories that affect | ||
* the versions of packages in the $packages array | ||
* | ||
* @param Advisory[] $advisories | ||
* @param array $packages | ||
* @return Advisory[] Filtered advisories array | ||
*/ | ||
private function filterAdvisories(array $advisories, array $packages): array | ||
{ | ||
$filteredAdvisories = []; | ||
foreach ($packages as $package => $version) { | ||
// Skip any packages with no declared versions | ||
if (is_numeric($package)) { | ||
continue; | ||
} | ||
// Filter advisories by version | ||
if (array_key_exists($package, $advisories)) { | ||
foreach ($advisories[$package] as $advisory) { | ||
if (Semver::satisfies($version, $advisory->getAffectedVersions())) { | ||
$filteredAdvisories[$package][] = $advisory; | ||
} | ||
} | ||
} | ||
} | ||
return $filteredAdvisories; | ||
} | ||
|
||
/** | ||
* Assemble the packagist URL with the route | ||
* | ||
|
@@ -212,6 +304,21 @@ protected function respond(string $url) | |
return $this->create($response); | ||
} | ||
|
||
/** | ||
* Execute the POST request and parse the response | ||
* | ||
* @param string $url | ||
* @param array $option | ||
* @return array|Package | ||
*/ | ||
protected function respondPost(string $url, array $options) | ||
{ | ||
$response = $this->postRequest($url, $options); | ||
$response = $this->parse($response); | ||
|
||
return $this->create($response); | ||
} | ||
|
||
/** | ||
* Execute two URLs request, parse and merge the responses by adding the versions from the second URL | ||
* into the versions from the first URL. | ||
|
@@ -241,6 +348,22 @@ protected function multiRespond(string $url1, string $url2) | |
return $this->create($response1); | ||
} | ||
|
||
/** | ||
* Execute the POST request | ||
* | ||
* @param string $url | ||
* @param array $options | ||
* @return string | ||
* @throws GuzzleException | ||
*/ | ||
protected function postRequest(string $url, array $options): string | ||
{ | ||
return $this->httpClient | ||
->request('POST', $url, $options) | ||
->getBody() | ||
->getContents(); | ||
} | ||
|
||
/** | ||
* Execute the request URL | ||
* | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this docblock would benefit from some extra information describing what the
$updatedSince
and$filterByVersion
arguments are for. It's not immediately obvious to me.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated the phpdoc to be more descriptive