Skip to content

Commit 075513c

Browse files
authored
Merge pull request #14 from infocyph/feature/improvement-25
Feature/improvement 25
2 parents 8b8d61e + 0671eef commit 075513c

32 files changed

Lines changed: 1356 additions & 232 deletions

.gitattributes

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1+
.github export-ignore
2+
benchmarks export-ignore
3+
docs export-ignore
4+
examples export-ignore
5+
tests export-ignore
6+
17
.editorconfig export-ignore
8+
.gitattributes export-ignore
29
.gitignore export-ignore
3-
tests export-ignore
4-
docs export-ignore
5-
.github export-ignore
610
.readthedocs.yaml export-ignore
711
captainhook.json export-ignore
12+
pest.xml export-ignore
13+
phpbench.json export-ignore
14+
phpcs.xml.dist export-ignore
15+
phpstan.neon.dist export-ignore
816
phpunit.xml export-ignore
917
pint.json export-ignore
10-
rector.php export-ignore
11-
.gitattributes export-ignore
1218
psalm.xml export-ignore
13-
pest.xml export-ignore
19+
rector.php export-ignore
1420

15-
* text eol=lf
21+
* text eol=lf
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
$command = 'composer audit --format=json --no-interaction --abandoned=report';
6+
7+
$descriptorSpec = [
8+
0 => ['pipe', 'r'],
9+
1 => ['pipe', 'w'],
10+
2 => ['pipe', 'w'],
11+
];
12+
13+
$process = proc_open($command, $descriptorSpec, $pipes);
14+
15+
if (! \is_resource($process)) {
16+
fwrite(STDERR, "Failed to start composer audit process.\n");
17+
exit(1);
18+
}
19+
20+
fclose($pipes[0]);
21+
$stdout = stream_get_contents($pipes[1]) ?: '';
22+
$stderr = stream_get_contents($pipes[2]) ?: '';
23+
fclose($pipes[1]);
24+
fclose($pipes[2]);
25+
26+
$exitCode = proc_close($process);
27+
28+
/** @var array<string,mixed>|null $decoded */
29+
$decoded = json_decode($stdout, true);
30+
31+
if (! \is_array($decoded)) {
32+
fwrite(STDERR, "Unable to parse composer audit JSON output.\n");
33+
if (trim($stdout) !== '') {
34+
fwrite(STDERR, $stdout . "\n");
35+
}
36+
if (trim($stderr) !== '') {
37+
fwrite(STDERR, $stderr . "\n");
38+
}
39+
40+
exit($exitCode !== 0 ? $exitCode : 1);
41+
}
42+
43+
$advisories = $decoded['advisories'] ?? [];
44+
$abandoned = $decoded['abandoned'] ?? [];
45+
46+
$advisoryCount = 0;
47+
48+
if (\is_array($advisories)) {
49+
foreach ($advisories as $entries) {
50+
if (\is_array($entries)) {
51+
$advisoryCount += \count($entries);
52+
}
53+
}
54+
}
55+
56+
$abandonedPackages = [];
57+
58+
if (\is_array($abandoned)) {
59+
foreach ($abandoned as $package => $replacement) {
60+
if (\is_string($package) && $package !== '') {
61+
$abandonedPackages[$package] = $replacement;
62+
}
63+
}
64+
}
65+
66+
echo sprintf(
67+
"Composer audit summary: %d advisories, %d abandoned packages.\n",
68+
$advisoryCount,
69+
\count($abandonedPackages),
70+
);
71+
72+
if ($abandonedPackages !== []) {
73+
fwrite(STDERR, "Warning: abandoned packages detected (non-blocking):\n");
74+
foreach ($abandonedPackages as $package => $replacement) {
75+
$target = \is_string($replacement) && $replacement !== '' ? $replacement : 'none';
76+
fwrite(STDERR, sprintf(" - %s (replacement: %s)\n", $package, $target));
77+
}
78+
}
79+
80+
if ($advisoryCount > 0) {
81+
fwrite(STDERR, "Security vulnerabilities detected by composer audit.\n");
82+
exit(1);
83+
}
84+
85+
exit(0);

.github/scripts/phpstan-sarif.php

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Convert PHPStan JSON output to SARIF 2.1.0 for GitHub Code Scanning upload.
7+
*
8+
* Usage:
9+
* php .github/scripts/phpstan-sarif.php <phpstan-json> [sarif-output]
10+
*/
11+
12+
$argv = $_SERVER['argv'] ?? [];
13+
$input = $argv[1] ?? '';
14+
$output = $argv[2] ?? 'phpstan-results.sarif';
15+
16+
if (! is_string($input) || $input === '') {
17+
fwrite(STDERR, "Error: missing input file.\n");
18+
fwrite(STDERR, "Usage: php .github/scripts/phpstan-sarif.php <phpstan-json> [sarif-output]\n");
19+
exit(2);
20+
}
21+
22+
if (! is_file($input) || ! is_readable($input)) {
23+
fwrite(STDERR, "Error: input file not found or unreadable: {$input}\n");
24+
exit(2);
25+
}
26+
27+
$raw = file_get_contents($input);
28+
if ($raw === false) {
29+
fwrite(STDERR, "Error: failed to read input file: {$input}\n");
30+
exit(2);
31+
}
32+
33+
$decoded = json_decode($raw, true);
34+
if (! is_array($decoded)) {
35+
fwrite(STDERR, "Error: input is not valid JSON.\n");
36+
exit(2);
37+
}
38+
39+
/**
40+
* @return non-empty-string
41+
*/
42+
function normalizeUri(string $path): string
43+
{
44+
$normalized = str_replace('\\', '/', $path);
45+
$cwd = getcwd();
46+
47+
if (is_string($cwd) && $cwd !== '') {
48+
$cwd = rtrim(str_replace('\\', '/', $cwd), '/');
49+
50+
if (preg_match('/^[A-Za-z]:\//', $normalized) === 1) {
51+
if (stripos($normalized, $cwd . '/') === 0) {
52+
$normalized = substr($normalized, strlen($cwd) + 1);
53+
}
54+
} elseif (str_starts_with($normalized, '/')) {
55+
if (str_starts_with($normalized, $cwd . '/')) {
56+
$normalized = substr($normalized, strlen($cwd) + 1);
57+
}
58+
}
59+
}
60+
61+
$normalized = ltrim($normalized, './');
62+
63+
return $normalized === '' ? 'unknown.php' : $normalized;
64+
}
65+
66+
$results = [];
67+
$rules = [];
68+
69+
$globalErrors = $decoded['errors'] ?? [];
70+
if (is_array($globalErrors)) {
71+
foreach ($globalErrors as $error) {
72+
if (! is_string($error) || $error === '') {
73+
continue;
74+
}
75+
76+
$ruleId = 'phpstan.internal';
77+
$rules[$ruleId] = true;
78+
$results[] = [
79+
'ruleId' => $ruleId,
80+
'level' => 'error',
81+
'message' => [
82+
'text' => $error,
83+
],
84+
];
85+
}
86+
}
87+
88+
$files = $decoded['files'] ?? [];
89+
if (is_array($files)) {
90+
foreach ($files as $filePath => $fileData) {
91+
if (! is_string($filePath) || ! is_array($fileData)) {
92+
continue;
93+
}
94+
95+
$messages = $fileData['messages'] ?? [];
96+
if (! is_array($messages)) {
97+
continue;
98+
}
99+
100+
foreach ($messages as $messageData) {
101+
if (! is_array($messageData)) {
102+
continue;
103+
}
104+
105+
$messageText = (string) ($messageData['message'] ?? 'PHPStan issue');
106+
$line = (int) ($messageData['line'] ?? 1);
107+
$identifier = (string) ($messageData['identifier'] ?? '');
108+
$ruleId = $identifier !== '' ? $identifier : 'phpstan.issue';
109+
110+
if ($line < 1) {
111+
$line = 1;
112+
}
113+
114+
$rules[$ruleId] = true;
115+
$results[] = [
116+
'ruleId' => $ruleId,
117+
'level' => 'error',
118+
'message' => [
119+
'text' => $messageText,
120+
],
121+
'locations' => [[
122+
'physicalLocation' => [
123+
'artifactLocation' => [
124+
'uri' => normalizeUri($filePath),
125+
],
126+
'region' => [
127+
'startLine' => $line,
128+
],
129+
],
130+
]],
131+
];
132+
}
133+
}
134+
}
135+
136+
$ruleDescriptors = [];
137+
$ruleIds = array_keys($rules);
138+
sort($ruleIds);
139+
140+
foreach ($ruleIds as $ruleId) {
141+
$ruleDescriptors[] = [
142+
'id' => $ruleId,
143+
'name' => $ruleId,
144+
'shortDescription' => [
145+
'text' => $ruleId,
146+
],
147+
];
148+
}
149+
150+
$sarif = [
151+
'$schema' => 'https://json.schemastore.org/sarif-2.1.0.json',
152+
'version' => '2.1.0',
153+
'runs' => [[
154+
'tool' => [
155+
'driver' => [
156+
'name' => 'PHPStan',
157+
'informationUri' => 'https://phpstan.org/',
158+
'rules' => $ruleDescriptors,
159+
],
160+
],
161+
'results' => $results,
162+
]],
163+
];
164+
165+
$encoded = json_encode($sarif, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
166+
if (! is_string($encoded)) {
167+
fwrite(STDERR, "Error: failed to encode SARIF JSON.\n");
168+
exit(2);
169+
}
170+
171+
$written = file_put_contents($output, $encoded . PHP_EOL);
172+
if ($written === false) {
173+
fwrite(STDERR, "Error: failed to write output file: {$output}\n");
174+
exit(2);
175+
}
176+
177+
fwrite(STDOUT, sprintf("SARIF generated: %s (%d findings)\n", $output, count($results)));
178+
exit(0);

0 commit comments

Comments
 (0)