1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Add SARIF as report output (#4582)

https://docs.oasis-open.org/sarif/sarif/v2.0/sarif-v2.0.html
This commit is contained in:
Lukas Reschke 2020-11-17 19:23:20 +01:00 committed by Daniil Gentili
parent 2c69618347
commit c42927c6e4
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
7 changed files with 552 additions and 2 deletions

View File

@ -368,6 +368,7 @@ class ProjectAnalyzer
'.emacs' => Report::TYPE_EMACS, '.emacs' => Report::TYPE_EMACS,
'.pylint' => Report::TYPE_PYLINT, '.pylint' => Report::TYPE_PYLINT,
'.console' => Report::TYPE_CONSOLE, '.console' => Report::TYPE_CONSOLE,
'.sarif' => Report::TYPE_SARIF,
]; ];
foreach ($report_file_paths as $report_file_path) { foreach ($report_file_paths as $report_file_path) {

View File

@ -29,6 +29,7 @@ use Psalm\Report\CompactReport;
use Psalm\Report\ConsoleReport; use Psalm\Report\ConsoleReport;
use Psalm\Report\EmacsReport; use Psalm\Report\EmacsReport;
use Psalm\Report\GithubActionsReport; use Psalm\Report\GithubActionsReport;
use Psalm\Report\SarifReport;
use Psalm\Report\JsonReport; use Psalm\Report\JsonReport;
use Psalm\Report\JsonSummaryReport; use Psalm\Report\JsonSummaryReport;
use Psalm\Report\JunitReport; use Psalm\Report\JunitReport;
@ -757,6 +758,10 @@ class IssueBuffer
case Report::TYPE_PHP_STORM: case Report::TYPE_PHP_STORM:
$output = new PhpStormReport($normalized_data, self::$fixable_issue_counts, $report_options); $output = new PhpStormReport($normalized_data, self::$fixable_issue_counts, $report_options);
break; break;
case Report::TYPE_SARIF:
$output = new SarifReport($normalized_data, self::$fixable_issue_counts, $report_options);
break;
} }
return $output->create(); return $output->create();

View File

@ -19,6 +19,7 @@ abstract class Report
public const TYPE_TEXT = 'text'; public const TYPE_TEXT = 'text';
public const TYPE_GITHUB_ACTIONS = 'github'; public const TYPE_GITHUB_ACTIONS = 'github';
public const TYPE_PHP_STORM = 'phpstorm'; public const TYPE_PHP_STORM = 'phpstorm';
public const TYPE_SARIF = 'sarif';
public const SUPPORTED_OUTPUT_TYPES = [ public const SUPPORTED_OUTPUT_TYPES = [
self::TYPE_COMPACT, self::TYPE_COMPACT,
@ -34,6 +35,7 @@ abstract class Report
self::TYPE_TEXT, self::TYPE_TEXT,
self::TYPE_GITHUB_ACTIONS, self::TYPE_GITHUB_ACTIONS,
self::TYPE_PHP_STORM, self::TYPE_PHP_STORM,
self::TYPE_SARIF,
]; ];
/** /**

View File

@ -0,0 +1,129 @@
<?php
namespace Psalm\Report;
use Psalm\Internal\Json\Json;
use function max;
use Psalm\Config;
use Psalm\Issue;
use Psalm\Report;
use function file_exists;
use function file_get_contents;
/**
* SARIF report format suitable for import into any SARIF compatible solution
*
* https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html
*/
class SarifReport extends Report
{
public function create(): string
{
$report = [
'version' => '2.1.0',
'runs' => [
[
'tool' => [
'driver' => [
'name' => 'Psalm',
'version' => PSALM_VERSION,
],
],
'results' => [],
],
],
];
$rules = [];
foreach ($this->issues_data as $issue_data) {
$rules[$issue_data->shortcode] = [
'id' => (string)$issue_data->shortcode,
'name' => $issue_data->type,
'shortDescription' => [
'text' => $issue_data->type,
],
'properties' => [
'tags' => [
(\substr($issue_data->type, 0, 7) === 'Tainted') ? 'security' : 'maintainability',
],
],
];
$markdown_documentation_path = __DIR__ . '/../../../docs/running_psalm/issues/' . $issue_data->type . '.md';
if (file_exists($markdown_documentation_path)) {
$markdown_documentation = file_get_contents($markdown_documentation_path);
$rules[$issue_data->shortcode]['help']['markdown'] = $markdown_documentation;
$rules[$issue_data->shortcode]['help']['text'] = $markdown_documentation;
}
$jsonEntry = [
'ruleId' => (string)$issue_data->shortcode,
'message' => [
'text' => $issue_data->message,
],
'level' => ($issue_data->severity === Config::REPORT_ERROR) ? 'error' : 'note',
'locations' => [
[
'physicalLocation' => [
'artifactLocation' => [
'uri' => $issue_data->file_name,
],
'region' => [
'startLine' => $issue_data->line_from,
'endLine' => $issue_data->line_to,
'startColumn' => $issue_data->column_from,
'endColumn' => $issue_data->column_to,
],
],
]
],
];
if ($issue_data->taint_trace != null) {
$jsonEntry['codeFlows'] = [
[
'message' => [
'text' => 'Tracing the path from user input to insecure usage',
],
'threadFlows' => [
[
'locations' => [],
],
],
]
];
/** @var \Psalm\Internal\Analyzer\DataFlowNodeData $trace */
foreach ($issue_data->taint_trace as $trace) {
if (isset($trace->line_from)) {
$jsonEntry['codeFlows'][0]['threadFlows'][0]['locations'][] = [
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => $trace->file_name,
],
'region' => [
'startLine' => $trace->line_from,
'endLine' => $trace->line_to,
'startColumn' => $trace->column_from,
'endColumn' => $trace->column_to,
],
],
],
];
}
}
}
$report['runs'][0]['results'][] = $jsonEntry;
}
foreach ($rules as $rule) {
$report['runs'][0]['tool']['driver']['rules'][] = $rule;
}
$options = $this->pretty ? Json::PRETTY : Json::DEFAULT;
return Json::encode($report, $options) . "\n";
}
}

View File

@ -387,7 +387,7 @@ Reports:
--report=PATH --report=PATH
The path where to output report file. The output format is based on the file extension. The path where to output report file. The output format is based on the file extension.
(Currently supported formats: ".json", ".xml", ".txt", ".emacs", ".pylint", ".console", (Currently supported formats: ".json", ".xml", ".txt", ".emacs", ".pylint", ".console",
"checkstyle.xml", "sonarqube.json", "summary.json", "junit.xml") ".sarif", "checkstyle.xml", "sonarqube.json", "summary.json", "junit.xml")
--report-show-info[=BOOLEAN] --report-show-info[=BOOLEAN]
Whether the report should include non-errors in its output (defaults to true) Whether the report should include non-errors in its output (defaults to true)

View File

@ -65,6 +65,415 @@ class ReportOutputTest extends TestCase
ProjectAnalyzer::getFileReportOptions(['/tmp/report.log']); ProjectAnalyzer::getFileReportOptions(['/tmp/report.log']);
} }
public function analyzeTaintFlowFilesForReport() : void
{
$vulnerable_file_contents = '<?php
function addPrefixToInput($prefix, $input): string {
return $prefix . $input;
}
$prefixedData = addPrefixToInput(\'myprefix\', $_POST[\'cmd\']);
shell_exec($prefixedData);
echo "Successfully executed the command: " . $prefixedData;';
$this->addFile(
'taintflow-test/vulnerable.php',
$vulnerable_file_contents
);
$this->analyzeFile('taintflow-test/vulnerable.php', new Context(), true, true);
}
public function testSarifReport(): void
{
$this->analyzeTaintFlowFilesForReport();
$issue_data = [
'version' => '2.1.0',
'runs' => [
[
'tool' => [
'driver' => [
'name' => 'Psalm',
'version' => '2.0.0',
'rules' => [
[
'id' => '246',
'name' => 'TaintedShell',
'shortDescription' => [
'text' => 'TaintedShell'
],
'properties' => [
'tags' => [
'security'
],
],
'help' => [
'markdown' => file_get_contents(__DIR__ . '/../docs/running_psalm/issues/TaintedShell.md'),
'text' => file_get_contents(__DIR__ . '/../docs/running_psalm/issues/TaintedShell.md'),
],
],
[
'id' => '245',
'name' => 'TaintedHtml',
'shortDescription' => [
'text' => 'TaintedHtml'
],
'properties' => [
'tags' => [
'security'
],
],
'help' => [
'markdown' => file_get_contents(__DIR__ . '/../docs/running_psalm/issues/TaintedHtml.md'),
'text' => file_get_contents(__DIR__ . '/../docs/running_psalm/issues/TaintedHtml.md'),
],
],
]
]
],
'results' => [
[
'ruleId' => '246',
'message' => [
'text' => 'Detected tainted shell code'
],
'level' => 'error',
'locations' => [
[
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 9,
'endLine' => 9,
'startColumn' => 12,
'endColumn' => 25
]
]
]
],
'codeFlows' => [
[
'message' => [
'text' => 'Tracing the path from user input to insecure usage'
],
'threadFlows' => [
[
'locations' => [
[
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 7,
'endLine' => 7,
'startColumn' => 46,
'endColumn' => 52
]
]
]
],
[
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 7,
'endLine' => 7,
'startColumn' => 46,
'endColumn' => 59
]
]
]
],
[
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 3,
'endLine' => 3,
'startColumn' => 36,
'endColumn' => 42
]
]
]
],
[
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 3,
'endLine' => 3,
'startColumn' => 36,
'endColumn' => 42
]
]
]
],
[
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 4,
'endLine' => 4,
'startColumn' => 12,
'endColumn' => 28
]
]
]
],
[
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 3,
'endLine' => 3,
'startColumn' => 45,
'endColumn' => 51
]
]
]
],
[
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 7,
'endLine' => 7,
'startColumn' => 1,
'endColumn' => 14
]
]
]
],
[
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 9,
'endLine' => 9,
'startColumn' => 12,
'endColumn' => 25
]
]
]
]
]
]
]
]
]
],
[
'ruleId' => '245',
'message' => [
'text' => 'Detected tainted HTML'
],
'level' => 'error',
'locations' => [
[
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 11,
'endLine' => 11,
'startColumn' => 6,
'endColumn' => 59
]
]
]
],
'codeFlows' => [
[
'message' => [
'text' => 'Tracing the path from user input to insecure usage'
],
'threadFlows' => [
[
'locations' => [
[
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 7,
'endLine' => 7,
'startColumn' => 46,
'endColumn' => 52
]
]
]
],
[
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 7,
'endLine' => 7,
'startColumn' => 46,
'endColumn' => 59
]
]
]
],
[
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 3,
'endLine' => 3,
'startColumn' => 36,
'endColumn' => 42
]
]
]
],
[
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 3,
'endLine' => 3,
'startColumn' => 36,
'endColumn' => 42
]
]
]
],
[
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 4,
'endLine' => 4,
'startColumn' => 12,
'endColumn' => 28
]
]
]
],
[
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 3,
'endLine' => 3,
'startColumn' => 45,
'endColumn' => 51
]
]
]
],
[
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 7,
'endLine' => 7,
'startColumn' => 1,
'endColumn' => 14
]
]
]
],
[
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 11,
'endLine' => 11,
'startColumn' => 6,
'endColumn' => 59
]
]
]
],
[
'location' => [
'physicalLocation' => [
'artifactLocation' => [
'uri' => 'taintflow-test/vulnerable.php'
],
'region' => [
'startLine' => 11,
'endLine' => 11,
'startColumn' => 6,
'endColumn' => 59
]
]
]
]
]
]
]
]
]
]
]
]
]
];
$sarif_report_options = ProjectAnalyzer::getFileReportOptions([__DIR__ . '/test-report.sarif'])[0];
$this->assertSame(
$issue_data,
json_decode(IssueBuffer::getOutput(IssueBuffer::getIssuesData(), $sarif_report_options), true)
);
}
public function analyzeFileForReport() : void public function analyzeFileForReport() : void
{ {
$file_contents = '<?php $file_contents = '<?php

View File

@ -91,10 +91,14 @@ class TestCase extends BaseTestCase
* @param string $file_path * @param string $file_path
* *
*/ */
public function analyzeFile($file_path, \Psalm\Context $context, bool $track_unused_suppressions = true): void public function analyzeFile($file_path, \Psalm\Context $context, bool $track_unused_suppressions = true, bool $taint_flow_tracking = false): void
{ {
$codebase = $this->project_analyzer->getCodebase(); $codebase = $this->project_analyzer->getCodebase();
if ($taint_flow_tracking) {
$this->project_analyzer->trackTaintedInputs();
}
$codebase->addFilesToAnalyze([$file_path => $file_path]); $codebase->addFilesToAnalyze([$file_path => $file_path]);
$codebase->scanFiles(); $codebase->scanFiles();