/, redacts e-mail addresses that are not the company's, refuses to publish when a secret-like string is found, writes the manifest with a SHA-256 per file and the manifest's own checksum, and points current.json at the new version. Older versions stay. Exit 0 published or unchanged, 2 refused. Installed to inc/code-snapshot.php by the AI platform's deploy hook; source deploy/research/code-snapshot.php in districthive/ai. Shown in the tree. */ declare(strict_types=1); if (PHP_SAPI !== 'cli') { http_response_code(404); exit; } $root = dirname(__DIR__); $rules = require __DIR__ . '/code-allowlist.php'; $snapRoot = __DIR__ . '/snapshots'; $never = static function (string $rel) use ($rules): bool { foreach ($rules['never'] as $n) { if ($rel === $n || str_starts_with($rel, rtrim($n, '/') . '/')) { return true; } } return false; }; $skip = static function (string $name) use ($rules): bool { foreach ($rules['skip_patterns'] as $re) { if (preg_match($re, $name)) { return true; } } return false; }; /* 1. Collect */ $files = []; $add = static function (string $rel) use (&$files, $root, $rules, $never, $skip): void { if ($never($rel) || $skip(basename($rel))) { return; } $ext = strtolower(pathinfo($rel, PATHINFO_EXTENSION)); if (!in_array($ext, $rules['extensions'], true)) { return; } $abs = $root . '/' . $rel; if (!is_file($abs) || is_link($abs)) { return; } if (filesize($abs) > $rules['max_file_bytes']) { fwrite(STDERR, "skip (too large): $rel\n"); return; } $files[$rel] = $abs; }; foreach ($rules['include'] as $entry) { $abs = $root . '/' . $entry; if (is_file($abs)) { $add($entry); continue; } if (!is_dir($abs)) { continue; } $it = new RecursiveIteratorIterator(new RecursiveCallbackFilterIterator(new RecursiveDirectoryIterator($abs, FilesystemIterator::SKIP_DOTS), static function ($cur) use ($root, $never, $skip) { $rel = substr($cur->getPathname(), strlen($root) + 1); return !$never($rel) && !$skip($cur->getFilename()); })); foreach ($it as $f) { $add(substr($f->getPathname(), strlen($root) + 1)); } } ksort($files); if ($files === []) { fwrite(STDERR, "nothing to snapshot\n"); exit(2); } /* 2. Read, redact, scan */ $contents = []; $redacted = 0; $findings = []; /* The configuration's own secret values, read here and compared, never copied. */ $secretValues = []; if (is_file("$root/config.php") && !empty($rules['config_constant_pattern'])) { $before = get_defined_constants(true)['user'] ?? []; try { require_once "$root/config.php"; } catch (Throwable $e) {} foreach (array_diff_key(get_defined_constants(true)['user'] ?? [], $before) as $name => $val) { 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; } } } $secretRe = '/\b(pass(word|wd)?|secret|api[_-]?key|apikey|token|private[_-]?key|client[_-]?secret|auth)\b\s*(=>|=|:)\s*[\'"]([^\'"]{6,})[\'"]/i'; foreach ($files as $rel => $abs) { $c = (string) file_get_contents($abs); $c = preg_replace_callback('/[A-Za-z0-9._%+-]+@([A-Za-z0-9.-]+\.[A-Za-z]{2,})/', static function ($m) use ($rules, &$redacted) { foreach ($rules['allowed_email_domains'] as $d) { if (strcasecmp($m[1], $d) === 0) { return $m[0]; } } $redacted++; return '[e-mail redacted]'; }, $c); 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 foreach ($secretValues as $name => $val) { if (str_contains($c, $val)) { $findings[] = "$rel: contains the value of $name"; } } if (preg_match_all($secretRe, $c, $mm, PREG_SET_ORDER)) { foreach ($mm as $m) { $v = $m[4]; 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 $findings[] = "$rel: secret-like assignment (" . $m[1] . ')'; } } 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)) { // long hex or base64 runs: keys look like this; hashes in documentation are allowed preg_match('/\b[0-9a-f]{40,}\b|\b[A-Za-z0-9+\/]{48,}={0,2}\b/', $c, $hm); if (!preg_match('/^[0-9a-f]{64}$/', $hm[0]) || stripos($c, 'sha256') === false) { $findings[] = "$rel: long key-like string"; } } if (preg_match('/mysql:\/\/[^\s\'"]+:[^\s\'"]+@/i', $c)) { $findings[] = "$rel: connection string with credentials"; } $contents[$rel] = $c; } if ($findings !== []) { fwrite(STDERR, "REFUSED: the snapshot was not published. Remove these before the next deploy:\n " . implode("\n ", array_unique($findings)) . "\n"); exit(2); } /* 3. Manifest */ $entries = []; foreach ($contents as $rel => $c) { $e = ['path' => $rel, 'bytes' => strlen($c), 'sha256' => hash('sha256', $c), 'lines' => substr_count($c, "\n") + 1]; if (str_ends_with($rel, '.php')) { $fns = []; $lines = explode("\n", $c); $starts = []; 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]; } } foreach ($starts as $k => [$name, $start]) { $fns[$name] = [$start, isset($starts[$k + 1]) ? $starts[$k + 1][1] - 1 : count($lines)]; } if ($fns !== []) { $e['functions'] = $fns; } } $entries[] = $e; } $treeHash = hash('sha256', implode("\n", array_map(static fn ($e) => $e['path'] . ':' . $e['sha256'], $entries))); $version = gmdate('Y.m.d') . '-' . substr($treeHash, 0, 7); $current = is_file("$snapRoot/current.json") ? json_decode((string) file_get_contents("$snapRoot/current.json"), true) : null; if (is_array($current) && ($current['tree_sha256'] ?? '') === $treeHash && is_dir("$snapRoot/" . ($current['version'] ?? ''))) { echo "code snapshot unchanged: " . $current['version'] . " (" . count($entries) . " files)\n"; exit(0); } /* 4. Write (into a temporary folder, then rename) */ if (!is_dir($snapRoot) && !mkdir($snapRoot, 0755, true)) { fwrite(STDERR, "cannot create $snapRoot\n"); exit(2); } file_put_contents("$snapRoot/.htaccess", "Require all denied\n"); $tmp = "$snapRoot/.tmp-" . bin2hex(random_bytes(4)); mkdir($tmp, 0755, true); foreach ($contents as $rel => $c) { $dst = "$tmp/$rel"; if (!is_dir(dirname($dst))) { mkdir(dirname($dst), 0755, true); } file_put_contents($dst, $c); } $manifest = ['version' => $version, 'generated_at' => gmdate('c'), 'application' => 'Districthive Research Labs', 'root' => 'application root (paths relative)', 'commit' => null, 'redaction' => ['ran' => true, 'emails_redacted' => $redacted, 'secret_scan' => 'passed', 'rules' => 'inc/code-allowlist.php'], 'tree_sha256' => $treeHash, 'files' => $entries]; $manifest['manifest_sha256'] = hash('sha256', json_encode($manifest['files'], JSON_UNESCAPED_SLASHES)); file_put_contents("$tmp/code-manifest.json", json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); if (is_dir("$snapRoot/$version")) { rename("$snapRoot/$version", "$snapRoot/.old-$version-" . time()); } rename($tmp, "$snapRoot/$version"); 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)); foreach (glob("$snapRoot/.old-*") ?: [] as $old) { exec('rm -rf ' . escapeshellarg($old)); } 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) {} echo "code snapshot published: $version (" . count($entries) . " files, manifest " . substr($manifest['manifest_sha256'], 0, 12) . ")\n";