From f8fa834dc2d7ca3ff3e523ea211e42813c77e28c Mon Sep 17 00:00:00 2001 From: butschster Date: Sat, 20 Apr 2024 22:08:18 +0400 Subject: [PATCH] Added integration between api, spa and ws 1. Added integration with Github API 2. Fixed SPA grid 3. Added subscription on WS events --- .docker/centrifugo/config.json | 10 +- app/.rr-prod.yaml | 15 +- app/app/config/broadcasting.php | 18 + app/app/config/cache.php | 4 +- app/app/config/centrifugo.php | 17 + app/app/config/events.php | 9 + .../Bootloader/GithubBootloader.php | 34 + .../Bootloader/RoutesBootloader.php | 15 +- .../BroadcastEventInterceptor.php | 35 + .../Broadcasting/Channel/Channel.php | 17 + .../Broadcasting/Channel/EventsChannel.php | 13 + .../Broadcasting/ShouldBroadcastInterface.php | 14 + app/app/src/Application/Kernel.php | 8 +- .../Endpoint/Centrifugo/ConnectService.php | 30 + .../src/Endpoint/Centrifugo/RPCService.php | 80 + .../Http/Controller/GithubWebhookAction.php | 74 + .../Http/Controller/SettingsAction.php | 30 + .../Http/Resource/SettingsResource.php | 12 + app/app/src/Github/Client.php | 76 + app/app/src/Github/ClientInterface.php | 14 + app/app/src/Github/Event/NewRelease.php | 36 + .../src/Github/Event/RepositoryStarred.php | 36 + app/app/src/Github/WebhookGate.php | 21 + app/composer.json | 14 +- app/composer.lock | 330 +- docker-compose.yaml | 10 +- spa/app/api/Api.ts | 35 + spa/app/api/methods.ts | 27 + spa/app/api/schema.d.ts | 3191 +++++++++++++++++ spa/app/logger.ts | 73 + spa/app/plugins/apiClient.ts | 18 + spa/app/plugins/centrifugo.ts | 35 + spa/app/plugins/logger.ts | 9 + spa/app/ws/RPCClient.ts | 29 + spa/app/ws/channel.ts | 82 + spa/app/ws/client.ts | 58 + spa/assets/img/pacman.png | Bin 30525 -> 0 bytes spa/{public => assets}/site.webmanifest | 0 spa/components/v1/CopyCommand.vue | 5 +- spa/components/v1/Demo.vue | 2 +- spa/components/v1/Demo/Buttons.vue | 17 +- spa/components/v1/Features.vue | 6 +- spa/components/v1/GithubStars.vue | 74 +- spa/components/v1/Hero.vue | 7 +- spa/components/v1/Support.vue | 4 +- spa/components/v1/Team.vue | 2 +- spa/components/v1/Trap.vue | 21 +- spa/config/types.ts | 43 + spa/nuxt.config.ts | 28 +- spa/package-lock.json | 271 +- spa/package.json | 4 + spa/public/images/bg.jpg | Bin 811135 -> 0 bytes spa/public/images/clients.png | Bin 7397 -> 0 bytes spa/public/images/pacman.png | Bin 30525 -> 0 bytes spa/stores/app.ts | 37 +- 55 files changed, 4965 insertions(+), 85 deletions(-) create mode 100644 app/app/config/broadcasting.php create mode 100644 app/app/config/centrifugo.php create mode 100644 app/app/config/events.php create mode 100644 app/app/src/Application/Bootloader/GithubBootloader.php create mode 100644 app/app/src/Application/Broadcasting/BroadcastEventInterceptor.php create mode 100644 app/app/src/Application/Broadcasting/Channel/Channel.php create mode 100644 app/app/src/Application/Broadcasting/Channel/EventsChannel.php create mode 100644 app/app/src/Application/Broadcasting/ShouldBroadcastInterface.php create mode 100644 app/app/src/Endpoint/Centrifugo/ConnectService.php create mode 100644 app/app/src/Endpoint/Centrifugo/RPCService.php create mode 100644 app/app/src/Endpoint/Http/Controller/GithubWebhookAction.php create mode 100644 app/app/src/Endpoint/Http/Controller/SettingsAction.php create mode 100644 app/app/src/Endpoint/Http/Resource/SettingsResource.php create mode 100644 app/app/src/Github/Client.php create mode 100644 app/app/src/Github/ClientInterface.php create mode 100644 app/app/src/Github/Event/NewRelease.php create mode 100644 app/app/src/Github/Event/RepositoryStarred.php create mode 100644 app/app/src/Github/WebhookGate.php create mode 100644 spa/app/api/Api.ts create mode 100644 spa/app/api/methods.ts create mode 100644 spa/app/api/schema.d.ts create mode 100644 spa/app/logger.ts create mode 100644 spa/app/plugins/apiClient.ts create mode 100644 spa/app/plugins/centrifugo.ts create mode 100644 spa/app/plugins/logger.ts create mode 100644 spa/app/ws/RPCClient.ts create mode 100644 spa/app/ws/channel.ts create mode 100644 spa/app/ws/client.ts delete mode 100644 spa/assets/img/pacman.png rename spa/{public => assets}/site.webmanifest (100%) create mode 100644 spa/config/types.ts delete mode 100644 spa/public/images/bg.jpg delete mode 100644 spa/public/images/clients.png delete mode 100644 spa/public/images/pacman.png diff --git a/.docker/centrifugo/config.json b/.docker/centrifugo/config.json index 9ac01b5..e5ab7be 100644 --- a/.docker/centrifugo/config.json +++ b/.docker/centrifugo/config.json @@ -6,20 +6,16 @@ ], "publish": true, "proxy_publish": true, - "proxy_subscribe": true, "proxy_connect": true, - "allow_subscribe_for_client": true, "address": "0.0.0.0", "port": 8089, "grpc_api": true, "grpc_api_address": "0.0.0.0", "grpc_api_port": 10000, - "proxy_connect_endpoint": "grpc://api:10001", + "proxy_connect_endpoint": "grpc://buggregator-api:10001", "proxy_connect_timeout": "10s", - "proxy_publish_endpoint": "grpc://api:10001", + "proxy_publish_endpoint": "grpc://buggregator-api:10001", "proxy_publish_timeout": "10s", - "proxy_subscribe_endpoint": "grpc://api:10001", - "proxy_subscribe_timeout": "10s", - "proxy_rpc_endpoint": "grpc://api:10001", + "proxy_rpc_endpoint": "grpc://buggregator-api:10001", "proxy_rpc_timeout": "10s" } diff --git a/app/.rr-prod.yaml b/app/.rr-prod.yaml index 1703b2a..d4f911a 100644 --- a/app/.rr-prod.yaml +++ b/app/.rr-prod.yaml @@ -15,12 +15,15 @@ http: address: '0.0.0.0:8000' middleware: - gzip - - static - static: - dir: public - forbid: - - .php - - .htaccess + - headers + headers: + cors: + allowed_origin: ${RR_CORS_ALLOWED_ORIGIN:-*} + allowed_headers: ${RR_CORS_ALLOWED_HEADERS:-*} + allowed_methods: ${RR_CORS_ALLOWED_METHODS:-GET,POST,PUT,DELETE} + allow_credentials: false + exposed_headers: ${RR_CORS_EXPOSED_HEADERS:-Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,Content-Disposition,X-RateLimit-Remaining,X-RateLimit-Retry-After,X-RateLimit-Limit,X-Captcha} + max_age: 600 pool: num_workers: 4 supervisor: diff --git a/app/app/config/broadcasting.php b/app/app/config/broadcasting.php new file mode 100644 index 0000000..d31fdd6 --- /dev/null +++ b/app/app/config/broadcasting.php @@ -0,0 +1,18 @@ + env('BROADCAST_CONNECTION', 'centrifugo'), + 'aliases' => [], + 'connections' => [ + 'centrifugo' => [ + 'driver' => 'centrifugo', + ], + 'null' => [ + 'driver' => NullBroadcast::class, + ], + ], +]; diff --git a/app/app/config/cache.php b/app/app/config/cache.php index 25eada2..db5120b 100644 --- a/app/app/config/cache.php +++ b/app/app/config/cache.php @@ -5,7 +5,9 @@ return [ 'default' => env('CACHE_STORAGE', 'rr'), - 'aliases' => [], + 'aliases' => [ + 'github' => 'rr', + ], 'storages' => [ 'rr' => [ diff --git a/app/app/config/centrifugo.php b/app/app/config/centrifugo.php new file mode 100644 index 0000000..19d5720 --- /dev/null +++ b/app/app/config/centrifugo.php @@ -0,0 +1,17 @@ + [ + RequestType::Connect->value => ConnectService::class, + RequestType::RPC->value => RPCService::class, + ], + 'interceptors' => [ + '*' => [], + ], +]; diff --git a/app/app/config/events.php b/app/app/config/events.php new file mode 100644 index 0000000..e3efc65 --- /dev/null +++ b/app/app/config/events.php @@ -0,0 +1,9 @@ + [ + \App\Application\Broadcasting\BroadcastEventInterceptor::class + ] +]; diff --git a/app/app/src/Application/Bootloader/GithubBootloader.php b/app/app/src/Application/Bootloader/GithubBootloader.php new file mode 100644 index 0000000..45e57ed --- /dev/null +++ b/app/app/src/Application/Bootloader/GithubBootloader.php @@ -0,0 +1,34 @@ + static fn(CacheStorageProviderInterface $cache) => new Client( + client: new \GuzzleHttp\Client([ + 'base_uri' => 'https://api.github.com/', + 'headers' => [ + 'Accept' => 'application/vnd.github.v3+json', + ], + ]), + cache: $cache, + ), + + WebhookGate::class => static fn( + EnvironmentInterface $env, + ) => new WebhookGate(secret: $env->get('GITHUB_WEBHOOK_SECRET', 'secret')), + ]; + } +} diff --git a/app/app/src/Application/Bootloader/RoutesBootloader.php b/app/app/src/Application/Bootloader/RoutesBootloader.php index 2836e2a..03cf099 100644 --- a/app/app/src/Application/Bootloader/RoutesBootloader.php +++ b/app/app/src/Application/Bootloader/RoutesBootloader.php @@ -4,19 +4,15 @@ namespace App\Application\Bootloader; -use Nyholm\Psr7\Stream; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; use Spiral\Bootloader\Http\RoutesBootloader as BaseRoutesBootloader; use Spiral\Cookies\Middleware\CookiesMiddleware; use Spiral\Csrf\Middleware\CsrfMiddleware; -use Spiral\Debug\Middleware\DumperMiddleware; use Spiral\Debug\StateCollector\HttpCollector; use Spiral\Filter\ValidationHandlerMiddleware; use Spiral\Http\Middleware\ErrorHandlerMiddleware; use Spiral\Http\Middleware\JsonPayloadMiddleware; use Spiral\Router\Bootloader\AnnotatedRoutesBootloader; -use Spiral\Router\Loader\Configurator\RoutingConfigurator; +use Spiral\Router\GroupRegistry; use Spiral\Session\Middleware\SessionMiddleware; /** @@ -57,13 +53,8 @@ protected function middlewareGroups(): array ]; } - protected function defineRoutes(RoutingConfigurator $routes): void + protected function configureRouteGroups(GroupRegistry $groups): void { - $routes->default('/') - ->callable(function (ServerRequestInterface $r, ResponseInterface $response) { - return $response - ->withStatus(404) - ->withBody(Stream::create('Not found')); - }); + $groups->getGroup('api')->setPrefix('/api'); } } diff --git a/app/app/src/Application/Broadcasting/BroadcastEventInterceptor.php b/app/app/src/Application/Broadcasting/BroadcastEventInterceptor.php new file mode 100644 index 0000000..2e2d02c --- /dev/null +++ b/app/app/src/Application/Broadcasting/BroadcastEventInterceptor.php @@ -0,0 +1,35 @@ +callAction($controller, $action, $parameters); + + if ($event instanceof ShouldBroadcastInterface) { + $this->broadcast->publish( + $event->getBroadcastTopics(), + \json_encode([ + 'event' => $event->getEventName(), + 'data' => $event->jsonSerialize(), + ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE), + ); + } + + return $result; + } +} diff --git a/app/app/src/Application/Broadcasting/Channel/Channel.php b/app/app/src/Application/Broadcasting/Channel/Channel.php new file mode 100644 index 0000000..eeca81c --- /dev/null +++ b/app/app/src/Application/Broadcasting/Channel/Channel.php @@ -0,0 +1,17 @@ +name; + } +} diff --git a/app/app/src/Application/Broadcasting/Channel/EventsChannel.php b/app/app/src/Application/Broadcasting/Channel/EventsChannel.php new file mode 100644 index 0000000..9bc8487 --- /dev/null +++ b/app/app/src/Application/Broadcasting/Channel/EventsChannel.php @@ -0,0 +1,13 @@ +respond( + new ConnectResponse( + user: (string)$request->getAttribute('user_id'), + channels: ['events'], + ), + ); + } catch (\Throwable $e) { + $request->error($e->getCode(), $e->getMessage()); + } + } +} diff --git a/app/app/src/Endpoint/Centrifugo/RPCService.php b/app/app/src/Endpoint/Centrifugo/RPCService.php new file mode 100644 index 0000000..1e25aac --- /dev/null +++ b/app/app/src/Endpoint/Centrifugo/RPCService.php @@ -0,0 +1,80 @@ +http->handle( + $this->createHttpRequest($request), + ); + + $result = \json_decode((string)$response->getBody(), true); + $result['code'] = $response->getStatusCode(); + } catch (ValidationException $e) { + $result['code'] = $e->getCode(); + $result['errors'] = $e->errors; + $result['message'] = $e->getMessage(); + } catch (\Throwable $e) { + $result['code'] = $e->getCode(); + $result['message'] = $e->getMessage(); + } + + try { + $request->respond( + new RPCResponse( + data: $result, + ), + ); + } catch (\Throwable $e) { + $request->error($e->getCode(), $e->getMessage()); + } + } + + public function createHttpRequest(Request\RPC $request): ServerRequestInterface + { + [$method, $uri] = \explode(':', $request->method, 2); + $method = \strtoupper($method); + + $httpRequest = $this->requestFactory->createServerRequest(\strtoupper($method), \ltrim($uri, '/')) + ->withHeader('Content-Type', 'application/json'); + + $data = $request->getData(); + + $token = $data['token'] ?? null; + unset($data['token']); + + $httpRequest = match ($method) { + 'GET', 'HEAD' => $httpRequest->withQueryParams($data), + 'POST', 'PUT', 'DELETE' => $httpRequest->withParsedBody($data), + default => throw new \InvalidArgumentException('Unsupported method'), + }; + + if (!empty($token)) { + $httpRequest = $httpRequest->withHeader('X-Auth-Token', $token); + } + + return $httpRequest; + } +} diff --git a/app/app/src/Endpoint/Http/Controller/GithubWebhookAction.php b/app/app/src/Endpoint/Http/Controller/GithubWebhookAction.php new file mode 100644 index 0000000..9f09b25 --- /dev/null +++ b/app/app/src/Endpoint/Http/Controller/GithubWebhookAction.php @@ -0,0 +1,74 @@ +validate((string)$input->request()->getBody(), $input->header('x-hub-signature-256'))) { + // throw new ForbiddenException('Invalid signature'); + } + + $event = match ($input->header('x-github-event')) { + 'star' => $this->handleStar($input), + 'release' => $this->handleRelease($input), + default => null, + }; + + if ($event !== null) { + $client->clearCache($input->data('repository.full_name')); + $this->events->dispatch($event); + } + + return $response->create(200); + } + + private function handleStar(InputManager $input): ?object + { + $action = $input->data('action'); + + return match ($action) { + 'created' => new RepositoryStarred( + repository: $input->data('repository.name'), + stars: $input->data('repository.stargazers_count'), + ), + default => null, + }; + } + + private function handleRelease(InputManager $input): ?object + { + $action = $input->data('action'); + + return match ($action) { + 'published' => new NewRelease( + repository: $input->data('repository.name'), + version: $input->data('release.tag_name'), + ), + default => null, + }; + } +} diff --git a/app/app/src/Endpoint/Http/Controller/SettingsAction.php b/app/app/src/Endpoint/Http/Controller/SettingsAction.php new file mode 100644 index 0000000..d8dada5 --- /dev/null +++ b/app/app/src/Endpoint/Http/Controller/SettingsAction.php @@ -0,0 +1,30 @@ + [ + 'server' => [ + 'stars' => $client->getStars('buggregator/server'), + 'last_version' => $client->getLastVersion('buggregator/server'), + ], + 'trap' => [ + 'stars' => $client->getStars('buggregator/trap'), + 'last_version' => $client->getLastVersion('buggregator/trap'), + ], + ], + ]); + } +} diff --git a/app/app/src/Endpoint/Http/Resource/SettingsResource.php b/app/app/src/Endpoint/Http/Resource/SettingsResource.php new file mode 100644 index 0000000..f1f0156 --- /dev/null +++ b/app/app/src/Endpoint/Http/Resource/SettingsResource.php @@ -0,0 +1,12 @@ +cache = $cache->storage('github'); + } + + public function getStars(string $repository): int + { + $cacheKey = $this->getCacheKey($repository, __METHOD__); + if ($this->cache->has($cacheKey)) { + return $this->cache->get($cacheKey); + } + + $response = $this->client->sendRequest( + new Request( + 'GET', + "repos/{$repository}", + ), + ); + + $data = \json_decode($response->getBody()->getContents(), true); + $this->cache->set($cacheKey, $data['stargazers_count']); + + return $data['stargazers_count']; + } + + public function getLastVersion(string $repository): string + { + $cacheKey = $this->getCacheKey($repository, __METHOD__); + if ($this->cache->has($cacheKey)) { + return $this->cache->get($cacheKey); + } + + $response = $this->client->sendRequest( + new Request( + 'GET', + "repos/{$repository}/releases/latest", + ), + ); + + $data = \json_decode($response->getBody()->getContents(), true); + $this->cache->set($cacheKey, $data['tag_name']); + + return $data['tag_name']; + } + + private function getCacheKey(string $repository, string $prefix): string + { + return "{$prefix}:{$repository}"; + } + + public function clearCache(string $repository): void + { + $refl = new \ReflectionClass($this); + foreach ($refl->getMethods() as $method) { + if ($method->isPublic()) { + $this->cache->delete($this->getCacheKey($repository, $method->getName())); + } + } + } +} diff --git a/app/app/src/Github/ClientInterface.php b/app/app/src/Github/ClientInterface.php new file mode 100644 index 0000000..92e2998 --- /dev/null +++ b/app/app/src/Github/ClientInterface.php @@ -0,0 +1,14 @@ + $this->repository, + 'version' => $this->version, + ]; + } +} diff --git a/app/app/src/Github/Event/RepositoryStarred.php b/app/app/src/Github/Event/RepositoryStarred.php new file mode 100644 index 0000000..ed0bc7a --- /dev/null +++ b/app/app/src/Github/Event/RepositoryStarred.php @@ -0,0 +1,36 @@ + $this->repository, + 'stars' => $this->stars, + ]; + } +} diff --git a/app/app/src/Github/WebhookGate.php b/app/app/src/Github/WebhookGate.php new file mode 100644 index 0000000..aca8775 --- /dev/null +++ b/app/app/src/Github/WebhookGate.php @@ -0,0 +1,21 @@ +secret), + \substr($signature, 7), + ); + } +} diff --git a/app/composer.json b/app/composer.json index 91b9c4c..483e926 100644 --- a/app/composer.json +++ b/app/composer.json @@ -10,16 +10,18 @@ }, "require": { "php": ">=8.2", - "ext-sockets": "*", "ext-mbstring": "*", - "spiral/framework": "^3.12", - "spiral/roadrunner-cli": "^2.5", - "spiral/nyholm-bridge": "^1.3", - "spiral/roadrunner-bridge": "^3.0", + "ext-sockets": "*", + "guzzlehttp/guzzle": "^7.8", + "spiral-packages/laravel-validator": "^1.1", "spiral-packages/yii-error-handler-bridge": "^1.1", + "spiral-packages/league-event": "^1.0", "spiral/cycle-bridge": "^2.5", - "spiral-packages/laravel-validator": "^1.1", "spiral/data-grid-bridge": "^3.0", + "spiral/framework": "^3.12", + "spiral/nyholm-bridge": "^1.3", + "spiral/roadrunner-bridge": "^3.0", + "spiral/roadrunner-cli": "^2.5", "spiral/sentry-bridge": "^v2.2" }, "require-dev": { diff --git a/app/composer.lock b/app/composer.lock index b9787e4..11afd69 100644 --- a/app/composer.lock +++ b/app/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "37a2fcb507a1d780934864aa88651310", + "content-hash": "761610fde8b364c52240d0252a2d87cd", "packages": [ { "name": "alexkart/curl-builder", @@ -1492,6 +1492,215 @@ }, "time": "2023-08-14T23:57:54+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:35:24+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:19:20+00:00" + }, { "name": "guzzlehttp/psr7", "version": "2.6.2", @@ -2160,6 +2369,65 @@ }, "time": "2024-03-08T09:58:59+00:00" }, + { + "name": "league/event", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/event.git", + "reference": "221867a61087ee265ca07bd39aa757879afca820" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/event/zipball/221867a61087ee265ca07bd39aa757879afca820", + "reference": "221867a61087ee265ca07bd39aa757879afca820", + "shasum": "" + }, + "require": { + "php": ">=7.2.0", + "psr/event-dispatcher": "^1.0" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "phpstan/phpstan": "^0.12.45", + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Event\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Event package", + "keywords": [ + "emitter", + "event", + "listener" + ], + "support": { + "issues": "https://github.com/thephpleague/event/issues", + "source": "https://github.com/thephpleague/event/tree/3.0.2" + }, + "time": "2022-10-29T09:31:25+00:00" + }, { "name": "league/flysystem", "version": "3.27.0", @@ -4609,6 +4877,62 @@ }, "time": "2022-09-29T15:46:44+00:00" }, + { + "name": "spiral-packages/league-event", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/spiral-packages/league-event.git", + "reference": "ef6e87e2e5a2d12ecfc92e99a6e6f0aec72f7aaf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spiral-packages/league-event/zipball/ef6e87e2e5a2d12ecfc92e99a6e6f0aec72f7aaf", + "reference": "ef6e87e2e5a2d12ecfc92e99a6e6f0aec72f7aaf", + "shasum": "" + }, + "require": { + "league/event": "^3.0", + "php": "^8.1", + "spiral/events": "^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "phpunit/phpunit": "^9.5", + "roave/security-advisories": "dev-latest", + "spiral/testing": "^2.0", + "vimeo/psalm": "^4.22" + }, + "type": "library", + "extra": { + "spiral": { + "bootloaders": [ + "Spiral\\League\\Event\\Bootloader\\EventBootloader" + ] + } + }, + "autoload": { + "psr-4": { + "Spiral\\League\\Event\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The League Event bridge for Spiral Framework", + "homepage": "https://github.com/spiral-packages/symfony-event-dispatcher", + "keywords": [ + "event-dispatcher", + "spiral", + "spiral-packages" + ], + "support": { + "issues": "https://github.com/spiral-packages/league-event/issues", + "source": "https://github.com/spiral-packages/league-event/tree/1.0.1" + }, + "time": "2022-09-14T08:02:26+00:00" + }, { "name": "spiral-packages/yii-error-handler-bridge", "version": "1.1.0", @@ -11678,8 +12002,8 @@ "prefer-lowest": false, "platform": { "php": ">=8.2", - "ext-sockets": "*", - "ext-mbstring": "*" + "ext-mbstring": "*", + "ext-sockets": "*" }, "platform-dev": [], "plugin-api-version": "2.3.0" diff --git a/docker-compose.yaml b/docker-compose.yaml index 8e71dab..b7bae4f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -48,12 +48,6 @@ services: RR_CENTRIFUGE_PROXY_ADDRESS: tcp://0.0.0.0:10001 RR_CENTRIFUGE_GRPC_API_ADDRESS: tcp://buggregator-ws:10000 - healthcheck: - test: curl --fail http://127.0.0.1:2114 || exit 1 - interval: 10s - timeout: 30s - retries: 3 - start_period: 5s labels: - "traefik.enable=true" - "traefik.http.routers.buggregator-api-http.entrypoints=web" @@ -76,8 +70,8 @@ services: - "traefik.http.routers.buggregator-spa-http.entrypoints=web" - "traefik.http.routers.buggregator-spa-http.rule=Host(`buggregator.localhost`)" - "traefik.http.services.buggregator-spa-http.loadbalancer.server.port=3000" - volumes: - - ./spa:/app +# volumes: +# - ./spa:/app networks: - buggregator-network diff --git a/spa/app/api/Api.ts b/spa/app/api/Api.ts new file mode 100644 index 0000000..c26063f --- /dev/null +++ b/spa/app/api/Api.ts @@ -0,0 +1,35 @@ +import apiMethods from '~/app/api/methods'; +import { RPCClient } from "~/app/ws/RPCClient"; +import { SettingsResponse } from "~/config/types"; + +type SettingsApi = { + get: () => SettingsResponse, +} + +type ExamplesApi = { + call: (action: string) => void, +} + +export default class Api { + private readonly rpc: RPCClient; + private readonly _api_url: string; + private readonly _examples_url: string; + + constructor(rpc: RPCClient, api_url: string, examples_url: string) { + this.rpc = rpc; + this._api_url = api_url; + this._examples_url = examples_url; + } + + get settings(): SettingsApi { + return { + get: apiMethods.settings(this.rpc), + } + } + + get examples(): ExamplesApi { + return { + call: apiMethods.callExampleAction(this._examples_url), + } + } +} diff --git a/spa/app/api/methods.ts b/spa/app/api/methods.ts new file mode 100644 index 0000000..04ace8f --- /dev/null +++ b/spa/app/api/methods.ts @@ -0,0 +1,27 @@ +import { + SettingsResponse, + ServerResponse +} from "~/config/types"; +import { RPCClient } from "~/app/ws/RPCClient"; + +const settings = (rpc: RPCClient) => () => rpc.call('get:api/settings') + .then((response: ServerResponse) => response.data); + +const callExampleAction = (host: string) => (action: string) => { + action = action.toLowerCase(); + + const path: string = action === 'profiler:report' ? '_profiler' : ''; + + return useFetch(`${host}/${path}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({action}) + }) +} + +export default { + settings, + callExampleAction +} diff --git a/spa/app/api/schema.d.ts b/spa/app/api/schema.d.ts new file mode 100644 index 0000000..dae9206 --- /dev/null +++ b/spa/app/api/schema.d.ts @@ -0,0 +1,3191 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + + +/** OneOf type helpers */ +type Without = { [P in Exclude]?: never }; +type XOR = (T | U) extends object ? (Without & U) | (Without & T) : T | U; +type OneOf = T extends [infer Only] ? Only : T extends [infer A, infer B, ...infer Rest] ? OneOf<[XOR, ...Rest]> : never; + +export interface paths { + "/reports/download": { + post: operations["2b005afc5ea56154dcaee39c5aaf760c"]; + }; + "/translations/{filename}": { + get: operations["8c26f9ccde28cd0eb6130fd2f44605a6"]; + }; + "/api/v1/asset/domain/{uuid}": { + get: operations["asset/domain/get"]; + }; + "/api/v1/asset/{uuid}": { + get: operations["asset/get"]; + }; + "/api/v1/asset/ip/{uuid}": { + get: operations["asset/ip/get"]; + }; + "/api/v1/asset/os/{uuid}": { + get: operations["asset/os/get"]; + }; + "/api/v1/asset/port/{uuid}": { + get: operations["asset/port/get"]; + }; + "/api/v1/asset/{uuid}/vulnerability/{vulUuid}/details": { + get: operations["asset/vulnerability/details"]; + }; + "/api/v1/asset/{uuid}/vulnerabilities": { + get: operations["asset/vulnerability/list"]; + }; + "/api/v1/auth/change-password": { + /** @description Change password */ + post: operations["changePassword"]; + }; + "/api/v1/auth/forgot-password": { + /** @description Reset password */ + post: operations["forgotPassword"]; + }; + "/api/v1/auth/login": { + /** @description Log in by credentials */ + post: operations["login"]; + }; + "/api/v1/auth/logout": { + /** @description Logout a customer */ + post: operations["logout"]; + }; + "/api/v1/me": { + get: operations["me"]; + }; + "/api/v1/auth/register": { + /** @description Register a new customer */ + post: operations["register"]; + }; + "/api/v1/auth/sessions": { + /** @description Retrieve a list of sessions */ + get: operations["sessions.list"]; + }; + "/api/v1/auth/sessions/{uuid}/revoke": { + /** @description Revoke a session by UUID */ + post: operations["sessions.revoke"]; + }; + "/api/v1/auth/sessions/revoke": { + /** @description Revoke all sessions for the current user */ + post: operations["sessions.revokeAll"]; + }; + "/api/v1/profile/change-password": { + /** @description Change password */ + post: operations["changePasswordProfile"]; + }; + "/api/v1/profile/change-timezone": { + /** @description Change timezone */ + post: operations["changeTimeZone"]; + }; + "/api/v1/dashboard": { + get: operations["getDashboard"]; + }; + "/api/v1/dashboard/asset-changes": { + get: operations["3b44fdc99fa98a60db9f7ff54f5fe7c8"]; + }; + "/api/v1/dashboard/asset-count": { + get: operations["ecc48c49a2b657934d9ae02d44dad622"]; + }; + "/api/v1/dashboard/attack-vector-count": { + get: operations["5f173cefa03130c82d7cb590454151d8"]; + }; + "/api/v1/dashboard/credentials": { + get: operations["8fbabf2484f42b43812d52ca18a9a988"]; + }; + "/api/v1/dashboard/os": { + get: operations["34847e5e0fb5d3b3cf990d3890ac6b06"]; + }; + "/api/v1/dashboard/open-ports": { + get: operations["4b71b242c7e6f92273eb65e7b71bf8ca"]; + }; + "/api/v1/dashboard/overall-risk-score": { + get: operations["a04f7b986fb9621a5b23776135540585"]; + }; + "/api/v1/dashboard/vulnerability-count": { + get: operations["755168a919c3f29bf53ad0a6971d0f0d"]; + }; + "/api/v1/downloads": { + /** @description Get downloads list */ + get: operations["downloads/list"]; + }; + "/api/v1/notifications/unread": { + /** @description Unread notifications */ + get: operations["getUnreadNotifications"]; + }; + "/api/v1/notifications": { + /** @description Mark notification as read */ + delete: operations["markNotificationsAsRead"]; + }; + "/api/v1/project/{uuid}/assets/change-affiliations": { + post: operations["project/asset/change-affiliations"]; + }; + "/api/v1/project/{uuid}/assets/domains": { + /** @description Get project domain list */ + get: operations["project/asset/domain/list"]; + }; + "/api/v1/project/{uuid}/assets/export": { + /** @description Export project asset list */ + post: operations["project/asset/list/export"]; + }; + "/api/v1/project/{uuid}/asset-counters": { + /** @description Get project asset counters */ + get: operations["project/asset/counters"]; + }; + "/api/v1/project/{uuid}/assets/ip": { + /** @description Get project ip list */ + get: operations["project/asset/ip/list"]; + }; + "/api/v1/project/{uuid}/assets": { + /** @description Get project asset list */ + get: operations["project/asset/list"]; + }; + "/api/v1/project/{uuid}/assets/os": { + /** @description Get project operation system asset list */ + get: operations["project/asset/os/list"]; + }; + "/api/v1/project/{uuid}/assets/ports": { + /** @description Get project port asset list */ + get: operations["project/asset/ports/list"]; + }; + "/api/v1/project": { + /** @description Create a new project */ + post: operations["project/create"]; + }; + "/api/v1/project/{uuid}": { + /** @description Get project by UUID */ + get: operations["project/get"]; + }; + "/api/v1/projects": { + /** @description Get projects list */ + get: operations["project/list"]; + }; + "/api/v1/project/{uuid}/vulnerability/change-statuses": { + post: operations["project/vulnerability/change-statuses"]; + }; + "/api/v1/project/{uuid}/vulnerabilities/export": { + /** @description Export project vulnerability list */ + post: operations["asset/vulnerability/list/export"]; + }; + "/api/v1/project/{uuid}/vulnerability-counters": { + get: operations["getProjectVulnerabilityCounters"]; + }; + "/api/v1/project/{uuid}/vulnerabilities": { + get: operations["listProjectVulnerabilities"]; + }; + "/api/v1/reports": { + get: operations["listReports"]; + }; + "/api/v1/two-step-auth/connect": { + post: operations["twoStepConnect"]; + }; + "/api/v1/two-step-auth/disconnect": { + post: operations["twoStepDisconnect"]; + }; + "/api/v1/two-step-auth/show-qr-code": { + get: operations["twoStepShowQrCode"]; + }; + "/api/v1/two-step-auth/verify-code": { + post: operations["twoStepShowVerifyCode"]; + }; + "/api/v1/vulnerability/{uuid}": { + get: operations["getVulnerabilityByUuid"]; + }; +} + +export type webhooks = Record; + +export interface components { + schemas: { + /** @example 018944be-ffa3-728b-9439-cc2f9922a65b */ + Uuid: string; + /** + * @description ICANN domain name without any protocols + * @example intruforce.com + */ + DomainName: string; + /** + * @description IP address name + * @example 185.215.4.51 + */ + IpName: string; + /** + * @description Operation system name + * @example Linux v1.0 - v99.0 + */ + OsName: string; + /** + * @description Network port number + * @example 443 + */ + PortNumber: Record; + AssetCounters: { + total?: number; + domains?: number; + ips?: number; + }; + VulnerabilityCounters: { + total?: number; + vulnerabilities?: number; + }; + Status: { + status?: boolean; + }; + PaginationMeta: { + pagination?: { + /** + * @description Current page number + * @example 1 + */ + current_page?: Record; + /** + * @description Last page number + * @example 20 + */ + last_page?: Record; + /** + * @description Items per page + * @example 10 + */ + per_page?: Record; + /** + * @description Total count of items + * @example 200 + */ + total?: Record; + }; + }; + Project: { + /** + * @description Project UUID + * @example 018944be-ffa3-728b-9439-cc2f9922a65b + */ + uuid?: string; + /** + * @description Project name + * @example intruforce + */ + name?: string; + /** + * @description Project description + * @example Dolor sit amet + */ + description?: string; + }; + Asset: { + /** @example 018944be-ffa3-728b-9439-cc2f9922a65b */ + uuid?: string; + project?: { + /** @example 018944be-ffa3-728b-9439-cc2f9922a654 */ + uuid?: string; + /** @example Project name */ + name?: string; + }; + /** @example intruforce.com */ + static_value?: string; + /** @example 0 */ + affiliation?: number; + /** @example 0 */ + type?: number; + /** @example 2023-07-06T01:01:01+00:00 */ + last_discovery_time?: string; + /** @example 2023-07-06T01:01:01+00:00 */ + discovery_time?: string; + /** @example 2023-07-06T01:01:01+00:00 */ + last_scan_time?: string; + /** @example {"foo": "bar"} */ + data?: string; + }; + Vulnerability: { + /** @example 018944be-ffa3-728b-9439-cc2f9922a65b */ + uuid?: string; + /** @example intruforce.com */ + code?: string; + /** @example Dolor sit amet */ + description?: string; + /** @example https://intruforce.com */ + url?: string; + /** + * @description timestamp + * @example 1234567890 + */ + discovered_at?: string; + /** + * @description timestamp + * @example 1234567890 + */ + last_discovered_at?: string; + }; + Report: { + /** @example 018944be-ffa3-728b-9439-cc2f9922a65b */ + uuid?: string; + /** @example Report #11 */ + name?: string; + /** @example Dolor sit amet */ + description?: string; + /** @example reports/report_3832752.pdf */ + path?: string; + /** @example pdf */ + file_type?: string; + /** @example 10000 */ + file_size?: number; + /** @example 2023-07-06T01:01:01+00:00 */ + uploaded_at?: string; + }; + QRCode: { + url?: string; + secret?: string; + }; + Token: { + token?: string; + type?: string; + /** Format: date-time */ + expires_at?: string; + }; + BackupCodes: string[]; + Notifications: { + uuid?: string; + type?: string; + data?: string; + }[]; + Auth: { + token?: { + /** @example test */ + token?: string; + /** @example 2023-07-06T01:01:01+00:00 */ + expires_at?: string; + }; + }; + Customer: { + customer?: { + /** @example 018944be-ffa3-728b-9439-cc2f9922a65b */ + uuid?: string; + /** @example test@example.com */ + email?: string; + profile?: { + full_name?: { + /** @example John */ + first_name?: string; + /** @example Doe */ + last_name?: string; + }; + /** @example intruforce */ + company_name?: string; + /** @example Head of security */ + position_in_company?: string; + }; + }; + }; + ThreatAssessment: { + /** @example 8 */ + score?: number; + /** @example 2023-07-06T01:01:01+00:00 */ + updated_at?: string; + }; + AttackVector: { + /** @example 4 */ + real?: number; + /** @example 10 */ + potential?: number; + /** @example 2023-07-06T01:01:01+00:00 */ + updated_at?: string; + }; + PasswordLeak: { + /** @example 15 */ + email_total?: number; + /** @example 5 */ + email_compromised?: number; + /** @example 30 */ + desktop_total?: number; + /** @example 14 */ + desktop_compromised?: number; + /** @example 2023-07-06T01:01:01+00:00 */ + updated_at?: string; + }; + Credential: { + /** @example 19 */ + total?: number; + /** @example 6 */ + compromised?: number; + service?: OneOf<[{ + /** @example TCP */ + name?: string; + /** @example 100 */ + total?: number; + /** @example 20 */ + compromised?: number; + }, { + /** @example UDP */ + name?: string; + /** @example 120 */ + total?: number; + /** @example 20 */ + compromised?: number; + }]>[]; + /** @example 2023-07-06T01:01:01+00:00 */ + updated_at?: string; + }; + TotalAsset: { + /** @example 100 */ + total?: number; + /** @example 5 */ + critical_vulnerabilities?: number; + /** @example 30 */ + high_level_vulnerabilities?: number; + /** @example 2023-07-06T01:01:01+00:00 */ + updated_at?: string; + }; + OpenPort: { + /** @example 79 */ + total?: number; + /** @example 3 */ + high_risk?: number; + /** @example 40 */ + medium_risk?: number; + /** @example 2023-07-06T01:01:01+00:00 */ + updated_at?: string; + }; + AssetChange: { + /** @example 5 */ + new?: number; + /** @example 0 */ + unavailable?: number; + /** @example 2023-07-06T01:01:01+00:00 */ + updated_at?: string; + }; + Download: { + /** + * @description Download UUID + * @example 018944be-ffa3-728b-9439-cc2f9922a65b + */ + uuid?: string; + /** + * @description Download name + * @example Some download + */ + name?: string; + /** + * @description Download description + * @example Dolor sit amet + */ + description?: string; + /** + * @description Download status + * @example 0 + */ + status?: number; + /** + * @description Download progress + * @example 0 + */ + progress?: number; + files?: { + /** + * @description File UUID + * @example 018944be-ffa3-728b-9439-cc2f9922a65b + */ + uuid?: string; + /** + * @description File name + * @example file.txt + */ + name?: string; + /** + * @description File URL + * @example https://example.com/file.txt + */ + url?: string; + /** + * @description File type + * @example txt + */ + fileType?: string; + /** + * @description File size + * @example 100 + */ + fileSize?: number; + }; + /** + * @description Download creation date + * @example 2023-07-06T01:01:01+00:00 + */ + created_at?: string; + /** + * @description Download update date + * @example 2023-07-06T01:01:01+00:00 + */ + updated_at?: string; + /** + * @description Download expiration date + * @example 2023-07-06T01:01:01+00:00 + */ + expires_at?: string; + }; + UnauthorizedError: { + /** @example unauthorized */ + message?: string; + /** @example 401 */ + code?: number; + }; + ForbiddenError: { + /** @example token_is_missing */ + message?: string; + /** @example 403 */ + code?: number; + }; + ValidationError: { + /** @example validation_errors */ + message?: string; + /** @example 422 */ + code?: number; + errors?: { + /** @example name */ + field?: string; + errors?: { + /** @example string_expected */ + message?: string; + meta?: { + /** @example min */ + key?: string; + /** @example 3 */ + value?: string; + }[]; + }[]; + }[]; + }; + /** + * @description Risk types: + * * `0` - Total risk of asset or issue + * * `1` - Expert - risk by expert + * * `4` - CVSS v2 + * * `5` - CVSS v3 + * @example 0 + * @enum {integer} + */ + RiskType: None | Zero | Low | Medium | High | Critical; + /** + * @description Risk levels: + * * `0` - None (not set) + * * `1` - Zero + * * `2` - Low + * * `3` - Medium + * * `4` - High + * * `5` - Critical + * @example 5 + * @enum {integer} + */ + RiskLevel: None | Zero | Low | Medium | High | Critical; + /** + * @description Risk value object, can be convert to float by amount / denominator + * @enum {object} + */ + Risk: None | Zero | Low | Medium | High | Critical; + DownloadReportFilter: { + /** @description Reports to download */ + reports: components["schemas"]["ReportUuidFilter"][]; + }; + ReportUuidFilter: { + /** @description Report UUID */ + uuid: string; + }; + AssetCountersSchema: { + filter?: components["schemas"]["AssetCountersSchemaFilter"]; + }; + AssetCountersSchemaFilter: { + /** + * @description Asset affiliation status + * @default [ + * 0, + * 1 + * ] + */ + affiliation?: ((0 | 1 | 3 | 4 | null)[]) | null; + }; + AssetListSchema: { + sort?: components["schemas"]["AssetListSchemaSort"]; + filter?: components["schemas"]["AssetListSchemaFilter"]; + paginate?: components["schemas"]["AssetListSchemaSort"]; + }; + AssetListSchemaSort: { + /** + * @default desc + * @enum {string|null} + */ + by_risk?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + risk?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + risk_level?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + static_value?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + affiliation?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_affiliation?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + type?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_type?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + last_discovery_time?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_last_discovery_time?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + discovery_time?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_discovery_time?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + dns?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_dns?: "asc" | "desc" | null; + }; + AssetListSchemaPaginate: { + /** @default 1 */ + page?: Record | null; + /** + * @default 25 + * @enum {intval|null} + */ + limit?: 10 | 25 | 50 | null; + }; + AssetListSchemaFilter: { + /** + * @default all + * @enum {string} + */ + type?: "all" | "domain" | "ip_address"; + /** @description Name of asset */ + static_value?: string | null; + /** + * @description Asset affiliation status + * @default [ + * 0, + * 1 + * ] + */ + affiliation?: ((0 | 1 | 3 | 4 | null)[]) | null; + /** + * @description Risk levels + * @default [] + */ + risk_level?: ((0 | 1 | 2 | 3 | 4 | 5 | null)[]) | null; + }; + DomainListSchema: { + sort?: components["schemas"]["DomainListSchemaSort"]; + filter?: components["schemas"]["DomainListSchemaFilter"]; + paginate?: components["schemas"]["DomainListSchemaSort"]; + }; + DomainListSchemaSort: { + /** + * @default desc + * @enum {string|null} + */ + by_risk?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + risk?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + risk_level?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + static_value?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_name?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + affiliation?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_affiliation?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + last_discovery_time?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_last_discovery_time?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + discovery_time?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_discovery_time?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + dns?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_dns?: "asc" | "desc" | null; + }; + DomainListSchemaPaginate: { + /** @default 1 */ + page?: Record | null; + /** + * @default 25 + * @enum {intval|null} + */ + limit?: 10 | 25 | 50 | null; + }; + DomainListSchemaFilter: { + /** @description Name of asset */ + static_value?: string | null; + /** + * @description Asset affiliation status + * @default [ + * 0, + * 1 + * ] + */ + affiliation?: ((0 | 1 | 3 | 4 | null)[]) | null; + /** + * @description Risk levels + * @default [] + */ + risk_level?: ((0 | 1 | 2 | 3 | 4 | 5 | null)[]) | null; + }; + IpAddressListSchema: { + sort?: components["schemas"]["IpAddressListSchemaSort"]; + filter?: components["schemas"]["IpAddressListSchemaFilter"]; + paginate?: components["schemas"]["IpAddressListSchemaSort"]; + }; + IpAddressListSchemaSort: { + /** + * @default desc + * @enum {string|null} + */ + by_risk?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + risk?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + risk_level?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + static_value?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_name?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + affiliation?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_affiliation?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + last_discovery_time?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_last_discovery_time?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + discovery_time?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_discovery_time?: "asc" | "desc" | null; + }; + IpAddressListSchemaPaginate: { + /** @default 1 */ + page?: Record | null; + /** + * @default 25 + * @enum {intval|null} + */ + limit?: 10 | 25 | 50 | null; + }; + IpAddressListSchemaFilter: { + /** @description Name of asset */ + static_value?: string | null; + /** + * @description Asset affiliation status + * @default [ + * 0, + * 1 + * ] + */ + affiliation?: ((0 | 1 | 3 | 4 | null)[]) | null; + /** + * @description Risk levels + * @default [] + */ + risk_level?: ((0 | 1 | 2 | 3 | 4 | 5 | null)[]) | null; + }; + OperationSystemListSchema: { + sort?: components["schemas"]["OperationSystemListSchemaSort"]; + filter?: components["schemas"]["OperationSystemListSchemaFilter"]; + paginate?: components["schemas"]["OperationSystemListSchemaSort"]; + }; + OperationSystemListSchemaSort: { + /** + * @default desc + * @enum {string|null} + */ + by_risk?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + risk?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + risk_level?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + static_value?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_name?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + affiliation?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_affiliation?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + last_discovery_time?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_last_discovery_time?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + discovery_time?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_discovery_time?: "asc" | "desc" | null; + }; + OperationSystemListSchemaPaginate: { + /** @default 1 */ + page?: Record | null; + /** + * @default 25 + * @enum {intval|null} + */ + limit?: 10 | 25 | 50 | null; + }; + OperationSystemListSchemaFilter: { + /** @description Name of asset */ + static_value?: string | null; + /** + * @description Asset affiliation status + * @default [ + * 0, + * 1 + * ] + */ + affiliation?: ((0 | 1 | 3 | 4 | null)[]) | null; + /** + * @description Risk levels + * @default [] + */ + risk_level?: ((0 | 1 | 2 | 3 | 4 | 5 | null)[]) | null; + }; + PortListSchema: { + sort?: components["schemas"]["PortListSchemaSort"]; + filter?: components["schemas"]["PortListSchemaFilter"]; + paginate?: components["schemas"]["PortListSchemaSort"]; + }; + PortListSchemaSort: { + /** + * @default desc + * @enum {string|null} + */ + by_risk?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + risk?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + risk_level?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + static_value?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_name?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + affiliation?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_affiliation?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + last_discovery_time?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_last_discovery_time?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + discovery_time?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_discovery_time?: "asc" | "desc" | null; + }; + PortListSchemaPaginate: { + /** @default 1 */ + page?: Record | null; + /** + * @default 25 + * @enum {intval|null} + */ + limit?: 10 | 25 | 50 | null; + }; + PortListSchemaFilter: { + /** @description Name of asset */ + static_value?: string | null; + /** + * @description Asset affiliation status + * @default [ + * 0, + * 1 + * ] + */ + affiliation?: ((0 | 1 | 3 | 4 | null)[]) | null; + /** + * @description Risk levels + * @default [] + */ + risk_level?: ((0 | 1 | 2 | 3 | 4 | 5 | null)[]) | null; + }; + IssueCountersSchema: { + filter?: components["schemas"]["IssueCountersSchemaFilter"]; + }; + IssueCountersSchemaFilter: { + /** + * @description Issue status + * @default [ + * 0, + * 2 + * ] + */ + status?: ((0 | 1 | 2 | 3 | 4 | null)[]) | null; + }; + IssueListSchema: { + sort?: components["schemas"]["IssueListSchemaSort"]; + filter?: components["schemas"]["IssueListSchemaFilter"]; + paginate?: components["schemas"]["IssueListSchemaPaginate"]; + }; + IssueListSchemaSort: { + /** + * @default null + * @enum {string|null} + */ + risk?: "asc" | "desc" | null; + /** + * @default desc + * @enum {string|null} + */ + by_risk?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + risk_level?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + name?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_name?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + asset_name?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_asset_name?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + status?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + last_discovered_at?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_last_discovered_at?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + discovered_at?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_discovered_at?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + port?: "asc" | "desc" | null; + /** + * @default null + * @enum {string|null} + */ + by_port?: "asc" | "desc" | null; + }; + IssueListSchemaPaginate: { + /** @default 1 */ + page?: Record | null; + /** + * @default 25 + * @enum {intval|null} + */ + limit?: 10 | 25 | 50 | null; + }; + IssueListSchemaFilter: { + /** @description Name or code of issue */ + name?: string | null; + /** + * @description Asset affiliation status + * @default [ + * 0, + * 1 + * ] + */ + affiliation?: ((0 | 1 | 3 | 4 | null)[]) | null; + /** + * @description Issue status + * @default [ + * 0, + * 2 + * ] + */ + status?: ((0 | 1 | 2 | 3 | 4 | null)[]) | null; + /** @description Risk levels */ + risk_level?: ((0 | 1 | 2 | 3 | 4 | 5 | null)[]) | null; + /** @description Port numbers */ + port?: number[] | null; + /** @description Name of asset */ + asset_name?: string | null; + }; + AssetAffiliationFilter: unknown; + ChangeAffiliation: { + /** @description Assets to change affiliation */ + assets?: components["schemas"]["AssetAffiliationFilter"][]; + }; + ListAssetsFilter: { + page?: number; + per_page?: number; + type?: string; + affiliation?: string; + }; + ChangeVulnerabilityStatuses: { + /** @description Vulnerabilities to change statuses */ + vulnerabilities: components["schemas"]["VulnerabilityStatus"][]; + }; + ListAssetVulnerabilitiesFilter: { + statuses?: number[]; + page?: number; + per_page?: number; + }; + VulnerabilityStatus: unknown; + ChangePasswordFilter: { + /** @description Auth token */ + token: Record | null; + password: Record | null; + }; + ForgotPasswordFilter: { + /** Format: email */ + email: string; + }; + LoginFilter: { + /** Format: email */ + email: string; + password: string; + }; + RegisterFilter: { + /** + * Format: email + * @example customer@site.com + */ + email: string; + /** @example John */ + first_name?: string; + /** @example Doe */ + last_name?: string; + /** @example Some Company */ + company_name?: string; + /** @example CEO */ + position?: string; + /** @example Company infrastructure */ + project_name?: string; + /** @example Company infrastructure description */ + project_description?: string; + }; + CustomerChangePasswordFilter: { + current_password: Record | null; + new_password: Record | null; + }; + CustomerChangeTimezoneFilter: { + timezone: Record | null; + }; + DashboardCommonFilter: []; + ExportVulnerabilitiesFilter: { + format: []; + }; + MarkNotificationAsReadFilter: { + /** @description Notifications to mark as read */ + notifications: components["schemas"]["NotificationUuidFilter"][]; + }; + NotificationUuidFilter: { + /** @description Notification UUID */ + uuid: string; + }; + CreateProjectFilter: { + name: string; + description: string; + }; + GetProjectAssetCountersFilter: { + affiliations?: string; + }; + GetProjectVulnerabilityCountersFilter: { + affiliations?: string; + asset_affiliations?: string; + }; + ListReportFilter: { + page?: number; + }; + ConnectFilter: { + verification_code: string; + secret: string; + }; + DisconnectFilter: { + verification_code?: string; + }; + VerifyCodeFilter: { + verification_code?: string; + signature: string; + }; + ListVulnerabilitiesFilter: { + page?: number; + per_page?: number; + statuses?: string; + affiliation?: string; + }; + /** @description Count of risk levels of one of type issue */ + AssetRiskLevelCount: { + level?: components["schemas"]["RiskLevel"]; + /** @example 10 */ + count?: number; + }; + /** @description Asset risk by type and level */ + AssetRisk: { + risk?: components["schemas"]["Risk"]; + type?: components["schemas"]["RiskType"]; + level?: components["schemas"]["RiskLevel"]; + }; + AssetDiscovering: { + /** + * Format: date-time + * @example 2024-03-02T06:58:23.000000Z + */ + first_seen?: string; + /** + * Format: date-time + * @example 2024-03-02T06:58:23.000000Z + */ + last_seen?: string; + source?: components["schemas"]["AssetSource"]; + }; + AssetProject: { + uuid?: components["schemas"]["Uuid"]; + /** @example Project - intruforce.com */ + name?: string; + }; + AssetScanning: { + /** + * Format: date-time + * @example 2024-03-02T06:58:23.000000Z + */ + last_scan?: string; + }; + SoftwareService: { + /** + * @description Common type of software service + * @example ssh + */ + name?: string; + /** + * @description Name of product of software like Nginx or OpenSSH, can be null + * @example OpenSSH + */ + product?: string | null; + /** + * @description Version of product, can be null + * @example v1.0.0 + */ + version?: string | null; + }; + AssetDomainInfo: { + domain?: components["schemas"]["DomainName"]; + /** + * @description Level of nesting domain name + * @example 2 + */ + level?: number; + /** + * @description Domain zone, 1 level domain name + * @example com + */ + zone?: string; + }; + Domain: { + uuid?: components["schemas"]["Uuid"]; + name?: components["schemas"]["DomainName"]; + affiliation?: components["schemas"]["AssetAffiliation"]; + type?: components["schemas"]["AssetType"]; + info?: components["schemas"]["AssetDomainInfo"]; + project?: components["schemas"]["AssetProject"]; + discovering?: components["schemas"]["AssetDiscovering"]; + scanning?: components["schemas"]["AssetScanning"]; + risks?: components["schemas"]["AssetRisk"][]; + /** @description Risk levels counts of all open visible issues of asset */ + issue_levels?: components["schemas"]["AssetRiskLevelCount"][]; + /** @example false */ + wildcard_detected?: boolean; + summary_info?: components["schemas"]["AssetDomainSummaryInfo"]; + whois?: components["schemas"]["WhoisRaw"]; + /** @description Dns records of domain */ + dns?: components["schemas"]["DnsRecord"][]; + }; + RelatedDomains: { + /** @description Several or all related domain names, depends on List or Get requests */ + domains?: components["schemas"]["DomainName"][]; + /** @description Quantity of all related domains */ + quantity?: number; + }; + DnsRecord: { + /** + * @description Dns type of records like `A`, `AAAA`, `MX` etc + * @example MX + */ + type?: string; + /** @description Value of dns record */ + value?: string; + /** + * Format: date-time + * @description Last seen of dns record + */ + last_discovery_time?: string; + /** + * Format: date-time + * @description First seen of dns record + */ + discovery_time?: string; + /** @description DNS priority */ + priority?: number | null; + }; + AssetDomainSummaryInfo: { + related_domains?: components["schemas"]["RelatedDomains"]; + ip_addresses?: components["schemas"]["AssetIpAddressSummaryItem"][]; + /** @description Quantity of related ip addresses */ + ip_addresses_count?: number | null; + /** @description Group of dns types with counts */ + dns_type_counts?: components["schemas"]["DnsTypCount"][]; + }; + DnsTypCount: { + /** + * @description Dns type like A, AAAA, MX + * @example MX + */ + type?: string; + count?: number; + }; + AssetIpAddressSummaryItem: { + uuid?: components["schemas"]["Uuid"]; + name?: components["schemas"]["IpName"]; + }; + IpAddress: { + uuid?: components["schemas"]["Uuid"]; + name?: components["schemas"]["IpName"]; + info?: components["schemas"]["AssetIpInfo"]; + affiliation?: components["schemas"]["AssetAffiliation"]; + type?: components["schemas"]["AssetType"]; + project?: components["schemas"]["AssetProject"]; + discovering?: components["schemas"]["AssetDiscovering"]; + scanning?: components["schemas"]["AssetScanning"]; + risks?: components["schemas"]["AssetRisk"][]; + /** @description Risk levels counts of all open visible issues of asset */ + issue_levels?: components["schemas"]["AssetRiskLevelCount"][]; + summary_info?: components["schemas"]["AssetIpAddressSummaryInfo"]; + whois?: components["schemas"]["WhoisRaw"]; + }; + AssetIpAddressSummaryInfo: { + related_domains?: components["schemas"]["RelatedDomains"]; + tcp_ports?: components["schemas"]["AssetPortSummaryItem"][]; + operation_systems?: components["schemas"]["AssetOperationSystemSummaryItem"][]; + }; + AssetIpInfo: { + ip?: components["schemas"]["IpName"]; + /** + * @description Version of IP Address 4 or 6 + * @example 4 + * @enum {integer} + */ + ip_version?: 4 | 6; + }; + AssetOperationSystemInfo: { + name?: components["schemas"]["OsName"]; + accuracy?: components["schemas"]["Accuracy"]; + }; + AssetOperationSystemSummaryItem: { + uuid?: components["schemas"]["Uuid"]; + name?: components["schemas"]["OsName"]; + accuracy?: components["schemas"]["Accuracy"]; + }; + /** + * @description Accuracy of usage + * @example 77 + */ + Accuracy: Record | null; + OperationSystem: { + uuid?: components["schemas"]["Uuid"]; + name?: components["schemas"]["OsName"]; + info?: components["schemas"]["AssetOperationSystemInfo"]; + ip?: components["schemas"]["AssetIpAddressSummaryItem"]; + service?: components["schemas"]["SoftwareService"]; + affiliation?: components["schemas"]["AssetAffiliation"]; + type?: components["schemas"]["AssetType"]; + discovering?: components["schemas"]["AssetDiscovering"]; + scanning?: components["schemas"]["AssetScanning"]; + risks?: components["schemas"]["AssetRisk"][]; + /** @description Risk levels counts of all open visible issues of asset */ + issue_levels?: components["schemas"]["AssetRiskLevelCount"][]; + }; + AssetPortInfo: { + number?: components["schemas"]["PortNumber"]; + protocol?: components["schemas"]["AssetTransportProtocol"]; + port_state?: components["schemas"]["AssetPortState"]; + }; + AssetPortSummaryItem: { + uuid?: components["schemas"]["Uuid"]; + number?: components["schemas"]["PortNumber"]; + risk_level?: components["schemas"]["RiskLevel"]; + }; + Port: { + uuid?: components["schemas"]["Uuid"]; + /** + * @description Port asset name (with service) + * @example 22/ssh + */ + name?: string; + info?: components["schemas"]["AssetPortInfo"]; + ip?: components["schemas"]["AssetIpAddressSummaryItem"]; + service?: components["schemas"]["SoftwareService"]; + affiliation?: components["schemas"]["AssetAffiliation"]; + type?: components["schemas"]["AssetType"]; + discovering?: components["schemas"]["AssetDiscovering"]; + scanning?: components["schemas"]["AssetScanning"]; + risks?: components["schemas"]["AssetRisk"][]; + /** @description Risk levels counts of all open visible issues of asset */ + issue_levels?: components["schemas"]["AssetRiskLevelCount"][]; + }; + /** + * @description Asset affiliation statuses: + * * `0` - Unknown + * * `1` - Owned + * * `3` - Not owned + * * `4` - Ignored + * @enum {int} + */ + AssetAffiliation: 0 | 1 | 3 | 4; + /** + * @description Asset discovering sources statuses: + * * `0` - Initial + * * `1` - Expert (by manager) + * * `2` - Asm (by scanner) + * @enum {int} + */ + AssetSource: 0 | 1 | 2; + /** + * @description Asset types: + * * `0` - Domain + * * `1` - IpAddress + * * `2` - Port (software) + * * `5` - Operation System + * @enum {int} + */ + AssetType: 0 | 1 | 2 | 5; + /** + * @description Asset port states from nmap: + * * `0` - Open + * * `1` - Filtered + * * `2` - Unfiltered + * * `3` - Closed + * * `4` - Open Filtered + * * `5` - Closed Filtered + * * `6` - Unknown + * @enum {int} + */ + AssetPortState: 0 | 1 | 3 | 6; + /** + * @description Asset types: + * * `1` - TCP + * * `2` - UDP + * @enum {int} + */ + AssetTransportProtocol: 1 | 2; + WhoisRaw: { + /** @description Big plain raw whois text */ + raw?: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} + +export type external = Record; + +export interface operations { + + "2b005afc5ea56154dcaee39c5aaf760c": { + requestBody: { + content: { + "application/json": components["schemas"]["DownloadReportFilter"]; + }; + }; + responses: { + /** @description Result */ + 200: { + headers: { + /** @description Content type */ + "Content-Type"?: string; + /** @description Content type */ + "Content-Disposition"?: string; + }; + content: { + "application/pdf": string; + "application/zip": string; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + "8c26f9ccde28cd0eb6130fd2f44605a6": { + parameters: { + path: { + /** + * @description Filename + * @example en.json + */ + filename: string; + }; + }; + responses: { + /** @description Json file with translations */ + 200: { + headers: { + /** @description Content type */ + "Content-Type"?: string; + }; + content: { + "application/json": string; + }; + }; + /** @description File not found */ + 404: never; + }; + }; + "asset/domain/get": { + parameters: { + path: { + /** @description Domain UUID */ + uuid: string; + }; + }; + responses: { + /** @description Retrieve the asset */ + 200: { + content: { + "application/json": components["schemas"]["Domain"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + "asset/get": { + parameters: { + path: { + /** @description Asset UUID */ + uuid: string; + }; + }; + responses: { + /** @description Retrieve the asset */ + 200: { + content: { + "application/json": components["schemas"]["Asset"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + "asset/ip/get": { + parameters: { + path: { + /** @description IpAddress UUID */ + uuid: components["schemas"]["Uuid"]; + }; + }; + responses: { + /** @description Retrieve the ip address */ + 200: { + content: { + "application/json": components["schemas"]["IpAddress"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + "asset/os/get": { + parameters: { + path: { + /** @description OperationSystem UUID */ + uuid: components["schemas"]["Uuid"]; + }; + }; + responses: { + /** @description Retrieve the operation system */ + 200: { + content: { + "application/json": components["schemas"]["OperationSystem"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + "asset/port/get": { + parameters: { + path: { + /** @description PortAsset UUID */ + uuid: components["schemas"]["Uuid"]; + }; + }; + responses: { + /** @description Retrieve the port asset */ + 200: { + content: { + "application/json": components["schemas"]["Port"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + "asset/vulnerability/details": { + parameters: { + path: { + /** @description Asset UUID */ + assetUuid: string; + /** @description Vulnerability UUID */ + vulUuid: string; + }; + }; + responses: { + /** @description Retrieve the vulnerability details */ + 200: { + content: { + "application/json": components["schemas"]["Vulnerability"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + "asset/vulnerability/list": { + parameters: { + query?: { + sort?: components["schemas"]["IssueListSchemaSort"]; + filter?: components["schemas"]["IssueListSchemaFilter"]; + paginate?: components["schemas"]["IssueListSchemaPaginate"]; + }; + path: { + /** + * @description Asset UUID + * @example 00000000-0000-0000-0000-000000000000 + */ + uuid: string; + }; + }; + responses: { + /** @description Retrieve a list of asset vulnerabilities */ + 200: { + content: { + "application/json": { + data?: components["schemas"]["Vulnerability"][]; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Change password */ + changePassword: { + requestBody: { + content: { + "application/json": components["schemas"]["ChangePasswordFilter"]; + }; + }; + responses: { + /** @description Retrieve a result */ + 200: { + content: { + "application/json": { + result?: boolean; + }; + }; + }; + /** @description Validation error */ + 422: { + content: { + "application/json": components["schemas"]["ValidationError"]; + }; + }; + }; + }; + /** @description Reset password */ + forgotPassword: { + requestBody: { + content: { + "application/json": components["schemas"]["ForgotPasswordFilter"]; + }; + }; + responses: { + /** @description Retrieve a result */ + 200: { + content: { + "application/json": { + result?: boolean; + }; + }; + }; + /** @description Validation error */ + 422: { + content: { + "application/json": components["schemas"]["ValidationError"]; + }; + }; + }; + }; + /** @description Log in by credentials */ + login: { + parameters: { + header?: { + /** @example Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 */ + "User-Agent"?: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LoginFilter"]; + }; + }; + responses: { + /** @description Retrieve a customer */ + 200: { + headers: { + /** @description Remaining tokens */ + "X-RateLimit-Remaining"?: number; + /** @description Retry after */ + "X-RateLimit-Retry-After"?: number; + /** @description Rate Limit */ + "X-RateLimit-Limit"?: number; + /** @description Captcha client key. If this header is present, captcha is required. */ + "X-Captcha"?: string; + }; + content: { + "application/json": components["schemas"]["Auth"]; + }; + }; + /** @description Validation error */ + 422: { + content: { + "application/json": components["schemas"]["ValidationError"]; + }; + }; + }; + }; + /** @description Logout a customer */ + logout: { + parameters: { + header?: { + /** @example Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 */ + "User-Agent"?: string; + }; + }; + responses: { + /** @description No content */ + 204: never; + /** @description Validation error */ + 422: { + content: { + "application/json": components["schemas"]["ValidationError"]; + }; + }; + }; + }; + me: { + responses: { + /** @description Retrieve a profile */ + 200: { + content: { + "application/json": components["schemas"]["Customer"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Register a new customer */ + register: { + requestBody: { + content: { + "application/json": components["schemas"]["RegisterFilter"]; + }; + }; + responses: { + /** @description Retrieve a customer */ + 200: { + content: { + "application/json": components["schemas"]["Auth"]; + }; + }; + /** @description Validation error */ + 422: { + content: { + "application/json": components["schemas"]["ValidationError"]; + }; + }; + }; + }; + /** @description Retrieve a list of sessions */ + "sessions.list": { + responses: { + /** @description Retrieve a list of sessions */ + 200: { + content: { + "application/json": { + sessions?: { + uuid?: string; + /** @example 2021-01-01T00:00:00+00:00 */ + last_activity_at?: string; + /** @example 2021-01-01T00:00:00+00:00 */ + started_at?: string; + user_agent?: string; + ip?: string; + is_active?: boolean; + /** @example 2021-01-01T00:00:00+00:00 */ + expires_at?: string; + }[]; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Revoke a session by UUID */ + "sessions.revoke": { + parameters: { + path: { + /** @description Session UUID */ + uuid: string; + }; + }; + responses: { + /** @description Session revoked successfully */ + 200: never; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Revoke all sessions for the current user */ + "sessions.revokeAll": { + responses: { + /** @description Sessions revoked successfully */ + 200: never; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Change password */ + changePasswordProfile: { + requestBody: { + content: { + "application/json": components["schemas"]["CustomerChangePasswordFilter"]; + }; + }; + responses: { + /** @description Result */ + 200: never; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + /** @description Validation error */ + 422: { + content: { + "application/json": components["schemas"]["ValidationError"]; + }; + }; + }; + }; + /** @description Change timezone */ + changeTimeZone: { + requestBody: { + content: { + "application/json": components["schemas"]["CustomerChangeTimezoneFilter"]; + }; + }; + responses: { + /** @description Result */ + 200: never; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + /** @description Validation error */ + 422: { + content: { + "application/json": components["schemas"]["ValidationError"]; + }; + }; + }; + }; + getDashboard: { + responses: { + /** @description Retrieve a dashboard */ + 200: { + content: { + "application/json": { + /** + * @deprecated + * @description Dashboard main + */ + main?: { + threat_assessment?: components["schemas"]["ThreatAssessment"]; + vulnerability?: components["schemas"]["Vulnerability"]; + attack_vector?: components["schemas"]["AttackVector"]; + password_leak?: components["schemas"]["PasswordLeak"]; + credential?: components["schemas"]["Credential"]; + }; + /** + * @deprecated + * @description Dashboard assets + */ + assets?: { + total_asset?: components["schemas"]["TotalAsset"]; + open_port?: components["schemas"]["OpenPort"]; + attack_vector?: components["schemas"]["AssetChange"]; + }; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + "3b44fdc99fa98a60db9f7ff54f5fe7c8": { + parameters: { + query?: { + /** @example 0000-0000-0000-0000-0000 */ + project_uuid?: string; + }; + }; + responses: { + /** @description Спислок изменений активов */ + 200: { + content: { + "application/json": { + changes?: { + static_value?: string; + ports?: { + port?: number; + }[]; + status?: number; + }[]; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + ecc48c49a2b657934d9ae02d44dad622: { + parameters: { + query?: { + /** @example 0000-0000-0000-0000-0000 */ + project_uuid?: string; + }; + }; + responses: { + /** @description Количество активов */ + 200: { + content: { + "application/json": { + total?: number; + critical_risk?: number; + high_risk?: number; + new?: number; + unavailable?: number; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + "5f173cefa03130c82d7cb590454151d8": { + parameters: { + query?: { + /** @example 0000-0000-0000-0000-0000 */ + project_uuid?: string; + }; + }; + responses: { + /** @description Количество атакующих векторов */ + 200: { + content: { + "application/json": { + real?: number; + potential?: number; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + "8fbabf2484f42b43812d52ca18a9a988": { + parameters: { + query?: { + /** @example 0000-0000-0000-0000-0000 */ + project_uuid?: string; + }; + }; + responses: { + /** @description Список учетных данных */ + 200: { + content: { + "application/json": { + credentials?: { + service_name?: string; + total?: number; + compromised?: number; + }[]; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + "34847e5e0fb5d3b3cf990d3890ac6b06": { + parameters: { + query?: { + /** @example 0000-0000-0000-0000-0000 */ + project_uuid?: string; + }; + }; + responses: { + /** @description Спислок операционных систем */ + 200: { + content: { + "application/json": { + os?: { + os?: string; + total?: number; + }[]; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + "4b71b242c7e6f92273eb65e7b71bf8ca": { + parameters: { + query?: { + /** @example 0000-0000-0000-0000-0000 */ + project_uuid?: string; + }; + }; + responses: { + /** @description Спислок открытых портов */ + 200: { + content: { + "application/json": { + ports?: { + port?: number; + protocol?: string; + total?: number; + }[]; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + a04f7b986fb9621a5b23776135540585: { + parameters: { + query?: { + /** @example 0000-0000-0000-0000-0000 */ + project_uuid?: string; + }; + }; + responses: { + /** @description Общая оценка угроз */ + 200: { + content: { + "application/json": { + /** @description Оценка */ + score?: string; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + "755168a919c3f29bf53ad0a6971d0f0d": { + parameters: { + query?: { + /** @example 0000-0000-0000-0000-0000 */ + project_uuid?: string; + }; + }; + responses: { + /** @description Количество уязвимостей */ + 200: { + content: { + "application/json": { + /** @description Критический риск */ + critical_risk?: number; + /** @description Высокий риск */ + high_risk?: number; + /** @description Средний риск */ + medium_risk?: number; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Get downloads list */ + "downloads/list": { + responses: { + /** @description Retrieve a list of downloads */ + 200: { + content: { + "application/json": { + data?: components["schemas"]["Download"][]; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Unread notifications */ + getUnreadNotifications: { + responses: { + /** @description Result */ + 200: { + content: { + "application/json": components["schemas"]["Notifications"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + /** @description Validation error */ + 422: { + content: { + "application/json": components["schemas"]["ValidationError"]; + }; + }; + }; + }; + /** @description Mark notification as read */ + markNotificationsAsRead: { + requestBody: { + content: { + "application/json": components["schemas"]["MarkNotificationAsReadFilter"]; + }; + }; + responses: { + /** @description Result */ + 200: never; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + /** @description Validation error */ + 422: { + content: { + "application/json": components["schemas"]["ValidationError"]; + }; + }; + }; + }; + "project/asset/change-affiliations": { + requestBody: { + content: { + "application/json": components["schemas"]["ChangeAffiliation"]; + }; + }; + responses: { + /** @description Retrieve the status */ + 200: { + content: { + "application/json": components["schemas"]["Status"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Get project domain list */ + "project/asset/domain/list": { + parameters: { + query?: { + sort?: components["schemas"]["DomainListSchemaSort"]; + filter?: components["schemas"]["DomainListSchemaFilter"]; + paginate?: components["schemas"]["DomainListSchemaPaginate"]; + }; + path: { + /** @description Project UUID */ + uuid: string; + }; + }; + responses: { + /** @description Retrieve a list of domains */ + 200: { + content: { + "application/json": { + data?: components["schemas"]["Domain"][]; + meta?: components["schemas"]["PaginationMeta"][]; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Export project asset list */ + "project/asset/list/export": { + parameters: { + path: { + /** @description Project UUID */ + uuid: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ExportVulnerabilitiesFilter"]; + }; + }; + responses: { + /** @description Retrieve a download UUID */ + 200: { + content: { + "application/json": { + /** + * @description Download UUID + * @example 00000000-0000-0000-0000-000000000000 + */ + uuid?: string; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Get project asset counters */ + "project/asset/counters": { + parameters: { + query?: { + filter?: components["schemas"]["AssetCountersSchemaFilter"]; + }; + path: { + /** @description Project UUID */ + uuid: string; + }; + }; + responses: { + /** @description Retrieve the status */ + 200: { + content: { + "application/json": components["schemas"]["AssetCounters"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Get project ip list */ + "project/asset/ip/list": { + parameters: { + query?: { + sort?: components["schemas"]["IpAddressListSchemaSort"]; + filter?: components["schemas"]["IpAddressListSchemaFilter"]; + paginate?: components["schemas"]["IpAddressListSchemaPaginate"]; + }; + path: { + /** @description Project UUID */ + uuid: string; + }; + }; + responses: { + /** @description Retrieve a list of ip addresses */ + 200: { + content: { + "application/json": { + data?: components["schemas"]["IpAddress"][]; + meta?: components["schemas"]["PaginationMeta"][]; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Get project asset list */ + "project/asset/list": { + parameters: { + query?: { + sort?: components["schemas"]["AssetListSchemaSort"]; + filter?: components["schemas"]["AssetListSchemaFilter"]; + paginate?: components["schemas"]["AssetListSchemaPaginate"]; + }; + path: { + /** @description Project UUID */ + uuid: string; + }; + }; + responses: { + /** @description Retrieve a list of assets */ + 200: { + content: { + "application/json": { + data?: components["schemas"]["Asset"][]; + meta?: components["schemas"]["PaginationMeta"][]; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Get project operation system asset list */ + "project/asset/os/list": { + parameters: { + query?: { + sort?: components["schemas"]["OperationSystemListSchemaSort"]; + filter?: components["schemas"]["OperationSystemListSchemaFilter"]; + paginate?: components["schemas"]["OperationSystemListSchemaPaginate"]; + }; + path: { + /** @description Project UUID */ + uuid: string; + }; + }; + responses: { + /** @description Retrieve a list of operation systems */ + 200: { + content: { + "application/json": { + data?: components["schemas"]["OperationSystem"][]; + meta?: components["schemas"]["PaginationMeta"][]; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Get project port asset list */ + "project/asset/ports/list": { + parameters: { + query?: { + sort?: components["schemas"]["PortListSchemaSort"]; + filter?: components["schemas"]["PortListSchemaFilter"]; + paginate?: components["schemas"]["PortListSchemaPaginate"]; + }; + path: { + /** @description Project UUID */ + uuid: string; + }; + }; + responses: { + /** @description Retrieve a list of ports */ + 200: { + content: { + "application/json": { + data?: components["schemas"]["Port"][]; + meta?: components["schemas"]["PaginationMeta"][]; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Create a new project */ + "project/create": { + requestBody: { + content: { + "application/json": components["schemas"]["CreateProjectFilter"]; + }; + }; + responses: { + /** @description Retrieve a project */ + 200: { + content: { + "application/json": components["schemas"]["Project"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Get project by UUID */ + "project/get": { + parameters: { + path: { + /** @description Project UUID */ + uuid: string; + }; + }; + responses: { + /** @description Retrieve a project */ + 200: { + content: { + "application/json": components["schemas"]["Project"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Get projects list */ + "project/list": { + responses: { + /** @description Retrieve a list of projects */ + 200: { + content: { + "application/json": { + data?: components["schemas"]["Project"][]; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + "project/vulnerability/change-statuses": { + requestBody: { + content: { + "application/json": components["schemas"]["ChangeVulnerabilityStatuses"]; + }; + }; + responses: { + /** @description Retrieve the result */ + 200: { + content: { + "application/json": components["schemas"]["Status"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + /** @description Export project vulnerability list */ + "asset/vulnerability/list/export": { + parameters: { + path: { + /** + * @description Project UUID + * @example 00000000-0000-0000-0000-000000000000 + */ + uuid: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ExportVulnerabilitiesFilter"]; + }; + }; + responses: { + /** @description Retrieve a download UUID */ + 200: { + content: { + "application/json": { + /** + * @description Download UUID + * @example 00000000-0000-0000-0000-000000000000 + */ + uuid?: string; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + getProjectVulnerabilityCounters: { + parameters: { + query?: { + filter?: components["schemas"]["IssueCountersSchema"]; + }; + path: { + /** @description Project UUID */ + uuid: string; + }; + }; + responses: { + /** @description Retrieve the status */ + 200: { + content: { + "application/json": components["schemas"]["VulnerabilityCounters"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + listProjectVulnerabilities: { + parameters: { + query?: { + sort?: components["schemas"]["IssueListSchemaSort"]; + filter?: components["schemas"]["IssueListSchemaFilter"]; + paginate?: components["schemas"]["IssueListSchemaPaginate"]; + }; + path: { + /** + * @description Project UUID + * @example 00000000-0000-0000-0000-000000000000 + */ + uuid: string; + }; + }; + responses: { + /** @description Retrieve a list of vulnerabilities */ + 200: { + content: { + "application/json": { + data?: components["schemas"]["Vulnerability"][]; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + listReports: { + parameters: { + query?: { + /** @example 1 */ + page?: string; + }; + }; + responses: { + /** @description Retrieve a list of reports */ + 200: { + content: { + "application/json": { + data?: components["schemas"]["Report"][]; + meta?: components["schemas"]["PaginationMeta"][]; + }; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + twoStepConnect: { + requestBody: { + content: { + "application/json": components["schemas"]["ConnectFilter"]; + }; + }; + responses: { + /** @description Returns 2fa backup codes */ + 200: { + content: { + "application/json": components["schemas"]["BackupCodes"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + twoStepDisconnect: { + requestBody: { + content: { + "application/json": components["schemas"]["DisconnectFilter"]; + }; + }; + responses: { + /** @description Returns QR code */ + 200: { + content: { + "application/json": components["schemas"]["QRCode"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + twoStepShowQrCode: { + responses: { + /** @description Returns QR Code */ + 200: { + content: { + "application/json": components["schemas"]["QRCode"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; + twoStepShowVerifyCode: { + requestBody: { + content: { + "application/json": components["schemas"]["VerifyCodeFilter"]; + }; + }; + responses: { + /** @description Returns auth token */ + 200: { + content: { + "application/json": components["schemas"]["Token"]; + }; + }; + }; + }; + getVulnerabilityByUuid: { + parameters: { + path: { + /** @description Vulnerability UUID */ + uuid: string; + }; + }; + responses: { + /** @description Retrieve the asset */ + 200: { + content: { + "application/json": components["schemas"]["Vulnerability"]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["UnauthorizedError"]; + }; + }; + /** @description Forbidden */ + 403: { + content: { + "application/json": components["schemas"]["ForbiddenError"]; + }; + }; + }; + }; +} diff --git a/spa/app/logger.ts b/spa/app/logger.ts new file mode 100644 index 0000000..d1b08db --- /dev/null +++ b/spa/app/logger.ts @@ -0,0 +1,73 @@ +import { white } from 'console-log-colors' + +const colorMapper = (service: string) => { + switch (service.toLowerCase()) { + case 'rpc': + return white.bgGreen.bold + case 'api': + return white.bgBlue.bold + case 'ws': + return white.bgMagenta.bold + case 'store': + return white.bgRed.bold + } + + return white.bgBlack.bold +} + +export class Logger { + private readonly mode: string + private readonly prefix: string + private readonly mapper; + + constructor(mode: string, prefix: string = '') { + this.mode = mode + this.prefix = prefix + this.mapper = colorMapper(prefix) + } + + withPrefix(prefix: string): Logger { + return new Logger(this.mode, prefix) + } + + debug(...content): void { + this.__log('debug', ...content) + } + + error(...content): void { + this.__log('error', ...content) + } + + info(...content): void { + this.__log('info', ...content) + } + + success(...content): void { + this.__log('success', ...content) + } + + __log(type: string, ...content): void { + if (this.mode !== 'development') { + return + } + + if (this.prefix) { + content.unshift(this.mapper(`[${this.prefix}]`)) + } + + switch (type) { + case 'debug': + console.info(...content) + break + case 'success': + console.info(...content) + break + case 'info': + console.info(...content) + break + case 'error': + console.error(...content) + break + } + } +} diff --git a/spa/app/plugins/apiClient.ts b/spa/app/plugins/apiClient.ts new file mode 100644 index 0000000..6e43865 --- /dev/null +++ b/spa/app/plugins/apiClient.ts @@ -0,0 +1,18 @@ +import Api from "~/app/api/Api"; +import { WsClient } from "~/app/ws/client"; +import { RPCClient } from "~/app/ws/RPCClient"; +import { Logger } from "~/app/logger"; + +export default defineNuxtPlugin(({$ws, $logger}: { $ws: WsClient, $logger: Logger }) => { + const rpc: RPCClient = new RPCClient($ws, $logger) + + const config = useRuntimeConfig() + const examples_url: string = config.public.examples_url + const api_url: string = config.public.rest_api_url + + return { + provide: { + api: new Api(rpc, api_url, examples_url) + } + } +}) diff --git a/spa/app/plugins/centrifugo.ts b/spa/app/plugins/centrifugo.ts new file mode 100644 index 0000000..f451011 --- /dev/null +++ b/spa/app/plugins/centrifugo.ts @@ -0,0 +1,35 @@ +import { WsClient } from "~/app/ws/client"; +import { Centrifuge } from "centrifuge"; +import { useAppStore } from "~/stores/app"; + +const guessWsConnection = (): string => { + const WS_HOST: string = window.location.host + const WS_PROTOCOL: string = window.location.protocol === 'https:' ? 'wss' : 'ws' + + return `${WS_PROTOCOL}://${WS_HOST}/connection/websocket`; +} + +export default defineNuxtPlugin(async (nuxtApp) => { + const config = useRuntimeConfig() + const ws_url: string = (config.public.ws_url) || guessWsConnection() + + const client: WsClient = new WsClient( + new Centrifuge(ws_url), + nuxtApp.$logger + ) + + await client.connect() + + nuxtApp.hook('app:created', () => { + const settings = useAppStore() + settings.fetch() + + settings.subscribeToUpdates() + }) + + return { + provide: { + ws: client + } + } +}) diff --git a/spa/app/plugins/logger.ts b/spa/app/plugins/logger.ts new file mode 100644 index 0000000..ffc5ebd --- /dev/null +++ b/spa/app/plugins/logger.ts @@ -0,0 +1,9 @@ +import { Logger } from "~/app/logger"; + +export default defineNuxtPlugin(() => { + return { + provide: { + logger: new Logger(process.env.NODE_ENV!).withPrefix('APP'), + } + } +}) diff --git a/spa/app/ws/RPCClient.ts b/spa/app/ws/RPCClient.ts new file mode 100644 index 0000000..490f731 --- /dev/null +++ b/spa/app/ws/RPCClient.ts @@ -0,0 +1,29 @@ +import {WsClient} from "~/app/ws/client"; +import {Logger} from "~/app/logger"; + +export class RPCClient { + private ws: WsClient; + readonly logger: Logger; + + constructor(ws: WsClient, logger: Logger) { + this.ws = ws; + this.logger = logger.withPrefix('rpc'); + } + + async call(method: string, data: object = {}) { + this.logger.debug(`Request [${method}]`, data); + + return await this.ws.rpc(method, data) + .then(result => { + this.logger.debug(`Response [${method}]`, result); + + if (result.data.code !== 200) { + this.logger.error(`Error [${method}]`, result.data); + + throw new Error(`Something went wrong [${method}]. Error: ${result.data.message}`); + } + + return result; + }) + } +} diff --git a/spa/app/ws/channel.ts b/spa/app/ws/channel.ts new file mode 100644 index 0000000..59b71e7 --- /dev/null +++ b/spa/app/ws/channel.ts @@ -0,0 +1,82 @@ +import {WsClient} from "~/app/ws/client"; +import {PublicationContext, Subscription} from "centrifuge"; + +type Listeners = Function[]; + +export default class Channel { + /** The name of the channel. */ + readonly name: string; + /** The event callbacks applied to the socket. */ + private event: Function | null = null; + /** User supplied callbacks for events on this channel. */ + private listeners: Listeners = []; + private readonly ws: WsClient; + private subscription: Subscription | null = null; + + /** + * Create a new class instance. + */ + constructor(ws: WsClient, name: string) { + this.ws = ws + this.name = name + this._subscribe() + } + + /** + * Listen for an event on the channel instance. + */ + listen(callback: Function): Channel { + this._on(callback) + return this + } + + /** + * Bind the channel's socket to an event and store the callback. + */ + private _on(callback: Function): Channel { + if (!callback) { + throw new Error('Callback should be specified.'); + } + + if (!this.event) { + this.event = (context: PublicationContext): void => { + this.listeners.forEach(cb => cb(context, context.data)) + } + this.subscription!.on('publication', this.event) + } + + this.listeners.push((context: any, data: any): void => { + this.ws.logger.debug(`Event [${context.channel}] received`, context) + callback(data) + }) + + return this + } + + unsubscribe(): void { + this.subscription!.removeAllListeners() + this.ws.centrifuge.removeSubscription(this.subscription) + this.listeners = [] + this.event = null + } + + _subscribe(): void { + let sub: Subscription | null = this.ws.centrifuge.getSubscription(this.name) + + if (!sub) { + sub = this.ws.centrifuge.newSubscription(this.name) + sub.on('subscribing', (context) => { + this.ws.logger.debug(`Subscribing to [${this.name}]`, context) + }) + sub.on('subscribed', (context) => { + this.ws.logger.debug(`Subscribed to [${this.name}]`, context) + }) + sub.on('unsubscribed', (context) => { + this.ws.logger.debug(`Unsubscribed from [${this.name}]`, context) + }) + sub.subscribe() + } + + this.subscription = sub + } +} \ No newline at end of file diff --git a/spa/app/ws/client.ts b/spa/app/ws/client.ts new file mode 100644 index 0000000..4facca2 --- /dev/null +++ b/spa/app/ws/client.ts @@ -0,0 +1,58 @@ +import { Centrifuge } from "centrifuge"; +import Channel from './channel' +import { Logger } from "~/app/logger"; + +type Channels = { + [key: string]: Channel +} + +export class WsClient { + channels: Channels = {} + readonly centrifuge: Centrifuge + readonly logger: Logger + + constructor(centrifuge: Centrifuge, logger: Logger) { + this.centrifuge = centrifuge + this.logger = logger.withPrefix('ws') + } + + rpc(method: string, data: object = {}) { + return this.centrifuge.rpc(method, data) + } + + connect() { + this.centrifuge.on('connecting', (context) => { + this.logger.debug('Connecting', context) + }) + this.centrifuge.on('connected', (context) => { + this.logger.debug('Connected', context) + }) + this.centrifuge.on('disconnected', (context) => { + this.logger.debug('Disconnected', context) + }) + + this.centrifuge.connect() + + return this.centrifuge.ready() + } + + disconnect(): void { + this.disconnectChannels() + this.centrifuge.removeAllListeners() + this.centrifuge.disconnect() + } + + disconnectChannels(): void { + Object.entries(this.channels).forEach(([key, channel]): void => { + channel.unsubscribe() + }) + } + + channel(channel: string): Channel { + if (!this.channels[channel]) { + this.channels[channel] = new Channel(this, channel); + } + + return this.channels[channel]; + } +} diff --git a/spa/assets/img/pacman.png b/spa/assets/img/pacman.png deleted file mode 100644 index f2bd0a09cf974923982a3a9059ee098dcc21699b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30525 zcmV)4K+3;~P)t(XpoAoVhy=|J%2SUCD37+MO@dl`Z)*wKdwm^A(sOTXeI@+vueR3PCh^(F z@0J7~^tSgj32J+LTTNoy+jH`sSK*%rVFKkH>s0g#Xd5OK367fW?5YsA*wN+rULYFt_|em=j83x<~21@&(ct z`IG*K2uS&U`b@~BsxnnY)eWtqeCj-dQg^vm{#nKk`v4LXUb`$eG%=8+Mgiap_sh4?Q%D4GB8KqRHMbJ8=kKVj6kNd2n7nxO_ zVO~=M-Vv?KM-glkb3d53!dK>D5x4Z8RFComg+}Kn8lOb2&}&v<%?!^^wfdO_0C~WP zY;c5ri1&p)u8*H56G8}e)v5r2E|xLW7RY6v=wSLgnL0U<%1ox($5SJ_U}|at#utvm z|3DYQPy4!@mZtyae@oK8Md{z%eA{&06qzsPFAG(Do6E|#8VNf6L{~vl@QUReR6^yf z8YcWEOK3nFz=%OXX)1kb)8p6_M9d|PJXgMeYXWtGeMqsg^h~R$W>_m=sq_#9Xm&%u zc#6g;tW`gQFLZnY?dQ*|g!d&e~wm(jI!Jjf`j&`(%i?M`CncOxbJ!RfM9Tyjw0(1EZ|f z{$eGFPI}-q3izpU30P;KtkSKJh;H#JPT1(CtAWN!2R#%x90P2|p>4M(z#E01;`P%3 zxD09tS0+VW3E3-ifauC(cjU??h!nXV6hMSL)D_Xs^*jKUYk-544f24EW6Ld`j@)tu zW6uetajN7$U}@bsiXdYip2|6>M?0w-4- zjxJ_O(c)D}!#XNHMy8m;C0jl84mfmxy67wtMFmv*$kihH z-=fl}eFSG~M~Ne!6eNnL#Z{Q^YIX+V=qqn!j8x7rbQ6sIwc(SohRUl%m!VoxBPNNl z0O&Bo-A+il;1va#G;f&Y$@}BR2pmL%3$9(BNH<24hrI>H&l!cE@b!}exTQ1~o`luu zQ~o!v^Q(=jx#WJ=H4{Y2p<_AR^Uea(n5;DJ{m7tVpTx=~mRBP&M6Vt?qDnRRX^3U5 zfR1zMo&rmZ-`J>E4i^X3K$R5+_sUeHN}XBY8i7FyA$t@!2Qdcb61JLypfPg+dk?9= zpEE5$EiXI})&y_|2WTlpX12h{0dT)DqD7L`t*G)d3F=<6S#i-0LQ?# zCh=f(V!#}NXw{)|`sKeYsSxgU=&e9dN;zbNi_FrCVA{Az4hOJy$<47*JyXyFA`>_h zS9Ikd575l22LO?rOhq31Tfv}~oLLZmQ~=~pCO}|rnh9?ukU^Cy;8C&$+t>9AVvIMBEH8^&uF)U;=)O*N+e21lZE2 z_W|eCSUDQ?6o3b;g(_)3=cM##npPM&6JX(s0A6wi;7IyA@1(l=T*03GT+hyX0n=v~ zzbF0s4#DIh3D49XQpZfy3hS`pKr(3Ms@#^`6p?aSmOKV%8>5^b+NOz-S0v6GVVO zuaylj<&IsC!BfLi%C%$xkqtP;1YX5jsoTIr0@eVgQ~@2CzuZC50Z&aDv9fi)Ty!|6 zhZb#OT?2HCpiK%%AILt$9xK?WA@uHtj08`@Krrfzu8OY^<*$>T@Vs190{0n0xE}i8 zgoKTC9<7E9 zwaDEW5LdO86Cg|PtLIrx!r}FWn0VE(Brw(ud808L>+IZl%uH%3u-J-nv^|m50L5mdK^O!kTEuST3Yrbr#bl@ z`$Z7IEEv}~RuDT^Avb_os4!r`i|>LPd;Y;_XnniW%vJK{&f5ad@%7vS+~qWq{<{fy zR&u{gVk z?n*^>oG_qnY_20%^@w>20Lho8NWZFFSxiq1%uy*|15`H#NP!6W?wiJ3tvpi!sUwS* zfeH%IO6z!-Pd}(f+5}Z2;9+VJdmT4_r0N=KI#edj^6Y<;9D}wp@<9XBWCYGw^<0Ln z&mF)euPqV%2kFz?00%-m^ECt6nl}Tk{$GIv)H)sPy$HG%|Jglf-zQQSyX7CJAORgt z?5CjMCLwtMTL+ga7Za=MTDdT@3Q_1}xm#?*jdc{bJp5!cSuxQZ$bmsC>s>9sQlMF* z(n`r)mApy>s1nsN&EoAvuSJ_}mCV|6Wl|*3oC}mbfb_BE5%(V|aD*^nLt)LJEj59l z-@MdB@}P~@m1k&WJQP^;*@DzHq?nNqs!BpUf>g_Ttb}yH+y=p9NaY@E?8IZk4)3`O z$_pI{>KGxA?`HDI7-zv~mD3Bu;q8g@?GrpM`v!0-?QU^`t*vsUF(hRetMlL%631si zK+k5JJx`3%0h)xSP2Sw14e%UXKRUqWT-%Wpj<&Qq&ODY&IyU!Yz%_41O2CFa-Wr!2 zq^GNsT`M!)bJKk<*Oq^5Y2E+x4DPwVjVplkkuH$h6{knt3Mn3%V8+-`!Nvz3>ZH4T zjEihLV2KhIf}2b08C6b708RO-Nf#rh24@vanF%rCW9VX~NP$I{Xw)cKfZpy=0c=yK zDmIeh+BXIuu2TpX-i8U$;Hyxzq(OPk*g(z)9PG36j=$Q(OHgj;8FwEq1#|~5=b0iF z1PLXm0&}Ma2GIv-kk%O9sgbd^rJFE>Zh`?IXD_;OqANK80rJ|WBl!wBHAY=S;j$m8 zcVL7dThuYCYK0-Df>LhoTZ^uNAMyIp04|55L|aoOuOtk@90qmGTN+sVpD~7^UEt^g zuIaw_f!(pzRXv-%xa85{x<7x4>JIuHG1hLedv3!>#h_ zAwkTAwXr)27i1xkEFP9_VL-iFp{0oA_8P?)h4+pygw^<9eexa$g|65Ps;p8>WC7yL z9n@A`l?teGIS>s>hO-$^#CB*vL4%MA$CB4Qou>b@_eBBuO=5DUKaPcHW*>s z35YyJ{qJXB%r$Gs-(8Cu#ug{2X|I6sizeWQync9qEBUr|NBXotB+FzycVf{w&}{mH z8L;>q=#0yAdL6B9I?8Y5{>1$)EPL0JFtNXt1gNT{8p z5J%|5!?kC|j|eTH3l>fq9Xgp4)|YAJ2AaC9$*Pa*tvD?9Fcs`+;7JLc_z;K{cy#l@ zMT7RCcRC_&FgT+0SQ#k!MCRlP?n4B4=!9};83PXcmG2!}6NWF-+Dl znQ2g{M+cz*AmPHQ{m^DTu-%S3FVu@KVnIac9IXwg;(9;~^Q?S5wG`H$|3_qpvJc}me3%F~g3KhmM)(KQK=Fe;ev2uW@?u{&Q(_EsV0(J66A-ARV zM1WDD#NZox^eHWBg1vxdhrNLw4Tf`Ft%h4Z=^D0~2~-dc^i7L!Rpts+N~^`Ap$PZE zBU&=V!cGFe6F$a-B!Y!c(G!U|Pt7r+A!Yy>G<t$zefF6jCno_EQi(0?FX@zRkO^B{UIgbA2> z`YO2nvR&{juV)W%mmFS_8ZNAM$t&B;I=_EP|l8HFGPCUynz&|M}79*a#12v2>Zx`Kesu9ynMEFM7==9r?Ar8YSPYz!kh z`8#1eRluc6ZR`hKwxc71&h(&JW}FoLFaW631q7Xk(0_vv8S8IxLZ*@z?@YCvEGjwq z!o2)IYcgU5F_3Q0DUkSgCMI}ggXp2ORufqPoWW&STVzvQ83YbB!OVmW4}Z3arI0oO z&(nn7Tb{Xe97OJvoP*FlwG8g~xo5SR|Jeb^#H4TI2d`6 zyAp78fa_j;Woa6k`v7vf--oqVwVOg#T=rXsOUN1(u{L!1wu24n?|#43g=X1;VXi22 zk8n`vL;X4d5u~q!iyCe&>PPf(*rafyOA z+0}HgzIVI@2$MV(f?CMTHF)S+;Zht%8*l*}3io5W!Ky^(m5wY#7ZgPoDey_%w~u?9F%X0!MdI-?+f8aCy1l^TmtwTuA_IUwpGI zLpmmbZpe9<8J3A3u>j_sLFh}l*{JYGd{?#`Z_s~{HI*vgmMWo;HHK>VG_RKm?4}uX z%piB{%757pc4u(whFAsMuAh~`ErJOhrT#AP3rcYiH=ZnI7N2`O zt$zE-xbh7r!Lz)kYj@zc1ah$KHy?xXd!8Bagz{A>zO8dIEffqxn@)!?kPtI4!SYoz zU0@W2Hj3nl@{n8eIF&CT5KvTWiD86mlZBz&2j_^Qrg)^)1V;syB;7Lur=Pk*Ke!z9E#9ctYo$opYyW~+BtC)CzDiu$_Tff?~~sQyls-nfIOsP!}dmq z)|QO<(F=@phmjIhF{Dss0Jqdo@y8<+y(EwAjHF;7@?+z0+=+R1brOzp9UH)1a`@WR z^VR%Cs7X&QI;Vjh*PnnLaEq5Cd4HSn_bxc5+0AEg+5i3kP3(WB7lcO6oJl+W{0z*0 z;plytj>bOP%9R0j**hPDT?ynUhJL^v0D@T}f0iiZgKUv2fmk5`UpI#q=74w-sP$5X zCvnoE@`5@sB$!x7kf|p`V8HQ%E_i?}oHc+@aID}`9|Qv?l72C$yA$G=5)`V+Ica|| zg!J{Y5COGeo-it311j;?$Vl!7*i0mO1C9#yiV;-rDstPT*1R;9499#QCU!TG-2F4( z4I59xik;9hSQF)n7%WFllU??#N*L_LfeggJ=!3}R%&G=3Ko!}N zDgdZwz_lq`r4US(Gp=jwWO~Cqjdmm4{vVIJPVT4x*IaUFb@G9)b?eSXc;w6mzH#C21ev(Y}Sl!1ELPF~XMawe! z*YZNbYc#!LeIg9Kj5FujZtbZ+Cx_=9q$p*fZR*pN{7u*L2*E83q!m>b40H>1^|Bga zg#8B$0Bzfu@EamHscgETbuPrzg21hUyu_@*BNvf$(E;_9>wXLPL%nv-H&kjff0h{uhXYltPtE*0=E>FW^l9IW)~bV{2gLyrU#bIUpx$3 z{^4+6ebtr0XVwXbOJ6;!ul?@lK0rel1nBC@^)Fv~JZ>L70+WZQWH3Z9*;7G_PP*2# zYDfi9g*t+cI6_5w2-OP$_bB8kb^t<;)O%2Aw;xo?(bWvBUi%F^uf(BlQ?!a)5ZSXN zj4R`G&fnTWRqPBXykr8AFaj4q;2VT|j(L0r6{!9N;=qKP!ML<59niW%l&H&5Dj*=} z`n-j?7;}U!>yq{Y{u{@QDjwJUvrI)} z%VGZ2e*+I}85*iKFx@pZV-x1(AjE^qORI=B{lP2<;N~3he#EN}Y=#1sUK>;1t*1;c z0F9;m+>qa<1Kz&65o*A*&m(8ffKBU8#-OB$(`^yfa$-*yWKQslz_DUvcL;=!dE%T{ zSk#sW|~(Q0ceev@*GK@1jmSU zT+qD5+$WKXQoqUrxjbZ^WFDn**%;&nZJF=v3AJiGEI$SU^fK1mKIyoZbZ?`)wf+&I z^+`PHD5o;MAhHDtWW`-d^1#bXYW;^Sqn3nH^DU5aDcnyz{=F<5PNtz=P20EFfq~RZdi81KesGUTK{X5e03&0c8 zCGWO-`<$U(mr6g>AcoWYKJ>ziC#Z+UVf6uhbENk^sAn9i?)(nW4{k&IufD)=qna2&!`Ib*ZUWoj}`TA{8(K$Up-iVzL=m7jxjQ7kY01VHSx4Pk6h29v!z zS+i&oU}Ht#IY`nW@)c&J$BEY`Ojk>6x-w15G3EI{pTM)1;wW0%Rf@r;nyNl(4o@42 zX$A-{_9CKzh3SRa2k43Br7*ERS4$SqQ_ysj6@BdB^uvfiM_qcI)fb*?q2im1j4tI|Y1TzyZNRQnpZ}p`!AUG`wMJ z51<}GvDT#g&Y{78g;NU;RU3=~(^;EYF;*l~P;N!u3LW_E2JeXg_+d~pB3dOrF-Z7AQzM9hT^hNNx zN(#fhPvEol(E6e2gCG;7AL7ZiQ>5W)J>E<^nur&ErAMcXZ%83brzvTp74KTvK@35q zPgCeLlkuLSd=`F8Ny1q?T$5eR?`FqZ1f2am`z1;ah(3gl=g(hR@x5YV!1N{A^dAu^i*jv-{jY8y|n)ybN-&-s#!}r~z-- z)uqqsN;9_k7T)}gCuMNi8vyr?Ff`2L7ZT?KvywGkq50T0b;#1+NoJ&qpx4udK*8YJ zk`#C0!eoZdg(C$4%VXbYe~9Tu3Vf3=NMr@8`&I|YfAsLf#ydqk=?qoUK9kr0TDxR@ zWiMDbW&8#UwTKIW<*$RU!1W@ADgd-gpbrSm>A97LWZ!F&jR#xwtx1F3OGG@Y2+M&D#P8R%j>%$>!^wtA|(%1#I zs;)#I<@ZOR3U}}*EGb*QnuTLahLX1dw&rMxUFxBb&{hj(%7$(VD_jH3Vt0QQjEj+M-V1+ zlAc8Kav4x~y&lQzGzrLy*UdDD2j1G4k$PGxkEFaZ*%06a&nAR_2Ix}&aRP{>av+=Z zaSxow56k3RsWf?U&l~XKY7Z`XZ)qGhK^=PGwg1OSxb#)W6+10dMTI9A1dVQ)_*}Pq zyssC|Iss|)w_+kz|tz4SGFd z;LZDzX0!qb>zH&_&#W;9fEJxEg__XO&`*GBVJGLK1Qej=M-UP7#MHxb-Y~Ut&X^#f z_uu$HwsQc3l9AISMQsew09Ninq@j@1y1AvX{3!WV0brI#6mMKR6)$v|ZmoZTccN^h z8m759u&_YHP`zoDU!$))nZ3AQtGu|Hj#uqpk;-lYUGA5DUywYc-CsPl3kXNM2nIUX zTc!&ry>)H*`@?y=7LMQjRCy+^c}xFj^;=I?aHFHNaWq=-S(LYOc@5seU*1Cp54F+> z7aB6ATZX?;4Fn$_6C}eW67f(4krm>|I03A?aYY&nA?o`QP`7O91cv$qQmqJ3oeH)j zH-wNm?wF7^bdjwAip6`;5_EnS9HCGI%`*b0ia!{!aUD+S} z3!Qtz>OC6x3~OW9@5AZDC+vvST0eF$y~M3~NWh82~V><5JP`nO=3~;=)VT7sX4xA;97wUn@95 zZ3MI`6;z%p48)0t6;{>9RmA#{6p14+*G&ZF#}4nT^I}`dn4!NcTVc8YFxZT0XO*xFqr!s6q4LGtQe{yrmYRTa<0JPc|!&h=8 zv#awxFxvX89598m233WjQfRe&;#PVUJo(!KqKD^NSJkZRx^Y@Xlz|ML-(J+f9?U=I zp|z0KNUJX8T>ai;aGZB{VD^vzBHD}VKQDbYW}`#n9t^nH#p0Yl2h-@9Oj(>&9qiH4LL zR(2Q-@%M5s0UK`ks{^z#f!pMvDLoK_=_57&U;`d}4;?4sM6imK`2%M8SQRxAl?vFx zktvjwceRmU*`v^57r&p#!deAW;K>C3NM$L93#TZh@3pkVKC>ifCwR8dR@OH`$wHh$ zVDT}S@P}%7O_#7y8)yyIh|D8WJ_z0lxCv+y$(}i|c$o8$V4k$NnXddi>*BKN-U`#i z5gh7RfWfTvsdA(ce3mZ=l7N?hGLb633>8<-+fI3mgY0>S(WnA2ZlI2>9cg05k1BNt z7S4rJoX4^T!}AmD`RSv~Xg@I}|7%ebffQfj4ApwKV3#C+Of`!|&^_Bg^+T51knL$C z5)08GfZ%=txl&BQmFeDSE@S|go?c2a)veOAe90{TxgR)=c156?4q){6VZbxpb1>vN z=(OAY3`5G>#lZF`ZT##3O27tYOHhG9REJU6DOKCf8@n*^{>t=Uf9at|1NIXegNDn6Y|W%h!KkNqvg+};6{O0rmw1y`d0GGm3nras)pYv05Pj2war6P`y2H<->th`<99y^8xp8(`G-lAM?5EFUKpt-C&o#=uQt?H<>^iQ z5H(9@gCv$uR;M#a_$(g=EieCRjp&ww2ckM+w*e_Ix{YKR#!xi{ z=qnyz7_{g@R7x+uLMSRefF$-oCN89p2o$^=LR0z3eJ>xzvNZxRsK1V3B*k?U9E zqpq)!8e(Gt>hKbI03^&0Ac$%J5Gp?CxZCm(Loml>01w8z#ztAfs3Pa3L4>sE?W>u| zpW6^8l6YUO;sRE6h@XV&b(Y^-FxV167Zc`Za+z0Lj0}arM$p!MrrSW3!}FGccxcxh zkIVCBDj+X=Q7=={7>#4N5RNd1UiP<3;XD7!C<9#EUKs~8SGwfl6T3jr1)u4_q7Q_+ zW&7&Ep69z)U-_e5eV_{6_C~+{IIO$=AsYL}4-h49A-e!%xx9uFQOUp^HwqX=9^ctM zDQ(OnP)WhGk{@c-5^1RAQp`<|oql9;NU@v9E9$LJ0amFR6LGQZU7H4ripU{c1_vAr zXyK8Ev~67^JGLuC?$UyUYsP-zuYowlT%I;0o1w`d02GCa{>F5#PM5V^DaVuqG4=gNDW%bmk zde~t32h@ka5lDx}Yp|SYi()I>wvrNxE^9iF7Iv0r`35RbIpjknuBI`+uA>Sm~Ou9FsPRuG#6RGTALpWBPKva?s-VgLg9oea40fKRqEYB9_QI&kP2d06v$ z5M?Lm9aUo-WFmm8SQpS&x@GUE38Gz$FIpx**I%j_*hB)!Xts(~$evb|O%Z*?JXHHZ zNJLNpwI)CO2RY{s>_>N`H3ahbiXWX?n!c}>92_kHO&W6FsAqp?AE5Nz!)bm$mO7s8 z=e~=2>L2*K<%2;{n)i?;yv zf*u_@ti3d*(~u`p#K+(`dI@eskaJheXlCB&2T8K4AfjRys!nK<1$k4v$_R$wNKiLT)Bzhtx; zXUd4R@=Rugjs6mmPQA~0z_RF+99n6{T%DT@Y#mEQzq8Y_qSWuFnanv=UGtL@VSR+O9Hk% z_df;dN2yt$&=#8A@t6rz&Vw%5tg0#Ul$O9as)kc6=-CG=$!BzwKpp=e{a`eqfDjrW zQVi#K?GkU^CU2kI5E_voPSoH0zlMw6=h8CSd1Dx#$&|}#Yx4X%Q82X})$1}47ETGJ z4ITg#bp&ITzo2pqmh|yB1$7Ldnh5}RdNn5+*3q$gXt>6S>{6v187qL}gKSNKc?;^z z3UrxwtR9|x)I{9NoKHJM$Jo`#r5he_QZzA*;aMSn1~Gl402}6dH8KyHWW-gGtGvWY8#>( zRyAc*uDNi|=N2aqZfXSBL*X|Br7w%lJ8l?AbuS$99gk8r)(_L%KNfHs26k2b1=v3O z5M1}!N9n-fXN;-RtQru%ujWA&Iao~90Qt@Yw7SZ_b;gBIi62B8YZ+Vy27Q2h zL&6L8!bslErM8Ka62hf?w*iYcNTsMzKs4BjgC2~ieqar#a*7}m8Ah{@oH-NGCVN$2 z)T(_kon(J*=AxNNpcZAPAHpKbMdzLn80y0p7~Kad!+@|0Qr&vG&v*ZJ+rp#NU6nm} zxTR|oux=;(+Y^ifbnaI?l^Ph(oo&S6~@Yobg?0=HR z?|d98jB*aD(7WNbgodK0Mxnl%*(lLiA^n{@dnPQsWKQbOOqhJ^X&ArjFiq^^H=#*n zh9Ev3djRTFup-Z$eO!9}l>9tS-2ViO-+4G0rl%t?OXS#AhDQ=>2EfZ((h!`c)& zix;~1{5h%qlS}c5y+6qHCxiYpg6HGRaAS=KrvFPXJq;Gle?IQKb25#=qynNMR+E># z*h+K=Xe}HbnSUZq?E3)#^yCIhqdGrg%K?H1ZCo}x9<=Isx2QWzvCBr|-d=qNSXgg~&I1Yw|ZjTrzYIZ5mSoa$Kj&KJX#ubGGI zuipoow?0TA0jv7kmXkem){%!k|^~H*S&iLR=;EZfPq>6 z=?Pl*sR`J9<7Kem%vt64=H;VAYFnT@4{P7OFg<@(l(ABF{ip7O^8A1OSsa-^i+g|J zO=Sd^UU~}b*nDxJJpQmr>V`2H%z2<_QHW%URUTCz4Z~iV#O^4=F#wa zx;0myO>5tC4rE?M{uDa5?uGTAyf^jX@iLyfZ@MD&;d#mjlkth0uF!kXoc~2N(WQyy zuYFOo_PysP@IKkf$g{M0>vv)OC+>zl`yLmQ%tm>^{O95Bue@IP_XAk=j<3SjPhAR& zFPK|Ojo$JQEW7$&$Safs3~Vw-e&IM7wqE};xa86oWWa{AulOf2R}8|!BNA7vESaIO z^^45nt0{si<|m1rWKxK0Gu_4*@Cdv?+s>Jpg59}f33K*A7S12~8on;L^p!gn;F}KQ zs8g;F%=+3XpGR*y1naMVfU*ZBb}CqSNB!_LfrmL*lhJZ0CtxTJpRL}u=_QHcg0%@P z@U3<4Ity1^c^WMH^*fVgebPLig=d|BtKV@(d1&oB=hNnG-#1rHPlw<>|)bO&>W2FM88Asf2!@fjX!-S?JS= z7CPhWG_HI1!W8bEWWSHzc7V3s^bk%yHkCrfIT_vO zDf(k{Fy&(NEalb9nZ8BmpNt9UiZ`X|vi?2ikknnujNSGqZ2R&9C9KS#k%8-d?-+sl z(xaY`*1q?=T*gLm$Yt;f;FkFJ5N!Fv_h9$F!?5(S7o^`8gysZt2wDLWQ?cE~`?C;EkEG(UhKEF74dP5m5OpT#rNwD8R1 zOc;NQcRxOdM=6if)Di3Hy_dttE!!W>z@~0%7Od7@E2;a1JV9DiV`TpEFmmSc zDP%iD>cQq8@?^x^G>iOb0RT@fWUpxM>?Y-2j-ZaKb4W~?rlvH#AcMGh>-S;Bub!5| zhv%md@_D%7cNWmf58j(YF1q?xUr?$@o?Gh61KKSfG>t?VfO$p=I7h3FV}lh>aowl) z!1_<^Q63fAuDxL|?EL)2qDZsg$}3L6E!!Wk*)a$}@5ctU=9TgRozV#SS%)i^pJ9Wt z{VNZ_^7nkJG@1oz%eMO|hsry)yb3E1ND|v~lH#tu`XweO8*bPG*L?hLYiY~2y|n&Q zC*qDRmuT2&-iCl=B@0<0SN`6&VeHmNMd*>{qJWQu0Dh!i~SPAmyMI;m*H%C1@DO6QZQ%G0yAmHxfh__RI&lcI*6>|2~R^_KKK2-+7pF}C&a zk+y7mAOXSz@hPp+_w*@wD`}Etukt$y-y8;IXyZ75V1m?LfGl^c2{IvJ3vVb`mb|l+ zSJN@2H6OcMhcxfuF3aI_GCt+OD7M3VOT4{wuH~dsJ}Y@QdkFM3iBeAj09y5dZ%29K zapkeDj9>P=ZocIazF+>|!t&Rgu3>BTzG&kOyM?91<3m$z@_0zRl{CR|Tn@MMa+~B~ zoo;pK-`9WgZsZxi@~`lCJWUcLuPn+j^FHx$$)%@QpSj$3_^Z&CmGFiy>@nVCuLnU> zDefiPMGoxw`6ZW~X1TfqdTgDMQ(lTlKrQT19JCoC|YKnn|h`fo@py(QhUX z@Iii;EMo!kX7{o_1>~)1Lcmlp>n4lo!}uMKrtto$QrCjBo{u?~H~P8P;I+SZv3a3O zE_s3EKwExy1(|${LrLO|wUz_56SE7(wj4>3y_pTTo~ZveYn?neU4)p&qyZd=XM&1~n$heb2;xD+m%pRZiT8`P7B zQF37U-{@_RHQ9U1*Okjo!R5Od{L+=* zN=finCQEuk3cD<9fpypKNec9&hg`NYBY$emL&xAal6XmyZY}@ak-|I4nE-h5BKNaR zp&x1ze`!vZqi#Qiip8?JQkR^?$lJz@WC89kyn2oqvFyD~-2Vej4FK3q48p8@5XP7% z{4&s5(jImt5KhmBO&&#Qiie<~l2K`sVh--v`^d?9O}Xla<*#|6$)L?>I->%(!kBs+#?q<*`xxJDfJNY6y%d{lzk=q0ME{k$LH}oR_{(vf zEju|jaEd^r1B63<1DM{^>-*kYK81L&^KW0CW(4npu>>^YiaMmY<>R4IuQqJK%LUi` z(QaCKq3t(4k_81p-?^!4ID-2exD$MBp2FXgyqT+3 z-%dOK?&7Mi^G=$XLZdUwvWW4!9xuWe_B4WIXJr{&E3-fc?1F|)D5VycI<98iW zkm1=#nd6FNlfG|(w_;?0$e}>v$_(m46gH2XHOu<2GkGYgd{{Q1H>eW<7FlSgWi{}~ znU*)B(OVvYvJK0eM9`Bu0HBMpqyTPJPD)AvI%}^Mcy5k%<|PlZrdP*J!F;!PaQVuL zo56D-dw%8G@wEqaB%}$EF<-0Znj^S5k#bG>A8M*omd2Rba{+Z(x6J_*I%|d;_}van zh0J!A-*?@40Lt6gKw~?UwT?{Cwj1}t$~P`7zc0REPRb*GkD$|r8xHX0Kjm9M7^bF# z?@KOy;ZXh06Ak%qHtZWV-w7N4@=mZZipI_&1(UBPIwgi`3il|>+X-~BC>sXDNFh>W zb|(4B24%qWx*DpI=|erX?%!d*^+@MXAVe4rjz&kx>ukCp<*XjU^`G9Gyq_cDJL$Z! zLOV%@GJlwAWNb=5Z@z63axy;GmvUnn+;DA5(#L81vD*&9)~`I0|MEIApSlLt5&=4B zL}_&`lPs9@P8J|tdDRQkOzIi6K&NZ_H|2nQzj9u4rQQ(>TR2rEgwalG zn|N)LS(uXNa(s19QAP2>G&`RAjPh?r^cylh|$En^ievDmh&%l&P|gC=^wxl z{a^O>FSWbwcoc+FX!Tnk5oCKD*~AA{Yw4kUN8iUnVEewj+0uL7*x>*|l!M_($nO=D z06(uy$%%Ft;z3p1sLMPX4~;tY01OAtzP8HueGchb z{<@dY*6Wv5d`;h*tKPXIIS3ESWB_l8)2Uh1yrRgd$WY{m@X?$cLOqxlFrc6;hhv^; z=sKB|^Pta^G4m!k86F1@Is?+ZsPngbuUi*}f5&p+u3ESkDgoT%ek;!ca`(VVfBBSk4r8+IfZK(@YyexYJQ6F{?trFB7h%k;~X*O8ISySPrHJtu<)07Vmk2AH6U2n4`SfuNoIEtKJ$X zjZ@g!c9fKjgDfHj0SGtu$ddyW5wbqBy_Hwz#SS`X`s}X#Oa`)SY$DarWq|*!WU~}Z zb^FYym<|n#S$>$;Ex#ht+aMs7vGi$Snz`Kh#a~R};fQ%$WtYdDlQ4SAgOFw(J5))3 z$#@LLBQkzaM-53{`^Y)lf>ZoDJ$^0Y6xPNBAX+!)1DTP~S1Ohcp~|)3ET zYgY1%ZVt=WUAME`8<~F+?fm;Uq{)q!6IazrWAO#$dDM8)pnONxTbCPcLv+yIAtwrg>#K`bwe`AJz(sGo zt!y0vJ?4Xb3}g>b+c2{$zDb)VEgr?O+YZo@OXj5n`28uI-krBDK~#vm05^+FO0p*_ zmu5S+OlVxK2C2-7%2o=A0l}Njkih#YI(DF;3J z0j7l@ytiW!p$~{0_z(@)R22!VLZ}w>l7?Sh$SQAQch`XV7uvn%W4rPeGO{+zB|$-S zPBA?C*i+aEi=94_XEQ({1MIoEfvhTLk!e^&gKleGSV7_kUGR$?6@fC{Je9ksqsMsM^Z+(>ZBYw=5Z=&0qKqtlzk^d|vmyi%Kom{{E$ux9}z41HnvEgI0uFPQLQcGnbgM zAy-$CVi=?QYm0t>@-Y8txP^9NCgIe0D!v$Pl!*4+eH$^ItYS&;0K+1bd>LX zz}eScFD)sLhAWnzPH%kg9c4i!i0KV4909ei`OeyRFT~|noK`By-qn`v`-`i){*$}m z+7FbiHL&?(=hM;^-&Cm+C6io>v1!Wy5|amyka;9_wH)DadyV!pJjcX~;2k{l zv@0L6=jidRW(Zn>I?qJvMN@Zt9WY#8xw{=tjFk$)$;QGV&OO;X1M1x0)*JVwyyN5c zU5Hn?*M?ECFU{VTMY8$p+~?1fX9V``y{+PH#Y-@L`D@Rz1++O|wd@@?<(bA%1Z#ki zr_olp4jLxVzMDL-$-JTDP2cKh`o0}V;Ysq5leR%AQRKQNYGFW#_V zFl|}rWjm1>lY?bX>{J{;ro5fvc8UO3p#p@?7ann*kiX}Jq^x``N(gGe9~4Qkqx`mK zpP$2568juVgM}UjVwEDQfuhcRPO~6*x`oKrx8-YI8gB+DUZZOLBTFp^JN3=!?#C=R z>vC!t_X+I8Qq6@~-hfmf34`Fb zQyfnaE1ji=67%I`R^DR7IRq3pjUSLjmI90@ISRSQ)*T1v`a`^H>Wr z0I>rU%(OI9dR*Z7qk!%qaeLSf^bLh>@eHA_f4ZQ2{EkQMwFoF&kD!ecM81aHqJhbjEuhtqRTli(Z28DpCnaOYQQx`*Xaa<`pnJ>T-9@W@dk0Cs zGJQm5pkuz35JLWvC!?lRhCmk-n{gl?XO&&vKdue|CV-m(m(>NL zqXAdf`pGyAm|^^!|ZOCIrzKv!!(vgrId@p)!nA~A)G=(L}iy*Tm&I=5Zg&%hL9K>I~eK?#CV zO5BtFjotd7_3x_J%_s3}$rJgeT(W%Qcm)aC@wM+8$;)4N7TL;w8F!s|TJ_H{+f4QTkUd%{3JT$8BHUqrXw|qR4V+0xax>6OEKz1*aGv zM!$AHVeOd*!^3hWjLhTLFd$FK1cQ6ID7lmM8*ZhAKmXUX?5$rcVY2E639aH;B2Rd@ zOxnPiCrc7~$}R^TOW?@IWy5y(s1B6pd}2)vLp#yH_|J`}n+J=#S!Q0?0*-zCU<8aV zcyvEw&*v^U>0j)rW0)s6tnR*NrUT*Z&C%`~e~#9@=WJ6@Uf$tMBg-{2Lvy&4*N<_L z0!|>?f%O@cR8nis+%C_eZ9P#?0$E;>%B}jqI9>JbuhW{3-yxx^DJDs%2SFHsL2^n$ zr&a<#o53r5T>jb@!MgXJD=CPqfK$ba=Oc9FG>Zaq+rGSC`#`}9OJ!bCa!vw{r5_9D z=lzc7b$H7sZjZ?d`F!OyX6xR2q2aK4D@*stfl)WwT z*=g79qW@Ds${z_wc{~-k8B`dpB=4H@T!)^((Od4WwV z1p1aGutqi8OE$k}R#=}mg|VZnrHHj7WoG^6*)_>ym!rv#4 zOyR6qGm?i`_I%>yi%(7cnV;yG3Fn`EQmShNumA8R1MRQ7?%PFyQer)o;q6r!D6iXe zC+Rs8GM|m#fA|TSJLiN%Tk!&uH~7Mo8+&11pb94*-z0v$5LUeD?9|R>rJWRR6M{2NKQYmFa+*1O8EyKbpTo>M=kMVJBHtk$Ljk|M z7#831hJ|=`3g<3OwB)4xwg2a3x=2>V1vYHXC(Rs@+3V!_U3dyEzVKA$&&8(~Ie6zAAboBwzTp8wLhMK&fX zZoiZ(sfiGV+DA=?GOOb78ufXH8D%Y^8v0bc+r<62Iq z)`iF5{qvoS1^m#D$Oc(DEF)ywh)CPhoe2mwbF?{uSMt2d&uc#V?KER`es?jc@gk=l zmj0))vIdK(;fbi#D_(}z{@$XhY`k{eIZ2Bym}~d(-2cyL&80X0;ib6ZjU(M>ZA?>w z9aD(sj5zoI_1sfZfYHyshL$kM^~%APFYKljvdyRr$2p%XA2K-Y_+0T$a;^;*F53oG z`^-h0lR#w0XMZt))4XAFVeM}I{CDuzfA_z!AHl#xJHPk_%z376S-mlO%L5twkU#H- zE8h0cCJPzF)~tGY_gVYA{<@v*>Qygq=FVoITmG9j(igw{U11L;Fg`PRdB5B>`p}&H z=cEv@cFn6mW-9x}pcG18TM8lnKIH-*M%k`{3+B(J-8aA8o@*9sTo8ag2eomX!Vl1=SAWd+WbxX_xas3S+i)&x%XE)qV z`Ov>P=P*Sf4^=|GW$(B-pL{XEr?FcfqNVQ`O`gdCkR(0(IH`sWo9}^jpSZnO`8RI4 zCxPOQj-ir;;T#I$s^7hZ(({qWt`T+Of#8~td@a%c%}hhZUw-y$DSvev zq@=$+y6o~7Ay0I0K5X=seVCtzKADzKeEr7TFeUN%oVB`4;mzpR_Q6HJ^0%pv4@tgi zN)`aO6_|<51)nLFoV0ms9#-m#gu-kJc~exCwuSG&`8d`evTc81Vy8UUI^mp$M6f(t zN!g%GN^lxNQh4ZLv`w%SxoBux9IWKU-SlWSSh~wOmsxu0T->qwA`=4W2NRg)e&262 zukCfirH$(VpG{(1@@36qT59m%)@*->%8QF?6~FoWiP4}jC~P0S-$f!iu_zo ze5YAn8cWaRg|WIre1M}WDdNQ@FN9J^Rn95PA!G7v$|m@{Mr-%zmDYvifl;VKd6buN zq~}jdbUz;s9)2n*CLb)h@T}Qz-{|tfpPX}=ck$n-#b_Y9%^La;_fL(9?47g$Saf0T z}^_K$JEOzyHbwje!w@X!$)OP*eN7Xz@jPr9azeGq?7t#l~y0AG2*3us=-Ri<1Q zZ{lZpAbF>1d3y1Mr%|Gl=A1k$g}P6siIV+k44>3!0ZqSEZag(ZF+}jM6!ljQduSSF z5EmygIGt@V@(`KSMBRx}Br+{VDhPb{KF0g#yd`BH>F(YM*z?;ZcQG?ivIW|9b$L|8 zVf@a=NcK(kc}Eb4oDN7LFb)ICzGq;XdZz=cX9JA(?26@QVfshwKXrHVI__2`s*uSr z25dqHBL1qco>Q+v7XX`}aH(3b2z(=wgr`v@NY|8u?LL=HHzrg95WG2~)%;)a)N&rl zXk?wE_Cgu%%Dg%Lj@^l@1#?KpL<~RIddH zWZ$P&CH>a^g)kLRmO?osflLc%Iz{$=M0Bm-ON9-6-3_^+s!~Skt-0!eVO$mu8r9+O z72;Fvc2IecwM%&+;l>-jdo*wu1~z^7u=87AIp`^aF#Uxv2!Pb{xm$l13}NlN&%@m} zy)G|YMUai#VXY1eBA>Xyt~o1&2@v?C0f6imEeDWl2&%JEe43@mimy9j?BnOlqF^Lj zVp4u%7@9PwI(+{~0d0~}gR2?CO?g|k^L0$WE^+D2WLpfh8(MvGm3mQ)SPRj$I` zL9y&CvaRw4bhdI4FQX_tE~mU=Sy0+gTmlfn%R+10$~v6%`~|iQgZR`8jfzMYB%>@4 zMxL=N^rEdtw3YvA04#N&eUjJZdB}veBobGNwP~Z3*Ckr_K>^@Y8W`hzr%ni0uWA9t`C&D9^iCWA5DF9O-E;W z!AJ(VYd`P`$Y+*O&3sj6RDqCz5KhY(_0U#%GX*XbAt1KAfSTusgWCF9J@e97=qyhv z$|k2j$j$);3|lbUih}a80s;jtUymdL4=A%!=TuL(OK^8FQWI?n`X>y7ux z!e~1g1t5%vIDG(dZKNAC9cOeSCif}>U}Fph5ORu*0vm#@B_ji-0AvatvQ|z+R~C|! zx{}7bLcg5ATPAMuu}5xT7_2?QQ4)38hZ`|JAlhHp z4SU}|rsFZAQt#8=o_CxtWld)#DpUE#H+gvl-|m1x_}u3~O$TIsK-UE}=zPbXdSD1G z*z?=4cDl5d?|G?2UN(`#s$F+YG9P_yjs%0YPN!juULF^ZF4jrX7NPz%BE%yXZyhbC z2cV~iNDu%OiJAj}5Y#e=qIr1QH4M4LKnH+%2pldUI&DY_%B}Q=Ali;BP%D4!WzENb z`{Fc9`re-L#tTlwx4dBl{_Mk-l{H$qy1Z3s{dMCxZ)I&{V9c|O%U|~r z%nMR;e0JijnQ+DnX5pKEWg%Suk^H(qp219cGFtWio0WFb{u1xMnXyX1gL0IZ!jhGC4ulqQKl6?teveQ5u&HF{RR0IM(nq2e9)R^%-&JEF)4 z4Ong2Q=ab+dK2VF?xH0NC+FBWpmj58cI!TQP$mt?Hu>{w^ z!JBq@KjO7`$6R>wiTlzca{*oR%aV~VFVi#32A^|o5Bu#{%0~5#;MN78=>YI(pgOGn zqn6#Wbt26a-hq4e{y?OFLYpN~Zv&4eVsZec9`NT)`ZK=pL}ZUiHqK zVEc`G7)`nIWI$d*oBre%@-f%cDPGA&o^?#gdwY>dRN~5}$UAu~J!C4WQ)iZu4vTG| zWAL1mwQF8Y>)v-sFHP}#0ykRrmTftNM)nM`BPTL(q10%FvKd}@s>Y87+L8b-Ch)3@ z4OpX}IVmP>kmkZloF~hV$O(*VK*_<+nZhE;URB4=0?9j|RraE&M7BCL5XH?zytSku z=O9Uw)shqRJfTXdTM^+-e)gHCUkVSbpHS**5AK02Pr_+$o0T40CR{Jn<^_c(&YDr) ztq4w9cM!Utrvn@8l%a4BhWw?T`^O4y(5WAK>Vi-wcFLFIp;ultlDv}!$8P<8D9uTG z6l*P($`|vR@TjGw&>&|GU}Q7KzllX=ywo-t2#X#u_LS5!RGJhp;}GaZ zXazBFE6wGF0Fsj+T6*B&Cz|ayewXq>)*P;hH&gL7&n#Z`TVKXBt4g))AiV3ogYY+h ze_uHncHxU>7w-=`;WoZcyXDQ-x|$sxR)&b zR2eOoFNF$uP%+DLpa7WRQgD;9ih-d0)_5GnO}zo`U%Lf5gH&IOwxvrRO zBzJE61(Y{uL+I^vpfc?00^8B*>2evTsjKg~=k&@46<+a<9Xhoj3IYN&>^is!Hzdj* znozp*LnbzBp!RxWPpIY?VO^I2M-aNO1Vz0yQ#L7}S!_gmNsLH#U|x~2MiNsX6KD$9 zvfhNBSbEuu@=}vDb9zDvNfRi+uG=3r?*xf_F{ye_;%wx7po=a%m6l!pqC!z#vzEav zhko7>^#7xlnmI>I`A1VxX8YHON?bunFEtieP5plFR4AMK3?Cl->QkL&+oE zO?j;xhyzA3)O6lr7!nyH|IRki4n*UzRV!koP%v0UXjIX|I2v?#KPZTeOvDRu)wfpjJ3YQ!zgkcVeb|c;C^ z5t|~&)O{6?kof(0GQlc`S~h6mYiBVM0aUW;M#!O#crpa4)CRLLo;s}VBJd;umec%g zD;0R0t$j6vUQq;!I8=>%)ItQ0bfpU$Vc)uIpmWWDzU#$r-jyb(eltCFq74Y-(=~pO zSEu7I{rn3dG}j9P;q$QHVMzJD=ZAqL9;K~5&^ub2G^E~gf88g(P20b`*9Io3yPuru zr#-N6mm%YDiLH;~0t2Ac0U@q5ff2S#=*vVzWtUKvr@Kw>J*q5iv*FXwRGvdUAs4F< zqYE-RG0~{@vYu+&{EQ2Lhg~}RiU=O68wOclNW$#K>7okU7~lbYM5t38*>je0veyP0 zJ;LjVZ}vHcJo#o*mlJTZF*9(HAH|R{uo>Q>W}{-5&k24AJU~M&%O{$uIb$tk=J94Q zAQcW287!f@fMKIX0~K05!7t;{3b?iq5Da~(2S{dH`btDUZ;9O$Tn!X$OY1H65V!wbxfip&_qaf9o}0b+-b2 zmJ33j$~6Mi%<1Y*2p&GoXmp+Ga{XS zf}^44qjb!_p4ILk;=LD9DNPXy*{hptYc~QaVjxji&>>mzS5sfXlHWvwH424l?WRHKDah z5cmsG9;JK(!ALtDsPfFb0_(3yrpV$N*#Dt1=((m>_;bFnBUP}3%=IlCD_eO+KeL1u z@GIG-12u>BV`;B0M;$u*+JR16ePw?bfQ0U|<;dx`-D1YF3f^p?D6=S`ZBCN* zRLrarrBWmqWcJ@ZT<(9qqYgdUT9`P?E@ny;#3fAY2dK8fKon=&^?|9Yxfs&)Rnj z2oz}tkdt>DuniS0DgvF4t94@%rk=SN9{8gP7;?>k>8^)AKMAM&+LNia*K5yJt@660 zFa68+=`FwVl6?MDEvFrZ!Ek`l^25MwIuOGl_lMDd9U8l!3VqMzy^qV@{?Cw?GuTa8 zWzEJ~PK8z1S(VF@v<7&lIUtH#R$e!r_Yti}O;ls$DR>z%sbD8ASkLkg8cx|9m*%7Z zRM|pKj_WhA;lfq;qxxbZZ)Uw_C3IM+I3!>jFsV}}@Vr{UYBa~XEi{t6nh>qaErQcv zyhiY)Jjw(f6RxSTu(9d_(;fS2`Nzs1wU8FTnH|;Fc&tre-a!!iDDUqGXRV=a=PG?` zgRmN3^3{a4(7Y-w6Y+?@qxFL^Mhll^DhTH4i3=r5G30j!v)2+pDCWvod8)D1wovT=(}pM>DX-_!=UWGL z)WTvYFt$Sq;kpCGd1~cAj7|PRjWf;+PsrI#aa~4p%x}pu3c*-07}B~%=90@Ykb*FN zehZ2gKqs6HTngXgVTd5$BOzSqxBV_(3)LC{#kO=ot72H_l65>c*MhIpV&&@aH>?%p zRH-cCmxZaEBa?NG)D!!4*xf64nmOn#8X(FhGV#n6=*mg zH$^*A<0GIf_8}_oWz1*V=Odqo)H}$V`nTz5u_1+LM|-#4SPu>>m(SX|>YX>0MXCUj zV7288=s;#qQOM7Lds!6w%t|H|hP^~!N=#KBd$89RQ&Q}NK53sLVpJh2hPolt6YzwA z%xIPJqDNXbPIyFV1pypMHG8g)K~+IW>5}pg^uc`^NlTG>zhS6+6l>2y!fM$7Cw2E3 z9WzE?&j45+mHF6q6g7YaicjsNlPkE)Au%NIL%go62>{{uWm^&`FoA-lv?&^HTp+X@ z6dNt&j3OkDx{|F1o84A-#(T)xiVlt6A~0y$jZH)ZxPMK}2k-kbjTXPbQHJ@jhm zw4eLF$9u<8cGxq+>V@w8v0L`j%HJNt@WK$VNX=5mqQ#a!LbH03Lq@+Vu0E?Bu`IE?x*v|`A)QZV`KRhTxKryl^b!ox25I<2k@7`Rj+ip*DNuRBOMCvhYiWd+Q1lNSYHM}ZUX4(E8u|*6L5^z z(Q|O|dU*RJoce2HsmV7d%4hKlR`j$zC?|++{=$8QlBND0M(ae+_hA6l1tpxO4!yeN z`~G&HukLT$^l$W*_uWK%s3L$SvPFsj*i$PiE>_6}3VeN4AW6hhMv_{!kq9>A)PZ6G z$%|=B1`asv)s&;kHGgA+ulbWW)2h!y537^~!Nbu$Y8eGLG+j{;GVg^uLcDgsCOD)Y z5a?F`vm7FYeWg&*>4RnpK-xctPf=nGU~J+<>F1-OxCuFWNWClplPb}#3MHSbzuH%c zsQ8AQ^%oXl3wBFLXCp0n!)h$a?g;>R?y10nq5&!z8VmvV8QnvbN5US@AcNYDSdqpV zDu;H+c>0Mk)~+<5FG^Qqtun@wY|Kc_g%ZhJaFEU5-Elt%qFf>2k^7GTGV4Mu;Unc%)<_X6Bvgwu-TG#=H0mmrOpQ$ zve@E6xgDztYQaDP8uDoyOj7HVf)5$GX_0|pwg?&ChqyS*dJ4fsBK6LFh7?0r6u>1U zlv*?$BM*rPbKI_bmOG6D1fKGbq^_s145fOvQcW#~-jH#r6il95)4UP({mD2y%j<^& zxP0Ys^%?vM=A4`>T3`YT@7nh*&S%y`I8NtS0jlq!qtHGbXb${-@X%9q?SJ_euD|XZ z6a-TML@icNAVxv-04N?G1eU{5>*`Jh4S+XA8l_Jniz?^MudEn8>b8$gpaY1>t!a&G~p?~Du80b<@PG4#9 zM11*fxhBx+M7!-?1T>2e10^~v#RbC3J}5C%W30vHmfGm_DdH;tRZxf4J*Z%bJS--$ zk!9a!AOoun>|iui*eNg7X!1nnBh>h)rUIbi z=I+$7XBC#l>xT!pe2pZKYtqC6zybk#nd>FTWhke*;`-GW(baD}rxt`-#k!;20fT{S zx^lySu9VMjINGr3+bJadHclRV(()VN2?=ro9tBM%&iI^qK4wM+$z3*^=0o+L0xcBn zQlLFRh2t8R^%+5b7C=KAbuhJWXp7bXe0DH*qE+`LChaBWBAoD9D(-@p-^huUxJyBJHCPXpS1 zuPa>25;Vql>>&*r6HV%Y+Jt> zZK8J#a;PAS57~5xH&S}IVE$}a^Xsq3haAF!vu3-J4F@>rlz}0?4=Y!d$*+&wu<2W{ z@h`uXKT`)(Kn*jGmC#>uDw42vOHY9m6>0$m>g|`F&>$LKsFf=sTWp;~^j>60fi zTo7xnO3!5*!$O5_2cW5xYDQxMD_Fqju{dVYJX{?*S~56PriF2DYnk z0icxDC>9!ynIGrn*|X@Fj%o%FU@%0ral$T55p6Kq0dgZjol{97&wzS^xXSvwlp0Wb z{|J4g!Ix~r99wOQW520N@{@N`L)O3_ie4?~Z6P-9J4IFMR!+Y}nKS45%N9f%rvTcO zdu|3eMmGVlnfi@z1lGgkhJg+A&*pk=04`r=|HYd0-?i!A92b1_7zKl5r5w@9H=PG7 ze(l`+Dz$D+!$7K*o&LYQSAH*K^4MtXYkSKp+lZnLgn^~5oN73wr!EkfuOcLQVe;{k z888X&fHbtTarEZVlT~5dwa1lb87#HL=jfyjYhPSe@Ah>V+`K zmvFMJ$Dwi%fc3x}BXTdw8S9>jp}la^l$X?*61`<45m4~HhnY{P>h8@CL6=@pdXK#- zMGD*|41 z@xsiM{(JuwyZbVk|Z#%i>lu|G*VBe zOFt?Y7BP{demJ6p;E|A8Uj!%4oIwP!C1j`(^HZp~@Y+%+&AFile2E4Ndk2vuf6@{S z+7&VCsw`2X?VH1R7hJ4?SSt+-wE_!-kSACo1Zy`$rr{aS7C&S_kPc09Qq|Z^hb2A4XJ5pZs zkzc+$e*42TvF{L${p&u;-#d9gQ2m069QC{*-q+%pI1jfqtg|pqfRa!=3z>u>^^hQh zHQrySK@Ebv2C#WcI{*T`BgR34J#d8=B?;|t4x>nt$5x;rCIHy#aiu#L3{?OHY2gq! z00jV{%3%^+^v@Eja;felMNpMsU%&wmg4}o>QZI9)D%|yX2zAxbcY{ap^_kBO4FEiQ zE5qf!ogz&(3TS8dVE|kT3U5DFi}b+T;&M4vDXvX$cwFcJGz3(as0!RB5wJSO zCUP$LnWh;&&{iarH**))csdc)7#itnl`)RGV4skiyf#Q)+b5qBuPt6b9)QbN_U3?A zrw**azaU` z1u;jRw(A@i#QvZ_X>YDBw6DS$kT-}lXcs{9Vp5q(P?uK}+OGuB!WZ%|12~M*#xT?o z#N7ZvG{y655zcjR6@9=5J4PtsIq*=wW>hV$=p7!wLl^|mE+~L83|)eRaX~Ve&mBk8 zq$Wne`LBm*)x~_mKoi+8M>@mUSGQdHGkDJHX-gG)K2fldqjFb2Jkpu!5?1}y^@n$U(-&*3WPUZS}pg-p<} z(NV{qh3JcTvdM5?llL*is6>o9cP!D;B8^-zxpl4!RfWGpdEfiXz_cz$}0o z6ahNKP@ln7UV#ng zVNRzYi+s`4SN=wwL`@t%4Oc^xdQtct!HiE4GITsf7SR|e6+kIEW6yyl=-r{AAw0J7 z7XPSOM19Vp4Mh$q!|ATZ!N$jttt7cFqO#O}2QjXV2%E0QMCVFKZbz%*eG#`VIK9?) z_AuP4JGwJrb*=oNOkN;_9A5`Od<-y590ItnLRCWB*um{~_GngP&|o;GCkk8@y*Ul$ ze!{?oP~#zZ%n(9kh&%)1hGBwP7j^l%1}+LPYL3S@#Y4@Tz;6m8S2ay_9~6$g1&B*W zFpw&2pqZJ{SyH;Z}O*cja;`R3Drs{FPTnYSCoMoot-q$b4S8JPIFzz zr{CuHc{e&`?P(y#M8iJxDqNw_MhHTV4J?^VW*?;(L3f$RfKdkrO|a*{Fjs4%00U+n zT^dBq5$qsYJ2M7jW6Tnr#S&X8q0E5r+9Os&?GzfjLyoDjXvlME3g~X5vFe}4m)fZ& zv|THq<)DN|q)Q8=?`1lgw?7$1qg5mxqz+D0eFiT+HeDhC%RlNEbnGCylSF~F!r1%s7FI=;?6WPPTQHM~ut3A0gX_Fogeic1F4B5$u$$AX=+>U=`oac+pQ)%%l`|H2==d1;6_i~ zh9><3%NAcTIY#yX|gJe5ro{ZDP<80PziCn?~@0*UN4#}ehMq#EcXfO#2Dg$ z4RaY_0@W>*%fh&B@Dq)wg*vtt#0Yc2i^^ewoj@$U@vTotsnQYdN!eY4;T3T4>Vkvx z10*Z~gsu~a@t!uIeO7NI4oK)jXw;!L*4uqfjD=5<#0St{S`u}(WwZbY_8%Civm90WDYL=fb&RMYmiO7uphp{BCE=bff^pI}^q!mfF%05oQGFObmoh%8Y-M z*~%bm8QymtP7jG?IMw@T!ux~i*1=R_JdvKm&2fNdcELk`@vQfY{y4Ay4pBCR { diff --git a/spa/components/v1/Demo.vue b/spa/components/v1/Demo.vue index 615257c..414c5c7 100644 --- a/spa/components/v1/Demo.vue +++ b/spa/components/v1/Demo.vue @@ -4,7 +4,7 @@ import Buttons from "~/components/v1/Demo/Buttons.vue";