Files
033f0c84da v4.7 (#12478)
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>
2026-11-06 14:25:02 +01:00

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;
}
}