inc/code-snapshot.php
Snapshot 2026.09.17-a2894de (current) · 130 lines · 8,508 bytes · tree · plain text
SHA-256
3022420d25260e3db2389fd3a8cb8805d38f9b721982853fd9b0e0d5a2b37d52| 1 | <?php |
| 2 | /* WEB-001: the snapshot behind the Code tab. Run at deploy, on the command line, |
| 3 | from the application root's inc/ folder. Copies the allowlisted tree into |
| 4 | inc/snapshots/<version>/, redacts e-mail addresses that are not the company's, |
| 5 | refuses to publish when a secret-like string is found, writes the manifest with |
| 6 | a SHA-256 per file and the manifest's own checksum, and points current.json at |
| 7 | the new version. Older versions stay. Exit 0 published or unchanged, 2 refused. |
| 8 | Installed to inc/code-snapshot.php by the AI platform's deploy hook; source |
| 9 | deploy/research/code-snapshot.php in districthive/ai. Shown in the tree. */ |
| 10 | declare(strict_types=1); |
| 11 | if (PHP_SAPI !== 'cli') { http_response_code(404); exit; } |
| 12 | $root = dirname(__DIR__); |
| 13 | $rules = require __DIR__ . '/code-allowlist.php'; |
| 14 | $snapRoot = __DIR__ . '/snapshots'; |
| 15 | |
| 16 | $never = static function (string $rel) use ($rules): bool { |
| 17 | foreach ($rules['never'] as $n) { if ($rel === $n || str_starts_with($rel, rtrim($n, '/') . '/')) { return true; } } |
| 18 | return false; |
| 19 | }; |
| 20 | $skip = static function (string $name) use ($rules): bool { |
| 21 | foreach ($rules['skip_patterns'] as $re) { if (preg_match($re, $name)) { return true; } } |
| 22 | return false; |
| 23 | }; |
| 24 | |
| 25 | /* 1. Collect */ |
| 26 | $files = []; |
| 27 | $add = static function (string $rel) use (&$files, $root, $rules, $never, $skip): void { |
| 28 | if ($never($rel) || $skip(basename($rel))) { return; } |
| 29 | $ext = strtolower(pathinfo($rel, PATHINFO_EXTENSION)); |
| 30 | if (!in_array($ext, $rules['extensions'], true)) { return; } |
| 31 | $abs = $root . '/' . $rel; |
| 32 | if (!is_file($abs) || is_link($abs)) { return; } |
| 33 | if (filesize($abs) > $rules['max_file_bytes']) { fwrite(STDERR, "skip (too large): $rel\n"); return; } |
| 34 | $files[$rel] = $abs; |
| 35 | }; |
| 36 | foreach ($rules['include'] as $entry) { |
| 37 | $abs = $root . '/' . $entry; |
| 38 | if (is_file($abs)) { $add($entry); continue; } |
| 39 | if (!is_dir($abs)) { continue; } |
| 40 | $it = new RecursiveIteratorIterator(new RecursiveCallbackFilterIterator(new RecursiveDirectoryIterator($abs, FilesystemIterator::SKIP_DOTS), static function ($cur) use ($root, $never, $skip) { |
| 41 | $rel = substr($cur->getPathname(), strlen($root) + 1); |
| 42 | return !$never($rel) && !$skip($cur->getFilename()); |
| 43 | })); |
| 44 | foreach ($it as $f) { $add(substr($f->getPathname(), strlen($root) + 1)); } |
| 45 | } |
| 46 | ksort($files); |
| 47 | if ($files === []) { fwrite(STDERR, "nothing to snapshot\n"); exit(2); } |
| 48 | |
| 49 | /* 2. Read, redact, scan */ |
| 50 | $contents = []; $redacted = 0; $findings = []; |
| 51 | /* The configuration's own secret values, read here and compared, never copied. */ |
| 52 | $secretValues = []; |
| 53 | if (is_file("$root/config.php") && !empty($rules['config_constant_pattern'])) { |
| 54 | $before = get_defined_constants(true)['user'] ?? []; |
| 55 | try { require_once "$root/config.php"; } catch (Throwable $e) {} |
| 56 | foreach (array_diff_key(get_defined_constants(true)['user'] ?? [], $before) as $name => $val) { |
| 57 | if (is_string($val) && strlen($val) >= 6 && preg_match($rules['config_constant_pattern'], $name) && !in_array(strtolower($val), ['localhost', '127.0.0.1', 'to fill', 'change me'], true)) { $secretValues[$name] = $val; } |
| 58 | } |
| 59 | } |
| 60 | $secretRe = '/\b(pass(word|wd)?|secret|api[_-]?key|apikey|token|private[_-]?key|client[_-]?secret|auth)\b\s*(=>|=|:)\s*[\'"]([^\'"]{6,})[\'"]/i'; |
| 61 | foreach ($files as $rel => $abs) { |
| 62 | $c = (string) file_get_contents($abs); |
| 63 | $c = preg_replace_callback('/[A-Za-z0-9._%+-]+@([A-Za-z0-9.-]+\.[A-Za-z]{2,})/', static function ($m) use ($rules, &$redacted) { |
| 64 | foreach ($rules['allowed_email_domains'] as $d) { if (strcasecmp($m[1], $d) === 0) { return $m[0]; } } |
| 65 | $redacted++; return '[e-mail redacted]'; |
| 66 | }, $c); |
| 67 | if ($rel !== 'inc/code-allowlist.php') { foreach ($rules['forbidden_strings'] as $s) { if (stripos($c, $s) !== false) { $findings[] = "$rel: forbidden string"; } } } // the rules file names the patterns it forbids |
| 68 | foreach ($secretValues as $name => $val) { if (str_contains($c, $val)) { $findings[] = "$rel: contains the value of $name"; } } |
| 69 | if (preg_match_all($secretRe, $c, $mm, PREG_SET_ORDER)) { |
| 70 | foreach ($mm as $m) { |
| 71 | $v = $m[4]; |
| 72 | if (preg_match('/^(TO FILL|CHANGE ME|your[-_ ]|xxx|\.\.\.|<|\$|\{|\.)/i', $v) || preg_match('/^[A-Za-z_][A-Za-z0-9_]*[,;]?$/', $v)) { continue; } // template placeholders, concatenated constants, bare identifiers |
| 73 | $findings[] = "$rel: secret-like assignment (" . $m[1] . ')'; |
| 74 | } |
| 75 | } |
| 76 | if (preg_match('/\b[0-9a-f]{40,}\b|\b[A-Za-z0-9+\/]{48,}={0,2}\b/', $c) && !in_array(pathinfo($rel, PATHINFO_EXTENSION), ['md'], true)) { |
| 77 | // long hex or base64 runs: keys look like this; hashes in documentation are allowed |
| 78 | preg_match('/\b[0-9a-f]{40,}\b|\b[A-Za-z0-9+\/]{48,}={0,2}\b/', $c, $hm); |
| 79 | if (!preg_match('/^[0-9a-f]{64}$/', $hm[0]) || stripos($c, 'sha256') === false) { $findings[] = "$rel: long key-like string"; } |
| 80 | } |
| 81 | if (preg_match('/mysql:\/\/[^\s\'"]+:[^\s\'"]+@/i', $c)) { $findings[] = "$rel: connection string with credentials"; } |
| 82 | $contents[$rel] = $c; |
| 83 | } |
| 84 | if ($findings !== []) { |
| 85 | fwrite(STDERR, "REFUSED: the snapshot was not published. Remove these before the next deploy:\n " . implode("\n ", array_unique($findings)) . "\n"); |
| 86 | exit(2); |
| 87 | } |
| 88 | |
| 89 | /* 3. Manifest */ |
| 90 | $entries = []; |
| 91 | foreach ($contents as $rel => $c) { |
| 92 | $e = ['path' => $rel, 'bytes' => strlen($c), 'sha256' => hash('sha256', $c), 'lines' => substr_count($c, "\n") + 1]; |
| 93 | if (str_ends_with($rel, '.php')) { |
| 94 | $fns = []; $lines = explode("\n", $c); $starts = []; |
| 95 | foreach ($lines as $i => $l) { if (preg_match('/^\s*(?:public |private |protected |static )*function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/', $l, $m)) { $starts[] = [$m[1], $i + 1]; } } |
| 96 | foreach ($starts as $k => [$name, $start]) { $fns[$name] = [$start, isset($starts[$k + 1]) ? $starts[$k + 1][1] - 1 : count($lines)]; } |
| 97 | if ($fns !== []) { $e['functions'] = $fns; } |
| 98 | } |
| 99 | $entries[] = $e; |
| 100 | } |
| 101 | $treeHash = hash('sha256', implode("\n", array_map(static fn ($e) => $e['path'] . ':' . $e['sha256'], $entries))); |
| 102 | $version = gmdate('Y.m.d') . '-' . substr($treeHash, 0, 7); |
| 103 | $current = is_file("$snapRoot/current.json") ? json_decode((string) file_get_contents("$snapRoot/current.json"), true) : null; |
| 104 | if (is_array($current) && ($current['tree_sha256'] ?? '') === $treeHash && is_dir("$snapRoot/" . ($current['version'] ?? ''))) { |
| 105 | echo "code snapshot unchanged: " . $current['version'] . " (" . count($entries) . " files)\n"; |
| 106 | exit(0); |
| 107 | } |
| 108 | |
| 109 | /* 4. Write (into a temporary folder, then rename) */ |
| 110 | if (!is_dir($snapRoot) && !mkdir($snapRoot, 0755, true)) { fwrite(STDERR, "cannot create $snapRoot\n"); exit(2); } |
| 111 | file_put_contents("$snapRoot/.htaccess", "Require all denied\n"); |
| 112 | $tmp = "$snapRoot/.tmp-" . bin2hex(random_bytes(4)); |
| 113 | mkdir($tmp, 0755, true); |
| 114 | foreach ($contents as $rel => $c) { |
| 115 | $dst = "$tmp/$rel"; |
| 116 | if (!is_dir(dirname($dst))) { mkdir(dirname($dst), 0755, true); } |
| 117 | file_put_contents($dst, $c); |
| 118 | } |
| 119 | $manifest = ['version' => $version, 'generated_at' => gmdate('c'), 'application' => 'Districthive Research Labs', 'root' => 'application root (paths relative)', 'commit' => null, |
| 120 | 'redaction' => ['ran' => true, 'emails_redacted' => $redacted, 'secret_scan' => 'passed', 'rules' => 'inc/code-allowlist.php'], |
| 121 | 'tree_sha256' => $treeHash, 'files' => $entries]; |
| 122 | $manifest['manifest_sha256'] = hash('sha256', json_encode($manifest['files'], JSON_UNESCAPED_SLASHES)); |
| 123 | file_put_contents("$tmp/code-manifest.json", json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); |
| 124 | if (is_dir("$snapRoot/$version")) { rename("$snapRoot/$version", "$snapRoot/.old-$version-" . time()); } |
| 125 | rename($tmp, "$snapRoot/$version"); |
| 126 | file_put_contents("$snapRoot/current.json", json_encode(['version' => $version, 'generated_at' => $manifest['generated_at'], 'tree_sha256' => $treeHash, 'manifest_sha256' => $manifest['manifest_sha256']], JSON_PRETTY_PRINT)); |
| 127 | foreach (glob("$snapRoot/.old-*") ?: [] as $old) { exec('rm -rf ' . escapeshellarg($old)); } |
| 128 | try { if (is_file(__DIR__ . '/db.php')) { define('RL_CLI', 1); require_once __DIR__ . '/db.php'; log_activity('system', 'code_snapshot', $version . ': ' . count($entries) . ' files, manifest sha256 ' . $manifest['manifest_sha256'] . ($redacted ? ", $redacted e-mail address(es) redacted" : '')); } } catch (Throwable $e) {} |
| 129 | echo "code snapshot published: $version (" . count($entries) . " files, manifest " . substr($manifest['manifest_sha256'], 0, 12) . ")\n"; |
| 130 | |
Lines can be cited as inc/code-snapshot.php L120-L140, platform code 2026.09.17-a2894de; add #L120-L140 to this page's address to highlight them. Source shown for scrutiny of the research platform. All rights reserved. Not offered as installable software.