generate-sarif.php•4.14 kB
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Generate SARIF report from fuzzing crashes.
*/
$crashDir = __DIR__ . '/crashes';
$crashes = glob($crashDir . '/*.crash');
$sarif = [
'version' => '2.1.0',
'$schema' => 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
'runs' => [
[
'tool' => [
'driver' => [
'name' => 'PHP Fuzzer',
'version' => '1.0.0',
'informationUri' => 'https://github.com/nikic/PHP-Fuzzer',
'rules' => [
[
'id' => 'FUZZ001',
'name' => 'FuzzingCrash',
'shortDescription' => [
'text' => 'Input causes unexpected crash or error',
],
'fullDescription' => [
'text' => 'The fuzzer discovered an input that causes the application to crash or throw an unexpected error.',
],
'help' => [
'text' => 'Review the crash details and add appropriate input validation.',
],
'defaultConfiguration' => [
'level' => 'error',
],
],
],
],
],
'results' => [],
],
],
];
foreach ($crashes as $crashFile) {
$crash = json_decode(file_get_contents($crashFile), true);
if (! $crash) {
continue;
}
// Try to extract file and line from the trace
$location = extractLocation($crash['trace'] ?? '');
$result = [
'ruleId' => 'FUZZ001',
'level' => 'error',
'message' => [
'text' => sprintf(
'Fuzzing crash in %s: %s (Input length: %d)',
$crash['target'] ?? 'unknown',
$crash['error'] ?? 'unknown error',
strlen($crash['input'] ?? ''),
),
],
'locations' => [
[
'physicalLocation' => [
'artifactLocation' => [
'uri' => $location['file'] ?? 'unknown',
'uriBaseId' => 'SRCROOT',
],
'region' => [
'startLine' => $location['line'] ?? 1,
],
],
],
],
'partialFingerprints' => [
'fuzzerTarget' => $crash['target'] ?? 'unknown',
'errorType' => getErrorType($crash['error'] ?? ''),
],
'properties' => [
'inputSample' => substr($crash['input'] ?? '', 0, 100),
'inputLength' => strlen($crash['input'] ?? ''),
'crashFile' => basename($crashFile),
],
];
$sarif['runs'][0]['results'][] = $result;
}
echo json_encode($sarif, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
function extractLocation(string $trace): array
{
// Try to find the first source file (not vendor) in the trace
if (preg_match('/#\d+\s+([^(]+)\((\d+)\)/', $trace, $matches)) {
$file = $matches[1];
$line = (int) $matches[2];
// Convert absolute path to relative
$srcRoot = realpath(__DIR__ . '/../..');
if (str_starts_with($file, $srcRoot)) {
$file = substr($file, strlen($srcRoot) + 1);
}
return ['file' => $file, 'line' => $line];
}
return ['file' => 'unknown', 'line' => 1];
}
function getErrorType(string $error): string
{
if (false !== stripos($error, 'memory')) {
return 'memory_exhaustion';
}
if (false !== stripos($error, 'timeout')) {
return 'timeout';
}
if (false !== stripos($error, 'injection')) {
return 'injection_attempt';
}
if (false !== stripos($error, 'overflow')) {
return 'buffer_overflow';
}
return 'general_error';
}