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