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>
124 lines
3.9 KiB
PHP
124 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services\ImageScan;
|
|
|
|
use App\Models\ImageScanIgnore;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Collection;
|
|
|
|
/**
|
|
* Matches findings against the active ignores of one resource and its team.
|
|
*/
|
|
final class IgnoreMatcher
|
|
{
|
|
/** An ignore of a problem with an exploit signal applies for at most this many days after it was made (spec 15.2). */
|
|
public const EXPLOIT_SIGNAL_IGNORE_DAYS = 90;
|
|
|
|
/** @var array<string, list<ImageScanIgnore>> */
|
|
private array $byVulnerability = [];
|
|
|
|
/** @var array<string, list<ImageScanIgnore>> */
|
|
private array $byPackage = [];
|
|
|
|
/**
|
|
* @param Collection<int, ImageScanIgnore> $ignores
|
|
*/
|
|
private function __construct(Collection $ignores)
|
|
{
|
|
foreach ($ignores as $ignore) {
|
|
if ($ignore->expires_at !== null && $ignore->expires_at->isPast()) {
|
|
continue;
|
|
}
|
|
|
|
$vulnerabilityId = $this->normalize($ignore->vulnerability_id);
|
|
$package = $this->normalizePackage($ignore->package);
|
|
|
|
if ($vulnerabilityId !== null) {
|
|
$this->byVulnerability[$vulnerabilityId][] = $ignore;
|
|
} elseif ($package !== null) {
|
|
$this->byPackage[$package][] = $ignore;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, ImageScanIgnore> $ignores
|
|
*/
|
|
public static function fromIgnores(Collection $ignores): self
|
|
{
|
|
return new self($ignores);
|
|
}
|
|
|
|
public static function forResource(Model $resource): self
|
|
{
|
|
$teamId = method_exists($resource, 'team') ? $resource->team()?->id : null;
|
|
|
|
if ($teamId === null) {
|
|
return new self(collect());
|
|
}
|
|
|
|
$ignores = ImageScanIgnore::query()
|
|
->active()
|
|
->where('team_id', $teamId)
|
|
->where(function (Builder $query) use ($resource) {
|
|
$query->whereNull('resource_type')
|
|
->orWhere(function (Builder $query) use ($resource) {
|
|
$query->where('resource_type', $resource->getMorphClass())
|
|
->where('resource_id', $resource->getKey());
|
|
});
|
|
})
|
|
->get();
|
|
|
|
return new self($ignores);
|
|
}
|
|
|
|
public function matches(Finding $finding): ?ImageScanIgnore
|
|
{
|
|
$exploitSignal = SecurityRules::exploitSignal($finding) !== null && ! SecurityRules::isCpeOnlyOsMatch($finding);
|
|
|
|
foreach ([$finding->id, ...$finding->aliases] as $candidate) {
|
|
foreach ($this->byVulnerability[$this->normalize($candidate)] ?? [] as $ignore) {
|
|
$package = $this->normalizePackage($ignore->package);
|
|
|
|
if (($package === null || $package === $finding->package) && $this->applies($ignore, $exploitSignal)) {
|
|
return $ignore;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($this->byPackage[$finding->package] ?? [] as $ignore) {
|
|
if ($this->applies($ignore, $exploitSignal)) {
|
|
return $ignore;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* An ignore of a problem with an exploit signal ends 90 days after it was made, also when it was set to never end
|
|
* or the problem got the signal later (a team-wide or package ignore, a new KEV entry).
|
|
*/
|
|
private function applies(ImageScanIgnore $ignore, bool $exploitSignal): bool
|
|
{
|
|
if (! $exploitSignal || $ignore->created_at === null) {
|
|
return true;
|
|
}
|
|
|
|
return $ignore->created_at->gte(now()->subDays(self::EXPLOIT_SIGNAL_IGNORE_DAYS));
|
|
}
|
|
|
|
private function normalize(?string $value): ?string
|
|
{
|
|
$value = trim((string) $value);
|
|
|
|
return $value === '' ? null : mb_strtolower($value);
|
|
}
|
|
|
|
private function normalizePackage(?string $value): ?string
|
|
{
|
|
return $value === null || $value === '' ? null : $value;
|
|
}
|
|
}
|