terminalOutcomeKey($finding); $verificationState = $this->verificationState($finding); return [ 'terminal_outcome_key' => $terminalOutcomeKey, 'label' => $terminalOutcomeKey !== null ? $this->terminalOutcomeLabel($terminalOutcomeKey) : null, 'verification_state' => $verificationState, 'verification_label' => $verificationState !== self::VERIFICATION_NOT_APPLICABLE ? $this->verificationStateLabel($verificationState) : null, 'report_bucket' => $terminalOutcomeKey !== null ? $this->reportBucket($terminalOutcomeKey) : null, ]; } public function terminalOutcomeKey(Finding $finding): ?string { return match ((string) $finding->status) { Finding::STATUS_RESOLVED => $this->resolvedTerminalOutcomeKey((string) ($finding->resolved_reason ?? '')), Finding::STATUS_CLOSED => $this->closedTerminalOutcomeKey((string) ($finding->closed_reason ?? '')), Finding::STATUS_RISK_ACCEPTED => self::OUTCOME_RISK_ACCEPTED, default => null, }; } public function verificationState(Finding $finding): string { if ((string) $finding->status !== Finding::STATUS_RESOLVED) { return self::VERIFICATION_NOT_APPLICABLE; } $reason = (string) ($finding->resolved_reason ?? ''); if (Finding::isSystemResolveReason($reason)) { return self::VERIFICATION_VERIFIED; } if (Finding::isManualResolveReason($reason)) { return self::VERIFICATION_PENDING; } return self::VERIFICATION_NOT_APPLICABLE; } public function systemReopenReasonFor(Finding $finding): string { return $this->verificationState($finding) === self::VERIFICATION_PENDING ? Finding::REOPEN_REASON_VERIFICATION_FAILED : Finding::REOPEN_REASON_RECURRED_AFTER_RESOLUTION; } /** * @return array */ public function terminalOutcomeOptions(): array { return [ self::OUTCOME_RESOLVED_PENDING_VERIFICATION => $this->terminalOutcomeLabel(self::OUTCOME_RESOLVED_PENDING_VERIFICATION), self::OUTCOME_VERIFIED_CLEARED => $this->terminalOutcomeLabel(self::OUTCOME_VERIFIED_CLEARED), self::OUTCOME_CLOSED_FALSE_POSITIVE => $this->terminalOutcomeLabel(self::OUTCOME_CLOSED_FALSE_POSITIVE), self::OUTCOME_CLOSED_DUPLICATE => $this->terminalOutcomeLabel(self::OUTCOME_CLOSED_DUPLICATE), self::OUTCOME_CLOSED_NO_LONGER_APPLICABLE => $this->terminalOutcomeLabel(self::OUTCOME_CLOSED_NO_LONGER_APPLICABLE), self::OUTCOME_RISK_ACCEPTED => $this->terminalOutcomeLabel(self::OUTCOME_RISK_ACCEPTED), ]; } /** * @return array */ public function verificationStateOptions(): array { return [ self::VERIFICATION_PENDING => $this->verificationStateLabel(self::VERIFICATION_PENDING), self::VERIFICATION_VERIFIED => $this->verificationStateLabel(self::VERIFICATION_VERIFIED), self::VERIFICATION_NOT_APPLICABLE => $this->verificationStateLabel(self::VERIFICATION_NOT_APPLICABLE), ]; } public function terminalOutcomeLabel(string $terminalOutcomeKey): string { return match ($terminalOutcomeKey) { self::OUTCOME_RESOLVED_PENDING_VERIFICATION => 'Resolved pending verification', self::OUTCOME_VERIFIED_CLEARED => 'Verified cleared', self::OUTCOME_CLOSED_FALSE_POSITIVE => 'Closed as false positive', self::OUTCOME_CLOSED_DUPLICATE => 'Closed as duplicate', self::OUTCOME_CLOSED_NO_LONGER_APPLICABLE => 'Closed as no longer applicable', self::OUTCOME_RISK_ACCEPTED => 'Risk accepted', default => 'Unknown outcome', }; } public function verificationStateLabel(string $verificationState): string { return match ($verificationState) { self::VERIFICATION_PENDING => 'Pending verification', self::VERIFICATION_VERIFIED => 'Verified cleared', default => 'Not applicable', }; } public function reportBucket(string $terminalOutcomeKey): string { return match ($terminalOutcomeKey) { self::OUTCOME_RESOLVED_PENDING_VERIFICATION => 'remediation_pending_verification', self::OUTCOME_VERIFIED_CLEARED => 'remediation_verified', self::OUTCOME_RISK_ACCEPTED => 'accepted_risk', default => 'administrative_closure', }; } public function compactOutcomeSummary(array $counts): ?string { $parts = []; foreach ($this->orderedOutcomeKeys() as $outcomeKey) { $count = (int) ($counts[$outcomeKey] ?? 0); if ($count < 1) { continue; } $parts[] = sprintf('%d %s', $count, strtolower($this->terminalOutcomeLabel($outcomeKey))); } return $parts === [] ? null : implode(', ', $parts); } /** * @return array */ public function orderedOutcomeKeys(): array { return [ self::OUTCOME_RESOLVED_PENDING_VERIFICATION, self::OUTCOME_VERIFIED_CLEARED, self::OUTCOME_CLOSED_FALSE_POSITIVE, self::OUTCOME_CLOSED_DUPLICATE, self::OUTCOME_CLOSED_NO_LONGER_APPLICABLE, self::OUTCOME_RISK_ACCEPTED, ]; } private function resolvedTerminalOutcomeKey(string $reason): ?string { if (Finding::isSystemResolveReason($reason)) { return self::OUTCOME_VERIFIED_CLEARED; } if (Finding::isManualResolveReason($reason)) { return self::OUTCOME_RESOLVED_PENDING_VERIFICATION; } return null; } private function closedTerminalOutcomeKey(string $reason): ?string { return match ($reason) { Finding::CLOSE_REASON_FALSE_POSITIVE => self::OUTCOME_CLOSED_FALSE_POSITIVE, Finding::CLOSE_REASON_DUPLICATE => self::OUTCOME_CLOSED_DUPLICATE, Finding::CLOSE_REASON_NO_LONGER_APPLICABLE => self::OUTCOME_CLOSED_NO_LONGER_APPLICABLE, default => null, }; } }