$currentFindings * @param list $candidateFindings * @param array|null $currentEol The end-of-life distro of the current image. * @param array|null $candidateEol The end-of-life distro of the candidate. */ public function isBetter(array $currentFindings, array $candidateFindings, RelevanceRules $rules, ?array $currentEol, ?array $candidateEol): bool { $current = []; foreach ($this->notIgnored($currentFindings, $rules) as $finding) { foreach ($this->keys($finding) as $key) { $current[$key] = true; } } $candidateRelevant = $this->relevant($candidateFindings, $rules); foreach ($candidateRelevant as $finding) { if (SecurityRules::exploitSignal($finding) !== null && array_intersect_key(array_flip($this->keys($finding)), $current) === []) { return false; } } $currentRelevant = $this->relevant($currentFindings, $rules); $fixesExploit = array_filter($this->fixed($currentRelevant, $candidateFindings), fn (Finding $finding): bool => SecurityRules::exploitSignal($finding) !== null) !== []; return $fixesExploit || ($currentEol !== null && $candidateEol === null) || count($candidateRelevant) + ($candidateEol === null ? 0 : 1) < count($currentRelevant) + ($currentEol === null ? 0 : 1); } /** * IDs and aliases of the relevant current findings that the candidate no longer has under any of their IDs. * * @param list $current * @param list $candidate All findings of the candidate. * @return list */ public function fixedIds(array $current, array $candidate, RelevanceRules $rules): array { $fixed = []; foreach ($this->fixed($this->relevant($current, $rules), $candidate) as $finding) { array_push($fixed, ...$this->ids($finding)); } return array_values(array_unique($fixed)); } /** * @param list $findings * @return list */ public function relevant(array $findings, RelevanceRules $rules): array { return array_values(array_filter($findings, fn (Finding $finding): bool => SecurityRules::isRelevant($finding, $rules))); } /** * The current findings that the candidate no longer has. * * @param list $current * @param list $candidate * @return list */ public function fixed(array $current, array $candidate): array { $remaining = []; foreach ($candidate as $finding) { foreach ($this->keys($finding) as $key) { $remaining[$key] = true; } } return array_values(array_filter($current, fn (Finding $finding) => array_intersect_key(array_flip($this->keys($finding)), $remaining) === [])); } /** * The current findings that the other list also has under one of their IDs. * * @param list $current * @param list $other * @return list */ public function shared(array $current, array $other): array { $fixed = $this->fixed($current, $other); return array_values(array_filter($current, fn (Finding $finding) => ! in_array($finding, $fixed, true))); } /** * The relevant critical, high, and medium findings of the candidate that the relevant current findings do not have * under any of their IDs, counted once per finding ID. The same basis as `counts()` of `fixed()`. * * @param list $currentRelevant * @param list $candidate All findings of the candidate. */ public function added(array $currentRelevant, array $candidate, RelevanceRules $rules): int { $known = []; foreach ($currentRelevant as $finding) { foreach ($this->keys($finding) as $key) { $known[$key] = true; } } $added = []; foreach ($this->relevant($candidate, $rules) as $finding) { if (in_array(strtolower($finding->severity), self::COUNTED_SEVERITIES, true) && array_intersect_key(array_flip($this->keys($finding)), $known) === []) { $added[mb_strtolower($finding->id)] = true; } } return count($added); } /** * @param list $findings * @return array{critical: int, high: int, medium: int} */ public function counts(array $findings): array { $counts = array_fill_keys(self::COUNTED_SEVERITIES, 0); foreach ($findings as $finding) { $severity = strtolower($finding->severity); if (isset($counts[$severity])) { $counts[$severity]++; } } return $counts; } /** * @param list $findings * @return list */ private function notIgnored(array $findings, RelevanceRules $rules): array { return array_values(array_filter($findings, fn (Finding $finding): bool => $rules->ignores->matches($finding) === null)); } /** * @return list */ private function ids(Finding $finding): array { return array_values(array_unique(array_filter([$finding->id, ...$finding->aliases], fn (string $id) => $id !== ''))); } /** * IDs compare without case, like ignores and the fix classifier. * * @return list */ private function keys(Finding $finding): array { return array_values(array_unique(array_map('mb_strtolower', $this->ids($finding)))); } }