Skip to content

Commit

Permalink
Merge pull request #914 from Zombaya/roave-security-checker
Browse files Browse the repository at this point in the history
Add security-checker using roave/security-advisories
  • Loading branch information
veewee authored Jul 14, 2021
2 parents d823e51 + d03c89c commit d100d05
Show file tree
Hide file tree
Showing 5 changed files with 353 additions and 0 deletions.
2 changes: 2 additions & 0 deletions doc/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ grumphp:
robo: ~
securitychecker_enlightn: ~
securitychecker_local: ~
securitychecker_roave: ~
securitychecker_symfony: ~
shell: ~
stylelint: ~
Expand Down Expand Up @@ -117,6 +118,7 @@ Every task has it's own default configuration. It is possible to overwrite the p
- [Security Checker](tasks/securitychecker.md)
- [Enlightn](tasks/securitychecker/enlightn.md)
- [Local](tasks/securitychecker/local.md)
- [Roave](tasks/securitychecker/roave.md)
- [Symfony](tasks/securitychecker/symfony.md)
- [Shell](tasks/shell.md)
- [Stylelint](tasks/stylelint.md)
Expand Down
42 changes: 42 additions & 0 deletions doc/tasks/securitychecker/roave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Roave Security Checker

The Security Checker will check your `composer.lock` file for known security vulnerabilities.

***Composer***

```
composer require --dev roave/security-advisories:lastest-dev
```
More information about the library can be found on [github](https://github.com/Roave/SecurityAdvisories).

***Config***

The task lives under the `securitychecker_roave` namespace and has the following configurable parameters:

```yaml
# grumphp.yml
grumphp:
tasks:
securitychecker_roave:
jsonfile: ./composer.json
lockfile: ./composer.lock
run_always: false
```
**jsonfile**
*Default: ./composer.json*
If your `composer.json` file is located in an exotic location, you can specify the location with this option. By default, the task will try to load a `composer.json` file in the current directory.

**lockfile**

*Default: ./composer.lock*

If your `composer.lock` file is located in an exotic location, you can specify the location with this option. By default, the task will try to load a `composer.lock` file in the current directory.

**run_always**

*Default: false*

When this option is set to `false`, the task will only run when the `composer.lock` file has changed. If it is set to `true`, the `composer.lock` file will be checked on every commit.
8 changes: 8 additions & 0 deletions resources/config/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,14 @@ services:
tags:
- {name: grumphp.task, task: securitychecker_local}

GrumPHP\Task\SecurityCheckerRoave:
arguments:
- '@process_builder'
- '@formatter.raw_process'
- '@grumphp.util.filesystem'
tags:
- {name: grumphp.task, task: securitychecker_roave}

GrumPHP\Task\SecurityCheckerSymfony:
arguments:
- '@process_builder'
Expand Down
114 changes: 114 additions & 0 deletions src/Task/SecurityCheckerRoave.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

declare(strict_types=1);

namespace GrumPHP\Task;

use GrumPHP\Formatter\ProcessFormatterInterface;
use GrumPHP\Process\ProcessBuilder;
use GrumPHP\Runner\TaskResult;
use GrumPHP\Runner\TaskResultInterface;
use GrumPHP\Task\Context\ContextInterface;
use GrumPHP\Task\Context\GitPreCommitContext;
use GrumPHP\Task\Context\RunContext;
use GrumPHP\Util\Filesystem;
use Symfony\Component\OptionsResolver\OptionsResolver;

class SecurityCheckerRoave extends AbstractExternalTask
{
/** @var Filesystem */
private $filesystem;

public function __construct(
ProcessBuilder $processBuilder,
ProcessFormatterInterface $formatter,
Filesystem $filesystem
) {
parent::__construct($processBuilder, $formatter);
$this->filesystem = $filesystem;
}

public static function getConfigurableOptions(): OptionsResolver
{
$resolver = new OptionsResolver();
$resolver->setDefaults([
'jsonfile' => './composer.json',
'lockfile' => './composer.lock',
'run_always' => false,
]);

$resolver->addAllowedTypes('jsonfile', ['string']);
$resolver->addAllowedTypes('lockfile', ['string']);
$resolver->addAllowedTypes('run_always', ['bool']);

return $resolver;
}

public function canRunInContext(ContextInterface $context): bool
{
return $context instanceof GitPreCommitContext || $context instanceof RunContext;
}

public function run(ContextInterface $context): TaskResultInterface
{

$config = $this->getConfig()->getOptions();
$composerFile = $config['jsonfile'];

if (!$this->filesystem->isFile($composerFile)) {
return TaskResult::createSkipped($this, $context);
}

if (!$this->hasRoaveSecurityAdvisoriesInstalled($composerFile)) {
return TaskResult::createFailed(
$this,
$context,
'This task is only available when roave/security-advisories is installed as a library.'
);
}

$config = $this->getConfig()->getOptions();
$files = $context->getFiles()
->path(pathinfo($config['lockfile'], PATHINFO_DIRNAME))
->name(pathinfo($config['lockfile'], PATHINFO_BASENAME));
if (0 === \count($files) && !$config['run_always']) {
return TaskResult::createSkipped($this, $context);
}

$arguments = $this->processBuilder->createArgumentsForCommand('composer');
$arguments->add('update');
$arguments->add('--dry-run');
$arguments->add('roave/security-advisories');

$process = $this->processBuilder->buildProcess($arguments);
$process->run();

if (!$process->isSuccessful()) {
return TaskResult::createFailed($this, $context, $this->formatter->format($process));
}

return TaskResult::createPassed($this, $context);
}

private function hasRoaveSecurityAdvisoriesInstalled(string $composerFile): bool
{
$json = $this->filesystem->readPath($composerFile);
try {
$package = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
return false;
}

if (array_key_exists('require', $package)
&& array_key_exists('roave/security-advisories', $package['require'])) {
return true;
}

if (array_key_exists('require-dev', $package)
&& array_key_exists('roave/security-advisories', $package['require-dev'])) {
return true;
}

return false;
}
}
187 changes: 187 additions & 0 deletions test/Unit/Task/SecurityCheckerRoaveTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<?php

declare(strict_types=1);

namespace GrumPHPTest\Unit\Task;

use GrumPHP\Task\Context\ContextInterface;
use GrumPHP\Task\Context\GitPreCommitContext;
use GrumPHP\Task\Context\RunContext;
use GrumPHP\Task\SecurityCheckerRoave;
use GrumPHP\Task\TaskInterface;
use GrumPHP\Test\Task\AbstractExternalTaskTestCase;
use GrumPHP\Util\Filesystem;
use Prophecy\Argument;
use Symfony\Component\Process\Process;

class SecurityCheckerRoaveTest extends AbstractExternalTaskTestCase
{
/** @var \Prophecy\Prophecy\ObjectProphecy */
private $filesystem;

protected function provideTask(): TaskInterface
{
$this->filesystem = $this->prophesize(Filesystem::class);

return new SecurityCheckerRoave(
$this->processBuilder->reveal(),
$this->formatter->reveal(),
$this->filesystem->reveal()
);
}

private function mockComposerJsonWithRoaveSecurityAdvisories(): void
{
$this->filesystem->isFile(Argument::exact('./composer.json'))->willReturn(true);
$this->filesystem->readPath(Argument::exact('./composer.json'))->willReturn(
json_encode([
'require' => ['roave/security-advisories'=>'dev-latest'],
])
);
}

private function mockComposerJsonWithoutRoaveSecurityAdvisories(): void
{
$this->filesystem->isFile(Argument::exact('./composer.json'))->willReturn(true);
$this->filesystem->readPath(Argument::exact('./composer.json'))->willReturn(
json_encode([
'require' => [],
])
);
}

private function mockMissingComposerJson(): void
{
$this->filesystem->isFile(Argument::exact('./composer.json'))->willReturn(false);
}

public function provideConfigurableOptions(): iterable
{
yield 'defaults' => [
[],
[
'jsonfile' => './composer.json',
'lockfile' => './composer.lock',
'run_always' => false,
]
];
}

public function provideRunContexts(): iterable
{
yield 'run-context' => [
true,
$this->mockContext(RunContext::class)
];

yield 'pre-commit-context' => [
true,
$this->mockContext(GitPreCommitContext::class)
];

yield 'other' => [
false,
$this->mockContext()
];
}

public function provideFailsOnStuff(): iterable
{
yield 'exitCode1' => [
[],
$this->mockContext(RunContext::class, ['composer.lock']),
function () {
$this->mockProcessBuilder('composer', $process = $this->mockProcess(1));
$this->mockComposerJsonWithRoaveSecurityAdvisories();
$this->formatter->format($process)->willReturn('nope');
},
'nope'
];
yield 'no-roave-security-advisories' =>
[
[],
$this->mockContext(RunContext::class, ['composer.lock']),
function () {
$this->mockProcessBuilder('composer', $this->mockProcess(0));
$this->mockComposerJsonWithoutRoaveSecurityAdvisories();
},
'This task is only available when roave/security-advisories is installed as a library.'
];
}

public function providePassesOnStuff(): iterable
{
yield 'exitCode0' => [
[],
$this->mockContext(RunContext::class, ['composer.lock']),
function () {
$this->mockProcessBuilder('composer', $this->mockProcess(0));
$this->mockComposerJsonWithRoaveSecurityAdvisories();
},
];
yield 'exitCode0WhenRunAlways' => [
[
'run_always' => true
],
$this->mockContext(RunContext::class, ['notrelated.php']),
function () {
$this->mockProcessBuilder('composer', $this->mockProcess(0));
$this->mockComposerJsonWithRoaveSecurityAdvisories();
}
];
}

public function provideSkipsOnStuff(): iterable
{
yield 'no-files' => [
[],
$this->mockContext(RunContext::class),
function () {
$this->mockComposerJsonWithRoaveSecurityAdvisories();
}
];
yield 'no-composer.json-file' => [
[
'run_always' => true
],
$this->mockContext(RunContext::class, []),
function () {
$this->mockMissingComposerJson();
}
];
}

/**
* @test
* @dataProvider provideExternalTaskRuns
*/
public function it_runs_external_task(
array $config,
ContextInterface $context,
string $taskName,
array $cliArguments,
?Process $process = null
): void
{
$configurator = function () {
$this->mockComposerJsonWithRoaveSecurityAdvisories();
};
\Closure::bind($configurator, $this)();

parent::it_runs_external_task($config,$context,$taskName,$cliArguments,$process);
}

public function provideExternalTaskRuns(): iterable
{
yield 'defaults' => [
[],
$this->mockContext(RunContext::class, ['composer.lock']),
'composer',
[
'update',
'--dry-run',
'roave/security-advisories',
]
];
}
}

0 comments on commit d100d05

Please sign in to comment.