Skip to content

Commit

Permalink
Export advisories in OSV format
Browse files Browse the repository at this point in the history
Fixes #576
  • Loading branch information
jaylinski committed Nov 18, 2021
1 parent 486a92e commit 1b49ea4
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 18 deletions.
42 changes: 42 additions & 0 deletions .github/workflows/export-osv.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Export to OSV format

on:
push:
branches:
- export-osv

jobs:
publish-web:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
with:
# Required in order to extract dates from commit history
fetch-depth: 0

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.0"
coverage: none
tools: composer

- name: Install dependencies
run: composer install --prefer-dist --no-progress

- name: Export to OSV format
run: |
git config user.name github-actions
git config user.email github-actions@github.com
php export-osv.php export
git add packagist
git stash
git checkout osv
echo `date` > published
git add published
git rm -r --ignore-unmatch packagist
git commit -m "Update OSV data export"
git stash pop
git commit --amend --no-edit --allow-empty
git push
36 changes: 18 additions & 18 deletions .github/workflows/php.yaml
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
name: Validation

on:
push:
pull_request:
push:
pull_request:

jobs:
run:
runs-on: ubuntu-latest
run:
runs-on: ubuntu-latest

name: Validation
steps:
- name: Checkout
uses: actions/checkout@v2
name: Validation
steps:
- name: Checkout
uses: actions/checkout@v2

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.0"
coverage: none
tools: composer
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.0"
coverage: none
tools: composer

- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Install dependencies
run: composer install --prefer-dist --no-progress

- name: Run tests
run: php -d memory_limit=-1 validator.php
- name: Run tests
run: php -d memory_limit=-1 validator.php
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ not** serve as the primary source of information for security issues, it is
not authoritative for any referenced software, but it allows to centralize
information for convenience and easy consumption.

We also export advisory data to the [OSV](https://github.com/ossf/osv-schema) format,
see the [`osv`](https://github.com/FriendsOfPHP/security-advisories/tree/osv) branch.

License
-------

Expand Down
171 changes: 171 additions & 0 deletions export-osv.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?php

/**
* Script for exporting advisories to OSV format.
*
* Usage: `php export-osv.php export target_folder`
*
* @see https://ossf.github.io/osv-schema/
*/

namespace FriendsOfPhp\SecurityAdvisories;

use DirectoryIterator;
use FilesystemIterator;
use SplFileInfo;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Yaml\Yaml;

if (!is_file($autoloader = __DIR__ . '/vendor/autoload.php')) {
echo 'Dependencies are not installed, please run "composer install" first!' . PHP_EOL;
exit(1);
}

require $autoloader;

final class ExportOsv extends Command
{
private const OSV_ECOSYSTEM = 'Packagist';
private const OSV_PACKAGE_URL = 'https://packagist.org/packages/';
private const OSV_PREFIX = 'PHPSEC';

protected function configure(): void
{
$this
->setName('export')
->setDescription('Export advisories in OSV format')
->addArgument('target',InputArgument::OPTIONAL, 'Target folder', 'packagist');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
mkdir($targetFolder = $input->getArgument('target'));

$namespaceIterator = new DirectoryIterator(__DIR__);

// Package namespaces
foreach ($namespaceIterator as $namespaceInfo) {
if ($namespaceInfo->isDot() || !$namespaceInfo->isDir() || $namespaceInfo->getFilename() === 'vendor' || strpos($namespaceInfo->getFilename() , '.') === 0) continue;

$namespace = $namespaceInfo->getFilename();
$packageIterator = new DirectoryIterator($namespaceInfo->getPathname());

// Packages inside namespace
foreach ($packageIterator as $packageInfo) {
if ($packageIterator->isDot() || !$packageInfo->isDir()) continue;

$package = $packageInfo->getFilename();
$fileSystemIterator = new FilesystemIterator($packageInfo->getPathname());

$output->write('Converting "' . $namespace . '/' . $package . '" ...' . str_repeat(' ', 20) . "\r");

foreach ($fileSystemIterator as $fileInfo) {
$osv = self::convertToOsv($fileInfo, $namespace . '/' . $package);

if (is_null($osv)) {
$output->writeln('Skipped "' . $namespace . '/' . $package . '/' . $fileInfo->getFilename());
continue;
}

$path = $targetFolder . DIRECTORY_SEPARATOR . $osv['id'] . '.json';

file_put_contents($path, json_encode($osv, JSON_PRETTY_PRINT));
}
}
}

$output->writeln('');

// Command::SUCCESS and Command::FAILURE constants were introduced in Symfony 5.1
return 0;
}

private function convertToOsv(SplFileInfo $fileInfo, string $package): ?array
{
$advisory = Yaml::parseFile($fileInfo->getPathname());

// Advisories with custom repositories are currently not supported
if (isset($advisory['composer-repository'])) {
return null;
}

return [
'id' => $advisory['cve'] ?? self::OSV_PREFIX . '-' . $fileInfo->getBasename('.yaml'),
'modified' => self::getDateFromGitLog($fileInfo),
'published' => self::getDateFromGitLog($fileInfo, true),
'aliases' => [],
'related' => [],
'summary' => $advisory['title'] ?? '',
'details' => '',
'affected' => self::getAffected($advisory, $package),
'references' => self::getReferences($advisory, $package),
];
}

private static function getAffected(array $advisory, string $package): array
{
return [
'package' => [
'ecosystem' => self::OSV_ECOSYSTEM,
'name' => $package,
'purl' => sprintf('pkg:packagist/%s', $package),
],
'ranges' => [
'type' => 'SEMVER',
'events' => self::getEvents($advisory['branches']),
],
];
}

private static function getDateFromGitLog(SplFileInfo $fileInfo, bool $created = false): string
{
$timestamp = shell_exec(sprintf(
'git log --format="%%at" %s %s %s %s',
$created ? '' : '--max-count 1',
$created ? '--reverse' : '',
escapeshellarg($fileInfo->getPathname()),
$created ? '| head -1' : ''
));

return date('Y-m-d\TH:i:s\Z', (int) trim($timestamp));
}

private static function getEvents(array $branches): array
{
$events = [];

foreach (array_column($branches, 'versions') as $branch) {
if (count($branch) === 2) {
array_push($events, ['introduced' => $branch[0]]); // TODO Parse Semver and fetch version
array_push($events, ['fixed' => $branch[1]]); // TODO Parse Semver and fetch version
} else {
array_push($events, ['introduced' => '0']);
array_push($events, ['fixed' => $branch[0]]); // TODO Parse Semver and fetch version
}
}

return $events;
}

private static function getReferences(array $advisory, string $package): array
{
return [
[
'type' => 'PACKAGE',
'url' => self::OSV_PACKAGE_URL . $package,
],
[
'type' => 'ADVISORY',
'url' => $advisory['link'],
],
];
}
}

$application = new Application();
$application->add(new ExportOsv());
$application->run();

0 comments on commit 1b49ea4

Please sign in to comment.