diff --git a/CHANGELOG.md b/CHANGELOG.md index 766924f..9d05a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ 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.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 8593d58..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.3", + "laranail/package-tools": "^0.1.10", "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..4442b2a --- /dev/null +++ b/tests/Unit/Architecture/NoLiteralCredentialsTest.php @@ -0,0 +1,19 @@ +assertNoLiteralCredentialsIn([dirname(__DIR__, 2)], minFiles: 15); + + expect(true)->toBeTrue(); +}); + +it('names every generated credential with a single word', function (): void { + $this->assertCredentialPurposesAreWordsIn([dirname(__DIR__, 2)], minFiles: 15); + + expect(true)->toBeTrue(); +});