| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Integration\Reputation;
- use App\Domain\Ip\Cidr;
- use App\Domain\Ip\IpAddress;
- use App\Domain\Reputation\BlocklistBuilder;
- use App\Infrastructure\Policy\PolicyRepository;
- use App\Tests\Integration\Support\AppTestCase;
- use Doctrine\DBAL\ParameterType;
- /**
- * Integration tests for the per-policy `BlocklistBuilder`.
- * - allowlist filters scored entries
- * - manual subnet covering a scored IP suppresses the single entry
- * - sort: IPv4 before IPv6, stable across rebuilds
- * - dedup: scored single + manual single → scored wins (carries categories)
- */
- final class BlocklistBuilderTest extends AppTestCase
- {
- public function testScoredIpAppearsInBlocklistWithCategorySlugs(): void
- {
- $bruteForceId = (int) $this->db->fetchOne('SELECT id FROM categories WHERE slug = :s', ['s' => 'brute_force']);
- $this->insertScore('203.0.113.5', $bruteForceId, 2.0);
- $blocklist = $this->buildFor('paranoid');
- self::assertCount(1, $blocklist->entries);
- $entry = $blocklist->entries[0];
- self::assertSame('203.0.113.5', $entry->ipOrCidr);
- self::assertSame('scored', $entry->reason);
- self::assertContains('brute_force', $entry->categories);
- }
- public function testAllowlistedScoredIpIsExcluded(): void
- {
- $bruteForceId = (int) $this->db->fetchOne('SELECT id FROM categories WHERE slug = :s', ['s' => 'brute_force']);
- $this->insertScore('203.0.113.5', $bruteForceId, 2.0);
- $this->insertAllowIp('203.0.113.5');
- $blocklist = $this->buildFor('paranoid');
- self::assertCount(0, $blocklist->entries);
- }
- public function testManualSubnetSuppressesScoredSingleIpInside(): void
- {
- $bruteForceId = (int) $this->db->fetchOne('SELECT id FROM categories WHERE slug = :s', ['s' => 'brute_force']);
- $this->insertScore('198.51.100.42', $bruteForceId, 2.0);
- $this->insertManualSubnet('198.51.100.0/24');
- $blocklist = $this->buildFor('paranoid');
- self::assertCount(1, $blocklist->entries);
- self::assertSame('198.51.100.0/24', $blocklist->entries[0]->ipOrCidr);
- self::assertTrue($blocklist->entries[0]->isCidr);
- self::assertSame('manual', $blocklist->entries[0]->reason);
- }
- public function testV4EntriesSortedBeforeV6(): void
- {
- $bruteForceId = (int) $this->db->fetchOne('SELECT id FROM categories WHERE slug = :s', ['s' => 'brute_force']);
- $this->insertScore('2001:db8::1', $bruteForceId, 2.0);
- $this->insertScore('203.0.113.5', $bruteForceId, 2.0);
- $blocklist = $this->buildFor('paranoid');
- self::assertCount(2, $blocklist->entries);
- self::assertTrue($blocklist->entries[0]->isIpv4);
- self::assertFalse($blocklist->entries[1]->isIpv4);
- }
- public function testManualSingleIpAndScoredSamePrefersScored(): void
- {
- $bruteForceId = (int) $this->db->fetchOne('SELECT id FROM categories WHERE slug = :s', ['s' => 'brute_force']);
- $this->insertScore('203.0.113.7', $bruteForceId, 2.0);
- $this->insertManualIp('203.0.113.7');
- $blocklist = $this->buildFor('paranoid');
- self::assertCount(1, $blocklist->entries);
- self::assertSame('scored', $blocklist->entries[0]->reason);
- }
- private function buildFor(string $policyName): \App\Domain\Reputation\Blocklist
- {
- /** @var PolicyRepository $policies */
- $policies = $this->container->get(PolicyRepository::class);
- $policy = $policies->findByName($policyName);
- self::assertNotNull($policy);
- /** @var BlocklistBuilder $builder */
- $builder = $this->container->get(BlocklistBuilder::class);
- return $builder->build($policy);
- }
- private function insertManualIp(string $ip): void
- {
- $bin = IpAddress::fromString($ip)->binary();
- $stmt = $this->db->prepare(
- 'INSERT INTO manual_blocks (kind, ip_bin, reason) VALUES (:kind, :ip_bin, :reason)'
- );
- $stmt->bindValue('kind', 'ip');
- $stmt->bindValue('ip_bin', $bin, ParameterType::LARGE_OBJECT);
- $stmt->bindValue('reason', 't');
- $stmt->executeStatement();
- }
- private function insertManualSubnet(string $cidr): void
- {
- $c = Cidr::fromString($cidr);
- $stmt = $this->db->prepare(
- 'INSERT INTO manual_blocks (kind, network_bin, prefix_length, reason) VALUES (:kind, :net, :pl, :reason)'
- );
- $stmt->bindValue('kind', 'subnet');
- $stmt->bindValue('net', $c->network(), ParameterType::LARGE_OBJECT);
- $stmt->bindValue('pl', $c->prefixLength(), ParameterType::INTEGER);
- $stmt->bindValue('reason', 't');
- $stmt->executeStatement();
- }
- private function insertAllowIp(string $ip): void
- {
- $bin = IpAddress::fromString($ip)->binary();
- $stmt = $this->db->prepare(
- 'INSERT INTO allowlist (kind, ip_bin, reason) VALUES (:kind, :ip_bin, :reason)'
- );
- $stmt->bindValue('kind', 'ip');
- $stmt->bindValue('ip_bin', $bin, ParameterType::LARGE_OBJECT);
- $stmt->bindValue('reason', 't');
- $stmt->executeStatement();
- }
- private function insertScore(string $ip, int $categoryId, float $score): void
- {
- $ipObj = IpAddress::fromString($ip);
- $stmt = $this->db->prepare(
- 'INSERT INTO ip_scores (ip_bin, ip_text, category_id, score, report_count_30d, last_report_at, recomputed_at) '
- . 'VALUES (:b, :t, :c, :s, 1, :now, :now)'
- );
- $stmt->bindValue('b', $ipObj->binary(), ParameterType::LARGE_OBJECT);
- $stmt->bindValue('t', $ipObj->text());
- $stmt->bindValue('c', $categoryId, ParameterType::INTEGER);
- $stmt->bindValue('s', number_format($score, 4, '.', ''));
- $stmt->bindValue('now', (new \DateTimeImmutable('now'))->format('Y-m-d H:i:s'));
- $stmt->executeStatement();
- }
- }
|