Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 2m1s
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.
164 lines
8.1 KiB
PHP
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,
|
|
];
|
|
}
|
|
}
|