1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 12:24:49 +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,
'.pylint' => Report::TYPE_PYLINT,
'.console' => Report::TYPE_CONSOLE,
'.sarif' => Report::TYPE_SARIF,
];
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\EmacsReport;
use Psalm\Report\GithubActionsReport;
use Psalm\Report\SarifReport;
use Psalm\Report\JsonReport;
use Psalm\Report\JsonSummaryReport;
use Psalm\Report\JunitReport;
@ -757,6 +758,10 @@ class IssueBuffer
case Report::TYPE_PHP_STORM:
$output = new PhpStormReport($normalized_data, self::$fixable_issue_counts, $report_options);
break;
case Report::TYPE_SARIF:
$output = new SarifReport($normalized_data, self::$fixable_issue_counts, $report_options);
break;
}
return $output->create();

View File

@ -19,6 +19,7 @@ abstract class Report
public const TYPE_TEXT = 'text';
public const TYPE_GITHUB_ACTIONS = 'github';
public const TYPE_PHP_STORM = 'phpstorm';
public const TYPE_SARIF = 'sarif';
public const SUPPORTED_OUTPUT_TYPES = [
self::TYPE_COMPACT,
@ -34,6 +35,7 @@ abstract class Report
self::TYPE_TEXT,
self::TYPE_GITHUB_ACTIONS,
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
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",
"checkstyle.xml", "sonarqube.json", "summary.json", "junit.xml")
".sarif", "checkstyle.xml", "sonarqube.json", "summary.json", "junit.xml")
--report-show-info[=BOOLEAN]
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']);
}
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
{
$file_contents = '<?php

View File

@ -91,10 +91,14 @@ class TestCase extends BaseTestCase
* @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();
if ($taint_flow_tracking) {
$this->project_analyzer->trackTaintedInputs();
}
$codebase->addFilesToAnalyze([$file_path => $file_path]);
$codebase->scanFiles();