db->fetchOne('SELECT id FROM categories WHERE slug = :s', ['s' => 'brute_force']); $spamId = (int) $this->db->fetchOne('SELECT id FROM categories WHERE slug = :s', ['s' => 'spam']); $this->seedScores(50_000, [$bruteForceId, $spamId]); $this->seedManualSubnets(100); /** @var PolicyRepository $policies */ $policies = $this->container->get(PolicyRepository::class); $paranoid = $policies->findByName('paranoid'); self::assertNotNull($paranoid); /** @var BlocklistBuilder $builder */ $builder = $this->container->get(BlocklistBuilder::class); $start = hrtime(true); $blocklist = $builder->build($paranoid); $elapsedMs = (hrtime(true) - $start) / 1_000_000.0; self::assertGreaterThan(0, $blocklist->count()); // Second run to measure warm path (SPEC's <500ms is the warm budget). $start = hrtime(true); $blocklist = $builder->build($paranoid); $elapsedMs = (hrtime(true) - $start) / 1_000_000.0; self::assertLessThan( 500.0, $elapsedMs, sprintf('blocklist build took %.2f ms; budget 500 ms', $elapsedMs) ); // Surface the measured number for the PROGRESS notes. fwrite(STDERR, sprintf("\n[perf] BlocklistBuilder@50k = %.1f ms (entries=%d)\n", $elapsedMs, $blocklist->count())); } /** * @param list $categoryIds */ private function seedScores(int $count, array $categoryIds): void { // Mix of v4 (75%) and v6 (25%) addresses, varied scores so that // some are above and some below the 0.3 paranoid threshold. $this->db->beginTransaction(); $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)' ); $now = (new \DateTimeImmutable('now'))->format('Y-m-d H:i:s'); for ($i = 0; $i < $count; ++$i) { $isV4 = ($i % 4) !== 0; if ($isV4) { $a = ($i >> 16) & 0xff; $b = ($i >> 8) & 0xff; $c = $i & 0xff; // Use 10.x.x.x to keep these well outside any allowlist concern in the test DB. $text = sprintf('10.%d.%d.%d', $a, $b, $c); } else { $text = sprintf('2001:db8::%x', $i); } $bin = $isV4 ? "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff" . pack('C4', 10, ($i >> 16) & 0xff, ($i >> 8) & 0xff, $i & 0xff) : (string) inet_pton($text); $score = number_format(0.5 + (($i % 5) * 0.5), 4, '.', ''); // 0.5 .. 2.5 $catId = $categoryIds[$i % count($categoryIds)]; $stmt->bindValue('b', $bin, ParameterType::LARGE_OBJECT); $stmt->bindValue('t', $text); $stmt->bindValue('c', $catId, ParameterType::INTEGER); $stmt->bindValue('s', $score); $stmt->bindValue('now', $now); $stmt->executeStatement(); } $this->db->commit(); } private function seedManualSubnets(int $count): void { $this->db->beginTransaction(); $stmt = $this->db->prepare( 'INSERT INTO manual_blocks (kind, network_bin, prefix_length, reason) VALUES (:kind, :net, :pl, :reason)' ); for ($i = 0; $i < $count; ++$i) { $cidr = Cidr::fromString(sprintf('192.0.%d.0/24', $i)); $stmt->bindValue('kind', 'subnet'); $stmt->bindValue('net', $cidr->network(), ParameterType::LARGE_OBJECT); $stmt->bindValue('pl', $cidr->prefixLength(), ParameterType::INTEGER); $stmt->bindValue('reason', 'perf'); $stmt->executeStatement(); } $this->db->commit(); } }