Repository navigation
Expand file tree
/
Copy pathrequirements-check.php
More file actions
executable file
·365 lines (344 loc) · 15.7 KB
/
Copy pathrequirements-check.php
File metadata and controls
executable file
·365 lines (344 loc) · 15.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
#!/usr/bin/env php
<?php
/**
* PHPNetMap — standalone server requirements checker.
*
* Run from the console on the machine that will host PHPNetMap, before
* (or after) a manual/standalone install (no Docker):
*
* php requirements-check.php
*
* It only reads the environment — PHP version/extensions/ini, filesystem
* permissions under this project, and (best-effort) the local Apache
* binary — it does not modify anything. Checks mirror exactly what the
* project's own Dockerfile installs/configures, so a server that passes
* this script matches what actually ships in the Docker image. See
* STANDALONE_INSTALLATION_GUIDE.md for how to fix anything reported here.
*
* Exit code: 0 if every required check passes (warnings are still shown
* but don't fail the run), non-zero if any required check fails — safe to
* use in a provisioning script, e.g. `php requirements-check.php || exit 1`.
*/
if (PHP_SAPI !== 'cli') {
fwrite(STDERR, "This script is meant to be run from the command line: php requirements-check.php\n");
exit(1);
}
$root = __DIR__;
$results = array(); // each: array('label', 'status' => pass|warn|fail, 'detail')
function pnm_check($label, $status, $detail = '') {
global $results;
$results[] = array('label' => $label, 'status' => $status, 'detail' => $detail);
}
// Plain ASCII by default (safest over SSH/odd terminals); pass --no-color to
// force it off even when a color-capable TTY is detected.
$useColor = function_exists('posix_isatty') && @posix_isatty(STDOUT) && !in_array('--no-color', $argv, true);
function pnm_color($text, $code, $useColor) {
return $useColor ? "\033[" . $code . "m" . $text . "\033[0m" : $text;
}
// ---------------------------------------------------------------------
// 1. PHP version — the Docker image is php:8.4-apache (>= 8.4.27, the first
// 8.4 release fixed for CVE-2026-109618 / CVE-2026-109619).
// ---------------------------------------------------------------------
$phpVersion = PHP_VERSION;
if (version_compare($phpVersion, '8.4.0', '<')) {
pnm_check(
'PHP version',
'fail',
"$phpVersion — unsupported: PHP 8.4 (>= 8.4.27) is what the Docker image (and this checklist) targets. "
. "Older versions are vulnerable to CVE-2026-109618 / CVE-2026-109619."
);
} elseif (version_compare($phpVersion, '8.4.27', '<')) {
// Distro packages (Debian/Ubuntu) backport security fixes without
// bumping PHP_VERSION, so a lower version is not proof of being vulnerable.
pnm_check(
'PHP version',
'warn',
"$phpVersion — upstream PHP below 8.4.27 is vulnerable to CVE-2026-109618 / CVE-2026-109619. "
. "If this is a distro package, check its changelog for the backported fix; otherwise update PHP."
);
} elseif (version_compare($phpVersion, '8.5.0', '>=')) {
pnm_check(
'PHP version',
'warn',
"$phpVersion — this app (Yii 1.1.32) supports up to PHP 8.4; PHP 8.5+ is untested here."
);
} else {
pnm_check('PHP version', 'pass', $phpVersion);
}
// ---------------------------------------------------------------------
// 2. Required PHP extensions — exactly what the Dockerfile installs
// (docker-php-ext-install snmp pdo_sqlite; pecl install apcu), plus
// the couple of core ones the app code itself calls directly.
// ---------------------------------------------------------------------
$requiredExtensions = array(
'pdo' => 'base PDO layer required by pdo_sqlite',
'pdo_sqlite' => 'stores all application data (hosts, connections, vlans, SNMP templates, host faces, config) in protected/data/phpnetmap.db',
'snmp' => 'queries network devices via SNMP v1/v2c/v3 — the core feature of this app (protected/components/PNMSnmp.php)',
'json' => 'used throughout the AJAX endpoints (port info/status/traffic, CAM/ARP lookups, etc.)',
'session' => "backs Yii's user/session component (protected/config/main.php 'user' => array('allowAutoLogin' => true))",
);
foreach ($requiredExtensions as $ext => $why) {
if (extension_loaded($ext)) {
pnm_check("Extension: $ext", 'pass', $why);
} else {
pnm_check("Extension: $ext", 'fail', "not loaded — $why");
}
}
// apcu gets its own check: protected/components/CacheAPC.php throws an
// Exception at construction time if apc.enabled is off, even when the
// extension is present — and caching is on by default (see "cache" in
// protected/data/params.ini, editable from the Configuration page).
if (extension_loaded('apcu')) {
if (ini_get('apc.enabled')) {
pnm_check('Extension: apcu', 'pass', 'caches SNMP results (CAM/ARP tables, GET responses) per the TTLs on the Configuration page');
} else {
pnm_check(
'Extension: apcu',
'warn',
'loaded but apc.enabled is Off in php.ini — CacheAPC will throw an exception as soon as the app tries to '
. 'cache anything, unless you disable "Cache" on the Configuration page. Set apc.enabled=1.'
);
}
} else {
pnm_check(
'Extension: apcu',
'warn',
'not installed — caching is enabled by default (Configuration page), and will throw an exception until you '
. 'either install apcu or turn "Cache" off there.'
);
}
// ---------------------------------------------------------------------
// php.ini settings — the app never calls ini_set() for any of these
// (see index.php), so it's entirely at the mercy of the server's own
// php.ini. None of these are "the app won't start" failures, so they're
// warnings, not hard fails.
// ---------------------------------------------------------------------
function pnm_ini_bytes($value) {
$value = trim((string) $value);
if ($value === '' || $value === '-1') {
return -1; // unlimited
}
$unit = strtolower(substr($value, -1));
$number = (float) $value;
switch ($unit) {
case 'g': return (int) ($number * 1024 * 1024 * 1024);
case 'm': return (int) ($number * 1024 * 1024);
case 'k': return (int) ($number * 1024);
default: return (int) $number;
}
}
$displayErrors = ini_get('display_errors');
if ($displayErrors && strtolower($displayErrors) !== 'off' && $displayErrors !== '0') {
pnm_check(
'php.ini: display_errors',
'warn',
"currently On — a stray PHP warning/notice would get printed straight into an AJAX response (port info/status/"
. "traffic, etc.) and break it client-side (invalid JSON). Set display_errors=Off and log_errors=On in production."
);
} else {
pnm_check('php.ini: display_errors', 'pass', 'Off, as recommended for production');
}
// The Host Face editor lets you upload an equipment photo (stored as a
// base64 data URI in the SVG) — both limits need to cover the photo, and
// post_max_size must be >= upload_max_filesize or uploads silently fail.
$uploadMax = pnm_ini_bytes(ini_get('upload_max_filesize'));
$postMax = pnm_ini_bytes(ini_get('post_max_size'));
$minRecommendedBytes = 8 * 1024 * 1024; // 8M
if ($uploadMax !== -1 && $uploadMax < $minRecommendedBytes) {
pnm_check(
'php.ini: upload_max_filesize',
'warn',
ini_get('upload_max_filesize') . ' — a decent-resolution equipment photo (Host Face editor "Upload image") '
. 'can exceed this. Recommend at least 8M.'
);
} else {
pnm_check('php.ini: upload_max_filesize', 'pass', ini_get('upload_max_filesize'));
}
if ($postMax !== -1 && $uploadMax !== -1 && $postMax < $uploadMax) {
pnm_check(
'php.ini: post_max_size',
'warn',
ini_get('post_max_size') . " is smaller than upload_max_filesize (" . ini_get('upload_max_filesize')
. ') — uploads will fail silently. post_max_size must be >= upload_max_filesize.'
);
} else {
pnm_check('php.ini: post_max_size', 'pass', ini_get('post_max_size'));
}
$memoryLimit = pnm_ini_bytes(ini_get('memory_limit'));
$minRecommendedMemory = 128 * 1024 * 1024; // 128M
if ($memoryLimit !== -1 && $memoryLimit < $minRecommendedMemory) {
pnm_check(
'php.ini: memory_limit',
'warn',
ini_get('memory_limit') . ' — a full SNMP walk (CAM/ARP table) on a switch with thousands of entries can use '
. 'more than this. Recommend at least 128M.'
);
} else {
pnm_check('php.ini: memory_limit', 'pass', ini_get('memory_limit'));
}
$timezone = ini_get('date.timezone');
if ($timezone === '' || $timezone === false) {
pnm_check(
'php.ini: date.timezone',
'warn',
'not set — PHP silently falls back to UTC, which is fine functionally but means every timestamp shown in the '
. 'UI is UTC regardless of where this server is. Set date.timezone to your local zone (e.g. America/Sao_Paulo) if that matters to you.'
);
} else {
pnm_check('php.ini: date.timezone', 'pass', $timezone);
}
// ---------------------------------------------------------------------
// 3. Writable directories — same paths the Dockerfile chown's to
// www-data (mkdir -p protected/data protected/runtime assets).
// ---------------------------------------------------------------------
$writableDirs = array(
'protected/data' => 'the SQLite database (phpnetmap.db) and params.ini (persistent Configuration-page settings) live here',
'protected/runtime' => "Yii's own runtime cache/log directory",
'assets' => 'Yii publishes CSS/JS assets here at runtime',
);
foreach ($writableDirs as $dir => $why) {
$path = $root . '/' . $dir;
if (!is_dir($path)) {
pnm_check(
"Writable: $dir",
'fail',
"directory does not exist — create it (mkdir -p $dir) and make it writable by the user Apache runs as. $why"
);
} elseif (!is_writable($path)) {
pnm_check(
"Writable: $dir",
'fail',
"exists but is not writable by the user running this check — chown/chmod it for the Apache user. $why"
);
} else {
pnm_check("Writable: $dir", 'pass', $why);
}
}
// ---------------------------------------------------------------------
// 4. .htpasswd — used for Apache Basic Auth (.htaccess) when Authentication
// Mode is set to "HTTP Basic Auth (.htpasswd, legacy)" on the
// Configuration screen (/config/index). Since this is now optional —
// the default is the app's own Yii login instead — an empty/missing
// file is no longer a warning by itself; it only matters if htpasswd
// mode is actually selected.
// ---------------------------------------------------------------------
$htpasswdPath = $root . '/.htpasswd';
if (file_exists($htpasswdPath) && filesize($htpasswdPath) > 0) {
pnm_check('.htpasswd', 'pass', 'present and non-empty — ready if Authentication Mode is switched to "HTTP Basic Auth (.htpasswd)"');
} elseif (file_exists($htpasswdPath)) {
pnm_check(
'.htpasswd',
'pass',
'exists but is empty — fine under the default Yii login mode. Only create an entry '
. '(htpasswd -c ' . $htpasswdPath . ' admin) if you plan to switch Authentication Mode to "HTTP Basic Auth" on the Configuration screen.'
);
} else {
pnm_check(
'.htpasswd',
'pass',
'not found at project root — fine under the default Yii login mode. Only needed if you switch Authentication Mode to '
. '"HTTP Basic Auth" on the Configuration screen; create it with: htpasswd -c ' . $htpasswdPath . ' admin'
);
}
// ---------------------------------------------------------------------
// 5. Apache modules — best-effort only: apache_get_modules() only works
// when PHP itself runs as the Apache SAPI, not from the CLI, so this
// shells out to apache2ctl/httpd -M if one is on PATH. Never fatal.
// ---------------------------------------------------------------------
$apacheBinary = null;
foreach (array('apache2ctl', 'apachectl', 'httpd') as $bin) {
$which = trim((string) @shell_exec('command -v ' . escapeshellarg($bin) . ' 2>/dev/null'));
if ($which !== '') {
$apacheBinary = $which;
break;
}
}
if ($apacheBinary === null) {
pnm_check(
'Apache modules',
'warn',
'no apache2ctl/apachectl/httpd found on PATH — could not check automatically. This app needs mod_rewrite '
. '(clean URLs) and mod_authn_file + mod_auth_basic (the .htpasswd login) enabled, and AllowOverride All '
. 'on the vhost so .htaccess is honored. Verify manually if this server does run Apache elsewhere.'
);
} else {
$modulesOutput = (string) @shell_exec(escapeshellarg($apacheBinary) . ' -M 2>/dev/null');
if ($modulesOutput === '') {
pnm_check(
'Apache modules',
'warn',
"found $apacheBinary but couldn't list modules (permissions? not the same Apache serving this app?) — "
. 'verify manually that mod_rewrite and mod_authn_file + mod_auth_basic are enabled.'
);
} else {
$requiredModules = array(
'rewrite_module' => 'mod_rewrite — clean URLs (.htaccess RewriteRule)',
'authn_file_module' => 'mod_authn_file — reads .htpasswd',
'authz_user_module' => 'mod_authz_user — the "Require valid-user" directive in .htaccess',
'version_module' => 'mod_version — the <If> directive in .htaccess that carves out /mcp from Basic Auth for the MCP server',
);
foreach ($requiredModules as $needle => $why) {
if (strpos($modulesOutput, $needle) !== false) {
pnm_check("Apache: $needle", 'pass', $why);
} else {
pnm_check("Apache: $needle", 'fail', "not enabled — $why. Enable with: a2enmod " . str_replace('_module', '', $needle));
}
}
}
}
// ---------------------------------------------------------------------
// 6. Files that should never reach production — apc.php (bundled APCu
// cache inspector, unauthenticated by default) and index-test.php (a
// debug-mode entry point, its own header says as much). The Docker
// image excludes both via .dockerignore; a standalone checkout doesn't
// get that for free.
// ---------------------------------------------------------------------
foreach (array('apc.php', 'index-test.php') as $riskyFile) {
$riskyPath = $root . '/' . $riskyFile;
if (file_exists($riskyPath)) {
pnm_check(
$riskyFile,
'fail',
'present at the project root — remove it before going to production: rm -f ' . $riskyPath
. ' (see STANDALONE_INSTALLATION_GUIDE.md)'
);
} else {
pnm_check($riskyFile, 'pass', 'correctly absent');
}
}
// ---------------------------------------------------------------------
// Report
// ---------------------------------------------------------------------
$labelWidth = 0;
foreach ($results as $r) {
$labelWidth = max($labelWidth, strlen($r['label']));
}
echo "\nPHPNetMap — standalone server requirements check\n";
echo str_repeat('=', 50) . "\n\n";
$counts = array('pass' => 0, 'warn' => 0, 'fail' => 0);
foreach ($results as $r) {
$counts[$r['status']]++;
$badge = array(
'pass' => pnm_color('PASS', '32', $useColor),
'warn' => pnm_color('WARN', '33', $useColor),
'fail' => pnm_color('FAIL', '31', $useColor),
);
printf("[%s] %-" . $labelWidth . "s %s\n", $badge[$r['status']], $r['label'], $r['detail']);
}
echo "\n" . str_repeat('-', 50) . "\n";
printf(
"%d passed, %d warning(s), %d failed\n",
$counts['pass'],
$counts['warn'],
$counts['fail']
);
if ($counts['fail'] > 0) {
echo "\nFix the FAIL items above before running PHPNetMap — see STANDALONE_INSTALLATION_GUIDE.md.\n";
exit(1);
}
if ($counts['warn'] > 0) {
echo "\nNo blocking issues, but review the WARN items above.\n";
exit(0);
}
echo "\nEverything looks good.\n";
exit(0);