diff --git a/build-conflicts.php b/build-conflicts.php index 6425baeb..b563e916 100644 --- a/build-conflicts.php +++ b/build-conflicts.php @@ -23,6 +23,7 @@ use DateTime; use DateTimeZone; use ErrorException; +use Exception; use Http\Client\Curl\Client; use Psl\Dict; use Psl\Env; @@ -34,8 +35,12 @@ use Roave\SecurityAdvisories\AdvisorySources\GetAdvisoriesFromFriendsOfPhp; use Roave\SecurityAdvisories\AdvisorySources\GetAdvisoriesFromGithubApi; use Roave\SecurityAdvisories\AdvisorySources\GetAdvisoriesFromMultipleSources; +use Roave\SecurityAdvisories\Helper\ConstraintsMap; use Roave\SecurityAdvisories\Rule\RuleProviderFactory; +use function assert; +use function file_get_contents; +use function iterator_to_array; use function set_error_handler; use const E_NOTICE; @@ -89,10 +94,6 @@ static function (int $errorCode, string $message = '', string $file = '', int $l $cloneRoaveAdvisories = static function () use ($roaveAdvisoriesRepository, $buildDir): void { Shell\execute('git', ['clone', $roaveAdvisoriesRepository, $buildDir . '/roave-security-advisories']); - Shell\execute( - 'cp', - ['-r', $buildDir . '/roave-security-advisories', $buildDir . '/roave-security-advisories-original'] - ); }; $buildComponents = @@ -160,7 +161,15 @@ static function (array $components): array { Shell\execute('cp', [$sourceComposerJsonPath, $targetComposerJsonPath]); }; - $commitComposerJson = static function (string $composerJsonPath): void { + /** + * @param string $composerJsonPath + * @param array $addedAdvisories + * + * @return void + * + * @throws Exception + */ + $commitComposerJson = static function (string $composerJsonPath, array $addedAdvisories): void { $originalHash = Shell\execute( 'git', ['rev-parse', 'HEAD'], @@ -178,9 +187,30 @@ static function (array $components): array { $message .= "\n" . Str\format( 'Original commit: "%s"', - 'https://github.com/FriendsOfPHP/security-advisories/commit/' . $originalHash + 'https://github.com/FriendsOfPHP/security-advisories/commit/' . $originalHash, ); + $updatedAdvisoriesMessage = ''; + foreach ($addedAdvisories as $advisory) { + assert($advisory instanceof Advisory); + $updatedAdvisoriesMessage .= Str\format( + "\n\t%-15s| %s\n\t%-15s| %s\n\t%-15s| %s\n\t%-15s| %s\n", + 'Package name', + $advisory->package->packageName, + 'Summary', + $advisory->source->summary, + 'URI', + $advisory->source->uri, + 'Constraints', + $advisory->getConstraint() ?? '', + ); + } + + if (Str\Grapheme\length($updatedAdvisoriesMessage) !== 0) { + $updatedAdvisoriesMessage = "\n\n Security advisories updated:" . $updatedAdvisoriesMessage; + $message .= $updatedAdvisoriesMessage . "\n"; + } + try { Shell\execute('git', ['diff-index', '--quiet', 'HEAD'], $workingDirectory); } catch (Shell\Exception\FailedExecutionException) { @@ -219,9 +249,16 @@ static function (array $components): array { $validateComposerJson(__DIR__ . '/build/composer.json'); + $prevComposerJSONFileData = file_get_contents(__DIR__ . '/build/roave-security-advisories/composer.json'); + /** @var array> $prevComposerDecodedData */ + $prevComposerDecodedData = Json\decode($prevComposerJSONFileData, true); + $currentConstraints = ConstraintsMap::fromArray($prevComposerDecodedData['conflict']); + $updatedAdvisories = $currentConstraints->advisoriesDiff(iterator_to_array($getAdvisories())); + $copyGeneratedComposerJson( __DIR__ . '/build/composer.json', __DIR__ . '/build/roave-security-advisories/composer.json' ); - $commitComposerJson(__DIR__ . '/build/roave-security-advisories/composer.json'); + + $commitComposerJson(__DIR__ . '/build/roave-security-advisories/composer.json', $updatedAdvisories); })(); diff --git a/src/Roave/SecurityAdvisories/Advisory.php b/src/Roave/SecurityAdvisories/Advisory.php index 8d98eb6a..72048b1d 100644 --- a/src/Roave/SecurityAdvisories/Advisory.php +++ b/src/Roave/SecurityAdvisories/Advisory.php @@ -33,17 +33,23 @@ final class Advisory /** @var list */ private array $branchConstraints; - /** @param list $branchConstraints */ - private function __construct(PackageName $package, array $branchConstraints) + public Source $source; + + /** + * @param list $branchConstraints + */ + private function __construct(PackageName $package, array $branchConstraints, Source $source) { $this->package = $package; $this->branchConstraints = $this->sortVersionConstraints($branchConstraints); + $this->source = $source; } /** * @psalm-param array{ * branches: array}>, - * reference: string + * reference: string, + * source: array{summary: string, link:string}, * } $config * * @return Advisory @@ -70,7 +76,8 @@ static function (array $branchConfig): VersionConstraint { return VersionConstraint::fromString(Str\join(Vec\values($versions), ',')); } - ) + ), + Source::new($config['source']['summary'], $config['source']['link']), ); } diff --git a/src/Roave/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromFriendsOfPhp.php b/src/Roave/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromFriendsOfPhp.php index d1f8241f..a8bc4cb3 100644 --- a/src/Roave/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromFriendsOfPhp.php +++ b/src/Roave/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromFriendsOfPhp.php @@ -62,8 +62,12 @@ static function (SplFileInfo $advisoryFile): Advisory { 'versions' => Type\union(Type\string(), Type\vec(Type\string())), ], true)), 'reference' => Type\string(), + 'title' => Type\string(), + 'link' => Type\string(), ], true)->assert(Yaml::parse($yaml, Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE)); + $definition['source'] = ['summary' => $definition['title'], 'link' => $definition['link']]; + return Advisory::fromArrayData($definition); }, ); diff --git a/src/Roave/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromGithubApi.php b/src/Roave/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromGithubApi.php index bdab7304..e5170bc1 100644 --- a/src/Roave/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromGithubApi.php +++ b/src/Roave/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromGithubApi.php @@ -45,6 +45,9 @@ final class GetAdvisoriesFromGithubApi implements GetAdvisories } advisory { withdrawnAt + ghsaId + permalink + summary } } } @@ -83,7 +86,8 @@ public function __invoke(): Generator $versions = Type\shape([0 => Type\non_empty_string(), 1 => Type\optional(Type\non_empty_string())]) ->assert(Str\split($item['node']['vulnerableVersionRange'], ',')); - if ($item['node']['advisory']['withdrawnAt'] !== null) { + $advisory = $item['node']['advisory']; + if ($advisory['withdrawnAt'] !== null) { // Skip withdrawn advisories. continue; } @@ -93,6 +97,7 @@ public function __invoke(): Generator [ 'reference' => $item['node']['package']['name'], 'branches' => [['versions' => $versions]], + 'source' => ['summary' => $advisory['summary'], 'link' => $advisory['permalink']], ] ); } catch (InvalidPackageName) { @@ -115,7 +120,11 @@ public function __invoke(): Generator * node: array{ * vulnerableVersionRange: string, * package: array{name: string}, - * advisory: array{withdrawnAt: string|null} + * advisory: array{ + * withdrawnAt: string|null, + * permalink: string, + * summary: string, + * } * } * }> * @@ -126,8 +135,8 @@ private function getAdvisories(): iterable $cursor = ''; do { - $response = $this->client->sendRequest($this->getRequest($cursor)); - $data = Json\typed($response->getBody()->__toString(), Type\shape([ + $response = $this->client->sendRequest($this->getRequest($cursor)); + $data = Json\typed($response->getBody()->__toString(), Type\shape([ 'data' => Type\shape([ 'securityVulnerabilities' => Type\shape([ 'edges' => Type\dict(Type\int(), Type\shape([ @@ -135,7 +144,11 @@ private function getAdvisories(): iterable 'node' => Type\shape([ 'vulnerableVersionRange' => Type\string(), 'package' => Type\shape(['name' => Type\string()]), - 'advisory' => Type\shape(['withdrawnAt' => Type\nullable(Type\string())]), + 'advisory' => Type\shape([ + 'withdrawnAt' => Type\nullable(Type\string()), + 'permalink' => Type\string(), + 'summary' => Type\string(), + ]), ]), ])), 'pageInfo' => Type\shape([ @@ -145,6 +158,7 @@ private function getAdvisories(): iterable ]), ]), ])); + $vulnerabilities = $data['data']['securityVulnerabilities']; yield from $vulnerabilities['edges']; diff --git a/src/Roave/SecurityAdvisories/Helper/ConstraintsMap.php b/src/Roave/SecurityAdvisories/Helper/ConstraintsMap.php new file mode 100644 index 00000000..15495276 --- /dev/null +++ b/src/Roave/SecurityAdvisories/Helper/ConstraintsMap.php @@ -0,0 +1,98 @@ +> $map */ + private array $map; + + /** + * @param array> $conflicts + */ + private function __construct(array $conflicts) + { + $this->map = $conflicts; + } + + /** + * @param array $packageConflictsParsedData + * + * @return ConstraintsMap + */ + public static function fromArray(array $packageConflictsParsedData): self + { + $packageConflicts = []; + + foreach ($packageConflictsParsedData as $referenceName => $constraintsString) { + $packageConstraints = []; + foreach (split($constraintsString, '|') as $range) { + $packageConstraints[] = VersionConstraint::fromString($range); + } + + $packageConflicts[$referenceName] = $packageConstraints; + } + + return new self($packageConflicts); + } + + /** + * @param array $advisoriesToFilter + * + * @return array + */ + public function advisoriesDiff(array $advisoriesToFilter): array + { + $filteredAdvisories = []; + + foreach ($advisoriesToFilter as $advisoryToFilter) { + $pkgNameKey = $advisoryToFilter->package->packageName; + + $isNewAdvisory = ! array_key_exists($pkgNameKey, $this->map); + + if ($isNewAdvisory) { + $filteredAdvisories[] = $advisoryToFilter; + continue; + } + + $isUpdateAdvisory = $this->isAdvisoryUpdate($pkgNameKey, $advisoryToFilter); + + if (! $isUpdateAdvisory) { + continue; + } + + $filteredAdvisories[] = $advisoryToFilter; + } + + return $filteredAdvisories; + } + + private function isAdvisoryUpdate(string $packageName, Advisory $advisoryToCheck): bool + { + $packageConstraints = $this->map[$packageName]; + + foreach ($advisoryToCheck->getVersionConstraints() as $advisoryConstraint) { + $included = false; + foreach ($packageConstraints as $pkgConstraint) { + if ($pkgConstraint->contains($advisoryConstraint)) { + $included = true; + break; + } + } + + if (! $included) { + return true; + } + } + + return false; + } +} diff --git a/src/Roave/SecurityAdvisories/Matchers.php b/src/Roave/SecurityAdvisories/Matchers.php index a8df4e87..c74b6633 100644 --- a/src/Roave/SecurityAdvisories/Matchers.php +++ b/src/Roave/SecurityAdvisories/Matchers.php @@ -23,8 +23,6 @@ /** * @see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string * - * @fixme: throw this garbage away and use existing regexp from semver.org - * * @psalm-immutable */ final class Matchers diff --git a/src/Roave/SecurityAdvisories/Rule/RuleProviderFactory.php b/src/Roave/SecurityAdvisories/Rule/RuleProviderFactory.php index 8514c2a3..e6f9e48e 100644 --- a/src/Roave/SecurityAdvisories/Rule/RuleProviderFactory.php +++ b/src/Roave/SecurityAdvisories/Rule/RuleProviderFactory.php @@ -47,6 +47,7 @@ static function (Advisory $advisory): Advisory { 'branches' => [ ['versions' => ['<2.17.1']], ], + 'source' => ['summary' => 'summary', 'link' => 'link'], ]); }, ]; diff --git a/src/Roave/SecurityAdvisories/Source.php b/src/Roave/SecurityAdvisories/Source.php new file mode 100644 index 00000000..998d4909 --- /dev/null +++ b/src/Roave/SecurityAdvisories/Source.php @@ -0,0 +1,32 @@ +summary = $summary; + $this->uri = $uri; + } + + public static function new(string $summary, string $uri): self + { + return new self(Type\non_empty_string()->assert($summary), Type\non_empty_string()->assert($uri)); + } +} diff --git a/src/Roave/SecurityAdvisories/VersionConstraint.php b/src/Roave/SecurityAdvisories/VersionConstraint.php index 5c5d8b3e..b4b2482e 100644 --- a/src/Roave/SecurityAdvisories/VersionConstraint.php +++ b/src/Roave/SecurityAdvisories/VersionConstraint.php @@ -11,6 +11,8 @@ use Psl\Str; use Psl\Vec; +use function strcmp; + /** * A simple version constraint - naively assumes that it is only about ranges like ">=1.2.3,<4.5.6" * @@ -33,7 +35,7 @@ private function __construct() */ public static function fromString(string $versionConstraint): self { - $constraintString = $versionConstraint; + $constraintString = Str\replace($versionConstraint, ' ', ''); $instance = new self(); if (Regex\matches($constraintString, Matchers::CLOSED_RANGE_MATCHER)) { @@ -142,8 +144,12 @@ public function mergeWith(self $other): self )); } - private function contains(self $other): bool + public function contains(self $other): bool { + if ($this->constraintString !== null && $other->constraintString !== null) { + return strcmp($this->constraintString, $other->constraintString) === 0; + } + return $this->isSimpleRangeString() // cannot compare - too complex :-( && $other->isSimpleRangeString() // cannot compare - too complex :-( && $this->containsLowerBound($other->lowerBoundary) diff --git a/test/RoaveTest/SecurityAdvisories/AdvisorySources/GetAdvisoriesAdvisoryRuleDecoratorTest.php b/test/RoaveTest/SecurityAdvisories/AdvisorySources/GetAdvisoriesAdvisoryRuleDecoratorTest.php index 34bdab7e..6f7deeb7 100644 --- a/test/RoaveTest/SecurityAdvisories/AdvisorySources/GetAdvisoriesAdvisoryRuleDecoratorTest.php +++ b/test/RoaveTest/SecurityAdvisories/AdvisorySources/GetAdvisoriesAdvisoryRuleDecoratorTest.php @@ -57,6 +57,7 @@ static function (Advisory $advisory): Advisory { return Advisory::fromArrayData([ 'reference' => $packageName, 'branches' => [['versions' => ['<1.1']]], + 'source' => ['summary' => 'summary', 'link' => 'link'], ]); }; @@ -74,6 +75,7 @@ static function (Advisory $advisory): Advisory { return Advisory::fromArrayData([ 'reference' => $packageName, 'branches' => [['versions' => ['>=3']]], + 'source' => ['summary' => 'summary', 'link' => 'link'], ]); }; @@ -100,18 +102,21 @@ static function (Advisory $advisory): Advisory { ['versions' => ['>2.2']], ], 'reference' => 'composer://3f/pygmentize', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), Advisory::fromArrayData([ 'branches' => [ ['versions' => ['<1.1']], // changed by rule ], 'reference' => 'composer://3f/pygmentize', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), Advisory::fromArrayData([ 'branches' => [ ['versions' => ['>=3']], // changed by rule ], 'reference' => 'composer://other/package-name', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), Advisory::fromArrayData([ @@ -119,6 +124,7 @@ static function (Advisory $advisory): Advisory { ['versions' => ['>5']], ], 'reference' => 'composer://other/package-name', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), ], Vec\values($decoratedAdvisories)); } @@ -163,6 +169,8 @@ static function (Advisory $advisory): Advisory { $config['reference'] = $packageName; $config['branches'] = [['versions' => ['<1.0|>2.0']]]; + $config['source'] = ['summary' => 'summary', 'link' => 'link']; + return Advisory::fromArrayData($config); }; @@ -185,24 +193,28 @@ static function (Advisory $advisory): Advisory { ['versions' => ['>2.2']], ], 'reference' => 'composer://3f/pygmentize', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), Advisory::fromArrayData([ 'branches' => [ ['versions' => ['<1.0|>2.0']], ], 'reference' => 'composer://3f/pygmentize', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), Advisory::fromArrayData([ 'branches' => [ ['versions' => ['<2|>4']], ], 'reference' => 'composer://other/package-name', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), Advisory::fromArrayData([ 'branches' => [ ['versions' => ['>5']], ], 'reference' => 'composer://other/package-name', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), ], Vec\values($decoratedAdvisories) @@ -232,6 +244,7 @@ static function (Advisory $advisory): Advisory { 'versions' => ['<2.17.1'], // change constraint to <2.17.1 ], ]; + $config['source'] = ['summary' => 'summary', 'link' => 'link']; return Advisory::fromArrayData($config); }; @@ -243,6 +256,7 @@ static function (Advisory $advisory): Advisory { Advisory::fromArrayData([ 'branches' => [['versions' => ['<2.17.2']]], 'reference' => 'composer://laminas/laminas-form', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), ); @@ -263,28 +277,33 @@ static function (Advisory $advisory): Advisory { ['versions' => ['>2.2']], ], 'reference' => 'composer://3f/pygmentize', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), Advisory::fromArrayData([ 'branches' => [ ['versions' => ['<1.2']], ], 'reference' => 'composer://3f/pygmentize', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), Advisory::fromArrayData([ 'branches' => [ ['versions' => ['<2|>4']], ], 'reference' => 'composer://other/package-name', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), Advisory::fromArrayData([ 'branches' => [ ['versions' => ['>5']], ], 'reference' => 'composer://other/package-name', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), Advisory::fromArrayData([ 'branches' => [['versions' => ['<2.17.1']]], 'reference' => 'composer://laminas/laminas-form', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), ], Vec\values($decoratedAdvisories) @@ -314,6 +333,7 @@ static function (Advisory $advisory): Advisory { 'versions' => ['<2.17.1'], // change constraint to <2.17.1 ], ]; + $config['source'] = ['summary' => 'summary', 'link' => 'link']; return Advisory::fromArrayData($config); }; @@ -329,6 +349,7 @@ static function (Advisory $advisory): Advisory { ], ], 'reference' => 'composer://laminas/laminas-form', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), ); $getAdvisories->addAdvisory( @@ -339,6 +360,7 @@ static function (Advisory $advisory): Advisory { ], ], 'reference' => 'composer://laminas/laminas-form', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), ); @@ -358,36 +380,42 @@ static function (Advisory $advisory): Advisory { ['versions' => ['>2.2']], ], 'reference' => 'composer://3f/pygmentize', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), Advisory::fromArrayData([ 'branches' => [ ['versions' => ['<1.2']], ], 'reference' => 'composer://3f/pygmentize', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), Advisory::fromArrayData([ 'branches' => [ ['versions' => ['<2|>4']], ], 'reference' => 'composer://other/package-name', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), Advisory::fromArrayData([ 'branches' => [ ['versions' => ['>5']], ], 'reference' => 'composer://other/package-name', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), Advisory::fromArrayData([ 'branches' => [ ['versions' => ['<2.17.1']], ], 'reference' => 'composer://laminas/laminas-form', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), Advisory::fromArrayData([ 'branches' => [ ['versions' => ['>=3', '<3.0.2']], ], 'reference' => 'composer://laminas/laminas-form', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]), ], Vec\values($decoratedAdvisories)); } @@ -418,6 +446,7 @@ public function __invoke(): Generator ['versions' => ['>2.2']], ], 'reference' => 'composer://3f/pygmentize', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]); yield Advisory::fromArrayData([ @@ -425,6 +454,7 @@ public function __invoke(): Generator ['versions' => ['<1.2']], ], 'reference' => 'composer://3f/pygmentize', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]); yield Advisory::fromArrayData([ @@ -432,6 +462,7 @@ public function __invoke(): Generator ['versions' => ['<2|>4']], ], 'reference' => 'composer://other/package-name', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]); yield Advisory::fromArrayData([ @@ -439,6 +470,7 @@ public function __invoke(): Generator ['versions' => ['>5']], ], 'reference' => 'composer://other/package-name', + 'source' => ['summary' => 'summary', 'link' => 'link'], ]); if (count($this->advisories) === 0) { diff --git a/test/RoaveTest/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromFriendsOfPhpTest.php b/test/RoaveTest/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromFriendsOfPhpTest.php index 942607b5..5802d187 100644 --- a/test/RoaveTest/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromFriendsOfPhpTest.php +++ b/test/RoaveTest/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromFriendsOfPhpTest.php @@ -42,6 +42,10 @@ public function testThatAdvisoriesAreBuiltFromYamlFiles(): void ], ], 'reference' => 'composer://3f/pygmentize', + 'source' => [ + 'summary' => 'Remote Code Execution', + 'link' => 'https://github.com/dedalozzo/pygmentize/issues/1', + ], ]), ], Vec\values($advisories)); } diff --git a/test/RoaveTest/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromGithubApiTest.php b/test/RoaveTest/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromGithubApiTest.php index be82c626..84146d50 100644 --- a/test/RoaveTest/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromGithubApiTest.php +++ b/test/RoaveTest/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromGithubApiTest.php @@ -98,10 +98,12 @@ public function testGithubAdvisoriesIsAbleToProduceAdvisories(array $apiResponse Advisory::fromArrayData([ 'reference' => 'enshrined/svg-sanitize', 'branches' => [['versions' => ['> 0.12.0, < 0.12.1 ']]], + 'source' => ['summary' => 'some summary', 'link' => 'https://example.com'], ]), Advisory::fromArrayData([ 'reference' => 'foo/bar', 'branches' => [['versions' => ['> 1.2.3, < 4.5.6 ']]], + 'source' => ['summary' => 'some summary', 'link' => 'https://example.com'], ]), ], Vec\values($advisories()) @@ -152,7 +154,9 @@ public function correctResponsesSequenceDataProvider(): array "name": "enshrined/svg-sanitize" }, "advisory": { - "withdrawnAt": null + "withdrawnAt": null, + "summary": "some summary", + "permalink": "https://example.com" } } }, @@ -164,7 +168,9 @@ public function correctResponsesSequenceDataProvider(): array "name": "foo/bar" }, "advisory": { - "withdrawnAt": null + "withdrawnAt": null, + "summary": "some summary", + "permalink": "https://example.com" } } } @@ -222,10 +228,12 @@ public function testWillSkipAdvisoriesWithMalformedNames(ResponseInterface ...$r Advisory::fromArrayData([ 'reference' => 'aa/bb', 'branches' => [['versions' => ['> 0.12.0, < 0.12.1 ']]], + 'source' => ['summary' => 'some summary', 'link' => 'https://example.com'], ]), Advisory::fromArrayData([ 'reference' => 'dd/ee', 'branches' => [['versions' => ['> 1.2.3, < 4.5.6 ']]], + 'source' => ['summary' => 'some summary', 'link' => 'https://example.com'], ]), ], Vec\values($advisories()) @@ -250,6 +258,7 @@ public function testWillSkipWithdrawnAdvisories(ResponseInterface ...$responses) Advisory::fromArrayData([ 'reference' => 'aa/bb', 'branches' => [['versions' => ['<= 1.1.0']]], + 'source' => ['summary' => 'some summary', 'link' => 'https://example.com'], ]), ], Vec\Values($advisories())); } @@ -271,7 +280,9 @@ public function correctResponsesWithInvalidAdvisoryNames(): array "name": "aa/bb" }, "advisory": { - "withdrawnAt": null + "withdrawnAt": null, + "summary": "some summary", + "permalink": "https://example.com" } } }, @@ -283,7 +294,9 @@ public function correctResponsesWithInvalidAdvisoryNames(): array "name": "cc" }, "advisory": { - "withdrawnAt": null + "withdrawnAt": null, + "summary": "some summary", + "permalink": "https://example.com" } } }, @@ -295,7 +308,9 @@ public function correctResponsesWithInvalidAdvisoryNames(): array "name": "dd/ee" }, "advisory": { - "withdrawnAt": null + "withdrawnAt": null, + "summary": "some summary", + "permalink": "https://example.com" } } } @@ -350,7 +365,9 @@ public function responsesWithIncorrectRangesProvider(): array "name": "enshrined/svg-sanitize" }, "advisory": { - "withdrawnAt": null + "withdrawnAt": null, + "summary": "some summary", + "permalink": "https://example.com" } } } @@ -396,7 +413,9 @@ public function correctResponseWithWithdrawnAdvisories(): array "name": "aa/bb" }, "advisory": { - "withdrawnAt": "2021-11-17T15:54:51Z" + "withdrawnAt": "2021-11-17T15:54:51Z", + "summary": "some summary", + "permalink": "https://example.com" } } }, @@ -408,7 +427,9 @@ public function correctResponseWithWithdrawnAdvisories(): array "name": "aa/bb" }, "advisory": { - "withdrawnAt": null + "withdrawnAt": null, + "summary": "some summary", + "permalink": "https://example.com" } } } diff --git a/test/RoaveTest/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromMultipleSourcesTest.php b/test/RoaveTest/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromMultipleSourcesTest.php index 71e70cdb..11d03e2b 100644 --- a/test/RoaveTest/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromMultipleSourcesTest.php +++ b/test/RoaveTest/SecurityAdvisories/AdvisorySources/GetAdvisoriesFromMultipleSourcesTest.php @@ -44,6 +44,7 @@ public function testMultipleAdvisoriesSources(): void Advisory::fromArrayData([ 'reference' => 'test/package', 'branches' => [['versions' => ['<1']]], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]), ], Vec\values($advisories()) @@ -55,6 +56,7 @@ private function getGenerator(): Generator return yield Advisory::fromArrayData([ 'reference' => 'test/package', 'branches' => [['versions' => ['<1']]], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); } } diff --git a/test/RoaveTest/SecurityAdvisories/AdvisoryTest.php b/test/RoaveTest/SecurityAdvisories/AdvisoryTest.php index 6ae9cd5e..f517ed43 100644 --- a/test/RoaveTest/SecurityAdvisories/AdvisoryTest.php +++ b/test/RoaveTest/SecurityAdvisories/AdvisoryTest.php @@ -43,6 +43,7 @@ public function testFromArrayWithValidConfig(): void 'versions' => ['>=2.0', '<2.1'], ], ], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); self::assertEquals(PackageName::fromName('foo/bar'), $advisory->package); @@ -67,6 +68,7 @@ public function testFromArrayWithComplexValidConfig(): void 'versions' => ['>=2.0-rc.5', '<2.1-rc.6'], ], ], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); self::assertEquals(PackageName::fromName('foo/bar'), $advisory->package); @@ -87,6 +89,7 @@ public function testFromArrayWithStringVersion(): void '1.0.x' => ['versions' => '<1.1'], '2.0.x' => ['versions' => '<2.1'], ], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); self::assertEquals(PackageName::fromName('foo/bar'), $advisory->package); @@ -107,6 +110,7 @@ public function testFromArrayWithComplexStringVersion(): void '1.0.x' => ['versions' => '<1.1-beta.0.1'], '2.0.x' => ['versions' => '<2.1-beta.0.1'], ], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); self::assertEquals(PackageName::fromName('foo/bar'), $advisory->package); @@ -124,6 +128,7 @@ public function testFromArrayWithWrongPackageName(): void $advisory = Advisory::fromArrayData([ 'reference' => 'composer://foo\bar', 'branches' => [], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); self::assertEquals(PackageName::fromName('foo/bar'), $advisory->package); @@ -146,6 +151,7 @@ public function testFromArrayGeneratesSortedResult( '2.0.x' => ['versions' => $versionConstraint2], '1.0.x' => ['versions' => $versionConstraint1], ], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); self::assertSame($expected, $advisory->getConstraint()); diff --git a/test/RoaveTest/SecurityAdvisories/ComponentTest.php b/test/RoaveTest/SecurityAdvisories/ComponentTest.php index a879b13c..8686dd97 100644 --- a/test/RoaveTest/SecurityAdvisories/ComponentTest.php +++ b/test/RoaveTest/SecurityAdvisories/ComponentTest.php @@ -44,6 +44,7 @@ public function testFromMultipleAdvisories(): void 'versions' => ['>=2.0-beta.1.1', '<2.1-beta.1.1'], ], ], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); $advisory2 = Advisory::fromArrayData([ 'reference' => 'composer://foo/bar', @@ -55,6 +56,7 @@ public function testFromMultipleAdvisories(): void 'versions' => ['>=4.0-beta.1.1', '<4.1-beta.1.1'], ], ], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); $component = new Component(PackageName::fromName('foo/bar'), $advisory1, $advisory2); @@ -77,6 +79,7 @@ public function testDeDuplicatesOverlappingAdvisories(): void 'versions' => ['>=2.0', '<2.1'], ], ], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); $advisory2 = Advisory::fromArrayData([ 'reference' => 'composer://foo/bar', @@ -94,6 +97,7 @@ public function testDeDuplicatesOverlappingAdvisories(): void 'versions' => ['>=3.0', '<3.1'], ], ], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); $advisory3 = Advisory::fromArrayData([ 'reference' => 'composer://foo/bar', @@ -103,6 +107,7 @@ public function testDeDuplicatesOverlappingAdvisories(): void 'versions' => ['>=3.0.1', '<3.0.99'], ], ], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); $component = new Component(PackageName::fromName('foo/bar'), $advisory1, $advisory2, $advisory3); @@ -123,6 +128,7 @@ public function testDeDuplicatesOverlappingComplexAdvisories(): void 'versions' => ['>=2.0-rc', '<2.1-p'], ], ], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); $advisory2 = Advisory::fromArrayData([ 'reference' => 'composer://foo/bar', @@ -140,6 +146,7 @@ public function testDeDuplicatesOverlappingComplexAdvisories(): void 'versions' => ['>=3.0-stable.5', '<3.1'], ], ], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); $advisory3 = Advisory::fromArrayData([ 'reference' => 'composer://foo/bar', @@ -149,6 +156,7 @@ public function testDeDuplicatesOverlappingComplexAdvisories(): void 'versions' => ['>=3.0.1', '<3.0.99'], ], ], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); $component = new Component(PackageName::fromName('foo/bar'), $advisory1, $advisory2, $advisory3); @@ -167,6 +175,7 @@ public function testSortAdvisoriesWithRealCase(): void 'versions' => ['>=3.1.0', '<=3.1.9'], ], ], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); $advisory2 = clone $advisory1; $advisory3 = Advisory::fromArrayData([ @@ -179,6 +188,7 @@ public function testSortAdvisoriesWithRealCase(): void 'versions' => ['>=3.1.0', '<3.1.11'], ], ], + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); $component = new Component(PackageName::fromName('foo/bar'), $advisory1, $advisory2, $advisory3); @@ -203,11 +213,13 @@ public function testSortComplexAdvisoriesWithRealCase( $advisory1 = Advisory::fromArrayData([ 'reference' => $reference, 'branches' => $advisory1Branches, + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); $advisory2 = clone $advisory1; $advisory3 = Advisory::fromArrayData([ 'reference' => $reference, 'branches' => $advisory2Branches, + 'source' => ['summary' => 'some vulnerability', 'link' => 'https://example.com'], ]); $component = new Component(PackageName::fromName('foo/bar'), $advisory1, $advisory2, $advisory3); diff --git a/test/RoaveTest/SecurityAdvisories/Helper/ConstraintsMapTest.php b/test/RoaveTest/SecurityAdvisories/Helper/ConstraintsMapTest.php new file mode 100644 index 00000000..045f0c50 --- /dev/null +++ b/test/RoaveTest/SecurityAdvisories/Helper/ConstraintsMapTest.php @@ -0,0 +1,228 @@ +> $data + * @param array $incomingAdvisories + * + * @dataProvider newAdvisoriesDataProvider + */ + public function testAdvisoriesDiffDetectsUpdatedAndNewAdvisory( + array $data, + array $incomingAdvisories, + string $expectedAdvisoryConstraint + ): void { + $map = ConstraintsMap::fromArray($data['conflict']); + $result = $map->advisoriesDiff($incomingAdvisories); + + self::assertCount(1, $result); + self::assertEquals($expectedAdvisoryConstraint, $result[0]->getConstraint()); + } + + /** + * @param array> $data + * @param array $incomingAdvisories + * + * @dataProvider sameAdvisoriesDataProvider + */ + public function testSameAdvisoriesAreFilteredOut( + array $data, + array $incomingAdvisories, + ): void { + $map = ConstraintsMap::fromArray($data['conflict']); + $result = $map->advisoriesDiff($incomingAdvisories); + + self::assertCount(0, $result); + } + + /** + * @return array + */ + public function newAdvisoriesDataProvider(): array + { + return [ + 'existing package but with new version added' => [ + [ + 'conflict' => ['foo/bar' => '>=4,<4.4.56|>=4.5,<4.9.18|>=4.10,<4.11.7|>=4.13,<4.13.3'], + ], + [ + Advisory::fromArrayData([ + 'branches' => [ + ['versions' => ['>5']], + ], + 'reference' => 'composer://foo/bar', + 'source' => ['summary' => 'summary', 'link' => 'link'], + ]), + ], + '>5', + ], + 'new package ' => [ + [ + 'conflict' => ['foo/bar' => '>=4,<4.4.56|>=4.5,<4.9.18|>=4.10,<4.11.7|>=4.13,<4.13.3'], + ], + [ + Advisory::fromArrayData([ + 'branches' => [ + ['versions' => ['>1']], + ], + 'reference' => 'composer://test/example', + 'source' => ['summary' => 'summary', 'link' => 'link'], + ]), + ], + '>1', + ], + 'advisory with expanded range' => [ + [ + 'conflict' => ['foo/bar' => '>=4,<4.4.56|>=4.5,<4.9.18|>=4.10,<4.11.7|>=4.13,<4.13.3'], + ], + [ + Advisory::fromArrayData([ + 'branches' => [ + [ + 'versions' => + ['>=4.13', '<4.13.4'], + ], + ], + 'reference' => 'composer://foo/bar', + 'source' => ['summary' => 'summary', 'link' => 'link'], + ]), + ], + '>=4.13,<4.13.4', + ], + 'existing conflict updated with new range' => [ + [ + 'conflict' => ['foo/bar' => '>=4,<4.4.56|>=4.5,<4.9.18|>=4.10,<4.11.7|>=4.13,<4.13.3'], + ], + [ + Advisory::fromArrayData([ + 'branches' => [ + [ + 'versions' => + ['>=4.13', '<4.13.3'], + ], + [ + 'versions' => + ['>6'], + ], + ], + 'reference' => 'composer://foo/bar', + 'source' => ['summary' => 'summary', 'link' => 'link'], + ]), + ], + '>=4.13,<4.13.3|>6', + ], + ]; + } + + /** + * @return array + */ + public function sameAdvisoriesDataProvider(): array + { + return [ + 'single range equals to already existing range' => [ + [ + 'conflict' => ['foo/bar' => '>=4,<4.4.56|>=4.5,<4.9.18|>=4.10,<4.11.7|>=4.13,<4.13.3'], + ], + [ + Advisory::fromArrayData([ + 'branches' => [ + ['versions' => ['>=4,<4.4.56']], + ], + 'reference' => 'composer://foo/bar', + 'source' => ['summary' => 'summary', 'link' => 'link'], + ]), + ], + ], + 'all ranges are fully included' => [ + [ + 'conflict' => ['foo/bar' => '>=4,<4.4.56|>=4.5,<4.9.18|>=4.10,<4.11.7|>=4.13,<4.13.3'], + ], + [ + Advisory::fromArrayData([ + 'branches' => [ + ['versions' => ['>=4,<4.4.56']], + ['versions' => ['>=4.5,<4.9.18']], + ['versions' => ['>=4.10,<4.11.7']], + ['versions' => ['>=4.13,<4.13.3']], + ], + 'reference' => 'composer://foo/bar', + 'source' => ['summary' => 'summary', 'link' => 'link'], + ]), + ], + ], + 'smaller single range fully included' => [ + [ + 'conflict' => ['foo/bar' => '>=4,<4.4.56|>=4.5,<4.9.18|>=4.10,<4.11.7|>=4.13,<4.13.3'], + ], + [ + Advisory::fromArrayData([ + 'branches' => [ + ['versions' => ['>4.1,<4.2']], + ], + 'reference' => 'composer://foo/bar', + 'source' => ['summary' => 'summary', 'link' => 'link'], + ]), + ], + ], + 'complex range with stability flags' => [ + [ + 'conflict' => ['snipe/snipe-it' => '<5.4.3|>=6.0.0-RC-1,<=6.0.0-RC-5'], + ], + [ + Advisory::fromArrayData([ + 'branches' => [ + ['versions' => ['>=6.0.0-RC-1, <=6.0.0-RC-5']], + ], + 'reference' => 'composer://snipe/snipe-it', + 'source' => ['summary' => 'summary', 'link' => 'link'], + ]), + ], + ], + 'a simple ranges and fixed version advisories' => [ + [ + 'conflict' => ['october/cms' => '=1.1.1|=1.0.471|=1.0.469|>=1.0.319,<1.0.469'], + ], + [ + Advisory::fromArrayData([ + 'branches' => [ + ['versions' => ['=1.0.471']], + ], + 'reference' => 'composer://october/cms', + 'source' => ['summary' => 'summary', 'link' => 'link'], + ]), + ], + ], + ]; + } +} diff --git a/test/RoaveTest/SecurityAdvisories/Rule/RuleProviderFactoryTest.php b/test/RoaveTest/SecurityAdvisories/Rule/RuleProviderFactoryTest.php index 75bf0bd0..d5c8dba5 100644 --- a/test/RoaveTest/SecurityAdvisories/Rule/RuleProviderFactoryTest.php +++ b/test/RoaveTest/SecurityAdvisories/Rule/RuleProviderFactoryTest.php @@ -56,6 +56,7 @@ public function testProviderProvidesRuleIsApplied(): void $config['branches'] = [ ['versions' => ['<2.17.2']], ]; + $config['source'] = ['summary' => 'summary', 'link' => 'link']; $advisory = Advisory::fromArrayData($config); @@ -87,6 +88,7 @@ public function testProviderProvidesRuleNotAppliedBecauseOfPackageName(): void 'versions' => ['<2.17.2'], ], ]; + $config['source'] = ['summary' => 'summary', 'link' => 'link']; $advisory = Advisory::fromArrayData($config); @@ -118,6 +120,7 @@ public function testProviderProvidesRuleNotAppliedBecauseOfUnexpectedConstraint( 'versions' => ['>3.2'], ], ]; + $config['source'] = ['summary' => 'summary', 'link' => 'link']; $advisory = Advisory::fromArrayData($config); diff --git a/test/RoaveTest/SecurityAdvisories/VersionConstraintTest.php b/test/RoaveTest/SecurityAdvisories/VersionConstraintTest.php index 105c5b12..79b2da19 100644 --- a/test/RoaveTest/SecurityAdvisories/VersionConstraintTest.php +++ b/test/RoaveTest/SecurityAdvisories/VersionConstraintTest.php @@ -280,7 +280,7 @@ public function complexRangesProvider(): array ['>1a2b3,<4c5d6'], ['>1-a.2'], ['<1-a.2'], - ['<1-a.2, >1-p.1.2'], + ['<1-a.2,>1-p.1.2'], ['1-beta.2.0|1-rc.1.2.3'], ]; @@ -519,6 +519,7 @@ static function (array $entry) { public function normalizableRangesProvider(): array { $samples = [ + ['= 1.1.1', '=1.1.1'], ['>1.0,<2.0', '>1,<2'], ['>=1.0,<2.0', '>=1,<2'], ['>1.0,<=2.0', '>1,<=2'], @@ -536,6 +537,8 @@ public function normalizableRangesProvider(): array ['<1.0', '<1'], ['<=1.0', '<=1'], ['<=1.0.3.0.5.0-beta.0.5.0.0', '<=1.0.3.0.5-beta.0.5'], + ['>= 6.0.0-RC-1', '>=6.0.0-RC-1'], + ['<= 6.0.0-RC-5', '<=6.0.0-RC-5'], ]; return Dict\associate(