|  | 
|  | 1 | +<?php | 
|  | 2 | + | 
|  | 3 | +/** | 
|  | 4 | + * Script for exporting advisories to OSV format. | 
|  | 5 | + * | 
|  | 6 | + * Usage: `php export-osv.php target_folder` | 
|  | 7 | + * | 
|  | 8 | + * @see https://ossf.github.io/osv-schema/ | 
|  | 9 | + */ | 
|  | 10 | + | 
|  | 11 | +namespace FriendsOfPhp\SecurityAdvisories; | 
|  | 12 | + | 
|  | 13 | +use DirectoryIterator; | 
|  | 14 | +use FilesystemIterator; | 
|  | 15 | +use SplFileInfo; | 
|  | 16 | +use Symfony\Component\Yaml\Yaml; | 
|  | 17 | + | 
|  | 18 | +if (!is_file($autoloader = __DIR__ . '/vendor/autoload.php')) { | 
|  | 19 | +    echo 'Dependencies are not installed, please run "composer install" first!' . PHP_EOL; | 
|  | 20 | +    exit(1); | 
|  | 21 | +} | 
|  | 22 | + | 
|  | 23 | +require $autoloader; | 
|  | 24 | + | 
|  | 25 | +function convertToOsv(SplFileInfo $fileInfo, string $package): array | 
|  | 26 | +{ | 
|  | 27 | +    $advisory = Yaml::parseFile($fileInfo->getPathname()); | 
|  | 28 | + | 
|  | 29 | +    return [ | 
|  | 30 | +        'id' => $advisory['cve'] ?? 'PHPSEC-' . $fileInfo->getBasename('.yaml'), | 
|  | 31 | +        'modified' => getDateFromGitLog($fileInfo), | 
|  | 32 | +        'published' => getDateFromGitLog($fileInfo, true), | 
|  | 33 | +        'aliases' => [], | 
|  | 34 | +        'related' => [], | 
|  | 35 | +        'summary' => $advisory['title'] ?? '', | 
|  | 36 | +        'details' => '', | 
|  | 37 | +        'affected' => [ | 
|  | 38 | +            'package' => [ | 
|  | 39 | +                'ecosystem' => 'Packagist', | 
|  | 40 | +                'name' => $package, | 
|  | 41 | +                'purl' => sprintf('pkg:packagist/%s', $package), | 
|  | 42 | +            ], | 
|  | 43 | +            'ranges' => [ | 
|  | 44 | +                'type' => 'SEMVER', | 
|  | 45 | +                'events' => getEvents($advisory['branches']), | 
|  | 46 | +            ], | 
|  | 47 | +        ], | 
|  | 48 | +        'references' => [ | 
|  | 49 | +            array_key_exists('link', $advisory) ? [ | 
|  | 50 | +                'type' => 'ADVISORY', | 
|  | 51 | +                'url' => $advisory['link'], | 
|  | 52 | +            ] : null, | 
|  | 53 | +            [ | 
|  | 54 | +                'type' => 'PACKAGE', | 
|  | 55 | +                'url' => 'https://packagist.org/packages/' . $package, | 
|  | 56 | +            ], | 
|  | 57 | +        ], | 
|  | 58 | +    ]; | 
|  | 59 | +} | 
|  | 60 | + | 
|  | 61 | +function getEvents(array $branches): array | 
|  | 62 | +{ | 
|  | 63 | +    $events = []; | 
|  | 64 | + | 
|  | 65 | +    foreach (array_column($branches, 'versions') as $branch) { | 
|  | 66 | +        if (count($branch) === 2) { | 
|  | 67 | +            array_push($events, ['introduced' => $branch[0]]); | 
|  | 68 | +            array_push($events, ['fixed' => $branch[1]]); | 
|  | 69 | +        } else { | 
|  | 70 | +            array_push($events, ['fixed' => $branch[0]]); | 
|  | 71 | +        } | 
|  | 72 | +    } | 
|  | 73 | + | 
|  | 74 | +    return $events; | 
|  | 75 | +} | 
|  | 76 | + | 
|  | 77 | +function getDateFromGitLog(SplFileInfo $fileInfo, bool $created = false): string | 
|  | 78 | +{ | 
|  | 79 | +    $timestamp = shell_exec(sprintf( | 
|  | 80 | +        'git log --format="%%at" %s %s %s %s', | 
|  | 81 | +        $created ? '' : '--max-count 1', | 
|  | 82 | +        $created ? '--reverse' : '', | 
|  | 83 | +        escapeshellarg($fileInfo->getPathname()), | 
|  | 84 | +        $created ? '| head -1' : '' | 
|  | 85 | +    )); | 
|  | 86 | + | 
|  | 87 | +    return date('Y-m-d\TH:i:s\Z', (int) trim($timestamp)); | 
|  | 88 | +} | 
|  | 89 | + | 
|  | 90 | +mkdir($targetFolder = $argv[1] ?? 'packagist'); | 
|  | 91 | + | 
|  | 92 | +$namespaceIterator = new DirectoryIterator(__DIR__); | 
|  | 93 | + | 
|  | 94 | +// Package namespaces | 
|  | 95 | +foreach ($namespaceIterator as $namespaceInfo) { | 
|  | 96 | +    if ($namespaceInfo->isDot() || !$namespaceInfo->isDir() || $namespaceInfo->getFilename() === 'vendor' || strpos($namespaceInfo->getFilename() , '.') === 0) continue; | 
|  | 97 | + | 
|  | 98 | +    $namespace = $namespaceInfo->getFilename(); | 
|  | 99 | +    $packageIterator = new DirectoryIterator($namespaceInfo->getPathname()); | 
|  | 100 | + | 
|  | 101 | +    // Packages inside namespace | 
|  | 102 | +    foreach ($packageIterator as $packageInfo) { | 
|  | 103 | +        if ($packageIterator->isDot() || !$packageInfo->isDir()) continue; | 
|  | 104 | + | 
|  | 105 | +        $package = $packageInfo->getFilename(); | 
|  | 106 | +        $fileSystemIterator = new FilesystemIterator($packageInfo->getPathname()); | 
|  | 107 | + | 
|  | 108 | +        echo 'Converting "' . $namespace . '/' . $package . '" ...' . str_repeat(' ', 20) . "\r"; | 
|  | 109 | + | 
|  | 110 | +        foreach ($fileSystemIterator as $fileInfo) { | 
|  | 111 | +            $osv = convertToOsv($fileInfo, $namespace . '/' . $package); | 
|  | 112 | +            $path = $targetFolder . DIRECTORY_SEPARATOR . $osv['id'] . '.json'; | 
|  | 113 | + | 
|  | 114 | +            file_put_contents($path, json_encode($osv, JSON_PRETTY_PRINT)); | 
|  | 115 | +        } | 
|  | 116 | +    } | 
|  | 117 | +} | 
|  | 118 | + | 
|  | 119 | +echo PHP_EOL; | 
0 commit comments