Skip to content
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

Issues/451 Add advisory information to a commit messages #459

Open
wants to merge 25 commits into
base: latest
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 44 additions & 7 deletions build-conflicts.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use DateTime;
use DateTimeZone;
use ErrorException;
use Exception;
use Http\Client\Curl\Client;
use Psl\Dict;
use Psl\Env;
Expand All @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
use function file_get_contents;
use Psl\Filesystem;

$content = Filesystem\read_file($filename);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you!

use function iterator_to_array;
use function set_error_handler;

use const E_NOTICE;
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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<Advisory> $addedAdvisories
*
* @return void
*
* @throws Exception
*/
$commitComposerJson = static function (string $composerJsonPath, array $addedAdvisories): void {
$originalHash = Shell\execute(
'git',
['rev-parse', 'HEAD'],
Expand All @@ -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) {
Expand Down Expand Up @@ -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<string, array<string, string>> $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);
})();
15 changes: 11 additions & 4 deletions src/Roave/SecurityAdvisories/Advisory.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,23 @@ final class Advisory
/** @var list<VersionConstraint> */
private array $branchConstraints;

/** @param list<VersionConstraint> $branchConstraints */
private function __construct(PackageName $package, array $branchConstraints)
public Source $source;

/**
* @param list<VersionConstraint> $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<array-key, array{versions: string|array<array-key, string>}>,
* reference: string
* reference: string,
* source: array{summary: string, link:string},
* } $config
*
* @return Advisory
Expand All @@ -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']),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ final class GetAdvisoriesFromGithubApi implements GetAdvisories
}
advisory {
withdrawnAt
ghsaId
permalink
summary
}
}
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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) {
Expand All @@ -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,
* }
* }
* }>
*
Expand All @@ -126,16 +135,20 @@ 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([
'cursor' => Type\string(),
'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([
Expand All @@ -145,6 +158,7 @@ private function getAdvisories(): iterable
]),
]),
]));

$vulnerabilities = $data['data']['securityVulnerabilities'];

yield from $vulnerabilities['edges'];
Expand Down
98 changes: 98 additions & 0 deletions src/Roave/SecurityAdvisories/Helper/ConstraintsMap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

namespace Roave\SecurityAdvisories\Helper;

use Roave\SecurityAdvisories\Advisory;
use Roave\SecurityAdvisories\VersionConstraint;

use function array_key_exists;
use function Psl\Str\split;

final class ConstraintsMap
{
/** @var array<string, array<VersionConstraint>> $map */
private array $map;

/**
* @param array<string, array<VersionConstraint>> $conflicts
*/
private function __construct(array $conflicts)
{
$this->map = $conflicts;
}

/**
* @param array<string, string> $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<Advisory> $advisoriesToFilter
*
* @return array<Advisory>
*/
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;
}
}
2 changes: 0 additions & 2 deletions src/Roave/SecurityAdvisories/Matchers.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/Roave/SecurityAdvisories/Rule/RuleProviderFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ static function (Advisory $advisory): Advisory {
'branches' => [
['versions' => ['<2.17.1']],
],
'source' => ['summary' => 'summary', 'link' => 'link'],
]);
},
];
Expand Down
32 changes: 32 additions & 0 deletions src/Roave/SecurityAdvisories/Source.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Roave\SecurityAdvisories;

// value object
use Psl\Type;

final class Source
{
/** @var non-empty-string $summary */
public string $summary;

/** @var non-empty-string $uri */
public string $uri;

/**
* @param non-empty-string $summary
* @param non-empty-string $uri
*/
private function __construct(string $summary, string $uri)
{
$this->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));
}
}
Loading