From 4c76eeb73700a9c5e19cdd5f07cd5036c5d8ea95 Mon Sep 17 00:00:00 2001 From: "Beau Beauchamp, WebTigers" Date: Sat, 24 Oct 2026 09:10:44 -0400 Subject: [PATCH] =?UTF-8?q?TIGER-382:=20CI=20guard=20=E2=80=94=20forbid=20?= =?UTF-8?q?a=20plaintext=20literal=20through=20Tiger=5FSecret::resolve()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tiger_Secret::resolve() returns an unrecognized value unchanged as plaintext (the adoption-friendly passthrough). This token-based unit guard makes the convention airtight: a STRING-LITERAL argument to resolve() must be a recognized scheme ref (ssm:/env:/file:/enc:/...), never a bare plaintext secret. Variables and concatenations (the runtime-config path) are not flagged. Scans library/core/modules (never tests/, which exercises the passthrough). Mutation-tested: the detector must catch a plaintext literal and pass a scheme ref, a variable, a concatenation and a comment. Rides the existing unit PHPUnit job; no workflow change. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ --- .../SecretResolvePlaintextGuardTest.php | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 tests/Unit/Secret/SecretResolvePlaintextGuardTest.php diff --git a/tests/Unit/Secret/SecretResolvePlaintextGuardTest.php b/tests/Unit/Secret/SecretResolvePlaintextGuardTest.php new file mode 100644 index 00000000..090c4afa --- /dev/null +++ b/tests/Unit/Secret/SecretResolvePlaintextGuardTest.php @@ -0,0 +1,139 @@ +' ) | "" — the whole argument is one string literal. + if (preg_match_all('/Tiger_Secret::resolve\(\s*([\'"])(.*?)\1\s*[),]/s', $code, $m, PREG_SET_ORDER)) { + foreach ($m as $row) { + $literal = $row[2]; + // A recognized scheme reference is allowed; a bare plaintext literal is not. + if (!preg_match('/^[a-z][a-z0-9+_-]*:/', $literal)) { + $hits[] = $literal; + } + } + } + return $hits; + } + + /** Every production .php file under the scanned roots. */ + private static function phpFiles(): array + { + $files = []; + foreach (self::ROOTS as $root) { + $dir = self::repo() . '/' . $root; + if (!is_dir($dir)) { + continue; + } + $it = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS) + ); + foreach ($it as $file) { + if ($file->isFile() && strtolower($file->getExtension()) === 'php') { + $files[] = $file->getPathname(); + } + } + } + return $files; + } + + #[Test] + public function no_production_code_hands_a_plaintext_literal_to_tiger_secret_resolve(): void + { + $files = self::phpFiles(); + $this->assertNotEmpty($files, 'no production .php files found under ' . implode('/', self::ROOTS) . ' — the scan is wrong, not the rule'); + + $offenders = []; + foreach ($files as $file) { + $hits = self::offences(file_get_contents($file)); + if ($hits) { + $offenders[str_replace(self::repo() . '/', '', $file)] = array_values(array_unique($hits)); + } + } + $this->assertSame( + [], + $offenders, + "A plaintext literal is handed to Tiger_Secret::resolve() (TIGER-382) — it would be returned " + . "verbatim as a secret. Use a provider reference (ssm:/… env:… enc:… file:…) read from config, " + . "not a hard-coded value:\n" . json_encode($offenders, JSON_PRETTY_PRINT) + ); + } + + #[Test] + public function the_plaintext_detector_actually_catches_the_defect(): void + { + // Mutation guard: a bare plaintext literal handed to resolve() MUST be caught... + $this->assertNotEmpty(self::offences("assertNotEmpty(self::offences('assertEmpty(self::offences("assertEmpty(self::offences("assertEmpty(self::offences("assertEmpty(self::offences('assertEmpty(self::offences("assertEmpty(self::offences("