From 939d894bd98131ef1b30a026e79a87fd62476f4a Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Mon, 19 Oct 2026 08:48:28 -0400 Subject: [PATCH 1/2] Generate test credentials at run time Secret scanning cannot tell a test password from a leaked one: GitGuardian raised 28 incidents across laranail, 27 of them test literals closed by hand. Callback signing secrets, transport tokens and the argv canary now come from package-tools' TestCredentials, and a guard test fails on any literal left behind. The package-tools floor rises to ^0.1.9, the release that ships the helpers, so the prefer-lowest legs resolve them. --- CHANGELOG.md | 4 ++++ composer.json | 2 +- tests/Feature/HttpTransportTest.php | 9 +++++---- tests/Feature/Process/RealInterpreterTest.php | 7 ++++--- tests/Feature/ScaffoldSignatureTest.php | 12 ++++++++---- tests/Feature/Security/CallbackReplayTest.php | 17 +++++++++++------ .../Architecture/NoLiteralCredentialsTest.php | 13 +++++++++++++ 7 files changed, 46 insertions(+), 18 deletions(-) create mode 100644 tests/Unit/Architecture/NoLiteralCredentialsTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 766924f..315e4c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Test credentials are generated at run time.** Secrets and tokens in the tests come from + `TestCredentials` instead of literals, which secret scanning reported as leaks. A guard test + fails on any literal left behind. The `laranail/package-tools` floor is now `^0.1.9`, the + release that ships these helpers. - `illuminate/filesystem` is now declared in `require` at `^13.0`. `src/` imports it, and it was only arriving transitively. - Requires `laranail/package-tools ^0.1.3` (for the naming assertions). - `laranail::polyglot.install` now extends laranail/package-tools' `InstallCommand` and takes diff --git a/composer.json b/composer.json index 8593d58..5ac84aa 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,7 @@ "illuminate/routing": "^13.0", "illuminate/support": "^13.0", "laranail/console": "^0.1", - "laranail/package-tools": "^0.1.3", + "laranail/package-tools": "^0.1.9", "psr/clock": "^1.0", "psr/log": "^3.0" }, diff --git a/tests/Feature/HttpTransportTest.php b/tests/Feature/HttpTransportTest.php index 77ab8e1..7a5cb83 100644 --- a/tests/Feature/HttpTransportTest.php +++ b/tests/Feature/HttpTransportTest.php @@ -9,6 +9,7 @@ use Simtabi\Laranail\Polyglot\Enums\ErrorCode; use Simtabi\Laranail\Polyglot\Facades\Polyglot; use Simtabi\Laranail\Polyglot\Contracts\HttpClient; +use Simtabi\Laranail\Package\Tools\Testing\TestCredentials; use Simtabi\Laranail\Polyglot\Exceptions\MissingBaseUrlException; use Simtabi\Laranail\Polyglot\Exceptions\UnknownServiceException; @@ -82,12 +83,12 @@ public function test_a_bearer_token_is_sent(): void config()->set('laranail.polyglot.services.fastapi.auth', [ 'scheme' => 'bearer', - 'token' => 'secret-token-value', + 'token' => TestCredentials::secret('bearer'), ]); $this->client()->service('fastapi')->get('/anything'); - Http::assertSent(fn ($request): bool => $request->hasHeader('Authorization', 'Bearer secret-token-value')); + Http::assertSent(fn ($request): bool => $request->hasHeader('Authorization', sprintf('Bearer %s', TestCredentials::secret('bearer')))); } public function test_an_api_key_header_is_sent(): void @@ -96,13 +97,13 @@ public function test_an_api_key_header_is_sent(): void config()->set('laranail.polyglot.services.fastapi.auth', [ 'scheme' => 'api_key', - 'token' => 'key-value-here', + 'token' => TestCredentials::secret('header'), 'header' => 'X-Custom-Key', ]); $this->client()->service('fastapi')->get('/anything'); - Http::assertSent(fn ($request): bool => $request->hasHeader('X-Custom-Key', 'key-value-here')); + Http::assertSent(fn ($request): bool => $request->hasHeader('X-Custom-Key', TestCredentials::secret('header'))); } public function test_an_auth_scheme_without_its_credential_is_reported_incomplete(): void diff --git a/tests/Feature/Process/RealInterpreterTest.php b/tests/Feature/Process/RealInterpreterTest.php index 3819433..bd024ee 100644 --- a/tests/Feature/Process/RealInterpreterTest.php +++ b/tests/Feature/Process/RealInterpreterTest.php @@ -11,6 +11,7 @@ use Simtabi\Laranail\Polyglot\Enums\Transport; use Simtabi\Laranail\Polyglot\Contracts\ProcessRunner; use Simtabi\Laranail\Polyglot\ValueObjects\ProcessCall; +use Simtabi\Laranail\Package\Tools\Testing\TestCredentials; /** * The process transport against a real Python interpreter. @@ -198,7 +199,7 @@ public function the_payload_goes_on_stdin_and_never_into_argv(): void // readable by every other user on the host for the process's lifetime. $result = $this->runner()->run(new ProcessCall( script: 'argv', - payload: ['api_key' => 'SUPERSECRET'], + payload: ['api_key' => TestCredentials::secret('payload')], args: ['verbose'], )); @@ -208,9 +209,9 @@ public function the_payload_goes_on_stdin_and_never_into_argv(): void self::assertIsArray($argv); self::assertSame(['verbose'], $argv, 'Only the explicit args should reach argv.'); - self::assertNotContains('SUPERSECRET', $argv); + self::assertNotContains(TestCredentials::secret('payload'), $argv); self::assertStringNotContainsString( - 'SUPERSECRET', + TestCredentials::secret('payload'), implode(' ', array_map(strval(...), $argv)), 'The payload was passed on the command line.', ); diff --git a/tests/Feature/ScaffoldSignatureTest.php b/tests/Feature/ScaffoldSignatureTest.php index de0f8a6..5dcb281 100644 --- a/tests/Feature/ScaffoldSignatureTest.php +++ b/tests/Feature/ScaffoldSignatureTest.php @@ -6,6 +6,7 @@ use Simtabi\Laranail\Polyglot\Tests\TestCase; use Simtabi\Laranail\Polyglot\Contracts\CallbackVerifier; +use Simtabi\Laranail\Package\Tools\Testing\TestCredentials; /** * The scaffold's signing must agree with the verifier, or the scaffold is a lie @@ -18,8 +19,6 @@ */ final class ScaffoldSignatureTest extends TestCase { - private const string SECRET = 'scaffold-secret-value-x'; - public function test_the_stub_signs_the_string_the_verifier_expects(): void { $stub = file_get_contents(__DIR__ . '/../../stubs/fastapi/main.py.stub'); @@ -42,7 +41,7 @@ public function test_a_signature_built_the_stub_way_verifies(): void $body = json_encode(['task_id' => 't1', 'status' => 'succeeded'], JSON_THROW_ON_ERROR); $timestamp = time(); - $stubSignature = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, self::SECRET); + $stubSignature = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $this->secret()); self::assertSame($stubSignature, $verifier->sign($body, $timestamp)); } @@ -58,6 +57,11 @@ public function test_the_stub_declares_the_health_contract_the_config_expects(): protected function defineEnvironment($app): void { - $app['config']->set('laranail.polyglot.callbacks.secrets', [self::SECRET]); + $app['config']->set('laranail.polyglot.callbacks.secrets', [$this->secret()]); + } + + private function secret(): string + { + return TestCredentials::secret('scaffold'); } } diff --git a/tests/Feature/Security/CallbackReplayTest.php b/tests/Feature/Security/CallbackReplayTest.php index 8689161..7d2af3e 100644 --- a/tests/Feature/Security/CallbackReplayTest.php +++ b/tests/Feature/Security/CallbackReplayTest.php @@ -13,6 +13,7 @@ use Simtabi\Laranail\Polyglot\Events\CallbackReceived; use Simtabi\Laranail\Polyglot\Events\CallbackRejected; use Simtabi\Laranail\Polyglot\Contracts\CallbackVerifier; +use Simtabi\Laranail\Package\Tools\Testing\TestCredentials; /** * The callback surface is the one unauthenticated entry point this package can @@ -20,8 +21,6 @@ */ final class CallbackReplayTest extends TestCase { - private const string SECRET = 'callback-secret-value-for-tests'; - private FrozenClock $clock; protected function setUp(): void @@ -93,7 +92,7 @@ public function test_a_signature_over_a_reencoded_body_is_refused(): void $response = $this->call('POST', 'api/polyglot/callbacks', [], [], [], $this->serverHeaders([ 'X-Laranail-Timestamp' => (string) $timestamp, - 'X-Laranail-Signature' => 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $reencoded, self::SECRET), + 'X-Laranail-Signature' => 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $reencoded, $this->secret()), 'X-Laranail-Id' => 'reordered', ]), $raw); @@ -147,7 +146,8 @@ public function test_an_oversized_body_is_refused_before_it_is_parsed(): void public function test_a_second_secret_still_verifies_during_rotation(): void { - config()->set('laranail.polyglot.callbacks.secrets', ['the-new-secret-value', self::SECRET]); + self::assertNotSame(TestCredentials::secret('rotated'), $this->secret()); + config()->set('laranail.polyglot.callbacks.secrets', [TestCredentials::secret('rotated'), $this->secret()]); $this->app->forgetInstance(CallbackVerifier::class); // Signed with the OLD secret, which is now second in the list. @@ -157,11 +157,16 @@ public function test_a_second_secret_still_verifies_during_rotation(): void protected function defineEnvironment($app): void { $app['config']->set('laranail.polyglot.callbacks.enabled', true); - $app['config']->set('laranail.polyglot.callbacks.secrets', [self::SECRET]); + $app['config']->set('laranail.polyglot.callbacks.secrets', [$this->secret()]); $app['config']->set('laranail.polyglot.callbacks.tolerance', 300); $app['config']->set('cache.default', 'array'); } + private function secret(): string + { + return TestCredentials::secret('callback'); + } + /** * @param array $overrides */ @@ -172,7 +177,7 @@ private function deliver(array $payload, array $overrides = [], ?int $at = null) $headers = [ 'X-Laranail-Timestamp' => (string) $timestamp, - 'X-Laranail-Signature' => 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, self::SECRET), + 'X-Laranail-Signature' => 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $this->secret()), 'X-Laranail-Id' => 'delivery-1', 'Content-Type' => 'application/json', ...$overrides, diff --git a/tests/Unit/Architecture/NoLiteralCredentialsTest.php b/tests/Unit/Architecture/NoLiteralCredentialsTest.php new file mode 100644 index 0000000..8f7f0f8 --- /dev/null +++ b/tests/Unit/Architecture/NoLiteralCredentialsTest.php @@ -0,0 +1,13 @@ +assertNoLiteralCredentialsIn([dirname(__DIR__, 2)], minFiles: 15); + + expect(true)->toBeTrue(); +}); From 4ac2c2c540ae138c2036bf6e00fea5ba7332bda1 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Mon, 19 Oct 2026 10:31:20 -0400 Subject: [PATCH 2/2] Adopt package-tools 0.1.10's credential checks GitGuardian reads a compound purpose such as TestCredentials::password('smtp-new') as the password itself, so a generated credential can be reported as a leaked one. package-tools 0.1.10 adds assertCredentialPurposesAreWordsIn(), which fails on any purpose that is not a single plain lowercase word; the guard test now runs it beside the literal check, over the same directories and file floor. Raise the package-tools floor to ^0.1.10 for it. That release's scanner also recognises Eloquent cast names, provider names and reference keys as non-credentials, so exemption markers it no longer needs are removed. Each was checked by confirming CredentialLiterals::in() reports nothing on the line the marker covered. --- CHANGELOG.md | 5 +++-- composer.json | 2 +- tests/Unit/Architecture/NoLiteralCredentialsTest.php | 6 ++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 315e4c2..9d05a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Test credentials are generated at run time.** Secrets and tokens in the tests come from `TestCredentials` instead of literals, which secret scanning reported as leaks. A guard test - fails on any literal left behind. The `laranail/package-tools` floor is now `^0.1.9`, the - release that ships these helpers. + fails on any literal left behind. The `laranail/package-tools` floor is now `^0.1.10`, which + adds a second check: every purpose passed to `TestCredentials` must be a single plain lowercase + word, because secret scanning reads a compound purpose as the password itself. - `illuminate/filesystem` is now declared in `require` at `^13.0`. `src/` imports it, and it was only arriving transitively. - Requires `laranail/package-tools ^0.1.3` (for the naming assertions). - `laranail::polyglot.install` now extends laranail/package-tools' `InstallCommand` and takes diff --git a/composer.json b/composer.json index 5ac84aa..979d69e 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,7 @@ "illuminate/routing": "^13.0", "illuminate/support": "^13.0", "laranail/console": "^0.1", - "laranail/package-tools": "^0.1.9", + "laranail/package-tools": "^0.1.10", "psr/clock": "^1.0", "psr/log": "^3.0" }, diff --git a/tests/Unit/Architecture/NoLiteralCredentialsTest.php b/tests/Unit/Architecture/NoLiteralCredentialsTest.php index 8f7f0f8..4442b2a 100644 --- a/tests/Unit/Architecture/NoLiteralCredentialsTest.php +++ b/tests/Unit/Architecture/NoLiteralCredentialsTest.php @@ -11,3 +11,9 @@ expect(true)->toBeTrue(); }); + +it('names every generated credential with a single word', function (): void { + $this->assertCredentialPurposesAreWordsIn([dirname(__DIR__, 2)], minFiles: 15); + + expect(true)->toBeTrue(); +});