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("