diff --git a/bin/phplint b/bin/phplint index b97cc9c8..5d33baa8 100755 --- a/bin/phplint +++ b/bin/phplint @@ -45,10 +45,17 @@ if (true === $input->hasParameterOption(['--no-progress'], true)) { } } -$extensions[] = new OutputFormat([ +$formats = [ OptionDefinition::LOG_JSON, OptionDefinition::LOG_JUNIT, -]); +]; + +if (\class_exists('\Bartlett\Sarif\SarifLog')) { + // only when Composer development dependencies were installed too! + $formats[] = OptionDefinition::LOG_SARIF; +} + +$extensions[] = new OutputFormat($formats); $dispatcher = new EventDispatcher($extensions); diff --git a/composer.json b/composer.json index a005c299..29db4e7a 100644 --- a/composer.json +++ b/composer.json @@ -31,10 +31,11 @@ "symfony/yaml": "^5.4 || ^6.0" }, "require-dev": { - "php-parallel-lint/php-console-highlighter": "^1.0", + "bamarni/composer-bin-plugin": "^1.4", + "bartlett/sarif-php-sdk": "^1.0", "brainmaestro/composer-git-hooks": "^2.8.5 || 3.0.0-alpha.1", "jetbrains/phpstorm-stubs": "^2021.3 || ^2022.3", - "bamarni/composer-bin-plugin": "^1.4" + "php-parallel-lint/php-console-highlighter": "^1.0" }, "autoload": { "psr-4": { diff --git a/src/Command/ConfigureCommandTrait.php b/src/Command/ConfigureCommandTrait.php index 3de75a39..7d0fb3cc 100644 --- a/src/Command/ConfigureCommandTrait.php +++ b/src/Command/ConfigureCommandTrait.php @@ -99,6 +99,12 @@ protected function configureCommand(Command $command): void InputOption::VALUE_OPTIONAL, 'Log scan results in JUnit XML format to file (default: ' . OptionDefinition::DEFAULT_STANDARD_OUTPUT_LABEL . ')' ) + ->addOption( + 'log-sarif', + null, + InputOption::VALUE_OPTIONAL, + 'Log scan results in SARIF format to file (default: ' . OptionDefinition::DEFAULT_STANDARD_OUTPUT_LABEL . ')' + ) ->addOption( 'warning', 'w', diff --git a/src/Configuration/AbstractOptionsResolver.php b/src/Configuration/AbstractOptionsResolver.php index 59f0021d..402aeb73 100644 --- a/src/Configuration/AbstractOptionsResolver.php +++ b/src/Configuration/AbstractOptionsResolver.php @@ -47,6 +47,7 @@ public function __construct(InputInterface $input, array $configuration = []) OptionDefinition::NO_PROGRESS => false, OptionDefinition::LOG_JSON => null, OptionDefinition::LOG_JUNIT => null, + OptionDefinition::LOG_SARIF => false, OptionDefinition::WARNING => false, OptionDefinition::OPTION_MEMORY_LIMIT => ini_get('memory_limit'), OptionDefinition::IGNORE_EXIT_CODE => false, @@ -96,6 +97,7 @@ public function __construct(InputInterface $input, array $configuration = []) OptionDefinition::PROGRESS, OptionDefinition::LOG_JSON, OptionDefinition::LOG_JUNIT, + OptionDefinition::LOG_SARIF, OptionDefinition::WARNING, OptionDefinition::OPTION_MEMORY_LIMIT, OptionDefinition::IGNORE_EXIT_CODE, diff --git a/src/Configuration/OptionDefinition.php b/src/Configuration/OptionDefinition.php index 1751987e..aa7fda56 100644 --- a/src/Configuration/OptionDefinition.php +++ b/src/Configuration/OptionDefinition.php @@ -33,6 +33,7 @@ interface OptionDefinition public const NO_PROGRESS = 'no-progress'; public const LOG_JSON = 'log-json'; public const LOG_JUNIT = 'log-junit'; + public const LOG_SARIF = 'log-sarif'; public const IGNORE_EXIT_CODE = 'ignore-exit-code'; public const DEFAULT_JOBS = 5; diff --git a/src/Configuration/OptionsFactory.php b/src/Configuration/OptionsFactory.php index 43b9dcdf..8187a03b 100644 --- a/src/Configuration/OptionsFactory.php +++ b/src/Configuration/OptionsFactory.php @@ -54,6 +54,7 @@ protected function configureOptions(OptionsResolver $resolver): void OptionDefinition::NO_PROGRESS => 'bool', OptionDefinition::LOG_JSON => ['bool', 'null', 'string'], OptionDefinition::LOG_JUNIT => ['bool', 'null', 'string'], + OptionDefinition::LOG_SARIF => ['bool', 'null', 'string'], OptionDefinition::WARNING => 'bool', OptionDefinition::OPTION_MEMORY_LIMIT => ['int', 'string'], OptionDefinition::IGNORE_EXIT_CODE => 'bool', @@ -89,5 +90,6 @@ protected function configureOptions(OptionsResolver $resolver): void }; $resolver->setNormalizer(OptionDefinition::LOG_JSON, $outputFormat); $resolver->setNormalizer(OptionDefinition::LOG_JUNIT, $outputFormat); + $resolver->setNormalizer(OptionDefinition::LOG_SARIF, $outputFormat); } } diff --git a/src/Extension/OutputFormat.php b/src/Extension/OutputFormat.php index 09e32ee6..08b76009 100644 --- a/src/Extension/OutputFormat.php +++ b/src/Extension/OutputFormat.php @@ -23,6 +23,7 @@ use Overtrue\PHPLint\Output\ConsoleOutput; use Overtrue\PHPLint\Output\JsonOutput; use Overtrue\PHPLint\Output\JunitOutput; +use Overtrue\PHPLint\Output\SarifOutput; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -70,6 +71,8 @@ public function initFormat(ConsoleCommandEvent $event): void $this->handlers[] = new JsonOutput(fopen($filename, 'w')); } elseif (OptionDefinition::LOG_JUNIT == $name) { $this->handlers[] = new JunitOutput(fopen($filename, 'w')); + } elseif (OptionDefinition::LOG_SARIF == $name) { + $this->handlers[] = new SarifOutput(fopen($filename, 'w')); } } } diff --git a/src/Output/SarifOutput.php b/src/Output/SarifOutput.php new file mode 100644 index 00000000..51115c78 --- /dev/null +++ b/src/Output/SarifOutput.php @@ -0,0 +1,131 @@ +getFailures(); + + $driver = new ToolComponent('PHPLint'); + $driver->setInformationUri('https://github.com/overtrue/phplint'); + $driver->setVersion('9.1.0'); + + $tool = new Tool($driver); + + $results = []; + + foreach ($failures as $file => $failure) { + $result = new Result(new Message($failure['error'])); + + $artifactLocation = new ArtifactLocation(); + $artifactLocation->setUri($this->pathToArtifactLocation($file)); + $artifactLocation->setUriBaseId('WORKINGDIR'); + + $location = new Location(); + $physicalLocation = new PhysicalLocation($artifactLocation); + $physicalLocation->setRegion(new Region($failure['line'])); + $location->setPhysicalLocation($physicalLocation); + $result->addLocations([$location]); + + $results[] = $result; + } + + $run = new Run($tool); + $workingDir = new ArtifactLocation(); + $workingDir->setUri($this->pathToUri(getcwd() . '/')); + $originalUriBaseIds = [ + 'WORKINGDIR' => $workingDir, + ]; + $run->addAdditionalProperties($originalUriBaseIds); + $run->addResults($results); + + $log = new SarifLog([$run]); + + $jsonString = json_encode( + $log, + JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT + ); + + $this->write($jsonString, true); + fclose($this->getStream()); + } + + /** + * Returns path to resource (file) scanned. + */ + protected function pathToArtifactLocation(string $path): string + { + $workingDir = getcwd(); + if ($workingDir === false) { + $workingDir = '.'; + } + if (substr($path, 0, strlen($workingDir)) === $workingDir) { + // relative path + return substr($path, strlen($workingDir) + 1); + } + + // absolute path with protocol + return $this->pathToUri($path); + } + + /** + * Returns path to resource (file) scanned with protocol. + */ + protected function pathToUri(string $path): string + { + if (parse_url($path, PHP_URL_SCHEME) !== null) { + // already a URL + return $path; + } + + $path = str_replace(DIRECTORY_SEPARATOR, '/', $path); + + // file:///C:/... on Windows systems + if (substr($path, 0, 1) !== '/') { + $path = '/' . $path; + } + + return 'file://' . $path; + } +} \ No newline at end of file