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:
parent
2c69618347
commit
c42927c6e4
@ -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) {
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
];
|
||||
|
||||
/**
|
||||
|
129
src/Psalm/Report/SarifReport.php
Normal file
129
src/Psalm/Report/SarifReport.php
Normal 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";
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user