Skip to content

Commit

Permalink
Add audit command to check for security issues
Browse files Browse the repository at this point in the history
  • Loading branch information
GuySartorelli committed May 26, 2022
1 parent f1f013e commit 451632d
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 0 deletions.
15 changes: 15 additions & 0 deletions doc/03-cli.md
Expand Up @@ -955,6 +955,21 @@ php composer.phar archive vendor/package 2.0.21 --format=zip
* **--dir:** Write the archive to this directory (default: ".")
* **--file:** Write the archive with the given file name.
## audit
This command is used to audit the packages in your composer.lock
for possible security issues. Currently this only checks for and
lists security vulnerability advisories according to the
[packagist api](https://packagist.org/apidoc#list-security-advisories).
```sh
php composer.phar audit
```
### Options
* **--no-dev:** Disables auditing of require-dev packages.
## help
To get more information about a certain command, you can use `help`.
Expand Down
61 changes: 61 additions & 0 deletions src/Composer/Command/AuditCommand.php
@@ -0,0 +1,61 @@
<?php

namespace Composer\Command;

use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Composer\Factory;
use Composer\Util\Auditor;
use Composer\Util\Filesystem;
use Symfony\Component\Console\Input\InputOption;

class AuditCommand extends InitCommand
{

/**
* @return void
*/
protected function configure()
{
$this
->setName('audit')
->setDescription('Checks for security vulnerability advisories for packages in your composer.lock.')
->setDefinition(array(
new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables auditing of require-dev packages.'),
))
->setHelp(
<<<EOT
The <info>audit</info> command checks for security vulnerability advisories for packages in your composer.lock.
If you do not want to include dev dependencies in the audit you can omit them with --no-dev
Read more at https://getcomposer.org/doc/03-cli.md#audit
EOT
)
;
}

/**
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$lockFile = Factory::getLockFile(Factory::getComposerFile());
if (!Filesystem::isReadable($lockFile)) {
$this->getIO()->writeError('<error>' . $lockFile . ' is not readable.</error>');
return 1;
}

$composer = $this->getComposer(true, $input->getOption('no-plugins'));
$locker = $composer->getLocker();
$packages = $locker->getLockedRepository(!$input->getOption('no-dev'))->getPackages();
$httpDownloader = Factory::createHttpDownloader($this->getIO(), $composer->getConfig());

return Auditor::audit($this->getIO(), $packages, $httpDownloader);
}

protected function interact(InputInterface $input, OutputInterface $output)
{
return;
}
}
1 change: 1 addition & 0 deletions src/Composer/Console/Application.php
Expand Up @@ -528,6 +528,7 @@ protected function getDefaultCommands(): array
new Command\UpdateCommand(),
new Command\SearchCommand(),
new Command\ValidateCommand(),
new Command\AuditCommand(),
new Command\ShowCommand(),
new Command\SuggestsCommand(),
new Command\RequireCommand(),
Expand Down
87 changes: 87 additions & 0 deletions src/Composer/Util/Auditor.php
@@ -0,0 +1,87 @@
<?php

namespace Composer\Util;

use Composer\IO\IOInterface;
use Composer\Package\BasePackage;
use Composer\Semver\Semver;

class Auditor
{
public const API_URL = 'https://packagist.org/api/security-advisories';

/**
* @param BasePackage[] $packages
* @param HttpDownloader $httpDownloader
* @return int
*/
public static function audit(IOInterface $io, array $packages, HttpDownloader $httpDownloader): int
{
$allAdvisories = static::getAdvisories($packages, $httpDownloader);
if (!empty($allAdvisories)) {
$error = ['<warning>Found the following security vulnerability advisories:</warning>'];
foreach ($allAdvisories as $package => $advisories) {
$error[] = "$package:";
foreach ($advisories as $advisory) {
$cve = $advisory['cve'] ?: 'NO CVE';
$error[] = "\t{$cve}";
$error[] = "\t\tTitle: {$advisory['title']}";
$error[] = "\t\tURL: {$advisory['link']}";
$error[] = "\t\tAffected versions: {$advisory['affectedVersions']}";
$error[] = "\t\tReported on: {$advisory['reportedAt']}";
}
}
$io->writeError($error);
return 1;
}
$io->writeError('<info>No issues found</info>');
return 0;
}

/**
* Get advisories from packagist.org that match the package versions
*
* @param BasePackage[] $packages
* @param HttpDownloader $httpDownloader
* @return array
*/
public static function getAdvisories(array $packages, HttpDownloader $httpDownloader): array
{
// Get advisories for the given packages
$getVar = [];
$packageNames = [];
foreach ($packages as $package) {
// $getVar[] = urlencode($package->getName());
$packageNames[] = $package->getName();
}
$url = static::API_URL;// . '?packages[]=' . join('&packages[]=', $getVar);
$response = $httpDownloader->get($url, static::getPostOptions($packageNames));
// TODO check response is okay.
$advisories = $response->decodeJson()['advisories'];
$relevantAdvisories = [];
foreach ($packages as $package) {
if (array_key_exists($package->getName(), $advisories)) {
foreach ($advisories[$package->getName()] as $advisory) {
if (Semver::satisfies($package->getVersion(), $advisory['affectedVersions'])) {
$relevantAdvisories[$package->getName()][] = $advisory;
}
}
}
}

return $relevantAdvisories;
}

private static function getPostOptions(array $packageNames): array
{
return [
'retry-auth-failure' => false,
'http' => [
'method' => 'POST',
'header' => array('Content-Type: application/json'),//array('Content-type: application/x-www-form-urlencoded'),
'content' => json_encode(['packages' => $packageNames]),
// 'timeout' => 3,
],
];
}
}
50 changes: 50 additions & 0 deletions tests/Composer/Test/Util/AuditorTest.php
@@ -0,0 +1,50 @@
<?php

namespace Composer\Test\Util;

use Composer\IO\IOInterface;
use Composer\Test\TestCase;

class AuditorTest extends TestCase
{
/** @var \Composer\IO\IOInterface&\PHPUnit\Framework\MockObject\MockObject */
private $io;

protected function setUp()
{
$this->io = $this
->getMockBuilder('Composer\IO\IOInterface')
->disableOriginalConstructor()
->getMock();
}

public function auditProvider()
{
return array(
array(
'some value',
'some other value',
),
array(
'some value',
'some other value',
),
);
}

/**
* @dataProvider auditProvider
*/
public function testAudit(string $value1, string $value2)
{
Auditor::audit($this->io, $packages, $this->httpProvider);
}

/**
* @dataProvider auditProvider
*/
public function testGetAdvisories(string $value1, string $value2)
{
Auditor::getAdvisories($packages, $this->httpProvider);
}
}

0 comments on commit 451632d

Please sign in to comment.