mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Co-authored-by: Aditya Tripathi <aditya@climactic.co>
95 lines
3.0 KiB
PHP
95 lines
3.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services\ImageScan;
|
|
|
|
/**
|
|
* Which findings matter (spec 15.2): relevant findings and the exploit signal.
|
|
*/
|
|
final class SecurityRules
|
|
{
|
|
public const EXPLOITED = 'exploited';
|
|
|
|
public const LIKELY = 'likely';
|
|
|
|
private const LIKELY_EPSS = 0.10;
|
|
|
|
private const LIKELY_SCORE = 7.0;
|
|
|
|
private const SERIOUS_SEVERITIES = ['critical', 'high'];
|
|
|
|
private const OS_PACKAGE_TYPES = ['apk', 'deb', 'rpm'];
|
|
|
|
/**
|
|
* `exploited` for a finding in CISA KEV; `likely` for EPSS 0.10 or more with CVSS 7.0 or more (severity high or
|
|
* critical without a score); else null.
|
|
*/
|
|
public static function exploitSignal(Finding $finding): ?string
|
|
{
|
|
if ($finding->kev) {
|
|
return self::EXPLOITED;
|
|
}
|
|
|
|
$severe = $finding->cvssScore !== null
|
|
? $finding->cvssScore >= self::LIKELY_SCORE
|
|
: self::isSerious($finding);
|
|
|
|
return $finding->epss !== null && $finding->epss >= self::LIKELY_EPSS && $severe ? self::LIKELY : null;
|
|
}
|
|
|
|
/**
|
|
* Not ignored, not a CPE-only OS match, and an exploit signal, severity critical or high, or a counted medium.
|
|
*/
|
|
public static function isRelevant(Finding $finding, RelevanceRules $rules): bool
|
|
{
|
|
return ! self::isCpeOnlyOsMatch($finding)
|
|
&& (self::exploitSignal($finding) !== null || self::isSerious($finding) || self::isCountedMedium($finding, $rules->countMedium))
|
|
&& $rules->ignores->matches($finding) === null;
|
|
}
|
|
|
|
/**
|
|
* An OS package (apk, deb, rpm) that Grype matched only by CPE.
|
|
*/
|
|
public static function isCpeOnlyOsMatch(Finding $finding): bool
|
|
{
|
|
return str_starts_with(strtolower($finding->namespace), 'nvd:cpe')
|
|
&& in_array(strtolower($finding->type), self::OS_PACKAGE_TYPES, true);
|
|
}
|
|
|
|
/**
|
|
* The IDs and aliases of the relevant findings, lowercase.
|
|
*
|
|
* @param list<Finding> $findings
|
|
* @return list<string>
|
|
*/
|
|
public static function relevantIds(array $findings, RelevanceRules $rules): array
|
|
{
|
|
$ids = [];
|
|
foreach ($findings as $finding) {
|
|
if (self::isRelevant($finding, $rules)) {
|
|
foreach ([$finding->id, ...$finding->aliases] as $id) {
|
|
if ($id !== '') {
|
|
$ids[] = mb_strtolower($id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return array_values(array_unique($ids));
|
|
}
|
|
|
|
/**
|
|
* A medium finding with EPSS 0.10 or more, or any medium finding when the rules count medium problems. Not an
|
|
* exploit signal.
|
|
*/
|
|
private static function isCountedMedium(Finding $finding, bool $countMedium): bool
|
|
{
|
|
return strtolower($finding->severity) === 'medium'
|
|
&& ($countMedium || ($finding->epss !== null && $finding->epss >= self::LIKELY_EPSS));
|
|
}
|
|
|
|
private static function isSerious(Finding $finding): bool
|
|
{
|
|
return in_array(strtolower($finding->severity), self::SERIOUS_SEVERITIES, true);
|
|
}
|
|
}
|