|
@@ -9,9 +9,13 @@ use App\Domain\Audit\AuditEmitter;
|
|
|
use App\Domain\Category\Category;
|
|
use App\Domain\Category\Category;
|
|
|
use App\Domain\Policy\Policy;
|
|
use App\Domain\Policy\Policy;
|
|
|
use App\Domain\Reputation\BlocklistBuilder;
|
|
use App\Domain\Reputation\BlocklistBuilder;
|
|
|
|
|
+use App\Domain\Reputation\Decay;
|
|
|
|
|
+use App\Domain\Time\Clock;
|
|
|
use App\Infrastructure\Category\CategoryRepository;
|
|
use App\Infrastructure\Category\CategoryRepository;
|
|
|
|
|
+use App\Infrastructure\ManualBlock\ManualBlockRepository;
|
|
|
use App\Infrastructure\Policy\PolicyRepository;
|
|
use App\Infrastructure\Policy\PolicyRepository;
|
|
|
use App\Infrastructure\Reputation\BlocklistCache;
|
|
use App\Infrastructure\Reputation\BlocklistCache;
|
|
|
|
|
+use DateTimeImmutable;
|
|
|
use Psr\Http\Message\ResponseInterface;
|
|
use Psr\Http\Message\ResponseInterface;
|
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
|
|
|
|
|
@@ -36,6 +40,8 @@ final class PoliciesController
|
|
|
private readonly BlocklistBuilder $blocklistBuilder,
|
|
private readonly BlocklistBuilder $blocklistBuilder,
|
|
|
private readonly BlocklistCache $blocklistCache,
|
|
private readonly BlocklistCache $blocklistCache,
|
|
|
private readonly AuditEmitter $audit,
|
|
private readonly AuditEmitter $audit,
|
|
|
|
|
+ private readonly ManualBlockRepository $manualBlocks,
|
|
|
|
|
+ private readonly Clock $clock,
|
|
|
) {
|
|
) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -278,16 +284,88 @@ final class PoliciesController
|
|
|
|
|
|
|
|
$blocklist = $this->blocklistBuilder->build($policy);
|
|
$blocklist = $this->blocklistBuilder->build($policy);
|
|
|
$sample = array_slice($blocklist->entries, 0, 50);
|
|
$sample = array_slice($blocklist->entries, 0, 50);
|
|
|
- $sampleStrings = array_map(static fn ($e) => $e->ipOrCidr, $sample);
|
|
|
|
|
|
|
+
|
|
|
|
|
+ $catBySlug = [];
|
|
|
|
|
+ foreach ($this->categories->listAll() as $cat) {
|
|
|
|
|
+ /** @var Category $cat */
|
|
|
|
|
+ $catBySlug[$cat->slug] = $cat;
|
|
|
|
|
+ }
|
|
|
|
|
+ $thresholdBySlug = [];
|
|
|
|
|
+ $slugByCategoryId = $this->slugByCategoryId();
|
|
|
|
|
+ foreach ($policy->thresholds as $catId => $threshold) {
|
|
|
|
|
+ $slug = $slugByCategoryId[$catId] ?? null;
|
|
|
|
|
+ if ($slug !== null) {
|
|
|
|
|
+ $thresholdBySlug[$slug] = $threshold;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $manualExpiryByKey = [];
|
|
|
|
|
+ foreach ($this->manualBlocks->listIps() as $mb) {
|
|
|
|
|
+ if ($mb->ip !== null) {
|
|
|
|
|
+ $manualExpiryByKey['ip:' . $mb->ip->text()] = $mb->expiresAt;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ foreach ($this->manualBlocks->listSubnets() as $mb) {
|
|
|
|
|
+ if ($mb->cidr !== null) {
|
|
|
|
|
+ $manualExpiryByKey['cidr:' . $mb->cidr->text()] = $mb->expiresAt;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $now = $this->clock->now();
|
|
|
|
|
+ $sampleOut = [];
|
|
|
|
|
+ foreach ($sample as $entry) {
|
|
|
|
|
+ $expiresAt = null;
|
|
|
|
|
+ $estimated = false;
|
|
|
|
|
+ if ($entry->reason === 'scored') {
|
|
|
|
|
+ $maxDays = null;
|
|
|
|
|
+ foreach ($entry->categoryScores as $slug => $score) {
|
|
|
|
|
+ if (!isset($thresholdBySlug[$slug], $catBySlug[$slug])) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $cat = $catBySlug[$slug];
|
|
|
|
|
+ $days = Decay::daysUntilThreshold(
|
|
|
|
|
+ $cat->decayFunction,
|
|
|
|
|
+ $cat->decayParam,
|
|
|
|
|
+ $score,
|
|
|
|
|
+ $thresholdBySlug[$slug],
|
|
|
|
|
+ );
|
|
|
|
|
+ if ($days === null) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($maxDays === null || $days > $maxDays) {
|
|
|
|
|
+ $maxDays = $days;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($maxDays !== null) {
|
|
|
|
|
+ $seconds = (int) round($maxDays * 86400.0);
|
|
|
|
|
+ $expiresAt = $now->modify(($seconds >= 0 ? '+' : '') . $seconds . ' seconds');
|
|
|
|
|
+ $estimated = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ } elseif ($entry->reason === 'manual') {
|
|
|
|
|
+ $key = ($entry->isCidr ? 'cidr:' : 'ip:') . $entry->ipOrCidr;
|
|
|
|
|
+ $expiresAt = $manualExpiryByKey[$key] ?? null;
|
|
|
|
|
+ }
|
|
|
|
|
+ $sampleOut[] = [
|
|
|
|
|
+ 'ip_or_cidr' => $entry->ipOrCidr,
|
|
|
|
|
+ 'reason' => $entry->reason,
|
|
|
|
|
+ 'expires_at' => self::isoOrNull($expiresAt),
|
|
|
|
|
+ 'expires_estimated' => $estimated,
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
return self::json($response, 200, [
|
|
return self::json($response, 200, [
|
|
|
'count' => $blocklist->count(),
|
|
'count' => $blocklist->count(),
|
|
|
- 'sample' => $sampleStrings,
|
|
|
|
|
|
|
+ 'sample' => $sampleOut,
|
|
|
'generated_at' => $blocklist->generatedAt->format('Y-m-d\TH:i:s\Z'),
|
|
'generated_at' => $blocklist->generatedAt->format('Y-m-d\TH:i:s\Z'),
|
|
|
'policy' => $policy->name,
|
|
'policy' => $policy->name,
|
|
|
]);
|
|
]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private static function isoOrNull(?DateTimeImmutable $dt): ?string
|
|
|
|
|
+ {
|
|
|
|
|
+ return $dt?->format('Y-m-d\TH:i:s\Z');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Resolve a body-supplied `{slug: number}` map to `[category_id => float]`.
|
|
* Resolve a body-supplied `{slug: number}` map to `[category_id => float]`.
|
|
|
* Returns the integer-keyed map on success, or a single human-readable
|
|
* Returns the integer-keyed map on success, or a single human-readable
|