make(Kernel::class)->bootstrap(); portalQaAssertTarget($target); $private = $root.'/storage/app/private/investmoz-v2/portal-audit'; $sessions = realpath($root.'/storage/app/private/investmoz-v2/qa-opportunity-resources-sessions'); $privateRoot = realpath($root.'/storage/app/private/investmoz-v2'); abort_unless($sessions && $privateRoot && str_starts_with($sessions, $privateRoot.DIRECTORY_SEPARATOR) && ! config('session.encrypt'), 422); abort_unless(($argv[1] ?? '') === '--rotate', 422, 'Explicit QA-only rotation required.'); $actors = []; $before = []; foreach (['manager' => 'opportunity-manager@example.invalid', 'outsider' => 'investor-path-outsider-qa@example.invalid'] as $role => $email) { $user = User::where('email', $email)->firstOrFail(); $before[$role] = $user->getRawOriginal(); $actors[$role] = ['id' => $user->id, 'email' => $email, 'password' => Str::password(40), 'totp_secret' => $user->two_factor_secret ? Crypt::decryptString($user->two_factor_secret) : null]; } $account18 = DB::table('users')->where('id', 18)->first(); $account18Hash = hash('sha256', json_encode($account18, JSON_THROW_ON_ERROR)); DB::transaction(function () use ($actors): void { foreach ($actors as $actor) { DB::table('users')->where('id', $actor['id'])->where('email', $actor['email']) ->update(['password' => Hash::make($actor['password']), 'remember_token' => Str::random(60), 'updated_at' => now()]); } }); $invalidated = 0; foreach (glob($sessions.DIRECTORY_SEPARATOR.'*') ?: [] as $file) { $path = realpath($file); if (! $path || ! is_file($path) || ! str_starts_with($path, $sessions.DIRECTORY_SEPARATOR) || ! preg_match('/^[a-zA-Z0-9]{40}$/', basename($path))) { continue; } $data = @unserialize(file_get_contents($path), ['allowed_classes' => false]); if (! is_array($data)) { continue; } foreach ($data as $key => $value) { if ((str_starts_with($key, 'login_web_') || $key === 'login.id') && in_array((int) $value, array_column($actors, 'id'), true)) { abort_unless(unlink($path), 422, 'QA session invalidation failed.'); $invalidated++; break; } } } foreach ($actors as $role => $actor) { $after = DB::table('users')->where('id', $actor['id'])->first(); foreach (['two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at'] as $column) { abort_unless($before[$role][$column] === $after->$column, 422, 'MFA preservation check failed.'); } abort_unless(! hash_equals($before[$role]['password'], $after->password), 422); } abort_unless($account18Hash === hash('sha256', json_encode(DB::table('users')->where('id', 18)->first(), JSON_THROW_ON_ERROR)), 422, 'Unrelated investor preservation check failed.'); file_put_contents($private.'/resource-http-actors-private.json', json_encode($actors, JSON_THROW_ON_ERROR)); $proof = ['captured_at' => gmdate('c'), 'target' => $target, 'rotated_synthetic_actors' => count($actors), 'qa_sessions_invalidated' => $invalidated, 'remember_tokens_rotated' => true, 'mfa_preserved' => true, 'investor_18_preserved' => true, 'active_writes' => 0, 'credentials_exported' => false]; file_put_contents($private.'/resource-http-actors-rotation-qa.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); echo json_encode($proof, JSON_THROW_ON_ERROR).PHP_EOL;