Research Labs

2026-09-17 20:36 UTCopen field data · live

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. */
10declare(strict_types=1);
11if (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};
36foreach ($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}
46ksort($files);
47if ($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 = [];
53if (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';
61foreach ($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}
84if ($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 = [];
91foreach ($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;
104if (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) */
110if (!is_dir($snapRoot) && !mkdir($snapRoot, 0755, true)) { fwrite(STDERR, "cannot create $snapRoot\n"); exit(2); }
111file_put_contents("$snapRoot/.htaccess", "Require all denied\n");
112$tmp = "$snapRoot/.tmp-" . bin2hex(random_bytes(4));
113mkdir($tmp, 0755, true);
114foreach ($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));
123file_put_contents("$tmp/code-manifest.json", json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
124if (is_dir("$snapRoot/$version")) { rename("$snapRoot/$version", "$snapRoot/.old-$version-" . time()); }
125rename($tmp, "$snapRoot/$version");
126file_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));
127foreach (glob("$snapRoot/.old-*") ?: [] as $old) { exec('rm -rf ' . escapeshellarg($old)); }
128try { 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) {}
129echo "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.