TenantAtlas/apps/platform/app/Services/Evidence/Sources/FindingsSummarySource.php
Ahmed Darrazi 54f81d4a50
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 2m1s
feat: review pack pdf and html renderer v1 (spec 356)
Implemented the first version of the PDF and HTML renderer for review packs. Added ReviewPackRenderedReportController and related blade views to render reports. Updated EnvironmentReviewResource, ReviewPackResource, ReviewPackService, and routing. Added new tests for the renderer and download actions, and updated UI documentation.
2026-06-05 22:38:10 +02:00

164 lines
8.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Evidence\Sources;
use App\Models\Finding;
use App\Models\ManagedEnvironment;
use App\Services\Evidence\Contracts\EvidenceSourceProvider;
use App\Services\Findings\FindingRiskGovernanceResolver;
use App\Support\Findings\FindingOutcomeSemantics;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\Artifacts\ArtifactSourceResolver;
final class FindingsSummarySource implements EvidenceSourceProvider
{
public function __construct(
private readonly FindingRiskGovernanceResolver $governanceResolver,
private readonly FindingOutcomeSemantics $findingOutcomeSemantics,
private readonly ArtifactSourceResolver $artifactSourceResolver,
) {}
public function key(): string
{
return 'findings_summary';
}
public function collect(ManagedEnvironment $tenant): array
{
$findings = Finding::query()
->where('managed_environment_id', (int) $tenant->getKey())
->with(['findingException.currentDecision', 'findingException.owner'])
->orderByDesc('updated_at')
->get();
$latest = $findings->max('updated_at') ?? $findings->max('created_at');
$entries = $findings->map(function (Finding $finding): array {
$exception = $finding->findingException;
$governanceState = $this->governanceResolver->resolveFindingState($finding, $finding->findingException);
$governanceWarning = $this->governanceResolver->resolveWarningMessage($finding, $finding->findingException);
$outcome = $this->findingOutcomeSemantics->describe($finding);
$descriptor = $this->artifactSourceResolver->forFinding($finding);
$providerDetail = $this->artifactSourceResolver->providerDetailForFinding($finding);
$canonicalControlResolution = $this->artifactSourceResolver->canonicalControlResolutionForFinding($finding);
return [
'id' => (int) $finding->getKey(),
'source_descriptor' => $descriptor->toArray(),
'provider_detail' => $providerDetail->toArray(),
'control_key' => $descriptor->controlKey,
'finding_type' => (string) $finding->finding_type,
'severity' => (string) $finding->severity,
'status' => (string) $finding->status,
'title' => $finding->resolvedSubjectDisplayName(),
'description' => null,
'created_at' => $finding->created_at?->toIso8601String(),
'updated_at' => $finding->updated_at?->toIso8601String(),
'verification_state' => $outcome['verification_state'],
'report_bucket' => $outcome['report_bucket'],
'terminal_outcome_key' => $outcome['terminal_outcome_key'],
'terminal_outcome_label' => $outcome['label'],
'terminal_outcome' => $outcome['terminal_outcome_key'] !== null ? [
'key' => $outcome['terminal_outcome_key'],
'label' => $outcome['label'],
'verification_state' => $outcome['verification_state'],
'report_bucket' => $outcome['report_bucket'],
'governance_state' => $governanceState,
] : null,
'canonical_control_resolution' => $canonicalControlResolution,
'governance_state' => $governanceState,
'governance_warning' => $governanceWarning,
'customer_summary' => is_string($finding->evidence_jsonb['customer_summary'] ?? null)
? (string) $finding->evidence_jsonb['customer_summary']
: null,
'exception_status' => $exception !== null ? (string) $exception->status : null,
'review_due_at' => $exception?->review_due_at?->toDateString(),
'expires_at' => $exception?->expires_at?->toDateString(),
'owner' => $exception?->owner !== null ? [
'id' => (int) $exception->owner->getKey(),
'name' => (string) $exception->owner->name,
] : null,
];
});
$outcomeCounts = array_fill_keys($this->findingOutcomeSemantics->orderedOutcomeKeys(), 0);
$reportBucketCounts = [
'remediation_pending_verification' => 0,
'remediation_verified' => 0,
'administrative_closure' => 0,
'accepted_risk' => 0,
];
foreach ($entries as $entry) {
$terminalOutcomeKey = $entry['terminal_outcome_key'] ?? null;
$reportBucket = $entry['report_bucket'] ?? null;
if (is_string($terminalOutcomeKey) && array_key_exists($terminalOutcomeKey, $outcomeCounts)) {
$outcomeCounts[$terminalOutcomeKey]++;
}
if (is_string($reportBucket) && array_key_exists($reportBucket, $reportBucketCounts)) {
$reportBucketCounts[$reportBucket]++;
}
}
$canonicalControls = $entries
->map(static fn (array $entry): mixed => data_get($entry, 'canonical_control_resolution.control'))
->filter(static fn (mixed $control): bool => is_array($control) && filled($control['control_key'] ?? null))
->unique(static fn (array $control): string => (string) $control['control_key'])
->values()
->all();
$riskAcceptedEntries = $entries->filter(
static fn (array $entry): bool => ($entry['status'] ?? null) === Finding::STATUS_RISK_ACCEPTED,
);
$warningStates = [
'expired_exception',
'revoked_exception',
'rejected_exception',
'risk_accepted_without_valid_exception',
];
$summary = [
'count' => $findings->count(),
'open_count' => $findings->filter(fn (Finding $finding): bool => $finding->hasOpenStatus())->count(),
'severity_counts' => [
'critical' => $findings->where('severity', Finding::SEVERITY_CRITICAL)->count(),
'high' => $findings->where('severity', Finding::SEVERITY_HIGH)->count(),
'medium' => $findings->where('severity', Finding::SEVERITY_MEDIUM)->count(),
'low' => $findings->where('severity', Finding::SEVERITY_LOW)->count(),
],
'risk_acceptance' => [
'status_marked_count' => $riskAcceptedEntries->count(),
'valid_governed_count' => $riskAcceptedEntries->filter(
static fn (array $entry): bool => in_array($entry['governance_state'] ?? null, ['valid_exception', 'expiring_exception'], true),
)->count(),
'warning_count' => $riskAcceptedEntries->filter(
static fn (array $entry): bool => in_array($entry['governance_state'] ?? null, $warningStates, true),
)->count(),
'expired_count' => $riskAcceptedEntries->where('governance_state', 'expired_exception')->count(),
'revoked_count' => $riskAcceptedEntries->where('governance_state', 'revoked_exception')->count(),
'missing_exception_count' => $riskAcceptedEntries->where('governance_state', 'risk_accepted_without_valid_exception')->count(),
],
'outcome_counts' => $outcomeCounts,
'report_bucket_counts' => $reportBucketCounts,
'canonical_controls' => $canonicalControls,
'entries' => $entries->all(),
];
return [
'dimension_key' => $this->key(),
'state' => $findings->isEmpty() ? EvidenceCompletenessState::Missing->value : EvidenceCompletenessState::Complete->value,
'required' => true,
'source_kind' => 'model_summary',
'source_record_type' => 'finding',
'source_record_id' => null,
'source_fingerprint' => $findings->max('fingerprint'),
'measured_at' => $latest,
'freshness_at' => $latest,
'summary_payload' => $summary,
'fingerprint_payload' => $summary + ['latest' => $latest?->format(DATE_ATOM)],
'sort_order' => 10,
];
}
}