|null $settings Optional override (tests pass in fixtures). */ public static function build(?array $settings = null): ContainerInterface { $settings ??= require __DIR__ . '/../../config/settings.php'; $builder = new ContainerBuilder(); $builder->useAutowiring(true); $builder->addDefinitions([ 'settings' => $settings, 'settings.db' => $settings['db'], 'settings.ui_service_token' => $settings['ui_service_token'] ?? '', 'settings.app_env' => $settings['app_env'] ?? 'production', 'settings.log_level' => $settings['log_level'] ?? \Monolog\Level::Info, 'settings.oidc_default_role' => $settings['oidc_default_role'] ?? Role::Viewer, 'settings.score_hard_cutoff_days' => (int) ($settings['score_hard_cutoff_days'] ?? 365), 'settings.rate_limit_per_second' => (int) ($settings['rate_limit_per_second'] ?? 60), 'settings.internal_job_token' => (string) ($settings['internal_job_token'] ?? ''), 'settings.internal_cidr_allowlist' => (string) ($settings['internal_cidr_allowlist'] ?? ''), 'settings.api_docs_public' => (bool) ($settings['api_docs_public'] ?? false), 'settings.job_recompute_max_runtime_seconds' => (int) ($settings['job_recompute_max_runtime_seconds'] ?? 240), 'settings.job_recompute_max_rows_per_tick' => (int) ($settings['job_recompute_max_rows_per_tick'] ?? 5000), 'settings.job_audit_retention_days' => (int) ($settings['job_audit_retention_days'] ?? 180), 'settings.cidr_evaluator_ttl_seconds' => (int) ($settings['cidr_evaluator_ttl_seconds'] ?? 60), 'settings.blocklist_cache_ttl_seconds' => (int) ($settings['blocklist_cache_ttl_seconds'] ?? 30), 'settings.geoip' => $settings['geoip'] ?? [ 'enabled' => true, 'provider' => 'dbip', 'country_db' => '/data/geoip/country.mmdb', 'asn_db' => '/data/geoip/asn.mmdb', 'maxmind_license_key' => '', 'ipinfo_token' => '', 'refresh_interval_days' => 7, ], ConnectionFactory::class => factory(static function (ContainerInterface $c): ConnectionFactory { /** @var array{driver: string, sqlite_path: string, mysql_host: string, mysql_port: int, mysql_database: string, mysql_username: string, mysql_password: string} $db */ $db = $c->get('settings.db'); return new ConnectionFactory($db); }), Connection::class => factory(static function (ContainerInterface $c): Connection { /** @var ConnectionFactory $factory */ $factory = $c->get(ConnectionFactory::class); return $factory->create(); }), LoggerInterface::class => factory(static function (ContainerInterface $c): LoggerInterface { $logger = new Logger('api'); /** @var \Monolog\Level $level */ $level = $c->get('settings.log_level'); $handler = new StreamHandler('php://stdout', $level); $handler->setFormatter(new JsonFormatter()); $logger->pushHandler($handler); $logger->pushProcessor(new \App\Infrastructure\Logging\SecretScrubbingProcessor()); return $logger; }), ResponseFactoryInterface::class => autowire(ResponseFactory::class), Clock::class => autowire(SystemClock::class), TokenHasher::class => autowire(), TokenIssuer::class => autowire(), TokenRepository::class => autowire(), RoleMappingRepository::class => autowire(), UserRepository::class => autowire(), ReporterRepository::class => autowire(), ConsumerRepository::class => autowire(), CategoryRepository::class => autowire(), ReportRepository::class => autowire(), IpScoreRepository::class => autowire(), ManualBlockRepository::class => autowire(), AllowlistRepository::class => autowire(), PolicyRepository::class => autowire(), IpEnrichmentRepository::class => autowire(), IpHistoryRepository::class => autowire(), DashboardStatsRepository::class => autowire(), CidrEvaluatorFactory::class => factory(static function (ContainerInterface $c): CidrEvaluatorFactory { /** @var ManualBlockRepository $manual */ $manual = $c->get(ManualBlockRepository::class); /** @var AllowlistRepository $allow */ $allow = $c->get(AllowlistRepository::class); /** @var Clock $clock */ $clock = $c->get(Clock::class); /** @var LoggerInterface $logger */ $logger = $c->get(LoggerInterface::class); /** @var int $ttl */ $ttl = $c->get('settings.cidr_evaluator_ttl_seconds'); return new CidrEvaluatorFactory($manual, $allow, $clock, $logger, $ttl); }), EffectiveStatusService::class => autowire(), BlocklistBuilder::class => autowire(), BlocklistCache::class => factory(static function (ContainerInterface $c): BlocklistCache { /** @var BlocklistBuilder $builder */ $builder = $c->get(BlocklistBuilder::class); /** @var Clock $clock */ $clock = $c->get(Clock::class); /** @var int $ttl */ $ttl = $c->get('settings.blocklist_cache_ttl_seconds'); return new BlocklistCache($builder, $clock, $ttl); }), ServiceTokenBootstrap::class => autowire(), TokenAuthenticationMiddleware::class => autowire(), ImpersonationMiddleware::class => autowire(), AuditContextMiddleware::class => autowire(), \App\Infrastructure\Http\Middleware\RequestBodySizeLimitMiddleware::class => factory(static function (ContainerInterface $c): \App\Infrastructure\Http\Middleware\RequestBodySizeLimitMiddleware { /** @var ResponseFactoryInterface $rf */ $rf = $c->get(ResponseFactoryInterface::class); // SEC_REVIEW F69: 256 KiB global cap. Per-endpoint // caps (e.g. the 4 KiB `metadata` cap on /report) // layer on top. return new \App\Infrastructure\Http\Middleware\RequestBodySizeLimitMiddleware($rf, 256 * 1024); }), AuditRepository::class => autowire(), AuditEmitter::class => autowire(DbAuditEmitter::class), AppSettings::class => autowire(DbAppSettings::class), PairScorer::class => factory(static function (ContainerInterface $c): PairScorer { /** @var ReportRepository $reports */ $reports = $c->get(ReportRepository::class); /** @var CategoryRepository $categories */ $categories = $c->get(CategoryRepository::class); /** @var Clock $clock */ $clock = $c->get(Clock::class); /** @var int $cutoff */ $cutoff = $c->get('settings.score_hard_cutoff_days'); return new PairScorer($reports, $categories, $clock, $cutoff); }), RateLimiter::class => factory(static function (ContainerInterface $c): RateLimiter { /** @var Clock $clock */ $clock = $c->get(Clock::class); /** @var int $perSecond */ $perSecond = $c->get('settings.rate_limit_per_second'); $perSecond = max(1, $perSecond); return new RateLimiter($clock, (float) $perSecond, (float) ($perSecond * 2)); }), RateLimitMiddleware::class => autowire(), JobLockRepository::class => autowire(), JobRunRepository::class => autowire(), JobRunner::class => factory(static function (ContainerInterface $c): JobRunner { /** @var JobLockRepository $locks */ $locks = $c->get(JobLockRepository::class); /** @var JobRunRepository $runs */ $runs = $c->get(JobRunRepository::class); /** @var Clock $clock */ $clock = $c->get(Clock::class); /** @var LoggerInterface $logger */ $logger = $c->get(LoggerInterface::class); return new JobRunner($locks, $runs, $clock, $logger); }), RecomputeScoresJob::class => factory(static function (ContainerInterface $c): RecomputeScoresJob { /** @var ReportRepository $reports */ $reports = $c->get(ReportRepository::class); /** @var IpScoreRepository $ipScores */ $ipScores = $c->get(IpScoreRepository::class); /** @var PairScorer $scorer */ $scorer = $c->get(PairScorer::class); /** @var int $maxRuntime */ $maxRuntime = $c->get('settings.job_recompute_max_runtime_seconds'); /** @var int $maxRows */ $maxRows = $c->get('settings.job_recompute_max_rows_per_tick'); return new RecomputeScoresJob($reports, $ipScores, $scorer, $maxRuntime, $maxRows); }), CleanupAuditJob::class => factory(static function (ContainerInterface $c): CleanupAuditJob { /** @var Connection $conn */ $conn = $c->get(Connection::class); /** @var int $days */ $days = $c->get('settings.job_audit_retention_days'); return new CleanupAuditJob($conn, $days); }), CleanupExpiredManualBlocksJob::class => autowire(), GuzzleClientInterface::class => factory(static function (): GuzzleClientInterface { // SEC_REVIEW F50: lock the GeoIP downloader's HTTP client // down. `allow_redirects` is now a tight whitelist — // HTTPS-only, ≤3 redirects, no Referer header. The // PrivateHostGuardMiddleware refuses to dial loopback / // link-local / RFC1918 / metadata-service hosts even // after a redirect rewrites the URL. $stack = \GuzzleHttp\HandlerStack::create(); $stack->push(\App\Infrastructure\Enrichment\Downloaders\PrivateHostGuardMiddleware::factory()); return new GuzzleClient([ 'handler' => $stack, 'connect_timeout' => 10, 'timeout' => 120, 'http_errors' => true, 'allow_redirects' => [ 'max' => 3, 'protocols' => ['https'], 'strict' => true, 'referer' => false, 'track_redirects' => false, ], 'headers' => [ 'User-Agent' => 'irdb-geoip-refresh/1.0', ], ]); }), RecordAdapter::class => factory(static function (ContainerInterface $c): RecordAdapter { /** @var array{provider: string} $g */ $g = $c->get('settings.geoip'); return $g['provider'] === 'ipinfo' ? new IpinfoRecordAdapter() : new MaxMindRecordAdapter(); }), MmdbEnrichmentService::class => factory(static function (ContainerInterface $c): MmdbEnrichmentService { /** @var array{country_db: string, asn_db: string} $g */ $g = $c->get('settings.geoip'); /** @var RecordAdapter $adapter */ $adapter = $c->get(RecordAdapter::class); /** @var Clock $clock */ $clock = $c->get(Clock::class); /** @var LoggerInterface $logger */ $logger = $c->get(LoggerInterface::class); return new MmdbEnrichmentService($g['country_db'], $g['asn_db'], $adapter, $clock, $logger); }), EnrichmentService::class => factory(static function (ContainerInterface $c): EnrichmentService { /** @var MmdbEnrichmentService $svc */ $svc = $c->get(MmdbEnrichmentService::class); return $svc; }), GeoIpDownloader::class => factory(static function (ContainerInterface $c): GeoIpDownloader { /** @var array{provider: string, maxmind_license_key: string, ipinfo_token: string} $g */ $g = $c->get('settings.geoip'); /** @var GuzzleClientInterface $http */ $http = $c->get(GuzzleClientInterface::class); /** @var Clock $clock */ $clock = $c->get(Clock::class); return match ($g['provider']) { 'maxmind' => new MaxMindDownloader($http, $g['maxmind_license_key']), 'ipinfo' => new IPinfoDownloader($http, $g['ipinfo_token']), default => new DbipDownloader($http, $clock), }; }), EnrichPendingJob::class => factory(static function (ContainerInterface $c): EnrichPendingJob { /** @var EnrichmentService $svc */ $svc = $c->get(EnrichmentService::class); /** @var IpEnrichmentRepository $repo */ $repo = $c->get(IpEnrichmentRepository::class); return new EnrichPendingJob($svc, $repo); }), RefreshGeoipJob::class => factory(static function (ContainerInterface $c): RefreshGeoipJob { /** @var GeoIpDownloader $downloader */ $downloader = $c->get(GeoIpDownloader::class); /** @var MmdbEnrichmentService $svc */ $svc = $c->get(MmdbEnrichmentService::class); /** @var IpEnrichmentRepository $repo */ $repo = $c->get(IpEnrichmentRepository::class); /** @var array{country_db: string, asn_db: string, refresh_interval_days: int} $g */ $g = $c->get('settings.geoip'); return new RefreshGeoipJob( $downloader, $svc, $repo, $g['country_db'], $g['asn_db'], (int) $g['refresh_interval_days'], ); }), TickJob::class => factory(static function (ContainerInterface $c): TickJob { /** @var JobRunner $runner */ $runner = $c->get(JobRunner::class); /** @var JobRunRepository $runs */ $runs = $c->get(JobRunRepository::class); // Closure indirection: TickJob iterates JobRegistry at // run() time, after the registry is fully populated. A // direct JobRegistry dependency would create a build-time // cycle (registry → tick → registry). $resolver = static fn (): array => $c->get(JobRegistry::class)->all(); return new TickJob($resolver, $runner, $runs); }), JobRegistry::class => factory(static function (ContainerInterface $c): JobRegistry { $registry = new JobRegistry(); /** @var RecomputeScoresJob $recompute */ $recompute = $c->get(RecomputeScoresJob::class); /** @var CleanupAuditJob $cleanup */ $cleanup = $c->get(CleanupAuditJob::class); /** @var CleanupExpiredManualBlocksJob $cleanupExpired */ $cleanupExpired = $c->get(CleanupExpiredManualBlocksJob::class); /** @var EnrichPendingJob $enrich */ $enrich = $c->get(EnrichPendingJob::class); /** @var RefreshGeoipJob $refresh */ $refresh = $c->get(RefreshGeoipJob::class); /** @var TickJob $tick */ $tick = $c->get(TickJob::class); $registry->register($recompute); $registry->register($cleanup); $registry->register($cleanupExpired); $registry->register($enrich); $registry->register($refresh); $registry->register($tick); return $registry; }), JobsController::class => autowire(), InternalNetworkMiddleware::class => factory(static function (ContainerInterface $c): InternalNetworkMiddleware { /** @var ResponseFactoryInterface $rf */ $rf = $c->get(ResponseFactoryInterface::class); /** @var string $raw */ $raw = $c->get('settings.internal_cidr_allowlist'); return new InternalNetworkMiddleware($rf, InternalNetworkMiddleware::parseCidrList($raw)); }), InternalTokenMiddleware::class => factory(static function (ContainerInterface $c): InternalTokenMiddleware { /** @var ResponseFactoryInterface $rf */ $rf = $c->get(ResponseFactoryInterface::class); /** @var string $token */ $token = $c->get('settings.internal_job_token'); return new InternalTokenMiddleware($rf, $token); }), JsonErrorHandler::class => factory(static function (ContainerInterface $c): JsonErrorHandler { /** @var ResponseFactoryInterface $factory */ $factory = $c->get(ResponseFactoryInterface::class); /** @var LoggerInterface $logger */ $logger = $c->get(LoggerInterface::class); return new JsonErrorHandler( $factory, $logger, $c->get('settings.app_env') === 'development', ); }), AuthController::class => factory(static function (ContainerInterface $c): AuthController { /** @var Role|null $role */ $role = $c->get('settings.oidc_default_role'); /** @var UserRepository $users */ $users = $c->get(UserRepository::class); /** @var AuditEmitter $audit */ $audit = $c->get(AuditEmitter::class); /** @var Connection $conn */ $conn = $c->get(Connection::class); return new AuthController($users, $role ?? Role::Viewer, $audit, $conn); }), MeController::class => autowire(), ReportersController::class => autowire(), ConsumersController::class => autowire(), TokensController::class => autowire(), ReportController::class => autowire(), ManualBlocksController::class => autowire(), AllowlistController::class => autowire(), PoliciesController::class => autowire(), BlocklistController::class => autowire(), IpsController::class => autowire(), StatsController::class => autowire(), CategoriesController::class => autowire(), UsersController::class => autowire(), AuditController::class => autowire(), JobsAdminController::class => autowire(), MaintenanceController::class => autowire(), AppSettingsController::class => autowire(), ConfigController::class => factory(static function (ContainerInterface $c): ConfigController { /** @var array $settings */ $settings = $c->get('settings'); return new ConfigController($settings); }), DocsController::class => factory(static function (): DocsController { return new DocsController(__DIR__ . '/../../public/openapi.yaml'); }), ]); return $builder->build(); } }