TenantAtlas/apps/platform/app/Console/Commands/SeedReviewOutputBrowserFixture.php
ahmido b7907bd69d feat: add report profile and disclosure policy to rendered review reports (#428)
Implementing report profiles and disclosure policy as per spec 357.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #428
2026-06-06 09:41:19 +00:00

588 lines
24 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
use App\Filament\Resources\EnvironmentReviewResource;
use App\Models\EnvironmentReview;
use App\Models\EnvironmentReviewSection;
use App\Models\EvidenceSnapshot;
use App\Models\EvidenceSnapshotItem;
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentMembership;
use App\Models\OperationRun;
use App\Models\ReviewPack;
use App\Models\StoredReport;
use App\Models\User;
use App\Models\UserTenantPreference;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Services\EnvironmentReviews\EnvironmentReviewLifecycleService;
use App\Services\EnvironmentReviews\EnvironmentReviewService;
use App\Services\Evidence\EvidenceSnapshotService;
use App\Services\OperationRunService;
use App\Support\EnvironmentReviewCompletenessState;
use App\Support\EnvironmentReviewStatus;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class SeedReviewOutputBrowserFixture extends Command
{
protected $signature = 'tenantpilot:review-output:seed-browser-fixture';
protected $description = 'Seed a local/testing browser fixture for the Spec 351 ready-review publish scenario.';
public function handle(): int
{
if (! app()->environment(['local', 'testing'])) {
$this->error('This fixture command is limited to local and testing environments.');
return self::FAILURE;
}
$fixture = config('tenantpilot.review_output.browser_smoke_fixture');
if (! is_array($fixture)) {
$this->error('The review-output browser smoke fixture is not configured.');
return self::FAILURE;
}
$workspaceConfig = is_array($fixture['workspace'] ?? null) ? $fixture['workspace'] : [];
$userConfig = is_array($fixture['user'] ?? null) ? $fixture['user'] : [];
$scenarioConfig = is_array($fixture['ready_draft'] ?? null) ? $fixture['ready_draft'] : [];
$workspace = Workspace::query()->updateOrCreate(
['slug' => (string) ($workspaceConfig['slug'] ?? 'spec-351-review-output-smoke')],
['name' => (string) ($workspaceConfig['name'] ?? 'Spec 351 Review Output Smoke')],
);
$password = (string) ($userConfig['password'] ?? 'password');
$user = User::query()->updateOrCreate(
['email' => (string) ($userConfig['email'] ?? 'smoke-requester+351@tenantpilot.local')],
[
'name' => (string) ($userConfig['name'] ?? 'Spec 351 Requester'),
'password' => Hash::make($password),
'email_verified_at' => now(),
],
);
$environment = ManagedEnvironment::query()->updateOrCreate(
['slug' => (string) ($scenarioConfig['managed_environment_slug'] ?? 'spec-351-browser-ready-draft')],
[
'workspace_id' => (int) $workspace->getKey(),
'name' => (string) ($scenarioConfig['managed_environment_name'] ?? 'Spec 351 Browser Ready Draft'),
'lifecycle_status' => ManagedEnvironment::STATUS_ACTIVE,
'kind' => 'dev',
'is_current' => false,
'metadata' => ['fixture' => 'spec-351-review-output-browser-smoke'],
'rbac_status' => 'ok',
'rbac_last_checked_at' => now(),
],
);
WorkspaceMembership::query()->updateOrCreate(
['workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey()],
['role' => 'owner'],
);
ManagedEnvironmentMembership::query()->updateOrCreate(
['managed_environment_id' => (int) $environment->getKey(), 'user_id' => (int) $user->getKey()],
['role' => 'owner', 'source' => 'manual', 'source_ref' => 'spec-351-review-output-browser-smoke'],
);
if (Schema::hasColumn('users', 'last_workspace_id')) {
$user->forceFill(['last_workspace_id' => (int) $workspace->getKey()])->save();
}
if (Schema::hasTable('user_managed_environment_preferences')) {
UserTenantPreference::query()->updateOrCreate(
['user_id' => (int) $user->getKey(), 'managed_environment_id' => (int) $environment->getKey()],
['last_used_at' => now()],
);
}
$readyReview = DB::transaction(function () use ($environment, $user, $scenarioConfig): EnvironmentReview {
$this->resetFixtureGraph($environment);
$snapshot = $this->seedReadyEvidenceSnapshot($environment, $user, $scenarioConfig);
return $this->seedPublishedLoopWithReadySuccessor($environment, $user, $snapshot, $scenarioConfig);
});
$workspaceUrl = CustomerReviewWorkspace::environmentFilterUrl($environment);
$detailUrl = EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $readyReview], $environment);
$detailRedirect = $this->relativeAdminRedirect($detailUrl);
$publishedReviewId = EnvironmentReview::query()
->where('superseded_by_review_id', (int) $readyReview->getKey())
->latest('id')
->value('id');
$loginUrl = route('admin.local.smoke-login', [
'email' => (string) $user->email,
'tenant' => (string) $environment->slug,
'workspace' => (string) $workspace->slug,
'redirect' => $detailRedirect,
], absolute: false);
$this->table(
['Fixture', 'Value'],
[
['Workspace', (string) $workspace->name],
['User email', (string) $user->email],
['User password', $password],
['ManagedEnvironment', (string) $environment->name],
['Workspace URL', $workspaceUrl],
['Published review id', $publishedReviewId !== null ? (string) $publishedReviewId : '—'],
['Ready review id', (string) $readyReview->getKey()],
['Ready review detail URL', $detailUrl],
['Fixture login URL', $loginUrl],
],
);
$this->info('The fixture seeds a published review plus a linked ready successor so the workspace shows Open draft review and the detail page shows Publish review.');
return self::SUCCESS;
}
private function resetFixtureGraph(ManagedEnvironment $environment): void
{
$environmentId = (int) $environment->getKey();
$reviewIds = EnvironmentReview::query()
->where('managed_environment_id', $environmentId)
->pluck('id');
$snapshotIds = EvidenceSnapshot::query()
->where('managed_environment_id', $environmentId)
->pluck('id');
if ($reviewIds->isNotEmpty()) {
ReviewPack::query()->whereIn('environment_review_id', $reviewIds)->delete();
EnvironmentReviewSection::query()->whereIn('environment_review_id', $reviewIds)->delete();
EnvironmentReview::query()->whereIn('id', $reviewIds)->delete();
}
if ($snapshotIds->isNotEmpty()) {
EvidenceSnapshotItem::query()->whereIn('evidence_snapshot_id', $snapshotIds)->delete();
EvidenceSnapshot::query()->whereIn('id', $snapshotIds)->delete();
}
OperationRun::query()->where('managed_environment_id', $environmentId)->delete();
StoredReport::query()->where('managed_environment_id', $environmentId)->delete();
}
/**
* @param array<string, mixed> $scenarioConfig
*/
private function seedReadyEvidenceSnapshot(
ManagedEnvironment $environment,
User $user,
array $scenarioConfig,
): EvidenceSnapshot {
StoredReport::query()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'report_type' => StoredReport::REPORT_TYPE_PERMISSION_POSTURE,
'payload' => [
'posture_score' => 92,
'required_count' => 14,
'granted_count' => 14,
'checked_at' => now()->subMinutes(5)->toIso8601String(),
'permissions' => [
['key' => 'DeviceManagementConfiguration.ReadWrite.All', 'status' => 'granted'],
],
],
]);
StoredReport::query()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'report_type' => StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES,
'payload' => [
'provider_key' => 'microsoft',
'domain' => 'entra.admin_roles',
'measured_at' => now()->subMinutes(5)->toIso8601String(),
'roles' => [
[
'displayName' => 'Global Administrator',
'userPrincipalName' => 'admin@contoso.com',
],
],
],
]);
$operationRun = OperationRun::query()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'user_id' => (int) $user->getKey(),
'initiator_name' => (string) ($scenarioConfig['operation_initiator_name'] ?? 'Spec 351 Browser Operator'),
'type' => OperationRunType::PolicySync->value,
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
'run_identity_hash' => hash('sha256', sprintf(
'spec-351-review-output-browser-smoke-%s-%s',
$environment->getKey(),
(string) Str::uuid(),
)),
'summary_counts' => [],
'failure_summary' => [],
'context' => ['fixture' => 'spec-351-review-output-browser-smoke'],
'started_at' => now()->subMinutes(6),
'completed_at' => now()->subMinutes(5),
]);
/** @var EvidenceSnapshotService $service */
$service = app(EvidenceSnapshotService::class);
$payload = $service->buildSnapshotPayload($environment);
$snapshot = EvidenceSnapshot::query()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'operation_run_id' => (int) $operationRun->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
'status' => EvidenceSnapshotStatus::Active->value,
'fingerprint' => $payload['fingerprint'],
'completeness_state' => $payload['completeness'],
'summary' => $payload['summary'],
'generated_at' => now()->subMinutes(4),
]);
foreach ($payload['items'] as $item) {
$snapshot->items()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'dimension_key' => $item['dimension_key'],
'state' => $item['state'],
'required' => $item['required'],
'source_kind' => $item['source_kind'],
'source_record_type' => $item['source_record_type'],
'source_record_id' => $item['source_record_id'],
'source_fingerprint' => $item['source_fingerprint'],
'measured_at' => $item['measured_at'],
'freshness_at' => $item['freshness_at'],
'summary_payload' => $item['summary_payload'],
'sort_order' => $item['sort_order'],
]);
}
return $snapshot->load('items');
}
/**
* @param array<string, mixed> $scenarioConfig
*/
private function seedPublishedLoopWithReadySuccessor(
ManagedEnvironment $environment,
User $user,
EvidenceSnapshot $snapshot,
array $scenarioConfig,
): EnvironmentReview {
/** @var EnvironmentReviewService $service */
$service = app(EnvironmentReviewService::class);
/** @var EnvironmentReviewLifecycleService $lifecycle */
$lifecycle = app(EnvironmentReviewLifecycleService::class);
Queue::fake();
$publishedReview = $service->create($environment, $snapshot, $user)->refresh();
if ($publishedReview->generated_at === null || ! $publishedReview->sections()->exists()) {
$publishedReview = $service->compose($publishedReview);
$this->completeFixtureComposeRun($publishedReview);
}
$publishedReview = $this->markReadyReview($publishedReview);
$publishedReview->forceFill([
'status' => EnvironmentReviewStatus::Ready->value,
'published_at' => null,
'published_by_user_id' => null,
])->save();
$publishedReview = $lifecycle->publish(
$publishedReview->refresh(),
$user,
(string) ($scenarioConfig['publish_reason'] ?? 'Seed published predecessor for Spec 351 browser verification.'),
);
$this->attachPublishedReviewPack($environment, $user, $publishedReview, $snapshot, $scenarioConfig);
$successorReview = $lifecycle->createNextReview($publishedReview->fresh(), $user, $snapshot)->refresh();
if ($successorReview->generated_at === null || ! $successorReview->sections()->exists()) {
$successorReview = $service->compose($successorReview);
$this->completeFixtureComposeRun($successorReview);
}
$successorReview = $this->markReadyReview($successorReview);
$successorReview->forceFill([
'status' => EnvironmentReviewStatus::Ready->value,
'published_at' => null,
'published_by_user_id' => null,
])->save();
return $successorReview->refresh()->load(['tenant', 'evidenceSnapshot.items', 'sections']);
}
private function completeFixtureComposeRun(EnvironmentReview $review): void
{
$run = $review->operationRun()->first();
if (! $run instanceof OperationRun || (string) $run->type !== OperationRunType::EnvironmentReviewCompose->value) {
return;
}
/** @var OperationRunService $service */
$service = app(OperationRunService::class);
if ((string) $run->status === OperationRunStatus::Queued->value) {
$service->updateRun(
$run,
status: OperationRunStatus::Running->value,
outcome: OperationRunOutcome::Pending->value,
);
$run = $run->fresh();
}
if (! $run instanceof OperationRun || (string) $run->status === OperationRunStatus::Completed->value) {
return;
}
$summary = is_array($review->summary) ? $review->summary : [];
$service->updateRun(
$run,
status: OperationRunStatus::Completed->value,
outcome: OperationRunOutcome::Succeeded->value,
summaryCounts: [
'created' => 1,
'finding_count' => (int) ($summary['finding_count'] ?? 0),
'report_count' => (int) ($summary['report_count'] ?? 0),
'operation_count' => (int) ($summary['operation_count'] ?? 0),
'errors_recorded' => 0,
],
);
}
/**
* @param array<string, mixed> $scenarioConfig
*/
private function attachPublishedReviewPack(
ManagedEnvironment $environment,
User $user,
EnvironmentReview $review,
EvidenceSnapshot $snapshot,
array $scenarioConfig,
): void {
$filePath = (string) ($scenarioConfig['published_review_pack_path'] ?? 'review-packs/spec351-browser-ready-published.zip');
$fileContents = (string) ($scenarioConfig['published_review_pack_contents'] ?? 'PK-spec351-browser-ready-published');
Storage::disk('exports')->put($filePath, $fileContents);
$fingerprint = hash('sha256', implode('|', [
'spec-351-review-output-browser-smoke-pack',
(string) $environment->getKey(),
(string) $review->getKey(),
$filePath,
]));
$reviewPack = ReviewPack::query()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'environment_review_id' => (int) $review->getKey(),
'status' => ReviewPack::STATUS_READY,
'fingerprint' => $fingerprint,
'summary' => [
'fixture' => 'spec-351-review-output-browser-smoke',
],
'options' => [
'include_pii' => false,
'include_operations' => true,
],
'file_disk' => 'exports',
'file_path' => $filePath,
'file_size' => strlen($fileContents),
'sha256' => hash('sha256', $fileContents),
'generated_at' => now()->subMinutes(3),
'expires_at' => now()->addDays(7),
]);
$summary = is_array($review->summary) ? $review->summary : [];
$summary['has_ready_export'] = true;
$review->forceFill([
'current_export_review_pack_id' => (int) $reviewPack->getKey(),
'summary' => $summary,
])->save();
}
private function markReadyReview(EnvironmentReview $review): EnvironmentReview
{
$review->loadMissing(['sections', 'evidenceSnapshot.items']);
$disclosure = 'TenantPilot interprets available evidence for review readiness. This is not a certification, legal attestation, or compliance guarantee.';
$controlSummary = [
'control_key' => 'customer-output',
'control_name' => 'Customer output',
'domain_key' => 'customer_delivery',
'readiness_bucket' => 'evidence_on_record',
'readiness_label' => 'Evidence on record',
'limitation_flags' => [],
'limitation_labels' => [],
'customer_summary' => 'Customer output has evidence on record in this released review.',
'evidence_basis_summary' => '1 evidence signal references this control.',
'accepted_risk_summary' => null,
'recommended_next_action' => 'Publish review.',
'detail_anchor' => 'control-customer-output',
'supporting_finding_ids' => [],
'finding_count' => 0,
'open_finding_count' => 0,
'accepted_risk_count' => 0,
];
$controlExplanation = [
'title' => 'Customer output',
'control_key' => 'customer-output',
'control_name' => 'Customer output',
'readiness_bucket' => 'evidence_on_record',
'readiness_label' => 'Evidence on record',
'limitation_flags' => [],
'limitation_labels' => [],
'customer_summary' => 'Customer output has evidence on record in this released review.',
'evidence_basis_summary' => '1 evidence signal references this control.',
'accepted_risk_summary' => null,
'explanation_text' => 'Customer output has evidence on record in this released review.',
'evidence_basis_items' => [
'1 evidence signal references this control.',
],
'accepted_risk_context' => null,
'recommended_next_action' => 'Publish review.',
'proof_access_state' => 'available',
'supporting_finding_ids' => [],
];
$snapshot = $review->evidenceSnapshot;
if ($snapshot instanceof EvidenceSnapshot) {
$snapshot->items->each(function (EvidenceSnapshotItem $item): void {
$item->forceFill([
'state' => EvidenceCompletenessState::Complete->value,
])->save();
});
$snapshotSummary = is_array($snapshot->summary) ? $snapshot->summary : [];
$snapshotSummary['missing_dimensions'] = 0;
$snapshotSummary['stale_dimensions'] = 0;
$snapshotSummary['dimensions'] = collect($snapshotSummary['dimensions'] ?? [])
->map(static function (mixed $dimension): mixed {
if (! is_array($dimension)) {
return $dimension;
}
$dimension['state'] = EnvironmentReviewCompletenessState::Complete->value;
return $dimension;
})
->values()
->all();
$snapshot->forceFill([
'completeness_state' => EvidenceCompletenessState::Complete->value,
'summary' => $snapshotSummary,
])->save();
}
$review->sections->each(function (EnvironmentReviewSection $section) use ($controlExplanation, $disclosure): void {
$attributes = [
'completeness_state' => EnvironmentReviewCompletenessState::Complete->value,
];
if ($section->isControlInterpretation()) {
$attributes['summary_payload'] = array_replace_recursive([
'version_key' => ComplianceEvidenceMappingV1::VERSION_KEY,
'display_label' => 'Compliance evidence mapping v1',
'non_certification_disclosure' => $disclosure,
'mapped_control_count' => 1,
'follow_up_required_count' => 0,
'limitation_counts' => [],
'limitations' => [],
], is_array($section->summary_payload) ? $section->summary_payload : []);
$attributes['render_payload'] = array_replace_recursive([
'entries' => [$controlExplanation],
'disclosure' => $disclosure,
'next_actions' => ['Publish review.'],
'empty_state' => null,
], is_array($section->render_payload) ? $section->render_payload : []);
}
$section->forceFill($attributes)->save();
});
$sectionCount = $review->sections->count();
$summary = is_array($review->summary) ? $review->summary : [];
$existingControlInterpretation = is_array($summary['control_interpretation'] ?? null)
? $summary['control_interpretation']
: [];
$existingControls = collect($existingControlInterpretation['controls'] ?? [])
->filter(static fn (mixed $control): bool => is_array($control))
->values();
$summary['control_interpretation'] = array_replace_recursive([
'version_key' => ComplianceEvidenceMappingV1::VERSION_KEY,
'display_label' => 'Compliance evidence mapping v1',
'non_certification_disclosure' => $disclosure,
'mapped_control_count' => 1,
'follow_up_required_count' => 0,
'limitation_counts' => [],
'limitations' => [],
'controls' => [$controlSummary],
], $existingControlInterpretation);
$summary['control_interpretation']['controls'] = [
array_replace(
$controlSummary,
is_array($existingControls->first()) ? $existingControls->first() : [],
),
];
$summary['publish_blockers'] = [];
$summary['section_count'] = $sectionCount;
$summary['section_state_counts'] = [
'complete' => $sectionCount,
'partial' => 0,
'missing' => 0,
'stale' => 0,
];
$review->forceFill([
'completeness_state' => EnvironmentReviewCompletenessState::Complete->value,
'summary' => $summary,
])->save();
return $review->refresh();
}
private function relativeAdminRedirect(string $url): string
{
$parsed = parse_url($url);
$path = '/'.ltrim((string) ($parsed['path'] ?? ''), '/');
$query = isset($parsed['query']) ? '?'.$parsed['query'] : '';
$fragment = isset($parsed['fragment']) ? '#'.$parsed['fragment'] : '';
return $path.$query.$fragment;
}
}