| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493 |
- <?php
- declare(strict_types=1);
- namespace App\App;
- use App\Application\Admin\AllowlistController;
- use App\Application\Admin\AppSettingsController;
- use App\Application\Admin\AuditController;
- use App\Application\Admin\CategoriesController;
- use App\Application\Admin\ConfigController;
- use App\Application\Admin\ConsumersController;
- use App\Application\Admin\IpsController;
- use App\Application\Admin\JobsAdminController;
- use App\Application\Admin\MaintenanceController;
- use App\Application\Admin\ManualBlocksController;
- use App\Application\Admin\MeController;
- use App\Application\Admin\PoliciesController;
- use App\Application\Admin\ReportersController;
- use App\Application\Admin\StatsController;
- use App\Application\Admin\TokensController;
- use App\Application\Admin\UsersController;
- use App\Application\Auth\AuthController;
- use App\Application\Internal\JobsController;
- use App\Application\Jobs\CleanupAuditJob;
- use App\Application\Jobs\CleanupExpiredManualBlocksJob;
- use App\Application\Jobs\EnrichPendingJob;
- use App\Application\Jobs\RecomputeScoresJob;
- use App\Application\Jobs\RefreshGeoipJob;
- use App\Application\Jobs\TickJob;
- use App\Application\Public\BlocklistController;
- use App\Application\Public\DocsController;
- use App\Application\Public\ReportController;
- use App\Domain\Audit\AuditEmitter;
- use App\Domain\Auth\Role;
- use App\Domain\Auth\TokenHasher;
- use App\Domain\Auth\TokenIssuer;
- use App\Domain\Enrichment\EnrichmentService;
- use App\Domain\Reputation\BlocklistBuilder;
- use App\Domain\Reputation\EffectiveStatusService;
- use App\Domain\Reputation\PairScorer;
- use App\Domain\Settings\AppSettings;
- use App\Domain\Time\Clock;
- use App\Domain\Time\SystemClock;
- use App\Infrastructure\Allowlist\AllowlistRepository;
- use App\Infrastructure\Audit\AuditRepository;
- use App\Infrastructure\Audit\DbAuditEmitter;
- use App\Infrastructure\Auth\RoleMappingRepository;
- use App\Infrastructure\Auth\ServiceTokenBootstrap;
- use App\Infrastructure\Auth\TokenRepository;
- use App\Infrastructure\Auth\UserRepository;
- use App\Infrastructure\Category\CategoryRepository;
- use App\Infrastructure\Consumer\ConsumerRepository;
- use App\Infrastructure\Db\ConnectionFactory;
- use App\Infrastructure\Enrichment\Downloaders\DbipDownloader;
- use App\Infrastructure\Enrichment\Downloaders\GeoIpDownloader;
- use App\Infrastructure\Enrichment\Downloaders\IPinfoDownloader;
- use App\Infrastructure\Enrichment\Downloaders\MaxMindDownloader;
- use App\Infrastructure\Enrichment\IpinfoRecordAdapter;
- use App\Infrastructure\Enrichment\MaxMindRecordAdapter;
- use App\Infrastructure\Enrichment\MmdbEnrichmentService;
- use App\Infrastructure\Enrichment\RecordAdapter;
- use App\Infrastructure\Http\JsonErrorHandler;
- use App\Infrastructure\Http\Middleware\AuditContextMiddleware;
- use App\Infrastructure\Http\Middleware\ImpersonationMiddleware;
- use App\Infrastructure\Http\Middleware\InternalNetworkMiddleware;
- use App\Infrastructure\Http\Middleware\InternalTokenMiddleware;
- use App\Infrastructure\Http\Middleware\RateLimitMiddleware;
- use App\Infrastructure\Http\Middleware\TokenAuthenticationMiddleware;
- use App\Infrastructure\Http\RateLimiter;
- use App\Infrastructure\Jobs\JobLockRepository;
- use App\Infrastructure\Jobs\JobRegistry;
- use App\Infrastructure\Jobs\JobRunner;
- use App\Infrastructure\Jobs\JobRunRepository;
- use App\Infrastructure\ManualBlock\ManualBlockRepository;
- use App\Infrastructure\Policy\PolicyRepository;
- use App\Infrastructure\Reporter\ReporterRepository;
- use App\Infrastructure\Reputation\BlocklistCache;
- use App\Infrastructure\Reputation\CidrEvaluatorFactory;
- use App\Infrastructure\Reputation\DashboardStatsRepository;
- use App\Infrastructure\Reputation\IpEnrichmentRepository;
- use App\Infrastructure\Reputation\IpHistoryRepository;
- use App\Infrastructure\Reputation\IpScoreRepository;
- use App\Infrastructure\Reputation\ReportRepository;
- use App\Infrastructure\Settings\DbAppSettings;
- use function DI\autowire;
- use DI\ContainerBuilder;
- use function DI\factory;
- use Doctrine\DBAL\Connection;
- use GuzzleHttp\Client as GuzzleClient;
- use GuzzleHttp\ClientInterface as GuzzleClientInterface;
- use Monolog\Formatter\JsonFormatter;
- use Monolog\Handler\StreamHandler;
- use Monolog\Logger;
- use Psr\Container\ContainerInterface;
- use Psr\Http\Message\ResponseFactoryInterface;
- use Psr\Log\LoggerInterface;
- use Slim\Psr7\Factory\ResponseFactory;
- /**
- * Builds the api's DI container.
- *
- * M04 additions: ReporterRepository / ConsumerRepository / CategoryRepository /
- * ReportRepository / IpScoreRepository, the PairScorer + Clock pair, and the
- * RateLimiter singleton plus its middleware. The rate-limit settings flow
- * through `settings` so tests can override capacity/refill cleanly.
- */
- final class Container
- {
- /**
- * @param array<string, mixed>|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<string, mixed> $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();
- }
- }
|