From 1aec2f1cefa55117ee9a66937e20d01481edab60 Mon Sep 17 00:00:00 2001 From: Joe Stewart Date: Mon, 29 Jan 2024 14:01:08 -0500 Subject: [PATCH 1/9] Add security-checker using Composer Audit --- resources/config/tasks.yml | 7 +++ src/Task/SecurityCheckerComposeraudit.php | 74 +++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 src/Task/SecurityCheckerComposeraudit.php diff --git a/resources/config/tasks.yml b/resources/config/tasks.yml index d459ce1a..c1d5aafe 100644 --- a/resources/config/tasks.yml +++ b/resources/config/tasks.yml @@ -336,6 +336,13 @@ services: tags: - {name: grumphp.task, task: securitychecker} + GrumPHP\Task\SecurityCheckerComposeraudit: + arguments: + - '@process_builder' + - '@formatter.raw_process' + tags: + - {name: grumphp.task, task: securitychecker_composeraudit} + GrumPHP\Task\SecurityCheckerEnlightn: arguments: - '@process_builder' diff --git a/src/Task/SecurityCheckerComposeraudit.php b/src/Task/SecurityCheckerComposeraudit.php new file mode 100644 index 00000000..628575a9 --- /dev/null +++ b/src/Task/SecurityCheckerComposeraudit.php @@ -0,0 +1,74 @@ + + */ +class SecurityCheckerComposeraudit extends AbstractExternalTask +{ + public static function getConfigurableOptions(): ConfigOptionsResolver + { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'format' => null, + 'locked' => true, + 'no_dev' => false, + 'run_always' => false, + 'working_dir' => './', + ]); + + // $resolver->addAllowedTypes('lockfile', ['string']); + + $resolver->addAllowedTypes('format', ['null', 'string']); + $resolver->addAllowedTypes('locked', ['bool']); + $resolver->addAllowedTypes('no_dev', ['bool']); + $resolver->addAllowedTypes('run_always', ['bool']); + $resolver->addAllowedTypes('working_dir', ['string']); + + return ConfigOptionsResolver::fromOptionsResolver($resolver); + } + + public function canRunInContext(ContextInterface $context): bool + { + return $context instanceof GitPreCommitContext || $context instanceof RunContext; + } + + public function run(ContextInterface $context): TaskResultInterface + { + $config = $this->getConfig()->getOptions(); + $files = $context->getFiles() + ->path(pathinfo($config['working_dir'] . "/composer.lock", PATHINFO_DIRNAME)) + ->name(pathinfo($config['working_dir'] . "/composer.lock", PATHINFO_BASENAME)); + if (0 === \count($files) && !$config['run_always']) { + return TaskResult::createSkipped($this, $context); + } + + $arguments = $this->processBuilder->createArgumentsForCommand('composer'); + $arguments->add('audit'); + $arguments->addOptionalArgument('--format=%s', $config['format']); + $arguments->addOptionalArgument('--locked', $config['locked']); + $arguments->addOptionalArgument('--no-dev', $config['no_dev']); + $arguments->addOptionalArgument('--working-dir=%s', $config['working_dir']); + + $process = $this->processBuilder->buildProcess($arguments); + $process->run(); + + if (!$process->isSuccessful()) { + return TaskResult::createFailed($this, $context, $this->formatter->format($process)); + } + + return TaskResult::createPassed($this, $context); + } +} From 9523d6274e870e0505f303dd68c1a6ce13a74712 Mon Sep 17 00:00:00 2001 From: Joe Stewart Date: Mon, 29 Jan 2024 14:01:50 -0500 Subject: [PATCH 2/9] Add test for security-checker using Composer Audit --- .../Task/SecurityCheckerComposerauditTest.php | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 test/Unit/Task/SecurityCheckerComposerauditTest.php diff --git a/test/Unit/Task/SecurityCheckerComposerauditTest.php b/test/Unit/Task/SecurityCheckerComposerauditTest.php new file mode 100644 index 00000000..e5a5e018 --- /dev/null +++ b/test/Unit/Task/SecurityCheckerComposerauditTest.php @@ -0,0 +1,115 @@ +processBuilder->reveal(), + $this->formatter->reveal() + ); + } + + public function provideConfigurableOptions(): iterable + { + yield 'defaults' => [ + [], + [ + 'format' => null, + 'locked' => true, + 'no_dev' => false, + 'run_always' => false, + 'working_dir' => './', + ] + ]; + } + + 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->formatter->format($process)->willReturn('nope'); + }, + 'nope' + ]; + } + + public function providePassesOnStuff(): iterable + { + yield 'exitCode0' => [ + [], + $this->mockContext(RunContext::class, ['composer.lock']), + function () { + $this->mockProcessBuilder('composer', $this->mockProcess(0)); + } + ]; + yield 'exitCode0WhenRunAlways' => [ + [ + 'run_always' => true + ], + $this->mockContext(RunContext::class, ['notrelated.php']), + function () { + $this->mockProcessBuilder('composer', $this->mockProcess(0)); + } + ]; + } + + public function provideSkipsOnStuff(): iterable + { + yield 'no-files' => [ + [], + $this->mockContext(RunContext::class), + function () {} + ]; + yield 'no-composer-file' => [ + [], + $this->mockContext(RunContext::class, ['thisisnotacomposerfile.lock']), + function () {} + ]; + } + + public function provideExternalTaskRuns(): iterable + { + yield 'defaults' => [ + [], + $this->mockContext(RunContext::class, ['composer.lock']), + 'composer', + [ + 'audit', + '--locked', + '--working-dir=./', + ] + ]; + } +} From 2a01f7e466f9b7546dc11f0f7d5cd2d0fe602128 Mon Sep 17 00:00:00 2001 From: Joe Stewart Date: Mon, 29 Jan 2024 14:04:12 -0500 Subject: [PATCH 3/9] Add docs for security-checker using Composer Audit --- doc/tasks.md | 6 ++- doc/tasks/securitychecker.md | 1 + doc/tasks/securitychecker/composeraudit.md | 48 ++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 doc/tasks/securitychecker/composeraudit.md diff --git a/doc/tasks.md b/doc/tasks.md index 0c8c7884..bc618fad 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -54,6 +54,7 @@ grumphp: psalm: ~ rector: ~ robo: ~ + securitychecker_composeraudit: ~ securitychecker_enlightn: ~ securitychecker_local: ~ securitychecker_roave: ~ @@ -119,6 +120,7 @@ Every task has its own default configuration. It is possible to overwrite the pa - [Rector](tasks/rector.md) - [Robo](tasks/robo.md) - [Security Checker](tasks/securitychecker.md) + - [Composer Audit](tasks/securitychecker/composeraudit.md) - [Enlightn](tasks/securitychecker/enlightn.md) - [Local](tasks/securitychecker/local.md) - [Roave](tasks/securitychecker/roave.md) @@ -205,7 +207,7 @@ interface TaskInterface } ``` -* `getConfigurableOptions`: This method has to return all configurable options for the task. +* `getConfigurableOptions`: This method has to return all configurable options for the task. * `canRunInContext`: Tells GrumPHP if it can run in `pre-commit`, `commit-msg` or `run` context. * `run`: Executes the task and returns a result * `getConfig`: Provides the resolved configuration for the task or an empty config for newly instantiated tasks. @@ -260,7 +262,7 @@ For a more detailed view on how to use these classes, you can scroll through our In some cases you might want to run the same task but with different configuration. Good news: This is perfectly possible! -You can use any name you want for the task, as long as you configure an existing task in the metadata section. +You can use any name you want for the task, as long as you configure an existing task in the metadata section. Configuration of the additional task will look like this: ```yaml diff --git a/doc/tasks/securitychecker.md b/doc/tasks/securitychecker.md index b6458094..bea62d39 100644 --- a/doc/tasks/securitychecker.md +++ b/doc/tasks/securitychecker.md @@ -4,6 +4,7 @@ The SensioLabs Security Checker API is abandoned You can use one of following tasks as a replacement: +- [securitychecker_composeraudit](securitychecker/composeraudit.md) - [securitychecker_enlightn](securitychecker/enlightn.md) - [securitychecker_local](securitychecker/local.md) - [securitychecker_roave](securitychecker/roave.md) diff --git a/doc/tasks/securitychecker/composeraudit.md b/doc/tasks/securitychecker/composeraudit.md new file mode 100644 index 00000000..b175729b --- /dev/null +++ b/doc/tasks/securitychecker/composeraudit.md @@ -0,0 +1,48 @@ +# Composer Audit Security Checker + +The Security Checker will check your `composer.lock` file for known security vulnerabilities. + +***Config*** + +The task lives under the `securitychecker_composeraudit` namespace and has the following configurable parameters: + +```yaml +# grumphp.yml +grumphp: + tasks: + securitychecker_composeraudit: + locked: true + no_dev: false + run_always: false + working_dir: ./ +``` + +**format** + +*Default: null* + +You can choose the format of the output. The available options are `table`, `plain`, `json` and `summary`. By default, grumphp will use the format `table`. + +**locked** + +*Default: true* + +Audit packages from the lock file, regardless of what is currently in vendor dir. + +**no_dev** + +*Default: false* + +When this option is set to `true`, the task will skip packages under `require-dev`. + +**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. + +**working_dir** + +*Default: ./* + +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. \ No newline at end of file From fbb205a156bde9c6fc8a39b96cd956f3cf609edf Mon Sep 17 00:00:00 2001 From: Joe Stewart Date: Fri, 2 Feb 2024 13:05:32 -0500 Subject: [PATCH 4/9] Add testing for options --- .../Task/SecurityCheckerComposerauditTest.php | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/test/Unit/Task/SecurityCheckerComposerauditTest.php b/test/Unit/Task/SecurityCheckerComposerauditTest.php index e5a5e018..b3ad13fb 100644 --- a/test/Unit/Task/SecurityCheckerComposerauditTest.php +++ b/test/Unit/Task/SecurityCheckerComposerauditTest.php @@ -111,5 +111,58 @@ public function provideExternalTaskRuns(): iterable '--working-dir=./', ] ]; + + yield 'format' => [ + [ + 'format' => 'json', + ], + $this->mockContext(RunContext::class, ['composer.lock']), + 'composer', + [ + 'audit', + '--format=json', + '--locked', + '--working-dir=./', + ] + ]; + + yield 'locked' => [ + [ + 'locked' => false, + ], + $this->mockContext(RunContext::class, ['composer.lock']), + 'composer', + [ + 'audit', + '--working-dir=./', + ] + ]; + + yield 'no-dev' => [ + [ + 'no_dev' => true, + ], + $this->mockContext(RunContext::class, ['composer.lock']), + 'composer', + [ + 'audit', + '--locked', + '--no-dev', + '--working-dir=./', + ] + ]; + + yield 'working-dir' => [ + [ + 'working_dir' => './', + ], + $this->mockContext(RunContext::class, ['composer.lock']), + 'composer', + [ + 'audit', + '--locked', + '--working-dir=./', + ] + ]; } } From 90227bf0ccc4e3a4bd9d22a81e4dddefc616c220 Mon Sep 17 00:00:00 2001 From: Joe Stewart Date: Fri, 2 Feb 2024 13:07:05 -0500 Subject: [PATCH 5/9] Add format option to sample --- doc/tasks/securitychecker/composeraudit.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/tasks/securitychecker/composeraudit.md b/doc/tasks/securitychecker/composeraudit.md index b175729b..8bb959ea 100644 --- a/doc/tasks/securitychecker/composeraudit.md +++ b/doc/tasks/securitychecker/composeraudit.md @@ -11,6 +11,7 @@ The task lives under the `securitychecker_composeraudit` namespace and has the f grumphp: tasks: securitychecker_composeraudit: + format: null locked: true no_dev: false run_always: false From 9fb0b10469da75a0ebb0f3832edc9387ae45b0bb Mon Sep 17 00:00:00 2001 From: Joe Stewart Date: Fri, 2 Feb 2024 13:41:02 -0500 Subject: [PATCH 6/9] Remove commented code --- src/Task/SecurityCheckerComposeraudit.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Task/SecurityCheckerComposeraudit.php b/src/Task/SecurityCheckerComposeraudit.php index 628575a9..3c05d57e 100644 --- a/src/Task/SecurityCheckerComposeraudit.php +++ b/src/Task/SecurityCheckerComposeraudit.php @@ -29,8 +29,6 @@ public static function getConfigurableOptions(): ConfigOptionsResolver 'working_dir' => './', ]); - // $resolver->addAllowedTypes('lockfile', ['string']); - $resolver->addAllowedTypes('format', ['null', 'string']); $resolver->addAllowedTypes('locked', ['bool']); $resolver->addAllowedTypes('no_dev', ['bool']); From 126c16df6a6476ac6230f218cc2f44af54d97d09 Mon Sep 17 00:00:00 2001 From: Joe Stewart Date: Fri, 2 Feb 2024 15:46:34 -0500 Subject: [PATCH 7/9] working_dir defaults to null --- src/Task/SecurityCheckerComposeraudit.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Task/SecurityCheckerComposeraudit.php b/src/Task/SecurityCheckerComposeraudit.php index 3c05d57e..d57dc1a6 100644 --- a/src/Task/SecurityCheckerComposeraudit.php +++ b/src/Task/SecurityCheckerComposeraudit.php @@ -26,14 +26,14 @@ public static function getConfigurableOptions(): ConfigOptionsResolver 'locked' => true, 'no_dev' => false, 'run_always' => false, - 'working_dir' => './', + 'working_dir' => null, ]); $resolver->addAllowedTypes('format', ['null', 'string']); $resolver->addAllowedTypes('locked', ['bool']); $resolver->addAllowedTypes('no_dev', ['bool']); $resolver->addAllowedTypes('run_always', ['bool']); - $resolver->addAllowedTypes('working_dir', ['string']); + $resolver->addAllowedTypes('working_dir', ['null', 'string']); return ConfigOptionsResolver::fromOptionsResolver($resolver); } @@ -47,8 +47,8 @@ public function run(ContextInterface $context): TaskResultInterface { $config = $this->getConfig()->getOptions(); $files = $context->getFiles() - ->path(pathinfo($config['working_dir'] . "/composer.lock", PATHINFO_DIRNAME)) - ->name(pathinfo($config['working_dir'] . "/composer.lock", PATHINFO_BASENAME)); + ->path(pathinfo("composer.lock", PATHINFO_DIRNAME)) + ->name(pathinfo("composer.lock", PATHINFO_BASENAME)); if (0 === \count($files) && !$config['run_always']) { return TaskResult::createSkipped($this, $context); } From 4bf95d6ee0b81827444677dc3b23640cfcf4bd75 Mon Sep 17 00:00:00 2001 From: Joe Stewart Date: Fri, 2 Feb 2024 15:48:16 -0500 Subject: [PATCH 8/9] working dir defaults to null --- test/Unit/Task/SecurityCheckerComposerauditTest.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/test/Unit/Task/SecurityCheckerComposerauditTest.php b/test/Unit/Task/SecurityCheckerComposerauditTest.php index b3ad13fb..fae103a7 100644 --- a/test/Unit/Task/SecurityCheckerComposerauditTest.php +++ b/test/Unit/Task/SecurityCheckerComposerauditTest.php @@ -29,7 +29,7 @@ public function provideConfigurableOptions(): iterable 'locked' => true, 'no_dev' => false, 'run_always' => false, - 'working_dir' => './', + 'working_dir' => null, ] ]; } @@ -108,7 +108,6 @@ public function provideExternalTaskRuns(): iterable [ 'audit', '--locked', - '--working-dir=./', ] ]; @@ -122,7 +121,6 @@ public function provideExternalTaskRuns(): iterable 'audit', '--format=json', '--locked', - '--working-dir=./', ] ]; @@ -134,7 +132,6 @@ public function provideExternalTaskRuns(): iterable 'composer', [ 'audit', - '--working-dir=./', ] ]; @@ -148,20 +145,19 @@ public function provideExternalTaskRuns(): iterable 'audit', '--locked', '--no-dev', - '--working-dir=./', ] ]; yield 'working-dir' => [ [ - 'working_dir' => './', + 'working_dir' => 'dir', ], $this->mockContext(RunContext::class, ['composer.lock']), 'composer', [ 'audit', '--locked', - '--working-dir=./', + '--working-dir=dir', ] ]; } From 090365b8de1c92e5a5362aff2bef7b30a490bfec Mon Sep 17 00:00:00 2001 From: Joe Stewart Date: Fri, 2 Feb 2024 15:48:51 -0500 Subject: [PATCH 9/9] working dir defaults to null --- doc/tasks/securitychecker/composeraudit.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/tasks/securitychecker/composeraudit.md b/doc/tasks/securitychecker/composeraudit.md index 8bb959ea..0da96d56 100644 --- a/doc/tasks/securitychecker/composeraudit.md +++ b/doc/tasks/securitychecker/composeraudit.md @@ -15,7 +15,7 @@ grumphp: locked: true no_dev: false run_always: false - working_dir: ./ + working_dir: null ``` **format** @@ -44,6 +44,6 @@ When this option is set to `false`, the task will only run when the `composer.lo **working_dir** -*Default: ./* +*Default: null 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. \ No newline at end of file