syncDefaults(); [$user, $environment] = createMinimalUserWithTenant(role: 'owner'); $connection = ProviderConnection::factory()->withCredential()->create([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), ]); $graph = spec420OperationGraphClient([ 'conditionalAccessPolicy' => [[ 'id' => 'cap-1', 'displayName' => 'Require MFA', 'clientSecret' => 'spec420-operation-client-secret', 'tokenClaims' => [ 'access_token' => 'spec420-operation-access-token', ], ]], ]); app()->instance(GraphClientInterface::class, $graph); $originalLogger = Log::getFacadeRoot(); $logger = spec420RecordingLogger(); Log::swap($logger); $run = OperationRun::factory()->withUser($user)->forTenant($environment)->create([ 'type' => OperationRunType::TenantConfigurationCapture->value, 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'context' => [ 'target_scope' => [ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), 'provider_connection_id' => (int) $connection->getKey(), ], 'resource_types' => ['conditionalAccessPolicy', 'acceptedDomain', 'appPermissionPolicy', 'dlpCompliancePolicy'], 'required_capability' => 'evidence.manage', ], ]); try { app(CaptureTenantConfigurationEvidenceJob::class, ['run' => $run])->handle( app(GenericContentEvidenceCaptureService::class), app(OperationRunService::class), app(AuditRecorder::class), ); $run->refresh(); expect($run->type)->toBe(OperationRunType::TenantConfigurationCapture->value) ->and($run->type)->not->toBe('tenant_configuration.m365_capture') ->and($run->status)->toBe(OperationRunStatus::Completed->value) ->and($run->outcome)->toBe(OperationRunOutcome::Succeeded->value) ->and($run->summary_counts)->toMatchArray([ 'total' => 4, 'processed' => 4, 'succeeded' => 1, 'skipped' => 3, 'failed' => 0, 'errors_recorded' => 0, ]) ->and(array_diff(array_keys($run->summary_counts), OperationSummaryKeys::all()))->toBe([]) ->and(json_encode($run->context, JSON_THROW_ON_ERROR))->not->toContain('client_secret') ->and(json_encode($run->context, JSON_THROW_ON_ERROR))->not->toContain('access_token') ->and(json_encode($run->context, JSON_THROW_ON_ERROR))->not->toContain('spec420-operation-client-secret') ->and(json_encode($run->context, JSON_THROW_ON_ERROR))->not->toContain('spec420-operation-access-token') ->and(json_encode($run->failure_summary ?? [], JSON_THROW_ON_ERROR))->not->toContain('spec420-operation-client-secret') ->and(json_encode($run->failure_summary ?? [], JSON_THROW_ON_ERROR))->not->toContain('spec420-operation-access-token'); $evidence = TenantConfigurationResourceEvidence::query()->sole(); $audit = AuditLog::query() ->where('action', 'tenant_configuration.capture.completed') ->latest('id') ->first(); expect($evidence->raw_payload['clientSecret'])->toBe('spec420-operation-client-secret') ->and($evidence->normalized_payload['clientSecret'])->toBe('[redacted]') ->and($evidence->normalized_payload['tokenClaims'])->toBe('[redacted]') ->and(json_encode($evidence->normalized_payload, JSON_THROW_ON_ERROR))->not->toContain('spec420-operation-client-secret') ->and(json_encode($evidence->normalized_payload, JSON_THROW_ON_ERROR))->not->toContain('spec420-operation-access-token') ->and($audit)->not->toBeNull() ->and(json_encode($audit?->metadata, JSON_THROW_ON_ERROR))->not->toContain('spec420-operation-client-secret') ->and(json_encode($audit?->metadata, JSON_THROW_ON_ERROR))->not->toContain('spec420-operation-access-token'); spec420AssertSecretsWereNotLogged($logger, [ 'spec420-operation-client-secret', 'spec420-operation-access-token', ]); } finally { Log::swap($originalLogger); } }); it('Spec420 deduplicates active capture starts for the selected M365 first pack', function (): void { Queue::fake(); [$user, $environment] = createMinimalUserWithTenant(role: 'owner'); $connection = ProviderConnection::factory()->create([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), ]); $types = ['conditionalAccessPolicy', 'acceptedDomain', 'appPermissionPolicy', 'dlpCompliancePolicy']; $first = app(StartTenantConfigurationCapture::class)->start($environment, $connection, $user, $types); $second = app(StartTenantConfigurationCapture::class)->start($environment, $connection, $user, $types); expect($second->getKey())->toBe($first->getKey()) ->and($first->type)->toBe(OperationRunType::TenantConfigurationCapture->value) ->and(data_get($first->context, 'resource_types'))->toBe(collect($types)->sort()->values()->all()); Queue::assertPushed(CaptureTenantConfigurationEvidenceJob::class, 1); }); function spec420OperationGraphClient(array $responses): GraphClientInterface { return new class($responses) implements GraphClientInterface { public function __construct(private readonly array $responses) {} public function listPolicies(string $policyType, array $options = []): GraphResponse { return new GraphResponse(true, $this->responses[$policyType] ?? []); } public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse { return new GraphResponse(false, [], 501); } public function getOrganization(array $options = []): GraphResponse { return new GraphResponse(false, [], 501); } public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse { return new GraphResponse(false, [], 501); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(false, [], 501); } public function request(string $method, string $path, array $options = []): GraphResponse { return new GraphResponse(false, [], 501); } }; } function spec420RecordingLogger(): object { return new class { /** * @var list}> */ public array $entries = []; public function __call(string $method, array $arguments): self { if (in_array($method, ['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'], true)) { $this->entries[] = ['level' => $method, 'arguments' => $arguments]; } return $this; } }; } /** * @param list $secrets */ function spec420AssertSecretsWereNotLogged(object $logger, array $secrets): void { $entries = property_exists($logger, 'entries') && is_array($logger->entries) ? $logger->entries : []; $logged = spec420LogArguments($entries); foreach ($secrets as $secret) { expect($logged)->not->toContain($secret); } } /** * @param array $arguments */ function spec420LogArguments(array $arguments): string { return json_encode($arguments, JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_UNESCAPED_SLASHES) ?: ''; }