|
|
@@ -44,49 +44,150 @@ final class MaintenanceController
|
|
|
use AdminControllerSupport;
|
|
|
|
|
|
private const DEMO_REPORTERS = [
|
|
|
- ['name' => 'demo-web-edge', 'description' => 'Demo: edge webserver', 'trust_weight' => 1.0],
|
|
|
- ['name' => 'demo-fail2ban', 'description' => 'Demo: fail2ban agents', 'trust_weight' => 1.2],
|
|
|
- ['name' => 'demo-honeypot', 'description' => 'Demo: research honeypot', 'trust_weight' => 1.5],
|
|
|
+ ['name' => 'demo-web-edge', 'description' => 'Demo: edge webserver', 'trust_weight' => 1.0],
|
|
|
+ ['name' => 'demo-web-eu', 'description' => 'Demo: EU edge webserver', 'trust_weight' => 1.0],
|
|
|
+ ['name' => 'demo-web-us', 'description' => 'Demo: US edge webserver', 'trust_weight' => 1.0],
|
|
|
+ ['name' => 'demo-fail2ban', 'description' => 'Demo: fail2ban agents', 'trust_weight' => 1.2],
|
|
|
+ ['name' => 'demo-fail2ban-prod','description' => 'Demo: fail2ban prod fleet', 'trust_weight' => 1.3],
|
|
|
+ ['name' => 'demo-honeypot', 'description' => 'Demo: research honeypot', 'trust_weight' => 1.5],
|
|
|
+ ['name' => 'demo-honeypot-asia','description' => 'Demo: APAC honeypot mesh', 'trust_weight' => 1.6],
|
|
|
+ ['name' => 'demo-ids-suricata', 'description' => 'Demo: Suricata IDS sensors', 'trust_weight' => 1.4],
|
|
|
+ ['name' => 'demo-mail-gateway', 'description' => 'Demo: anti-spam mail gateway', 'trust_weight' => 1.1],
|
|
|
+ ['name' => 'demo-waf-cloud', 'description' => 'Demo: cloud WAF aggregator', 'trust_weight' => 1.3],
|
|
|
];
|
|
|
|
|
|
private const DEMO_CONSUMERS = [
|
|
|
- ['name' => 'demo-firewall-prod', 'description' => 'Demo: production edge firewall', 'policy' => 'moderate'],
|
|
|
- ['name' => 'demo-proxy-staging', 'description' => 'Demo: staging proxy', 'policy' => 'strict'],
|
|
|
+ ['name' => 'demo-firewall-prod', 'description' => 'Demo: production edge firewall', 'policy' => 'moderate'],
|
|
|
+ ['name' => 'demo-firewall-dmz', 'description' => 'Demo: DMZ perimeter firewall', 'policy' => 'paranoid'],
|
|
|
+ ['name' => 'demo-proxy-staging', 'description' => 'Demo: staging proxy', 'policy' => 'strict'],
|
|
|
+ ['name' => 'demo-haproxy-public', 'description' => 'Demo: public HAProxy LB', 'policy' => 'moderate'],
|
|
|
+ ['name' => 'demo-nginx-api', 'description' => 'Demo: API nginx ingress', 'policy' => 'strict'],
|
|
|
+ ['name' => 'demo-vpn-gateway', 'description' => 'Demo: VPN gateway', 'policy' => 'paranoid'],
|
|
|
+ ['name' => 'demo-mail-relay', 'description' => 'Demo: outbound mail relay', 'policy' => 'moderate'],
|
|
|
];
|
|
|
|
|
|
private const DEMO_MANUAL_BLOCKS = [
|
|
|
- ['kind' => 'ip', 'value' => '198.51.100.42', 'reason' => 'Demo: known malicious actor'],
|
|
|
- ['kind' => 'subnet', 'value' => '198.51.100.0/24', 'reason' => 'Demo: blanket block on test net'],
|
|
|
+ ['kind' => 'ip', 'value' => '198.51.100.42', 'reason' => 'Demo: known malicious actor', 'age_days' => 1],
|
|
|
+ ['kind' => 'ip', 'value' => '198.51.100.55', 'reason' => 'Demo: persistent brute-force', 'age_days' => 2],
|
|
|
+ ['kind' => 'ip', 'value' => '203.0.113.190', 'reason' => 'Demo: WAF auto-block', 'age_days' => 3],
|
|
|
+ ['kind' => 'ip', 'value' => '192.0.2.244', 'reason' => 'Demo: spam relay', 'age_days' => 5],
|
|
|
+ ['kind' => 'ip', 'value' => '198.51.100.140', 'reason' => 'Demo: scanner cluster member', 'age_days' => 7],
|
|
|
+ ['kind' => 'ip', 'value' => '203.0.113.230', 'reason' => 'Demo: confirmed C2 endpoint', 'age_days' => 10],
|
|
|
+ ['kind' => 'ip', 'value' => '192.0.2.99', 'reason' => 'Demo: malware dropper host', 'age_days' => 14],
|
|
|
+ ['kind' => 'ip', 'value' => '198.51.100.250', 'reason' => 'Demo: botnet member', 'age_days' => 21],
|
|
|
+ ['kind' => 'ip', 'value' => '203.0.113.120', 'reason' => 'Demo: credential stuffing', 'age_days' => 30],
|
|
|
+ ['kind' => 'ip', 'value' => '192.0.2.42', 'reason' => 'Demo: reflective DDoS source', 'age_days' => 45],
|
|
|
+ ['kind' => 'subnet', 'value' => '198.51.100.0/24','reason' => 'Demo: blanket block on test net','age_days' => 60],
|
|
|
+ ['kind' => 'subnet', 'value' => '192.0.2.128/26', 'reason' => 'Demo: hostile subnet range', 'age_days' => 75],
|
|
|
];
|
|
|
|
|
|
private const DEMO_ALLOWLIST = [
|
|
|
- ['kind' => 'ip', 'value' => '203.0.113.10', 'reason' => 'Demo: trusted partner'],
|
|
|
+ ['kind' => 'ip', 'value' => '203.0.113.10', 'reason' => 'Demo: trusted partner'],
|
|
|
+ ['kind' => 'ip', 'value' => '203.0.113.50', 'reason' => 'Demo: monitoring probe'],
|
|
|
+ ['kind' => 'ip', 'value' => '203.0.113.77', 'reason' => 'Demo: corporate VPN exit'],
|
|
|
['kind' => 'subnet', 'value' => '203.0.113.0/28', 'reason' => 'Demo: office NAT range'],
|
|
|
+ ['kind' => 'subnet', 'value' => '198.18.0.0/16', 'reason' => 'Demo: lab benchmark range'],
|
|
|
];
|
|
|
|
|
|
+ /**
|
|
|
+ * 70 demo IPs spanning many countries / ASNs / address blocks. RFC 5737
|
|
|
+ * documentation prefixes plus the 100.64.0.0/10 CGNAT space and a
|
|
|
+ * handful of 2001:db8::/32 doc IPv6 endpoints. These IPs are not
|
|
|
+ * routable on the internet so they're safe to seed.
|
|
|
+ */
|
|
|
private const DEMO_IPS = [
|
|
|
- ['ip' => '192.0.2.5', 'country' => 'US', 'asn' => 13335, 'org' => 'Cloudflare, Inc.'],
|
|
|
- ['ip' => '192.0.2.17', 'country' => 'DE', 'asn' => 24940, 'org' => 'Hetzner Online GmbH'],
|
|
|
- ['ip' => '192.0.2.42', 'country' => 'RU', 'asn' => 12389, 'org' => 'Rostelecom'],
|
|
|
- ['ip' => '192.0.2.99', 'country' => 'CN', 'asn' => 4134, 'org' => 'China Telecom'],
|
|
|
- ['ip' => '192.0.2.130', 'country' => 'BR', 'asn' => 8167, 'org' => 'V tal'],
|
|
|
- ['ip' => '192.0.2.180', 'country' => 'FR', 'asn' => 16276, 'org' => 'OVH SAS'],
|
|
|
- ['ip' => '192.0.2.211', 'country' => 'NL', 'asn' => 60781, 'org' => 'LeaseWeb Netherlands'],
|
|
|
- ['ip' => '198.51.100.7', 'country' => 'IN', 'asn' => 9498, 'org' => 'BHARTI Airtel'],
|
|
|
- ['ip' => '198.51.100.55','country' => 'GB', 'asn' => 5089, 'org' => 'Virgin Media'],
|
|
|
- ['ip' => '198.51.100.88','country' => 'JP', 'asn' => 4713, 'org' => 'NTT Communications'],
|
|
|
- ['ip' => '198.51.100.140','country' => 'KR','asn' => 4766, 'org' => 'Korea Telecom'],
|
|
|
- ['ip' => '198.51.100.200','country' => 'TR','asn' => 9121, 'org' => 'Turk Telekom'],
|
|
|
- ['ip' => '203.0.113.50', 'country' => 'CA', 'asn' => 577, 'org' => 'Bell Canada'],
|
|
|
- ['ip' => '203.0.113.77', 'country' => 'AU', 'asn' => 7545, 'org' => 'TPG Telecom'],
|
|
|
- ['ip' => '203.0.113.120','country' => 'IT', 'asn' => 12874, 'org' => 'Fastweb'],
|
|
|
- ['ip' => '203.0.113.150','country' => 'ES', 'asn' => 3352, 'org' => 'Telefonica de Espana'],
|
|
|
- ['ip' => '203.0.113.190','country' => 'PL', 'asn' => 5617, 'org' => 'Orange Polska'],
|
|
|
- ['ip' => '203.0.113.230','country' => 'UA', 'asn' => 13188, 'org' => 'Content Delivery Network'],
|
|
|
- ['ip' => '192.0.2.244', 'country' => 'VN', 'asn' => 7552, 'org' => 'Viettel Group'],
|
|
|
- ['ip' => '198.51.100.250','country' => 'ID','asn' => 17974, 'org' => 'PT Telekomunikasi Indonesia'],
|
|
|
- ['ip' => '2001:db8::1', 'country' => 'US', 'asn' => 15169, 'org' => 'Google LLC'],
|
|
|
- ['ip' => '2001:db8::abcd','country' => 'IE','asn' => 16509, 'org' => 'Amazon.com, Inc.'],
|
|
|
+ ['ip' => '192.0.2.5', 'country' => 'US', 'asn' => 13335, 'org' => 'Cloudflare, Inc.'],
|
|
|
+ ['ip' => '192.0.2.17', 'country' => 'DE', 'asn' => 24940, 'org' => 'Hetzner Online GmbH'],
|
|
|
+ ['ip' => '192.0.2.31', 'country' => 'DE', 'asn' => 8881, 'org' => '1&1 Versatel Deutschland'],
|
|
|
+ ['ip' => '192.0.2.42', 'country' => 'RU', 'asn' => 12389, 'org' => 'Rostelecom'],
|
|
|
+ ['ip' => '192.0.2.58', 'country' => 'RU', 'asn' => 8359, 'org' => 'MTS PJSC'],
|
|
|
+ ['ip' => '192.0.2.71', 'country' => 'BY', 'asn' => 6697, 'org' => 'Beltelecom'],
|
|
|
+ ['ip' => '192.0.2.86', 'country' => 'CN', 'asn' => 4134, 'org' => 'China Telecom'],
|
|
|
+ ['ip' => '192.0.2.99', 'country' => 'CN', 'asn' => 4837, 'org' => 'China Unicom'],
|
|
|
+ ['ip' => '192.0.2.114', 'country' => 'CN', 'asn' => 9808, 'org' => 'China Mobile'],
|
|
|
+ ['ip' => '192.0.2.130', 'country' => 'BR', 'asn' => 8167, 'org' => 'V.tal'],
|
|
|
+ ['ip' => '192.0.2.142', 'country' => 'BR', 'asn' => 26599, 'org' => 'TELEFONICA BRASIL'],
|
|
|
+ ['ip' => '192.0.2.156', 'country' => 'AR', 'asn' => 10481, 'org' => 'Telefonica de Argentina'],
|
|
|
+ ['ip' => '192.0.2.170', 'country' => 'MX', 'asn' => 8151, 'org' => 'Uninet S.A. de C.V.'],
|
|
|
+ ['ip' => '192.0.2.180', 'country' => 'FR', 'asn' => 16276, 'org' => 'OVH SAS'],
|
|
|
+ ['ip' => '192.0.2.196', 'country' => 'FR', 'asn' => 3215, 'org' => 'Orange S.A.'],
|
|
|
+ ['ip' => '192.0.2.211', 'country' => 'NL', 'asn' => 60781, 'org' => 'LeaseWeb Netherlands'],
|
|
|
+ ['ip' => '192.0.2.225', 'country' => 'NL', 'asn' => 1136, 'org' => 'KPN B.V.'],
|
|
|
+ ['ip' => '192.0.2.239', 'country' => 'BE', 'asn' => 5432, 'org' => 'Proximus NV'],
|
|
|
+ ['ip' => '192.0.2.244', 'country' => 'VN', 'asn' => 7552, 'org' => 'Viettel Group'],
|
|
|
+ ['ip' => '192.0.2.250', 'country' => 'TH', 'asn' => 23969, 'org' => 'TOT Public Company'],
|
|
|
+
|
|
|
+ ['ip' => '198.51.100.7', 'country' => 'IN', 'asn' => 9498, 'org' => 'BHARTI Airtel'],
|
|
|
+ ['ip' => '198.51.100.21', 'country' => 'IN', 'asn' => 55836, 'org' => 'Reliance Jio'],
|
|
|
+ ['ip' => '198.51.100.34', 'country' => 'PK', 'asn' => 17557, 'org' => 'Pakistan Telecom'],
|
|
|
+ ['ip' => '198.51.100.42', 'country' => 'BD', 'asn' => 24389, 'org' => 'Grameenphone Ltd.'],
|
|
|
+ ['ip' => '198.51.100.55', 'country' => 'GB', 'asn' => 5089, 'org' => 'Virgin Media'],
|
|
|
+ ['ip' => '198.51.100.66', 'country' => 'GB', 'asn' => 2856, 'org' => 'BT Group'],
|
|
|
+ ['ip' => '198.51.100.79', 'country' => 'IE', 'asn' => 5466, 'org' => 'eircom Limited'],
|
|
|
+ ['ip' => '198.51.100.88', 'country' => 'JP', 'asn' => 4713, 'org' => 'NTT Communications'],
|
|
|
+ ['ip' => '198.51.100.103', 'country' => 'JP', 'asn' => 17676, 'org' => 'SoftBank Corp.'],
|
|
|
+ ['ip' => '198.51.100.117', 'country' => 'KR', 'asn' => 4766, 'org' => 'Korea Telecom'],
|
|
|
+ ['ip' => '198.51.100.140', 'country' => 'KR', 'asn' => 9318, 'org' => 'SK Broadband'],
|
|
|
+ ['ip' => '198.51.100.155', 'country' => 'TW', 'asn' => 3462, 'org' => 'Chunghwa Telecom'],
|
|
|
+ ['ip' => '198.51.100.168', 'country' => 'HK', 'asn' => 4760, 'org' => 'HKT Limited'],
|
|
|
+ ['ip' => '198.51.100.180', 'country' => 'SG', 'asn' => 3758, 'org' => 'Singtel'],
|
|
|
+ ['ip' => '198.51.100.193', 'country' => 'MY', 'asn' => 4788, 'org' => 'TM Net'],
|
|
|
+ ['ip' => '198.51.100.200', 'country' => 'TR', 'asn' => 9121, 'org' => 'Turk Telekom'],
|
|
|
+ ['ip' => '198.51.100.213', 'country' => 'GR', 'asn' => 3329, 'org' => 'OTE SA'],
|
|
|
+ ['ip' => '198.51.100.225', 'country' => 'RO', 'asn' => 8708, 'org' => 'RCS & RDS'],
|
|
|
+ ['ip' => '198.51.100.240', 'country' => 'BG', 'asn' => 8866, 'org' => 'Vivacom'],
|
|
|
+ ['ip' => '198.51.100.250', 'country' => 'ID', 'asn' => 17974, 'org' => 'PT Telekomunikasi Indonesia'],
|
|
|
+
|
|
|
+ ['ip' => '203.0.113.7', 'country' => 'US', 'asn' => 7922, 'org' => 'Comcast Cable'],
|
|
|
+ ['ip' => '203.0.113.18', 'country' => 'US', 'asn' => 20115, 'org' => 'Charter Communications'],
|
|
|
+ ['ip' => '203.0.113.29', 'country' => 'US', 'asn' => 32934, 'org' => 'Meta Platforms, Inc.'],
|
|
|
+ ['ip' => '203.0.113.50', 'country' => 'CA', 'asn' => 577, 'org' => 'Bell Canada'],
|
|
|
+ ['ip' => '203.0.113.63', 'country' => 'CA', 'asn' => 812, 'org' => 'Rogers Communications'],
|
|
|
+ ['ip' => '203.0.113.77', 'country' => 'AU', 'asn' => 7545, 'org' => 'TPG Telecom'],
|
|
|
+ ['ip' => '203.0.113.91', 'country' => 'AU', 'asn' => 1221, 'org' => 'Telstra Corporation'],
|
|
|
+ ['ip' => '203.0.113.105', 'country' => 'NZ', 'asn' => 4648, 'org' => 'Spark New Zealand'],
|
|
|
+ ['ip' => '203.0.113.120', 'country' => 'IT', 'asn' => 12874, 'org' => 'Fastweb'],
|
|
|
+ ['ip' => '203.0.113.135', 'country' => 'IT', 'asn' => 1267, 'org' => 'Wind Tre S.p.A.'],
|
|
|
+ ['ip' => '203.0.113.150', 'country' => 'ES', 'asn' => 3352, 'org' => 'Telefonica de Espana'],
|
|
|
+ ['ip' => '203.0.113.165', 'country' => 'ES', 'asn' => 12479, 'org' => 'Orange Espagne'],
|
|
|
+ ['ip' => '203.0.113.178', 'country' => 'PT', 'asn' => 8657, 'org' => 'MEO'],
|
|
|
+ ['ip' => '203.0.113.190', 'country' => 'PL', 'asn' => 5617, 'org' => 'Orange Polska'],
|
|
|
+ ['ip' => '203.0.113.205', 'country' => 'CZ', 'asn' => 6855, 'org' => 'O2 Czech Republic'],
|
|
|
+ ['ip' => '203.0.113.218', 'country' => 'HU', 'asn' => 5483, 'org' => 'Magyar Telekom'],
|
|
|
+ ['ip' => '203.0.113.230', 'country' => 'UA', 'asn' => 13188, 'org' => 'Content Delivery Network'],
|
|
|
+ ['ip' => '203.0.113.245', 'country' => 'EG', 'asn' => 8452, 'org' => 'TE Data'],
|
|
|
+
|
|
|
+ ['ip' => '100.64.5.10', 'country' => 'ZA', 'asn' => 36937, 'org' => 'MTN South Africa'],
|
|
|
+ ['ip' => '100.64.32.55', 'country' => 'NG', 'asn' => 36873, 'org' => 'MTN Nigeria'],
|
|
|
+ ['ip' => '100.64.71.7', 'country' => 'KE', 'asn' => 33771, 'org' => 'Safaricom'],
|
|
|
+ ['ip' => '100.64.99.42', 'country' => 'AE', 'asn' => 5384, 'org' => 'Emirates Telecom'],
|
|
|
+ ['ip' => '100.64.128.3', 'country' => 'SA', 'asn' => 25019, 'org' => 'Saudi Telecom'],
|
|
|
+ ['ip' => '100.64.155.88', 'country' => 'IL', 'asn' => 8551, 'org' => 'Bezeq International'],
|
|
|
+ ['ip' => '100.64.180.21', 'country' => 'CL', 'asn' => 7418, 'org' => 'Movistar Chile'],
|
|
|
+
|
|
|
+ ['ip' => '2001:db8::1', 'country' => 'US', 'asn' => 15169, 'org' => 'Google LLC'],
|
|
|
+ ['ip' => '2001:db8::42', 'country' => 'US', 'asn' => 8075, 'org' => 'Microsoft Corporation'],
|
|
|
+ ['ip' => '2001:db8::abcd', 'country' => 'IE', 'asn' => 16509, 'org' => 'Amazon.com, Inc.'],
|
|
|
+ ['ip' => '2001:db8::cafe', 'country' => 'NL', 'asn' => 14061, 'org' => 'DigitalOcean LLC'],
|
|
|
+ ['ip' => '2001:db8::beef', 'country' => 'SG', 'asn' => 16509, 'org' => 'Amazon AWS Asia'],
|
|
|
+ ['ip' => '2001:db8:1::55', 'country' => 'JP', 'asn' => 2497, 'org' => 'IIJ Inc.'],
|
|
|
+ ];
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Reports span this many days into the past. Older reports decay more,
|
|
|
+ * which yields a long-tail distribution in the dashboard chart.
|
|
|
+ */
|
|
|
+ private const DEMO_REPORT_HORIZON_DAYS = 90;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Per-IP report-volume tiers. Indexed by `ip_index % count`. Each tuple
|
|
|
+ * is `[low, high]` inclusive — actual count is jittered between them.
|
|
|
+ */
|
|
|
+ private const DEMO_REPORT_VOLUME_TIERS = [
|
|
|
+ [15, 22], // mostly-quiet IPs
|
|
|
+ [25, 40], // moderate offenders
|
|
|
+ [45, 70], // heavy offenders
|
|
|
+ [80, 110], // hostile / persistent
|
|
|
];
|
|
|
|
|
|
public function __construct(
|
|
|
@@ -205,6 +306,12 @@ final class MaintenanceController
|
|
|
'enrichment' => 0,
|
|
|
];
|
|
|
|
|
|
+ // Wrap the bulk seed in a single transaction. The new dataset is
|
|
|
+ // ~3,000 reports — without a transaction, SQLite fsyncs per insert
|
|
|
+ // and the seed takes tens of seconds.
|
|
|
+ $this->connection->beginTransaction();
|
|
|
+
|
|
|
+ try {
|
|
|
$reporterIds = [];
|
|
|
foreach (self::DEMO_REPORTERS as $r) {
|
|
|
$reporterIds[$r['name']] = $this->reporters->create(
|
|
|
@@ -227,6 +334,12 @@ final class MaintenanceController
|
|
|
++$summary['consumers'];
|
|
|
}
|
|
|
|
|
|
+ $horizonHours = self::DEMO_REPORT_HORIZON_DAYS * 24;
|
|
|
+ $reporterCount = count(self::DEMO_REPORTERS);
|
|
|
+ $tierCount = count(self::DEMO_REPORT_VOLUME_TIERS);
|
|
|
+ $categorySlugs = array_keys($categoryIds);
|
|
|
+ $catCount = count($categorySlugs);
|
|
|
+
|
|
|
foreach (self::DEMO_IPS as $i => $row) {
|
|
|
try {
|
|
|
$ip = IpAddress::fromString($row['ip']);
|
|
|
@@ -243,20 +356,49 @@ final class MaintenanceController
|
|
|
++$summary['enrichment'];
|
|
|
++$summary['ips'];
|
|
|
|
|
|
- // Stagger reports across the past 30 days so decay produces
|
|
|
- // visually distinct scores in the dashboard.
|
|
|
- $reportCount = 2 + ($i % 6) * 3;
|
|
|
+ // Stagger reports across the past DEMO_REPORT_HORIZON_DAYS so
|
|
|
+ // decay produces visually distinct scores. Each IP draws from a
|
|
|
+ // volume tier so the dataset has a realistic long-tail shape.
|
|
|
+ [$tierLow, $tierHigh] = self::DEMO_REPORT_VOLUME_TIERS[$i % $tierCount];
|
|
|
+ $reportCount = $tierLow + (($i * 7 + 3) % max(1, $tierHigh - $tierLow + 1));
|
|
|
+
|
|
|
+ // Each IP "specialises" in one or two primary categories so the
|
|
|
+ // category breakdown isn't perfectly uniform.
|
|
|
+ $primaryCatIdx = $i % $catCount;
|
|
|
+ $secondaryCatIdx = ($i * 3 + 1) % $catCount;
|
|
|
+
|
|
|
for ($n = 0; $n < $reportCount; ++$n) {
|
|
|
- $slugIdx = ($i + $n) % count($categoryIds);
|
|
|
- $slug = array_keys($categoryIds)[$slugIdx];
|
|
|
+ // 70% primary, 20% secondary, 10% any other category.
|
|
|
+ $rolled = ($n * 13 + $i * 5) % 10;
|
|
|
+ if ($rolled < 7) {
|
|
|
+ $slugIdx = $primaryCatIdx;
|
|
|
+ } elseif ($rolled < 9) {
|
|
|
+ $slugIdx = $secondaryCatIdx;
|
|
|
+ } else {
|
|
|
+ $slugIdx = ($i + $n * 2) % $catCount;
|
|
|
+ }
|
|
|
+ $slug = $categorySlugs[$slugIdx];
|
|
|
$catId = $categoryIds[$slug];
|
|
|
- $reporterIdx = $n % count(self::DEMO_REPORTERS);
|
|
|
+
|
|
|
+ $reporterIdx = ($n + $i) % $reporterCount;
|
|
|
$reporterName = self::DEMO_REPORTERS[$reporterIdx]['name'];
|
|
|
$reporterId = $reporterIds[$reporterName];
|
|
|
$weight = self::DEMO_REPORTERS[$reporterIdx]['trust_weight'];
|
|
|
|
|
|
- $ageHours = (int) (($n * 17 + $i * 23) % (30 * 24));
|
|
|
- $receivedAt = $now->modify('-' . $ageHours . ' hours');
|
|
|
+ // Ages spread across the horizon with a bias toward "recent"
|
|
|
+ // (more activity in last 14 days), and minute-level jitter.
|
|
|
+ $bucket = ($n * 31 + $i * 19) % 100;
|
|
|
+ if ($bucket < 35) {
|
|
|
+ $ageHours = ($n * 11 + $i * 7) % (14 * 24);
|
|
|
+ } elseif ($bucket < 70) {
|
|
|
+ $ageHours = (14 * 24) + (($n * 17 + $i * 23) % (30 * 24));
|
|
|
+ } else {
|
|
|
+ $ageHours = (44 * 24) + (($n * 29 + $i * 13) % max(1, $horizonHours - 44 * 24));
|
|
|
+ }
|
|
|
+ $ageMinutes = ($n * 7 + $i * 11) % 60;
|
|
|
+ $receivedAt = $now
|
|
|
+ ->modify('-' . $ageHours . ' hours')
|
|
|
+ ->modify('-' . $ageMinutes . ' minutes');
|
|
|
|
|
|
$this->reports->insert(
|
|
|
$ip->binary(),
|
|
|
@@ -277,14 +419,14 @@ final class MaintenanceController
|
|
|
foreach (self::DEMO_MANUAL_BLOCKS as $mb) {
|
|
|
try {
|
|
|
if ($mb['kind'] === 'ip') {
|
|
|
- $this->manualBlocks->createIp(
|
|
|
+ $blockId = $this->manualBlocks->createIp(
|
|
|
IpAddress::fromString($mb['value']),
|
|
|
$mb['reason'],
|
|
|
null,
|
|
|
$actingUserId,
|
|
|
);
|
|
|
} else {
|
|
|
- $this->manualBlocks->createSubnet(
|
|
|
+ $blockId = $this->manualBlocks->createSubnet(
|
|
|
Cidr::fromString($mb['value']),
|
|
|
$mb['reason'],
|
|
|
null,
|
|
|
@@ -292,6 +434,21 @@ final class MaintenanceController
|
|
|
);
|
|
|
}
|
|
|
++$summary['manual_blocks'];
|
|
|
+
|
|
|
+ // Backfill `created_at` so the bans-by-day chart on the
|
|
|
+ // dashboard shows realistic spread instead of a single
|
|
|
+ // spike at seed time.
|
|
|
+ $ageDays = isset($mb['age_days']) ? (int) $mb['age_days'] : 0;
|
|
|
+ if ($ageDays > 0) {
|
|
|
+ $createdAt = $now->modify('-' . $ageDays . ' days');
|
|
|
+ $this->connection->executeStatement(
|
|
|
+ 'UPDATE manual_blocks SET created_at = :created_at WHERE id = :id',
|
|
|
+ [
|
|
|
+ 'created_at' => $createdAt->format('Y-m-d H:i:s'),
|
|
|
+ 'id' => $blockId,
|
|
|
+ ],
|
|
|
+ );
|
|
|
+ }
|
|
|
} catch (\Throwable) {
|
|
|
// Ignore — demo data shouldn't block setup on a single bad row.
|
|
|
}
|
|
|
@@ -318,6 +475,14 @@ final class MaintenanceController
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ $this->connection->commit();
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ if ($this->connection->isTransactionActive()) {
|
|
|
+ $this->connection->rollBack();
|
|
|
+ }
|
|
|
+ throw $e;
|
|
|
+ }
|
|
|
+
|
|
|
// Recompute so dashboards show the seeded scores immediately.
|
|
|
$outcome = $this->jobRunner->run($this->recomputeJob, ['full' => true], 'manual');
|
|
|
|