From 5c36246611af1736c52116b7a40a0e69ae1517a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Thu, 22 Oct 2026 18:44:28 +0200 Subject: [PATCH 01/61] add agents.md with contributor and agent working rules --- .gitignore | 10 +++++--- AGENTS.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + README.md | 2 +- 4 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index b585a66..be9a092 100644 --- a/.gitignore +++ b/.gitignore @@ -100,9 +100,13 @@ web_modules/ # Generated reports reports/ -# Generated reports -config/aws.json -config/azure.json +# Local configs (only the example templates are tracked) +config/* +!config/*_example.json + +# Terraform state and downloaded datasets +*.tfstate +datasets/ # TypeScript cache *.tsbuildinfo diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..57f5a3d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,68 @@ +# AGENTS.md + +## What this is + +cloudexit is an offline-first CLI. It collects cloud inventory and cost data with the user's own credentials, builds a local report, and can optionally upload the results to a platform through API v2. Cloud credentials stay on the user's machine and are never sent anywhere. + +## Setup + +- Python 3.14, the version CI runs on. +- Create and activate a virtualenv, then `pip install -r requirements-dev.txt`. +- Run the CLI with `python3 main.py`, or `pip install -e .` and use the `cloudexit` command. +- Local configs go in `config/` (start from a `*_example.json` template). They are never committed. + +## Lint and format + +CI runs exactly these, with the tool versions pinned in `requirements-dev.txt`: + +```bash +ruff check main.py core utils tests +black --check main.py core utils tests +``` + +Fix with `ruff check --fix main.py core utils tests` and `black main.py core utils tests`. Don't add a `# noqa` or an ignore without a code comment saying why. + +## Tests + +```bash +python -m unittest discover -s tests +``` + +- Add or update tests with every behaviour change. Fixtures live in `tests/`. +- Tests never call real cloud APIs or real platforms. Mock `boto3`, the Azure SDK and HTTP. + +## Branches + +- Name branches `task/`, one topic per branch. +- During 2.0 development, branch from and target `release/2.0`. `main` only gets hotfixes and Dependabot PRs (Dependabot always targets `main`). +- Before each release candidate, bring `main` into `release/2.0` with a PR from a branch `task/sync_main` (`git merge origin/main`). Merge it with a merge commit, not a squash. +- After the 2.0 release, branch from and target `main`. + +## Commits + +One line, lowercase, no trailing period, plain words about what changed, for example `load the egress resource registry from master data`. + +## Pull requests + +- A sentence-case title. +- A description with a Summary, a Changes section grouped by area, and a closing line on user-visible impact. +- State compatibility explicitly: config, exit codes, report output, the dataset and the API v2 definition copy. +- Wait for PR Checks to pass, then squash-merge (except `task/sync_main`, see Branches). + +## Never commit + +- Real values in `config.py`. +- Anything in `config/` except the `*_example.json` templates. +- `*.tfstate` files. +- `datasets/` and `reports/`. +- `.env` files, or credentials of any kind. + +## Compatibility rules + +- The CLI must keep working fully offline, without an account. +- `docs/api-v2/` is a vendored copy of the API v2 definition. Never edit it here; the version check test enforces this. +- The dataset is read only by column name. The CLI must tolerate extra tables and columns. + +## Security + +Never log API keys, cloud credentials, the `Authorization` header or full request and response payloads. Never add telemetry. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/README.md b/README.md index d0ed975..0a51326 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ Running cloudexit offline always produces a Basic assessment. Connect it to exit Contributions are welcome — bug reports, documentation improvements, new service mappings, and pull requests. -See the [contribution guidelines](https://cloudexit.escapecloud.io/contributing/how-to-contribute.html) for details. +See the [contribution guidelines](https://cloudexit.escapecloud.io/contributing/how-to-contribute.html) for details. Working rules for contributors and AI agents (setup, checks, branches, commits, pull requests) are in [AGENTS.md](./AGENTS.md). ## License From ccc0e965e2ba7b7334fbb8941490f16d4aeb2628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Thu, 22 Oct 2026 19:00:00 +0200 Subject: [PATCH 02/61] untrack config.py, add exit codes, refresh dependencies --- .github/workflows/release.yml | 47 ++++++++++++++----------- .gitignore | 3 ++ config.py | 19 ---------- core/utils_sync.py | 6 +++- main.py | 60 ++++++++++++++++--------------- pyproject.toml | 19 ++++++---- requirements-dev.txt | 4 +-- requirements.txt | 12 +++---- tests/http_fixtures.py | 52 +++++++++++++++++++++++++++ tests/test_http_fixtures.py | 66 +++++++++++++++++++++++++++++++++++ tests/test_utils_and_main.py | 30 ++++++++++++++++ tests/test_utils_sync.py | 35 +++++++++++++++++++ utils/codes.py | 17 +++++++++ utils/sync.py | 53 ---------------------------- 14 files changed, 286 insertions(+), 137 deletions(-) delete mode 100644 config.py create mode 100644 tests/http_fixtures.py create mode 100644 tests/test_http_fixtures.py delete mode 100644 utils/sync.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f714524..ae3e674 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: version: - description: "Version, no leading v (e.g. 1.1.5)" + description: "Version, no leading v (e.g. 1.1.5, or 2.0.0-rc.1 from a release branch)" required: true permissions: @@ -18,31 +18,38 @@ jobs: with: token: ${{ secrets.RELEASE_TOKEN }} - - name: Guard against non-main dispatch + - name: Validate branch, version and guard against existing tag + env: + VERSION: ${{ inputs.version }} run: | - if [ "${{ github.ref }}" != "refs/heads/main" ]; then - echo "Release must be dispatched from main, got ${{ github.ref }}"; exit 1 - fi - - - name: Validate version and guard against existing tag - run: | - v="${{ inputs.version }}" - echo "$v" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || { echo "Bad version format: $v"; exit 1; } - if git ls-remote --exit-code --tags origin "refs/tags/v$v" >/dev/null 2>&1; then - echo "Tag v$v already exists on origin"; exit 1 + case "$GITHUB_REF" in + refs/heads/main) + echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || { echo "Final versions (X.Y.Z) only from main, got: $VERSION"; exit 1; } + ;; + refs/heads/release/*) + echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$' || { echo "Only release candidates (X.Y.Z-rc.N) from a release branch, got: $VERSION"; exit 1; } + ;; + *) + echo "Release must be dispatched from main or a release/* branch, got $GITHUB_REF"; exit 1 + ;; + esac + if git ls-remote --exit-code --tags origin "refs/tags/v$VERSION" >/dev/null 2>&1; then + echo "Tag v$VERSION already exists on origin"; exit 1 fi - name: Bump utils/version.py + env: + VERSION: ${{ inputs.version }} run: | - v="${{ inputs.version }}" - sed -i "s/^__version__ = .*/__version__ = \"$v\"/" utils/version.py - grep -q "^__version__ = \"$v\"$" utils/version.py || { echo "sed did not update version"; exit 1; } + sed -i "s/^__version__ = .*/__version__ = \"$VERSION\"/" utils/version.py + grep -q "^__version__ = \"$VERSION\"$" utils/version.py || { echo "sed did not update version"; exit 1; } - - name: Commit, tag, and push to main + - name: Commit, tag, and push to the dispatched branch + env: + VERSION: ${{ inputs.version }} run: | - v="${{ inputs.version }}" git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git commit -am "Release v$v" - git tag -a "v$v" -m "Release v$v" - git push origin HEAD "refs/tags/v$v" + git commit -am "Release v$VERSION" + git tag -a "v$VERSION" -m "Release v$VERSION" + git push origin HEAD "refs/tags/v$VERSION" diff --git a/.gitignore b/.gitignore index be9a092..beed141 100644 --- a/.gitignore +++ b/.gitignore @@ -408,3 +408,6 @@ $RECYCLE.BIN/ output/ logs/ *.xlsx + +# Local platform credentials (deprecated) +/config.py diff --git a/config.py b/config.py deleted file mode 100644 index 7192d34..0000000 --- a/config.py +++ /dev/null @@ -1,19 +0,0 @@ -# config.py -""" -Configuration for integrating the local 'cloudexit' tool with the ExitCloud Platform (exitcloud.io). -This enables assessment extension and secure result storage in your selected data region. - -HOST: - EU → "eu.exitcloud.io" - US → "us.exitcloud.io" - -KEY: -To generate a key: - 1. Log in to your regional portal (https://eu.exitcloud.io or https://us.exitcloud.io). - 2. Click your user profile (top right corner). - 3. Select 'Keys' from the menu. - 4. Click 'New Key' and copy the provided key. -""" - -HOST = "" -KEY = "" diff --git a/core/utils_sync.py b/core/utils_sync.py index 177f4f6..995af97 100644 --- a/core/utils_sync.py +++ b/core/utils_sync.py @@ -5,13 +5,17 @@ import logging import os import time -import config import requests from typing import Any from core.utils_db import load_data from utils.version import __version__ +try: + import config +except ModuleNotFoundError: + config = None + # Configure logger logger = logging.getLogger("core.engine.sync") diff --git a/main.py b/main.py index 790cafd..bdea00e 100644 --- a/main.py +++ b/main.py @@ -1043,43 +1043,45 @@ def main(): # Configure logging before any side effects so verbosity applies everywhere. configure_logging(getattr(args, "verbose", 0)) - # Print ASCII art - console.print(ascii_art, style="bold cyan") + # Ctrl+C anywhere below exits with a distinct code, whether the CLI is run + # as `python3 main.py` or through the `cloudexit` console script. + try: + # Print ASCII art + console.print(ascii_art, style="bold cyan") - # Nothing to do without a subcommand — show help before any dataset download. - if not args.cloud_provider: - print_help_message() - return + # Nothing to do without a subcommand — show help before any dataset download. + if not args.cloud_provider: + print_help_message() + return - # Ensure latest dataset is available before proceeding - initialize_dataset() + # Ensure latest dataset is available before proceeding + initialize_dataset() - # Dispatch based on provided arguments - try: - if args.cloud_provider == "aws": - handle_aws(args) - elif args.cloud_provider == "azure": - handle_azure(args) - else: - console.print( - "[red]Invalid command. Use 'aws' or 'azure' as the first argument.[/red]" - ) - console.print( - "[green]Run 'python3 main.py --help' for usage instructions.[/green]" - ) - except ConfigError: - sys.exit(codes.CONFIG) + # Dispatch based on provided arguments + try: + if args.cloud_provider == "aws": + handle_aws(args) + elif args.cloud_provider == "azure": + handle_azure(args) + else: + console.print( + "[red]Invalid command. Use 'aws' or 'azure' as the first argument.[/red]" + ) + console.print( + "[green]Run 'python3 main.py --help' for usage instructions.[/green]" + ) + except ConfigError: + sys.exit(codes.CONFIG) + except KeyboardInterrupt: + console.print( + "\n[bold yellow]Operation cancelled by user (Ctrl+C). Exiting gracefully.[/bold yellow]" + ) + sys.exit(codes.INTERRUPTED) if __name__ == "__main__": try: main() - except KeyboardInterrupt: - console.print( - "\n[bold yellow]Operation cancelled by user (Ctrl+C). Exiting gracefully.[/bold yellow]" - ) - # logger.warning("Process interrupted by user via KeyboardInterrupt.") - sys.exit(0) except Exception as e: # logger.error(f"An unexpected error occurred: {e}", exc_info=True) console.print(f"[red]Unexpected error: {e}[/red]") diff --git a/pyproject.toml b/pyproject.toml index aa1d5ad..41f23b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,17 +9,17 @@ description = "Open-source cloud exit assessment CLI for evaluating cloud lock-i readme = "README.md" requires-python = ">=3.12" dependencies = [ - "azure-identity==1.25.3", + "azure-identity==1.26.0", "azure-mgmt-resource==24.0.0", "azure-mgmt-authorization==4.0.0", "azure-mgmt-costmanagement==4.0.1", - "boto3==1.43.5", - "botocore==1.43.5", + "boto3==1.43.117", + "botocore==1.43.117", "rich==15.0.0", "Jinja2==3.1.6", - "reportlab==4.5.0", - "pillow==12.3.0", - "requests==2.33.1", + "reportlab==5.1.0", + "pillow==13.0.0", + "requests==2.34.2", ] [project.optional-dependencies] @@ -31,11 +31,16 @@ dev = [ [project.scripts] cloudexit = "main:main" +[tool.ruff.lint] +# ruff 0.16 enlarged its default rule set; keep the previous defaults until +# the new rules are adopted deliberately +select = ["E4", "E7", "E9", "F"] + [tool.setuptools.dynamic] version = {attr = "utils.version.__version__"} [tool.setuptools] -py-modules = ["main", "config"] +py-modules = ["main"] [tool.setuptools.packages.find] where = ["."] diff --git a/requirements-dev.txt b/requirements-dev.txt index 8ff1d00..8d824b2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,3 @@ -r requirements.txt -black==26.5.1 -ruff==0.15.21 +black==26.10.1 +ruff==0.17.1 diff --git a/requirements.txt b/requirements.txt index eb67a0f..1f484de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ -azure-identity==1.25.3 +azure-identity==1.26.0 azure-mgmt-resource==24.0.0 azure-mgmt-authorization==4.0.0 azure-mgmt-costmanagement==4.0.1 -boto3==1.43.5 -botocore==1.43.5 +boto3==1.43.117 +botocore==1.43.117 rich==15.0.0 Jinja2==3.1.6 -reportlab==4.5.0 -pillow==12.3.0 -requests==2.33.1 +reportlab==5.1.0 +pillow==13.0.0 +requests==2.34.2 diff --git a/tests/http_fixtures.py b/tests/http_fixtures.py new file mode 100644 index 0000000..7097dd9 --- /dev/null +++ b/tests/http_fixtures.py @@ -0,0 +1,52 @@ +import json +from collections import namedtuple +from contextlib import contextmanager +from unittest import mock + +import requests + +RecordedRequest = namedtuple("RecordedRequest", "method url kwargs") + + +def make_response(status=200, json_body=None, text="", headers=None): + response = requests.Response() + response.status_code = status + response.encoding = "utf-8" + response.headers.update(headers or {}) + if json_body is not None: + response._content = json.dumps(json_body).encode("utf-8") + response.headers.setdefault("Content-Type", "application/json") + else: + response._content = text.encode("utf-8") + return response + + +class FakeHTTP: + def __init__(self, replies): + self.replies = list(replies) + self.requests = [] + + def __call__(self, session, method, url, **kwargs): + self.requests.append(RecordedRequest(method.upper(), url, kwargs)) + if not self.replies: + raise AssertionError(f"unexpected {method.upper()} request to {url}") + reply = self.replies.pop(0) + if isinstance(reply, BaseException): + raise reply + return reply + + +@contextmanager +def fake_http(*replies): + # Every requests.get/post/Session call goes through Session.request, so + # patching it there covers all of them. Each reply is returned in order; + # an exception instance in the list is raised instead (e.g. + # requests.ConnectionError). A request beyond the queued replies fails the + # test, and fake.requests records what was sent. + fake = FakeHTTP(replies) + with mock.patch.object( + requests.Session, "request", autospec=True, side_effect=fake + ): + yield fake + if fake.replies: + raise AssertionError(f"{len(fake.replies)} queued replies were never used") diff --git a/tests/test_http_fixtures.py b/tests/test_http_fixtures.py new file mode 100644 index 0000000..8dd775d --- /dev/null +++ b/tests/test_http_fixtures.py @@ -0,0 +1,66 @@ +import unittest + +import requests + +from tests.http_fixtures import fake_http, make_response +from utils.connection import get_jwt_token + + +class FakeHTTPTests(unittest.TestCase): + def test_replies_are_returned_in_order_and_requests_recorded(self): + with fake_http( + make_response(json_body={"n": 1}), + make_response(status=503, text="down"), + ) as fake: + first = requests.get("https://api.test/one", timeout=5) + second = requests.post("https://api.test/two", json={"a": 1}) + + self.assertEqual(first.json(), {"n": 1}) + self.assertEqual(second.status_code, 503) + self.assertEqual( + [(r.method, r.url) for r in fake.requests], + [("GET", "https://api.test/one"), ("POST", "https://api.test/two")], + ) + self.assertEqual(fake.requests[1].kwargs["json"], {"a": 1}) + + def test_exception_replies_are_raised(self): + with fake_http(requests.ConnectionError("refused")): + with self.assertRaises(requests.ConnectionError): + requests.get("https://api.test/") + + def test_request_beyond_the_queued_replies_fails(self): + with fake_http(): + with self.assertRaises(AssertionError): + requests.get("https://api.test/") + + def test_unused_replies_fail_the_test(self): + with self.assertRaises(AssertionError): + with fake_http(make_response()): + pass + + def test_raise_for_status_follows_the_status_code(self): + with fake_http(make_response(status=500)): + response = requests.get("https://api.test/") + with self.assertRaises(requests.HTTPError): + response.raise_for_status() + + +class FakeHTTPExampleTests(unittest.TestCase): + def test_get_jwt_token_reads_the_token_from_the_response(self): + with fake_http(make_response(json_body={"access_token": "jwt-1"})) as fake: + token = get_jwt_token(host="api.test", key="test-key") + + self.assertEqual(token, "jwt-1") + (request,) = fake.requests + self.assertEqual(request.url, "https://api.test/api/v1/auth/token/") + self.assertEqual( + request.kwargs["headers"], {"Authorization": "Bearer test-key"} + ) + + def test_get_jwt_token_returns_none_when_the_host_is_unreachable(self): + with fake_http(requests.ConnectionError("refused")): + self.assertIsNone(get_jwt_token(host="api.test", key="test-key")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_utils_and_main.py b/tests/test_utils_and_main.py index 27c5d92..3c51c7a 100644 --- a/tests/test_utils_and_main.py +++ b/tests/test_utils_and_main.py @@ -856,6 +856,36 @@ def test_config_error_from_handler_exits_config(self): main.main() self.assertEqual(ctx.exception.code, codes.CONFIG) + def test_ctrl_c_in_handler_exits_interrupted(self): + with ( + patch("main.initialize_dataset"), + patch("main.console.print"), + patch( + "main.parse_arguments", + return_value=Namespace(cloud_provider="aws"), + ), + patch("main.handle_aws", side_effect=KeyboardInterrupt), + ): + with self.assertRaises(SystemExit) as ctx: + main.main() + self.assertEqual(ctx.exception.code, codes.INTERRUPTED) + self.assertEqual(codes.INTERRUPTED, 130) + + def test_ctrl_c_during_dataset_download_exits_interrupted(self): + with ( + patch("main.initialize_dataset", side_effect=KeyboardInterrupt), + patch("main.console.print"), + patch( + "main.parse_arguments", + return_value=Namespace(cloud_provider="aws"), + ), + patch("main.handle_aws") as mock_handle, + ): + with self.assertRaises(SystemExit) as ctx: + main.main() + self.assertEqual(ctx.exception.code, codes.INTERRUPTED) + mock_handle.assert_not_called() + def test_bad_config_file_exits_config_end_to_end(self): args = Namespace( cloud_provider="aws", diff --git a/tests/test_utils_sync.py b/tests/test_utils_sync.py index 6c0694c..bc1418c 100644 --- a/tests/test_utils_sync.py +++ b/tests/test_utils_sync.py @@ -1,8 +1,11 @@ +import importlib.util import json import os +import sys import tempfile import types import unittest +from pathlib import Path from unittest.mock import MagicMock, patch from core.utils_sync import post_assessment @@ -67,6 +70,38 @@ def test_returns_clear_error_when_host_missing_everywhere(self): self.assertEqual(result["logs"], "HOST missing in environment and config.py") +class MissingConfigModuleTests(unittest.TestCase): + def load_without_config(self): + path = Path(__file__).resolve().parent.parent / "core" / "utils_sync.py" + spec = importlib.util.spec_from_file_location("utils_sync_no_config", path) + module = importlib.util.module_from_spec(spec) + # None in sys.modules makes `import config` fail as if the file were absent + with patch.dict(sys.modules, {"config": None}): + spec.loader.exec_module(module) + return module + + def test_module_imports_without_config(self): + self.assertIsNone(self.load_without_config().config) + + def test_post_assessment_reports_missing_host_without_config(self): + module = self.load_without_config() + with patch.dict(os.environ, {}, clear=True): + result = module.post_assessment( + name="Demo", + started_at=123, + report_path="/tmp/report", + meta={ + "exit_strategy": 1, + "cloud_service_provider": 2, + "assessment_type": 1, + }, + token="jwt-token", + ) + + self.assertFalse(result["success"]) + self.assertEqual(result["logs"], "HOST missing in environment and config.py") + + class WriteAssessmentPayloadTests(unittest.TestCase): def test_writes_payload_json_to_raw_data(self): with tempfile.TemporaryDirectory() as tmp_dir: diff --git a/utils/codes.py b/utils/codes.py index c6572f0..56c5432 100644 --- a/utils/codes.py +++ b/utils/codes.py @@ -10,3 +10,20 @@ RISK_ASSESSMENT = 7 REPORT = 8 EGRESS = 9 + +# Upload to a platform (API v2) +# 10 the platform rejected the submitted data +# 11 the API key is missing, invalid or not allowed +# 12 the platform is unreachable or temporarily unavailable +# 13 a plan quota or rate limit was reached +# 14 the upload target was not found or is not accessible +# 15 the platform does not support this API version or feature +UPLOAD_REJECTED = 10 +UPLOAD_AUTH = 11 +UPLOAD_UNAVAILABLE = 12 +UPLOAD_QUOTA = 13 +UPLOAD_TARGET = 14 +UPLOAD_UNSUPPORTED = 15 + +# Interrupted by the user (Ctrl+C) +INTERRUPTED = 130 diff --git a/utils/sync.py b/utils/sync.py deleted file mode 100644 index 609c058..0000000 --- a/utils/sync.py +++ /dev/null @@ -1,53 +0,0 @@ -# utils/sync.py -from __future__ import annotations - -import logging -import requests -import config -from typing import Any -from utils.auth import get_jwt_token - -logger = logging.getLogger("main.utils.sync") - -_ASSESS_PATH = "/api/v1/assessments/" - - -def _build_url(host: str) -> str: - host = host.strip().rstrip("/") - if host.startswith("http://"): - host = "https://" + host[len("http://") :] - elif not host.startswith("https://"): - host = f"https://{host}" - return f"{host}{_ASSESS_PATH}" - - -def submit_assessment( - payload: dict[str, Any], - *, - host: str | None = None, - key: str | None = None, - timeout: int = 10, -) -> requests.Response | None: - host = host or getattr(config, "HOST", "") if config else "" - if not host: - logger.debug("HOST not configured – skipping assessment sync.") - return None - - token = get_jwt_token(host=host, key=key) if key else get_jwt_token(host=host) - if not token: - logger.debug("Could not obtain JWT – skipping assessment sync.") - return None - - url = _build_url(host) - headers = { - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - } - - try: - resp = requests.post(url, headers=headers, json=payload, timeout=timeout) - logger.info("POST %s – status %s", url, resp.status_code) - return resp - except requests.RequestException as exc: - logger.error("Assessment POST failed: %s", exc, exc_info=True) - return None From a6ff00be8cc6a90693908026d52c72169d8ec3b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Thu, 22 Oct 2026 19:51:00 +0200 Subject: [PATCH 03/61] add config file, key files and config show command --- README.md | 16 + config.example.toml | 41 +++ core/engine.py | 6 + core/utils_sync.py | 24 +- main.py | 98 +++++- tests/test_engine.py | 3 + tests/test_settings.py | 587 +++++++++++++++++++++++++++++++++++ tests/test_utils_and_main.py | 84 +++-- tests/test_utils_sync.py | 120 +++---- utils/connection.py | 60 ++-- utils/settings.py | 446 ++++++++++++++++++++++++++ utils/utils.py | 2 + 12 files changed, 1325 insertions(+), 162 deletions(-) create mode 100644 config.example.toml create mode 100644 tests/test_settings.py create mode 100644 utils/settings.py diff --git a/README.md b/README.md index 0a51326..21d1d05 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,22 @@ See the [configuration reference](https://cloudexit.escapecloud.io/config/config Want to see how a regulatory-aligned report looks (DORA / FINMA / UK PRA)? Run with `--dry-run` and send the output `payload.json` to request_report@escapecloud.io — we'll generate a sample you can share with your risk or compliance team. +## Platform settings + +Connecting to a platform is optional. Without any settings cloudexit runs fully offline. Each value is resolved separately, highest precedence first: + +1. **Flags**: `--host`, `--api-key-file`, `--config-profile`, `--assessment-id`, `--ca-bundle`, `--timeout` +2. **Environment**: `CLOUDEXIT_HOST`, `CLOUDEXIT_API_KEY_FILE`, `CLOUDEXIT_API_KEY`, `CLOUDEXIT_PROFILE`, `CLOUDEXIT_ASSESSMENT_ID`, `CLOUDEXIT_UPLOAD`, `CLOUDEXIT_CA_BUNDLE`, `CLOUDEXIT_TIMEOUT` +3. **Config file**: `~/.config/cloudexit/config.toml` (`$XDG_CONFIG_HOME/cloudexit/config.toml`, or `%APPDATA%\cloudexit\config.toml` on Windows), with a `[default]` table and optional `[profiles.]` tables. See [`config.example.toml`](./config.example.toml). +4. **Deprecated**: the `HOST` and `KEY` environment variables (only when both are set) and `config.py`. Both still work and print a warning. + +Prefer a key file (`--api-key-file`, `CLOUDEXIT_API_KEY_FILE` or `api_key_file`) over a key in plain text, and keep it readable by you only. The CLI warns when the config file or a key file is readable by group or others. `http://` hosts are rejected, except `localhost` and `127.0.0.1`. + +```bash +cloudexit config path # where the config file is looked up +cloudexit config show # every resolved value and its source (keys are masked) +``` + ## Data Landscape & Egress Estimation (alpha) Add `--egress` to any assessment to measure how much data lives in the assessed scope and estimate the one-time internet egress fee for moving it out — based on the provider's tiered list prices, with no additional permissions required. diff --git a/config.example.toml b/config.example.toml new file mode 100644 index 0000000..03213f7 --- /dev/null +++ b/config.example.toml @@ -0,0 +1,41 @@ +# Copy this file to the location printed by `cloudexit config path` +# (default: ~/.config/cloudexit/config.toml, or $XDG_CONFIG_HOME/cloudexit/config.toml; +# %APPDATA%\cloudexit\config.toml on Windows) and restrict it: +# +# chmod 600 ~/.config/cloudexit/config.toml +# +# Every setting is optional. Without any configuration cloudexit runs fully +# offline. Precedence, highest first: +# 1. command-line flags (--host, --api-key-file, --config-profile, ...) +# 2. CLOUDEXIT_* environment variables +# 3. this file ([profiles.] first, then [default]) +# 4. deprecated: the HOST and KEY environment variables (only when both are +# set) and config.py +# Run `cloudexit config show` to see each value and where it came from. + +[default] +# Platform host. https:// is assumed when no scheme is given; http:// is only +# accepted for localhost and 127.0.0.1. +# host = "platform.example.com" + +# Prefer a key file over a key in this file. The path may be relative to this +# file, and `~` is expanded. Keep the key file readable by you only. +# api_key_file = "~/.config/cloudexit/api-key" + +# Only if you cannot use a key file. If both are set, api_key_file wins. +# api_key = "..." + +# Use an existing platform assessment. +# assessment_id = "..." + +# CA bundle for platforms that use a private certificate authority. +# ca_bundle = "/etc/ssl/certs/company-ca.pem" + +# Whether to upload results to the platform. +# upload = true + +# Select a profile with --config-profile NAME or CLOUDEXIT_PROFILE=NAME. +# Values in a profile override [default]. +# [profiles.staging] +# host = "staging.platform.example.com" +# api_key_file = "~/.config/cloudexit/staging-key" diff --git a/core/engine.py b/core/engine.py index 3910a65..4993aed 100644 --- a/core/engine.py +++ b/core/engine.py @@ -22,6 +22,7 @@ generate_json_report, ) from .utils_sync import post_assessment +from utils.settings import Settings, load_settings # Configure the logger logger = logging.getLogger("core.engine") @@ -316,6 +317,7 @@ def sync_assessment( metadata: dict[str, Any], mode: str, token: str | None, + settings: Settings | None = None, ) -> dict[str, Any]: if mode != "online" or not token: return { @@ -325,12 +327,16 @@ def sync_assessment( "logs": "offline – sync skipped.", } + settings = settings or load_settings() result = post_assessment( name=name, started_at=started_at, report_path=report_path, meta=metadata, token=token, + host=settings.host.value, + timeout=settings.timeout.value, + verify=settings.ca_bundle.value or True, ) if not result.get("success"): diff --git a/core/utils_sync.py b/core/utils_sync.py index 995af97..a40c108 100644 --- a/core/utils_sync.py +++ b/core/utils_sync.py @@ -9,13 +9,9 @@ from typing import Any from core.utils_db import load_data +from utils.settings import normalize_host from utils.version import __version__ -try: - import config -except ModuleNotFoundError: - config = None - # Configure logger logger = logging.getLogger("core.engine.sync") @@ -23,12 +19,7 @@ def _assess_url(host: str) -> str: - host = host.strip().rstrip("/") - if host.startswith("http://"): - host = "https://" + host[len("http://") :] - elif not host.startswith("https://"): - host = f"https://{host}" - return f"{host}{_ASSESS_PATH}" + return f"{normalize_host(host)}{_ASSESS_PATH}" def _build_payload( @@ -122,14 +113,15 @@ def post_assessment( report_path: str, meta: dict[str, int], token: str, - timeout: int = 10, + host: str | None, + timeout: float = 10, + verify: str | bool = True, ) -> dict[str, Any]: - host = os.environ.get("HOST", "").strip() or getattr(config, "HOST", "").strip() if not host: return { "success": False, "payload": None, - "logs": "HOST missing in environment and config.py", + "logs": "no upload host configured (see `cloudexit config show`)", } url = _assess_url(host) @@ -145,7 +137,9 @@ def post_assessment( ) try: - resp = requests.post(url, headers=headers, json=payload, timeout=timeout) + resp = requests.post( + url, headers=headers, json=payload, timeout=timeout, verify=verify + ) ok = resp.ok return { "success": ok, diff --git a/main.py b/main.py index bdea00e..1b3db08 100644 --- a/main.py +++ b/main.py @@ -9,6 +9,7 @@ import traceback from rich.console import Console from rich.logging import RichHandler +from rich.markup import escape from datetime import datetime from botocore.exceptions import NoCredentialsError, ProfileNotFound from azure.identity import DefaultAzureCredential, ClientSecretCredential @@ -51,6 +52,13 @@ require_env, require_env_int, ) +from utils.settings import ( + SettingsError, + config_file_path, + describe, + flags_from_args, + load_settings, +) from utils.validate import validate_region, validate_config from utils import codes from utils.version import __version__ @@ -237,6 +245,7 @@ def handle_aws(args): dry_run=args.dry_run, non_interactive=args.non_interactive, egress=args.egress, + settings=getattr(args, "settings", None), ) return @@ -272,6 +281,7 @@ def handle_aws(args): dry_run=args.dry_run, non_interactive=args.non_interactive, egress=args.egress, + settings=getattr(args, "settings", None), ) @@ -450,6 +460,7 @@ def handle_azure(args): dry_run=args.dry_run, non_interactive=args.non_interactive, egress=args.egress, + settings=getattr(args, "settings", None), ) return @@ -483,11 +494,18 @@ def handle_azure(args): dry_run=args.dry_run, non_interactive=args.non_interactive, egress=args.egress, + settings=getattr(args, "settings", None), ) def run_assessment( - config, provider_name, *, dry_run=False, non_interactive=False, egress=False + config, + provider_name, + *, + dry_run=False, + non_interactive=False, + egress=False, + settings=None, ): # Record the assessment start time to propagate across stages started_at = int(time.time()) @@ -519,7 +537,7 @@ def run_assessment( sys.exit(codes.CONFIG) # Detect ExitCloud Integration - mode, jwt = resolve_mode() + mode, jwt = resolve_mode(settings) if dry_run: print_step("Dry run mode – no remote sync.", status="ok") elif mode == "online": @@ -746,6 +764,7 @@ def run_assessment( }, mode=mode, token=jwt, + settings=settings, ) status = "ok" if sync_result["success"] else "error" @@ -916,6 +935,8 @@ def parse_arguments(): " python3 main.py azure --config config.json --egress\n" " python3 main.py aws --tfstate infra.tfstate # Assess a Terraform/OpenTofu state file\n" " python3 main.py azure --tfstate infra.tfstate --dry-run\n" + " python3 main.py config path # Print the config file location\n" + " python3 main.py config show # Show resolved settings and their sources\n" ), formatter_class=argparse.RawDescriptionHelpFormatter, ) @@ -928,7 +949,8 @@ def parse_arguments(): ) subparsers = parser.add_subparsers( - dest="cloud_provider", help="Specify the cloud provider (aws or azure)." + dest="cloud_provider", + help="Specify the cloud provider (aws or azure), or `config`.", ) # Shared options available on every subcommand (e.g. `aws --verbose`) @@ -941,9 +963,34 @@ def parse_arguments(): help="Increase log verbosity (-v for INFO, -vv for DEBUG + third-party).", ) + # Platform options: flags > CLOUDEXIT_* environment > config file > legacy + platform = argparse.ArgumentParser(add_help=False) + platform.add_argument( + "--host", type=str, help="Platform host (https://, or http:// for localhost)." + ) + platform.add_argument( + "--api-key-file", + type=str, + help="Path to a file containing the platform API key.", + ) + platform.add_argument( + "--config-profile", + type=str, + help="Profile to use from the cloudexit config file.", + ) + platform.add_argument( + "--assessment-id", type=str, help="Existing platform assessment to use." + ) + platform.add_argument( + "--ca-bundle", type=str, help="Path to a CA bundle for TLS verification." + ) + platform.add_argument( + "--timeout", type=float, help="Platform request timeout in seconds." + ) + # Subparser for AWS aws_parser = subparsers.add_parser( - "aws", parents=[common], help="Perform an AWS assessment." + "aws", parents=[common, platform], help="Perform an AWS assessment." ) aws_group = aws_parser.add_mutually_exclusive_group(required=False) aws_group.add_argument( @@ -988,7 +1035,7 @@ def parse_arguments(): # Subparser for Azure azure_parser = subparsers.add_parser( - "azure", parents=[common], help="Perform an Azure assessment." + "azure", parents=[common, platform], help="Perform an Azure assessment." ) azure_group = azure_parser.add_mutually_exclusive_group(required=False) azure_group.add_argument( @@ -1032,9 +1079,37 @@ def parse_arguments(): ), ) + # Subparser for the config helpers + config_parser = subparsers.add_parser( + "config", help="Inspect where settings come from." + ) + config_commands = config_parser.add_subparsers(dest="config_command", required=True) + config_commands.add_parser( + "path", parents=[common], help="Print the config file location." + ) + config_commands.add_parser( + "show", + parents=[common, platform], + help="Show every resolved setting and where it came from.", + ) + return parser.parse_args() +def handle_config(args): + if args.config_command == "path": + print(config_file_path()) + return + + try: + settings = load_settings(flags_from_args(args)) + except SettingsError as e: + console.print(f"[red]{escape(str(e))}[/red]") + sys.exit(codes.CONFIG) + for line in describe(settings): + print(line) + + def main(): # Parse arguments first so --version/--help exit before any side effects # (ASCII art, dataset download). @@ -1046,6 +1121,11 @@ def main(): # Ctrl+C anywhere below exits with a distinct code, whether the CLI is run # as `python3 main.py` or through the `cloudexit` console script. try: + # The config helpers print plain text and need no dataset. + if args.cloud_provider == "config": + handle_config(args) + return + # Print ASCII art console.print(ascii_art, style="bold cyan") @@ -1054,6 +1134,14 @@ def main(): print_help_message() return + # Resolve platform settings up front so a bad value fails before any work + if args.cloud_provider in ("aws", "azure"): + try: + args.settings = load_settings(flags_from_args(args)) + except SettingsError as e: + console.print(f"[red]{escape(str(e))}[/red]") + sys.exit(codes.CONFIG) + # Ensure latest dataset is available before proceeding initialize_dataset() diff --git a/tests/test_engine.py b/tests/test_engine.py index c055445..b142439 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch from core.engine import _load_tfstate_scope, sync_assessment, test_permissions +from utils.settings import Settings class TestPermissionsAwsHybridMode(unittest.TestCase): @@ -135,6 +136,7 @@ def test_server_failure_returns_dict_not_raises(self): metadata={}, mode="online", token="tok", + settings=Settings(), ) self.assertFalse(result["success"]) self.assertIn("401", result["logs"]) @@ -157,6 +159,7 @@ def test_local_db_failure_returns_dict_not_raises(self): metadata={}, mode="online", token="tok", + settings=Settings(), ) self.assertFalse(result["success"]) self.assertIn("store server risks", result["logs"]) diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 0000000..f820a7c --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,587 @@ +import contextlib +import io +import os +import tempfile +import types +import unittest +from argparse import Namespace +from pathlib import Path +from unittest.mock import patch + +import main +from utils import codes +from utils.settings import ( + DEFAULT_TIMEOUT, + Settings, + SettingsError, + config_file_path, + describe, + load_settings, + mask_key, + normalize_host, +) + +LOGGER = "main.utils.settings" +KEY = "sk_live_0123456789abcdef" + + +class SettingsTestCase(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.tmp = Path(tmp.name) + patcher = patch("utils.settings.legacy_config", None) + patcher.start() + self.addCleanup(patcher.stop) + + @property + def config_path(self): + return self.tmp / "cloudexit" / "config.toml" + + def write_config(self, text, mode=0o600): + self.config_path.parent.mkdir(parents=True, exist_ok=True) + self.config_path.write_text(text, encoding="utf-8") + self.config_path.chmod(mode) + return self.config_path + + def write_key(self, name="api-key", key=KEY, mode=0o600): + path = self.tmp / name + path.write_text(f"{key}\n", encoding="utf-8") + path.chmod(mode) + return str(path) + + def load(self, flags=None, env=None): + environ = {"XDG_CONFIG_HOME": str(self.tmp), **(env or {})} + return load_settings(flags, environ=environ) + + def legacy(self, **values): + return patch("utils.settings.legacy_config", types.SimpleNamespace(**values)) + + +class ConfigFilePathTests(unittest.TestCase): + def test_uses_xdg_config_home_when_absolute(self): + path = config_file_path({"XDG_CONFIG_HOME": "/xdg"}, platform="linux") + self.assertEqual(path, Path("/xdg/cloudexit/config.toml")) + + def test_defaults_to_dot_config_in_the_home_directory(self): + expected = Path.home() / ".config" / "cloudexit" / "config.toml" + self.assertEqual(config_file_path({}, platform="linux"), expected) + + def test_ignores_a_relative_xdg_config_home(self): + expected = Path.home() / ".config" / "cloudexit" / "config.toml" + path = config_file_path({"XDG_CONFIG_HOME": "relative"}, platform="linux") + self.assertEqual(path, expected) + + def test_uses_appdata_on_windows(self): + path = config_file_path({"APPDATA": "/appdata"}, platform="win32") + self.assertEqual(path, Path("/appdata/cloudexit/config.toml")) + + +class NormalizeHostTests(unittest.TestCase): + def test_adds_https_when_there_is_no_scheme(self): + self.assertEqual(normalize_host("example.com"), "https://example.com") + + def test_keeps_https_and_strips_the_trailing_slash(self): + self.assertEqual( + normalize_host(" https://example.com/ "), "https://example.com" + ) + + def test_rejects_plain_http_for_remote_hosts(self): + with self.assertRaises(SettingsError) as ctx: + normalize_host("http://example.com") + self.assertIn("https://", str(ctx.exception)) + + def test_accepts_plain_http_for_localhost(self): + self.assertEqual( + normalize_host("http://localhost:8000"), "http://localhost:8000" + ) + self.assertEqual(normalize_host("http://127.0.0.1"), "http://127.0.0.1") + + def test_rejects_other_schemes_and_empty_values(self): + for value in ("ftp://example.com", "", " ", "https://"): + with self.assertRaises(SettingsError, msg=value): + normalize_host(value) + + +class PrecedenceTests(SettingsTestCase): + def test_nothing_configured_is_all_unset(self): + settings = self.load() + + self.assertIsNone(settings.host.value) + self.assertIsNone(settings.api_key.value) + self.assertEqual(settings.timeout.value, DEFAULT_TIMEOUT) + self.assertEqual(settings.timeout.source, "default") + self.assertFalse(settings.config_found) + + def test_host_flag_over_env_over_file_over_legacy(self): + self.write_config('[default]\nhost = "file.test"\n') + env = { + "CLOUDEXIT_HOST": "env.test", + "HOST": "legacy-env.test", + "KEY": "legacy-key", + } + with self.legacy(HOST="legacy-config.test", KEY="legacy-key"): + steps = [ + ( + self.load({"host": "flag.test"}, env), + "https://flag.test", + "flag --host", + ), + (self.load(None, env), "https://env.test", "env CLOUDEXIT_HOST"), + ( + self.load(None, {"HOST": "legacy-env.test", "KEY": "k"}), + "https://file.test", + "config file [default]", + ), + ] + self.config_path.unlink() + steps += [ + ( + self.load(None, {"HOST": "legacy-env.test", "KEY": "k"}), + "https://legacy-env.test", + "legacy env HOST", + ), + (self.load(), "https://legacy-config.test", "legacy config.py"), + ] + + for settings, value, source in steps: + self.assertEqual(settings.host.value, value) + self.assertEqual(settings.host.source, source) + + def test_each_value_is_resolved_on_its_own(self): + ca_bundle = self.tmp / "ca.pem" + ca_bundle.write_text("pem") + self.write_config( + f'[default]\nassessment_id = "from-file"\nca_bundle = "{ca_bundle}"\n' + ) + + settings = self.load( + {"host": "flag.test"}, + {"CLOUDEXIT_ASSESSMENT_ID": "from-env", "CLOUDEXIT_TIMEOUT": "30"}, + ) + + self.assertEqual(settings.host.source, "flag --host") + self.assertEqual(settings.assessment_id.value, "from-env") + self.assertEqual(settings.assessment_id.source, "env CLOUDEXIT_ASSESSMENT_ID") + self.assertEqual(settings.ca_bundle.value, str(ca_bundle)) + self.assertEqual(settings.ca_bundle.source, "config file [default]") + self.assertEqual(settings.timeout.value, 30.0) + self.assertEqual(settings.timeout.source, "env CLOUDEXIT_TIMEOUT") + + def test_timeout_flag_overrides_env(self): + settings = self.load({"timeout": 5.0}, {"CLOUDEXIT_TIMEOUT": "30"}) + self.assertEqual(settings.timeout.value, 5.0) + self.assertEqual(settings.timeout.source, "flag --timeout") + + def test_invalid_timeouts_are_rejected(self): + for value in ("soon", "0", "-3"): + with self.assertRaises(SettingsError, msg=value): + self.load(None, {"CLOUDEXIT_TIMEOUT": value}) + with self.assertRaises(SettingsError): + self.load({"timeout": 0.0}) + + def test_upload_is_a_boolean_from_env_or_file(self): + self.assertIs(self.load(None, {"CLOUDEXIT_UPLOAD": "Yes"}).upload.value, True) + self.assertIs(self.load(None, {"CLOUDEXIT_UPLOAD": "off"}).upload.value, False) + self.write_config("[default]\nupload = true\n") + settings = self.load() + self.assertIs(settings.upload.value, True) + self.assertEqual(settings.upload.source, "config file [default]") + with self.assertRaises(SettingsError): + self.load(None, {"CLOUDEXIT_UPLOAD": "maybe"}) + + def test_http_host_is_rejected_with_its_source(self): + with self.assertRaises(SettingsError) as ctx: + self.load(None, {"CLOUDEXIT_HOST": "http://example.com"}) + self.assertIn("env CLOUDEXIT_HOST", str(ctx.exception)) + + def test_local_http_host_is_accepted(self): + settings = self.load({"host": "http://localhost:8000"}) + self.assertEqual(settings.host.value, "http://localhost:8000") + + def test_a_host_without_scheme_gets_https(self): + self.assertEqual( + self.load({"host": "example.com"}).host.value, "https://example.com" + ) + + def test_a_lower_precedence_bad_host_is_not_validated(self): + self.write_config('[default]\nhost = "http://example.com"\n') + self.assertEqual( + self.load({"host": "flag.test"}).host.value, "https://flag.test" + ) + + def test_spool_dir_and_catalogue_url_come_from_the_environment(self): + settings = self.load( + None, + { + "CLOUDEXIT_SPOOL_DIR": str(self.tmp / "spool"), + "CLOUDEXIT_CATALOGUE_URL": "https://data.example.test/db.gz", + }, + ) + self.assertEqual(settings.spool_dir.value, str(self.tmp / "spool")) + self.assertEqual(settings.catalogue_url.source, "env CLOUDEXIT_CATALOGUE_URL") + + def test_malformed_config_file_is_a_clear_error(self): + self.write_config("[default\n") + with self.assertRaises(SettingsError) as ctx: + self.load() + self.assertIn(str(self.config_path), str(ctx.exception)) + + def test_wrong_value_types_in_the_file_are_rejected(self): + self.write_config("[default]\nhost = 5\n") + with self.assertRaises(SettingsError): + self.load() + + def test_unknown_keys_in_the_file_warn(self): + self.write_config('[default]\nhots = "typo.test"\n') + with self.assertLogs(LOGGER, level="WARNING") as logs: + self.load() + self.assertIn("hots", logs.output[0]) + + def test_missing_ca_bundle_is_rejected(self): + with self.assertRaises(SettingsError): + self.load({"ca_bundle": str(self.tmp / "missing.pem")}) + + +class ProfileTests(SettingsTestCase): + CONFIG = """ +[default] +host = "default.test" +assessment_id = "default-assessment" + +[profiles.staging] +host = "staging.test" +""" + + def test_profile_from_flag_overrides_default_values(self): + self.write_config(self.CONFIG) + settings = self.load({"profile": "staging"}) + + self.assertEqual(settings.host.value, "https://staging.test") + self.assertEqual(settings.host.source, "config file [profiles.staging]") + self.assertEqual(settings.profile.source, "flag --config-profile") + + def test_unset_profile_values_fall_back_to_default(self): + self.write_config(self.CONFIG) + settings = self.load(None, {"CLOUDEXIT_PROFILE": "staging"}) + + self.assertEqual(settings.assessment_id.value, "default-assessment") + self.assertEqual(settings.assessment_id.source, "config file [default]") + self.assertEqual(settings.profile.source, "env CLOUDEXIT_PROFILE") + + def test_without_a_profile_only_default_is_used(self): + self.write_config(self.CONFIG) + self.assertEqual(self.load().host.value, "https://default.test") + + def test_unknown_profile_is_an_error(self): + self.write_config(self.CONFIG) + with self.assertRaises(SettingsError) as ctx: + self.load({"profile": "prod"}) + self.assertIn("prod", str(ctx.exception)) + + def test_profile_without_a_config_file_is_an_error(self): + with self.assertRaises(SettingsError): + self.load({"profile": "staging"}) + + def test_flag_still_beats_the_profile(self): + self.write_config(self.CONFIG) + settings = self.load({"profile": "staging", "host": "flag.test"}) + self.assertEqual(settings.host.value, "https://flag.test") + + +class KeyFileTests(SettingsTestCase): + def test_key_is_read_from_the_flag_file(self): + settings = self.load({"api_key_file": self.write_key()}) + + self.assertEqual(settings.api_key.value, KEY) + self.assertEqual(settings.api_key.source, "flag --api-key-file") + self.assertEqual(settings.api_key_file.value, str(self.tmp / "api-key")) + + def test_env_key_file_and_env_key(self): + from_file = self.load(None, {"CLOUDEXIT_API_KEY_FILE": self.write_key()}) + self.assertEqual(from_file.api_key.value, KEY) + self.assertEqual(from_file.api_key.source, "env CLOUDEXIT_API_KEY_FILE") + + inline = self.load(None, {"CLOUDEXIT_API_KEY": "inline-key-123456"}) + self.assertEqual(inline.api_key.value, "inline-key-123456") + self.assertEqual(inline.api_key.source, "env CLOUDEXIT_API_KEY") + self.assertIsNone(inline.api_key_file.value) + + def test_key_file_wins_over_an_inline_key_in_the_same_layer(self): + env = { + "CLOUDEXIT_API_KEY_FILE": self.write_key(), + "CLOUDEXIT_API_KEY": "inline-key-123456", + } + self.assertEqual(self.load(None, env).api_key.value, KEY) + + def test_a_higher_layer_inline_key_beats_a_lower_layer_key_file(self): + self.write_config(f'[default]\napi_key_file = "{self.write_key()}"\n') + settings = self.load(None, {"CLOUDEXIT_API_KEY": "inline-key-123456"}) + self.assertEqual(settings.api_key.value, "inline-key-123456") + + def test_relative_key_file_in_the_config_is_relative_to_the_config(self): + self.write_key("keys-api", key="relative-key-123456") + self.write_config('[default]\napi_key_file = "../keys-api"\n') + + settings = self.load() + + self.assertEqual(settings.api_key.value, "relative-key-123456") + self.assertEqual(settings.api_key.source, "config file [default]") + + def test_key_in_the_config_file(self): + self.write_config('[default]\napi_key = "file-key-0123456789"\n') + settings = self.load() + self.assertEqual(settings.api_key.value, "file-key-0123456789") + + def test_missing_and_empty_key_files_are_errors(self): + with self.assertRaises(SettingsError): + self.load({"api_key_file": str(self.tmp / "missing")}) + empty = self.tmp / "empty" + empty.write_text("\n") + with self.assertRaises(SettingsError): + self.load({"api_key_file": str(empty)}) + + def test_the_key_never_appears_in_a_repr(self): + settings = self.load({"api_key_file": self.write_key()}) + self.assertNotIn(KEY, repr(settings)) + + +@unittest.skipUnless(os.name == "posix", "POSIX permissions") +class PermissionWarningTests(SettingsTestCase): + def test_config_file_readable_by_others_warns(self): + path = self.write_config('[default]\nhost = "x.test"\n', mode=0o644) + with self.assertLogs(LOGGER, level="WARNING") as logs: + self.load() + self.assertIn(str(path), logs.output[0]) + self.assertIn("chmod 600", logs.output[0]) + + def test_private_config_file_does_not_warn(self): + self.write_config('[default]\nhost = "x.test"\n', mode=0o600) + with self.assertNoLogs(LOGGER, level="WARNING"): + self.load() + + def test_key_file_readable_by_group_warns(self): + path = self.write_key(mode=0o640) + with self.assertLogs(LOGGER, level="WARNING") as logs: + self.load({"api_key_file": path}) + self.assertIn(path, logs.output[0]) + self.assertNotIn(KEY, logs.output[0]) + + def test_private_key_file_does_not_warn(self): + with self.assertNoLogs(LOGGER, level="WARNING"): + self.load({"api_key_file": self.write_key(mode=0o600)}) + + +class LegacySourceTests(SettingsTestCase): + def test_host_and_key_env_vars_are_used_together_with_a_warning(self): + env = {"HOST": "legacy.test", "KEY": "legacy-key-0123456"} + with self.assertLogs(LOGGER, level="WARNING") as logs: + settings = self.load(None, env) + + self.assertEqual(settings.host.value, "https://legacy.test") + self.assertEqual(settings.host.source, "legacy env HOST") + self.assertEqual(settings.api_key.value, "legacy-key-0123456") + self.assertEqual(settings.api_key.source, "legacy env KEY") + self.assertEqual(len(logs.output), 1) + self.assertIn("deprecated", logs.output[0]) + self.assertNotIn("legacy-key-0123456", logs.output[0]) + + def test_host_alone_or_key_alone_is_ignored(self): + for env in ({"HOST": "legacy.test"}, {"KEY": "legacy-key-0123456"}): + with self.assertNoLogs(LOGGER, level="WARNING"): + settings = self.load(None, env) + self.assertIsNone(settings.host.value) + self.assertIsNone(settings.api_key.value) + + def test_config_py_is_used_with_a_warning(self): + with ( + self.legacy(HOST="old.test", KEY="old-key-0123456789"), + self.assertLogs(LOGGER, level="WARNING") as logs, + ): + settings = self.load() + + self.assertEqual(settings.host.value, "https://old.test") + self.assertEqual(settings.host.source, "legacy config.py") + self.assertEqual(settings.api_key.source, "legacy config.py") + self.assertEqual(len(logs.output), 1) + self.assertIn("config.py is deprecated", logs.output[0]) + self.assertNotIn("old-key-0123456789", logs.output[0]) + + def test_env_pair_beats_config_py(self): + env = {"HOST": "legacy.test", "KEY": "legacy-key-0123456"} + with self.legacy(HOST="old.test", KEY="old-key"): + with self.assertLogs(LOGGER, level="WARNING"): + settings = self.load(None, env) + self.assertEqual(settings.host.source, "legacy env HOST") + + def test_no_warning_when_a_higher_source_covers_everything(self): + with self.legacy(HOST="old.test", KEY="old-key-0123456789"): + with self.assertNoLogs(LOGGER, level="WARNING"): + settings = self.load( + {"host": "flag.test"}, {"CLOUDEXIT_API_KEY": "inline-key-123456"} + ) + self.assertEqual(settings.host.source, "flag --host") + + def test_an_empty_config_py_or_a_namespace_package_is_ignored(self): + for legacy in (types.SimpleNamespace(), types.SimpleNamespace(HOST="", KEY="")): + with self.legacy(**vars(legacy)): + settings = self.load() + self.assertIsNone(settings.host.value) + + +class DescribeTests(SettingsTestCase): + def test_masks_the_key_to_prefix_and_last_four(self): + for prefix in ("esc_sk_", "exc_eu_sk_", "exc_us_sk_"): + with self.subTest(prefix=prefix): + self.assertEqual(mask_key(f"{prefix}AAAABBBBCCCC"), f"{prefix}...CCCC") + self.assertEqual(mask_key(KEY), "sk_l...cdef") + self.assertEqual(mask_key("short"), "****") + self.assertEqual(mask_key("esc_sk_abcd"), "****") + self.assertEqual(mask_key(None), "-") + + def test_output_lists_values_with_sources_and_never_the_full_key(self): + settings = self.load( + {"host": "flag.test", "api_key_file": self.write_key()}, + {"CLOUDEXIT_ASSESSMENT_ID": "a-1"}, + ) + text = "\n".join(describe(settings)) + + self.assertNotIn(KEY, text) + self.assertIn("sk_l...cdef", text) + self.assertIn("https://flag.test", text) + self.assertIn("flag --host", text) + self.assertIn("flag --api-key-file", text) + self.assertIn("env CLOUDEXIT_ASSESSMENT_ID", text) + self.assertIn("(not found)", text) + + def test_unset_values_are_shown_as_unset(self): + lines = describe(Settings()) + host_line = next(line for line in lines if line.startswith("host")) + self.assertIn("-", host_line) + self.assertIn("unset", host_line) + + +class ConfigCommandTests(SettingsTestCase): + def run_main(self, argv, env=None): + environ = {"XDG_CONFIG_HOME": str(self.tmp), **(env or {})} + out = io.StringIO() + with ( + patch("sys.argv", ["cloudexit", *argv]), + patch.dict(os.environ, environ, clear=True), + patch("main.initialize_dataset") as mock_dataset, + patch("main.console.print") as mock_print, + contextlib.redirect_stdout(out), + ): + try: + main.main() + code = None + except SystemExit as exc: + code = exc.code + return out.getvalue(), code, mock_dataset, mock_print + + def test_config_path_prints_the_location_without_banner_or_dataset(self): + out, code, mock_dataset, mock_print = self.run_main(["config", "path"]) + + self.assertIsNone(code) + self.assertEqual(out.strip(), str(self.config_path)) + mock_dataset.assert_not_called() + mock_print.assert_not_called() + + def test_config_show_masks_keys(self): + out, code, mock_dataset, _ = self.run_main( + ["config", "show", "--host", "flag.test"], + {"CLOUDEXIT_API_KEY": KEY}, + ) + + self.assertIsNone(code) + self.assertNotIn(KEY, out) + self.assertIn("sk_l...cdef", out) + self.assertIn("https://flag.test", out) + self.assertIn("flag --host", out) + mock_dataset.assert_not_called() + + def test_config_show_with_a_bad_host_exits_with_the_config_code(self): + _, code, _, mock_print = self.run_main( + ["config", "show"], {"CLOUDEXIT_HOST": "http://example.com"} + ) + + self.assertEqual(code, codes.CONFIG) + self.assertIn("https://", mock_print.call_args.args[0]) + + def test_config_requires_a_subcommand(self): + with ( + patch("sys.argv", ["cloudexit", "config"]), + contextlib.redirect_stderr(io.StringIO()), + self.assertRaises(SystemExit), + ): + main.parse_arguments() + + +class ArgumentTests(unittest.TestCase): + def parse(self, *argv): + with patch("sys.argv", ["cloudexit", *argv]): + return main.parse_arguments() + + def test_platform_flags_on_aws_and_azure(self): + for provider in ("aws", "azure"): + args = self.parse( + provider, + "--host", + "platform.test", + "--api-key-file", + "/k", + "--config-profile", + "staging", + "--assessment-id", + "a-1", + "--ca-bundle", + "/ca.pem", + "--timeout", + "7.5", + ) + self.assertEqual(args.host, "platform.test") + self.assertEqual(args.api_key_file, "/k") + self.assertEqual(args.config_profile, "staging") + self.assertEqual(args.assessment_id, "a-1") + self.assertEqual(args.ca_bundle, "/ca.pem") + self.assertEqual(args.timeout, 7.5) + + def test_aws_profile_still_means_the_aws_profile(self): + args = self.parse("aws", "--profile", "my-aws") + self.assertEqual(args.profile, "my-aws") + self.assertIsNone(args.config_profile) + + def test_main_resolves_settings_before_the_dataset_download(self): + args = Namespace(cloud_provider="aws") + with ( + patch("main.parse_arguments", return_value=args), + patch("main.load_settings", return_value=Settings()) as mock_load, + patch("main.initialize_dataset") as mock_dataset, + patch("main.handle_aws") as mock_handle, + patch("main.console.print"), + ): + main.main() + + mock_load.assert_called_once() + self.assertIsInstance(args.settings, Settings) + mock_dataset.assert_called_once() + mock_handle.assert_called_once_with(args) + + def test_a_bad_setting_fails_before_the_dataset_download(self): + with ( + patch("main.parse_arguments", return_value=Namespace(cloud_provider="aws")), + patch("main.load_settings", side_effect=SettingsError("bad host")), + patch("main.initialize_dataset") as mock_dataset, + patch("main.handle_aws") as mock_handle, + patch("main.console.print"), + self.assertRaises(SystemExit) as ctx, + ): + main.main() + + self.assertEqual(ctx.exception.code, codes.CONFIG) + mock_dataset.assert_not_called() + mock_handle.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_utils_and_main.py b/tests/test_utils_and_main.py index 3c51c7a..7b580a0 100644 --- a/tests/test_utils_and_main.py +++ b/tests/test_utils_and_main.py @@ -1,7 +1,6 @@ import json import os import tempfile -import types import unittest from argparse import Namespace from pathlib import Path @@ -10,6 +9,7 @@ import main from utils import codes from utils.connection import resolve_mode +from utils.settings import Setting, Settings from utils.utils import load_config VALID_CONFIG = { @@ -422,7 +422,12 @@ def test_dry_run_flag_passed_from_handle_aws(self): main.handle_aws(_ni_aws_args(dry_run=True)) mock_run.assert_called_once_with( - ANY, "aws", dry_run=True, non_interactive=True, egress=False + ANY, + "aws", + dry_run=True, + non_interactive=True, + egress=False, + settings=None, ) @@ -824,7 +829,12 @@ def test_handle_azure_passes_egress_flag(self): main.handle_azure(_ni_azure_args(egress=True)) mock_run.assert_called_once_with( - ANY, "azure", dry_run=False, non_interactive=True, egress=True + ANY, + "azure", + dry_run=False, + non_interactive=True, + egress=True, + settings=None, ) def test_handle_aws_passes_egress_flag(self): @@ -837,11 +847,21 @@ def test_handle_aws_passes_egress_flag(self): main.handle_aws(_ni_aws_args(egress=True)) mock_run.assert_called_once_with( - ANY, "aws", dry_run=False, non_interactive=True, egress=True + ANY, + "aws", + dry_run=False, + non_interactive=True, + egress=True, + settings=None, ) class MainExitCodeTests(unittest.TestCase): + def setUp(self): + patcher = patch("main.load_settings", return_value=Settings()) + patcher.start() + self.addCleanup(patcher.stop) + def test_config_error_from_handler_exits_config(self): with ( patch("main.initialize_dataset"), @@ -909,32 +929,48 @@ def test_bad_config_file_exits_config_end_to_end(self): mock_run.assert_not_called() -class ResolveModeEnvVarTests(unittest.TestCase): - def test_host_and_key_env_override_config(self): - fake_config = types.SimpleNamespace(HOST="config-host.io", KEY="config-key") - with ( - patch("utils.connection.config", fake_config), - patch.dict( - os.environ, {"HOST": "env-host.io", "KEY": "env-key"}, clear=False - ), - patch("utils.connection.get_jwt_token", return_value="tok123"), - ): - mode, token = resolve_mode() +class ResolveModeSettingsTests(unittest.TestCase): + def test_host_and_key_from_settings_go_online(self): + settings = Settings( + host=Setting("https://platform.test", "env CLOUDEXIT_HOST"), + api_key=Setting("k" * 16, "env CLOUDEXIT_API_KEY"), + timeout=Setting(30.0, "flag --timeout"), + ca_bundle=Setting("/etc/ca.pem", "flag --ca-bundle"), + ) + with patch( + "utils.connection.get_jwt_token", return_value="tok123" + ) as mock_token: + mode, token = resolve_mode(settings) self.assertEqual(mode, "online") self.assertEqual(token, "tok123") + mock_token.assert_called_once_with( + host="https://platform.test", + key="k" * 16, + timeout=30.0, + verify="/etc/ca.pem", + ) - def test_falls_back_to_config_when_env_not_set(self): - fake_config = types.SimpleNamespace(HOST="", KEY="") - env = {k: v for k, v in os.environ.items() if k not in ("HOST", "KEY")} - with ( - patch("utils.connection.config", fake_config), - patch.dict(os.environ, env, clear=True), + def test_no_host_or_key_stays_offline_without_a_request(self): + for settings in ( + Settings(), + Settings(host=Setting("https://platform.test", "flag --host")), + Settings(api_key=Setting("k" * 16, "env CLOUDEXIT_API_KEY")), ): - mode, token = resolve_mode() + with patch("utils.connection.get_jwt_token") as mock_token: + mode, token = resolve_mode(settings) + + self.assertEqual(mode, "offline") + self.assertIsNone(token) + mock_token.assert_not_called() - self.assertEqual(mode, "offline") - self.assertIsNone(token) + def test_failed_authentication_falls_back_to_offline(self): + settings = Settings( + host=Setting("https://platform.test", "flag --host"), + api_key=Setting("k" * 16, "env CLOUDEXIT_API_KEY"), + ) + with patch("utils.connection.get_jwt_token", return_value=None): + self.assertEqual(resolve_mode(settings), ("offline", None)) TFSTATE_CONFIG = { diff --git a/tests/test_utils_sync.py b/tests/test_utils_sync.py index bc1418c..d843a46 100644 --- a/tests/test_utils_sync.py +++ b/tests/test_utils_sync.py @@ -1,105 +1,63 @@ -import importlib.util import json import os -import sys import tempfile -import types import unittest -from pathlib import Path -from unittest.mock import MagicMock, patch +from unittest.mock import patch from core.utils_sync import post_assessment +from tests.http_fixtures import fake_http, make_response +from utils.settings import SettingsError +META = {"exit_strategy": 1, "cloud_service_provider": 2, "assessment_type": 1} -class PostAssessmentHostResolutionTests(unittest.TestCase): - def test_uses_host_from_environment_when_config_host_empty(self): - fake_response = MagicMock() - fake_response.ok = True - fake_response.status_code = 200 - fake_response.json.return_value = {"data": {"ok": True}} +def post(**overrides): + kwargs = dict( + name="Demo", + started_at=123, + report_path="/tmp/report", + meta=META, + token="jwt-token", + host="platform.test", + ) + kwargs.update(overrides) + return post_assessment(**kwargs) + + +class PostAssessmentTests(unittest.TestCase): + def test_posts_to_the_given_host_over_https(self): with ( - patch.dict(os.environ, {"HOST": "env.exitcloud.io"}, clear=False), - patch( - "core.utils_sync.config", - types.SimpleNamespace(HOST=""), - ), patch("core.utils_sync._build_payload", return_value={"sample": "payload"}), - patch( - "core.utils_sync.requests.post", return_value=fake_response - ) as mock_post, + fake_http(make_response(json_body={"data": {"ok": True}})) as fake, ): - result = post_assessment( - name="Demo", - started_at=123, - report_path="/tmp/report", - meta={ - "exit_strategy": 1, - "cloud_service_provider": 2, - "assessment_type": 1, - }, - token="jwt-token", - ) + result = post() self.assertTrue(result["success"]) - called_url = mock_post.call_args.args[0] - self.assertEqual(called_url, "https://env.exitcloud.io/api/v1/assessments/") + (request,) = fake.requests + self.assertEqual(request.url, "https://platform.test/api/v1/assessments/") + self.assertEqual(request.kwargs["json"], {"sample": "payload"}) - def test_returns_clear_error_when_host_missing_everywhere(self): + def test_timeout_and_ca_bundle_are_passed_to_requests(self): with ( - patch.dict(os.environ, {}, clear=True), - patch( - "core.utils_sync.config", - types.SimpleNamespace(HOST=""), - ), + patch("core.utils_sync._build_payload", return_value={}), + fake_http(make_response(json_body={})) as fake, ): - result = post_assessment( - name="Demo", - started_at=123, - report_path="/tmp/report", - meta={ - "exit_strategy": 1, - "cloud_service_provider": 2, - "assessment_type": 1, - }, - token="jwt-token", - ) + post(timeout=42.5, verify="/etc/ca.pem") + + self.assertEqual(fake.requests[0].kwargs["timeout"], 42.5) + self.assertEqual(fake.requests[0].kwargs["verify"], "/etc/ca.pem") + + def test_returns_clear_error_when_no_host_is_configured(self): + result = post(host=None) self.assertFalse(result["success"]) self.assertEqual(result["payload"], None) - self.assertEqual(result["logs"], "HOST missing in environment and config.py") - - -class MissingConfigModuleTests(unittest.TestCase): - def load_without_config(self): - path = Path(__file__).resolve().parent.parent / "core" / "utils_sync.py" - spec = importlib.util.spec_from_file_location("utils_sync_no_config", path) - module = importlib.util.module_from_spec(spec) - # None in sys.modules makes `import config` fail as if the file were absent - with patch.dict(sys.modules, {"config": None}): - spec.loader.exec_module(module) - return module - - def test_module_imports_without_config(self): - self.assertIsNone(self.load_without_config().config) - - def test_post_assessment_reports_missing_host_without_config(self): - module = self.load_without_config() - with patch.dict(os.environ, {}, clear=True): - result = module.post_assessment( - name="Demo", - started_at=123, - report_path="/tmp/report", - meta={ - "exit_strategy": 1, - "cloud_service_provider": 2, - "assessment_type": 1, - }, - token="jwt-token", - ) + self.assertIn("no upload host configured", result["logs"]) - self.assertFalse(result["success"]) - self.assertEqual(result["logs"], "HOST missing in environment and config.py") + def test_plain_http_to_a_remote_host_is_refused(self): + with patch("core.utils_sync._build_payload", return_value={}): + with self.assertRaises(SettingsError): + post(host="http://platform.test") class WriteAssessmentPayloadTests(unittest.TestCase): diff --git a/utils/connection.py b/utils/connection.py index 6dd1f26..dd06f84 100644 --- a/utils/connection.py +++ b/utils/connection.py @@ -2,54 +2,38 @@ from __future__ import annotations import logging -import os import requests -logger = logging.getLogger("main.utils.connection") +from utils.settings import Settings, load_settings, normalize_host -try: - import config -except ModuleNotFoundError: - config = None +logger = logging.getLogger("main.utils.connection") _AUTH_PATH = "/api/v1/auth/token/" def _build_url(host: str) -> str: - host = host.strip().rstrip("/") - if host.startswith("http://"): - host = "https://" + host[len("http://") :] - elif not host.startswith("https://"): - host = f"https://{host}" - return f"{host}{_AUTH_PATH}" + return f"{normalize_host(host)}{_AUTH_PATH}" def get_jwt_token( - host: str | None = None, key: str | None = None, *, timeout: int = 10 + host: str | None = None, + key: str | None = None, + *, + timeout: float = 10, + verify: str | bool = True, ) -> str | None: - host = ( - host - or os.environ.get("HOST", "").strip() - or (getattr(config, "HOST", "") if config else "") - ) - key = ( - key - or os.environ.get("KEY", "").strip() - or (getattr(config, "KEY", "") if config else "") - ) - if not host: - logger.debug("HOST empty – skipping ExitCloud authentication.") + logger.debug("Host empty – skipping ExitCloud authentication.") return None if not key: - logger.debug("KEY empty – skipping ExitCloud authentication.") + logger.debug("API key empty – skipping ExitCloud authentication.") return None url = _build_url(host) headers = {"Authorization": f"Bearer {key}"} try: - resp = requests.post(url, headers=headers, timeout=timeout) + resp = requests.post(url, headers=headers, timeout=timeout, verify=verify) resp.raise_for_status() data = resp.json() @@ -77,22 +61,24 @@ def get_jwt_token( return None -def resolve_mode() -> tuple[str, str | None]: - host = os.environ.get("HOST", "").strip() or ( - getattr(config, "HOST", "") if config else "" - ) - key = os.environ.get("KEY", "").strip() or ( - getattr(config, "KEY", "") if config else "" - ) +def resolve_mode(settings: Settings | None = None) -> tuple[str, str | None]: + settings = settings or load_settings() + host = settings.host.value + key = settings.api_key.value if not host: - logger.debug("HOST empty – running in offline mode.") + logger.debug("Host empty – running in offline mode.") return "offline", None if not key: - logger.debug("KEY empty – running in offline mode.") + logger.debug("API key empty – running in offline mode.") return "offline", None - token = get_jwt_token(host=host, key=key) + token = get_jwt_token( + host=host, + key=key, + timeout=settings.timeout.value, + verify=settings.ca_bundle.value or True, + ) if token: return "online", token diff --git a/utils/settings.py b/utils/settings.py new file mode 100644 index 0000000..35f6796 --- /dev/null +++ b/utils/settings.py @@ -0,0 +1,446 @@ +from __future__ import annotations + +import logging +import os +import stat +import sys +import tomllib +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Mapping +from urllib.parse import urlsplit + +logger = logging.getLogger("main.utils.settings") + +try: + import config as legacy_config +except ModuleNotFoundError: + legacy_config = None + +DEFAULT_TIMEOUT = 10.0 +LOCAL_HTTP_HOSTS = ("localhost", "127.0.0.1") +KEY_PREFIXES = ("esc_sk_", "exc_eu_sk_", "exc_us_sk_") + +ENV_VARS = { + "host": "CLOUDEXIT_HOST", + "api_key": "CLOUDEXIT_API_KEY", + "api_key_file": "CLOUDEXIT_API_KEY_FILE", + "profile": "CLOUDEXIT_PROFILE", + "assessment_id": "CLOUDEXIT_ASSESSMENT_ID", + "upload": "CLOUDEXIT_UPLOAD", + "ca_bundle": "CLOUDEXIT_CA_BUNDLE", + "timeout": "CLOUDEXIT_TIMEOUT", + "spool_dir": "CLOUDEXIT_SPOOL_DIR", + "catalogue_url": "CLOUDEXIT_CATALOGUE_URL", +} + +FLAGS = { + "host": "--host", + "api_key_file": "--api-key-file", + "profile": "--config-profile", + "assessment_id": "--assessment-id", + "ca_bundle": "--ca-bundle", + "timeout": "--timeout", +} + +FILE_KEYS = ("host", "api_key", "api_key_file", "assessment_id", "ca_bundle", "upload") +PATH_KEYS = ("api_key_file", "ca_bundle", "spool_dir") + +TRUE_WORDS = ("1", "true", "yes", "on") +FALSE_WORDS = ("0", "false", "no", "off") + + +class SettingsError(Exception): + pass + + +@dataclass(frozen=True) +class Setting: + value: Any = field(default=None, repr=False) + source: str = "unset" + + +@dataclass(frozen=True) +class Settings: + host: Setting = field(default_factory=Setting) + api_key: Setting = field(default_factory=Setting) + api_key_file: Setting = field(default_factory=Setting) + profile: Setting = field(default_factory=Setting) + assessment_id: Setting = field(default_factory=Setting) + upload: Setting = field(default_factory=Setting) + ca_bundle: Setting = field(default_factory=Setting) + timeout: Setting = field( + default_factory=lambda: Setting(DEFAULT_TIMEOUT, "default") + ) + spool_dir: Setting = field(default_factory=Setting) + catalogue_url: Setting = field(default_factory=Setting) + config_path: Path | None = None + config_found: bool = False + + +def config_file_path( + environ: Mapping[str, str] | None = None, *, platform: str = sys.platform +) -> Path: + environ = os.environ if environ is None else environ + if platform == "win32": + base = environ.get("APPDATA") or str(Path.home() / "AppData" / "Roaming") + else: + xdg = environ.get("XDG_CONFIG_HOME", "") + base = xdg if xdg and os.path.isabs(xdg) else str(Path.home() / ".config") + return Path(base) / "cloudexit" / "config.toml" + + +def normalize_host(raw: str) -> str: + host = raw.strip() + if not host: + raise SettingsError("host is empty") + if "://" not in host: + host = f"https://{host}" + + parts = urlsplit(host) + if not parts.hostname: + raise SettingsError(f"host {raw!r} has no host name") + if parts.scheme == "http": + if parts.hostname not in LOCAL_HTTP_HOSTS: + raise SettingsError( + f"plain http:// is not allowed for {parts.hostname}; use https:// " + "(http is only accepted for localhost and 127.0.0.1)" + ) + elif parts.scheme != "https": + raise SettingsError(f"unsupported scheme {parts.scheme}:// in host {raw!r}") + return host.rstrip("/") + + +def mask_key(key: str | None) -> str: + if not key: + return "-" + if len(key) < 12: + return "****" + for prefix in KEY_PREFIXES: + if key.startswith(prefix): + return f"{prefix}...{key[-4:]}" + return f"{key[:4]}...{key[-4:]}" + + +def flags_from_args(args: Any) -> dict[str, Any]: + return { + name: getattr(args, attr, None) + for name, attr in ( + ("host", "host"), + ("api_key_file", "api_key_file"), + ("profile", "config_profile"), + ("assessment_id", "assessment_id"), + ("ca_bundle", "ca_bundle"), + ("timeout", "timeout"), + ) + if getattr(args, attr, None) is not None + } + + +def _expand(raw: str, base: Path | None) -> str: + path = Path(raw).expanduser() + if base is not None and not path.is_absolute(): + path = base / path + return str(path) + + +def _warn_if_readable(path: Path, what: str) -> None: + if os.name != "posix": + return + try: + mode = path.stat().st_mode + except OSError: + return + if mode & (stat.S_IRGRP | stat.S_IROTH): + logger.warning( + "%s %s is readable by group or others; restrict it with: chmod 600 %s", + what, + path, + path, + ) + + +def _flag_layer(flags: Mapping[str, Any]) -> dict[str, Setting]: + layer = {} + for name, flag in FLAGS.items(): + value = flags.get(name) + if isinstance(value, str): + value = value.strip() + if value is None or value == "": + continue + if name in PATH_KEYS: + value = _expand(value, None) + layer[name] = Setting(value, f"flag {flag}") + return layer + + +def _env_layer(environ: Mapping[str, str]) -> dict[str, Setting]: + layer = {} + for name, var in ENV_VARS.items(): + value = environ.get(var, "").strip() + if not value: + continue + if name in PATH_KEYS: + value = _expand(value, None) + layer[name] = Setting(value, f"env {var}") + return layer + + +def _legacy_env_layer(environ: Mapping[str, str]) -> dict[str, Setting]: + host = environ.get("HOST", "").strip() + key = environ.get("KEY", "").strip() + if host and key: + return { + "host": Setting(host, "legacy env HOST"), + "api_key": Setting(key, "legacy env KEY"), + } + if host or key: + logger.debug("Ignoring legacy HOST/KEY: they are only used when both are set.") + return {} + + +def _legacy_config_layer() -> dict[str, Setting]: + layer = {} + for name, attr in (("host", "HOST"), ("api_key", "KEY")): + value = getattr(legacy_config, attr, "") + if isinstance(value, str) and value.strip(): + layer[name] = Setting(value.strip(), "legacy config.py") + return layer + + +def _read_config_file(path: Path) -> dict[str, Any] | None: + if not path.is_file(): + return None + _warn_if_readable(path, "config file") + try: + with path.open("rb") as handle: + return tomllib.load(handle) + except (OSError, tomllib.TOMLDecodeError) as exc: + raise SettingsError(f"cannot read config file {path}: {exc}") from exc + + +def _table_layer(table: Any, label: str, path: Path) -> dict[str, Setting]: + if not isinstance(table, dict): + raise SettingsError(f"{label} in {path} must be a table") + + layer = {} + for key, value in table.items(): + if key not in FILE_KEYS: + logger.warning("Unknown key %r in %s of %s is ignored.", key, label, path) + continue + if key == "upload": + if not isinstance(value, bool): + raise SettingsError(f"{key} in {label} of {path} must be true or false") + elif not isinstance(value, str): + raise SettingsError(f"{key} in {label} of {path} must be a string") + elif not value.strip(): + continue + else: + value = value.strip() + if key in PATH_KEYS: + value = _expand(value, path.parent) + layer[key] = Setting(value, f"config file {label}") + return layer + + +def _file_layers( + data: dict[str, Any], path: Path, profile: str | None +) -> list[dict[str, Setting]]: + for key in data: + if key not in ("default", "profiles"): + logger.warning("Unknown table %r in %s is ignored.", key, path) + + profiles = data.get("profiles", {}) + if not isinstance(profiles, dict): + raise SettingsError(f"[profiles] in {path} must be a table of tables") + + layers = [] + if profile: + if profile not in profiles: + raise SettingsError(f"profile {profile!r} not found in {path}") + layers.append(_table_layer(profiles[profile], f"[profiles.{profile}]", path)) + layers.append(_table_layer(data.get("default", {}), "[default]", path)) + return layers + + +def _read_key_file(setting: Setting) -> str: + path = Path(setting.value) + if not path.is_file(): + raise SettingsError(f"API key file {path} ({setting.source}) not found") + _warn_if_readable(path, "API key file") + try: + key = path.read_text(encoding="utf-8").strip() + except (OSError, UnicodeDecodeError) as exc: + raise SettingsError(f"cannot read API key file {path}: {exc}") from exc + if not key: + raise SettingsError(f"API key file {path} is empty") + return key + + +def _resolve_key(layers: list[dict[str, Setting]]) -> tuple[Setting, Setting]: + for layer in layers: + key_file = layer.get("api_key_file") + if key_file is not None: + return Setting(_read_key_file(key_file), key_file.source), key_file + if "api_key" in layer: + return layer["api_key"], Setting() + return Setting(), Setting() + + +def _first(layers: list[dict[str, Setting]], name: str) -> Setting: + for layer in layers: + if name in layer: + return layer[name] + return Setting() + + +def _parse_timeout(setting: Setting) -> Setting: + try: + seconds = float(setting.value) + except (TypeError, ValueError): + seconds = 0.0 + if seconds <= 0: + raise SettingsError( + f"timeout ({setting.source}) must be a positive number of seconds" + ) + return Setting(seconds, setting.source) + + +def _parse_upload(setting: Setting) -> Setting: + value = setting.value + if isinstance(value, str): + word = value.lower() + if word in TRUE_WORDS: + value = True + elif word in FALSE_WORDS: + value = False + if not isinstance(value, bool): + raise SettingsError(f"upload ({setting.source}) must be true or false") + return Setting(value, setting.source) + + +def _check_host(setting: Setting) -> Setting: + try: + return Setting(normalize_host(setting.value), setting.source) + except SettingsError as exc: + raise SettingsError(f"host ({setting.source}): {exc}") from exc + + +def _check_ca_bundle(setting: Setting) -> Setting: + if not Path(setting.value).is_file(): + raise SettingsError( + f"CA bundle {setting.value} ({setting.source}) is not a file" + ) + return setting + + +def _warn_deprecated(settings: Settings) -> None: + sources = { + setting.source + for setting in (settings.host, settings.api_key) + if setting.source.startswith("legacy") + } + if "legacy env HOST" in sources or "legacy env KEY" in sources: + logger.warning( + "The HOST and KEY environment variables are deprecated; use " + "CLOUDEXIT_HOST and CLOUDEXIT_API_KEY_FILE (or CLOUDEXIT_API_KEY)." + ) + if "legacy config.py" in sources: + logger.warning( + "config.py is deprecated; move HOST and KEY to the config file " + "(see `cloudexit config path`) or to CLOUDEXIT_* environment variables." + ) + + +def load_settings( + flags: Mapping[str, Any] | None = None, + *, + environ: Mapping[str, str] | None = None, + platform: str = sys.platform, +) -> Settings: + environ = os.environ if environ is None else environ + top = [_flag_layer(flags or {}), _env_layer(environ)] + profile = _first(top, "profile") + + path = config_file_path(environ, platform=platform) + data = _read_config_file(path) + if data is None: + if profile.value: + raise SettingsError( + f"profile {profile.value!r} ({profile.source}) requested but " + f"there is no config file at {path}" + ) + file_layers = [] + else: + file_layers = _file_layers(data, path, profile.value) + + layers = [ + *top, + *file_layers, + _legacy_env_layer(environ), + _legacy_config_layer(), + ] + + api_key, api_key_file = _resolve_key(layers) + host = _first(layers, "host") + ca_bundle = _first(layers, "ca_bundle") + timeout = _first(layers, "timeout") + upload = _first(layers, "upload") + + settings = Settings( + host=_check_host(host) if host.value else host, + api_key=api_key, + api_key_file=api_key_file, + profile=profile, + assessment_id=_first(layers, "assessment_id"), + upload=_parse_upload(upload) if upload.value is not None else upload, + ca_bundle=_check_ca_bundle(ca_bundle) if ca_bundle.value else ca_bundle, + timeout=( + _parse_timeout(timeout) if timeout.value is not None else Settings().timeout + ), + spool_dir=_first(layers, "spool_dir"), + catalogue_url=_first(layers, "catalogue_url"), + config_path=path, + config_found=data is not None, + ) + _warn_deprecated(settings) + return settings + + +def describe(settings: Settings) -> list[str]: + found = "found" if settings.config_found else "not found" + rows = [] + for name in ( + "host", + "api_key", + "api_key_file", + "profile", + "assessment_id", + "upload", + "ca_bundle", + "timeout", + "spool_dir", + "catalogue_url", + ): + setting = getattr(settings, name) + value = setting.value + if name == "api_key": + shown = mask_key(value) + elif value is None: + shown = "-" + elif isinstance(value, bool): + shown = "true" if value else "false" + elif isinstance(value, float): + shown = f"{value:g}" + else: + shown = str(value) + rows.append((name, shown, setting.source)) + + name_width = max(len(row[0]) for row in rows) + value_width = max(len(row[1]) for row in rows) + lines = [f"config file: {settings.config_path} ({found})"] + lines.extend( + f"{name:<{name_width}} {shown:<{value_width}} {source}" + for name, shown, source in rows + ) + return lines diff --git a/utils/utils.py b/utils/utils.py index b6f264c..c2f2bb2 100644 --- a/utils/utils.py +++ b/utils/utils.py @@ -185,3 +185,5 @@ def print_help_message(): console.print(" python3 main.py azure --cli") console.print(" python3 main.py azure --name 'DMS System'") console.print(" python3 main.py azure --tfstate infra.tfstate --dry-run") + console.print(" python3 main.py config path") + console.print(" python3 main.py config show") From 1f267dec8d872d6515aefe5fa53e7322ce027319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Thu, 22 Oct 2026 20:05:13 +0200 Subject: [PATCH 04/61] add catalogue manifest update and catalogue commands --- README.md | 13 + core/utils.py | 6 +- core/utils_db.py | 17 +- main.py | 141 +++++- tests/catalogue_fixtures.py | 98 ++++ tests/http_fixtures.py | 6 +- tests/test_catalogue.py | 788 ++++++++++++++++++++++++++++++ tests/test_copy_assets.py | 5 +- tests/test_settings.py | 6 +- tests/test_utils_and_main.py | 8 +- tests/test_utils_report_egress.py | 9 +- utils/data.py | 701 +++++++++++++++++++++----- 12 files changed, 1635 insertions(+), 163 deletions(-) create mode 100644 tests/catalogue_fixtures.py create mode 100644 tests/test_catalogue.py diff --git a/README.md b/README.md index 21d1d05..d5c7e77 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,19 @@ cloudexit config path # where the config file is looked up cloudexit config show # every resolved value and its source (keys are masked) ``` +## Catalogue dataset + +cloudexit assesses against a catalogue dataset that it keeps up to date. It is stored in your data directory (`~/.local/share/cloudexit/catalogue/`, `$XDG_DATA_HOME/cloudexit/catalogue/`, or `%LOCALAPPDATA%\cloudexit\catalogue\` on Windows) together with the previous version. Each run checks for a newer catalogue first. If that check fails, the run continues with the installed catalogue and says so; without any catalogue it stops. + +```bash +cloudexit catalogue status # installed and latest catalogue +cloudexit catalogue update # download the latest (or --version V) +cloudexit catalogue import FILE.db.gz # install a file on an air-gapped machine (--sha256 to verify) +cloudexit aws --tfstate infra.tfstate --catalogue FILE.db.gz # use a file for one run, without installing it +``` + +Set `CLOUDEXIT_CATALOGUE_URL` to download from a mirror instead of the default location. A `./datasets/data.db` from an earlier version is copied to the data directory once. + ## Data Landscape & Egress Estimation (alpha) Add `--egress` to any assessment to measure how much data lives in the assessed scope and estimate the one-time internet egress fee for moving it out — based on the provider's tiered list prices, with no additional permissions required. diff --git a/core/utils.py b/core/utils.py index 6d8ea38..d4f355f 100644 --- a/core/utils.py +++ b/core/utils.py @@ -3,6 +3,8 @@ import shutil import logging +from utils.data import active_catalogue_path + logger = logging.getLogger("core.engine.utils") # Icon folder per cloud service provider id @@ -59,8 +61,8 @@ def copy_assets(report_path: str, cloud_service_provider: int) -> None: if not os.path.exists(dest_path): shutil.copytree(src_path, dest_path, ignore=_png_only, dirs_exist_ok=True) - # Copy datasets/data.db to data/assessment.db - db_src_path = "datasets/data.db" + # Copy the catalogue this run used to data/assessment.db + db_src_path = str(active_catalogue_path()) db_dest_dir = os.path.join(report_path, "data") db_dest_path = os.path.join(db_dest_dir, "assessment.db") diff --git a/core/utils_db.py b/core/utils_db.py index 2ef045d..a39a52c 100644 --- a/core/utils_db.py +++ b/core/utils_db.py @@ -2,12 +2,11 @@ import sqlite3 import logging +from utils.data import active_catalogue_path + # Configure logger for database operations (level left to the root handlers) logger = logging.getLogger("core.engine.db") -# Default master database -MASTER_DATABASE = "datasets/data.db" - ALLOWED_TABLES = { "resourcetype", "resourcetype_data", @@ -25,7 +24,9 @@ } -def connect(db_path=MASTER_DATABASE): +def connect(db_path=None): + # No path means the catalogue dataset this run uses + db_path = db_path or active_catalogue_path() try: conn = sqlite3.connect(db_path) return conn @@ -34,7 +35,7 @@ def connect(db_path=MASTER_DATABASE): raise -def load_data(table_name, db_path=MASTER_DATABASE): +def load_data(table_name, db_path=None): if table_name not in ALLOWED_TABLES: raise ValueError(f"Disallowed table name: {table_name}") try: @@ -50,7 +51,7 @@ def load_data(table_name, db_path=MASTER_DATABASE): raise -def execute_query(query, params=None, db_path=MASTER_DATABASE): +def execute_query(query, params=None, db_path=None): try: conn = connect(db_path) cursor = conn.cursor() @@ -64,7 +65,7 @@ def execute_query(query, params=None, db_path=MASTER_DATABASE): raise -def fetch_one(query, params=None, db_path=MASTER_DATABASE): +def fetch_one(query, params=None, db_path=None): try: conn = connect(db_path) cursor = conn.cursor() @@ -78,7 +79,7 @@ def fetch_one(query, params=None, db_path=MASTER_DATABASE): raise -def fetch_all(query, params=None, db_path=MASTER_DATABASE): +def fetch_all(query, params=None, db_path=None): try: conn = connect(db_path) cursor = conn.cursor() diff --git a/main.py b/main.py index 1b3db08..f6e4fa7 100644 --- a/main.py +++ b/main.py @@ -40,7 +40,17 @@ ) from utils.aws import is_aws_cli_installed, is_aws_profile_valid from utils.connection import resolve_mode -from utils.data import initialize_dataset +from utils.data import ( + CatalogueError, + CatalogueUnavailable, + catalogue_base_url, + ensure_catalogue, + import_catalogue, + set_active_catalogue, + status_lines, + update_catalogue, + use_catalogue_file, +) from utils.utils import ( ascii_art, build_config, @@ -937,6 +947,10 @@ def parse_arguments(): " python3 main.py azure --tfstate infra.tfstate --dry-run\n" " python3 main.py config path # Print the config file location\n" " python3 main.py config show # Show resolved settings and their sources\n" + " python3 main.py catalogue status # Show the installed and latest catalogue\n" + " python3 main.py catalogue update # Download the latest catalogue\n" + " python3 main.py catalogue import FILE.db.gz # Install a catalogue offline\n" + " python3 main.py aws --tfstate infra.tfstate --catalogue FILE.db.gz\n" ), formatter_class=argparse.RawDescriptionHelpFormatter, ) @@ -950,7 +964,7 @@ def parse_arguments(): subparsers = parser.add_subparsers( dest="cloud_provider", - help="Specify the cloud provider (aws or azure), or `config`.", + help="Specify the cloud provider (aws or azure), `config` or `catalogue`.", ) # Shared options available on every subcommand (e.g. `aws --verbose`) @@ -988,9 +1002,19 @@ def parse_arguments(): "--timeout", type=float, help="Platform request timeout in seconds." ) + # Run with a catalogue file instead of the installed one (not installed) + catalogue_flag = argparse.ArgumentParser(add_help=False) + catalogue_flag.add_argument( + "--catalogue", + type=str, + help="Use this catalogue file (.db or .db.gz) for the run without installing it.", + ) + # Subparser for AWS aws_parser = subparsers.add_parser( - "aws", parents=[common, platform], help="Perform an AWS assessment." + "aws", + parents=[common, platform, catalogue_flag], + help="Perform an AWS assessment.", ) aws_group = aws_parser.add_mutually_exclusive_group(required=False) aws_group.add_argument( @@ -1035,7 +1059,9 @@ def parse_arguments(): # Subparser for Azure azure_parser = subparsers.add_parser( - "azure", parents=[common, platform], help="Perform an Azure assessment." + "azure", + parents=[common, platform, catalogue_flag], + help="Perform an Azure assessment.", ) azure_group = azure_parser.add_mutually_exclusive_group(required=False) azure_group.add_argument( @@ -1093,9 +1119,107 @@ def parse_arguments(): help="Show every resolved setting and where it came from.", ) + # Subparser for the catalogue dataset + catalogue_parser = subparsers.add_parser( + "catalogue", help="Manage the catalogue dataset." + ) + catalogue_commands = catalogue_parser.add_subparsers( + dest="catalogue_command", required=True + ) + catalogue_commands.add_parser( + "status", parents=[common], help="Show the installed and the latest catalogue." + ) + update_parser = catalogue_commands.add_parser( + "update", parents=[common], help="Download the latest (or a given) catalogue." + ) + update_parser.add_argument( + "--version", type=str, help="Install this version listed in the manifest." + ) + import_parser = catalogue_commands.add_parser( + "import", parents=[common], help="Install a catalogue file (air-gapped use)." + ) + import_parser.add_argument("file", type=str, help="Path to a .db.gz (or .db) file.") + import_parser.add_argument( + "--sha256", type=str, help="Expected SHA-256 of the file." + ) + return parser.parse_args() +def _catalogue_settings(): + try: + settings = load_settings() + return catalogue_base_url(settings), settings.ca_bundle.value or True + except SettingsError as e: + console.print(f"[red]{escape(str(e))}[/red]") + sys.exit(codes.CONFIG) + + +def prepare_catalogue(args): + settings = getattr(args, "settings", None) + + if getattr(args, "catalogue", None): + try: + path, info = use_catalogue_file(args.catalogue) + except CatalogueError as e: + console.print(f"[red]{escape(str(e))}[/red]") + sys.exit(codes.CONFIG) + set_active_catalogue(path) + print_step( + f"Using catalogue {info.version if info else 'unknown'} from " + f"{os.path.basename(args.catalogue)} (not installed).", + status="ok", + ) + return + + try: + base_url = catalogue_base_url(settings or load_settings()) + result = ensure_catalogue( + base_url, verify=(settings.ca_bundle.value if settings else None) or True + ) + except SettingsError as e: + console.print(f"[red]{escape(str(e))}[/red]") + sys.exit(codes.CONFIG) + except CatalogueUnavailable as e: + console.print(f"[red]{escape(str(e))}[/red]") + sys.exit(codes.CONFIG) + + version = result.version or "unknown" + if result.migrated: + print_step("Catalogue copied from ./datasets to the data directory.", "ok") + if result.status == "failed": + print_step( + f"Catalogue update failed, using {version} from {result.date}.", + status="warning", + logs=result.reason, + ) + elif result.status == "updated": + print_step(f"Catalogue updated to {version}.", status="ok") + else: + print_step(f"Catalogue {version} is up to date.", status="ok") + + +def handle_catalogue(args): + base_url, verify = _catalogue_settings() + try: + if args.catalogue_command == "status": + for line in status_lines(base_url, verify=verify): + print(line) + elif args.catalogue_command == "update": + result = update_catalogue(base_url, version=args.version, verify=verify) + version = result.info.version if result.info else "unknown" + if result.status == "current": + print(f"Catalogue {version} is already installed.") + else: + print(f"Catalogue updated to {version}.") + else: + info = import_catalogue(args.file, sha256=args.sha256) + print(f"Catalogue {info.version if info else 'unknown'} installed.") + except (CatalogueError, OSError) as e: + console.print(f"[red]{escape(str(e))}[/red]") + sys.exit(codes.CONFIG) + + def handle_config(args): if args.config_command == "path": print(config_file_path()) @@ -1121,10 +1245,13 @@ def main(): # Ctrl+C anywhere below exits with a distinct code, whether the CLI is run # as `python3 main.py` or through the `cloudexit` console script. try: - # The config helpers print plain text and need no dataset. + # The config and catalogue helpers print plain text and need no banner. if args.cloud_provider == "config": handle_config(args) return + if args.cloud_provider == "catalogue": + handle_catalogue(args) + return # Print ASCII art console.print(ascii_art, style="bold cyan") @@ -1142,8 +1269,8 @@ def main(): console.print(f"[red]{escape(str(e))}[/red]") sys.exit(codes.CONFIG) - # Ensure latest dataset is available before proceeding - initialize_dataset() + # Make sure a catalogue is available before any work starts + prepare_catalogue(args) # Dispatch based on provided arguments try: diff --git a/tests/catalogue_fixtures.py b/tests/catalogue_fixtures.py new file mode 100644 index 0000000..c605671 --- /dev/null +++ b/tests/catalogue_fixtures.py @@ -0,0 +1,98 @@ +import gzip +import hashlib +import sqlite3 +import tempfile +from pathlib import Path + +RESOURCE_TYPES = [ + (1, 2, "AWS.EC2.DescribeInstances", "EC2 Instance", "/i/ec2.png", "t"), + (2, 2, "AWS.S3.ListBuckets", "S3 Bucket", "/i/s3.png", "f"), + (3, 1, "Azure.Compute.VirtualMachines", "Virtual Machine", "/i/vm.png", "t"), + (4, 2, "AWS.RDS.DescribeDBInstances", "RDS Instance", "/i/rds.png", "t"), + (5, 1, "Azure.Storage.Accounts", "Storage Account", "/i/sa.png", "f"), +] + + +def build_catalogue( + path, + *, + version="2026-11-02", + sequence=41, + meta=True, + meta_rows=1, + schema=1, + core_tables=True, + extras=False, +): + conn = sqlite3.connect(path) + if core_tables: + conn.executescript(""" + CREATE TABLE resourcetype ( + id INTEGER PRIMARY KEY, csp INTEGER NOT NULL, code TEXT NOT NULL, + name TEXT NOT NULL, icon TEXT NOT NULL, status TEXT NOT NULL, + tf_code TEXT + ); + CREATE TABLE risk (id INTEGER PRIMARY KEY, name TEXT NOT NULL); + """) + conn.executemany( + "INSERT INTO resourcetype (id, csp, code, name, icon, status) " + "VALUES (?, ?, ?, ?, ?, ?)", + RESOURCE_TYPES, + ) + conn.execute("INSERT INTO risk (id, name) VALUES (1, 'Example risk')") + if meta: + conn.execute(""" + CREATE TABLE catalogue_meta ( + catalogue_version TEXT, catalogue_sequence INTEGER, + catalogue_sha256 TEXT, dataset_schema INTEGER, exported_at TEXT, + exporter_version TEXT, a_future_column TEXT + ) + """) + for _ in range(meta_rows): + conn.execute( + "INSERT INTO catalogue_meta VALUES (?, ?, ?, ?, ?, ?, ?)", + ( + version, + sequence, + "c" * 64, + schema, + f"{version[:10]}T05:00:00Z", + "1.0.0", + "ignored", + ), + ) + if extras: + conn.execute("CREATE TABLE a_future_table (id INTEGER, note TEXT)") + conn.commit() + conn.close() + + +def catalogue_bytes(**kwargs): + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "build.db" + build_catalogue(path, **kwargs) + return path.read_bytes() + + +def catalogue_gz(**kwargs): + return gzip.compress(catalogue_bytes(**kwargs)) + + +def sha256(data): + return hashlib.sha256(data).hexdigest() + + +def manifest_entry(version, gz, *, sequence=41, file=None): + return { + "catalogue_version": version, + "catalogue_sequence": sequence, + "catalogue_sha256": "c" * 64, + "file": file or f"releases/cloudexit-{version}.db.gz", + "file_sha256": sha256(gz), + "size_bytes": len(gz), + "published_at": f"{version}T05:10:00Z", + } + + +def manifest(latest, previous=(), schema=1): + return {"dataset_schema": schema, "latest": latest, "previous": list(previous)} diff --git a/tests/http_fixtures.py b/tests/http_fixtures.py index 7097dd9..8017be0 100644 --- a/tests/http_fixtures.py +++ b/tests/http_fixtures.py @@ -8,12 +8,14 @@ RecordedRequest = namedtuple("RecordedRequest", "method url kwargs") -def make_response(status=200, json_body=None, text="", headers=None): +def make_response(status=200, json_body=None, text="", headers=None, content=None): response = requests.Response() response.status_code = status response.encoding = "utf-8" response.headers.update(headers or {}) - if json_body is not None: + if content is not None: + response._content = content + elif json_body is not None: response._content = json.dumps(json_body).encode("utf-8") response.headers.setdefault("Content-Type", "application/json") else: diff --git a/tests/test_catalogue.py b/tests/test_catalogue.py new file mode 100644 index 0000000..4410fe4 --- /dev/null +++ b/tests/test_catalogue.py @@ -0,0 +1,788 @@ +import contextlib +import gzip +import io +import json +import os +import sqlite3 +import tempfile +import unittest +from argparse import Namespace +from pathlib import Path +from unittest.mock import patch + +import requests + +import main +from core import utils_db +from tests.catalogue_fixtures import ( + build_catalogue, + catalogue_bytes, + catalogue_gz, + manifest, + manifest_entry, + sha256, +) +from tests.http_fixtures import fake_http, make_response +from utils import codes +from utils.data import ( + CATALOGUE_BASE_URL, + CatalogueError, + CatalogueUnavailable, + EnsureResult, + active_catalogue_path, + active_resource_type_ids, + catalogue_base_url, + catalogue_dir, + catalogue_info, + ensure_catalogue, + import_catalogue, + installed_catalogue, + migrate_legacy_dataset, + read_state, + resolve_file_url, + set_active_catalogue, + status_lines, + update_catalogue, + use_catalogue_file, +) +from utils.settings import Setting, Settings, SettingsError + +BASE = "https://catalogue.example.test/mirror" +MANIFEST_URL = f"{BASE}/cloudexit-manifest.json" +V1, V2, V3 = "2026-10-26", "2026-11-02", "2026-11-09" + + +class CatalogueTestCase(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.tmp = Path(tmp.name) + self.dir = self.tmp / "catalogue" + self.addCleanup(set_active_catalogue, None) + patcher = patch( + "utils.data.LEGACY_DATABASE", self.tmp / "no-legacy" / "data.db" + ) + patcher.start() + self.addCleanup(patcher.stop) + + def install(self, version=V1, **kwargs): + gz = catalogue_gz(version=version, **kwargs) + path = self.tmp / f"{version}.db.gz" + path.write_bytes(gz) + import_catalogue(path, directory=self.dir) + return gz + + def release(self, version, **kwargs): + gz = catalogue_gz(version=version, **kwargs) + return gz, manifest_entry(version, gz) + + def manifest_reply(self, latest, previous=(), **kwargs): + return make_response(json_body=manifest(latest, previous, **kwargs)) + + def version(self): + installed = installed_catalogue(self.dir) + return installed.version if installed else None + + +class ManifestUpdateTests(CatalogueTestCase): + def test_installs_the_latest_listed_in_the_manifest_below_the_base_path(self): + gz, entry = self.release(V2) + with fake_http(self.manifest_reply(entry), make_response(content=gz)) as fake: + result = update_catalogue(BASE, directory=self.dir) + + self.assertEqual(result.status, "updated") + self.assertEqual(result.info.version, V2) + self.assertEqual(self.version(), V2) + self.assertEqual( + [request.url for request in fake.requests], + [MANIFEST_URL, f"{BASE}/releases/cloudexit-{V2}.db.gz"], + ) + self.assertEqual(fake.requests[0].kwargs["timeout"], 10) + state = read_state(self.dir) + self.assertEqual(state["version"], V2) + self.assertEqual(state["sha256"], entry["file_sha256"]) + self.assertIn("downloaded_at", state) + + def test_unchanged_version_downloads_nothing(self): + self.install(V2) + _, entry = self.release(V2) + with fake_http(self.manifest_reply(entry)) as fake: + result = update_catalogue(BASE, directory=self.dir) + + self.assertEqual(result.status, "current") + self.assertEqual(len(fake.requests), 1) + + def test_checksum_mismatch_is_rejected_and_the_old_dataset_kept(self): + self.install(V1) + before = (self.dir / "data.db").read_bytes() + gz, entry = self.release(V2) + entry["file_sha256"] = "0" * 64 + with fake_http(self.manifest_reply(entry), make_response(content=gz)): + with self.assertRaises(CatalogueError) as ctx: + update_catalogue(BASE, directory=self.dir) + + self.assertIn("checksum mismatch", str(ctx.exception)) + self.assertEqual((self.dir / "data.db").read_bytes(), before) + self.assertEqual(self.version(), V1) + self.assertEqual( + sorted(path.name for path in self.dir.iterdir()), + ["data.db", "state.json"], + ) + + def test_a_version_that_differs_from_the_manifest_is_rejected(self): + self.install(V1) + gz = catalogue_gz(version=V3) + entry = manifest_entry(V2, gz) + with fake_http(self.manifest_reply(entry), make_response(content=gz)): + with self.assertRaises(CatalogueError): + update_catalogue(BASE, directory=self.dir) + self.assertEqual(self.version(), V1) + + def test_a_download_without_catalogue_meta_is_rejected(self): + gz = catalogue_gz(meta=False) + entry = manifest_entry(V2, gz) + with fake_http(self.manifest_reply(entry), make_response(content=gz)): + with self.assertRaises(CatalogueError): + update_catalogue(BASE, directory=self.dir) + self.assertIsNone(installed_catalogue(self.dir)) + + def test_catalogue_meta_must_have_exactly_one_row(self): + gz = catalogue_gz(version=V2, meta_rows=2) + entry = manifest_entry(V2, gz) + with fake_http(self.manifest_reply(entry), make_response(content=gz)): + with self.assertRaises(CatalogueError) as ctx: + update_catalogue(BASE, directory=self.dir) + self.assertIn("exactly one row", str(ctx.exception)) + + def test_the_swap_keeps_the_previous_dataset(self): + self.install(V1) + old = (self.dir / "data.db").read_bytes() + gz, entry = self.release(V2) + with fake_http(self.manifest_reply(entry), make_response(content=gz)): + update_catalogue(BASE, directory=self.dir) + + self.assertEqual((self.dir / "data.db.previous").read_bytes(), old) + self.assertEqual(catalogue_info(self.dir / "data.db.previous").version, V1) + self.assertEqual(self.version(), V2) + + def test_a_listed_older_version_can_be_installed(self): + self.install(V3) + gz1, entry1 = self.release(V1) + _, entry3 = self.release(V3) + with fake_http( + self.manifest_reply(entry3, [entry1]), make_response(content=gz1) + ): + result = update_catalogue(BASE, directory=self.dir, version=V1) + + self.assertEqual(result.info.version, V1) + + def test_an_unlisted_version_lists_the_available_ones(self): + _, entry = self.release(V2) + with fake_http(self.manifest_reply(entry)): + with self.assertRaises(CatalogueError) as ctx: + update_catalogue(BASE, directory=self.dir, version="2020-01-01") + self.assertIn(V2, str(ctx.exception)) + + def test_a_file_outside_the_catalogue_location_is_refused(self): + for file in ( + "https://elsewhere.example.test/x.db.gz", + "../other/x.db.gz", + "//elsewhere.example.test/x.db.gz", + ): + with self.subTest(file=file): + with self.assertRaises(CatalogueError): + resolve_file_url(MANIFEST_URL, file) + + def test_a_newer_dataset_schema_asks_for_a_cloudexit_update(self): + _, entry = self.release(V2) + with fake_http(self.manifest_reply(entry, schema=2)): + with self.assertRaises(CatalogueError) as ctx: + update_catalogue(BASE, directory=self.dir) + self.assertIn("Update cloudexit", str(ctx.exception)) + + def test_an_unreachable_server_is_an_error_not_a_fallback(self): + with fake_http(requests.ConnectionError("offline")) as fake: + with self.assertRaises(CatalogueError): + update_catalogue(BASE, directory=self.dir) + self.assertEqual(len(fake.requests), 1) + + +class LatestFallbackTests(CatalogueTestCase): + def test_without_a_manifest_the_latest_file_is_used(self): + gz = catalogue_gz(version=V2) + with fake_http( + make_response(status=404), + make_response(text=f"{sha256(gz)} cloudexit-latest.db.gz\n"), + make_response(content=gz), + ) as fake: + result = update_catalogue(BASE, directory=self.dir) + + self.assertEqual(result.status, "updated") + self.assertEqual( + [request.url for request in fake.requests], + [ + MANIFEST_URL, + f"{BASE}/cloudexit-latest.db.gz.sha256", + f"{BASE}/cloudexit-latest.db.gz", + ], + ) + self.assertEqual(self.version(), V2) + + def test_an_unchanged_checksum_downloads_nothing(self): + gz = catalogue_gz(version=V2) + reply = make_response(text=sha256(gz)) + with fake_http(make_response(status=404), reply, make_response(content=gz)): + update_catalogue(BASE, directory=self.dir) + with fake_http(make_response(status=404), make_response(text=sha256(gz))): + result = update_catalogue(BASE, directory=self.dir) + self.assertEqual(result.status, "current") + + def test_a_dataset_without_catalogue_meta_installs_as_version_unknown(self): + gz = catalogue_gz(meta=False) + with fake_http( + make_response(status=404), + make_response(text=sha256(gz)), + make_response(content=gz), + ): + result = update_catalogue(BASE, directory=self.dir) + + self.assertIsNone(result.info) + installed = installed_catalogue(self.dir) + self.assertIsNotNone(installed) + self.assertIsNone(installed.version) + self.assertEqual(active_resource_type_ids("aws", installed.path), [1, 4]) + + def test_nothing_published_is_an_error(self): + with fake_http(make_response(status=404), make_response(status=404)): + with self.assertRaises(CatalogueError) as ctx: + update_catalogue(BASE, directory=self.dir) + self.assertIn("no catalogue manifest or latest file", str(ctx.exception)) + + def test_a_version_cannot_be_chosen_without_a_manifest(self): + with fake_http(make_response(status=404)): + with self.assertRaises(CatalogueError): + update_catalogue(BASE, directory=self.dir, version=V1) + + def test_a_checksum_mismatch_is_rejected(self): + gz = catalogue_gz(version=V2) + with fake_http( + make_response(status=404), + make_response(text="0" * 64), + make_response(content=gz), + ): + with self.assertRaises(CatalogueError): + update_catalogue(BASE, directory=self.dir) + self.assertIsNone(installed_catalogue(self.dir)) + + +class CatalogueInfoTests(CatalogueTestCase): + def test_reads_catalogue_meta(self): + path = self.tmp / "data.db" + build_catalogue(path, version=V2, sequence=41) + + info = catalogue_info(path) + + self.assertEqual(info.version, V2) + self.assertEqual(info.sequence, 41) + self.assertEqual(info.sha256, "c" * 64) + self.assertEqual(info.dataset_schema, 1) + + def test_an_old_dataset_has_no_info(self): + path = self.tmp / "data.db" + build_catalogue(path, meta=False) + self.assertIsNone(catalogue_info(path)) + + def test_extra_tables_and_columns_are_tolerated(self): + path = self.tmp / "data.db" + build_catalogue(path, extras=True) + self.assertEqual(catalogue_info(path).version, V2) + self.assertEqual(active_resource_type_ids("azure", path), [3]) + + def test_active_resource_type_ids_filter_by_provider_and_status(self): + path = self.tmp / "data.db" + build_catalogue(path) + + self.assertEqual(active_resource_type_ids("aws", path), [1, 4]) + self.assertEqual(active_resource_type_ids("azure", path), [3]) + self.assertEqual(active_resource_type_ids(2, path), [1, 4]) + self.assertEqual(active_resource_type_ids("AWS", path), [1, 4]) + with self.assertRaises(ValueError): + active_resource_type_ids("gcp", path) + + def test_the_active_dataset_is_the_default(self): + path = self.tmp / "data.db" + build_catalogue(path, version=V3) + set_active_catalogue(path) + + self.assertEqual(active_catalogue_path(), path) + self.assertEqual(catalogue_info().version, V3) + self.assertEqual(active_resource_type_ids("azure"), [3]) + self.assertEqual(utils_db.load_data("risk")[0]["name"], "Example risk") + + +class ImportTests(CatalogueTestCase): + def test_imports_a_good_file(self): + path = self.tmp / "catalogue.db.gz" + gz = catalogue_gz(version=V2) + path.write_bytes(gz) + + info = import_catalogue(path, directory=self.dir, sha256=sha256(gz)) + + self.assertEqual(info.version, V2) + self.assertEqual(self.version(), V2) + self.assertEqual(read_state(self.dir)["sha256"], sha256(gz)) + + def test_imports_a_plain_database_and_an_old_one(self): + plain = self.tmp / "plain.db" + plain.write_bytes(catalogue_bytes(meta=False)) + + self.assertIsNone(import_catalogue(plain, directory=self.dir)) + self.assertIsNone(self.version()) + self.assertIsNotNone(installed_catalogue(self.dir)) + + def test_a_wrong_checksum_is_rejected(self): + path = self.tmp / "catalogue.db.gz" + path.write_bytes(catalogue_gz()) + with self.assertRaises(CatalogueError): + import_catalogue(path, directory=self.dir, sha256="0" * 64) + self.assertIsNone(installed_catalogue(self.dir)) + + def test_corrupt_files_are_rejected_and_nothing_is_installed(self): + self.install(V1) + before = (self.dir / "data.db").read_bytes() + good = catalogue_gz(version=V2) + samples = { + "truncated gzip": good[: len(good) // 2], + "not a database": gzip.compress(b"this is not sqlite"), + "random bytes": os.urandom(256), + "missing tables": gzip.compress(catalogue_bytes(core_tables=False)), + "two meta rows": catalogue_gz(meta_rows=2), + "newer schema": catalogue_gz(schema=2), + } + for name, data in samples.items(): + with self.subTest(name): + path = self.tmp / "bad.db.gz" + path.write_bytes(data) + with self.assertRaises(CatalogueError): + import_catalogue(path, directory=self.dir) + self.assertEqual((self.dir / "data.db").read_bytes(), before) + + def test_a_missing_file_is_an_error(self): + with self.assertRaises(CatalogueError): + import_catalogue(self.tmp / "missing.db.gz", directory=self.dir) + + +class UseCatalogueFileTests(CatalogueTestCase): + def test_a_file_is_used_without_touching_the_installed_dataset(self): + self.install(V1) + before = (self.dir / "data.db").read_bytes() + state = read_state(self.dir) + path = self.tmp / "other.db.gz" + path.write_bytes(catalogue_gz(version=V3)) + + used, info = use_catalogue_file(path) + + self.assertEqual(info.version, V3) + self.assertNotEqual(used, self.dir / "data.db") + self.assertEqual(catalogue_info(used).version, V3) + self.assertEqual((self.dir / "data.db").read_bytes(), before) + self.assertEqual(read_state(self.dir), state) + self.assertFalse((self.dir / "data.db.previous").exists()) + + def test_a_plain_database_is_used_in_place(self): + path = self.tmp / "plain.db" + build_catalogue(path, meta=False) + used, info = use_catalogue_file(path) + self.assertEqual(used, path) + self.assertIsNone(info) + + def test_an_invalid_file_is_an_error(self): + path = self.tmp / "bad.db" + path.write_bytes(b"nope") + with self.assertRaises(CatalogueError): + use_catalogue_file(path) + + +class MigrationTests(CatalogueTestCase): + def legacy(self, **kwargs): + path = self.tmp / "datasets" / "data.db" + path.parent.mkdir() + build_catalogue(path, **kwargs) + return path + + def test_copies_the_legacy_dataset_once(self): + legacy = self.legacy(version=V2) + + self.assertTrue(migrate_legacy_dataset(self.dir, legacy)) + + self.assertEqual(self.version(), V2) + self.assertEqual(read_state(self.dir)["version"], V2) + self.assertEqual((self.dir / "data.db").read_bytes(), legacy.read_bytes()) + self.assertFalse(migrate_legacy_dataset(self.dir, legacy)) + + def test_does_not_overwrite_an_installed_dataset(self): + self.install(V3) + legacy = self.legacy(version=V1) + + self.assertFalse(migrate_legacy_dataset(self.dir, legacy)) + self.assertEqual(self.version(), V3) + + def test_an_old_or_invalid_legacy_file(self): + self.assertTrue(migrate_legacy_dataset(self.dir, self.legacy(meta=False))) + self.assertIsNone(self.version()) + + other = self.tmp / "other" + bad = self.tmp / "bad.db" + bad.write_bytes(b"nope") + self.assertFalse(migrate_legacy_dataset(other, bad)) + self.assertFalse(migrate_legacy_dataset(other, self.tmp / "missing.db")) + + def test_ensure_reports_the_migration(self): + legacy = self.legacy(version=V2) + _, entry = self.release(V2) + with ( + patch("utils.data.LEGACY_DATABASE", legacy), + fake_http(self.manifest_reply(entry)), + ): + result = ensure_catalogue(BASE, directory=self.dir) + + self.assertTrue(result.migrated) + self.assertEqual(result.status, "current") + + +class EnsureCatalogueTests(CatalogueTestCase): + def test_a_failed_update_keeps_the_local_dataset(self): + self.install(V1) + with fake_http(requests.ConnectionError("offline")): + result = ensure_catalogue(BASE, directory=self.dir) + + self.assertEqual(result.status, "failed") + self.assertEqual(result.version, V1) + self.assertEqual(result.path, self.dir / "data.db") + self.assertIn("offline", result.reason) + self.assertRegex(result.date, r"^\d{4}-\d\d-\d\d$") + + def test_a_failed_update_with_an_unknown_version_dataset(self): + self.install(meta=False) + with fake_http(requests.ConnectionError("offline")): + result = ensure_catalogue(BASE, directory=self.dir) + self.assertEqual(result.status, "failed") + self.assertIsNone(result.version) + + def test_a_failed_update_without_any_dataset_is_unavailable(self): + with fake_http(requests.ConnectionError("offline")): + with self.assertRaises(CatalogueUnavailable): + ensure_catalogue(BASE, directory=self.dir) + + def test_a_server_error_does_not_stop_a_run_with_a_dataset(self): + self.install(V1) + with fake_http(make_response(status=503)): + result = ensure_catalogue(BASE, directory=self.dir) + self.assertEqual(result.status, "failed") + + def test_updates_and_reports_the_new_version(self): + self.install(V1) + gz, entry = self.release(V2) + with fake_http(self.manifest_reply(entry), make_response(content=gz)): + result = ensure_catalogue(BASE, directory=self.dir) + self.assertEqual((result.status, result.version), ("updated", V2)) + + +class StatusTests(CatalogueTestCase): + def test_shows_installed_and_latest(self): + self.install(V1) + _, entry = self.release(V2) + with fake_http(self.manifest_reply(entry)): + text = "\n".join(status_lines(BASE, directory=self.dir)) + + self.assertIn(f"installed: {V1} (sequence 41)", text) + self.assertIn("(today)", text) + self.assertIn(f"latest: {V2} (sequence 41)", text) + self.assertIn("an update is available", text) + + def test_shows_how_old_the_installed_dataset_is(self): + self.install(V1) + state = read_state(self.dir) + state["downloaded_at"] = "2020-01-01T00:00:00+00:00" + (self.dir / "state.json").write_text(json.dumps(state)) + with fake_http(make_response(status=404)): + text = "\n".join(status_lines(BASE, directory=self.dir)) + self.assertIn("2020-01-01 (", text) + self.assertIn("days ago)", text) + + def test_nothing_installed_and_no_manifest(self): + with fake_http(make_response(status=404)): + text = "\n".join(status_lines(BASE, directory=self.dir)) + self.assertIn("installed: none", text) + self.assertIn("no manifest is published", text) + + def test_unknown_version_and_unreachable_server(self): + self.install(meta=False) + with fake_http(requests.ConnectionError("offline")): + text = "\n".join(status_lines(BASE, directory=self.dir)) + self.assertIn("unknown (the dataset has no catalogue_meta)", text) + self.assertIn("unavailable", text) + + +class LocationTests(unittest.TestCase): + def test_xdg_data_home_and_defaults(self): + self.assertEqual( + catalogue_dir({"XDG_DATA_HOME": "/xdg"}, platform="linux"), + Path("/xdg/cloudexit/catalogue"), + ) + self.assertEqual( + catalogue_dir({}, platform="linux"), + Path.home() / ".local" / "share" / "cloudexit" / "catalogue", + ) + self.assertEqual( + catalogue_dir({"XDG_DATA_HOME": "relative"}, platform="linux"), + Path.home() / ".local" / "share" / "cloudexit" / "catalogue", + ) + + def test_localappdata_on_windows(self): + self.assertEqual( + catalogue_dir({"LOCALAPPDATA": "/local"}, platform="win32"), + Path("/local/cloudexit/catalogue"), + ) + + def test_the_base_url_defaults_and_can_be_overridden(self): + self.assertEqual(catalogue_base_url(Settings()), CATALOGUE_BASE_URL) + override = Settings( + catalogue_url=Setting(BASE + "/", "env CLOUDEXIT_CATALOGUE_URL") + ) + self.assertEqual(catalogue_base_url(override), BASE) + + def test_a_plain_http_base_url_is_refused_except_for_localhost(self): + remote = Settings(catalogue_url=Setting("http://example.com", "env X")) + with self.assertRaises(SettingsError): + catalogue_base_url(remote) + local = Settings(catalogue_url=Setting("http://localhost:9000/m", "env X")) + self.assertEqual(catalogue_base_url(local), "http://localhost:9000/m") + + +class RunIntegrationTests(CatalogueTestCase): + def args(self, **kwargs): + return Namespace(settings=Settings(), catalogue=None, **kwargs) + + def ensure(self, **kwargs): + defaults = dict( + path=Path("/x/data.db"), status="current", version=V2, date="2026-11-03" + ) + defaults.update(kwargs) + return patch("main.ensure_catalogue", return_value=EnsureResult(**defaults)) + + def test_a_failed_update_is_a_visible_step_result(self): + with ( + self.ensure(status="failed", version=V1, reason="cannot reach server"), + patch("main.print_step") as mock_step, + ): + main.prepare_catalogue(self.args()) + + mock_step.assert_called_once_with( + f"Catalogue update failed, using {V1} from 2026-11-03.", + status="warning", + logs="cannot reach server", + ) + + def test_a_failed_update_with_an_unknown_version_says_so(self): + with ( + self.ensure(status="failed", version=None, reason="offline"), + patch("main.print_step") as mock_step, + ): + main.prepare_catalogue(self.args()) + self.assertIn("using unknown from 2026-11-03", mock_step.call_args.args[0]) + + def test_up_to_date_and_updated_steps(self): + for status, text in ( + ("current", f"Catalogue {V2} is up to date."), + ("updated", f"Catalogue updated to {V2}."), + ): + with self.subTest(status): + with self.ensure(status=status), patch("main.print_step") as step: + main.prepare_catalogue(self.args()) + step.assert_called_once_with(text, status="ok") + + def test_without_any_dataset_the_run_stops_with_the_config_code(self): + with ( + patch( + "main.ensure_catalogue", side_effect=CatalogueUnavailable("no dataset") + ), + patch("main.console.print") as mock_print, + ): + with self.assertRaises(SystemExit) as ctx: + main.prepare_catalogue(self.args()) + + self.assertEqual(ctx.exception.code, codes.CONFIG) + self.assertIn("no dataset", mock_print.call_args.args[0]) + + def test_catalogue_flag_uses_the_file_without_updating_or_installing(self): + self.install(V1) + before = (self.dir / "data.db").read_bytes() + path = self.tmp / "run.db.gz" + path.write_bytes(catalogue_gz(version=V3)) + args = Namespace(settings=Settings(), catalogue=str(path)) + + with ( + patch("main.ensure_catalogue") as mock_ensure, + patch("main.print_step") as mock_step, + ): + main.prepare_catalogue(args) + + mock_ensure.assert_not_called() + self.assertIn(V3, mock_step.call_args.args[0]) + self.assertIn("not installed", mock_step.call_args.args[0]) + self.assertEqual(catalogue_info().version, V3) + self.assertEqual((self.dir / "data.db").read_bytes(), before) + + def test_a_bad_catalogue_flag_stops_the_run(self): + args = Namespace(settings=Settings(), catalogue=str(self.tmp / "missing.db")) + with patch("main.console.print"), self.assertRaises(SystemExit) as ctx: + main.prepare_catalogue(args) + self.assertEqual(ctx.exception.code, codes.CONFIG) + + def test_the_report_copy_uses_the_dataset_the_run_used(self): + path = self.tmp / "run.db" + build_catalogue(path, version=V3) + set_active_catalogue(path) + report = self.tmp / "report" + (report / "data").mkdir(parents=True) + + with patch("core.utils.shutil.copytree"): + from core.utils import copy_assets + + copy_assets(str(report), 2) + + copied = report / "data" / "assessment.db" + self.assertEqual(catalogue_info(copied).version, V3) + + +class CatalogueCommandTests(CatalogueTestCase): + def run_command(self, *argv, replies=()): + out = io.StringIO() + environ = {"XDG_DATA_HOME": str(self.tmp / "data")} + with ( + patch("sys.argv", ["cloudexit", "catalogue", *argv]), + patch.dict(os.environ, environ, clear=True), + patch( + "main.load_settings", + return_value=Settings( + catalogue_url=Setting(BASE, "env CLOUDEXIT_CATALOGUE_URL") + ), + ), + patch("main.prepare_catalogue") as mock_prepare, + patch("main.console.print") as mock_print, + fake_http(*replies) as fake, + contextlib.redirect_stdout(out), + ): + try: + main.main() + code = None + except SystemExit as exc: + code = exc.code + mock_prepare.assert_not_called() + return out.getvalue(), code, fake, mock_print + + @property + def command_dir(self): + return self.tmp / "data" / "cloudexit" / "catalogue" + + def test_update_installs_the_latest(self): + gz, entry = self.release(V2) + out, code, fake, _ = self.run_command( + "update", + replies=[self.manifest_reply(entry), make_response(content=gz)], + ) + + self.assertIsNone(code) + self.assertIn(f"Catalogue updated to {V2}.", out) + self.assertEqual(catalogue_info(self.command_dir / "data.db").version, V2) + + def test_update_with_a_version(self): + gz1, entry1 = self.release(V1) + _, entry2 = self.release(V2) + out, code, _, _ = self.run_command( + "update", + "--version", + V1, + replies=[ + self.manifest_reply(entry2, [entry1]), + make_response(content=gz1), + ], + ) + self.assertIsNone(code) + self.assertIn(V1, out) + + def test_update_failure_exits_with_the_config_code(self): + out, code, _, mock_print = self.run_command( + "update", replies=[requests.ConnectionError("offline")] + ) + self.assertEqual(code, codes.CONFIG) + self.assertIn("offline", mock_print.call_args.args[0]) + + def test_status_prints_the_installed_and_latest_catalogue(self): + _, entry = self.release(V2) + out, code, _, _ = self.run_command( + "status", replies=[self.manifest_reply(entry)] + ) + self.assertIsNone(code) + self.assertIn("installed: none", out) + self.assertIn(f"latest: {V2}", out) + + def test_import_installs_a_file(self): + path = self.tmp / "offline.db.gz" + gz = catalogue_gz(version=V2) + path.write_bytes(gz) + out, code, fake, _ = self.run_command( + "import", str(path), "--sha256", sha256(gz) + ) + + self.assertIsNone(code) + self.assertEqual(fake.requests, []) + self.assertIn(f"Catalogue {V2} installed.", out) + + def test_import_of_a_corrupt_file_fails(self): + path = self.tmp / "bad.db.gz" + path.write_bytes(b"\x1f\x8b broken") + _, code, _, _ = self.run_command("import", str(path)) + self.assertEqual(code, codes.CONFIG) + self.assertFalse((self.command_dir / "data.db").exists()) + + def test_the_subcommand_is_required(self): + with ( + patch("sys.argv", ["cloudexit", "catalogue"]), + contextlib.redirect_stderr(io.StringIO()), + self.assertRaises(SystemExit), + ): + main.parse_arguments() + + def test_the_run_flag_is_accepted_on_aws_and_azure(self): + for provider in ("aws", "azure"): + with patch("sys.argv", ["cloudexit", provider, "--catalogue", "f.db.gz"]): + self.assertEqual(main.parse_arguments().catalogue, "f.db.gz") + + +class DatabaseContractTests(CatalogueTestCase): + def test_a_corrupt_state_file_is_ignored(self): + self.install(V1) + (self.dir / "state.json").write_text("not json") + self.assertEqual(read_state(self.dir), {}) + self.assertEqual(self.version(), V1) + + def test_installed_catalogue_ignores_an_empty_file(self): + self.dir.mkdir() + (self.dir / "data.db").write_bytes(b"") + self.assertIsNone(installed_catalogue(self.dir)) + + def test_the_catalogue_database_is_opened_read_only_for_info(self): + path = self.tmp / "data.db" + build_catalogue(path) + before = path.read_bytes() + catalogue_info(path) + active_resource_type_ids("aws", path) + self.assertEqual(path.read_bytes(), before) + with sqlite3.connect(path) as conn: + self.assertEqual(conn.execute("PRAGMA integrity_check").fetchone()[0], "ok") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_copy_assets.py b/tests/test_copy_assets.py index 07cc1bc..63d6499 100644 --- a/tests/test_copy_assets.py +++ b/tests/test_copy_assets.py @@ -7,13 +7,14 @@ from unittest import mock from core.utils import copy_assets +from utils.data import active_catalogue_path SOURCE_ICONS = Path("assets/icons") @contextmanager def staged(cloud_service_provider): - # datasets/data.db is downloaded at runtime and is absent in CI, so the + # The catalogue is downloaded at runtime and is absent in CI, so the # assessment.db copy is stubbed out -- it isn't what these tests cover. # Replace the module binding inside core.utils rather than the attribute # on shutil itself: copytree calls copyfile internally, so patching it @@ -101,7 +102,7 @@ def test_assessment_db_is_still_copied(self): with staged(1) as (report_path, copyfile): self.assertTrue((report_path / "data").is_dir()) copyfile.assert_called_once_with( - "datasets/data.db", + str(active_catalogue_path()), os.path.join(str(report_path), "data", "assessment.db"), ) diff --git a/tests/test_settings.py b/tests/test_settings.py index f820a7c..036eecf 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -468,7 +468,7 @@ def run_main(self, argv, env=None): with ( patch("sys.argv", ["cloudexit", *argv]), patch.dict(os.environ, environ, clear=True), - patch("main.initialize_dataset") as mock_dataset, + patch("main.prepare_catalogue") as mock_dataset, patch("main.console.print") as mock_print, contextlib.redirect_stdout(out), ): @@ -556,7 +556,7 @@ def test_main_resolves_settings_before_the_dataset_download(self): with ( patch("main.parse_arguments", return_value=args), patch("main.load_settings", return_value=Settings()) as mock_load, - patch("main.initialize_dataset") as mock_dataset, + patch("main.prepare_catalogue") as mock_dataset, patch("main.handle_aws") as mock_handle, patch("main.console.print"), ): @@ -571,7 +571,7 @@ def test_a_bad_setting_fails_before_the_dataset_download(self): with ( patch("main.parse_arguments", return_value=Namespace(cloud_provider="aws")), patch("main.load_settings", side_effect=SettingsError("bad host")), - patch("main.initialize_dataset") as mock_dataset, + patch("main.prepare_catalogue") as mock_dataset, patch("main.handle_aws") as mock_handle, patch("main.console.print"), self.assertRaises(SystemExit) as ctx, diff --git a/tests/test_utils_and_main.py b/tests/test_utils_and_main.py index 7b580a0..3457d3a 100644 --- a/tests/test_utils_and_main.py +++ b/tests/test_utils_and_main.py @@ -864,7 +864,7 @@ def setUp(self): def test_config_error_from_handler_exits_config(self): with ( - patch("main.initialize_dataset"), + patch("main.prepare_catalogue"), patch("main.console.print"), patch( "main.parse_arguments", @@ -878,7 +878,7 @@ def test_config_error_from_handler_exits_config(self): def test_ctrl_c_in_handler_exits_interrupted(self): with ( - patch("main.initialize_dataset"), + patch("main.prepare_catalogue"), patch("main.console.print"), patch( "main.parse_arguments", @@ -893,7 +893,7 @@ def test_ctrl_c_in_handler_exits_interrupted(self): def test_ctrl_c_during_dataset_download_exits_interrupted(self): with ( - patch("main.initialize_dataset", side_effect=KeyboardInterrupt), + patch("main.prepare_catalogue", side_effect=KeyboardInterrupt), patch("main.console.print"), patch( "main.parse_arguments", @@ -917,7 +917,7 @@ def test_bad_config_file_exits_config_end_to_end(self): egress=False, ) with ( - patch("main.initialize_dataset"), + patch("main.prepare_catalogue"), patch("main.console.print"), patch("main.parse_arguments", return_value=args), patch("main.load_config", return_value=None), diff --git a/tests/test_utils_report_egress.py b/tests/test_utils_report_egress.py index cbd4c54..7e3b6f8 100644 --- a/tests/test_utils_report_egress.py +++ b/tests/test_utils_report_egress.py @@ -13,6 +13,7 @@ from tests.report_fixtures import stage_report_assets from core.utils_egress import GIB, write_egress_inventory +from utils.data import active_catalogue_path from core.utils_report_egress import ( CATEGORIES, _build_allocation, @@ -262,18 +263,14 @@ def test_falls_back_to_master_dataset_when_table_missing(self): self.assertEqual(mock_load.call_args_list[1].kwargs, {}) @unittest.skipUnless( - os.path.exists("datasets/data.db"), "master dataset not downloaded" + active_catalogue_path().exists(), "master dataset not downloaded" ) def test_shipped_dataset_has_contiguous_zone1_ladders(self): from itertools import groupby from core.utils_db import load_data as db_load - rows = [ - row - for row in db_load("egresspricing", db_path="datasets/data.db") - if not row["valid_to"] - ] + rows = [row for row in db_load("egresspricing") if not row["valid_to"]] rows.sort(key=lambda r: (r["csp"], r["component"], r["zone"], r["tier_from"])) seen = set() for key, group in groupby( diff --git a/utils/data.py b/utils/data.py index 31c0a8f..f955aaf 100644 --- a/utils/data.py +++ b/utils/data.py @@ -1,166 +1,609 @@ -# utils/data.py -import os -import sys +from __future__ import annotations + +import atexit import gzip -import shutil import hashlib -import time +import json +import logging +import os +import shutil +import sqlite3 +import sys +import tempfile +import zlib +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Mapping +from urllib.parse import urljoin, urlsplit + import requests -from datetime import datetime, timedelta, timezone -from pathlib import Path -from requests.exceptions import RequestException, ConnectionError, Timeout +from utils.settings import Settings, SettingsError, normalize_host + +logger = logging.getLogger("main.utils.data") + +CATALOGUE_BASE_URL = "https://cloudexit-oss-data-eu.fsn1.your-objectstorage.com" +MANIFEST_NAME = "cloudexit-manifest.json" +LATEST_NAME = "cloudexit-latest.db.gz" +SUPPORTED_SCHEMA = 1 +MANIFEST_TIMEOUT = 10 +DOWNLOAD_TIMEOUT = 30 +REQUIRED_TABLES = ("resourcetype", "risk") +LEGACY_DATABASE = Path("datasets") / "data.db" +MISSING_STATUSES = (403, 404, 410) +PROVIDER_IDS = {"azure": 1, "aws": 2} +GZIP_MAGIC = b"\x1f\x8b" + + +class CatalogueError(Exception): + pass -# Constants -DATASET_FOLDER = Path("datasets") -REMOTE_STORAGE_URL = "https://cloudexit-oss-data-eu.fsn1.your-objectstorage.com" +class CatalogueUnavailable(CatalogueError): + pass -def get_monday_date() -> str: - now = datetime.now(timezone.utc) - monday = now - timedelta(days=now.weekday()) - if now.weekday() == 0 and now.hour < 8: - last_monday = monday - timedelta(days=7) - return last_monday.strftime("cloudexit-%Y-%m-%d.db.gz") +@dataclass(frozen=True) +class CatalogueInfo: + version: str + sequence: int | None = None + sha256: str | None = None + dataset_schema: int | None = None + exported_at: str | None = None + exporter_version: str | None = None + + +@dataclass(frozen=True) +class Installed: + path: Path + info: CatalogueInfo | None + state: dict[str, Any] + + @property + def version(self) -> str | None: + return self.info.version if self.info else None + + @property + def date(self) -> str: + raw = self.state.get("downloaded_at") or ( + self.info.exported_at if self.info else None + ) + if raw: + return str(raw)[:10] + modified = datetime.fromtimestamp(self.path.stat().st_mtime, timezone.utc) + return modified.strftime("%Y-%m-%d") + + +@dataclass(frozen=True) +class UpdateResult: + status: str + info: CatalogueInfo | None + + +@dataclass(frozen=True) +class EnsureResult: + path: Path + status: str + version: str | None + date: str + reason: str | None = None + migrated: bool = False + + +def catalogue_dir( + environ: Mapping[str, str] | None = None, *, platform: str = sys.platform +) -> Path: + environ = os.environ if environ is None else environ + if platform == "win32": + base = environ.get("LOCALAPPDATA") or str(Path.home() / "AppData" / "Local") else: - return monday.strftime("cloudexit-%Y-%m-%d.db.gz") + xdg = environ.get("XDG_DATA_HOME", "") + base = ( + xdg if xdg and os.path.isabs(xdg) else str(Path.home() / ".local" / "share") + ) + return Path(base) / "cloudexit" / "catalogue" -def compute_file_hash(filepath: str) -> str: - hash_sha256 = hashlib.sha256() - with open(filepath, "rb") as f: - for chunk in iter(lambda: f.read(4096), b""): - hash_sha256.update(chunk) - return hash_sha256.hexdigest() +_active_override: Path | None = None -def download_file(url: str, destination: str, retries: int = 3, delay: int = 5) -> bool: - for attempt in range(retries): - try: - response = requests.get(url, stream=True, timeout=30) - response.raise_for_status() +def set_active_catalogue(path: str | Path | None) -> None: + global _active_override + _active_override = Path(path) if path else None - with open(destination, "wb") as f: - shutil.copyfileobj(response.raw, f) - print(f"[INFO] Download successful: {destination}") - return True +def active_catalogue_path() -> Path: + return _active_override or catalogue_dir() / "data.db" - except ConnectionError: - print( - f"[ERROR] Connection failed while downloading {url}. Retrying ({attempt + 1}/{retries})..." - ) - except Timeout: - print( - f"[ERROR] Request timed out while downloading {url}. Retrying ({attempt + 1}/{retries})..." - ) - except RequestException as e: - print(f"[ERROR] Failed to download {url}: {e}") - break - time.sleep(delay) +def catalogue_base_url(settings: Settings) -> str: + value = settings.catalogue_url.value + if not value: + return CATALOGUE_BASE_URL + try: + return normalize_host(value) + except SettingsError as exc: + raise SettingsError( + f"catalogue URL ({settings.catalogue_url.source}): {exc}" + ) from exc + + +def _open_readonly(path: Path) -> sqlite3.Connection: + return sqlite3.connect(f"{Path(path).resolve().as_uri()}?mode=ro", uri=True) + + +def _table_names(conn: sqlite3.Connection) -> set[str]: + rows = conn.execute("SELECT name FROM sqlite_master WHERE type = 'table'") + return {row[0] for row in rows} + - print(f"[ERROR] Unable to download file after {retries} attempts: {url}") - return False +def _read_meta(conn: sqlite3.Connection, tables: set[str]) -> CatalogueInfo | None: + if "catalogue_meta" not in tables: + return None + + cursor = conn.execute("SELECT * FROM catalogue_meta") + columns = [column[0] for column in cursor.description] + rows = cursor.fetchall() + if len(rows) != 1: + raise CatalogueError( + f"catalogue_meta must have exactly one row, found {len(rows)}" + ) + + row = dict(zip(columns, rows[0])) + if not row.get("catalogue_version"): + raise CatalogueError("catalogue_meta has no catalogue_version") + schema = row.get("dataset_schema") + if isinstance(schema, int) and schema > SUPPORTED_SCHEMA: + raise CatalogueError( + f"catalogue uses dataset schema {schema}; this cloudexit supports " + f"up to {SUPPORTED_SCHEMA}. Update cloudexit." + ) + return CatalogueInfo( + version=str(row["catalogue_version"]), + sequence=row.get("catalogue_sequence"), + sha256=row.get("catalogue_sha256"), + dataset_schema=schema, + exported_at=row.get("exported_at"), + exporter_version=row.get("exporter_version"), + ) -def fetch_remote_checksum( - checksum_url: str, retries: int = 3, delay: int = 5 -) -> str | None: - for attempt in range(retries): +def catalogue_info(db_path: str | Path | None = None) -> CatalogueInfo | None: + path = Path(db_path) if db_path else active_catalogue_path() + try: + conn = _open_readonly(path) try: - response = requests.get(checksum_url, timeout=10) - response.raise_for_status() - return response.text.strip().split()[0] + return _read_meta(conn, _table_names(conn)) + finally: + conn.close() + except sqlite3.Error as exc: + raise CatalogueError(f"cannot read catalogue {path}: {exc}") from exc - except ConnectionError: - print( - f"[ERROR] Connection failed when fetching {checksum_url}. Retrying ({attempt + 1}/{retries})..." - ) - except Timeout: - print( - f"[ERROR] Request timed out when fetching {checksum_url}. Retrying ({attempt + 1}/{retries})..." + +def active_resource_type_ids( + provider: str | int, db_path: str | Path | None = None +) -> list[int]: + if isinstance(provider, str): + try: + csp = PROVIDER_IDS[provider.lower()] + except KeyError: + raise ValueError(f"unknown cloud provider: {provider!r}") from None + else: + csp = int(provider) + + path = Path(db_path) if db_path else active_catalogue_path() + conn = _open_readonly(path) + try: + rows = conn.execute( + "SELECT id FROM resourcetype WHERE csp = ? AND status = 't' ORDER BY id", + (csp,), + ).fetchall() + finally: + conn.close() + return [row[0] for row in rows] + + +def validate_database( + path: str | Path, + *, + expect_version: str | None = None, + require_meta: bool = False, +) -> CatalogueInfo | None: + try: + conn = _open_readonly(Path(path)) + try: + tables = _table_names(conn) + missing = [name for name in REQUIRED_TABLES if name not in tables] + if missing: + raise CatalogueError( + f"not a catalogue database: missing table(s) {', '.join(missing)}" + ) + info = _read_meta(conn, tables) + finally: + conn.close() + except sqlite3.DatabaseError as exc: + raise CatalogueError(f"not a valid catalogue database: {exc}") from exc + + if require_meta and info is None: + raise CatalogueError("the catalogue has no catalogue_meta table") + if expect_version is not None and info and info.version != expect_version: + raise CatalogueError( + f"catalogue version {info.version} does not match the expected " + f"{expect_version}" + ) + return info + + +def read_state(directory: Path) -> dict[str, Any]: + try: + data = json.loads((directory / "state.json").read_text(encoding="utf-8")) + except (OSError, ValueError): + return {} + return data if isinstance(data, dict) else {} + + +def _write_state(directory: Path, state: dict[str, Any]) -> None: + temp = directory / "state.json.tmp" + temp.write_text(json.dumps(state, indent=2) + "\n", encoding="utf-8") + os.replace(temp, directory / "state.json") + + +def installed_catalogue(directory: Path | None = None) -> Installed | None: + directory = directory or catalogue_dir() + path = directory / "data.db" + if not path.is_file() or path.stat().st_size == 0: + return None + try: + info = catalogue_info(path) + except CatalogueError: + info = None + return Installed(path, info, read_state(directory)) + + +def migrate_legacy_dataset( + directory: Path | None = None, legacy: Path | None = None +) -> bool: + directory = directory or catalogue_dir() + legacy = legacy or LEGACY_DATABASE + if installed_catalogue(directory) or not legacy.is_file(): + return False + try: + info = validate_database(legacy) + except CatalogueError: + return False + + directory.mkdir(parents=True, exist_ok=True) + shutil.copy2(legacy, directory / "data.db") + modified = datetime.fromtimestamp(legacy.stat().st_mtime, timezone.utc) + _write_state( + directory, + { + "version": info.version if info else None, + "sha256": None, + "downloaded_at": modified.isoformat(timespec="seconds"), + }, + ) + return True + + +def _is_gzip(data: bytes) -> bool: + return data[:2] == GZIP_MAGIC + + +def _decompress(data: bytes) -> bytes: + if not _is_gzip(data): + return data + try: + return gzip.decompress(data) + except (OSError, EOFError, zlib.error) as exc: + raise CatalogueError(f"the file is not a valid gzip archive: {exc}") from exc + + +def _sha256(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def _install( + data: bytes, + directory: Path, + *, + sha256: str | None, + expect_version: str | None = None, + require_meta: bool = False, +) -> CatalogueInfo | None: + directory.mkdir(parents=True, exist_ok=True) + fd, temp_name = tempfile.mkstemp(dir=directory, prefix="data.db.", suffix=".tmp") + temp = Path(temp_name) + try: + with os.fdopen(fd, "wb") as handle: + handle.write(_decompress(data)) + info = validate_database( + temp, expect_version=expect_version, require_meta=require_meta + ) + + current = directory / "data.db" + if current.exists(): + shutil.copy2(current, directory / "data.db.previous") + os.replace(temp, current) + _write_state( + directory, + { + "version": info.version if info else None, + "sha256": sha256, + "downloaded_at": datetime.now(timezone.utc).isoformat( + timespec="seconds" + ), + }, + ) + return info + finally: + temp.unlink(missing_ok=True) + + +def _get(url: str, *, timeout: float, verify: str | bool) -> requests.Response: + try: + return requests.get(url, timeout=timeout, verify=verify) + except requests.RequestException as exc: + logger.debug("Catalogue request to %s failed: %s", url, exc) + reason = str(exc) if len(str(exc)) <= 80 else type(exc).__name__ + raise CatalogueError( + f"cannot reach the catalogue server at {urlsplit(url).netloc} ({reason})" + ) from exc + + +def _download(url: str, *, verify: str | bool) -> bytes: + response = _get(url, timeout=DOWNLOAD_TIMEOUT, verify=verify) + if not response.ok: + raise CatalogueError( + f"download of {url} failed with HTTP {response.status_code}" + ) + return response.content + + +def _check_checksum(data: bytes, expected: str, what: str) -> str: + actual = _sha256(data) + if actual != expected.strip().lower(): + raise CatalogueError( + f"checksum mismatch for {what}: expected {expected}, got {actual}" + ) + return actual + + +def _check_manifest(manifest: Any) -> dict[str, Any]: + if not isinstance(manifest, dict): + raise CatalogueError("the manifest is not a JSON object") + schema = manifest.get("dataset_schema") + if isinstance(schema, int) and schema > SUPPORTED_SCHEMA: + raise CatalogueError( + f"the catalogue uses dataset schema {schema}; this cloudexit supports " + f"up to {SUPPORTED_SCHEMA}. Update cloudexit." + ) + latest = manifest.get("latest") + if not isinstance(latest, dict) or not all( + isinstance(latest.get(key), str) + for key in ("catalogue_version", "file", "file_sha256") + ): + raise CatalogueError("the manifest has no usable 'latest' entry") + return manifest + + +def fetch_manifest(manifest_url: str, *, verify: str | bool = True) -> dict | None: + response = _get(manifest_url, timeout=MANIFEST_TIMEOUT, verify=verify) + if response.status_code in MISSING_STATUSES: + return None + if not response.ok: + raise CatalogueError( + f"manifest request failed with HTTP {response.status_code}" + ) + try: + return _check_manifest(response.json()) + except ValueError as exc: + raise CatalogueError("the manifest is not valid JSON") from exc + + +def resolve_file_url(manifest_url: str, file: str) -> str: + url = urljoin(manifest_url, file) + base, target = urlsplit(manifest_url), urlsplit(url) + folder = base.path.rsplit("/", 1)[0] + "/" + if ( + (target.scheme, target.netloc) != (base.scheme, base.netloc) + or not target.path.startswith(folder) + or ".." in target.path.split("/") + ): + raise CatalogueError( + f"the manifest file {file!r} points outside the catalogue location" + ) + return url + + +def _select_entry(manifest: dict[str, Any], version: str | None) -> dict[str, Any]: + if version is None: + return manifest["latest"] + entries = [manifest["latest"], *manifest.get("previous", [])] + for entry in entries: + if isinstance(entry, dict) and entry.get("catalogue_version") == version: + return entry + available = sorted( + str(entry["catalogue_version"]) + for entry in entries + if isinstance(entry, dict) and "catalogue_version" in entry + ) + raise CatalogueError( + f"version {version} is not in the manifest; available: {', '.join(available)}" + ) + + +def _update_from_latest( + base_url: str, directory: Path, *, force: bool, verify: str | bool +) -> UpdateResult: + base = base_url.rstrip("/") + response = _get( + f"{base}/{LATEST_NAME}.sha256", timeout=MANIFEST_TIMEOUT, verify=verify + ) + if response.status_code in MISSING_STATUSES: + raise CatalogueError(f"no catalogue manifest or latest file found at {base}") + if not response.ok or not response.text.strip(): + raise CatalogueError( + f"checksum request failed with HTTP {response.status_code}" + ) + expected = response.text.split()[0].lower() + + installed = installed_catalogue(directory) + if not force and installed and installed.state.get("sha256") == expected: + return UpdateResult("current", installed.info) + + data = _download(f"{base}/{LATEST_NAME}", verify=verify) + digest = _check_checksum(data, expected, LATEST_NAME) + return UpdateResult("updated", _install(data, directory, sha256=digest)) + + +def update_catalogue( + base_url: str, + *, + directory: Path | None = None, + version: str | None = None, + force: bool = False, + verify: str | bool = True, +) -> UpdateResult: + directory = directory or catalogue_dir() + manifest_url = f"{base_url.rstrip('/')}/{MANIFEST_NAME}" + manifest = fetch_manifest(manifest_url, verify=verify) + + if manifest is None: + if version is not None: + raise CatalogueError( + "no manifest is published here, so a specific version cannot be chosen" ) - except RequestException as e: - print(f"[ERROR] Failed to fetch {checksum_url}: {e}") - break + return _update_from_latest(base_url, directory, force=force, verify=verify) - time.sleep(delay) + entry = _select_entry(manifest, version) + target = entry["catalogue_version"] + installed = installed_catalogue(directory) + if not force and installed and installed.version == target: + return UpdateResult("current", installed.info) - print(f"[ERROR] Unable to fetch remote checksum after {retries} attempts.") - return None + data = _download(resolve_file_url(manifest_url, entry["file"]), verify=verify) + digest = _check_checksum(data, entry["file_sha256"], entry["file"]) + info = _install( + data, directory, sha256=digest, expect_version=target, require_meta=True + ) + return UpdateResult("updated", info) -def initialize_dataset() -> None: - DATASET_FOLDER.mkdir(exist_ok=True) +def import_catalogue( + file: str | Path, *, directory: Path | None = None, sha256: str | None = None +) -> CatalogueInfo | None: + path = Path(file) + try: + data = path.read_bytes() + except OSError as exc: + raise CatalogueError(f"cannot read {path}: {exc}") from exc + digest = _sha256(data) + if sha256 is not None: + _check_checksum(data, sha256, str(path)) + return _install(data, directory or catalogue_dir(), sha256=digest) - latest_file = get_monday_date() - latest_file_url = f"{REMOTE_STORAGE_URL}/{latest_file}" - latest_checksum_url = f"{REMOTE_STORAGE_URL}/{latest_file}.sha256" - latest_symlink_file = f"{REMOTE_STORAGE_URL}/cloudexit-latest.db.gz" - latest_symlink_checksum_url = f"{REMOTE_STORAGE_URL}/cloudexit-latest.db.gz.sha256" - local_db_path = DATASET_FOLDER / "data.db" - local_compressed_path = DATASET_FOLDER / latest_file +def use_catalogue_file(file: str | Path) -> tuple[Path, CatalogueInfo | None]: + path = Path(file) + try: + data = path.read_bytes() + except OSError as exc: + raise CatalogueError(f"cannot read {path}: {exc}") from exc - # Fetch checksum for the date-based file - remote_checksum = fetch_remote_checksum(latest_checksum_url) - if not remote_checksum: - print(f"[INFO] Unable to fetch remote checksum from {latest_checksum_url}.") - print(f"[INFO] Trying latest symlink from {latest_symlink_checksum_url}...") - remote_checksum = fetch_remote_checksum(latest_symlink_checksum_url) - latest_file_url = latest_symlink_file - latest_file = "cloudexit-latest.db.gz" - local_compressed_path = DATASET_FOLDER / latest_file + if _is_gzip(data): + temp_dir = tempfile.mkdtemp(prefix="cloudexit-catalogue-") + atexit.register(shutil.rmtree, temp_dir, ignore_errors=True) + path = Path(temp_dir) / "data.db" + path.write_bytes(_decompress(data)) + return path, validate_database(path) - if not remote_checksum: - print("[ERROR] Unable to fetch any remote checksum. Skipping update.") - else: - # Check if local compressed file exists - if local_compressed_path.exists(): - local_checksum = compute_file_hash(local_compressed_path) - if local_checksum == remote_checksum: - print("[INFO] Local dataset is up-to-date. No download needed.") - return - else: - print( - "[INFO] Local dataset is outdated. Removing old files and downloading new dataset..." - ) +def ensure_catalogue( + base_url: str, *, directory: Path | None = None, verify: str | bool = True +) -> EnsureResult: + directory = directory or catalogue_dir() + migrated = migrate_legacy_dataset(directory) + try: + result = update_catalogue(base_url, directory=directory, verify=verify) + except (CatalogueError, OSError) as exc: + installed = installed_catalogue(directory) + if installed is None: + raise CatalogueUnavailable( + f"no catalogue is installed and the update failed: {exc}" + ) from exc + return EnsureResult( + installed.path, + "failed", + installed.version, + installed.date, + str(exc), + migrated, + ) + + installed = installed_catalogue(directory) + return EnsureResult( + installed.path, + result.status, + installed.version, + installed.date, + None, + migrated, + ) - # Remove all old compressed and extracted files - for file in DATASET_FOLDER.glob("cloudexit-*.db.gz"): - os.remove(file) - if local_db_path.exists(): - os.remove(local_db_path) - - # Download and extract dataset - if download_file(latest_file_url, local_compressed_path): - downloaded_checksum = compute_file_hash(local_compressed_path) - if downloaded_checksum != remote_checksum: - print( - f"[ERROR] Checksum mismatch for {latest_file}. " - f"Expected {remote_checksum}, got {downloaded_checksum}. " - "Discarding downloaded file." - ) - os.remove(local_compressed_path) - else: - print( - f"[INFO] Checksum verified. Extracting dataset from {latest_file}..." - ) - with ( - gzip.open(local_compressed_path, "rb") as f_in, - open(local_db_path, "wb") as f_out, - ): - shutil.copyfileobj(f_in, f_out) +def _age(date_text: str) -> str: + try: + days = ( + datetime.now(timezone.utc).date() + - datetime.strptime(date_text[:10], "%Y-%m-%d").date() + ).days + except ValueError: + return "age unknown" + if days <= 0: + return "today" + return "1 day ago" if days == 1 else f"{days} days ago" - print("[INFO] Dataset updated successfully.") - if not local_db_path.exists() or local_db_path.stat().st_size == 0: - print( - f"[ERROR] Dataset database {local_db_path} is missing or empty! " - "Cannot proceed without data." +def _describe(info: CatalogueInfo | None) -> str: + if info is None: + return "unknown (the dataset has no catalogue_meta)" + if info.sequence is None: + return info.version + return f"{info.version} (sequence {info.sequence})" + + +def status_lines( + base_url: str, *, directory: Path | None = None, verify: str | bool = True +) -> list[str]: + directory = directory or catalogue_dir() + installed = installed_catalogue(directory) + if installed is None: + lines = [f"installed: none ({directory})"] + else: + lines = [ + f"installed: {_describe(installed.info)}", + f"updated: {installed.date} ({_age(installed.date)})", + f"location: {installed.path}", + ] + + try: + manifest = fetch_manifest( + f"{base_url.rstrip('/')}/{MANIFEST_NAME}", verify=verify ) - sys.exit(1) + except CatalogueError as exc: + lines.append(f"latest: unavailable ({exc})") + return lines + + if manifest is None: + lines.append("latest: unknown (no manifest is published)") + return lines + + latest = manifest["latest"] + sequence = latest.get("catalogue_sequence") + suffix = f" (sequence {sequence})" if sequence is not None else "" + lines.append(f"latest: {latest['catalogue_version']}{suffix}") + if installed is None or installed.version != latest["catalogue_version"]: + lines.append("an update is available: run `cloudexit catalogue update`") + return lines From f165af9b672725b9b76bbe37fafcf36db374b441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Fri, 23 Oct 2026 18:12:57 +0200 Subject: [PATCH 05/61] build sync.json from the api v2 definition --- README.md | 13 +- core/engine.py | 3 + core/sync_document.py | 792 +++++++++++ core/utils_sync.py | 32 +- docs/api-v2/MANIFEST.sha256 | 59 + docs/api-v2/README.md | 68 + docs/api-v2/VERSION | 1 + docs/api-v2/errors.yaml | 277 ++++ docs/api-v2/examples/01-meta.json | 77 + .../examples/02-whoami_assessment_key.json | 57 + .../examples/03-whoami_account_key.json | 44 + .../examples/04-request_id_replaced.json | 43 + .../05-first_sync_assessment_key.json | 146 ++ .../examples/06-second_sync_appended.json | 182 +++ .../api-v2/examples/07-idempotent_replay.json | 178 +++ .../examples/08-idempotency_key_reused.json | 181 +++ .../examples/09-idempotency_key_missing.json | 106 ++ .../examples/10-idempotency_key_mismatch.json | 107 ++ docs/api-v2/examples/11-scope_mismatch.json | 114 ++ .../api-v2/examples/12-provider_mismatch.json | 114 ++ docs/api-v2/examples/13-depth_too_deep.json | 118 ++ .../14-depth_three_without_egress.json | 108 ++ .../examples/15-depth_three_with_egress.json | 124 ++ .../examples/16-unknown_resource_types.json | 114 ++ .../examples/17-inactive_resource_types.json | 114 ++ .../18-all_resource_types_unknown.json | 105 ++ .../examples/19-duplicate_items_merged.json | 99 ++ .../20-catalogue_collector_older.json | 114 ++ .../21-catalogue_collector_newer.json | 107 ++ ...atalogue_versions_compared_as_numbers.json | 103 ++ .../api-v2/examples/23-catalogue_missing.json | 93 ++ .../24-catalogue_sha256_mismatch.json | 113 ++ .../25-account_key_creates_assessment.json | 109 ++ .../26-account_key_without_create_scope.json | 114 ++ .../27-account_key_without_target.json | 107 ++ ...count_key_appends_to_named_assessment.json | 104 ++ .../29-account_key_unknown_assessment.json | 104 ++ ...assessment_key_for_another_assessment.json | 117 ++ docs/api-v2/examples/31-key_revoked.json | 50 + docs/api-v2/examples/32-key_expired.json | 50 + docs/api-v2/examples/33-key_malformed.json | 47 + docs/api-v2/examples/34-key_bad_checksum.json | 47 + .../examples/35-authorization_missing.json | 45 + .../api-v2/examples/36-payload_too_large.json | 113 ++ .../api-v2/examples/37-validation_errors.json | 125 ++ .../examples/38-collection_too_old.json | 113 ++ docs/api-v2/examples/39-invalid_json.json | 50 + .../examples/40-unsupported_media_type.json | 50 + .../41-unsupported_schema_version.json | 114 ++ docs/api-v2/examples/42-get_sync.json | 126 ++ .../examples/43-get_sync_not_found.json | 47 + docs/api-v2/examples/44-get_assessment.json | 134 ++ .../examples/45-get_assessment_not_found.json | 49 + .../46-list_assessments_second_page.json | 94 ++ .../47-list_assessments_by_provider.json | 53 + .../48-list_assessments_needs_read_scope.json | 56 + .../49-list_assessments_bad_cursor.json | 55 + .../examples/50-list_assessment_syncs.json | 202 +++ .../examples/51-insufficient_credits.json | 111 ++ docs/api-v2/fixtures/catalogue.json | 48 + docs/api-v2/make_manifest.py | 40 + docs/api-v2/openapi.yaml | 1109 +++++++++++++++ docs/api-v2/vectors/content_sha256.json | 1234 +++++++++++++++++ docs/api-v2/vectors/scope_key.json | 509 +++++++ main.py | 103 +- pyproject.toml | 2 + requirements-dev.txt | 2 + tests/sync_fixtures.py | 125 ++ tests/test_api_v2_definition_copy.py | 76 + tests/test_sync_document.py | 1005 ++++++++++++++ tests/test_utils_and_main.py | 34 +- tests/test_utils_sync.py | 83 +- 72 files changed, 10543 insertions(+), 119 deletions(-) create mode 100644 core/sync_document.py create mode 100644 docs/api-v2/MANIFEST.sha256 create mode 100644 docs/api-v2/README.md create mode 100644 docs/api-v2/VERSION create mode 100644 docs/api-v2/errors.yaml create mode 100644 docs/api-v2/examples/01-meta.json create mode 100644 docs/api-v2/examples/02-whoami_assessment_key.json create mode 100644 docs/api-v2/examples/03-whoami_account_key.json create mode 100644 docs/api-v2/examples/04-request_id_replaced.json create mode 100644 docs/api-v2/examples/05-first_sync_assessment_key.json create mode 100644 docs/api-v2/examples/06-second_sync_appended.json create mode 100644 docs/api-v2/examples/07-idempotent_replay.json create mode 100644 docs/api-v2/examples/08-idempotency_key_reused.json create mode 100644 docs/api-v2/examples/09-idempotency_key_missing.json create mode 100644 docs/api-v2/examples/10-idempotency_key_mismatch.json create mode 100644 docs/api-v2/examples/11-scope_mismatch.json create mode 100644 docs/api-v2/examples/12-provider_mismatch.json create mode 100644 docs/api-v2/examples/13-depth_too_deep.json create mode 100644 docs/api-v2/examples/14-depth_three_without_egress.json create mode 100644 docs/api-v2/examples/15-depth_three_with_egress.json create mode 100644 docs/api-v2/examples/16-unknown_resource_types.json create mode 100644 docs/api-v2/examples/17-inactive_resource_types.json create mode 100644 docs/api-v2/examples/18-all_resource_types_unknown.json create mode 100644 docs/api-v2/examples/19-duplicate_items_merged.json create mode 100644 docs/api-v2/examples/20-catalogue_collector_older.json create mode 100644 docs/api-v2/examples/21-catalogue_collector_newer.json create mode 100644 docs/api-v2/examples/22-catalogue_versions_compared_as_numbers.json create mode 100644 docs/api-v2/examples/23-catalogue_missing.json create mode 100644 docs/api-v2/examples/24-catalogue_sha256_mismatch.json create mode 100644 docs/api-v2/examples/25-account_key_creates_assessment.json create mode 100644 docs/api-v2/examples/26-account_key_without_create_scope.json create mode 100644 docs/api-v2/examples/27-account_key_without_target.json create mode 100644 docs/api-v2/examples/28-account_key_appends_to_named_assessment.json create mode 100644 docs/api-v2/examples/29-account_key_unknown_assessment.json create mode 100644 docs/api-v2/examples/30-assessment_key_for_another_assessment.json create mode 100644 docs/api-v2/examples/31-key_revoked.json create mode 100644 docs/api-v2/examples/32-key_expired.json create mode 100644 docs/api-v2/examples/33-key_malformed.json create mode 100644 docs/api-v2/examples/34-key_bad_checksum.json create mode 100644 docs/api-v2/examples/35-authorization_missing.json create mode 100644 docs/api-v2/examples/36-payload_too_large.json create mode 100644 docs/api-v2/examples/37-validation_errors.json create mode 100644 docs/api-v2/examples/38-collection_too_old.json create mode 100644 docs/api-v2/examples/39-invalid_json.json create mode 100644 docs/api-v2/examples/40-unsupported_media_type.json create mode 100644 docs/api-v2/examples/41-unsupported_schema_version.json create mode 100644 docs/api-v2/examples/42-get_sync.json create mode 100644 docs/api-v2/examples/43-get_sync_not_found.json create mode 100644 docs/api-v2/examples/44-get_assessment.json create mode 100644 docs/api-v2/examples/45-get_assessment_not_found.json create mode 100644 docs/api-v2/examples/46-list_assessments_second_page.json create mode 100644 docs/api-v2/examples/47-list_assessments_by_provider.json create mode 100644 docs/api-v2/examples/48-list_assessments_needs_read_scope.json create mode 100644 docs/api-v2/examples/49-list_assessments_bad_cursor.json create mode 100644 docs/api-v2/examples/50-list_assessment_syncs.json create mode 100644 docs/api-v2/examples/51-insufficient_credits.json create mode 100644 docs/api-v2/fixtures/catalogue.json create mode 100644 docs/api-v2/make_manifest.py create mode 100644 docs/api-v2/openapi.yaml create mode 100644 docs/api-v2/vectors/content_sha256.json create mode 100644 docs/api-v2/vectors/scope_key.json create mode 100644 tests/sync_fixtures.py create mode 100644 tests/test_api_v2_definition_copy.py create mode 100644 tests/test_sync_document.py diff --git a/README.md b/README.md index d5c7e77..e6e30d4 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ The CLI supports multiple input modes for both AWS and Azure. See the [configuration reference](https://cloudexit.escapecloud.io/config/config-schema.html) for required permissions and config file format. -Want to see how a regulatory-aligned report looks (DORA / FINMA / UK PRA)? Run with `--dry-run` and send the output `payload.json` to request_report@escapecloud.io — we'll generate a sample you can share with your risk or compliance team. +Want to see how a regulatory-aligned report looks (DORA / FINMA / UK PRA)? Run with `--dry-run` and send the output `raw_data/sync.json` to request_report@escapecloud.io — we'll generate a sample you can share with your risk or compliance team. ## Platform settings @@ -136,6 +136,17 @@ cloudexit aws --tfstate infra.tfstate --catalogue FILE.db.gz # use a file for Set `CLOUDEXIT_CATALOGUE_URL` to download from a mirror instead of the default location. A `./datasets/data.db` from an earlier version is copied to the data directory once. +## Sync document + +Every run writes `raw_data/sync.json` next to the report: the run's result in the shape defined by the API v2 definition in [`docs/api-v2/`](./docs/api-v2/) (a vendored copy; do not edit it here). It holds the scope (an AWS account and region, an Azure subscription, or a Terraform state), the resource inventory, the monthly costs and, when `--egress` ran, the egress estimate. It never contains credentials and is built without a network connection. + +```bash +cloudexit aws --config config.json --emit-payload out/ # also write out/.json +cloudexit aws --config config.json --egress --egress-names hashed # send egress names as salted hashes +``` + +With `--egress-names hashed` each egress resource name is replaced by an HMAC-SHA256 with a random per-scope salt that stays on your machine (`~/.local/share/cloudexit/salts/`), so the same name hashes the same way in later runs of the same scope. + ## Data Landscape & Egress Estimation (alpha) Add `--egress` to any assessment to measure how much data lives in the assessed scope and estimate the one-time internet egress fee for moving it out — based on the provider's tiered list prices, with no additional permissions required. diff --git a/core/engine.py b/core/engine.py index 4993aed..c496c8a 100644 --- a/core/engine.py +++ b/core/engine.py @@ -147,6 +147,9 @@ def test_permissions( ) identity = sts_client.get_caller_identity() caller_arn = identity.get("Arn", "") + # The account id is the scope of the sync document + if identity.get("Account"): + provider_details["accountId"] = str(identity["Account"]) # IAM users keep existing policy-name validation for backwards compatibility. if ":user/" in caller_arn: diff --git a/core/sync_document.py b/core/sync_document.py new file mode 100644 index 0000000..0138bc7 --- /dev/null +++ b/core/sync_document.py @@ -0,0 +1,792 @@ +from __future__ import annotations + +import hashlib +import hmac +import json +import logging +import os +import re +import secrets +import sqlite3 +import sys +import time +import uuid +from dataclasses import dataclass +from datetime import datetime, timezone +from decimal import ROUND_HALF_UP, Decimal, InvalidOperation +from pathlib import Path +from typing import Any + +from utils.data import ( + CatalogueError, + active_resource_type_ids, + catalogue_dir, + catalogue_info, +) +from utils.version import __version__ + +from .utils_db import load_data + +logger = logging.getLogger("core.engine.sync_document") + +SCHEMA_VERSION = "2.0" +COLLECTOR_NAME = "cloudexit" +PROVIDERS = {1: "azure", 2: "aws"} + +MAX_BODY_BYTES = 1_000_000 +MAX_RESOURCE_ITEMS = 1000 +MAX_COUNT_PER_ITEM = 10000 +MAX_LOCATION_CHARS = 255 +MAX_TYPE_CODE_CHARS = 255 +MAX_COST_ITEMS = 12 +MAX_EGRESS_ITEMS = 1000 +MAX_NAME_CHARS = 255 +MAX_FLAGS = 10 +MAX_FLAG_CHARS = 64 +MAX_NOTES = 10 +MAX_NOTE_CHARS = 255 +MAX_TIERS = 10 +MAX_TIER_CHARS = 50 +MAX_CATALOGUE_TYPE_IDS = 5000 +MAX_SIZE_BYTES = 2**53 - 1 +MAX_EPOCH = 4102444800 + +UUID = r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" +UUID_RE = re.compile(f"^{UUID}$") +UUID7_RE = re.compile( + r"^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" +) +SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +MONTH_RE = re.compile(r"^[0-9]{4}-(0[1-9]|1[0-2])$") +AMOUNT_RE = re.compile(r"^-?[0-9]+(\.[0-9]+)?$") +CURRENCY_RE = re.compile(r"^[A-Za-z]{3}$") +CATALOGUE_VERSION_RE = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}(\.[0-9]+)?$") +AWS_REGION_RE = re.compile(r"^(\*|[a-z]{2}(-[a-z]+)+-[0-9]+)$") +AZURE_GROUP_RE = re.compile(r"^[A-Za-z0-9_()\-.]{1,90}$") +SCOPE_KEY_MAX = 255 + + +class SyncDocumentError(Exception): + def __init__(self, message: str, problems: list[str] | None = None): + super().__init__(message) + self.problems = problems or [] + + +class ScopeError(ValueError): + def __init__(self, message: str, pointer: str = ""): + super().__init__(message) + self.pointer = pointer + + +@dataclass +class RunContext: + sync_id: str + started_at: int + cloud_service_provider: int + assessment_type: int + provider_details: dict[str, Any] + egress_collected: bool = False + egress_names: str = "plain" + completed_at: int | None = None + target: dict[str, Any] | None = None + ci: dict[str, Any] | None = None + + +@dataclass(frozen=True) +class SyncOutput: + path: Path + size: int + sha256: str + emitted: Path | None + summary: str + + +def new_uuid7(now_ms: int | None = None) -> str: + if now_ms is None: + native = getattr(uuid, "uuid7", None) + if native is not None: + return str(native()) + now_ms = time.time_ns() // 1_000_000 + + value = (now_ms & (2**48 - 1)) << 80 + value |= 0x7 << 76 + value |= secrets.randbits(12) << 64 + value |= 0b10 << 62 + value |= secrets.randbits(62) + return str(uuid.UUID(int=value)) + + +def scope_key(scope: dict[str, Any]) -> str: + if not isinstance(scope, dict): + raise ScopeError("the scope must be an object") + provider = scope.get("provider") + if provider not in ("aws", "azure"): + raise ScopeError("the provider must be aws or azure", "/provider") + + if "tfstate" in scope: + for block in ("aws", "azure"): + if block in scope: + raise ScopeError( + "a Terraform state scope has no provider block", f"/{block}" + ) + tfstate = scope["tfstate"] if isinstance(scope["tfstate"], dict) else {} + lineage = tfstate.get("lineage") + if not isinstance(lineage, str) or not UUID_RE.match(lineage): + raise ScopeError("the lineage must be a UUID", "/tfstate/lineage") + key = f"tfstate:{lineage.lower()}" + elif provider == "aws": + key = _aws_key(_block(scope, "aws")) + else: + key = _azure_key(_block(scope, "azure")) + + if len(key) > SCOPE_KEY_MAX: + raise ScopeError("the scope key is too long") + return key + + +def _block(scope: dict[str, Any], name: str) -> dict[str, Any]: + block = scope.get(name) + if not isinstance(block, dict): + raise ScopeError(f"the {name} block is required", f"/{name}") + return block + + +def _aws_key(aws: dict[str, Any]) -> str: + account = aws.get("account_id") + if not isinstance(account, str) or not re.fullmatch(r"[0-9]{12}", account): + raise ScopeError("the account id must be 12 digits", "/aws/account_id") + + regions = aws.get("regions") + if not isinstance(regions, list) or not regions: + raise ScopeError("at least one region is required", "/aws/regions") + + distinct = [] + for index, region in enumerate(regions): + if not isinstance(region, str) or not AWS_REGION_RE.match(region): + raise ScopeError("not a region", f"/aws/regions/{index}") + if region not in distinct: + distinct.append(region) + region = distinct[0] if len(distinct) == 1 else "*" + return f"aws:{account}:{region}" + + +def _azure_key(azure: dict[str, Any]) -> str: + parts = [] + for name in ("tenant_id", "subscription_id"): + value = azure.get(name) + if not isinstance(value, str) or not UUID_RE.match(value): + raise ScopeError("must be a GUID", f"/azure/{name}") + parts.append(value.lower()) + + group = azure.get("resource_group") + if group is None: + group_key = "*" + elif ( + not isinstance(group, str) + or not AZURE_GROUP_RE.match(group) + or group.endswith(".") + ): + raise ScopeError("not a resource group name", "/azure/resource_group") + else: + group_key = group.lower() + return f"azure:{parts[0]}:{parts[1]}:{group_key}" + + +def decimal_amount(value: Any) -> str: + try: + amount = value if isinstance(value, Decimal) else Decimal(str(value)) + except InvalidOperation: + raise SyncDocumentError(f"cost amount {value!r} is not a number") from None + if not amount.is_finite(): + raise SyncDocumentError(f"cost amount {value!r} is not a finite number") + + try: + amount = amount.quantize(Decimal("0.000001"), rounding=ROUND_HALF_UP) + except InvalidOperation: + raise SyncDocumentError(f"cost amount {value!r} is too large") from None + if amount == 0: + return "0" + return format(amount.normalize(), "f") + + +def build_scope(context: RunContext, raw_data_path: str) -> dict[str, Any]: + provider = PROVIDERS.get(context.cloud_service_provider) + if provider is None: + raise SyncDocumentError( + f"unsupported cloud service provider {context.cloud_service_provider}" + ) + details = context.provider_details + + if details.get("tfstatePath"): + manifest_path = os.path.join(raw_data_path, "tfstate_manifest.json") + try: + with open(manifest_path, encoding="utf-8") as handle: + state_scope = json.load(handle).get("scope") or {} + except (OSError, ValueError) as exc: + raise SyncDocumentError( + f"cannot read the Terraform state scope: {exc}" + ) from exc + serial = state_scope.get("serial") + return { + "provider": provider, + "tfstate": { + "lineage": state_scope.get("lineage"), + "serial": serial if isinstance(serial, int) else None, + }, + } + + if provider == "aws": + return { + "provider": "aws", + "aws": { + "account_id": details.get("accountId"), + "regions": [details.get("region")], + }, + } + return { + "provider": "azure", + "azure": { + "tenant_id": details.get("tenantId"), + "subscription_id": details.get("subscriptionId"), + "resource_group": details.get("resourceGroupName") or None, + }, + } + + +def _cut(text: str, limit: int) -> tuple[str, bool]: + return (text[:limit], True) if len(text) > limit else (text, False) + + +def _resources(db_path: str) -> list[dict[str, Any]]: + codes = { + row["id"]: row["code"] for row in load_data("resourcetype", db_path=db_path) + } + rows = sorted( + load_data("resource_inventory", db_path=db_path), + key=lambda row: (int(row["resource_type"]), row["location"] or ""), + ) + + items = [] + for row in rows: + type_id = int(row["resource_type"]) + location = _cut(row["location"] or "unknown", MAX_LOCATION_CHARS)[0] + code = codes.get(type_id) + remaining = int(row["count"]) + # Counts above the per-item limit are split; the host merges items with + # the same type and location by summing their counts. + while True: + chunk = min(remaining, MAX_COUNT_PER_ITEM) + item = {"type_id": type_id} + if code: + item["type_code"] = _cut(code, MAX_TYPE_CODE_CHARS)[0] + item["location"] = location + item["count"] = chunk + items.append(item) + remaining -= chunk + if remaining <= 0: + break + return items + + +def _costs(db_path: str, completed_at: int) -> list[dict[str, Any]]: + current_month = datetime.fromtimestamp(completed_at, timezone.utc).strftime("%Y-%m") + items = [] + for row in load_data("cost_inventory", db_path=db_path): + month = str(row["month"])[:7] + currency = str(row["currency"]) + if not MONTH_RE.match(month) or not CURRENCY_RE.match(currency): + raise SyncDocumentError( + "a stored cost row has an invalid month or currency" + ) + items.append( + { + "month": month, + "amount": decimal_amount(row["cost"]), + "currency": currency.upper(), + "final": month < current_month, + } + ) + + items.sort(key=lambda item: item["month"]) + if len(items) > MAX_COST_ITEMS: + logger.warning( + "Cost data trimmed to the API v2 limit: %d month(s) dropped.", + len(items) - MAX_COST_ITEMS, + ) + items = items[-MAX_COST_ITEMS:] + return items + + +def salt_path(key: str) -> Path: + name = re.sub(r'[<>:"/\\|?*]', "_", key) if sys.platform == "win32" else key + return catalogue_dir().parent / "salts" / name + + +def load_salt(key: str) -> bytes: + path = salt_path(key) + path.parent.mkdir(parents=True, exist_ok=True) + for _ in range(2): + try: + return bytes.fromhex(path.read_text(encoding="ascii").strip()) + except FileNotFoundError: + pass + except (ValueError, OSError) as exc: + raise SyncDocumentError(f"the salt file {path} is unusable: {exc}") from exc + try: + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + except FileExistsError: + continue + with os.fdopen(fd, "w", encoding="ascii") as handle: + handle.write(secrets.token_hex(32) + "\n") + raise SyncDocumentError(f"cannot create the salt file {path}") + + +def hash_name(name: str, salt: bytes) -> str: + return hmac.new(salt, name.encode("utf-8"), hashlib.sha256).hexdigest() + + +def _json_list(raw: Any) -> list[str]: + try: + value = json.loads(raw or "[]") + except (TypeError, ValueError): + return [] + return [str(entry) for entry in value] if isinstance(value, list) else [] + + +def _egress(db_path: str, context: RunContext, key: str) -> dict[str, Any]: + if not context.egress_collected: + return {"collected": False, "truncated": False, "names": "plain", "items": []} + + hashed = context.egress_names == "hashed" + salt = load_salt(key) if hashed else b"" + + tiers_by_row: dict[int, list[dict[str, Any]]] = {} + for tier in load_data("egress_inventory_tier", db_path=db_path): + tiers_by_row.setdefault(tier["egress_inventory_id"], []).append(tier) + + trimmed = { + "items dropped": 0, + "names shortened": 0, + "sizes capped": 0, + "flags dropped": 0, + "notes dropped": 0, + "text shortened": 0, + "tiers dropped": 0, + } + + def size_of(value: Any) -> int | None: + if value is None: + return None + number = max(0, int(value)) + if number > MAX_SIZE_BYTES: + trimmed["sizes capped"] += 1 + return MAX_SIZE_BYTES + return number + + records = load_data("egress_inventory", db_path=db_path) + items = [] + for record in records: + name = str(record["name"]) + if hashed: + name = hash_name(name, salt) + else: + name, cut = _cut(name, MAX_NAME_CHARS) + trimmed["names shortened"] += cut + + size = None if record["size_unknown"] else size_of(record["size_bytes"]) + item: dict[str, Any] = { + "type_id": int(record["resource_type"]), + "name": name, + "size_bytes": size, + } + + for field, limit, chars in ( + ("flags", MAX_FLAGS, MAX_FLAG_CHARS), + ("notes", MAX_NOTES, MAX_NOTE_CHARS), + ): + values = _json_list(record[field]) + trimmed[f"{field} dropped"] += max(0, len(values) - limit) + kept = [] + for value in values[:limit]: + value, cut = _cut(value, chars) + trimmed["text shortened"] += cut + kept.append(value) + item[field] = kept + + tiers = sorted( + tiers_by_row.get(record["id"], []), + key=lambda tier: (-(tier["size_bytes"] or 0), str(tier["tier"])), + ) + trimmed["tiers dropped"] += max(0, len(tiers) - MAX_TIERS) + item["tiers"] = [] + for tier in tiers[:MAX_TIERS]: + label, cut = _cut(str(tier["tier"]), MAX_TIER_CHARS) + trimmed["text shortened"] += cut + item["tiers"].append( + { + "tier": label, + "size_bytes": size_of(tier["size_bytes"]), + "is_archive": bool(tier["is_archive"]), + } + ) + items.append(item) + + items.sort( + key=lambda item: ( + item["size_bytes"] is None, + -(item["size_bytes"] or 0), + item["type_id"], + item["name"], + ) + ) + if len(items) > MAX_EGRESS_ITEMS: + trimmed["items dropped"] = len(items) - MAX_EGRESS_ITEMS + items = items[:MAX_EGRESS_ITEMS] + + counts = {label: count for label, count in trimmed.items() if count} + if counts: + logger.warning( + "Egress data trimmed to the API v2 limits: %s.", + ", ".join(f"{count} {label}" for label, count in counts.items()), + ) + return { + "collected": True, + "truncated": bool(counts), + "names": "hashed" if hashed else "plain", + "items": items, + } + + +def _catalogue_claim(db_path: str, provider: str) -> dict[str, Any] | None: + try: + info = catalogue_info(db_path) + except CatalogueError: + return None + if ( + info is None + or not CATALOGUE_VERSION_RE.match(info.version) + or not isinstance(info.sequence, int) + or not isinstance(info.sha256, str) + or not SHA256_RE.match(info.sha256) + ): + # An older dataset has no usable catalogue_meta: the version is unknown. + return None + return { + "version": info.version, + "sequence": info.sequence, + "sha256": info.sha256, + "resource_type_ids": active_resource_type_ids(provider, db_path)[ + :MAX_CATALOGUE_TYPE_IDS + ], + } + + +def build_sync_document(report_path: str, context: RunContext) -> dict[str, Any]: + db_path = os.path.join(report_path, "data", "assessment.db") + raw_data_path = os.path.join(report_path, "raw_data") + provider = PROVIDERS.get(context.cloud_service_provider, "") + completed_at = context.completed_at or int(time.time()) + + scope = build_scope(context, raw_data_path) + try: + key = scope_key(scope) + except ScopeError as exc: + raise SyncDocumentError( + f"the scope is not valid ({exc.pointer or 'scope'}: {exc})" + ) from exc + + collector: dict[str, Any] = { + "name": COLLECTOR_NAME, + "version": __version__, + "mode": "tfstate" if "tfstate" in scope else "api", + } + try: + claim = _catalogue_claim(db_path, provider) + except sqlite3.Error as exc: + raise SyncDocumentError(f"cannot read the run's catalogue: {exc}") from exc + if claim: + collector["catalogue"] = claim + + sync: dict[str, Any] = { + "id": context.sync_id, + "started_at": context.started_at, + "completed_at": max(completed_at, context.started_at), + "collector": collector, + } + if context.ci: + sync["ci"] = context.ci + + document: dict[str, Any] = {"schema_version": SCHEMA_VERSION, "sync": sync} + if context.target: + document["target"] = context.target + document["assessment_type"] = ( + 3 if context.egress_collected else min(max(context.assessment_type, 1), 3) + ) + document["scope"] = scope + try: + document["inventory"] = { + "resources": _resources(db_path), + "costs": _costs(db_path, sync["completed_at"]), + "egress": _egress(db_path, context, key), + } + except sqlite3.Error as exc: + raise SyncDocumentError(f"cannot read the run's data: {exc}") from exc + return document + + +def _is_int(value: Any, low: int = 0, high: int | None = None) -> bool: + return ( + isinstance(value, int) + and not isinstance(value, bool) + and value >= low + and (high is None or value <= high) + ) + + +def _check_text( + problems: list[str], pointer: str, value: Any, limit: int, low: int = 0 +) -> None: + if not isinstance(value, str) or not low <= len(value) <= limit: + problems.append(f"{pointer}: must be a string of {low} to {limit} characters") + + +def validate_document(document: dict[str, Any]) -> None: + problems: list[str] = [] + + def require(pointer: str, ok: bool, message: str) -> None: + if not ok: + problems.append(f"{pointer}: {message}") + + for name in ("schema_version", "sync", "assessment_type", "scope", "inventory"): + require(f"/{name}", name in document, "is required") + if problems: + raise SyncDocumentError("the sync document is invalid", problems) + + require( + "/schema_version", + isinstance(document["schema_version"], str) + and re.match(r"^2\.[0-9]+$", document["schema_version"]), + "must look like 2.", + ) + require( + "/assessment_type", + _is_int(document["assessment_type"], 1, 3), + "must be 1, 2 or 3", + ) + + sync = document["sync"] + require("/sync/id", bool(UUID7_RE.match(str(sync.get("id")))), "must be a UUIDv7") + started, completed = sync.get("started_at"), sync.get("completed_at") + require("/sync/started_at", _is_int(started, 0, MAX_EPOCH), "must be epoch seconds") + require( + "/sync/completed_at", _is_int(completed, 0, MAX_EPOCH), "must be epoch seconds" + ) + if _is_int(started) and _is_int(completed): + require("/sync/completed_at", completed >= started, "is before started_at") + + collector = sync.get("collector") or {} + _check_text(problems, "/sync/collector/name", collector.get("name"), 64, 1) + _check_text(problems, "/sync/collector/version", collector.get("version"), 64, 1) + require( + "/sync/collector/mode", + collector.get("mode") in ("api", "tfstate"), + "must be api or tfstate", + ) + claim = collector.get("catalogue") + if claim is not None: + ids = claim.get("resource_type_ids") + require( + "/sync/collector/catalogue", + bool(CATALOGUE_VERSION_RE.match(str(claim.get("version")))) + and _is_int(claim.get("sequence")) + and bool(SHA256_RE.match(str(claim.get("sha256")))) + and isinstance(ids, list) + and len(ids) <= MAX_CATALOGUE_TYPE_IDS + and len(set(ids)) == len(ids) + and all(_is_int(entry, 1) for entry in ids), + "needs version, sequence, sha256 and unique resource_type_ids", + ) + + scope = document["scope"] + try: + scope_key(scope) + except ScopeError as exc: + problems.append(f"/scope{exc.pointer}: {exc}") + require( + "/scope", + ("tfstate" in scope) == (collector.get("mode") == "tfstate"), + "tfstate must be sent exactly when the collector mode is tfstate", + ) + require( + "/scope/provider", + scope.get("provider") in ("aws", "azure"), + "must be aws or azure", + ) + + inventory = document["inventory"] + resources = inventory.get("resources") + if not isinstance(resources, list) or len(resources) > MAX_RESOURCE_ITEMS: + problems.append( + f"/inventory/resources: needs at most {MAX_RESOURCE_ITEMS} items" + ) + resources = [] + for index, item in enumerate(resources): + pointer = f"/inventory/resources/{index}" + require(f"{pointer}/type_id", _is_int(item.get("type_id"), 1), "must be >= 1") + require( + f"{pointer}/count", + _is_int(item.get("count"), 0, MAX_COUNT_PER_ITEM), + f"must be 0 to {MAX_COUNT_PER_ITEM}", + ) + _check_text(problems, f"{pointer}/location", item.get("location"), 255) + if "type_code" in item: + _check_text(problems, f"{pointer}/type_code", item["type_code"], 255) + + costs = inventory.get("costs", []) + if not isinstance(costs, list) or len(costs) > MAX_COST_ITEMS: + problems.append(f"/inventory/costs: needs at most {MAX_COST_ITEMS} items") + costs = [] + for index, item in enumerate(costs): + pointer = f"/inventory/costs/{index}" + require( + f"{pointer}/month", + bool(MONTH_RE.match(str(item.get("month")))), + "is not YYYY-MM", + ) + amount = item.get("amount") + require( + f"{pointer}/amount", + isinstance(amount, str) + and len(amount) <= 40 + and bool(AMOUNT_RE.match(amount)), + "must be a decimal string", + ) + require( + f"{pointer}/currency", + bool(CURRENCY_RE.match(str(item.get("currency")))), + "must be three letters", + ) + require( + f"{pointer}/final", + isinstance(item.get("final", False), bool), + "must be a boolean", + ) + + egress = inventory.get("egress") + if egress is not None: + _validate_egress(egress, require, problems) + + body = json.dumps(document, separators=(",", ":"), ensure_ascii=False) + if len(body.encode("utf-8")) > MAX_BODY_BYTES: + problems.append(f"/: the document is larger than {MAX_BODY_BYTES} bytes") + + if problems: + raise SyncDocumentError("the sync document is invalid", problems) + + +def _validate_egress(egress: dict[str, Any], require: Any, problems: list[str]) -> None: + require( + "/inventory/egress/collected", + isinstance(egress.get("collected"), bool), + "must be a boolean", + ) + require( + "/inventory/egress/truncated", + isinstance(egress.get("truncated", False), bool), + "must be a boolean", + ) + require( + "/inventory/egress/names", + egress.get("names", "plain") in ("plain", "hashed"), + "must be plain or hashed", + ) + + items = egress.get("items", []) + if not isinstance(items, list) or len(items) > MAX_EGRESS_ITEMS: + problems.append( + f"/inventory/egress/items: needs at most {MAX_EGRESS_ITEMS} items" + ) + return + for index, item in enumerate(items): + pointer = f"/inventory/egress/items/{index}" + require(f"{pointer}/type_id", _is_int(item.get("type_id"), 1), "must be >= 1") + _check_text(problems, f"{pointer}/name", item.get("name"), MAX_NAME_CHARS) + size = item.get("size_bytes") + require( + f"{pointer}/size_bytes", + size is None or _is_int(size, 0, MAX_SIZE_BYTES), + "must be null or 0 to 2^53-1", + ) + for field, limit, chars in ( + ("flags", MAX_FLAGS, MAX_FLAG_CHARS), + ("notes", MAX_NOTES, MAX_NOTE_CHARS), + ): + values = item.get(field, []) + if not isinstance(values, list) or len(values) > limit: + problems.append(f"{pointer}/{field}: needs at most {limit} items") + continue + for position, value in enumerate(values): + _check_text(problems, f"{pointer}/{field}/{position}", value, chars) + tiers = item.get("tiers", []) + if not isinstance(tiers, list) or len(tiers) > MAX_TIERS: + problems.append(f"{pointer}/tiers: needs at most {MAX_TIERS} items") + continue + for position, tier in enumerate(tiers): + tier_pointer = f"{pointer}/tiers/{position}" + _check_text( + problems, f"{tier_pointer}/tier", tier.get("tier"), MAX_TIER_CHARS, 1 + ) + tier_size = tier.get("size_bytes") + require( + f"{tier_pointer}/size_bytes", + tier_size is None or _is_int(tier_size, 0, MAX_SIZE_BYTES), + "must be null or 0 to 2^53-1", + ) + require( + f"{tier_pointer}/is_archive", + isinstance(tier.get("is_archive", False), bool), + "must be a boolean", + ) + + +def summary_line(document: dict[str, Any], size: int, digest: str) -> str: + inventory = document["inventory"] + egress = inventory["egress"] + return ( + f"Sync document {document['sync']['id']}: " + f"{len(inventory['resources'])} resource item(s), " + f"{len(inventory['costs'])} cost month(s), " + f"egress collected={str(egress['collected']).lower()} " + f"truncated={str(egress['truncated']).lower()} " + f"({len(egress['items'])} item(s)), {size} bytes, sha256 {digest}" + ) + + +def write_sync_document( + document: dict[str, Any], raw_data_path: str, emit_dir: str | None = None +) -> SyncOutput: + data = (json.dumps(document, indent=2, ensure_ascii=False) + "\n").encode("utf-8") + digest = hashlib.sha256(data).hexdigest() + + path = Path(raw_data_path) / "sync.json" + path.write_bytes(data) + + emitted = None + if emit_dir: + emitted = Path(emit_dir) / f"{document['sync']['id']}.json" + emitted.parent.mkdir(parents=True, exist_ok=True) + emitted.write_bytes(data) + + summary = summary_line(document, len(data), digest) + logger.info(summary) + return SyncOutput(path, len(data), digest, emitted, summary) + + +def prepare_sync_document( + report_path: str, context: RunContext, emit_dir: str | None = None +) -> SyncOutput: + document = build_sync_document(report_path, context) + validate_document(document) + return write_sync_document( + document, os.path.join(report_path, "raw_data"), emit_dir + ) diff --git a/core/utils_sync.py b/core/utils_sync.py index a40c108..9e1c0c5 100644 --- a/core/utils_sync.py +++ b/core/utils_sync.py @@ -1,7 +1,6 @@ # core/utils_sync.py from __future__ import annotations -import json import logging import os import time @@ -77,33 +76,12 @@ def _build_payload( }, } - logger.debug("Outgoing payload:\n%s", json.dumps(payload, indent=2)) - return payload - - -def write_assessment_payload( - raw_data_path: str, - *, - report_path: str, - name: str, - started_at: int, - exit_strategy: int, - cloud_service_provider: int, - assessment_type: int, -) -> str: - payload = _build_payload( - report_path=report_path, - name=name, - started_at=started_at, - exit_strategy=exit_strategy, - cloud_service_provider=cloud_service_provider, - assessment_type=assessment_type, + logger.debug( + "Outgoing payload: %d resource row(s), %d cost month(s).", + len(res_payload), + len(cost_payload), ) - payload_path = os.path.join(raw_data_path, "payload.json") - with open(payload_path, "w", encoding="utf-8") as payload_file: - json.dump(payload, payload_file, indent=2) - payload_file.write("\n") - return payload_path + return payload def post_assessment( diff --git a/docs/api-v2/MANIFEST.sha256 b/docs/api-v2/MANIFEST.sha256 new file mode 100644 index 0000000..6d9102b --- /dev/null +++ b/docs/api-v2/MANIFEST.sha256 @@ -0,0 +1,59 @@ +119ef59f3225d40f64d1f9e030cee3ccc821768d82ca14bbea5bd9895b932739 README.md +c28fcca53637bc88e124af1725df13cb98c69dedefd62fb3cdbe1cdb6b760624 VERSION +adb8ff4e053be47dba564c849c9c2b4d10b3f39bf8a8284eca4e5d2764e2cf3a errors.yaml +14e3a8113d17528a1262eaf0362d5f9ad0378d97d6bd0d03b2b37d9463741c02 examples/01-meta.json +ad1b134a5fccea08ea219ffab57e666abc96687ace57e37acba49fc39c6fd76c examples/02-whoami_assessment_key.json +7110af9481d5f48cd84550ca5e34a88557a15cb034070ca37dd4ba1ea38e5bcd examples/03-whoami_account_key.json +de49ac858af2034de1f413ae68deb0a1916162a36466742c013c77c152aedf3a examples/04-request_id_replaced.json +863cde2521fc182049cb00cdab5ecdb46e794db9fd9a8398e214941153819f53 examples/05-first_sync_assessment_key.json +372796913b846ad64747b997adc66e4abcb20a0fab35f79e575b9852a3dcfe71 examples/06-second_sync_appended.json +9158eadc340c7fe08b02152fd7d86b87a60431d68ef80a5b0ee85e688e33dab7 examples/07-idempotent_replay.json +faeddd68c2abbe4fb3d83f96a1a57b43352d8a29886fe262aad32f59d856c995 examples/08-idempotency_key_reused.json +fe206334e0a138e44724e6839b34fc1b03c1e6d709b8c41e0eab8b2cb8a45444 examples/09-idempotency_key_missing.json +ff0c6d17a3ab5b402327f95a6719fab0d14776b59b039fb3ad41b653d517dbf8 examples/10-idempotency_key_mismatch.json +48e54d947243062912a02115c204d6803a2422c7a9dbc4a6eb09cb7568c7baf0 examples/11-scope_mismatch.json +8bc7b5ec8108eb9baa398e848d55288c249c5107ddb3c90bb438c921fa4e3eb4 examples/12-provider_mismatch.json +d4aa8cb874783f7b3c092b290fc54247623f7b9c688689f6391b4a904a226c78 examples/13-depth_too_deep.json +90823cf5874830853aaabacc85daa3a0a0e6ca12938f856b769077062f526de0 examples/14-depth_three_without_egress.json +fe1be812a9e516fb39f94cf6d9bc03934804c18ac99a33a9cbf4e5e51993aab4 examples/15-depth_three_with_egress.json +75fd6f19ca47f3d4c7c6c19229f3191fa5bf7de114b9fb84ad73a7015d88bc16 examples/16-unknown_resource_types.json +d125333273504e8beb0699ad51d8b05facfad2cc3e5e6922ad84e78b11f0cf56 examples/17-inactive_resource_types.json +7a065e6e721b239e48818aa35bc3977c1e5909cb565e27ff9165e7b61d133069 examples/18-all_resource_types_unknown.json +280f6b4bce363fb967b0e07cd664bb5adaec451f0a4ee8c5ea236f4a71452b54 examples/19-duplicate_items_merged.json +ae3ec39e597111545a26793ce414f2384a0c26007fe89654f146a2eb1ed57777 examples/20-catalogue_collector_older.json +5b58b3e06356b03dc9658deb4e6de541de3240da30df09740df4b3edfe7d4011 examples/21-catalogue_collector_newer.json +7dba2037e7c781d4a8a8518933ec8d76e42e61a232eb655b2e5d7704ddf1e4f4 examples/22-catalogue_versions_compared_as_numbers.json +234e5f435bfdaa112b22d96da52a2c11b8ffb4c677866452dc7a71d909c55757 examples/23-catalogue_missing.json +704f22ccedfa0c0989ec01363c7e09be93430848b878fc79c97810e534c80b5f examples/24-catalogue_sha256_mismatch.json +9b5a66c7d26b3fd38e543fec74ad405a4c69a00112591f95b744d6219a199975 examples/25-account_key_creates_assessment.json +391c5368d3e41729452d538de4d32d3274dd734fca72dd0b5d6623f7e54b5df3 examples/26-account_key_without_create_scope.json +b14ce21dcec8ec2eeb2f533922fc7f43224c7168f9da776e3591a8c2252f790c examples/27-account_key_without_target.json +edeaafa1a820d451706060731b2181df7c351ec192c8403ba406f9adc4f9939c examples/28-account_key_appends_to_named_assessment.json +bc101c21838bdfcce2611e65f9fd555d098dc8378a271fe31df06aa5611dc9da examples/29-account_key_unknown_assessment.json +f4370df95a03499ddb7e060abf83226240844963140952ac803ebb0006f982e0 examples/30-assessment_key_for_another_assessment.json +fb5151a3cfc9adcff90adc2f49f1ffe1493f1fd508f16467e4d6b56eb245c1d7 examples/31-key_revoked.json +71fe13b2307c572cef5f16ca358e295154beab9a52cd60c43977dc97e157f28a examples/32-key_expired.json +8cf143b65858d9bb31c3b6f76d2d28dec573fe9a9c5349c5db8977adaa751028 examples/33-key_malformed.json +44bd3e413e29ce06b8607f5d2532b5a8c663d2f9d6a489814fe5ccfe0cf44570 examples/34-key_bad_checksum.json +c77740bee9234b96212e873d4f73b3a0f58266bed8ed2eb4396d3d82ebef8c3a examples/35-authorization_missing.json +fff0522b3f17ae006cd1fbffd7c4f906e0a1022cebde724c09e30b44c6110ac2 examples/36-payload_too_large.json +813db769cde25a4974c021518ff33853525ed84ba1982db736a589a1cd5e35b3 examples/37-validation_errors.json +0dbd114cf078bbacc7104e284b287f3df5cfebd72ea798136b62f0c6a2c6807f examples/38-collection_too_old.json +513b03ba3c8dc171b0fda7262131f5ac84e408b91f275e0e6888b6efbeda00a4 examples/39-invalid_json.json +52079cf6503494d8b559573fa4f5847958c8cb7ba228cf4c6ff7d71f27e99e4f examples/40-unsupported_media_type.json +f3bd0d876ac4bf9ce03fa3b21bc53d8744d324ec58e7512a7daad2255970737e examples/41-unsupported_schema_version.json +0e6cafc93491bf2da0ba5e83b256b640ce7fc2bed61265771f2521abee497033 examples/42-get_sync.json +7b8ed52a8a5991b3ba54d97a36883ae0b9deaeb6de34d2c185967cfbf4560946 examples/43-get_sync_not_found.json +674249eff8de98090eef137278e37c575d4bf94dbddd5092fe28f17d460583e1 examples/44-get_assessment.json +8bc9d7efd965c993abfad73b243ee7f3c81163f73ea7bd09a76e4b94cac54e02 examples/45-get_assessment_not_found.json +353b6e7c02921356f969b5c02b2a186a5001f705be75db93d320a0abdcff9590 examples/46-list_assessments_second_page.json +4ca56306854c5277612af4fb72bd9386ed54d0fa7f12d21b3cca998735369c8f examples/47-list_assessments_by_provider.json +bf53352b6483364653d6c9bef5c85e061f316ef19c0a47540d27e4fe524c0555 examples/48-list_assessments_needs_read_scope.json +e4c838a8113cecf12dac5c7e25e13c68d2d4e7b7ff46f1f5f2eaa7748a401d66 examples/49-list_assessments_bad_cursor.json +cf11e67f0a2620ceed63f683f099acd2014f012848ab1ce7f8afbee693395ae2 examples/50-list_assessment_syncs.json +44c5925c07a88d4381f70216f6d33f3fcac68e5c4f1215b9d02e638fdb686687 examples/51-insufficient_credits.json +fff61bd1caa19aeaa793b5982f907cbe1b3e6c0aa4fe75e692430e07da1dbd72 fixtures/catalogue.json +99226f1eda12219a8a3d35409d13cb0afdfd31c90793068ebd27eda77881e849 make_manifest.py +43556abba1039b12902bf9deebd7541986d7f4a7b44b52d9130ce31a6caa3c55 openapi.yaml +a4acd6933651cd77cb80a67421db9739991bc450ca2b02a0db57a048dc50197a vectors/content_sha256.json +712f70b59424a85f31ab359e29a66b25cb942784549ce9f5514d1e3c56a4fb75 vectors/scope_key.json diff --git a/docs/api-v2/README.md b/docs/api-v2/README.md new file mode 100644 index 0000000..46038e5 --- /dev/null +++ b/docs/api-v2/README.md @@ -0,0 +1,68 @@ +# API v2 definition + +The definition of `/api/v2`, the contract between the cloudexit CLI and the hosts (EscapeCloud and ExitCloud). It is the single source of truth: both hosts and the CLI carry an identical copy. + +| File | What it is | +| --- | --- | +| `VERSION` | The version of the definition. | +| `openapi.yaml` | The API (OpenAPI 3.1): endpoints, headers, request and response schemas, limits. | +| `errors.yaml` | The error and warning code registry, with HTTP status, retry flag and the CLI exit code. | +| `vectors/scope_key.json` | Test vectors for the canonical `scope_key`. | +| `vectors/content_sha256.json` | Test vectors for the canonical content form and `content_sha256`. | +| `fixtures/catalogue.json` | The catalogue the examples run against. | +| `examples/*.json` | One scenario per file: the fixtures it needs, a request and what to expect. | +| `make_manifest.py`, `MANIFEST.sha256` | The file hashes that keep copies identical. | + +## Versions + +- `VERSION` is `MAJOR.MINOR.PATCH`. The major is the path (`/api/v2`). +- Additive changes (new optional request fields, new response fields, new enum values, new error or warning codes, new endpoints) raise the **minor** version. Fixes to wording and examples raise the patch. +- A breaking change is a new major: `/api/v3` paths and a new definition. +- `schema_version` in a request body is `2.`. `GET /meta` lists the minors a host accepts. + +## Keeping copies identical + +**Every copy must keep the same `VERSION` and the same files.** After any change run + +```bash +python docs/api-v2/make_manifest.py +``` + +and commit `MANIFEST.sha256` with the change. A copy whose files and manifest disagree is not a faithful copy. + +## Examples + +Each file in `examples/` is one scenario that a harness replays against a host. + +### An example file + +```json +{ + "name": "…", "description": "…", + "host": "escapecloud | exitcloud", + "fixtures": {"catalogue": "standard", "assessments": [], "keys": [], "credits": 0}, + "before": [{"request": {}, "expect": {}, "save": {"name": "/json/pointer"}}], + "request": {"method": "POST", "path": "/api/v2/syncs", "headers": {}, "body": {}}, + "expect": {"status": 201} +} +``` + +- `host` is optional. Without it the example runs on every host. +- `before` steps run first. A step may `save` values of its response (JSON pointers) as placeholders. +- `request` may instead have `raw_body` (a string sent as it is), or `pad_bytes_to` (the harness adds an unknown field so the body is that many bytes long). +- `expect` has `status`, `headers` (exact), `headers_present`, `headers_differ`, `body_subset`, `exists` (JSON pointers that must exist), `absent` and `lengths` (JSON pointer to list length). +- `body_subset` matches objects by their keys and lists by containment: every expected item must equal or be a subset of some actual item, in any order. + +### Fixtures + +- `catalogue`: `"standard"` (`fixtures/catalogue.json`), or `{"use": "standard", "version": "…"}` to change fields. The host installs it as its active catalogue. +- `assessments`: `{name, provider, exit_strategy, assessment_type, scope_key}`. `scope_key` is what the first sync already recorded (null before it). The assessments are created in order. +- `keys`: `{name, kind, assessment?, scopes?, state?}`. `kind` is `assessment` (with the `assessment` it is bound to; scope `syncs:write`) or `account`. `state` is `active` (default), `revoked` or `expired`. +- `credits`: the credit balance of the tenant (ExitCloud only). + +### Placeholders + +- A key is `{{}}`, the raw key. `{{|corrupt}}` is the same key with a wrong checksum. +- An assessment is `{{_id}}`. +- `{{now}}`, `{{now-300}}`, `{{now+60}}`: the current time in epoch seconds. A value that is only a placeholder keeps its type, so these become integers. +- Names saved by a `before` step. diff --git a/docs/api-v2/VERSION b/docs/api-v2/VERSION new file mode 100644 index 0000000..227cea2 --- /dev/null +++ b/docs/api-v2/VERSION @@ -0,0 +1 @@ +2.0.0 diff --git a/docs/api-v2/errors.yaml b/docs/api-v2/errors.yaml new file mode 100644 index 0000000..99304c1 --- /dev/null +++ b/docs/api-v2/errors.yaml @@ -0,0 +1,277 @@ +version: "2.0" +envelope: + example: + object: error + api_version: "2" + error: + code: validation_failed + message: 2 fields are invalid. + retryable: false + request_id: 9a3f0c1d2e4b5a67 + details: + - pointer: /inventory/resources/3/count + code: out_of_range + message: must be between 0 and 10000 + docs_url: https://cloudexit.escapecloud.io/errors/validation_failed + rules: + - Messages are safe to show to a user and never contain exception text or secrets. + - Clients branch on error.code. For an unknown code they fall back to the HTTP status class and error.retryable. + - details is always a list. An item has a code and a message, and a pointer (a JSON pointer into the request body) or a parameter (a query or header name) when it points at one input. Code-specific values are further keys of an item; the details entries below name them. + - request_id is the same value as the X-Request-ID response header. +detail_codes: + required: A required field is missing. + invalid_type: The value has the wrong type. + invalid_format: The value does not match the expected format or pattern. + out_of_range: A number is outside its allowed range. + too_long: A string is longer than allowed. + too_many_items: A list is longer than allowed. + not_allowed: The value is not one of the allowed values, or the combination of fields is not allowed. + before_started_at: completed_at is earlier than started_at. + too_old: completed_at is older than limits.max_collection_age_days. + in_the_future: A time is further in the future than limits.clock_skew_seconds allows. +exit_codes: + 10: The request was rejected as invalid. Fix the input; retrying the same request will not help. + 11: Authentication or authorisation failed. Check the key, its scopes and its binding. + 12: A temporary condition. Retrying with the same Idempotency-Key is safe. + 13: Credits or a quota ran out. + 14: The target assessment is missing, deleted, archived or does not match the sync. + 15: The client version, schema version or catalogue is not accepted. +errors: + unauthenticated: + http: 401 + retryable: false + cli_exit: 11 + hosts: [escapecloud, exitcloud] + meaning: The Authorization header is missing or not a Bearer credential. + invalid_api_key: + http: 401 + retryable: false + cli_exit: 11 + hosts: [escapecloud, exitcloud] + meaning: The key is malformed, has a bad checksum or is unknown (also for keys of another host). + api_key_expired: + http: 401 + retryable: false + cli_exit: 11 + hosts: [escapecloud, exitcloud] + meaning: The key is past its expiry date. + api_key_revoked: + http: 401 + retryable: false + cli_exit: 11 + hosts: [escapecloud, exitcloud] + meaning: The key was revoked, or the assessment it was bound to was deleted. + wrong_region: + http: 403 + retryable: false + cli_exit: 11 + hosts: [exitcloud] + meaning: The key belongs to the other ExitCloud region. + details: expected_host, the base URL of the region the key belongs to. + principal_inactive: + http: 403 + retryable: false + cli_exit: 11 + hosts: [escapecloud, exitcloud] + meaning: The service account or user behind the key is deactivated. + insufficient_scope: + http: 403 + retryable: false + cli_exit: 11 + hosts: [escapecloud, exitcloud] + meaning: The key lacks the scope the request needs. + details: required, the list of accepted scopes. + key_binding_violation: + http: 403 + retryable: false + cli_exit: 11 + hosts: [escapecloud, exitcloud] + meaning: An assessment key was used for an assessment other than its own. + assessment_key_v1: + http: 403 + retryable: false + cli_exit: 11 + hosts: [escapecloud, exitcloud] + api: v1 + meaning: An assessment key was used on the v1 API, which accepts account keys only. Returned by /api/v1 in the v1 error envelope, never by /api/v2. + idempotency_key_missing: + http: 400 + retryable: false + cli_exit: 10 + hosts: [escapecloud, exitcloud] + meaning: A POST without an Idempotency-Key header, or with one that does not match ^[A-Za-z0-9_-]{16,64}$. + idempotency_key_mismatch: + http: 422 + retryable: false + cli_exit: 10 + hosts: [escapecloud, exitcloud] + meaning: The Idempotency-Key header differs from sync.id. + idempotency_key_reused: + http: 422 + retryable: false + cli_exit: 10 + hosts: [escapecloud, exitcloud] + meaning: The Idempotency-Key was used before with a different request body. + request_in_progress: + http: 409 + retryable: true + cli_exit: 12 + hosts: [escapecloud, exitcloud] + meaning: A request with the same Idempotency-Key is still being processed. Retry after Retry-After. + invalid_json: + http: 400 + retryable: false + cli_exit: 10 + hosts: [escapecloud, exitcloud] + meaning: The body is not valid UTF-8 JSON. + unsupported_media_type: + http: 415 + retryable: false + cli_exit: 10 + hosts: [escapecloud, exitcloud] + meaning: The Content-Type is not application/json. + payload_too_large: + http: 413 + retryable: false + cli_exit: 10 + hosts: [escapecloud, exitcloud] + meaning: The body is larger than limits.max_body_bytes. + details: limit, the name of the limit (max_body_bytes). + limit_exceeded: + http: 422 + retryable: false + cli_exit: 10 + hosts: [escapecloud, exitcloud] + meaning: A list or value is beyond a limit announced in /meta. + details: limit, the name of the limit, from limits in /meta. + validation_failed: + http: 422 + retryable: false + cli_exit: 10 + hosts: [escapecloud, exitcloud] + meaning: The body does not match the schema, or a value is out of range. details lists every problem with a JSON pointer. + catalogue_mismatch: + http: 422 + retryable: false + cli_exit: 10 + hosts: [escapecloud, exitcloud] + meaning: Every resource in the sync is of a type the host does not know. + catalogue_sha256_mismatch: + http: 422 + retryable: false + cli_exit: 10 + hosts: [escapecloud, exitcloud] + meaning: The collector's catalogue has the same version as the host's but a different sha256 (a corrupt or tampered dataset). + details: host_version and host_sha256, the catalogue the host has. + unsupported_schema_version: + http: 400 + retryable: false + cli_exit: 15 + hosts: [escapecloud, exitcloud] + meaning: schema_version is not one the host accepts. + details: supported, the list from /meta.contract.schema_versions. + client_version_unsupported: + http: 400 + retryable: false + cli_exit: 15 + hosts: [escapecloud, exitcloud] + meaning: The collector version is below the host's minimum or is blocked. + details: min_supported and recommended, from /meta.clients. + assessment_not_found: + http: 404 + retryable: false + cli_exit: 14 + hosts: [escapecloud, exitcloud] + meaning: There is no such assessment for this tenant (also for assessments of another tenant). + sync_not_found: + http: 404 + retryable: false + cli_exit: 14 + hosts: [escapecloud, exitcloud] + meaning: There is no such sync for this tenant (also for syncs of another tenant). + assessment_deleted: + http: 410 + retryable: false + cli_exit: 14 + hosts: [escapecloud, exitcloud] + meaning: The assessment was deleted. Deleting an assessment also revokes its keys, so a bound key normally gets api_key_revoked. + assessment_archived: + http: 409 + retryable: false + cli_exit: 14 + hosts: [escapecloud, exitcloud] + meaning: An archived assessment does not accept new syncs. + target_mismatch: + http: 409 + retryable: false + cli_exit: 14 + hosts: [escapecloud, exitcloud] + meaning: The sync does not fit the assessment. + details: field (provider, exit_strategy, scope_key or assessment_type), expected (the assessment's value) and actual (the sync's value). + insufficient_credits: + http: 402 + retryable: false + cli_exit: 13 + hosts: [exitcloud] + meaning: The organization has too few credits for this sync. + details: required (the credits the sync costs) and balance, both credit amounts. + quota_exceeded: + http: 429 + retryable: false + cli_exit: 13 + hosts: [escapecloud, exitcloud] + meaning: A daily or monthly quota of the tenant is used up. + details: resets_at, when the quota resets, in epoch seconds. + rate_limited: + http: 429 + retryable: true + cli_exit: 12 + hosts: [escapecloud, exitcloud] + meaning: Too many requests. Retry after Retry-After. + maintenance: + http: 503 + retryable: true + cli_exit: 12 + hosts: [escapecloud, exitcloud] + meaning: The host is in maintenance. Retry after Retry-After. + service_unavailable: + http: 503 + retryable: true + cli_exit: 12 + hosts: [escapecloud, exitcloud] + meaning: A dependency of the host is unavailable. + internal_error: + http: 500 + retryable: true + cli_exit: 12 + hosts: [escapecloud, exitcloud] + meaning: Something failed on the host. Retrying with the same Idempotency-Key is safe. +warnings: + unknown_resource_types: + meaning: Resources of types unknown to the host's catalogue were stored but not scored. + details: type_ids lists the types. + resource_type_inactive: + meaning: Resources of types the host has deactivated were stored but not scored. + details: type_ids lists the types. + resource_type_code_mismatch: + meaning: A resource's type_code differs from the host's code for that type_id. The type_id wins. + details: type_ids lists the types. + duplicate_items_merged: + meaning: Resource items with the same type_id and location were merged by summing their counts. + tier_downgraded: + meaning: Depth 3 was requested without collected egress data, so the sync was recorded as depth 2. + name_ignored: + meaning: A name was sent for an existing assessment and differs from its name. Names are set at creation. + key_expiring: + meaning: The key expires within 14 days. + details: expires_at is the expiry, in epoch seconds. + client_version_deprecated: + meaning: The collector version is deprecated (below clients.cloudexit.deprecated_below). + collector_catalogue_outdated: + meaning: The collector's catalogue is older than the host's, so types added since were not collected. + details: missing_resource_types lists the host's active types for the provider that are not in the collector's resource_type_ids. + host_catalogue_outdated: + meaning: The collector's catalogue is newer than the host's, or it names types the host does not know yet. + details: unknown_type_ids lists the ids the host does not know. + collector_catalogue_unknown: + meaning: The sync carries no catalogue block, so the catalogues cannot be compared. diff --git a/docs/api-v2/examples/01-meta.json b/docs/api-v2/examples/01-meta.json new file mode 100644 index 0000000..00f8c1a --- /dev/null +++ b/docs/api-v2/examples/01-meta.json @@ -0,0 +1,77 @@ +{ + "name": "meta", + "description": "Capabilities need no key. In 2.0 processing is inline, upsert by scope is off and no tenant policy rejects old catalogues.", + "fixtures": { + "catalogue": "standard", + "assessments": [], + "keys": [] + }, + "request": { + "method": "GET", + "path": "/api/v2/meta", + "headers": {} + }, + "expect": { + "status": 200, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "meta", + "api_version": "2", + "contract": { + "schema_versions": [ + "2.0" + ], + "current": "2.0" + }, + "catalogue": { + "version": "2026-11-02", + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "collector_min_version": null + }, + "limits": { + "max_body_bytes": 1000000, + "max_resource_items": 1000, + "max_count_per_item": 10000, + "max_cost_items": 12, + "max_egress_items": 1000, + "clock_skew_seconds": 86400, + "max_collection_age_days": 30, + "idempotency_replay_hours": 168 + }, + "processing": { + "modes": [ + "inline" + ] + }, + "features": { + "upsert_by_scope_key": false, + "content_dedupe": false, + "oidc": { + "enabled": false + } + }, + "assessment_types": [ + { + "id": 1, + "code": "basic" + }, + { + "id": 2, + "code": "standard" + }, + { + "id": 3, + "code": "premium" + } + ] + }, + "exists": [ + "/host/product", + "/host/base_url", + "/host/app_version", + "/rate_limits/per_key/requests_per_minute" + ] + } +} diff --git a/docs/api-v2/examples/02-whoami_assessment_key.json b/docs/api-v2/examples/02-whoami_assessment_key.json new file mode 100644 index 0000000..6659df2 --- /dev/null +++ b/docs/api-v2/examples/02-whoami_assessment_key.json @@ -0,0 +1,57 @@ +{ + "name": "whoami assessment key", + "description": "An assessment key is bound to its assessment and can only write syncs.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "GET", + "path": "/api/v2/whoami", + "headers": { + "Authorization": "Bearer {{assessment_key}}" + } + }, + "expect": { + "status": 200, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "whoami", + "api_version": "2", + "key": { + "name": "assessment_key", + "kind": "assessment", + "scopes": [ + "syncs:write" + ], + "assessment_id": "{{main_id}}" + } + }, + "exists": [ + "/tenant/type", + "/tenant/id", + "/tenant/name", + "/key/id", + "/key/display", + "/key/expires_at", + "/warnings" + ] + } +} diff --git a/docs/api-v2/examples/03-whoami_account_key.json b/docs/api-v2/examples/03-whoami_account_key.json new file mode 100644 index 0000000..6aa43f2 --- /dev/null +++ b/docs/api-v2/examples/03-whoami_account_key.json @@ -0,0 +1,44 @@ +{ + "name": "whoami account key", + "description": "An account key belongs to the whole tenant and has no binding.", + "fixtures": { + "catalogue": "standard", + "assessments": [], + "keys": [ + { + "name": "account_key", + "kind": "account", + "scopes": [ + "assessments:create", + "assessments:read" + ] + } + ] + }, + "request": { + "method": "GET", + "path": "/api/v2/whoami", + "headers": { + "Authorization": "Bearer {{account_key}}" + } + }, + "expect": { + "status": 200, + "body_subset": { + "object": "whoami", + "key": { + "kind": "account", + "scopes": [ + "assessments:create", + "assessments:read" + ], + "assessment_id": null + } + }, + "exists": [ + "/tenant/id", + "/key/display", + "/key/expires_at" + ] + } +} diff --git a/docs/api-v2/examples/04-request_id_replaced.json b/docs/api-v2/examples/04-request_id_replaced.json new file mode 100644 index 0000000..4193969 --- /dev/null +++ b/docs/api-v2/examples/04-request_id_replaced.json @@ -0,0 +1,43 @@ +{ + "name": "request id replaced", + "description": "A client X-Request-ID that does not match the pattern is replaced, and the response always carries one.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "GET", + "path": "/api/v2/whoami", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "X-Request-ID": "bad id!" + } + }, + "expect": { + "status": 200, + "headers_present": [ + "X-Request-ID" + ], + "headers_differ": { + "X-Request-ID": "bad id!" + }, + "body_subset": { + "object": "whoami" + } + } +} diff --git a/docs/api-v2/examples/05-first_sync_assessment_key.json b/docs/api-v2/examples/05-first_sync_assessment_key.json new file mode 100644 index 0000000..db7fe1f --- /dev/null +++ b/docs/api-v2/examples/05-first_sync_assessment_key.json @@ -0,0 +1,146 @@ +{ + "name": "first sync assessment key", + "description": "The first sync of an assessment key's assessment records the scope key. The catalogue claim equals the host's, so there are no warnings.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0001", + "Content-Type": "application/json", + "X-Request-ID": "ci-run-20261103-01", + "Prefer": "wait=20" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0001", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 201, + "headers": { + "X-Request-ID": "ci-run-20261103-01" + }, + "headers_present": [ + "Location" + ], + "body_subset": { + "object": "sync", + "api_version": "2", + "status": "completed", + "client_sync_id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0001", + "request_id": "ci-run-20261103-01", + "assessment": { + "id": "{{main_id}}", + "created": false, + "scope_key": "aws:123456789012:eu-central-1" + }, + "ordering": { + "latest": true, + "late": false + }, + "content": { + "unchanged_from": null + }, + "assessment_type": { + "requested": 2, + "granted": 2 + }, + "catalogue": { + "host": { + "version": "2026-11-02" + }, + "unknown_resource_types": [], + "inactive_resource_types": [] + } + }, + "exists": [ + "/id", + "/request_id", + "/timestamps/received_at", + "/content/payload_sha256", + "/content/content_sha256", + "/results/scoring_version", + "/links/self", + "/links/assessment", + "/results/scores/exit", + "/results/risks" + ], + "lengths": { + "/warnings": 0 + } + } +} diff --git a/docs/api-v2/examples/06-second_sync_appended.json b/docs/api-v2/examples/06-second_sync_appended.json new file mode 100644 index 0000000..87a867e --- /dev/null +++ b/docs/api-v2/examples/06-second_sync_appended.json @@ -0,0 +1,182 @@ +{ + "name": "second sync appended", + "description": "A second push with the same key appends a new sync to the same assessment.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "before": [ + { + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0002", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0002", + "started_at": "{{now-700}}", + "completed_at": "{{now-600}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 201 + } + } + ], + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0003", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0003", + "started_at": "{{now-200}}", + "completed_at": "{{now-20}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 201, + "body_subset": { + "client_sync_id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0003", + "assessment": { + "id": "{{main_id}}", + "created": false, + "scope_key": "aws:123456789012:eu-central-1" + }, + "ordering": { + "latest": true, + "late": false + } + }, + "exists": [ + "/id" + ] + } +} diff --git a/docs/api-v2/examples/07-idempotent_replay.json b/docs/api-v2/examples/07-idempotent_replay.json new file mode 100644 index 0000000..c79288a --- /dev/null +++ b/docs/api-v2/examples/07-idempotent_replay.json @@ -0,0 +1,178 @@ +{ + "name": "idempotent replay", + "description": "The same key and body again returns 200 with Idempotent-Replayed and the original sync. A replay never charges, notifies or scores again.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "before": [ + { + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0004", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0004", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 201 + }, + "save": { + "first_sync_id": "/id" + } + } + ], + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0004", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0004", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 200, + "headers": { + "Idempotent-Replayed": "true" + }, + "body_subset": { + "object": "sync", + "id": "{{first_sync_id}}", + "client_sync_id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0004" + } + } +} diff --git a/docs/api-v2/examples/08-idempotency_key_reused.json b/docs/api-v2/examples/08-idempotency_key_reused.json new file mode 100644 index 0000000..950de2c --- /dev/null +++ b/docs/api-v2/examples/08-idempotency_key_reused.json @@ -0,0 +1,181 @@ +{ + "name": "idempotency key reused", + "description": "The same Idempotency-Key with a different body is refused.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "before": [ + { + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0005", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0005", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 201 + } + } + ], + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0005", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0005", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 99 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 422, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "idempotency_key_reused" + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/09-idempotency_key_missing.json b/docs/api-v2/examples/09-idempotency_key_missing.json new file mode 100644 index 0000000..2c38ce3 --- /dev/null +++ b/docs/api-v2/examples/09-idempotency_key_missing.json @@ -0,0 +1,106 @@ +{ + "name": "idempotency key missing", + "description": "Every POST needs an Idempotency-Key.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0006", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 400, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "idempotency_key_missing" + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/10-idempotency_key_mismatch.json b/docs/api-v2/examples/10-idempotency_key_mismatch.json new file mode 100644 index 0000000..37eabb1 --- /dev/null +++ b/docs/api-v2/examples/10-idempotency_key_mismatch.json @@ -0,0 +1,107 @@ +{ + "name": "idempotency key mismatch", + "description": "The Idempotency-Key must equal sync.id.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0008", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0007", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 422, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "idempotency_key_mismatch" + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/11-scope_mismatch.json b/docs/api-v2/examples/11-scope_mismatch.json new file mode 100644 index 0000000..25780c8 --- /dev/null +++ b/docs/api-v2/examples/11-scope_mismatch.json @@ -0,0 +1,114 @@ +{ + "name": "scope mismatch", + "description": "Once the first sync recorded the scope, a sync of another account is refused.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": "aws:123456789012:eu-central-1" + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0009", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0009", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "999999999999", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 409, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "target_mismatch", + "details": [ + { + "field": "scope_key", + "expected": "aws:123456789012:eu-central-1", + "actual": "aws:999999999999:eu-central-1" + } + ] + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/12-provider_mismatch.json b/docs/api-v2/examples/12-provider_mismatch.json new file mode 100644 index 0000000..d2e8901 --- /dev/null +++ b/docs/api-v2/examples/12-provider_mismatch.json @@ -0,0 +1,114 @@ +{ + "name": "provider mismatch", + "description": "A sync of another provider than the assessment's is refused.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "azure", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e000a", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e000a", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 409, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "target_mismatch", + "details": [ + { + "field": "provider", + "expected": "azure", + "actual": "aws" + } + ] + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/13-depth_too_deep.json b/docs/api-v2/examples/13-depth_too_deep.json new file mode 100644 index 0000000..aef6b0c --- /dev/null +++ b/docs/api-v2/examples/13-depth_too_deep.json @@ -0,0 +1,118 @@ +{ + "name": "depth too deep", + "description": "A sync deeper than its assessment is refused.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e000b", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e000b", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 3, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ], + "egress": { + "collected": true, + "items": [] + } + } + } + }, + "expect": { + "status": 409, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "target_mismatch", + "details": [ + { + "field": "assessment_type", + "expected": 2, + "actual": 3 + } + ] + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/14-depth_three_without_egress.json b/docs/api-v2/examples/14-depth_three_without_egress.json new file mode 100644 index 0000000..595bf00 --- /dev/null +++ b/docs/api-v2/examples/14-depth_three_without_egress.json @@ -0,0 +1,108 @@ +{ + "name": "depth three without egress", + "description": "Depth 3 without collected egress data is recorded as depth 2.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 3, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e000c", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e000c", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 3, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ], + "egress": { + "collected": false + } + } + } + }, + "expect": { + "status": 201, + "body_subset": { + "status": "completed_with_warnings", + "assessment_type": { + "requested": 3, + "granted": 2 + }, + "warnings": [ + { + "code": "tier_downgraded" + } + ] + } + } +} diff --git a/docs/api-v2/examples/15-depth_three_with_egress.json b/docs/api-v2/examples/15-depth_three_with_egress.json new file mode 100644 index 0000000..50b31e8 --- /dev/null +++ b/docs/api-v2/examples/15-depth_three_with_egress.json @@ -0,0 +1,124 @@ +{ + "name": "depth three with egress", + "description": "Depth 3 with collected egress data keeps depth 3.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 3, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e000d", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e000d", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 3, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ], + "egress": { + "collected": true, + "truncated": false, + "names": "plain", + "items": [ + { + "type_id": 12, + "name": "acme-logs", + "size_bytes": 5368709120, + "flags": [], + "notes": [], + "tiers": [ + { + "tier": "STANDARD", + "size_bytes": 5368709120, + "is_archive": false + } + ] + } + ] + } + } + } + }, + "expect": { + "status": 201, + "body_subset": { + "status": "completed", + "assessment_type": { + "requested": 3, + "granted": 3 + } + }, + "lengths": { + "/warnings": 0 + } + } +} diff --git a/docs/api-v2/examples/16-unknown_resource_types.json b/docs/api-v2/examples/16-unknown_resource_types.json new file mode 100644 index 0000000..30c666f --- /dev/null +++ b/docs/api-v2/examples/16-unknown_resource_types.json @@ -0,0 +1,114 @@ +{ + "name": "unknown resource types", + "description": "Resources of a type the host does not know are stored, excluded from scoring and reported as a warning.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e000e", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e000e", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 981, + "type_code": "AWS::Foo::Bar", + "location": "eu-central-1", + "count": 3 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 201, + "body_subset": { + "status": "completed_with_warnings", + "warnings": [ + { + "code": "unknown_resource_types", + "details": { + "type_ids": [ + 981 + ] + } + } + ], + "catalogue": { + "unknown_resource_types": [ + { + "type_id": 981, + "count": 3 + } + ] + } + } + } +} diff --git a/docs/api-v2/examples/17-inactive_resource_types.json b/docs/api-v2/examples/17-inactive_resource_types.json new file mode 100644 index 0000000..0181ff8 --- /dev/null +++ b/docs/api-v2/examples/17-inactive_resource_types.json @@ -0,0 +1,114 @@ +{ + "name": "inactive resource types", + "description": "Resources of a type the host has deactivated are stored and excluded from scoring.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e000f", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e000f", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 77, + "type_code": "AWS::SimpleDB::Domain", + "location": "eu-central-1", + "count": 2 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 201, + "body_subset": { + "status": "completed_with_warnings", + "warnings": [ + { + "code": "resource_type_inactive", + "details": { + "type_ids": [ + 77 + ] + } + } + ], + "catalogue": { + "inactive_resource_types": [ + { + "type_id": 77, + "count": 2 + } + ] + } + } + } +} diff --git a/docs/api-v2/examples/18-all_resource_types_unknown.json b/docs/api-v2/examples/18-all_resource_types_unknown.json new file mode 100644 index 0000000..36fad72 --- /dev/null +++ b/docs/api-v2/examples/18-all_resource_types_unknown.json @@ -0,0 +1,105 @@ +{ + "name": "all resource types unknown", + "description": "Only a sync whose resources are all unknown is refused.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0010", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0010", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 981, + "location": "eu-central-1", + "count": 3 + }, + { + "type_id": 982, + "location": "eu-central-1", + "count": 1 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 422, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "catalogue_mismatch" + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/19-duplicate_items_merged.json b/docs/api-v2/examples/19-duplicate_items_merged.json new file mode 100644 index 0000000..9c94160 --- /dev/null +++ b/docs/api-v2/examples/19-duplicate_items_merged.json @@ -0,0 +1,99 @@ +{ + "name": "duplicate items merged", + "description": "Items with the same type and location are merged by summing their counts.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0011", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0011", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "location": "eu-central-1", + "count": 30 + }, + { + "type_id": 12, + "location": "eu-central-1", + "count": 6 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 201, + "body_subset": { + "status": "completed_with_warnings", + "warnings": [ + { + "code": "duplicate_items_merged" + } + ] + } + } +} diff --git a/docs/api-v2/examples/20-catalogue_collector_older.json b/docs/api-v2/examples/20-catalogue_collector_older.json new file mode 100644 index 0000000..99259c1 --- /dev/null +++ b/docs/api-v2/examples/20-catalogue_collector_older.json @@ -0,0 +1,114 @@ +{ + "name": "catalogue collector older", + "description": "A collector with an older catalogue gets a warning listing the host's active types it could not collect.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0012", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0012", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-10-12", + "sequence": 38, + "sha256": "c0ffee0000000000000000000000000000000000000000000000000000000000", + "resource_type_ids": [ + 12, + 40 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 201, + "body_subset": { + "status": "completed_with_warnings", + "warnings": [ + { + "code": "collector_catalogue_outdated", + "details": { + "missing_resource_types": [ + 41, + 55 + ] + } + } + ], + "catalogue": { + "client": { + "version": "2026-10-12", + "sequence": 38 + }, + "host": { + "version": "2026-11-02" + } + } + } + } +} diff --git a/docs/api-v2/examples/21-catalogue_collector_newer.json b/docs/api-v2/examples/21-catalogue_collector_newer.json new file mode 100644 index 0000000..0885f21 --- /dev/null +++ b/docs/api-v2/examples/21-catalogue_collector_newer.json @@ -0,0 +1,107 @@ +{ + "name": "catalogue collector newer", + "description": "A collector with a newer catalogue gets a warning listing the type ids the host does not know.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0013", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0013", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-12-01", + "sequence": 45, + "sha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "resource_type_ids": [ + 12, + 40, + 41, + 55, + 981 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 201, + "body_subset": { + "status": "completed_with_warnings", + "warnings": [ + { + "code": "host_catalogue_outdated", + "details": { + "unknown_type_ids": [ + 981 + ] + } + } + ] + } + } +} diff --git a/docs/api-v2/examples/22-catalogue_versions_compared_as_numbers.json b/docs/api-v2/examples/22-catalogue_versions_compared_as_numbers.json new file mode 100644 index 0000000..3215d98 --- /dev/null +++ b/docs/api-v2/examples/22-catalogue_versions_compared_as_numbers.json @@ -0,0 +1,103 @@ +{ + "name": "catalogue versions compared as numbers", + "description": "Versions are compared as (date, N), not as text: 2026-11-02.9 is older than 2026-11-02.10.", + "fixtures": { + "catalogue": { + "use": "standard", + "version": "2026-11-02.10" + }, + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0014", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0014", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02.9", + "sequence": 40, + "sha256": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 201, + "body_subset": { + "warnings": [ + { + "code": "collector_catalogue_outdated" + } + ] + } + } +} diff --git a/docs/api-v2/examples/23-catalogue_missing.json b/docs/api-v2/examples/23-catalogue_missing.json new file mode 100644 index 0000000..fd07047 --- /dev/null +++ b/docs/api-v2/examples/23-catalogue_missing.json @@ -0,0 +1,93 @@ +{ + "name": "catalogue missing", + "description": "Without a catalogue block the catalogues cannot be compared.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0015", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0015", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 201, + "body_subset": { + "status": "completed_with_warnings", + "warnings": [ + { + "code": "collector_catalogue_unknown" + } + ], + "catalogue": { + "client": null + } + } + } +} diff --git a/docs/api-v2/examples/24-catalogue_sha256_mismatch.json b/docs/api-v2/examples/24-catalogue_sha256_mismatch.json new file mode 100644 index 0000000..13d2d81 --- /dev/null +++ b/docs/api-v2/examples/24-catalogue_sha256_mismatch.json @@ -0,0 +1,113 @@ +{ + "name": "catalogue sha256 mismatch", + "description": "The same version with a different hash means a corrupt or tampered dataset.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0016", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0016", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 422, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "catalogue_sha256_mismatch", + "details": [ + { + "host_version": "2026-11-02", + "host_sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3" + } + ] + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/25-account_key_creates_assessment.json b/docs/api-v2/examples/25-account_key_creates_assessment.json new file mode 100644 index 0000000..8037162 --- /dev/null +++ b/docs/api-v2/examples/25-account_key_creates_assessment.json @@ -0,0 +1,109 @@ +{ + "name": "account key creates assessment", + "description": "An account key with assessments:create creates a new assessment with this sync as its first one.", + "fixtures": { + "catalogue": "standard", + "assessments": [], + "keys": [ + { + "name": "account_key", + "kind": "account", + "scopes": [ + "assessments:create" + ] + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{account_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0017", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0017", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + }, + "target": { + "create": { + "name": "Payments prod (eu-central-1)", + "exit_strategy": "repatriation" + } + } + } + }, + "expect": { + "status": 201, + "body_subset": { + "status": "completed", + "assessment": { + "created": true, + "name": "Payments prod (eu-central-1)", + "scope_key": "aws:123456789012:eu-central-1" + }, + "assessment_type": { + "requested": 2, + "granted": 2 + } + }, + "exists": [ + "/assessment/id", + "/assessment/url" + ] + } +} diff --git a/docs/api-v2/examples/26-account_key_without_create_scope.json b/docs/api-v2/examples/26-account_key_without_create_scope.json new file mode 100644 index 0000000..394d402 --- /dev/null +++ b/docs/api-v2/examples/26-account_key_without_create_scope.json @@ -0,0 +1,114 @@ +{ + "name": "account key without create scope", + "description": "An account key without assessments:create cannot create an assessment.", + "fixtures": { + "catalogue": "standard", + "assessments": [], + "keys": [ + { + "name": "account_key", + "kind": "account", + "scopes": [ + "assessments:read" + ] + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{account_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0018", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0018", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + }, + "target": { + "create": { + "name": "Nope", + "exit_strategy": "repatriation" + } + } + } + }, + "expect": { + "status": 403, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "insufficient_scope", + "details": [ + { + "required": [ + "assessments:create" + ] + } + ] + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/27-account_key_without_target.json b/docs/api-v2/examples/27-account_key_without_target.json new file mode 100644 index 0000000..7dc5e21 --- /dev/null +++ b/docs/api-v2/examples/27-account_key_without_target.json @@ -0,0 +1,107 @@ +{ + "name": "account key without target", + "description": "An account key must say where the sync goes.", + "fixtures": { + "catalogue": "standard", + "assessments": [], + "keys": [ + { + "name": "account_key", + "kind": "account", + "scopes": [ + "assessments:create" + ] + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{account_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0019", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0019", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 422, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "validation_failed", + "details": [ + { + "pointer": "/target", + "code": "required" + } + ] + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/28-account_key_appends_to_named_assessment.json b/docs/api-v2/examples/28-account_key_appends_to_named_assessment.json new file mode 100644 index 0000000..bb0d906 --- /dev/null +++ b/docs/api-v2/examples/28-account_key_appends_to_named_assessment.json @@ -0,0 +1,104 @@ +{ + "name": "account key appends to named assessment", + "description": "An account key with syncs:write may name an existing assessment.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "account_key", + "kind": "account", + "scopes": [ + "syncs:write" + ] + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{account_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e001a", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e001a", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + }, + "target": { + "assessment_id": "{{main_id}}" + } + } + }, + "expect": { + "status": 201, + "body_subset": { + "assessment": { + "id": "{{main_id}}", + "created": false + } + } + } +} diff --git a/docs/api-v2/examples/29-account_key_unknown_assessment.json b/docs/api-v2/examples/29-account_key_unknown_assessment.json new file mode 100644 index 0000000..51d36ab --- /dev/null +++ b/docs/api-v2/examples/29-account_key_unknown_assessment.json @@ -0,0 +1,104 @@ +{ + "name": "account key unknown assessment", + "description": "An unknown assessment id (or one of another tenant) is not found.", + "fixtures": { + "catalogue": "standard", + "assessments": [], + "keys": [ + { + "name": "account_key", + "kind": "account", + "scopes": [ + "syncs:write" + ] + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{account_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e001b", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e001b", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + }, + "target": { + "assessment_id": "00000000-0000-4000-8000-000000000000" + } + } + }, + "expect": { + "status": 404, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "assessment_not_found" + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/30-assessment_key_for_another_assessment.json b/docs/api-v2/examples/30-assessment_key_for_another_assessment.json new file mode 100644 index 0000000..5d91456 --- /dev/null +++ b/docs/api-v2/examples/30-assessment_key_for_another_assessment.json @@ -0,0 +1,117 @@ +{ + "name": "assessment key for another assessment", + "description": "An assessment key cannot push to another assessment.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + }, + { + "name": "other", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e001c", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e001c", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + }, + "target": { + "assessment_id": "{{other_id}}" + } + } + }, + "expect": { + "status": 403, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "key_binding_violation" + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/31-key_revoked.json b/docs/api-v2/examples/31-key_revoked.json new file mode 100644 index 0000000..edb1d40 --- /dev/null +++ b/docs/api-v2/examples/31-key_revoked.json @@ -0,0 +1,50 @@ +{ + "name": "key revoked", + "description": "A revoked key is refused.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "revoked_key", + "kind": "account", + "scopes": [ + "assessments:read" + ], + "state": "revoked" + } + ] + }, + "request": { + "method": "GET", + "path": "/api/v2/whoami", + "headers": { + "Authorization": "Bearer {{revoked_key}}" + } + }, + "expect": { + "status": 401, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "api_key_revoked" + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/32-key_expired.json b/docs/api-v2/examples/32-key_expired.json new file mode 100644 index 0000000..a9b36ae --- /dev/null +++ b/docs/api-v2/examples/32-key_expired.json @@ -0,0 +1,50 @@ +{ + "name": "key expired", + "description": "An expired key is refused.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "expired_key", + "kind": "account", + "scopes": [ + "assessments:read" + ], + "state": "expired" + } + ] + }, + "request": { + "method": "GET", + "path": "/api/v2/whoami", + "headers": { + "Authorization": "Bearer {{expired_key}}" + } + }, + "expect": { + "status": 401, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "api_key_expired" + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/33-key_malformed.json b/docs/api-v2/examples/33-key_malformed.json new file mode 100644 index 0000000..e762758 --- /dev/null +++ b/docs/api-v2/examples/33-key_malformed.json @@ -0,0 +1,47 @@ +{ + "name": "key malformed", + "description": "A value that is not a key is refused without a lookup.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "GET", + "path": "/api/v2/whoami", + "headers": { + "Authorization": "Bearer not-a-key" + } + }, + "expect": { + "status": 401, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "invalid_api_key" + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/34-key_bad_checksum.json b/docs/api-v2/examples/34-key_bad_checksum.json new file mode 100644 index 0000000..1030d02 --- /dev/null +++ b/docs/api-v2/examples/34-key_bad_checksum.json @@ -0,0 +1,47 @@ +{ + "name": "key bad checksum", + "description": "A key with a wrong checksum is refused without a lookup.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "GET", + "path": "/api/v2/whoami", + "headers": { + "Authorization": "Bearer {{assessment_key|corrupt}}" + } + }, + "expect": { + "status": 401, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "invalid_api_key" + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/35-authorization_missing.json b/docs/api-v2/examples/35-authorization_missing.json new file mode 100644 index 0000000..17fecff --- /dev/null +++ b/docs/api-v2/examples/35-authorization_missing.json @@ -0,0 +1,45 @@ +{ + "name": "authorization missing", + "description": "A request without credentials is refused.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "GET", + "path": "/api/v2/whoami", + "headers": {} + }, + "expect": { + "status": 401, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "unauthenticated" + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/36-payload_too_large.json b/docs/api-v2/examples/36-payload_too_large.json new file mode 100644 index 0000000..e4e6c5a --- /dev/null +++ b/docs/api-v2/examples/36-payload_too_large.json @@ -0,0 +1,113 @@ +{ + "name": "payload too large", + "description": "A body beyond limits.max_body_bytes is refused before it is parsed. The harness pads the body with an unknown field.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e001d", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e001d", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + }, + "pad_bytes_to": 1100000 + }, + "expect": { + "status": 413, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "payload_too_large", + "details": [ + { + "limit": "max_body_bytes" + } + ] + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/37-validation_errors.json b/docs/api-v2/examples/37-validation_errors.json new file mode 100644 index 0000000..2bcb1f5 --- /dev/null +++ b/docs/api-v2/examples/37-validation_errors.json @@ -0,0 +1,125 @@ +{ + "name": "validation errors", + "description": "Every problem is listed with a JSON pointer, and nothing is stored.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e001e", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e001e", + "started_at": "{{now-300}}", + "completed_at": "{{now-600}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 4, + "scope": { + "provider": "aws", + "aws": { + "account_id": "1234", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": -1 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 422, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "validation_failed", + "details": [ + { + "pointer": "/inventory/resources/0/count", + "code": "out_of_range" + }, + { + "pointer": "/sync/completed_at", + "code": "before_started_at" + }, + { + "pointer": "/assessment_type", + "code": "out_of_range" + }, + { + "pointer": "/scope/aws/account_id", + "code": "invalid_format" + } + ] + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/38-collection_too_old.json b/docs/api-v2/examples/38-collection_too_old.json new file mode 100644 index 0000000..5774f2b --- /dev/null +++ b/docs/api-v2/examples/38-collection_too_old.json @@ -0,0 +1,113 @@ +{ + "name": "collection too old", + "description": "A sync collected more than limits.max_collection_age_days ago is refused.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e001f", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e001f", + "started_at": "{{now-4000000}}", + "completed_at": "{{now-3000000}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 422, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "validation_failed", + "details": [ + { + "pointer": "/sync/completed_at", + "code": "too_old" + } + ] + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/39-invalid_json.json b/docs/api-v2/examples/39-invalid_json.json new file mode 100644 index 0000000..253c1a0 --- /dev/null +++ b/docs/api-v2/examples/39-invalid_json.json @@ -0,0 +1,50 @@ +{ + "name": "invalid json", + "description": "A body that is not JSON is refused.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0020", + "Content-Type": "application/json" + }, + "raw_body": "{not json" + }, + "expect": { + "status": 400, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "invalid_json" + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/40-unsupported_media_type.json b/docs/api-v2/examples/40-unsupported_media_type.json new file mode 100644 index 0000000..60af04d --- /dev/null +++ b/docs/api-v2/examples/40-unsupported_media_type.json @@ -0,0 +1,50 @@ +{ + "name": "unsupported media type", + "description": "Only application/json is accepted.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0021", + "Content-Type": "text/plain" + }, + "raw_body": "hello" + }, + "expect": { + "status": 415, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "unsupported_media_type" + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/41-unsupported_schema_version.json b/docs/api-v2/examples/41-unsupported_schema_version.json new file mode 100644 index 0000000..6d091b0 --- /dev/null +++ b/docs/api-v2/examples/41-unsupported_schema_version.json @@ -0,0 +1,114 @@ +{ + "name": "unsupported schema version", + "description": "A schema version the host does not list in /meta is refused.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0022", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.7", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0022", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 400, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "unsupported_schema_version", + "details": [ + { + "supported": [ + "2.0" + ] + } + ] + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/42-get_sync.json b/docs/api-v2/examples/42-get_sync.json new file mode 100644 index 0000000..257ba00 --- /dev/null +++ b/docs/api-v2/examples/42-get_sync.json @@ -0,0 +1,126 @@ +{ + "name": "get sync", + "description": "A sync can be fetched by the host's id.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "before": [ + { + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0023", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0023", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 201 + }, + "save": { + "first_sync_id": "/id" + } + } + ], + "request": { + "method": "GET", + "path": "/api/v2/syncs/{{first_sync_id}}", + "headers": { + "Authorization": "Bearer {{assessment_key}}" + } + }, + "expect": { + "status": 200, + "body_subset": { + "object": "sync", + "id": "{{first_sync_id}}", + "client_sync_id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0023", + "status": "completed" + }, + "exists": [ + "/id", + "/request_id", + "/timestamps/received_at", + "/content/payload_sha256", + "/content/content_sha256", + "/results/scoring_version", + "/links/self", + "/links/assessment" + ] + } +} diff --git a/docs/api-v2/examples/43-get_sync_not_found.json b/docs/api-v2/examples/43-get_sync_not_found.json new file mode 100644 index 0000000..6b724bc --- /dev/null +++ b/docs/api-v2/examples/43-get_sync_not_found.json @@ -0,0 +1,47 @@ +{ + "name": "get sync not found", + "description": "An unknown sync id is not found.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ] + }, + "request": { + "method": "GET", + "path": "/api/v2/syncs/00000000-0000-4000-8000-000000000000", + "headers": { + "Authorization": "Bearer {{assessment_key}}" + } + }, + "expect": { + "status": 404, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "sync_not_found" + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/44-get_assessment.json b/docs/api-v2/examples/44-get_assessment.json new file mode 100644 index 0000000..5ade8db --- /dev/null +++ b/docs/api-v2/examples/44-get_assessment.json @@ -0,0 +1,134 @@ +{ + "name": "get assessment", + "description": "An assessment shows its source, depth, recorded scope and latest sync.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + }, + { + "name": "reader_key", + "kind": "account", + "scopes": [ + "assessments:read" + ] + } + ] + }, + "before": [ + { + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0024", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0024", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 201 + } + } + ], + "request": { + "method": "GET", + "path": "/api/v2/assessments/{{main_id}}", + "headers": { + "Authorization": "Bearer {{reader_key}}" + } + }, + "expect": { + "status": 200, + "body_subset": { + "object": "assessment", + "id": "{{main_id}}", + "source": "cli", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": "aws:123456789012:eu-central-1", + "state": "active", + "latest_sync": { + "object": "sync", + "client_sync_id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0024", + "status": "completed" + } + }, + "exists": [ + "/name", + "/created", + "/url" + ] + } +} diff --git a/docs/api-v2/examples/45-get_assessment_not_found.json b/docs/api-v2/examples/45-get_assessment_not_found.json new file mode 100644 index 0000000..c31f7f9 --- /dev/null +++ b/docs/api-v2/examples/45-get_assessment_not_found.json @@ -0,0 +1,49 @@ +{ + "name": "get assessment not found", + "description": "An unknown assessment id is not found.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "reader_key", + "kind": "account", + "scopes": [ + "assessments:read" + ] + } + ] + }, + "request": { + "method": "GET", + "path": "/api/v2/assessments/00000000-0000-4000-8000-000000000000", + "headers": { + "Authorization": "Bearer {{reader_key}}" + } + }, + "expect": { + "status": 404, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "assessment_not_found" + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/46-list_assessments_second_page.json b/docs/api-v2/examples/46-list_assessments_second_page.json new file mode 100644 index 0000000..fbd445b --- /dev/null +++ b/docs/api-v2/examples/46-list_assessments_second_page.json @@ -0,0 +1,94 @@ +{ + "name": "list assessments second page", + "description": "Assessments are listed newest first. The cursor of the first page gives the rest.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "a1", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + }, + { + "name": "a2", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + }, + { + "name": "a3", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "reader_key", + "kind": "account", + "scopes": [ + "assessments:read" + ] + } + ] + }, + "before": [ + { + "request": { + "method": "GET", + "path": "/api/v2/assessments?limit=2", + "headers": { + "Authorization": "Bearer {{reader_key}}" + } + }, + "expect": { + "status": 200, + "body_subset": { + "has_more": true, + "data": [ + { + "id": "{{a3_id}}" + }, + { + "id": "{{a2_id}}" + } + ] + }, + "lengths": { + "/data": 2 + } + }, + "save": { + "cursor": "/next_cursor" + } + } + ], + "request": { + "method": "GET", + "path": "/api/v2/assessments?limit=2&cursor={{cursor}}", + "headers": { + "Authorization": "Bearer {{reader_key}}" + } + }, + "expect": { + "status": 200, + "body_subset": { + "object": "list", + "has_more": false, + "next_cursor": null, + "data": [ + { + "id": "{{a1_id}}", + "object": "assessment" + } + ] + }, + "lengths": { + "/data": 1 + } + } +} diff --git a/docs/api-v2/examples/47-list_assessments_by_provider.json b/docs/api-v2/examples/47-list_assessments_by_provider.json new file mode 100644 index 0000000..08dbf1f --- /dev/null +++ b/docs/api-v2/examples/47-list_assessments_by_provider.json @@ -0,0 +1,53 @@ +{ + "name": "list assessments by provider", + "description": "The list can be filtered by provider.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "on_aws", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + }, + { + "name": "on_azure", + "provider": "azure", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "reader_key", + "kind": "account", + "scopes": [ + "assessments:read" + ] + } + ] + }, + "request": { + "method": "GET", + "path": "/api/v2/assessments?provider=azure", + "headers": { + "Authorization": "Bearer {{reader_key}}" + } + }, + "expect": { + "status": 200, + "body_subset": { + "data": [ + { + "id": "{{on_azure_id}}", + "provider": "azure" + } + ] + }, + "lengths": { + "/data": 1 + } + } +} diff --git a/docs/api-v2/examples/48-list_assessments_needs_read_scope.json b/docs/api-v2/examples/48-list_assessments_needs_read_scope.json new file mode 100644 index 0000000..6a23ea8 --- /dev/null +++ b/docs/api-v2/examples/48-list_assessments_needs_read_scope.json @@ -0,0 +1,56 @@ +{ + "name": "list assessments needs read scope", + "description": "Listing needs assessments:read.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "account_key", + "kind": "account", + "scopes": [ + "assessments:create" + ] + } + ] + }, + "request": { + "method": "GET", + "path": "/api/v2/assessments", + "headers": { + "Authorization": "Bearer {{account_key}}" + } + }, + "expect": { + "status": 403, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "insufficient_scope", + "details": [ + { + "required": [ + "assessments:read" + ] + } + ] + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/49-list_assessments_bad_cursor.json b/docs/api-v2/examples/49-list_assessments_bad_cursor.json new file mode 100644 index 0000000..2dfe05e --- /dev/null +++ b/docs/api-v2/examples/49-list_assessments_bad_cursor.json @@ -0,0 +1,55 @@ +{ + "name": "list assessments bad cursor", + "description": "A cursor the host did not issue is refused.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "reader_key", + "kind": "account", + "scopes": [ + "assessments:read" + ] + } + ] + }, + "request": { + "method": "GET", + "path": "/api/v2/assessments?cursor=not-a-cursor", + "headers": { + "Authorization": "Bearer {{reader_key}}" + } + }, + "expect": { + "status": 422, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "validation_failed", + "details": [ + { + "parameter": "cursor", + "code": "invalid_format" + } + ] + } + }, + "exists": [ + "/error/message", + "/error/request_id" + ] + } +} diff --git a/docs/api-v2/examples/50-list_assessment_syncs.json b/docs/api-v2/examples/50-list_assessment_syncs.json new file mode 100644 index 0000000..36b4298 --- /dev/null +++ b/docs/api-v2/examples/50-list_assessment_syncs.json @@ -0,0 +1,202 @@ +{ + "name": "list assessment syncs", + "description": "The history is ordered by collection time, newest first, whatever the arrival order.", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + }, + { + "name": "reader_key", + "kind": "account", + "scopes": [ + "assessments:read" + ] + } + ] + }, + "before": [ + { + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0026", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0026", + "started_at": "{{now-200}}", + "completed_at": "{{now-100}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 201 + } + }, + { + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0025", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0025", + "started_at": "{{now-700}}", + "completed_at": "{{now-600}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 201 + } + } + ], + "request": { + "method": "GET", + "path": "/api/v2/assessments/{{main_id}}/syncs?limit=1", + "headers": { + "Authorization": "Bearer {{reader_key}}" + } + }, + "expect": { + "status": 200, + "body_subset": { + "object": "list", + "has_more": true, + "data": [ + { + "object": "sync", + "client_sync_id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0026" + } + ] + }, + "lengths": { + "/data": 1 + }, + "exists": [ + "/next_cursor" + ] + } +} diff --git a/docs/api-v2/examples/51-insufficient_credits.json b/docs/api-v2/examples/51-insufficient_credits.json new file mode 100644 index 0000000..b2c0a6d --- /dev/null +++ b/docs/api-v2/examples/51-insufficient_credits.json @@ -0,0 +1,111 @@ +{ + "name": "insufficient credits", + "description": "ExitCloud only: a sync that costs more credits than the organization has is refused, and nothing is stored.", + "host": "exitcloud", + "fixtures": { + "catalogue": "standard", + "assessments": [ + { + "name": "main", + "provider": "aws", + "exit_strategy": "repatriation", + "assessment_type": 2, + "scope_key": null + } + ], + "keys": [ + { + "name": "assessment_key", + "kind": "assessment", + "assessment": "main" + } + ], + "credits": 0 + }, + "request": { + "method": "POST", + "path": "/api/v2/syncs", + "headers": { + "Authorization": "Bearer {{assessment_key}}", + "Idempotency-Key": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0027", + "Content-Type": "application/json" + }, + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e0027", + "started_at": "{{now-300}}", + "completed_at": "{{now-60}}", + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "resource_type_ids": [ + 12, + 40, + 41, + 55 + ] + } + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + } + }, + "expect": { + "status": 402, + "headers_present": [ + "X-Request-ID" + ], + "body_subset": { + "object": "error", + "api_version": "2", + "error": { + "code": "insufficient_credits" + } + }, + "exists": [ + "/error/message", + "/error/request_id", + "/error/details/0/required", + "/error/details/0/balance" + ] + } +} diff --git a/docs/api-v2/fixtures/catalogue.json b/docs/api-v2/fixtures/catalogue.json new file mode 100644 index 0000000..5f27fb2 --- /dev/null +++ b/docs/api-v2/fixtures/catalogue.json @@ -0,0 +1,48 @@ +{ + "name": "standard", + "version": "2026-11-02", + "sequence": 41, + "sha256": "9f1c5b7a3e2d4c6b8a0f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3", + "loaded_at": 1762070400, + "resource_types": { + "aws": [ + { + "id": 12, + "code": "AWS::S3::Bucket", + "active": true + }, + { + "id": 40, + "code": "AWS::EC2::Instance", + "active": true + }, + { + "id": 41, + "code": "AWS::Lambda::Function", + "active": true + }, + { + "id": 55, + "code": "AWS::RDS::DBInstance", + "active": true + }, + { + "id": 77, + "code": "AWS::SimpleDB::Domain", + "active": false + } + ], + "azure": [ + { + "id": 201, + "code": "Microsoft.Storage/storageAccounts", + "active": true + }, + { + "id": 202, + "code": "Microsoft.Compute/virtualMachines", + "active": true + } + ] + } +} diff --git a/docs/api-v2/make_manifest.py b/docs/api-v2/make_manifest.py new file mode 100644 index 0000000..b079111 --- /dev/null +++ b/docs/api-v2/make_manifest.py @@ -0,0 +1,40 @@ +import hashlib +import sys +from pathlib import Path + +DIRECTORY = Path(__file__).resolve().parent +MANIFEST = DIRECTORY / "MANIFEST.sha256" + + +def definition_files(): + return sorted( + path + for path in DIRECTORY.rglob("*") + if path.is_file() + and path != MANIFEST + and "__pycache__" not in path.parts + and path.suffix != ".pyc" + ) + + +def manifest_text(): + lines = [] + for path in definition_files(): + digest = hashlib.sha256(path.read_bytes()).hexdigest() + lines.append(f"{digest} {path.relative_to(DIRECTORY).as_posix()}") + return "\n".join(lines) + "\n" + + +def main(argv): + if "--check" in argv: + if MANIFEST.read_text() != manifest_text(): + print("MANIFEST.sha256 is out of date; run make_manifest.py") + return 1 + return 0 + MANIFEST.write_text(manifest_text()) + print(f"wrote {MANIFEST.name} ({len(definition_files())} files)") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/docs/api-v2/openapi.yaml b/docs/api-v2/openapi.yaml new file mode 100644 index 0000000..4b2f3c4 --- /dev/null +++ b/docs/api-v2/openapi.yaml @@ -0,0 +1,1109 @@ +openapi: 3.1.0 +info: + title: cloudexit platform API + version: 2.0.0 + summary: Push syncs from the cloudexit CLI to EscapeCloud or ExitCloud. + description: | + One contract for two hosts. A sync is one pushed result: the CLI collects an inventory of a cloud scope and the + host evaluates it and appends it to an assessment. + + - The API key itself is the credential (`Authorization: Bearer `). There is no token exchange. + - The key decides the target. An assessment key appends to its own assessment. An account key with + `assessments:create` creates an assessment with `target.create`, and with `syncs:write` it may name + `target.assessment_id`. + - Every `POST` carries an `Idempotency-Key` (the sync's own id), so retries never duplicate or double-charge. + - Changes inside this major version are additive only. A breaking change is a new `/api/v3`. + + Fields marked `x-host: exitcloud` or `x-host: escapecloud` appear on that host only. Everything else is the same + on both. Error and warning codes are in `errors.yaml`; hash and key rules have test vectors in `vectors/`. + license: + name: AGPL-3.0 + identifier: AGPL-3.0-only +servers: + - url: https://{host}/api/v2 + description: A host of this contract (EscapeCloud on a customer's own host, or an ExitCloud region). + variables: + host: + default: eu.exitcloud.io +security: + - apiKey: [] +tags: + - name: discovery + - name: syncs + - name: assessments +paths: + /meta: + get: + operationId: getMeta + tags: [discovery] + summary: Capabilities, limits and policies of the host. + description: Needs no key and contains nothing tenant-specific. Cacheable for 5 minutes. + security: [] + responses: + "200": + description: The host's capabilities. + headers: + X-Request-ID: {$ref: "#/components/headers/RequestId"} + Cache-Control: + description: public, max-age=300 + schema: {type: string} + content: + application/json: + schema: {$ref: "#/components/schemas/Meta"} + "429": {$ref: "#/components/responses/RateLimited"} + "503": {$ref: "#/components/responses/Unavailable"} + /whoami: + get: + operationId: getWhoami + tags: [discovery] + summary: What this key is and what it may do. + parameters: + - {$ref: "#/components/parameters/RequestIdHeader"} + responses: + "200": + description: The principal behind the key. + headers: + X-Request-ID: {$ref: "#/components/headers/RequestId"} + RateLimit: {$ref: "#/components/headers/RateLimit"} + RateLimit-Policy: {$ref: "#/components/headers/RateLimitPolicy"} + content: + application/json: + schema: {$ref: "#/components/schemas/Whoami"} + "401": {$ref: "#/components/responses/Unauthorized"} + "403": {$ref: "#/components/responses/Forbidden"} + "429": {$ref: "#/components/responses/RateLimited"} + "503": {$ref: "#/components/responses/Unavailable"} + /syncs: + post: + operationId: createSync + tags: [syncs] + summary: Push a sync. + description: | + The single push endpoint. Needs `syncs:write` (an assessment key, or an account key naming + `target.assessment_id`) or `assessments:create` (an account key with `target.create`). + + The host answers `201` in 2.0 (processing is inline, `meta.processing.modes` is `["inline"]`). `202` plus + polling `GET /syncs/{sync_id}` is defined for later and clients must handle it. + + A replay of a completed request returns `200` with `Idempotent-Replayed: true` and the original body. It never + charges, notifies or scores again. `Prefer: wait=N` is accepted and ignored in 2.0. + parameters: + - {$ref: "#/components/parameters/IdempotencyKey"} + - {$ref: "#/components/parameters/RequestIdHeader"} + - {$ref: "#/components/parameters/Prefer"} + requestBody: + required: true + content: + application/json: + schema: {$ref: "#/components/schemas/SyncRequest"} + responses: + "200": + description: A replay of a completed request. + headers: + X-Request-ID: {$ref: "#/components/headers/RequestId"} + Idempotent-Replayed: {$ref: "#/components/headers/IdempotentReplayed"} + RateLimit: {$ref: "#/components/headers/RateLimit"} + RateLimit-Policy: {$ref: "#/components/headers/RateLimitPolicy"} + content: + application/json: + schema: {$ref: "#/components/schemas/Sync"} + "201": + description: The sync was processed and stored. + headers: + X-Request-ID: {$ref: "#/components/headers/RequestId"} + Location: {$ref: "#/components/headers/Location"} + RateLimit: {$ref: "#/components/headers/RateLimit"} + RateLimit-Policy: {$ref: "#/components/headers/RateLimitPolicy"} + content: + application/json: + schema: {$ref: "#/components/schemas/Sync"} + "202": + description: | + Accepted and still processing. Not sent by 2.0 hosts. The body has `status: processing` and no `results`. + headers: + X-Request-ID: {$ref: "#/components/headers/RequestId"} + Location: {$ref: "#/components/headers/Location"} + Retry-After: {$ref: "#/components/headers/RetryAfter"} + content: + application/json: + schema: {$ref: "#/components/schemas/Sync"} + "400": {$ref: "#/components/responses/BadRequest"} + "401": {$ref: "#/components/responses/Unauthorized"} + "402": {$ref: "#/components/responses/PaymentRequired"} + "403": {$ref: "#/components/responses/Forbidden"} + "404": {$ref: "#/components/responses/NotFound"} + "409": {$ref: "#/components/responses/Conflict"} + "410": {$ref: "#/components/responses/Gone"} + "413": {$ref: "#/components/responses/PayloadTooLarge"} + "415": {$ref: "#/components/responses/UnsupportedMediaType"} + "422": {$ref: "#/components/responses/Unprocessable"} + "429": {$ref: "#/components/responses/RateLimited"} + "500": {$ref: "#/components/responses/InternalError"} + "503": {$ref: "#/components/responses/Unavailable"} + /syncs/{sync_id}: + get: + operationId: getSync + tags: [syncs] + summary: A sync's status and result. + description: "Needs `syncs:write` or `assessments:read`. Polled after a `202`. A failed sync has `status: failed` and an `error`." + parameters: + - {$ref: "#/components/parameters/SyncId"} + - {$ref: "#/components/parameters/RequestIdHeader"} + responses: + "200": + description: The sync. + headers: + X-Request-ID: {$ref: "#/components/headers/RequestId"} + RateLimit: {$ref: "#/components/headers/RateLimit"} + RateLimit-Policy: {$ref: "#/components/headers/RateLimitPolicy"} + content: + application/json: + schema: {$ref: "#/components/schemas/Sync"} + "401": {$ref: "#/components/responses/Unauthorized"} + "403": {$ref: "#/components/responses/Forbidden"} + "404": {$ref: "#/components/responses/NotFound"} + "429": {$ref: "#/components/responses/RateLimited"} + "503": {$ref: "#/components/responses/Unavailable"} + /assessments: + get: + operationId: listAssessments + tags: [assessments] + summary: List assessments. + description: Needs an account key with `assessments:read`. Newest first, cursor pagination. + parameters: + - {$ref: "#/components/parameters/Limit"} + - {$ref: "#/components/parameters/Cursor"} + - name: provider + in: query + schema: {$ref: "#/components/schemas/Provider"} + - name: state + in: query + schema: {type: string, enum: [active, archived], default: active} + - {$ref: "#/components/parameters/RequestIdHeader"} + responses: + "200": + description: A page of assessments. + headers: + X-Request-ID: {$ref: "#/components/headers/RequestId"} + RateLimit: {$ref: "#/components/headers/RateLimit"} + RateLimit-Policy: {$ref: "#/components/headers/RateLimitPolicy"} + content: + application/json: + schema: {$ref: "#/components/schemas/AssessmentList"} + "401": {$ref: "#/components/responses/Unauthorized"} + "403": {$ref: "#/components/responses/Forbidden"} + "422": {$ref: "#/components/responses/Unprocessable"} + "429": {$ref: "#/components/responses/RateLimited"} + "503": {$ref: "#/components/responses/Unavailable"} + /assessments/{assessment_id}: + get: + operationId: getAssessment + tags: [assessments] + summary: One assessment with its latest sync. + description: Needs an account key with `assessments:read`. + parameters: + - {$ref: "#/components/parameters/AssessmentId"} + - {$ref: "#/components/parameters/RequestIdHeader"} + responses: + "200": + description: The assessment. + headers: + X-Request-ID: {$ref: "#/components/headers/RequestId"} + RateLimit: {$ref: "#/components/headers/RateLimit"} + RateLimit-Policy: {$ref: "#/components/headers/RateLimitPolicy"} + content: + application/json: + schema: {$ref: "#/components/schemas/Assessment"} + "401": {$ref: "#/components/responses/Unauthorized"} + "403": {$ref: "#/components/responses/Forbidden"} + "404": {$ref: "#/components/responses/NotFound"} + "429": {$ref: "#/components/responses/RateLimited"} + "503": {$ref: "#/components/responses/Unavailable"} + /assessments/{assessment_id}/syncs: + get: + operationId: listAssessmentSyncs + tags: [assessments] + summary: The sync history of an assessment. + description: Needs an account key with `assessments:read`. Ordered by collection time, newest first, then by id. + parameters: + - {$ref: "#/components/parameters/AssessmentId"} + - {$ref: "#/components/parameters/Limit"} + - {$ref: "#/components/parameters/Cursor"} + - {$ref: "#/components/parameters/RequestIdHeader"} + responses: + "200": + description: A page of syncs. + headers: + X-Request-ID: {$ref: "#/components/headers/RequestId"} + RateLimit: {$ref: "#/components/headers/RateLimit"} + RateLimit-Policy: {$ref: "#/components/headers/RateLimitPolicy"} + content: + application/json: + schema: {$ref: "#/components/schemas/SyncList"} + "401": {$ref: "#/components/responses/Unauthorized"} + "403": {$ref: "#/components/responses/Forbidden"} + "404": {$ref: "#/components/responses/NotFound"} + "422": {$ref: "#/components/responses/Unprocessable"} + "429": {$ref: "#/components/responses/RateLimited"} + "503": {$ref: "#/components/responses/Unavailable"} +components: + securitySchemes: + apiKey: + type: http + scheme: bearer + bearerFormat: "esc_sk_… (EscapeCloud), exc_eu_sk_… or exc_us_sk_… (ExitCloud)" + description: | + The API key as issued, `_sk_` + 16 characters key id + 32 characters secret + 6 characters CRC32 + checksum (all base62). A key with a wrong prefix, length or checksum is refused without a lookup. + parameters: + IdempotencyKey: + name: Idempotency-Key + in: header + required: true + description: The sync's own id (`sync.id`). A different value gives `422 idempotency_key_mismatch`. + schema: {type: string, pattern: "^[A-Za-z0-9_-]{16,64}$"} + RequestIdHeader: + name: X-Request-ID + in: header + required: false + description: Echoed back if it matches the pattern, otherwise replaced by a generated value. Always returned. + schema: {type: string, pattern: "^[A-Za-z0-9._-]{8,64}$"} + Prefer: + name: Prefer + in: header + required: false + description: "`wait=` (RFC 7240). Accepted and ignored in 2.0, where processing is inline." + schema: {type: string, pattern: "^wait=[0-9]{1,3}$"} + SyncId: + name: sync_id + in: path + required: true + description: The host's id of the sync (`Sync ID` in the UI), not the client's `sync.id`. + schema: {type: string, format: uuid} + AssessmentId: + name: assessment_id + in: path + required: true + schema: {type: string, format: uuid} + Limit: + name: limit + in: query + schema: {type: integer, minimum: 1, maximum: 100, default: 25} + Cursor: + name: cursor + in: query + description: The `next_cursor` of the previous page. Opaque. + schema: {type: string, minLength: 1, maxLength: 512} + headers: + RequestId: + description: The request id, also in `error.request_id` of an error body. + schema: {type: string, pattern: "^[A-Za-z0-9._-]{8,64}$"} + IdempotentReplayed: + description: "`true` on a replayed response." + schema: {type: string, enum: ["true"]} + Location: + description: Where to fetch the sync, `/api/v2/syncs/{id}`. + schema: {type: string} + RetryAfter: + description: Seconds to wait before retrying. + schema: {type: integer, minimum: 0} + RateLimit: + description: "IETF httpapi ratelimit header, for example `limit=60, remaining=12, reset=31`." + schema: {type: string} + RateLimitPolicy: + description: "For example `60;w=60`." + schema: {type: string} + Deprecation: + description: Sent for a deprecated contract major or client version (RFC 9745). + schema: {type: string} + Sunset: + description: When a deprecated major or client version stops working (RFC 8594). + schema: {type: string} + Link: + description: "`<…>; rel=\"deprecation\"` pointing at the migration notes." + schema: {type: string} + responses: + BadRequest: + description: Malformed request (`invalid_json`, `idempotency_key_missing`, `unsupported_schema_version`, `client_version_unsupported`). + headers: &errorHeaders + X-Request-ID: {$ref: "#/components/headers/RequestId"} + content: + application/json: + schema: {$ref: "#/components/schemas/ErrorResponse"} + Unauthorized: + description: The key is missing, malformed, unknown, expired or revoked. + headers: *errorHeaders + content: + application/json: + schema: {$ref: "#/components/schemas/ErrorResponse"} + PaymentRequired: + description: Not enough credits (`insufficient_credits`). + x-host: exitcloud + headers: *errorHeaders + content: + application/json: + schema: {$ref: "#/components/schemas/ErrorResponse"} + Forbidden: + description: The key may not do this (`insufficient_scope`, `key_binding_violation`, `principal_inactive`, `wrong_region`). + headers: *errorHeaders + content: + application/json: + schema: {$ref: "#/components/schemas/ErrorResponse"} + NotFound: + description: No such assessment or sync for this tenant. + headers: *errorHeaders + content: + application/json: + schema: {$ref: "#/components/schemas/ErrorResponse"} + Conflict: + description: The sync does not fit the assessment (`target_mismatch`), the assessment is archived, or the same request is in progress. + headers: + X-Request-ID: {$ref: "#/components/headers/RequestId"} + Retry-After: {$ref: "#/components/headers/RetryAfter"} + content: + application/json: + schema: {$ref: "#/components/schemas/ErrorResponse"} + Gone: + description: The assessment was deleted (`assessment_deleted`). + headers: *errorHeaders + content: + application/json: + schema: {$ref: "#/components/schemas/ErrorResponse"} + PayloadTooLarge: + description: The body is beyond `limits.max_body_bytes`. + headers: *errorHeaders + content: + application/json: + schema: {$ref: "#/components/schemas/ErrorResponse"} + UnsupportedMediaType: + description: The Content-Type is not `application/json`. + headers: *errorHeaders + content: + application/json: + schema: {$ref: "#/components/schemas/ErrorResponse"} + Unprocessable: + description: Validation failed, a limit was exceeded, the idempotency key was reused or mismatched, or the catalogue does not fit. + headers: *errorHeaders + content: + application/json: + schema: {$ref: "#/components/schemas/ErrorResponse"} + RateLimited: + description: Too many requests (`rate_limited`) or a quota is used up (`quota_exceeded`). + headers: + X-Request-ID: {$ref: "#/components/headers/RequestId"} + Retry-After: {$ref: "#/components/headers/RetryAfter"} + content: + application/json: + schema: {$ref: "#/components/schemas/ErrorResponse"} + InternalError: + description: Something failed on the host. Retrying with the same Idempotency-Key is safe. + headers: *errorHeaders + content: + application/json: + schema: {$ref: "#/components/schemas/ErrorResponse"} + Unavailable: + description: The host is in maintenance or a dependency is down. + headers: + X-Request-ID: {$ref: "#/components/headers/RequestId"} + Retry-After: {$ref: "#/components/headers/RetryAfter"} + content: + application/json: + schema: {$ref: "#/components/schemas/ErrorResponse"} + schemas: + ApiVersion: + type: string + const: "2" + Provider: + type: string + enum: [aws, azure] + description: The cloud providers a sync can carry. More are added additively. + ExitStrategy: + type: string + enum: [repatriation, hybrid_cloud, alternate_cloud] + AssessmentType: + type: integer + minimum: 1 + maximum: 3 + description: The depth. 1 = inventory, cost and risks; 2 = plus scores and alternatives; 3 = plus detailed collection and egress. + EpochSeconds: + type: integer + minimum: 0 + maximum: 4102444800 + Sha256: + type: string + pattern: "^[0-9a-f]{64}$" + CatalogueVersion: + type: string + pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}(\\.[0-9]+)?$" + description: "`YYYY-MM-DD[.N]`. Compare as (date, N), never as text." + ErrorCode: + type: string + description: See errors.yaml. Unknown codes must be handled by HTTP class and `retryable`. + enum: + - unauthenticated + - invalid_api_key + - api_key_expired + - api_key_revoked + - wrong_region + - principal_inactive + - insufficient_scope + - key_binding_violation + - assessment_key_v1 + - idempotency_key_missing + - idempotency_key_mismatch + - idempotency_key_reused + - request_in_progress + - invalid_json + - unsupported_media_type + - payload_too_large + - limit_exceeded + - validation_failed + - catalogue_mismatch + - catalogue_sha256_mismatch + - unsupported_schema_version + - client_version_unsupported + - assessment_not_found + - sync_not_found + - assessment_deleted + - assessment_archived + - target_mismatch + - insufficient_credits + - quota_exceeded + - rate_limited + - maintenance + - service_unavailable + - internal_error + WarningCode: + type: string + description: See errors.yaml. Clients must tolerate unknown codes. + enum: + - unknown_resource_types + - resource_type_inactive + - resource_type_code_mismatch + - duplicate_items_merged + - tier_downgraded + - name_ignored + - key_expiring + - client_version_deprecated + - collector_catalogue_outdated + - host_catalogue_outdated + - collector_catalogue_unknown + ErrorDetail: + type: object + description: | + One problem. `pointer` or `parameter` says where. Code-specific values are further keys of the item, for + example `field`, `expected` and `actual` on `target_mismatch`; they are listed per code in errors.yaml. + required: [code, message] + additionalProperties: true + properties: + pointer: {type: string, description: A JSON pointer into the request body.} + parameter: {type: string, description: A query parameter or header name.} + code: {type: string} + message: {type: string} + field: {type: string} + expected: {} + actual: {} + required: {type: array, items: {type: string}} + limit: {type: string} + balance: {type: integer} + resets_at: {$ref: "#/components/schemas/EpochSeconds"} + supported: {type: array, items: {type: string}} + expected_host: {type: string, format: uri} + min_supported: {type: string} + recommended: {type: string} + host_version: {$ref: "#/components/schemas/CatalogueVersion"} + host_sha256: {$ref: "#/components/schemas/Sha256"} + Error: + type: object + required: [code, message, retryable, request_id, details] + properties: + code: {$ref: "#/components/schemas/ErrorCode"} + message: {type: string} + retryable: {type: boolean} + request_id: {type: string} + details: + type: array + items: {$ref: "#/components/schemas/ErrorDetail"} + docs_url: {type: string, format: uri} + ErrorResponse: + type: object + required: [object, api_version, error] + properties: + object: {const: error} + api_version: {$ref: "#/components/schemas/ApiVersion"} + error: {$ref: "#/components/schemas/Error"} + Warning: + type: object + required: [code, message] + properties: + code: {$ref: "#/components/schemas/WarningCode"} + message: {type: string} + details: + type: object + additionalProperties: true + AwsScope: + type: object + required: [provider, aws] + properties: + provider: {const: aws} + aws: + type: object + required: [account_id, regions] + properties: + account_id: {type: string, pattern: "^[0-9]{12}$"} + regions: + type: array + minItems: 1 + maxItems: 40 + items: + type: string + pattern: "^(\\*|[a-z]{2}(-[a-z]+)+-[0-9]+)$" + AzureScope: + type: object + required: [provider, azure] + properties: + provider: {const: azure} + azure: + type: object + required: [tenant_id, subscription_id] + properties: + tenant_id: {type: string, format: uuid} + subscription_id: {type: string, format: uuid} + resource_group: + type: [string, "null"] + minLength: 1 + maxLength: 90 + description: Missing or null means the whole subscription. + TfstateScope: + type: object + required: [provider, tfstate] + properties: + provider: {$ref: "#/components/schemas/Provider"} + tfstate: + type: object + required: [lineage] + properties: + lineage: {type: string, format: uuid} + serial: {type: [integer, "null"], minimum: 0} + Scope: + description: | + What the sync covers. The host derives the `scope_key` from it (rules and vectors in `vectors/scope_key.json`) + and checks that `provider` matches the assessment. `collector.mode` is `tfstate` exactly when `tfstate` is sent. + oneOf: + - {$ref: "#/components/schemas/AwsScope"} + - {$ref: "#/components/schemas/AzureScope"} + - {$ref: "#/components/schemas/TfstateScope"} + CatalogueClaim: + type: object + description: The catalogue the collector ran with, from its dataset's `catalogue_meta`. + required: [version, sequence, sha256, resource_type_ids] + properties: + version: {$ref: "#/components/schemas/CatalogueVersion"} + sequence: {type: integer, minimum: 0} + sha256: {$ref: "#/components/schemas/Sha256"} + resource_type_ids: + type: array + description: The sorted ids of the dataset's active resource types for the sync's provider. + maxItems: 5000 + uniqueItems: true + items: {type: integer, minimum: 1} + Collector: + type: object + required: [name, version, mode] + properties: + name: {type: string, minLength: 1, maxLength: 64} + version: {type: string, minLength: 1, maxLength: 64} + mode: {type: string, enum: [api, tfstate]} + catalogue: {$ref: "#/components/schemas/CatalogueClaim"} + CiContext: + type: object + properties: + provider: {type: string, maxLength: 64} + run_url: {type: string, format: uri, maxLength: 2048} + SyncInfo: + type: object + required: [id, started_at, completed_at, collector] + properties: + id: + type: string + pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + description: A UUIDv7 created at collection time. Also the Idempotency-Key. + started_at: {$ref: "#/components/schemas/EpochSeconds"} + completed_at: + allOf: + - {$ref: "#/components/schemas/EpochSeconds"} + description: Not before `started_at`, not older than `limits.max_collection_age_days`, not more than `limits.clock_skew_seconds` in the future. + collector: {$ref: "#/components/schemas/Collector"} + ci: {$ref: "#/components/schemas/CiContext"} + Target: + type: object + description: | + Optional with an assessment key (the binding decides; a different `assessment_id` gives `403 key_binding_violation`). + With an account key send exactly one of `assessment_id` (needs `syncs:write`) or `create` (needs `assessments:create`). + properties: + assessment_id: {type: string, format: uuid} + create: + type: object + required: [name, exit_strategy] + properties: + name: {type: string, minLength: 1, maxLength: 255} + exit_strategy: {$ref: "#/components/schemas/ExitStrategy"} + group: {type: string, minLength: 1, maxLength: 255} + not: + required: [assessment_id, create] + ResourceItem: + type: object + required: [type_id, location, count] + properties: + type_id: {type: integer, minimum: 1} + type_code: {type: string, maxLength: 255, description: A cross-check label. A mismatch with the host gives the warning `resource_type_code_mismatch`.} + location: {type: string, maxLength: 255} + count: {type: integer, minimum: 0, maximum: 10000} + CostItem: + type: object + required: [month, amount, currency] + properties: + month: {type: string, pattern: "^[0-9]{4}-(0[1-9]|1[0-2])$"} + amount: {type: string, pattern: "^-?[0-9]+(\\.[0-9]+)?$", maxLength: 40, description: A decimal string, never a float.} + currency: {type: string, pattern: "^[A-Za-z]{3}$"} + final: {type: boolean, default: false} + EgressTier: + type: object + required: [tier] + properties: + tier: {type: string, minLength: 1, maxLength: 50} + size_bytes: {type: [integer, "null"], minimum: 0, maximum: 9007199254740991} + is_archive: {type: boolean, default: false} + EgressItem: + type: object + required: [type_id, name] + properties: + type_id: {type: integer, minimum: 1} + name: {type: string, maxLength: 255} + size_bytes: {type: [integer, "null"], minimum: 0, maximum: 9007199254740991} + flags: + type: array + maxItems: 10 + items: {type: string, maxLength: 64} + notes: + type: array + maxItems: 10 + items: {type: string, maxLength: 255} + tiers: + type: array + maxItems: 10 + items: {$ref: "#/components/schemas/EgressTier"} + Egress: + type: object + required: [collected] + properties: + collected: {type: boolean} + truncated: {type: boolean, default: false} + names: {type: string, enum: [plain, hashed], default: plain} + items: + type: array + maxItems: 1000 + items: {$ref: "#/components/schemas/EgressItem"} + Inventory: + type: object + required: [resources] + properties: + resources: + type: array + maxItems: 1000 + items: {$ref: "#/components/schemas/ResourceItem"} + costs: + type: array + maxItems: 12 + items: {$ref: "#/components/schemas/CostItem"} + egress: {$ref: "#/components/schemas/Egress"} + SyncRequest: + type: object + description: | + Unknown fields are ignored within a major version. The body is at most `limits.max_body_bytes`. + `size_bytes` values are capped at 2^53-1 so that hashes are exact. + required: [schema_version, sync, assessment_type, scope, inventory] + properties: + schema_version: {type: string, pattern: "^2\\.[0-9]+$"} + sync: {$ref: "#/components/schemas/SyncInfo"} + target: {$ref: "#/components/schemas/Target"} + assessment_type: {$ref: "#/components/schemas/AssessmentType"} + scope: {$ref: "#/components/schemas/Scope"} + inventory: {$ref: "#/components/schemas/Inventory"} + SyncStatus: + type: string + enum: [accepted, processing, completed, completed_with_warnings, failed] + AssessmentRef: + type: object + required: [id, name, created, scope_key, url] + properties: + id: {type: string, format: uuid} + name: {type: string} + created: {type: boolean, description: "`true` when this request created the assessment."} + scope_key: {type: [string, "null"], maxLength: 255} + url: {type: string, format: uri} + Timestamps: + type: object + required: [collected_at, received_at] + properties: + collected_at: {$ref: "#/components/schemas/EpochSeconds"} + received_at: {$ref: "#/components/schemas/EpochSeconds"} + processed_at: {anyOf: [{$ref: "#/components/schemas/EpochSeconds"}, {type: "null"}]} + Ordering: + type: object + required: [latest, late] + properties: + latest: {type: boolean, description: This sync is now the assessment's latest.} + late: {type: boolean, description: A newer sync (by collection time) was already stored.} + ContentInfo: + type: object + required: [payload_sha256, content_sha256] + properties: + payload_sha256: {$ref: "#/components/schemas/Sha256"} + content_sha256: {$ref: "#/components/schemas/Sha256"} + unchanged_from: + type: [string, "null"] + format: uuid + description: Always null in 2.0, where an unchanged sync is stored like any other. + AssessmentTypeResult: + type: object + required: [requested, granted] + properties: + requested: {$ref: "#/components/schemas/AssessmentType"} + granted: {$ref: "#/components/schemas/AssessmentType"} + ResourceTypeCount: + type: object + required: [type_id, count] + properties: + type_id: {type: integer} + type_code: {type: string} + count: {type: integer, minimum: 0} + CatalogueResult: + type: object + required: [client, host, unknown_resource_types, inactive_resource_types] + properties: + client: + description: What the collector claimed. Null without a catalogue block. + oneOf: + - type: "null" + - type: object + required: [version, sequence] + properties: + version: {$ref: "#/components/schemas/CatalogueVersion"} + sequence: {type: integer} + host: + type: object + required: [version, sha256] + properties: + version: {$ref: "#/components/schemas/CatalogueVersion"} + sha256: {$ref: "#/components/schemas/Sha256"} + unknown_resource_types: + type: array + items: {$ref: "#/components/schemas/ResourceTypeCount"} + inactive_resource_types: + type: array + items: {$ref: "#/components/schemas/ResourceTypeCount"} + Billing: + type: object + x-host: exitcloud + required: [class, charged_credits, balance] + properties: + class: {type: string, enum: [new]} + charged_credits: {type: integer, minimum: 0} + balance: {type: integer} + transaction_id: {type: [string, "null"]} + ReviewState: + type: object + x-host: escapecloud + properties: + pending: {type: boolean} + approved_sync_id: {type: [string, "null"], format: uuid} + Severity: + type: string + enum: [low, medium, high, critical] + Results: + type: object + description: Only risks that have a severity are returned, identified by code. + required: [scoring_version, risks] + properties: + scoring_version: {type: string} + scores: + description: Present from depth 2. + type: object + required: [exit, human, technology, operational] + properties: + exit: {type: integer, minimum: 0, maximum: 100} + human: {type: integer, minimum: 0, maximum: 100} + technology: {type: integer, minimum: 0, maximum: 100} + operational: {type: integer, minimum: 0, maximum: 100} + risks: + type: array + items: + type: object + required: [code, severity, impacted_resource_type_ids] + properties: + code: {type: string} + severity: {$ref: "#/components/schemas/Severity"} + impacted_resource_type_ids: + type: array + items: {type: integer} + SyncLinks: + type: object + required: [self, assessment] + properties: + self: {type: string} + assessment: {type: string} + Sync: + type: object + required: [object, api_version, id, client_sync_id, request_id, status, assessment, timestamps, ordering, content, assessment_type, catalogue, warnings, links] + properties: + object: {const: sync} + api_version: {$ref: "#/components/schemas/ApiVersion"} + id: {type: string, format: uuid, description: "The host's id (`Sync.order`, `Sync ID` in the UI)."} + client_sync_id: {type: [string, "null"], description: "The CLI's `sync.id`. Null for syncs the host collected itself."} + request_id: {type: [string, "null"]} + status: {$ref: "#/components/schemas/SyncStatus"} + assessment: {$ref: "#/components/schemas/AssessmentRef"} + timestamps: {$ref: "#/components/schemas/Timestamps"} + ordering: {$ref: "#/components/schemas/Ordering"} + content: {$ref: "#/components/schemas/ContentInfo"} + assessment_type: {$ref: "#/components/schemas/AssessmentTypeResult"} + catalogue: {$ref: "#/components/schemas/CatalogueResult"} + warnings: + type: array + items: {$ref: "#/components/schemas/Warning"} + billing: {$ref: "#/components/schemas/Billing"} + review_state: {$ref: "#/components/schemas/ReviewState"} + results: {$ref: "#/components/schemas/Results"} + error: + description: Present when `status` is `failed`. + allOf: + - {$ref: "#/components/schemas/Error"} + links: {$ref: "#/components/schemas/SyncLinks"} + AssessmentSource: + type: string + enum: [engine, cli] + Assessment: + type: object + required: [object, api_version, id, name, source, provider, exit_strategy, assessment_type, scope_key, state, created, latest_sync, url] + properties: + object: {const: assessment} + api_version: {$ref: "#/components/schemas/ApiVersion"} + id: {type: string, format: uuid} + name: {type: string} + source: {$ref: "#/components/schemas/AssessmentSource"} + provider: {type: string, description: The provider code, for example `aws` or `azure`.} + exit_strategy: {$ref: "#/components/schemas/ExitStrategy"} + assessment_type: {$ref: "#/components/schemas/AssessmentType"} + scope_key: {type: [string, "null"], maxLength: 255, description: Recorded by the first sync.} + state: {type: string, enum: [active, archived]} + created: {$ref: "#/components/schemas/EpochSeconds"} + latest_sync: + description: The newest sync by collection time. Null before the first sync. + oneOf: + - type: "null" + - {$ref: "#/components/schemas/Sync"} + url: {type: string, format: uri} + ListPage: + type: object + required: [object, api_version, data, has_more, next_cursor] + properties: + object: {const: list} + api_version: {$ref: "#/components/schemas/ApiVersion"} + has_more: {type: boolean} + next_cursor: {type: [string, "null"], maxLength: 512} + AssessmentList: + allOf: + - {$ref: "#/components/schemas/ListPage"} + - type: object + properties: + data: + type: array + items: {$ref: "#/components/schemas/Assessment"} + SyncList: + allOf: + - {$ref: "#/components/schemas/ListPage"} + - type: object + properties: + data: + type: array + items: {$ref: "#/components/schemas/Sync"} + Tenant: + type: object + required: [type, id, name] + properties: + type: {type: string, enum: [organization, user]} + id: {type: string} + name: {type: string} + WhoamiKey: + type: object + required: [id, name, display, kind, scopes, assessment_id, expires_at] + properties: + id: {type: string, format: uuid} + name: {type: string} + display: {type: string, description: "The prefix and the last four characters, for example `esc_sk_…a1b2`."} + kind: {type: string, enum: [assessment, account]} + scopes: + type: array + items: {type: string, enum: [assessments:create, assessments:read, syncs:write]} + assessment_id: {type: [string, "null"], format: uuid, description: The binding of an assessment key.} + expires_at: {$ref: "#/components/schemas/EpochSeconds"} + last_used_at: {anyOf: [{$ref: "#/components/schemas/EpochSeconds"}, {type: "null"}]} + Whoami: + type: object + required: [object, api_version, request_id, tenant, key, warnings] + properties: + object: {const: whoami} + api_version: {$ref: "#/components/schemas/ApiVersion"} + request_id: {type: string} + tenant: {$ref: "#/components/schemas/Tenant"} + key: {$ref: "#/components/schemas/WhoamiKey"} + warnings: + type: array + items: {$ref: "#/components/schemas/Warning"} + billing: + type: object + x-host: exitcloud + required: [balance] + properties: + balance: {type: integer} + licensing: + type: object + x-host: escapecloud + description: Informational. Never blocks a sync. + properties: + active_assessments: {type: integer} + cloud_providers: {type: integer} + resources: {type: integer} + MetaHost: + type: object + required: [product, region, app_version, base_url] + properties: + product: {type: string, enum: [escapecloud, exitcloud]} + region: {type: [string, "null"], description: "`eu` or `us` on ExitCloud, null on EscapeCloud."} + app_version: {type: string} + base_url: {type: string, format: uri} + MetaLimits: + type: object + required: [max_body_bytes, max_resource_items, max_count_per_item, max_location_chars, max_cost_items, max_egress_items, max_name_chars, clock_skew_seconds, max_collection_age_days, idempotency_replay_hours] + properties: + max_body_bytes: {type: integer, const: 1000000} + max_resource_items: {type: integer, const: 1000} + max_count_per_item: {type: integer, const: 10000} + max_location_chars: {type: integer, const: 255} + max_cost_items: {type: integer, const: 12} + max_egress_items: {type: integer, const: 1000} + max_name_chars: {type: integer, const: 255} + max_egress_tiers_per_item: {type: integer, const: 10} + max_egress_flags_per_item: {type: integer, const: 10} + max_egress_notes_per_item: {type: integer, const: 10} + max_catalogue_type_ids: {type: integer, const: 5000} + clock_skew_seconds: {type: integer, const: 86400} + max_collection_age_days: {type: integer, const: 30} + idempotency_replay_hours: {type: integer, const: 168} + description: The values are the defaults of both hosts; a host may announce others. + AssessmentTypeInfo: + type: object + required: [id, code, name] + properties: + id: {$ref: "#/components/schemas/AssessmentType"} + code: {type: string, enum: [basic, standard, premium]} + name: {type: string, description: The host's own display name.} + requires: {type: array, items: {type: string}} + downgrade_to: {type: integer} + Meta: + type: object + required: [object, api_version, host, contract, clients, catalogue, limits, rate_limits, processing, features, assessment_types] + properties: + object: {const: meta} + api_version: {$ref: "#/components/schemas/ApiVersion"} + host: {$ref: "#/components/schemas/MetaHost"} + contract: + type: object + required: [schema_versions, current] + properties: + schema_versions: {type: array, items: {type: string, pattern: "^2\\.[0-9]+$"}} + current: {type: string, pattern: "^2\\.[0-9]+$"} + legacy: + type: object + additionalProperties: + type: object + properties: + status: {type: string} + sunset: {type: string, format: date} + clients: + type: object + additionalProperties: + type: object + properties: + min_supported: {type: string} + deprecated_below: {type: [string, "null"]} + recommended: {type: string} + blocked: {type: array, items: {type: string}} + catalogue: + type: object + required: [version, sha256] + properties: + version: {$ref: "#/components/schemas/CatalogueVersion"} + sha256: {$ref: "#/components/schemas/Sha256"} + loaded_at: {$ref: "#/components/schemas/EpochSeconds"} + collector_min_version: {type: [string, "null"], description: Always null in 2.0, where no tenant policy rejects old catalogues.} + collector_warn_after_days: {type: integer} + limits: {$ref: "#/components/schemas/MetaLimits"} + rate_limits: + type: object + properties: + per_key: + type: object + properties: + requests_per_minute: {type: integer} + syncs_per_hour: {type: integer} + per_tenant: + type: object + properties: + syncs_per_day: {type: integer} + processing: + type: object + required: [modes] + properties: + modes: + type: array + items: {type: string, enum: [inline, queued]} + description: '`["inline"]` in 2.0.' + max_wait_seconds: {type: integer} + features: + type: object + properties: + upsert_by_scope_key: {type: boolean, const: false} + content_dedupe: {type: boolean, const: false} + results_in_response: {type: boolean} + egress_inventory: {type: boolean} + reviews: {type: boolean, x-host: escapecloud} + oidc: + type: object + properties: + enabled: {type: boolean, const: false} + issuers: {type: array, items: {type: string}} + assessment_types: + type: array + items: {$ref: "#/components/schemas/AssessmentTypeInfo"} + billing: + type: [object, "null"] + x-host: exitcloud + properties: + model: {type: string, enum: [credits]} + prices: + type: object + properties: + new: + type: object + additionalProperties: {type: integer} + licensing: + type: [object, "null"] + x-host: escapecloud + properties: + model: {type: string, enum: [licence]} + enforcement: {type: string, enum: [none]} + reported: + type: array + items: {type: string} diff --git a/docs/api-v2/vectors/content_sha256.json b/docs/api-v2/vectors/content_sha256.json new file mode 100644 index 0000000..8f0a7cd --- /dev/null +++ b/docs/api-v2/vectors/content_sha256.json @@ -0,0 +1,1234 @@ +{ + "function": "content_sha256(body) -> str", + "description": "SHA-256 (lower-case hex) of the UTF-8 bytes of the RFC 8785 canonical JSON of the content form below. It identifies what was collected: it ignores the sync id, the timestamps, the collector, the catalogue block and anything else outside scope, inventory and assessment_type. 'body' is the request body of POST /syncs; only the fields used here need to be present.", + "content_form": { + "assessment_type": "the requested depth as sent", + "scope": "provider plus the provider block as in the request, with these normalisations: AWS regions sorted and de-duplicated; Azure ids and resource group lower-cased, a null resource group left out; the Terraform lineage lower-cased. Unknown fields are left out.", + "inventory.resources": "{count, location, type_id} only (type_code is a label and is left out). Items with the same type_id and location are merged by summing counts. Sorted by type_id, then location.", + "inventory.costs": "{amount, currency, final, month}. amount is normalised as a decimal (no trailing zeros, no exponent, '0' for zero), currency is upper-cased, final defaults to false. Sorted by month, then currency. Omitted costs equal an empty list.", + "inventory.egress": "{collected, items, names, truncated}. A missing egress block equals {collected: false, items: [], names: 'plain', truncated: false}. Items are {flags, name, notes, size_bytes, tiers, type_id}, with flags and notes sorted, tiers {is_archive, size_bytes, tier} sorted by tier, and the items sorted by type_id, then name." + }, + "serialisation": [ + "Object keys sorted, no whitespace, strings in UTF-8 without escaping beyond RFC 8785 (non-ASCII characters are written as they are).", + "Numbers are integers in the I-JSON safe range (at most 9007199254740991 in absolute value). Floats are not allowed." + ], + "cases": [ + { + "name": "minimal inventory", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + }, + "canonical_json": "{\"assessment_type\":2,\"inventory\":{\"costs\":[{\"amount\":\"1234.56\",\"currency\":\"USD\",\"final\":true,\"month\":\"2026-10\"}],\"egress\":{\"collected\":false,\"items\":[],\"names\":\"plain\",\"truncated\":false},\"resources\":[{\"count\":36,\"location\":\"eu-central-1\",\"type_id\":12},{\"count\":8,\"location\":\"eu-central-1\",\"type_id\":40}]},\"scope\":{\"aws\":{\"account_id\":\"123456789012\",\"regions\":[\"eu-central-1\"]},\"provider\":\"aws\"}}", + "sha256": "27ab581110b19cf629481e644386ded634aa3238e243eddb169e1763246562b8" + }, + { + "name": "resources in another order", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + }, + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + }, + "same_as": "minimal inventory" + }, + { + "name": "another sync id, timestamps, collector, catalogue and CI context", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933bff-0000-7000-8000-000000000001", + "started_at": 1762500000, + "completed_at": 1762500100, + "collector": { + "name": "cloudexit", + "version": "2.3.1", + "mode": "api", + "catalogue": { + "version": "2026-11-02", + "sequence": 41, + "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "resource_type_ids": [ + 12, + 40 + ] + } + }, + "ci": { + "provider": "github", + "run_url": "https://github.com/acme/infra/actions/runs/1" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + }, + "same_as": "minimal inventory" + }, + { + "name": "cost amount with a trailing zero and a lower-case currency", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.560", + "currency": "usd", + "final": true + } + ] + } + }, + "same_as": "minimal inventory" + }, + { + "name": "costs omitted equal costs empty", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ] + } + }, + "same_as": "empty costs" + }, + { + "name": "empty costs", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [] + } + }, + "canonical_json": "{\"assessment_type\":2,\"inventory\":{\"costs\":[],\"egress\":{\"collected\":false,\"items\":[],\"names\":\"plain\",\"truncated\":false},\"resources\":[{\"count\":36,\"location\":\"eu-central-1\",\"type_id\":12},{\"count\":8,\"location\":\"eu-central-1\",\"type_id\":40}]},\"scope\":{\"aws\":{\"account_id\":\"123456789012\",\"regions\":[\"eu-central-1\"]},\"provider\":\"aws\"}}", + "sha256": "104f1ac96760970a2b0363e7e745b111fae92627bdb93cd95b7e8df042b36d8d" + }, + { + "name": "egress omitted equals egress not collected", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ], + "egress": { + "collected": false + } + } + }, + "same_as": "egress omitted" + }, + { + "name": "egress omitted", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + }, + "canonical_json": "{\"assessment_type\":2,\"inventory\":{\"costs\":[{\"amount\":\"1234.56\",\"currency\":\"USD\",\"final\":true,\"month\":\"2026-10\"}],\"egress\":{\"collected\":false,\"items\":[],\"names\":\"plain\",\"truncated\":false},\"resources\":[{\"count\":36,\"location\":\"eu-central-1\",\"type_id\":12},{\"count\":8,\"location\":\"eu-central-1\",\"type_id\":40}]},\"scope\":{\"aws\":{\"account_id\":\"123456789012\",\"regions\":[\"eu-central-1\"]},\"provider\":\"aws\"}}", + "sha256": "27ab581110b19cf629481e644386ded634aa3238e243eddb169e1763246562b8" + }, + { + "name": "duplicate resource items are merged", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 40, + "location": "eu-central-1", + "count": 3 + }, + { + "type_id": 12, + "location": "eu-central-1", + "count": 30 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 5 + }, + { + "type_id": 12, + "location": "eu-central-1", + "count": 6 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + }, + "same_as": "merged resources" + }, + { + "name": "merged resources", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + }, + "canonical_json": "{\"assessment_type\":2,\"inventory\":{\"costs\":[{\"amount\":\"1234.56\",\"currency\":\"USD\",\"final\":true,\"month\":\"2026-10\"}],\"egress\":{\"collected\":false,\"items\":[],\"names\":\"plain\",\"truncated\":false},\"resources\":[{\"count\":36,\"location\":\"eu-central-1\",\"type_id\":12},{\"count\":8,\"location\":\"eu-central-1\",\"type_id\":40}]},\"scope\":{\"aws\":{\"account_id\":\"123456789012\",\"regions\":[\"eu-central-1\"]},\"provider\":\"aws\"}}", + "sha256": "27ab581110b19cf629481e644386ded634aa3238e243eddb169e1763246562b8" + }, + { + "name": "unknown fields are ignored", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ], + "profile": "ci" + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36, + "note": "ignored" + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ], + "extra": 1 + } + }, + "same_as": "minimal inventory" + }, + { + "name": "regions in another order and repeated", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "us-east-1", + "eu-west-1", + "us-east-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + }, + "same_as": "regions sorted" + }, + { + "name": "regions sorted", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-west-1", + "us-east-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + }, + "canonical_json": "{\"assessment_type\":2,\"inventory\":{\"costs\":[{\"amount\":\"1234.56\",\"currency\":\"USD\",\"final\":true,\"month\":\"2026-10\"}],\"egress\":{\"collected\":false,\"items\":[],\"names\":\"plain\",\"truncated\":false},\"resources\":[{\"count\":36,\"location\":\"eu-central-1\",\"type_id\":12},{\"count\":8,\"location\":\"eu-central-1\",\"type_id\":40}]},\"scope\":{\"aws\":{\"account_id\":\"123456789012\",\"regions\":[\"eu-west-1\",\"us-east-1\"]},\"provider\":\"aws\"}}", + "sha256": "a30728218d49aa16271a64dcfd8a10b51bfa5491b63fa8490f667a5984f3fdba" + }, + { + "name": "azure ids and resource group in another case", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "azure", + "azure": { + "tenant_id": "0B5F9A5E-7A64-4C0E-9A3A-5F5F1B8D2C11", + "subscription_id": "6F1C2F1E-92D1-4A6E-8C1E-0A3C1D2E4B5A", + "resource_group": "Prod-RG" + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + }, + "same_as": "azure lower case" + }, + { + "name": "azure lower case", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "azure", + "azure": { + "tenant_id": "0b5f9a5e-7a64-4c0e-9a3a-5f5f1b8d2c11", + "subscription_id": "6f1c2f1e-92d1-4a6e-8c1e-0a3c1d2e4b5a", + "resource_group": "prod-rg" + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + }, + "canonical_json": "{\"assessment_type\":2,\"inventory\":{\"costs\":[{\"amount\":\"1234.56\",\"currency\":\"USD\",\"final\":true,\"month\":\"2026-10\"}],\"egress\":{\"collected\":false,\"items\":[],\"names\":\"plain\",\"truncated\":false},\"resources\":[{\"count\":36,\"location\":\"eu-central-1\",\"type_id\":12},{\"count\":8,\"location\":\"eu-central-1\",\"type_id\":40}]},\"scope\":{\"azure\":{\"resource_group\":\"prod-rg\",\"subscription_id\":\"6f1c2f1e-92d1-4a6e-8c1e-0a3c1d2e4b5a\",\"tenant_id\":\"0b5f9a5e-7a64-4c0e-9a3a-5f5f1b8d2c11\"},\"provider\":\"azure\"}}", + "sha256": "5298b115143a7790880726569bcc8bc7eb03a4752c9f29a6b1d16eb7ca900af4" + }, + { + "name": "egress with names, tiers and unicode", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 3, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ], + "egress": { + "collected": true, + "truncated": false, + "names": "plain", + "items": [ + { + "type_id": 12, + "name": "acme-logs", + "size_bytes": 5368709120, + "flags": [ + "b-flag", + "a-flag" + ], + "notes": [], + "tiers": [ + { + "tier": "STANDARD", + "size_bytes": 5368709000, + "is_archive": false + }, + { + "tier": "GLACIER", + "size_bytes": 120, + "is_archive": true + } + ] + }, + { + "type_id": 12, + "name": "Ärchiv 日本 🙂", + "size_bytes": null, + "flags": [], + "notes": [ + "shared blocks" + ] + } + ] + } + } + }, + "canonical_json": "{\"assessment_type\":3,\"inventory\":{\"costs\":[{\"amount\":\"1234.56\",\"currency\":\"USD\",\"final\":true,\"month\":\"2026-10\"}],\"egress\":{\"collected\":true,\"items\":[{\"flags\":[\"a-flag\",\"b-flag\"],\"name\":\"acme-logs\",\"notes\":[],\"size_bytes\":5368709120,\"tiers\":[{\"is_archive\":true,\"size_bytes\":120,\"tier\":\"GLACIER\"},{\"is_archive\":false,\"size_bytes\":5368709000,\"tier\":\"STANDARD\"}],\"type_id\":12},{\"flags\":[],\"name\":\"Ärchiv 日本 🙂\",\"notes\":[\"shared blocks\"],\"size_bytes\":null,\"tiers\":[],\"type_id\":12}],\"names\":\"plain\",\"truncated\":false},\"resources\":[{\"count\":36,\"location\":\"eu-central-1\",\"type_id\":12},{\"count\":8,\"location\":\"eu-central-1\",\"type_id\":40}]},\"scope\":{\"aws\":{\"account_id\":\"123456789012\",\"regions\":[\"eu-central-1\"]},\"provider\":\"aws\"}}", + "sha256": "c016a343affea33e1fb77e91082949d6436dc29155ee5c245775215d3717d211" + }, + { + "name": "egress items, flags and tiers in another order", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 3, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ], + "egress": { + "collected": true, + "truncated": false, + "names": "plain", + "items": [ + { + "type_id": 12, + "name": "Ärchiv 日本 🙂", + "size_bytes": null, + "flags": [], + "notes": [ + "shared blocks" + ] + }, + { + "type_id": 12, + "name": "acme-logs", + "size_bytes": 5368709120, + "flags": [ + "a-flag", + "b-flag" + ], + "notes": [], + "tiers": [ + { + "tier": "GLACIER", + "size_bytes": 120, + "is_archive": true + }, + { + "tier": "STANDARD", + "size_bytes": 5368709000, + "is_archive": false + } + ] + } + ] + } + } + }, + "same_as": "egress with names, tiers and unicode" + }, + { + "name": "a different count", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 37 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + }, + "differs_from": "minimal inventory" + }, + { + "name": "a different assessment type", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 3, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + }, + "differs_from": "minimal inventory" + }, + { + "name": "a different scope", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-west-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": true + } + ] + } + }, + "differs_from": "minimal inventory" + }, + { + "name": "a different cost amount", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.57", + "currency": "USD", + "final": true + } + ] + } + }, + "differs_from": "minimal inventory" + }, + { + "name": "an estimated instead of a final cost", + "body": { + "schema_version": "2.0", + "sync": { + "id": "01933b9e-7c2a-7d41-9a51-3c8f0a6e21d4", + "started_at": 1762149604, + "completed_at": 1762149831, + "collector": { + "name": "cloudexit", + "version": "2.0.0", + "mode": "api" + } + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "inventory": { + "resources": [ + { + "type_id": 12, + "type_code": "AWS::S3::Bucket", + "location": "eu-central-1", + "count": 36 + }, + { + "type_id": 40, + "type_code": "AWS::EC2::Instance", + "location": "eu-central-1", + "count": 8 + } + ], + "costs": [ + { + "month": "2026-10", + "amount": "1234.56", + "currency": "USD", + "final": false + } + ] + } + }, + "differs_from": "minimal inventory" + } + ] +} diff --git a/docs/api-v2/vectors/scope_key.json b/docs/api-v2/vectors/scope_key.json new file mode 100644 index 0000000..723fa08 --- /dev/null +++ b/docs/api-v2/vectors/scope_key.json @@ -0,0 +1,509 @@ +{ + "function": "scope_key(scope) -> str", + "description": "The canonical scope key the host derives from a request's scope. Invalid input raises ValueError (optionally with a pointer attribute, a JSON pointer into scope).", + "rules": [ + "Everything is lower case, without whitespace; a key is at most 255 characters.", + "AWS: aws::. The account id is exactly 12 digits (kept as sent). Regions are lower case and distinct; one region gives that region, two or more regions or '*' give '*'.", + "Azure: azure:::. Tenant and subscription are GUIDs (any case in, lower case out). A missing or null resource group means the whole subscription: '*'. Resource groups are 1 to 90 characters of letters, digits, '_', '(', ')', '-' and '.', cannot end with '.', and are lower-cased.", + "Terraform state: tfstate:. The lineage is a UUID (lower-cased). A tfstate scope has no provider block; the provider is still required.", + "Fields that are not part of the scope are ignored." + ], + "cases": [ + { + "name": "aws, one region", + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "expect": { + "scope_key": "aws:123456789012:eu-central-1" + } + }, + { + "name": "aws, two regions are all regions", + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "us-east-1", + "eu-west-1" + ] + } + }, + "expect": { + "scope_key": "aws:123456789012:*" + } + }, + { + "name": "aws, explicit all regions", + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "*" + ] + } + }, + "expect": { + "scope_key": "aws:123456789012:*" + } + }, + { + "name": "aws, the same region twice", + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1", + "eu-central-1" + ] + } + }, + "expect": { + "scope_key": "aws:123456789012:eu-central-1" + } + }, + { + "name": "aws, gov-cloud region", + "scope": { + "provider": "aws", + "aws": { + "account_id": "210987654321", + "regions": [ + "us-gov-west-1" + ] + } + }, + "expect": { + "scope_key": "aws:210987654321:us-gov-west-1" + } + }, + { + "name": "aws, leading zeros in the account id are kept", + "scope": { + "provider": "aws", + "aws": { + "account_id": "000123456789", + "regions": [ + "ap-southeast-2" + ] + } + }, + "expect": { + "scope_key": "aws:000123456789:ap-southeast-2" + } + }, + { + "name": "aws, unknown fields are ignored", + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + }, + "note": "ignored" + }, + "expect": { + "scope_key": "aws:123456789012:eu-central-1" + } + }, + { + "name": "azure, resource group in mixed case", + "scope": { + "provider": "azure", + "azure": { + "tenant_id": "0b5f9a5e-7a64-4c0e-9a3a-5f5f1b8d2c11", + "subscription_id": "6f1c2f1e-92d1-4a6e-8c1e-0a3c1d2e4b5a", + "resource_group": "Prod-RG_Core" + } + }, + "expect": { + "scope_key": "azure:0b5f9a5e-7a64-4c0e-9a3a-5f5f1b8d2c11:6f1c2f1e-92d1-4a6e-8c1e-0a3c1d2e4b5a:prod-rg_core" + } + }, + { + "name": "azure, whole subscription (no resource group)", + "scope": { + "provider": "azure", + "azure": { + "tenant_id": "0b5f9a5e-7a64-4c0e-9a3a-5f5f1b8d2c11", + "subscription_id": "6f1c2f1e-92d1-4a6e-8c1e-0a3c1d2e4b5a" + } + }, + "expect": { + "scope_key": "azure:0b5f9a5e-7a64-4c0e-9a3a-5f5f1b8d2c11:6f1c2f1e-92d1-4a6e-8c1e-0a3c1d2e4b5a:*" + } + }, + { + "name": "azure, whole subscription (null resource group)", + "scope": { + "provider": "azure", + "azure": { + "tenant_id": "0b5f9a5e-7a64-4c0e-9a3a-5f5f1b8d2c11", + "subscription_id": "6f1c2f1e-92d1-4a6e-8c1e-0a3c1d2e4b5a", + "resource_group": null + } + }, + "expect": { + "scope_key": "azure:0b5f9a5e-7a64-4c0e-9a3a-5f5f1b8d2c11:6f1c2f1e-92d1-4a6e-8c1e-0a3c1d2e4b5a:*" + } + }, + { + "name": "azure, upper-case GUIDs are lowered", + "scope": { + "provider": "azure", + "azure": { + "tenant_id": "0B5F9A5E-7A64-4C0E-9A3A-5F5F1B8D2C11", + "subscription_id": "6F1C2F1E-92D1-4A6E-8C1E-0A3C1D2E4B5A", + "resource_group": "RG" + } + }, + "expect": { + "scope_key": "azure:0b5f9a5e-7a64-4c0e-9a3a-5f5f1b8d2c11:6f1c2f1e-92d1-4a6e-8c1e-0a3c1d2e4b5a:rg" + } + }, + { + "name": "azure, resource group with parentheses and a period", + "scope": { + "provider": "azure", + "azure": { + "tenant_id": "0b5f9a5e-7a64-4c0e-9a3a-5f5f1b8d2c11", + "subscription_id": "6f1c2f1e-92d1-4a6e-8c1e-0a3c1d2e4b5a", + "resource_group": "App.Rg(1)" + } + }, + "expect": { + "scope_key": "azure:0b5f9a5e-7a64-4c0e-9a3a-5f5f1b8d2c11:6f1c2f1e-92d1-4a6e-8c1e-0a3c1d2e4b5a:app.rg(1)" + } + }, + { + "name": "tfstate, lineage in upper case", + "scope": { + "provider": "aws", + "tfstate": { + "lineage": "7B1E5C0A-3D2F-4A8B-9C6D-1E2F3A4B5C6D", + "serial": 42 + } + }, + "expect": { + "scope_key": "tfstate:7b1e5c0a-3d2f-4a8b-9c6d-1e2f3a4b5c6d" + } + }, + { + "name": "tfstate, azure provider", + "scope": { + "provider": "azure", + "tfstate": { + "lineage": "7b1e5c0a-3d2f-4a8b-9c6d-1e2f3a4b5c6d" + } + }, + "expect": { + "scope_key": "tfstate:7b1e5c0a-3d2f-4a8b-9c6d-1e2f3a4b5c6d" + } + }, + { + "name": "not an object", + "scope": "aws:123456789012:eu-central-1", + "expect": { + "error": { + "pointer": "" + } + } + }, + { + "name": "missing provider", + "scope": { + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + } + }, + "expect": { + "error": { + "pointer": "/provider" + } + } + }, + { + "name": "unsupported provider", + "scope": { + "provider": "gcp", + "gcp": {} + }, + "expect": { + "error": { + "pointer": "/provider" + } + } + }, + { + "name": "aws, provider block missing", + "scope": { + "provider": "aws" + }, + "expect": { + "error": { + "pointer": "/aws" + } + } + }, + { + "name": "aws, block of the other provider", + "scope": { + "provider": "aws", + "azure": { + "tenant_id": "0b5f9a5e-7a64-4c0e-9a3a-5f5f1b8d2c11", + "subscription_id": "6f1c2f1e-92d1-4a6e-8c1e-0a3c1d2e4b5a" + } + }, + "expect": { + "error": { + "pointer": "/aws" + } + } + }, + { + "name": "aws, account id too short", + "scope": { + "provider": "aws", + "aws": { + "account_id": "12345678901", + "regions": [ + "eu-central-1" + ] + } + }, + "expect": { + "error": { + "pointer": "/aws/account_id" + } + } + }, + { + "name": "aws, account id with letters", + "scope": { + "provider": "aws", + "aws": { + "account_id": "12345678901a", + "regions": [ + "eu-central-1" + ] + } + }, + "expect": { + "error": { + "pointer": "/aws/account_id" + } + } + }, + { + "name": "aws, account id as a number", + "scope": { + "provider": "aws", + "aws": { + "account_id": 123456789012, + "regions": [ + "eu-central-1" + ] + } + }, + "expect": { + "error": { + "pointer": "/aws/account_id" + } + } + }, + { + "name": "aws, no regions", + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [] + } + }, + "expect": { + "error": { + "pointer": "/aws/regions" + } + } + }, + { + "name": "aws, region in upper case", + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "EU-CENTRAL-1" + ] + } + }, + "expect": { + "error": { + "pointer": "/aws/regions/0" + } + } + }, + { + "name": "aws, second region invalid", + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1", + "mars" + ] + } + }, + "expect": { + "error": { + "pointer": "/aws/regions/1" + } + } + }, + { + "name": "azure, tenant is not a GUID", + "scope": { + "provider": "azure", + "azure": { + "tenant_id": "not-a-guid", + "subscription_id": "6f1c2f1e-92d1-4a6e-8c1e-0a3c1d2e4b5a", + "resource_group": "rg" + } + }, + "expect": { + "error": { + "pointer": "/azure/tenant_id" + } + } + }, + { + "name": "azure, subscription is not a GUID", + "scope": { + "provider": "azure", + "azure": { + "tenant_id": "0b5f9a5e-7a64-4c0e-9a3a-5f5f1b8d2c11", + "subscription_id": "1234", + "resource_group": "rg" + } + }, + "expect": { + "error": { + "pointer": "/azure/subscription_id" + } + } + }, + { + "name": "azure, resource group with a space", + "scope": { + "provider": "azure", + "azure": { + "tenant_id": "0b5f9a5e-7a64-4c0e-9a3a-5f5f1b8d2c11", + "subscription_id": "6f1c2f1e-92d1-4a6e-8c1e-0a3c1d2e4b5a", + "resource_group": "my rg" + } + }, + "expect": { + "error": { + "pointer": "/azure/resource_group" + } + } + }, + { + "name": "azure, resource group ends with a period", + "scope": { + "provider": "azure", + "azure": { + "tenant_id": "0b5f9a5e-7a64-4c0e-9a3a-5f5f1b8d2c11", + "subscription_id": "6f1c2f1e-92d1-4a6e-8c1e-0a3c1d2e4b5a", + "resource_group": "rg." + } + }, + "expect": { + "error": { + "pointer": "/azure/resource_group" + } + } + }, + { + "name": "azure, resource group longer than 90 characters", + "scope": { + "provider": "azure", + "azure": { + "tenant_id": "0b5f9a5e-7a64-4c0e-9a3a-5f5f1b8d2c11", + "subscription_id": "6f1c2f1e-92d1-4a6e-8c1e-0a3c1d2e4b5a", + "resource_group": "rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr" + } + }, + "expect": { + "error": { + "pointer": "/azure/resource_group" + } + } + }, + { + "name": "azure, empty resource group", + "scope": { + "provider": "azure", + "azure": { + "tenant_id": "0b5f9a5e-7a64-4c0e-9a3a-5f5f1b8d2c11", + "subscription_id": "6f1c2f1e-92d1-4a6e-8c1e-0a3c1d2e4b5a", + "resource_group": "" + } + }, + "expect": { + "error": { + "pointer": "/azure/resource_group" + } + } + }, + { + "name": "tfstate, lineage is not a UUID", + "scope": { + "provider": "aws", + "tfstate": { + "lineage": "lineage-1" + } + }, + "expect": { + "error": { + "pointer": "/tfstate/lineage" + } + } + }, + { + "name": "tfstate together with a provider block", + "scope": { + "provider": "aws", + "aws": { + "account_id": "123456789012", + "regions": [ + "eu-central-1" + ] + }, + "tfstate": { + "lineage": "7b1e5c0a-3d2f-4a8b-9c6d-1e2f3a4b5c6d" + } + }, + "expect": { + "error": { + "pointer": "/aws" + } + } + } + ] +} diff --git a/main.py b/main.py index f6e4fa7..21cef17 100644 --- a/main.py +++ b/main.py @@ -30,7 +30,12 @@ generate_egress_html_report, generate_egress_pdf_report, ) -from core.utils_sync import write_assessment_payload +from core.sync_document import ( + RunContext, + SyncDocumentError, + new_uuid7, + prepare_sync_document, +) from utils.azure import ( select_subscription, select_resource_group, @@ -256,6 +261,8 @@ def handle_aws(args): non_interactive=args.non_interactive, egress=args.egress, settings=getattr(args, "settings", None), + emit_payload=getattr(args, "emit_payload", None), + egress_names=getattr(args, "egress_names", "plain"), ) return @@ -292,6 +299,8 @@ def handle_aws(args): non_interactive=args.non_interactive, egress=args.egress, settings=getattr(args, "settings", None), + emit_payload=getattr(args, "emit_payload", None), + egress_names=getattr(args, "egress_names", "plain"), ) @@ -471,6 +480,8 @@ def handle_azure(args): non_interactive=args.non_interactive, egress=args.egress, settings=getattr(args, "settings", None), + emit_payload=getattr(args, "emit_payload", None), + egress_names=getattr(args, "egress_names", "plain"), ) return @@ -505,6 +516,8 @@ def handle_azure(args): non_interactive=args.non_interactive, egress=args.egress, settings=getattr(args, "settings", None), + emit_payload=getattr(args, "emit_payload", None), + egress_names=getattr(args, "egress_names", "plain"), ) @@ -516,9 +529,13 @@ def run_assessment( non_interactive=False, egress=False, settings=None, + emit_payload=None, + egress_names="plain", ): - # Record the assessment start time to propagate across stages + # Record the assessment start time to propagate across stages. The sync id + # is created here, at collection start, and identifies the whole run. started_at = int(time.time()) + sync_id = new_uuid7() # Bound up front so the error handler can reference it even if the crash # happens before the report directory is created. @@ -730,18 +747,6 @@ def run_assessment( or f"Exit Assessment {datetime.now().strftime('%Y%m%d_%H%M%S')}" ) - payload_path = None - if dry_run: - payload_path = write_assessment_payload( - raw_data_path, - report_path=report_path, - name=name, - started_at=started_at, - exit_strategy=config["exitStrategy"], - cloud_service_provider=config["cloudServiceProvider"], - assessment_type=config["assessmentType"], - ) - # Stage 5 – Online / Offline Risk Assessment if dry_run or mode == "offline": console.print("Stage #5 – Offline Risk Assessment", style="bold") @@ -811,6 +816,7 @@ def run_assessment( # Stage 7: Egress Estimation (opt-in via --egress). # A failure here must not discard the completed assessment. egress_failed = False + egress_collected = False egress_json_path = None egress_html_path = None egress_pdf_path = None @@ -832,6 +838,7 @@ def run_assessment( if egress_result["success"]: print_step("Estimating egress data...", status="ok") egress_json_path = egress_result.get("json_path") + egress_collected = True with console.status("Generating egress report...", spinner="dots"): egress_report_result = generate_egress_html_report( @@ -872,6 +879,37 @@ def run_assessment( ) egress_failed = True + # Sync document: the run's result in the API v2 shape. Without + # --dry-run or --emit-payload a problem here does not discard the + # finished report. + sync_output = None + try: + sync_output = prepare_sync_document( + report_path, + RunContext( + sync_id=sync_id, + started_at=started_at, + cloud_service_provider=config["cloudServiceProvider"], + assessment_type=config["assessmentType"], + provider_details=config["providerDetails"], + egress_collected=egress_collected, + egress_names=egress_names, + ), + emit_dir=emit_payload, + ) + except (SyncDocumentError, OSError) as e: + wanted = bool(dry_run or emit_payload) + console.print("-------------------------------------------") + print_step( + "Writing the sync document...", + status="error" if wanted else "warning", + logs=str(e), + ) + for problem in getattr(e, "problems", [])[:10]: + console.print(f" ↳ {problem}", style="dim") + if wanted: + sys.exit(codes.REPORT) + # Output the report path after the separator if not (dry_run or non_interactive): console.print("-------------------------------------------") @@ -887,8 +925,10 @@ def run_assessment( console.print("Contact: request_report@escapecloud.io") console.print("-------------------------------------------") console.print("Outputs:", style="bold") - if payload_path: - console.print(f"Payload: {payload_path}", style="cyan") + if sync_output: + console.print(f"Sync document: {sync_output.path}", style="cyan") + if sync_output.emitted: + console.print(f"Emitted copy: {sync_output.emitted}", style="cyan") html_report_path = report_status.get("reports", {}).get("HTML") if html_report_path: console.print(f"HTML Report: {html_report_path}", style="cyan") @@ -939,7 +979,7 @@ def parse_arguments(): " python3 main.py azure --config config.json # Use a configuration file for Azure\n" " python3 main.py azure --cli # Use Azure CLI credentials\n" " python3 main.py azure --name 'DMS System' # Use a pre-defined assessment name\n" - " python3 main.py aws --config config.json --dry-run # Local report + payload.json, no remote sync\n" + " python3 main.py aws --config config.json --dry-run # Local report, no remote sync\n" " python3 main.py azure --config config.json --dry-run\n" " python3 main.py aws --config config.json --egress # Estimate egress data volume\n" " python3 main.py azure --config config.json --egress\n" @@ -951,6 +991,8 @@ def parse_arguments(): " python3 main.py catalogue update # Download the latest catalogue\n" " python3 main.py catalogue import FILE.db.gz # Install a catalogue offline\n" " python3 main.py aws --tfstate infra.tfstate --catalogue FILE.db.gz\n" + " python3 main.py aws --config config.json --emit-payload out/ # Copy sync.json to out/\n" + " python3 main.py aws --config config.json --egress --egress-names hashed\n" ), formatter_class=argparse.RawDescriptionHelpFormatter, ) @@ -1010,10 +1052,25 @@ def parse_arguments(): help="Use this catalogue file (.db or .db.gz) for the run without installing it.", ) + # Options for the sync document every assessment writes + sync_flags = argparse.ArgumentParser(add_help=False) + sync_flags.add_argument( + "--emit-payload", + metavar="DIR", + type=str, + help="Also write a copy of the sync document to DIR/.json.", + ) + sync_flags.add_argument( + "--egress-names", + choices=("plain", "hashed"), + default="plain", + help="Send egress resource names as they are, or as salted hashes.", + ) + # Subparser for AWS aws_parser = subparsers.add_parser( "aws", - parents=[common, platform, catalogue_flag], + parents=[common, platform, catalogue_flag, sync_flags], help="Perform an AWS assessment.", ) aws_group = aws_parser.add_mutually_exclusive_group(required=False) @@ -1045,8 +1102,8 @@ def parse_arguments(): "--dry-run", action="store_true", help=( - "Run a local assessment and also write raw_data/payload.json without " - "remote sync." + "Run a local assessment without remote sync. The sync document " + "raw_data/sync.json is written in every run." ), ) aws_parser.add_argument( @@ -1060,7 +1117,7 @@ def parse_arguments(): # Subparser for Azure azure_parser = subparsers.add_parser( "azure", - parents=[common, platform, catalogue_flag], + parents=[common, platform, catalogue_flag, sync_flags], help="Perform an Azure assessment.", ) azure_group = azure_parser.add_mutually_exclusive_group(required=False) @@ -1092,8 +1149,8 @@ def parse_arguments(): "--dry-run", action="store_true", help=( - "Run a local assessment and also write raw_data/payload.json without " - "remote sync." + "Run a local assessment without remote sync. The sync document " + "raw_data/sync.json is written in every run." ), ) azure_parser.add_argument( diff --git a/pyproject.toml b/pyproject.toml index 41f23b2..a43592b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,8 @@ dependencies = [ dev = [ "black", "ruff", + "jsonschema", + "PyYAML", ] [project.scripts] diff --git a/requirements-dev.txt b/requirements-dev.txt index 8d824b2..fae7aa9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,5 @@ -r requirements.txt black==26.10.1 ruff==0.17.1 +jsonschema==4.26.0 +PyYAML==6.0.3 diff --git a/tests/sync_fixtures.py b/tests/sync_fixtures.py new file mode 100644 index 0000000..0df270d --- /dev/null +++ b/tests/sync_fixtures.py @@ -0,0 +1,125 @@ +import json +import sqlite3 +from datetime import datetime, timezone +from pathlib import Path + +import yaml +from jsonschema import Draft202012Validator + +from tests.catalogue_fixtures import build_catalogue + +DEFINITION = Path(__file__).resolve().parent.parent / "docs" / "api-v2" + +STARTED_AT = int(datetime(2026, 10, 23, 12, 0, tzinfo=timezone.utc).timestamp()) +COMPLETED_AT = STARTED_AT + 90 +SYNC_ID = "01939b8e-7c2a-7d41-9a51-3c8f0a6e21d4" + +ACCESS_KEY = "AKIAFAKEACCESSKEY1234" +SECRET_KEY = "fake-secret-key-AAAABBBBCCCC" +SESSION_TOKEN = "fake-session-token-XYZXYZXYZ" +CLIENT_SECRET = "fake-client-secret-0123456789" +SECRETS = (ACCESS_KEY, SECRET_KEY, SESSION_TOKEN, CLIENT_SECRET) + +RUN_TABLES = """ +CREATE TABLE resource_inventory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, resource_type INTEGER NOT NULL, + location TEXT NOT NULL, count INTEGER NOT NULL, UNIQUE(resource_type, location) +); +CREATE TABLE cost_inventory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, month TEXT NOT NULL, cost REAL NOT NULL, + currency TEXT NOT NULL, UNIQUE(month) +); +CREATE TABLE egress_inventory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, resource_type INTEGER NOT NULL, + name TEXT NOT NULL, size_bytes INTEGER, size_unknown INTEGER NOT NULL DEFAULT 0, + flags TEXT, notes TEXT +); +CREATE TABLE egress_inventory_tier ( + id INTEGER PRIMARY KEY AUTOINCREMENT, egress_inventory_id INTEGER NOT NULL, + tier TEXT NOT NULL, size_bytes INTEGER NOT NULL, + is_archive INTEGER NOT NULL DEFAULT 0 +); +""" + + +def egress_row(type_id, name, size=None, flags=(), notes=(), tiers=()): + return { + "type_id": type_id, + "name": name, + "size": size, + "flags": list(flags), + "notes": list(notes), + "tiers": list(tiers), + } + + +def build_run( + report_path, + *, + resources=(), + costs=(), + egress=(), + tfstate_scope=None, + **catalogue, +): + report = Path(report_path) + (report / "data").mkdir(parents=True, exist_ok=True) + (report / "raw_data").mkdir(parents=True, exist_ok=True) + + db_path = report / "data" / "assessment.db" + build_catalogue(db_path, **catalogue) + conn = sqlite3.connect(db_path) + conn.executescript(RUN_TABLES) + conn.executemany( + "INSERT INTO resource_inventory (resource_type, location, count) " + "VALUES (?, ?, ?)", + resources, + ) + conn.executemany( + "INSERT INTO cost_inventory (month, cost, currency) VALUES (?, ?, ?)", costs + ) + for row in egress: + cursor = conn.execute( + "INSERT INTO egress_inventory " + "(resource_type, name, size_bytes, size_unknown, flags, notes) " + "VALUES (?, ?, ?, ?, ?, ?)", + ( + row["type_id"], + row["name"], + row["size"], + int(row["size"] is None), + json.dumps(row["flags"]), + json.dumps(row["notes"]), + ), + ) + conn.executemany( + "INSERT INTO egress_inventory_tier " + "(egress_inventory_id, tier, size_bytes, is_archive) VALUES (?, ?, ?, ?)", + [(cursor.lastrowid, *tier) for tier in row["tiers"]], + ) + conn.commit() + conn.close() + + if tfstate_scope is not None: + manifest = report / "raw_data" / "tfstate_manifest.json" + manifest.write_text(json.dumps({"scope": tfstate_scope}), encoding="utf-8") + return str(report) + + +def request_validator(): + spec = yaml.safe_load((DEFINITION / "openapi.yaml").read_text(encoding="utf-8")) + root = { + "$ref": "#/components/schemas/SyncRequest", + "components": spec["components"], + } + return Draft202012Validator( + root, format_checker=Draft202012Validator.FORMAT_CHECKER + ) + + +def schema_errors(document): + validator = request_validator() + return sorted( + f"/{'/'.join(str(part) for part in error.absolute_path)}: {error.message}" + for error in validator.iter_errors(document) + ) diff --git a/tests/test_api_v2_definition_copy.py b/tests/test_api_v2_definition_copy.py new file mode 100644 index 0000000..a5b6b7f --- /dev/null +++ b/tests/test_api_v2_definition_copy.py @@ -0,0 +1,76 @@ +import hashlib +import shutil +import tempfile +import unittest +from pathlib import Path + +DEFINITION = Path(__file__).resolve().parent.parent / "docs" / "api-v2" +MANIFEST = "MANIFEST.sha256" + + +def manifest_problems(directory): + directory = Path(directory) + expected = {} + for line in (directory / MANIFEST).read_text(encoding="utf-8").splitlines(): + digest, _, name = line.partition(" ") + expected[name] = digest + + actual = { + path.relative_to(directory) + .as_posix(): hashlib.sha256(path.read_bytes()) + .hexdigest() + for path in directory.rglob("*") + if path.is_file() + and path.name != MANIFEST + and "__pycache__" not in path.parts + and path.suffix != ".pyc" + } + + problems = [] + for name in sorted(expected.keys() - actual.keys()): + problems.append(f"missing: {name}") + for name in sorted(actual.keys() - expected.keys()): + problems.append(f"not in the manifest: {name}") + for name in sorted(expected.keys() & actual.keys()): + if expected[name] != actual[name]: + problems.append(f"modified: {name}") + return problems + + +class DefinitionCopyTests(unittest.TestCase): + def test_the_version_file_is_a_semantic_version(self): + version = (DEFINITION / "VERSION").read_text(encoding="utf-8").strip() + major, minor, patch = version.split(".") + self.assertEqual(major, "2") + self.assertTrue(minor.isdigit() and patch.isdigit()) + + def test_every_file_matches_the_manifest(self): + self.assertEqual(manifest_problems(DEFINITION), []) + + def test_the_manifest_lists_the_version_file(self): + manifest = (DEFINITION / MANIFEST).read_text(encoding="utf-8") + self.assertIn(" VERSION\n", manifest) + + def test_a_modified_file_is_detected(self): + with tempfile.TemporaryDirectory() as tmp: + copy = Path(tmp) / "api-v2" + shutil.copytree(DEFINITION, copy) + (copy / "VERSION").write_text("2.9.9\n", encoding="utf-8") + + self.assertEqual(manifest_problems(copy), ["modified: VERSION"]) + + def test_a_missing_or_extra_file_is_detected(self): + with tempfile.TemporaryDirectory() as tmp: + copy = Path(tmp) / "api-v2" + shutil.copytree(DEFINITION, copy) + (copy / "errors.yaml").unlink() + (copy / "extra.json").write_text("{}", encoding="utf-8") + + self.assertEqual( + manifest_problems(copy), + ["missing: errors.yaml", "not in the manifest: extra.json"], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_sync_document.py b/tests/test_sync_document.py new file mode 100644 index 0000000..7bce32a --- /dev/null +++ b/tests/test_sync_document.py @@ -0,0 +1,1005 @@ +import copy +import hashlib +import hmac +import json +import os +import stat +import tempfile +import unittest +from argparse import Namespace +from decimal import Decimal +from pathlib import Path +from unittest.mock import patch + +import main +from core import sync_document +from core.sync_document import ( + SCHEMA_VERSION, + RunContext, + ScopeError, + SyncDocumentError, + build_sync_document, + decimal_amount, + new_uuid7, + prepare_sync_document, + scope_key, + validate_document, +) +from tests.sync_fixtures import ( + ACCESS_KEY, + CLIENT_SECRET, + COMPLETED_AT, + DEFINITION, + SECRET_KEY, + SECRETS, + SESSION_TOKEN, + STARTED_AT, + SYNC_ID, + build_run, + egress_row, + schema_errors, +) +from utils import codes +from utils.version import __version__ + +LOGGER = "core.engine.sync_document" +AWS_DETAILS = { + "accessKey": ACCESS_KEY, + "secretKey": SECRET_KEY, + "sessionToken": SESSION_TOKEN, + "region": "eu-central-1", + "accountId": "123456789012", +} +AZURE_DETAILS = { + "tenantId": "11111111-1111-4111-8111-111111111111", + "subscriptionId": "22222222-2222-4222-8222-222222222222", + "resourceGroupName": "Prod-RG", + "clientId": "33333333-3333-4333-8333-333333333333", + "clientSecret": CLIENT_SECRET, + "credential": object(), +} +CLAIM = { + "version": "2026-11-02", + "sequence": 41, + "sha256": "c" * 64, +} + + +class SyncDocumentTestCase(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.tmp = Path(tmp.name) + patcher = patch.dict(os.environ, {"XDG_DATA_HOME": str(self.tmp / "data")}) + patcher.start() + self.addCleanup(patcher.stop) + + def report(self, name=None, **kwargs): + self.reports = getattr(self, "reports", 0) + 1 + return build_run(self.tmp / (name or f"report-{self.reports}"), **kwargs) + + def context(self, provider=2, details=None, **overrides): + values = dict( + sync_id=SYNC_ID, + started_at=STARTED_AT, + cloud_service_provider=provider, + assessment_type=2, + provider_details=details + or (AWS_DETAILS if provider == 2 else AZURE_DETAILS), + completed_at=COMPLETED_AT, + ) + values.update(overrides) + return RunContext(**values) + + def assert_valid(self, document): + self.assertEqual(schema_errors(document), []) + validate_document(document) + + +AWS_RESOURCES = [ + (1, "eu-central-1", 8), + (4, "eu-central-1", 2), + (2, "eu-central-1", 36), +] +AWS_COSTS = [("2026-09-01", 1234.5600000000002, "USD"), ("2026-10-01", 15.5, "USD")] + + +def resource(type_id, code, location, count): + return {"type_id": type_id, "type_code": code, "location": location, "count": count} + + +class GoldenDocumentTests(SyncDocumentTestCase): + def test_aws_api_run(self): + report = self.report(resources=AWS_RESOURCES, costs=AWS_COSTS) + + document = build_sync_document(report, self.context(2)) + + self.assertEqual( + document, + { + "schema_version": "2.0", + "sync": { + "id": SYNC_ID, + "started_at": STARTED_AT, + "completed_at": COMPLETED_AT, + "collector": { + "name": "cloudexit", + "version": __version__, + "mode": "api", + "catalogue": {**CLAIM, "resource_type_ids": [1, 4]}, + }, + }, + "assessment_type": 2, + "scope": { + "provider": "aws", + "aws": {"account_id": "123456789012", "regions": ["eu-central-1"]}, + }, + "inventory": { + "resources": [ + resource(1, "AWS.EC2.DescribeInstances", "eu-central-1", 8), + resource(2, "AWS.S3.ListBuckets", "eu-central-1", 36), + resource(4, "AWS.RDS.DescribeDBInstances", "eu-central-1", 2), + ], + "costs": [ + { + "month": "2026-09", + "amount": "1234.56", + "currency": "USD", + "final": True, + }, + { + "month": "2026-10", + "amount": "15.5", + "currency": "USD", + "final": False, + }, + ], + "egress": { + "collected": False, + "truncated": False, + "names": "plain", + "items": [], + }, + }, + }, + ) + self.assert_valid(document) + + def test_azure_api_run(self): + report = self.report( + resources=[(3, "westeurope", 5)], costs=[("2026-10-01", 7, "EUR")] + ) + + document = build_sync_document(report, self.context(1, assessment_type=1)) + + self.assertEqual( + document["scope"], + { + "provider": "azure", + "azure": { + "tenant_id": AZURE_DETAILS["tenantId"], + "subscription_id": AZURE_DETAILS["subscriptionId"], + "resource_group": "Prod-RG", + }, + }, + ) + self.assertEqual(document["assessment_type"], 1) + self.assertEqual( + document["sync"]["collector"]["catalogue"]["resource_type_ids"], [3] + ) + self.assertEqual( + document["inventory"]["resources"], + [resource(3, "Azure.Compute.VirtualMachines", "westeurope", 5)], + ) + self.assertEqual( + document["inventory"]["costs"], + [{"month": "2026-10", "amount": "7", "currency": "EUR", "final": False}], + ) + self.assert_valid(document) + + def test_azure_subscription_wide_scope_has_no_resource_group(self): + details = {**AZURE_DETAILS, "resourceGroupName": ""} + document = build_sync_document( + self.report(resources=[(3, "westeurope", 1)]), self.context(1, details) + ) + self.assertIsNone(document["scope"]["azure"]["resource_group"]) + self.assert_valid(document) + + def test_tfstate_run(self): + report = self.report( + resources=[(1, "eu-west-1", 3)], + tfstate_scope={ + "file": "infra.tfstate", + "lineage": "e1c2f0c0-0000-4000-8000-000000000000", + "serial": 7, + }, + ) + details = {"tfstatePath": "infra.tfstate"} + + document = build_sync_document( + report, self.context(2, details, assessment_type=1) + ) + + self.assertEqual(document["sync"]["collector"]["mode"], "tfstate") + self.assertEqual( + document["scope"], + { + "provider": "aws", + "tfstate": { + "lineage": "e1c2f0c0-0000-4000-8000-000000000000", + "serial": 7, + }, + }, + ) + self.assertEqual(document["inventory"]["costs"], []) + self.assert_valid(document) + + def test_a_tfstate_without_a_lineage_cannot_be_described(self): + report = self.report(tfstate_scope={"lineage": None, "serial": 1}) + with self.assertRaises(SyncDocumentError): + build_sync_document( + report, self.context(2, {"tfstatePath": "infra.tfstate"}) + ) + + def test_a_dataset_without_catalogue_meta_leaves_the_catalogue_out(self): + report = self.report(resources=AWS_RESOURCES, meta=False) + + document = build_sync_document(report, self.context(2)) + + self.assertNotIn("catalogue", document["sync"]["collector"]) + self.assert_valid(document) + + def test_a_missing_aws_account_id_is_an_error(self): + details = {k: v for k, v in AWS_DETAILS.items() if k != "accountId"} + with self.assertRaises(SyncDocumentError) as ctx: + build_sync_document(self.report(), self.context(2, details)) + self.assertIn("/aws/account_id", str(ctx.exception)) + + def test_assessment_type_follows_the_run_depth(self): + report = self.report(resources=AWS_RESOURCES) + for assessment_type, collected, expected in ( + (1, False, 1), + (2, False, 2), + (1, True, 3), + (2, True, 3), + ): + with self.subTest(assessment_type=assessment_type, collected=collected): + document = build_sync_document( + report, + self.context( + 2, + assessment_type=assessment_type, + egress_collected=collected, + ), + ) + self.assertEqual(document["assessment_type"], expected) + + def test_the_target_and_ci_are_left_out_unless_given(self): + document = build_sync_document(self.report(), self.context(2)) + self.assertNotIn("target", document) + self.assertNotIn("ci", document["sync"]) + + def test_the_collector_version_and_schema_version(self): + document = build_sync_document(self.report(), self.context(2)) + version = (DEFINITION / "VERSION").read_text(encoding="utf-8").strip() + major, minor, _ = version.split(".") + + self.assertEqual(SCHEMA_VERSION, f"{major}.{minor}") + self.assertEqual(document["schema_version"], SCHEMA_VERSION) + self.assertEqual(document["sync"]["collector"]["version"], __version__) + + +class EgressTests(SyncDocumentTestCase): + ROWS = [ + egress_row(2, "bucket-b", None, notes=["size unknown"]), + egress_row( + 1, + "web-1", + 5_000_000, + flags=["encrypted"], + notes=["n1"], + tiers=[("GLACIER", 2_000_000, 1), ("STANDARD", 3_000_000, 0)], + ), + ] + + def egress(self, rows=None, **overrides): + report = self.report( + resources=AWS_RESOURCES, egress=self.ROWS if rows is None else rows + ) + context = self.context(2, egress_collected=True, **overrides) + return build_sync_document(report, context) + + def test_the_egress_block_from_the_run_tables(self): + document = self.egress() + + self.assertEqual(document["assessment_type"], 3) + self.assertEqual( + document["inventory"]["egress"], + { + "collected": True, + "truncated": False, + "names": "plain", + "items": [ + { + "type_id": 1, + "name": "web-1", + "size_bytes": 5_000_000, + "flags": ["encrypted"], + "notes": ["n1"], + "tiers": [ + { + "tier": "STANDARD", + "size_bytes": 3_000_000, + "is_archive": False, + }, + { + "tier": "GLACIER", + "size_bytes": 2_000_000, + "is_archive": True, + }, + ], + }, + { + "type_id": 2, + "name": "bucket-b", + "size_bytes": None, + "flags": [], + "notes": ["size unknown"], + "tiers": [], + }, + ], + }, + ) + self.assert_valid(document) + + def test_egress_that_was_not_collected_is_not_sent(self): + report = self.report(resources=AWS_RESOURCES, egress=self.ROWS) + document = build_sync_document(report, self.context(2, egress_collected=False)) + + self.assertEqual( + document["inventory"]["egress"], + {"collected": False, "truncated": False, "names": "plain", "items": []}, + ) + self.assertNotIn("web-1", json.dumps(document)) + self.assert_valid(document) + + def test_collected_with_no_rows_is_still_collected(self): + document = self.egress(rows=[]) + egress = document["inventory"]["egress"] + self.assertTrue(egress["collected"]) + self.assertFalse(egress["truncated"]) + self.assertEqual(egress["items"], []) + + def test_too_many_items_keep_the_largest_and_set_truncated(self): + rows = [egress_row(1, f"secret-name-{n}", n + 1) for n in range(1005)] + with self.assertLogs(LOGGER, level="WARNING") as logs: + document = self.egress(rows=rows) + + egress = document["inventory"]["egress"] + self.assertTrue(egress["truncated"]) + self.assertEqual(len(egress["items"]), 1000) + self.assertEqual(egress["items"][0]["size_bytes"], 1005) + self.assertEqual(min(item["size_bytes"] for item in egress["items"]), 6) + self.assertEqual(len(logs.output), 1) + self.assertIn("5 items dropped", logs.output[0]) + self.assertNotIn("secret-name", logs.output[0]) + self.assert_valid(document) + + def test_long_strings_and_lists_are_trimmed_without_logging_names(self): + row = egress_row( + 1, + "n" * 300, + 2**60, + flags=[f"flag-{n}" + "f" * 80 for n in range(12)], + notes=[f"note-{n}" for n in range(11)], + tiers=[(f"tier-{n}" + "t" * 60, n, 0) for n in range(12)], + ) + with self.assertLogs(LOGGER, level="WARNING") as logs: + document = self.egress(rows=[row]) + + item = document["inventory"]["egress"]["items"][0] + self.assertTrue(document["inventory"]["egress"]["truncated"]) + self.assertEqual(len(item["name"]), 255) + self.assertEqual(item["size_bytes"], 2**53 - 1) + self.assertEqual(len(item["flags"]), 10) + self.assertTrue(all(len(flag) <= 64 for flag in item["flags"])) + self.assertEqual(len(item["notes"]), 10) + self.assertEqual(len(item["tiers"]), 10) + self.assertTrue(all(len(tier["tier"]) <= 50 for tier in item["tiers"])) + text = logs.output[0] + for label in ( + "names shortened", + "sizes capped", + "flags dropped", + "tiers dropped", + ): + self.assertIn(label, text) + self.assertNotIn("nnnn", text) + self.assert_valid(document) + + def test_untrimmed_egress_does_not_log_a_warning(self): + with self.assertNoLogs(LOGGER, level="WARNING"): + self.egress() + + +class HashedNamesTests(SyncDocumentTestCase): + ROWS = [egress_row(1, "web-1", 10), egress_row(2, "bucket-b", 5)] + + def document(self, details=None, **overrides): + report = self.report( + name=f"report-{len(list(self.tmp.iterdir()))}", + resources=AWS_RESOURCES, + egress=self.ROWS, + ) + context = self.context( + 2, details, egress_collected=True, egress_names="hashed", **overrides + ) + return build_sync_document(report, context) + + def names(self, document): + return { + item["type_id"]: item["name"] + for item in document["inventory"]["egress"]["items"] + } + + def test_names_are_salted_hashes_and_marked_hashed(self): + document = self.document() + + egress = document["inventory"]["egress"] + self.assertEqual(egress["names"], "hashed") + for name in self.names(document).values(): + self.assertRegex(name, r"^[0-9a-f]{64}$") + self.assertNotIn("web-1", json.dumps(document)) + self.assert_valid(document) + + def test_the_hash_is_an_hmac_with_the_stored_scope_salt(self): + document = self.document() + + salt_file = ( + self.tmp / "data" / "cloudexit" / "salts" / "aws:123456789012:eu-central-1" + ) + salt = bytes.fromhex(salt_file.read_text().strip()) + expected = hmac.new(salt, b"web-1", hashlib.sha256).hexdigest() + self.assertEqual(self.names(document)[1], expected) + + @unittest.skipUnless(os.name == "posix", "POSIX permissions") + def test_the_salt_file_is_private(self): + self.document() + salt_file = ( + self.tmp / "data" / "cloudexit" / "salts" / "aws:123456789012:eu-central-1" + ) + self.assertEqual(stat.S_IMODE(salt_file.stat().st_mode), 0o600) + + def test_hashes_are_stable_within_a_scope(self): + first = self.names(self.document()) + second = self.names(self.document()) + self.assertEqual(first, second) + + def test_hashes_differ_across_scopes(self): + first = self.names(self.document()) + other = {**AWS_DETAILS, "accountId": "210987654321"} + second = self.names(self.document(other)) + self.assertNotEqual(first[1], second[1]) + + def test_plain_names_are_unchanged_and_need_no_salt(self): + report = self.report(resources=AWS_RESOURCES, egress=self.ROWS) + document = build_sync_document(report, self.context(2, egress_collected=True)) + self.assertEqual( + {item["name"] for item in document["inventory"]["egress"]["items"]}, + {"web-1", "bucket-b"}, + ) + self.assertFalse((self.tmp / "data").exists()) + + +class ScopeKeyVectorTests(unittest.TestCase): + def test_the_published_vectors(self): + vectors = json.loads( + (DEFINITION / "vectors" / "scope_key.json").read_text(encoding="utf-8") + ) + self.assertGreater(len(vectors["cases"]), 20) + + for case in vectors["cases"]: + with self.subTest(case["name"]): + expect = case["expect"] + if "scope_key" in expect: + self.assertEqual(scope_key(case["scope"]), expect["scope_key"]) + else: + with self.assertRaises(ScopeError) as ctx: + scope_key(case["scope"]) + if "pointer" in expect["error"]: + self.assertEqual( + ctx.exception.pointer, expect["error"]["pointer"] + ) + + +class Uuid7Tests(unittest.TestCase): + def test_layout(self): + value = new_uuid7(now_ms=0x0193_9B8E_7C2A) + + self.assertRegex( + value, + r"^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + ) + self.assertTrue(value.startswith("01939b8e-7c2a-7")) + + def test_ids_sort_by_time(self): + ids = [new_uuid7(now_ms=1_700_000_000_000 + step * 1000) for step in range(50)] + self.assertEqual(ids, sorted(ids)) + self.assertEqual(len(set(ids)), 50) + + def test_the_default_id_is_a_version_7_uuid(self): + self.assertRegex(new_uuid7(), sync_document.UUID7_RE.pattern) + + def test_the_fallback_is_used_without_a_native_implementation(self): + with patch.object(sync_document.uuid, "uuid7", None, create=True): + self.assertRegex(new_uuid7(), sync_document.UUID7_RE.pattern) + + +class DecimalAmountTests(unittest.TestCase): + def test_amounts_are_exact_decimal_strings(self): + cases = { + 0.1 + 0.2: "0.3", + 1234.5600000000002: "1234.56", + 5: "5", + 0.0: "0", + -0.0000001: "0", + 1e-06: "0.000001", + 12.50: "12.5", + "12.50": "12.5", + Decimal("1E+3"): "1000", + -3.75: "-3.75", + 123456789.12: "123456789.12", + } + for value, expected in cases.items(): + with self.subTest(value=value): + self.assertEqual(decimal_amount(value), expected) + + def test_no_amount_looks_like_a_float(self): + for value in (1e-5, 1e15, 1 / 3, 99999999.99): + self.assertRegex(decimal_amount(value), r"^-?[0-9]+(\.[0-9]+)?$") + + def test_a_bad_amount_is_an_error(self): + for value in ("abc", float("nan"), float("inf"), 1e30): + with self.subTest(value=value), self.assertRaises(SyncDocumentError): + decimal_amount(value) + + +class InventoryTests(SyncDocumentTestCase): + def test_counts_above_the_item_limit_are_split_and_sum_up(self): + report = self.report(resources=[(1, "eu-central-1", 25000)]) + items = build_sync_document(report, self.context(2))["inventory"]["resources"] + + self.assertEqual([item["count"] for item in items], [10000, 10000, 5000]) + self.assertEqual({item["location"] for item in items}, {"eu-central-1"}) + + def test_a_zero_count_is_kept(self): + report = self.report(resources=[(1, "eu-central-1", 0)]) + items = build_sync_document(report, self.context(2))["inventory"]["resources"] + self.assertEqual([item["count"] for item in items], [0]) + + def test_more_than_twelve_months_keep_the_latest(self): + costs = [ + (f"2025-{month:02d}-01", float(month), "USD") for month in range(1, 13) + ] + costs += [("2026-01-01", 13.0, "USD"), ("2026-02-01", 14.0, "USD")] + report = self.report(costs=costs) + with self.assertLogs(LOGGER, level="WARNING") as logs: + items = build_sync_document(report, self.context(2))["inventory"]["costs"] + + self.assertEqual(len(items), 12) + self.assertEqual(items[0]["month"], "2025-03") + self.assertEqual(items[-1]["month"], "2026-02") + self.assertIn("2 month(s) dropped", logs.output[0]) + + def test_months_before_the_current_one_are_final(self): + costs = [("2026-08-01", 1.0, "USD"), ("2026-10-01", 2.0, "USD")] + items = build_sync_document(self.report(costs=costs), self.context(2))[ + "inventory" + ]["costs"] + self.assertEqual([item["final"] for item in items], [True, False]) + + def test_an_unreadable_run_database_is_a_sync_document_error(self): + with self.assertRaises(SyncDocumentError): + build_sync_document(str(self.tmp / "missing"), self.context(2)) + + +class NoCredentialsTests(SyncDocumentTestCase): + def test_no_secret_value_or_credential_key_is_in_any_document(self): + rows = [egress_row(1, "web-1", 10, notes=["ok"])] + for provider in (1, 2): + for collected in (False, True): + with self.subTest(provider=provider, collected=collected): + report = self.report( + name=f"r-{provider}-{collected}", + resources=[(3 if provider == 1 else 1, "x", 1)], + egress=rows, + ) + document = build_sync_document( + report, + self.context( + provider, egress_collected=collected, egress_names="hashed" + ), + ) + text = json.dumps(document) + + for secret in SECRETS: + self.assertNotIn(secret, text) + for key in ( + "accessKey", + "secretKey", + "sessionToken", + "clientSecret", + ): + self.assertNotIn(key, text) + self.assertNotIn("credential", text.lower()) + + def test_the_written_file_has_no_secrets_either(self): + report = self.report(resources=AWS_RESOURCES) + output = prepare_sync_document(report, self.context(2)) + text = output.path.read_text(encoding="utf-8") + for secret in SECRETS: + self.assertNotIn(secret, text) + + +class ValidationTests(SyncDocumentTestCase): + def valid(self): + report = self.report( + resources=AWS_RESOURCES, costs=AWS_COSTS, egress=EgressTests.ROWS + ) + return build_sync_document(report, self.context(2, egress_collected=True)) + + def mutations(self): + def set_path(path, value): + def apply(doc): + target = doc + for part in path[:-1]: + target = target[part] + target[path[-1]] = value + + return apply + + def remove(path): + def apply(doc): + target = doc + for part in path[:-1]: + target = target[part] + del target[path[-1]] + + return apply + + return { + "count over the limit": set_path( + ["inventory", "resources", 0, "count"], 10001 + ), + "type id zero": set_path(["inventory", "resources", 0, "type_id"], 0), + "float amount": set_path(["inventory", "costs", 0, "amount"], 1.5), + "scientific amount": set_path(["inventory", "costs", 0, "amount"], "1e3"), + "bad month": set_path(["inventory", "costs", 0, "month"], "2026-13"), + "bad currency": set_path(["inventory", "costs", 0, "currency"], "US"), + "missing sync id": remove(["sync", "id"]), + "sync id not v7": set_path( + ["sync", "id"], "01939b8e-7c2a-4d41-9a51-3c8f0a6e21d4" + ), + "empty regions": set_path(["scope", "aws", "regions"], []), + "bad account id": set_path(["scope", "aws", "account_id"], "123"), + "assessment type": set_path(["assessment_type"], 4), + "bad egress names": set_path(["inventory", "egress", "names"], "other"), + "egress flag too long": set_path( + ["inventory", "egress", "items", 0, "flags"], ["f" * 65] + ), + "egress size too large": set_path( + ["inventory", "egress", "items", 0, "size_bytes"], 2**53 + ), + "missing inventory": remove(["inventory"]), + "catalogue sha256": set_path( + ["sync", "collector", "catalogue", "sha256"], "ABC" + ), + } + + def test_the_valid_document_passes_both_checks(self): + self.assert_valid(self.valid()) + + def test_the_runtime_check_and_the_schema_reject_the_same_mistakes(self): + for name, mutate in self.mutations().items(): + with self.subTest(name): + document = copy.deepcopy(self.valid()) + mutate(document) + + self.assertNotEqual(schema_errors(document), [], "schema accepts it") + with self.assertRaises(SyncDocumentError): + validate_document(document) + + def test_rules_that_only_the_runtime_check_enforces(self): + # The host applies these; the JSON schema cannot express them. + for name, mutate in { + "completed before started": lambda doc: doc["sync"].update(completed_at=1), + "tfstate mode with an api scope": lambda doc: doc["sync"][ + "collector" + ].update(mode="tfstate"), + }.items(): + with self.subTest(name): + document = copy.deepcopy(self.valid()) + mutate(document) + with self.assertRaises(SyncDocumentError): + validate_document(document) + + def test_too_many_resource_items_are_rejected(self): + document = self.valid() + document["inventory"]["resources"] = [ + {"type_id": 1, "location": f"loc-{n}", "count": 1} for n in range(1001) + ] + self.assertNotEqual(schema_errors(document), []) + with self.assertRaises(SyncDocumentError): + validate_document(document) + + def test_a_document_over_the_body_limit_is_rejected(self): + document = self.valid() + document["inventory"]["egress"]["items"][0]["notes"] = ["x" * 255] * 10 + document["inventory"]["egress"]["items"] = [ + copy.deepcopy(document["inventory"]["egress"]["items"][0]) + for _ in range(500) + ] + with self.assertRaises(SyncDocumentError) as ctx: + validate_document(document) + self.assertIn("larger than", " ".join(ctx.exception.problems)) + + def test_problems_name_the_pointer(self): + document = self.valid() + document["inventory"]["resources"][0]["count"] = -1 + with self.assertRaises(SyncDocumentError) as ctx: + validate_document(document) + self.assertIn("/inventory/resources/0/count", ctx.exception.problems[0]) + + +class OutputTests(SyncDocumentTestCase): + def prepare(self, **kwargs): + report = self.report(resources=AWS_RESOURCES, costs=AWS_COSTS) + return report, prepare_sync_document(report, self.context(2), **kwargs) + + def test_the_document_is_written_next_to_the_report(self): + report, output = self.prepare() + + self.assertEqual(output.path, Path(report) / "raw_data" / "sync.json") + document = json.loads(output.path.read_text(encoding="utf-8")) + self.assertEqual(document["sync"]["id"], SYNC_ID) + self.assertEqual(schema_errors(document), []) + self.assertEqual(output.size, output.path.stat().st_size) + self.assertEqual( + output.sha256, hashlib.sha256(output.path.read_bytes()).hexdigest() + ) + self.assertIsNone(output.emitted) + + def test_emit_payload_writes_a_copy_named_after_the_sync_id(self): + out_dir = self.tmp / "out" / "nested" + report, output = self.prepare(emit_dir=str(out_dir)) + + self.assertEqual(output.emitted, out_dir / f"{SYNC_ID}.json") + self.assertEqual(output.emitted.read_bytes(), output.path.read_bytes()) + + def test_the_summary_line_has_counts_and_a_hash_but_no_names(self): + report = self.report( + resources=AWS_RESOURCES, + costs=AWS_COSTS, + egress=[egress_row(1, "very-secret-name", 10, notes=["secret note"])], + ) + with self.assertLogs(LOGGER, level="INFO") as logs: + output = prepare_sync_document( + report, self.context(2, egress_collected=True) + ) + + (line,) = logs.output + self.assertIn(SYNC_ID, line) + self.assertIn("3 resource item(s), 2 cost month(s)", line) + self.assertIn("egress collected=true truncated=false (1 item(s))", line) + self.assertIn(f"{output.size} bytes", line) + self.assertIn(output.sha256, line) + self.assertNotIn("very-secret-name", line) + self.assertNotIn("secret note", line) + self.assertEqual(output.summary, line.split(":", 2)[2]) + + def test_the_document_is_not_dumped_at_debug_level(self): + report = self.report( + resources=AWS_RESOURCES, + egress=[egress_row(1, "very-secret-name", 10)], + ) + with self.assertLogs(level="DEBUG") as logs: + prepare_sync_document(report, self.context(2, egress_collected=True)) + self.assertNotIn("very-secret-name", "\n".join(logs.output)) + + def test_nothing_is_written_when_the_document_is_invalid(self): + report = self.report(resources=AWS_RESOURCES) + details = {**AWS_DETAILS, "region": "not a region"} + with self.assertRaises(SyncDocumentError): + prepare_sync_document(report, self.context(2, details)) + self.assertFalse((Path(report) / "raw_data" / "sync.json").exists()) + + +class RunAssessmentTests(SyncDocumentTestCase): + CONFIG = { + "name": "Test Assessment", + "cloudServiceProvider": 2, + "exitStrategy": 1, + "assessmentType": 1, + "providerDetails": dict(AWS_DETAILS), + } + + def run_assessment(self, report, *, egress_ok=None, **kwargs): + raw_data = os.path.join(report, "raw_data") + patches = [ + patch("main.validate_config"), + patch("main.resolve_mode", return_value=("offline", None)), + patch("main.create_directory", return_value=(report, raw_data)), + patch("main.verify_credentials", return_value=(True, "ok")), + patch("main.test_permissions", return_value=(True, True, True, "ok")), + patch( + "main.create_resource_inventory", + return_value={"success": True, "logs": ""}, + ), + patch( + "main.create_cost_inventory", return_value={"success": True, "logs": ""} + ), + patch( + "main.perform_risk_assessment", + return_value={"success": True, "logs": ""}, + ), + patch( + "main.generate_report", return_value={"success": True, "reports": {}} + ), + patch("main.add_run_log_handler"), + patch("main.print_step"), + patch("main.console.print"), + ] + if egress_ok is not None: + patches += [ + patch( + "main.estimate_egress", + return_value={"success": egress_ok, "logs": "x", "json_path": "p"}, + ), + patch( + "main.generate_egress_html_report", + return_value={"success": True, "html_path": "h"}, + ), + patch( + "main.generate_egress_pdf_report", + return_value={"success": True, "pdf_path": "p"}, + ), + ] + for item in patches: + item.start() + self.addCleanup(item.stop) + return main.run_assessment(copy.deepcopy(self.CONFIG), "aws", **kwargs) + + def test_every_run_writes_a_sync_document(self): + report = self.report(resources=AWS_RESOURCES, costs=AWS_COSTS) + + self.run_assessment(report) + + document = json.loads( + (Path(report) / "raw_data" / "sync.json").read_text(encoding="utf-8") + ) + self.assertRegex(document["sync"]["id"], sync_document.UUID7_RE.pattern) + self.assertEqual(document["assessment_type"], 1) + self.assertFalse(document["inventory"]["egress"]["collected"]) + self.assertEqual(schema_errors(document), []) + + def test_the_sync_id_is_created_at_collection_start(self): + report = self.report(resources=AWS_RESOURCES) + with patch("main.new_uuid7", return_value=SYNC_ID) as mock_id: + self.run_assessment(report) + + mock_id.assert_called_once_with() + document = json.loads( + (Path(report) / "raw_data" / "sync.json").read_text(encoding="utf-8") + ) + self.assertEqual(document["sync"]["id"], SYNC_ID) + + def test_emit_payload_and_hashed_names_reach_the_document(self): + report = self.report(resources=AWS_RESOURCES, egress=EgressTests.ROWS) + out_dir = self.tmp / "emitted" + + self.run_assessment( + report, + egress=True, + egress_ok=True, + emit_payload=str(out_dir), + egress_names="hashed", + ) + + (emitted,) = out_dir.glob("*.json") + document = json.loads(emitted.read_text(encoding="utf-8")) + self.assertTrue(document["inventory"]["egress"]["collected"]) + self.assertEqual(document["inventory"]["egress"]["names"], "hashed") + self.assertEqual(document["assessment_type"], 3) + self.assertEqual( + emitted.read_bytes(), (Path(report) / "raw_data" / "sync.json").read_bytes() + ) + + def test_a_failed_egress_stage_is_not_collected(self): + report = self.report(resources=AWS_RESOURCES, egress=EgressTests.ROWS) + + with self.assertRaises(SystemExit) as ctx: + self.run_assessment(report, egress=True, egress_ok=False) + + self.assertEqual(ctx.exception.code, codes.EGRESS) + document = json.loads( + (Path(report) / "raw_data" / "sync.json").read_text(encoding="utf-8") + ) + self.assertFalse(document["inventory"]["egress"]["collected"]) + self.assertEqual(document["inventory"]["egress"]["items"], []) + + def test_a_problem_does_not_fail_a_plain_run(self): + report = str(self.tmp / "no-database") + os.makedirs(os.path.join(report, "raw_data")) + + self.run_assessment(report) + + self.assertFalse((Path(report) / "raw_data" / "sync.json").exists()) + statuses = [call.kwargs.get("status") for call in main.print_step.mock_calls] + self.assertIn("warning", statuses) + + def test_a_problem_fails_a_dry_run_and_an_emit_request(self): + for kwargs in ({"dry_run": True}, {"emit_payload": str(self.tmp / "out")}): + with self.subTest(kwargs=kwargs): + report = str(self.tmp / f"no-database-{len(kwargs)}{list(kwargs)[0]}") + os.makedirs(os.path.join(report, "raw_data")) + with self.assertRaises(SystemExit) as ctx: + self.run_assessment(report, **kwargs) + self.assertEqual(ctx.exception.code, codes.REPORT) + + +class ArgumentTests(unittest.TestCase): + def parse(self, *argv): + with patch("sys.argv", ["cloudexit", *argv]): + return main.parse_arguments() + + def test_defaults(self): + for provider in ("aws", "azure"): + args = self.parse(provider) + self.assertIsNone(args.emit_payload) + self.assertEqual(args.egress_names, "plain") + + def test_flags_on_aws_and_azure(self): + for provider in ("aws", "azure"): + args = self.parse( + provider, "--emit-payload", "out", "--egress-names", "hashed" + ) + self.assertEqual(args.emit_payload, "out") + self.assertEqual(args.egress_names, "hashed") + + def test_other_values_for_the_names_option_are_refused(self): + with ( + patch("sys.stderr"), + self.assertRaises(SystemExit), + ): + self.parse("aws", "--egress-names", "obfuscated") + + def test_handlers_pass_the_options_on(self): + env = { + "ESC_EXIT_STRATEGY": "1", + "ESC_ASSESSMENT_TYPE": "1", + "AWS_ACCESS_KEY_ID": "x", + } + args = Namespace( + config=None, + profile=None, + tfstate="infra.tfstate", + name=None, + non_interactive=True, + dry_run=False, + egress=False, + emit_payload="out", + egress_names="hashed", + ) + with ( + patch.dict(os.environ, env), + patch("main.run_assessment") as mock_run, + patch("main.console.print"), + ): + main.handle_aws(args) + + self.assertEqual(mock_run.call_args.kwargs["emit_payload"], "out") + self.assertEqual(mock_run.call_args.kwargs["egress_names"], "hashed") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_utils_and_main.py b/tests/test_utils_and_main.py index 3457d3a..f39f91b 100644 --- a/tests/test_utils_and_main.py +++ b/tests/test_utils_and_main.py @@ -7,6 +7,7 @@ from unittest.mock import ANY, MagicMock, patch import main +from core.sync_document import SyncOutput from utils import codes from utils.connection import resolve_mode from utils.settings import Setting, Settings @@ -346,13 +347,15 @@ def test_full_success_exits_0(self): self.assertIsNone(result) - def test_dry_run_writes_payload_generates_report_and_skips_sync(self): + def test_dry_run_writes_the_sync_document_generates_report_and_skips_sync(self): with tempfile.TemporaryDirectory() as tmp_dir: raw_data_path = os.path.join(tmp_dir, "raw_data") os.makedirs(raw_data_path, exist_ok=True) - payload_path = os.path.join(raw_data_path, "payload.json") config = VALID_CONFIG.copy() config["assessmentType"] = 2 + output = SyncOutput( + Path(raw_data_path) / "sync.json", 10, "0" * 64, None, "summary" + ) with ( patch("main.validate_config"), @@ -371,10 +374,7 @@ def test_dry_run_writes_payload_generates_report_and_skips_sync(self): "main.create_cost_inventory", return_value={"success": True, "logs": ""}, ), - patch( - "main.write_assessment_payload", - return_value=payload_path, - ) as mock_write, + patch("main.prepare_sync_document", return_value=output) as mock_write, patch( "main.perform_risk_assessment", return_value={"success": True, "logs": ""}, @@ -395,15 +395,13 @@ def test_dry_run_writes_payload_generates_report_and_skips_sync(self): ): main.run_assessment(config, "aws", dry_run=True) - mock_write.assert_called_once_with( - raw_data_path, - report_path=tmp_dir, - name=config["name"], - started_at=ANY, - exit_strategy=config["exitStrategy"], - cloud_service_provider=config["cloudServiceProvider"], - assessment_type=2, - ) + report_path, context = mock_write.call_args.args + self.assertEqual(report_path, tmp_dir) + self.assertEqual(context.assessment_type, 2) + self.assertEqual(context.cloud_service_provider, 2) + self.assertFalse(context.egress_collected) + self.assertEqual(context.egress_names, "plain") + self.assertIsNone(mock_write.call_args.kwargs["emit_dir"]) mock_risk.assert_called_once_with( exit_strategy=config["exitStrategy"], report_path=tmp_dir, @@ -428,6 +426,8 @@ def test_dry_run_flag_passed_from_handle_aws(self): non_interactive=True, egress=False, settings=None, + emit_payload=None, + egress_names="plain", ) @@ -835,6 +835,8 @@ def test_handle_azure_passes_egress_flag(self): non_interactive=True, egress=True, settings=None, + emit_payload=None, + egress_names="plain", ) def test_handle_aws_passes_egress_flag(self): @@ -853,6 +855,8 @@ def test_handle_aws_passes_egress_flag(self): non_interactive=True, egress=True, settings=None, + emit_payload=None, + egress_names="plain", ) diff --git a/tests/test_utils_sync.py b/tests/test_utils_sync.py index d843a46..e75357c 100644 --- a/tests/test_utils_sync.py +++ b/tests/test_utils_sync.py @@ -1,10 +1,7 @@ -import json -import os -import tempfile import unittest from unittest.mock import patch -from core.utils_sync import post_assessment +from core.utils_sync import _build_payload, post_assessment from tests.http_fixtures import fake_http, make_response from utils.settings import SettingsError @@ -60,55 +57,35 @@ def test_plain_http_to_a_remote_host_is_refused(self): post(host="http://platform.test") -class WriteAssessmentPayloadTests(unittest.TestCase): - def test_writes_payload_json_to_raw_data(self): - with tempfile.TemporaryDirectory() as tmp_dir: - report_path = os.path.join(tmp_dir, "report") - raw_data_path = os.path.join(report_path, "raw_data") - data_path = os.path.join(report_path, "data") - os.makedirs(raw_data_path, exist_ok=True) - os.makedirs(data_path, exist_ok=True) - - with patch("core.utils_sync.load_data") as mock_load: - mock_load.side_effect = lambda table, db_path=None: ( - [ - { - "resource_type": 10, - "location": "eu-central-1", - "count": 3, - } - ] - if table == "resource_inventory" - else [ - { - "month": "2025-01", - "cost": 42.5, - "currency": "USD", - } - ] - ) - - from core.utils_sync import write_assessment_payload - - payload_path = write_assessment_payload( - raw_data_path, - report_path=report_path, - name="Dry Run Demo", - started_at=1000, - exit_strategy=1, - cloud_service_provider=2, - assessment_type=2, - ) - - self.assertEqual(payload_path, os.path.join(raw_data_path, "payload.json")) - with open(payload_path, encoding="utf-8") as payload_file: - payload = json.load(payload_file) - - self.assertEqual(payload["type"], "local.assessment.succeeded") - self.assertEqual(payload["data"]["name"], "Dry Run Demo") - self.assertEqual(payload["data"]["assessmentType"], 2) - self.assertEqual(len(payload["data"]["resource_inventory"]), 1) - self.assertEqual(len(payload["data"]["cost_inventory"]), 1) +class BuildPayloadLoggingTests(unittest.TestCase): + def test_the_outgoing_payload_is_summarised_not_dumped(self): + rows = { + "resource_inventory": [ + {"resource_type": 10, "location": "eu-central-1", "count": 3} + ], + "cost_inventory": [{"month": "2025-01", "cost": 42.5, "currency": "USD"}], + } + with ( + patch( + "core.utils_sync.load_data", + side_effect=lambda table, db_path=None: rows[table], + ), + self.assertLogs("core.engine.sync", level="DEBUG") as logs, + ): + payload = _build_payload( + report_path="/tmp/report", + name="Secret Assessment Name", + started_at=1000, + exit_strategy=1, + cloud_service_provider=2, + assessment_type=2, + ) + + self.assertEqual(payload["data"]["name"], "Secret Assessment Name") + text = "\n".join(logs.output) + self.assertIn("1 resource row(s), 1 cost month(s)", text) + self.assertNotIn("Secret Assessment Name", text) + self.assertNotIn("eu-central-1", text) if __name__ == "__main__": From 09ff0187a025bff2c6a8f2be3d71ce460c279c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Fri, 23 Oct 2026 19:07:12 +0200 Subject: [PATCH 06/61] always write the local report and record upload links --- README.md | 4 + assets/template/index.html | 15 + core/engine.py | 143 +------- core/sync_document.py | 3 +- core/upload.py | 129 +++++++ core/utils_report.py | 12 + core/utils_sync.py | 1 + main.py | 197 +++++++---- tests/flow_fixtures.py | 69 ++++ tests/test_engine.py | 59 +--- tests/test_report_flow.py | 630 +++++++++++++++++++++++++++++++++++ tests/test_utils_and_main.py | 46 +-- 12 files changed, 1007 insertions(+), 301 deletions(-) create mode 100644 core/upload.py create mode 100644 tests/flow_fixtures.py create mode 100644 tests/test_report_flow.py diff --git a/README.md b/README.md index e6e30d4..3a9e84a 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,10 @@ cloudexit aws --config config.json --egress --egress-names hashed # send egres With `--egress-names hashed` each egress resource name is replaced by an HMAC-SHA256 with a random per-scope salt that stays on your machine (`~/.local/share/cloudexit/salts/`), so the same name hashes the same way in later runs of the same scope. +## Reports and uploads + +The local report is always the Basic report, computed on your machine, whether or not a platform is configured. Anything the platform computes stays on the platform: after an upload the local report only records the link (also stored in `raw_data/upload.json`). If an upload fails, the report is still written, with a short "Not uploaded: " banner, and the run exits with an upload code (10 or 12 for now) after everything else has finished. Problems in the collection stages take precedence over upload codes. + ## Data Landscape & Egress Estimation (alpha) Add `--egress` to any assessment to measure how much data lives in the assessed scope and estimate the one-time internet egress fee for moving it out — based on the provider's tiered list prices, with no additional permissions required. diff --git a/assets/template/index.html b/assets/template/index.html index aabffce..07cce24 100644 --- a/assets/template/index.html +++ b/assets/template/index.html @@ -50,7 +50,22 @@

+ {% if upload %} +
+ {% if upload.status == "uploaded" %} +
+ Uploaded to the platform.{% if upload.url %} + Open the platform report{% endif %} +
+ {% else %} + + {% endif %}
+ {% endif %}
diff --git a/core/engine.py b/core/engine.py index c496c8a..a0bf940 100644 --- a/core/engine.py +++ b/core/engine.py @@ -21,8 +21,6 @@ generate_pdf_report, generate_json_report, ) -from .utils_sync import post_assessment -from utils.settings import Settings, load_settings # Configure the logger logger = logging.getLogger("core.engine") @@ -312,123 +310,8 @@ def create_cost_inventory( return {"success": False, "logs": str(e)} -# Stage 5 - Online -def sync_assessment( - report_path: str, - name: str, - started_at: int, - metadata: dict[str, Any], - mode: str, - token: str | None, - settings: Settings | None = None, -) -> dict[str, Any]: - if mode != "online" or not token: - return { - "success": True, - "online": False, - "payload": None, - "logs": "offline – sync skipped.", - } - - settings = settings or load_settings() - result = post_assessment( - name=name, - started_at=started_at, - report_path=report_path, - meta=metadata, - token=token, - host=settings.host.value, - timeout=settings.timeout.value, - verify=settings.ca_bundle.value or True, - ) - - if not result.get("success"): - return { - "success": False, - "online": True, - "payload": None, - "logs": f"Assessment sync failed: {result.get('logs')}", - } - - logger.debug(result) - - try: - payload = result["payload"].get("data", {}) - server_risks = payload.get("risk_inventory", []) - - rows = [] - for entry in server_risks: - rid = entry["id"] - impacted = entry.get("impacted_resources", []) - if impacted: - for rt in impacted: - rows.append((rt, rid)) - else: - rows.append(("null", rid)) - - db_path = os.path.join(report_path, "data", "assessment.db") - with connect(db_path=db_path) as conn: - cursor = conn.cursor() - cursor.executemany( - """ - INSERT INTO risk_inventory (resource_type, risk) - VALUES (?, ?) - """, - rows, - ) - conn.commit() - - except Exception as e: - logger.error("Error saving server risks to local DB: %s", str(e), exc_info=True) - return { - "success": False, - "online": True, - "payload": None, - "logs": f"Failed to store server risks: {str(e)}", - } - - try: - scoring = payload.get("scoring_data") - if scoring: - db_path = os.path.join(report_path, "data", "assessment.db") - with connect(db_path=db_path) as conn: - cursor = conn.cursor() - cursor.execute( - """ - INSERT INTO scoring_data (exit_score, human_score, technology_score, operational_score) - VALUES (?, ?, ?, ?) - """, - ( - int(scoring["exit_score"]), - int(scoring["human_score"]), - int(scoring["technology_score"]), - int(scoring["operational_score"]), - ), - ) - conn.commit() - logger.debug("Scoring data saved to local DB.") - - except Exception as e: - logger.error("Error saving scoring data to local DB: %s", str(e), exc_info=True) - return { - "success": False, - "online": True, - "payload": None, - "logs": f"Failed to store scoring data: {str(e)}", - } - - return result - - -# Stage 5 - Offline -def perform_risk_assessment( - exit_strategy: int, report_path: str, mode: str -) -> dict[str, Any]: - - if mode != "offline": - logger.debug("Online mode – skipping local risk assessment.") - return {"success": True, "logs": "online mode – local risk skipped."} - +# Stage 6 - Local risk assessment (every run, connected or not) +def perform_risk_assessment(exit_strategy: int, report_path: str) -> dict[str, Any]: try: # Define the database path db_path = os.path.join(report_path, "data", "assessment.db") @@ -528,7 +411,7 @@ def _load_tfstate_scope(raw_data_path: str) -> dict[str, Any] | None: return None -# Stage 6 +# Stage 8 def generate_report( cloud_service_provider: int, provider_details: dict[str, Any], @@ -537,6 +420,7 @@ def generate_report( name: str, report_path: str, raw_data_path: str, + upload: dict[str, str] | None = None, ) -> dict[str, Any]: try: db_path = os.path.join(report_path, "data", "assessment.db") @@ -554,7 +438,9 @@ def generate_report( resource_inventory = load_data("resource_inventory", db_path=db_path) cost_data = load_data("cost_inventory", db_path=db_path) risk_data = load_data("risk_inventory", db_path=db_path) - scoring_data = load_data("scoring_data", db_path=db_path) + # The local report is always the Basic report: platform results are + # never rendered here, whatever the database holds. + scoring_data = None # Timestamp timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") @@ -566,18 +452,9 @@ def generate_report( "assessment_type": assessment_type, "timestamp": timestamp, } - - # Handle scoring_data - if isinstance(scoring_data, list): - if len(scoring_data) == 1: - scoring_data = scoring_data[0] - elif len(scoring_data) == 0: - scoring_data = None - else: - logger.warning( - "Unexpected multiple rows in scoring_data: %d", len(scoring_data) - ) - scoring_data = scoring_data[0] + if upload: + # Only the link (or why there is none): never platform results + metadata["upload"] = upload # Generate Outputs reports = {} diff --git a/core/sync_document.py b/core/sync_document.py index 0138bc7..5adb372 100644 --- a/core/sync_document.py +++ b/core/sync_document.py @@ -99,6 +99,7 @@ class SyncOutput: sha256: str emitted: Path | None summary: str + document: dict[str, Any] def new_uuid7(now_ms: int | None = None) -> str: @@ -779,7 +780,7 @@ def write_sync_document( summary = summary_line(document, len(data), digest) logger.info(summary) - return SyncOutput(path, len(data), digest, emitted, summary) + return SyncOutput(path, len(data), digest, emitted, summary, document) def prepare_sync_document( diff --git a/core/upload.py b/core/upload.py new file mode 100644 index 0000000..75e8f13 --- /dev/null +++ b/core/upload.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +import json +import logging +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +from utils import codes +from utils.settings import Settings, load_settings + +from .utils_sync import post_assessment + +logger = logging.getLogger("core.engine.upload") + +REASON_UNAVAILABLE = "upload_unavailable" +REASON_REJECTED = "upload_rejected" + + +@dataclass(frozen=True) +class UploadResult: + attempted: bool + success: bool + exit_code: int | None = None + reason: str | None = None + message: str = "" + sync_id: str | None = None + assessment_id: str | None = None + url: str | None = None + + @property + def failed(self) -> bool: + return self.attempted and not self.success + + +def failure(reason: str, message: str, sync_id: str | None = None) -> UploadResult: + exit_code = ( + codes.UPLOAD_REJECTED if reason == REASON_REJECTED else codes.UPLOAD_UNAVAILABLE + ) + return UploadResult(True, False, exit_code, reason, message, sync_id) + + +def _text(value: Any) -> str | None: + return value.strip() if isinstance(value, str) and value.strip() else None + + +def _link(value: Any) -> str | None: + url = _text(value) + return url if url and url.startswith(("https://", "http://")) else None + + +def upload( + document: dict[str, Any] | None, + settings: Settings | None, + *, + token: str, + name: str, + report_path: str, + started_at: int, + meta: dict[str, int], +) -> UploadResult: + # Transitional: the existing upload still builds its own request from the run + # data. The sync document is only used for its id until the v2 client + # replaces the inside of this function. + sync_id = ((document or {}).get("sync") or {}).get("id") + settings = settings or load_settings() + + try: + result = post_assessment( + name=name, + started_at=started_at, + report_path=report_path, + meta=meta, + token=token, + host=settings.host.value, + timeout=settings.timeout.value, + verify=settings.ca_bundle.value or True, + ) + except Exception as exc: + # Whatever goes wrong in the upload must not cost the local report + logger.debug("The upload raised %s", type(exc).__name__, exc_info=True) + return failure( + REASON_UNAVAILABLE, + f"unexpected upload error ({type(exc).__name__})", + sync_id, + ) + + if result.get("success"): + data = (result.get("payload") or {}).get("data") or {} + return UploadResult( + True, + True, + message="uploaded", + sync_id=sync_id, + assessment_id=_text(data.get("assessment_id")), + url=_link(data.get("url")), + ) + + status = result.get("status") + rejected = isinstance(status, int) and 400 <= status < 500 and status != 429 + return failure( + REASON_REJECTED if rejected else REASON_UNAVAILABLE, + str(result.get("logs") or "the upload failed"), + sync_id, + ) + + +def report_info(result: UploadResult | None) -> dict[str, str] | None: + if result is None or not result.attempted: + return None + info = { + "status": "uploaded" if result.success else "failed", + "reason": result.reason, + "sync_id": result.sync_id, + "assessment_id": result.assessment_id, + "url": result.url, + } + return {key: value for key, value in info.items() if value} + + +def write_upload_record(raw_data_path: str, result: UploadResult) -> Path | None: + info = report_info(result) + if info is None: + return None + info["recorded_at"] = datetime.now(timezone.utc).isoformat(timespec="seconds") + path = Path(raw_data_path) / "upload.json" + path.write_text(json.dumps(info, indent=2) + "\n", encoding="utf-8") + return path diff --git a/core/utils_report.py b/core/utils_report.py index 0777439..c10b725 100644 --- a/core/utils_report.py +++ b/core/utils_report.py @@ -1,6 +1,7 @@ # core/utils_report.py import os import json +from xml.sax.saxutils import escape import logging from typing import Any from jinja2 import Environment @@ -276,6 +277,17 @@ def _build_summary_section(metadata, styles, content_style): summary_table.setStyle(_default_table_style()) content.append(summary_table) content.append(Spacer(1, 12)) + + upload = metadata.get("upload") + if upload: + if upload.get("status") == "uploaded": + note = "Uploaded to the platform." + if upload.get("url"): + url = escape(upload["url"]) + note += f' {url}' + else: + note = f"Not uploaded: {escape(upload.get('reason', 'unknown'))}" + content.append(Paragraph(note, content_style)) return content diff --git a/core/utils_sync.py b/core/utils_sync.py index 9e1c0c5..4e6f4c8 100644 --- a/core/utils_sync.py +++ b/core/utils_sync.py @@ -122,6 +122,7 @@ def post_assessment( return { "success": ok, "payload": resp.json() if ok else None, + "status": resp.status_code, "logs": f"server responded {resp.status_code}", } except requests.RequestException as exc: diff --git a/main.py b/main.py index 21cef17..8ced425 100644 --- a/main.py +++ b/main.py @@ -7,6 +7,7 @@ import os import getpass import traceback +from dataclasses import replace from rich.console import Console from rich.logging import RichHandler from rich.markup import escape @@ -22,7 +23,6 @@ create_resource_inventory, create_cost_inventory, perform_risk_assessment, - sync_assessment, generate_report, ) from core.utils_egress import estimate_egress @@ -30,6 +30,13 @@ generate_egress_html_report, generate_egress_pdf_report, ) +from core.upload import ( + REASON_UNAVAILABLE, + failure, + report_info, + upload, + write_upload_record, +) from core.sync_document import ( RunContext, SyncDocumentError, @@ -84,6 +91,9 @@ # Initialize the console object console = Console() +# The local report is always the Basic report, whatever depth was requested +LOCAL_REPORT_ASSESSMENT_TYPE = 1 + # Third-party loggers kept quiet unless the user opts into deep verbosity (-vv) _THIRD_PARTY_NOISY = ("botocore", "boto3", "azure") @@ -565,13 +575,28 @@ def run_assessment( # Detect ExitCloud Integration mode, jwt = resolve_mode(settings) + configured = mode == "online" or bool( + settings and settings.host.value and settings.api_key.value + ) + signin_failure = None if dry_run: print_step("Dry run mode – no remote sync.", status="ok") elif mode == "online": print_step("ExitCloud integration configured.", status="ok") + elif configured: + # A configured platform that refuses the sign-in is a failed upload, + # not a reason to drop to Basic: the document keeps the requested depth. + signin_failure = failure( + REASON_UNAVAILABLE, "could not sign in to the platform" + ) + print_step( + "ExitCloud integration configured, but sign-in failed.", + status="warning", + logs="The report is still written locally; the upload counts as failed.", + ) else: print_step("ExitCloud integration not configured.", status="warning") - # Overwrite assessment type to basic + # Without a platform only the Basic assessment is possible if config["assessmentType"] != 1: print_step( "Forcing Basic Assessment due to offline mode.", status="warning" @@ -747,73 +772,7 @@ def run_assessment( or f"Exit Assessment {datetime.now().strftime('%Y%m%d_%H%M%S')}" ) - # Stage 5 – Online / Offline Risk Assessment - if dry_run or mode == "offline": - console.print("Stage #5 – Offline Risk Assessment", style="bold") - - with console.status("Performing risk assessment...", spinner="dots"): - risk_result = perform_risk_assessment( - exit_strategy=config["exitStrategy"], - report_path=report_path, - mode="offline", - ) - - status = "ok" if risk_result["success"] else "error" - print_step( - "Performing risk assessment...", status=status, logs=risk_result["logs"] - ) - if not risk_result["success"]: - sys.exit(codes.RISK_ASSESSMENT) - - elif mode == "online": - console.print("Stage #5 – Online Risk Assessment", style="bold") - - sync_result = sync_assessment( - name=name, - started_at=started_at, - report_path=report_path, - metadata={ - "cloud_service_provider": config["cloudServiceProvider"], - "exit_strategy": config["exitStrategy"], - "assessment_type": config["assessmentType"], - }, - mode=mode, - token=jwt, - settings=settings, - ) - - status = "ok" if sync_result["success"] else "error" - print_step("Sync assessment...", status=status, logs=sync_result["logs"]) - if not sync_result["success"]: - sys.exit(codes.RISK_ASSESSMENT) - - console.print("-------------------------------------------") - - # Stage 6: Generate Report - console.print("Stage #6 - Generate Report", style="bold") - - # Use a spinner to indicate progress - with console.status("Generating report...", spinner="dots"): - report_status = generate_report( - config["cloudServiceProvider"], - config["providerDetails"], - config["exitStrategy"], - config["assessmentType"], - name, - report_path, - raw_data_path, - ) - - # Handle the result - if report_status["success"]: - print_step("Generating report...", status="ok") - else: - print_step( - "Generating report...", status="error", logs=report_status["logs"] - ) - sys.exit(codes.REPORT) - - # Stage 7: Egress Estimation (opt-in via --egress). + # Stage 5: Egress Estimation (opt-in via --egress). # A failure here must not discard the completed assessment. egress_failed = False egress_collected = False @@ -821,8 +780,7 @@ def run_assessment( egress_html_path = None egress_pdf_path = None if egress: - console.print("-------------------------------------------") - console.print("Stage #7 – Egress Estimation", style="bold") + console.print("Stage #5 – Egress Estimation", style="bold") with console.status("Estimating egress data...", spinner="dots"): egress_result = estimate_egress( @@ -878,11 +836,33 @@ def run_assessment( logs=egress_result["logs"], ) egress_failed = True + if egress: + console.print("-------------------------------------------") + + # Stage 6: Local risk assessment. Every run computes the Basic results + # locally, connected or not; platform results are never used here. + console.print("Stage #6 – Risk Assessment", style="bold") + + with console.status("Performing risk assessment...", spinner="dots"): + risk_result = perform_risk_assessment( + exit_strategy=config["exitStrategy"], + report_path=report_path, + ) + + status = "ok" if risk_result["success"] else "error" + print_step( + "Performing risk assessment...", status=status, logs=risk_result["logs"] + ) + if not risk_result["success"]: + sys.exit(codes.RISK_ASSESSMENT) + + console.print("-------------------------------------------") # Sync document: the run's result in the API v2 shape. Without # --dry-run or --emit-payload a problem here does not discard the # finished report. sync_output = None + sync_failed = False try: sync_output = prepare_sync_document( report_path, @@ -907,8 +887,65 @@ def run_assessment( ) for problem in getattr(e, "problems", [])[:10]: console.print(f" ↳ {problem}", style="dim") - if wanted: - sys.exit(codes.REPORT) + sync_failed = wanted + + # Stage 7: Upload, only when a platform is configured. A failure never + # discards the local report: it is written next, with a banner. + upload_result = signin_failure + if signin_failure or (mode == "online" and not dry_run): + console.print("Stage #7 – Upload", style="bold") + if upload_result is None: + with console.status("Uploading to the platform...", spinner="dots"): + upload_result = upload( + sync_output.document if sync_output else None, + settings, + token=jwt, + name=name, + report_path=report_path, + started_at=started_at, + meta={ + "cloud_service_provider": config["cloudServiceProvider"], + "exit_strategy": config["exitStrategy"], + "assessment_type": config["assessmentType"], + }, + ) + if upload_result.sync_id is None: + upload_result = replace(upload_result, sync_id=sync_id) + if upload_result.success: + print_step("Uploading to the platform...", status="ok") + else: + print_step( + "Uploading to the platform...", + status="error", + logs=f"{upload_result.reason}: {upload_result.message}", + ) + write_upload_record(raw_data_path, upload_result) + console.print("-------------------------------------------") + + # Stage 8: Generate the local report (always Basic, connected or not) + console.print("Stage #8 - Generate Report", style="bold") + + # Use a spinner to indicate progress + with console.status("Generating report...", spinner="dots"): + report_status = generate_report( + config["cloudServiceProvider"], + config["providerDetails"], + config["exitStrategy"], + LOCAL_REPORT_ASSESSMENT_TYPE, + name, + report_path, + raw_data_path, + upload=report_info(upload_result), + ) + + # Handle the result + if report_status["success"]: + print_step("Generating report...", status="ok") + else: + print_step( + "Generating report...", status="error", logs=report_status["logs"] + ) + sys.exit(codes.REPORT) # Output the report path after the separator if not (dry_run or non_interactive): @@ -925,6 +962,15 @@ def run_assessment( console.print("Contact: request_report@escapecloud.io") console.print("-------------------------------------------") console.print("Outputs:", style="bold") + if upload_result and upload_result.success: + console.print( + ( + f"Platform: {upload_result.url}" + if upload_result.url + else "Uploaded to the platform." + ), + style="cyan", + ) if sync_output: console.print(f"Sync document: {sync_output.path}", style="cyan") if sync_output.emitted: @@ -944,8 +990,13 @@ def run_assessment( console.print(f"Egress PDF: {egress_pdf_path}", style="cyan") console.print("-------------------------------------------") + # Collection problems win over upload codes + if sync_failed: + sys.exit(codes.REPORT) if egress_failed: sys.exit(codes.EGRESS) + if upload_result and upload_result.failed: + sys.exit(upload_result.exit_code) except Exception as e: console.print(f"[red]Unexpected error: {e}[/red]") diff --git a/tests/flow_fixtures.py b/tests/flow_fixtures.py new file mode 100644 index 0000000..90944f7 --- /dev/null +++ b/tests/flow_fixtures.py @@ -0,0 +1,69 @@ +import sqlite3 +from pathlib import Path + +from tests.report_fixtures import stage_report_assets +from tests.sync_fixtures import build_run + +FLOW_TABLES = """ +DROP TABLE risk; +CREATE TABLE risk ( + id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL, + severity TEXT NOT NULL +); +CREATE TABLE risk_inventory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, resource_type INTEGER, risk INTEGER +); +CREATE TABLE scoring_data ( + id INTEGER PRIMARY KEY, exit_score INTEGER, human_score INTEGER, + technology_score INTEGER, operational_score INTEGER +); +CREATE TABLE alternative ( + id INTEGER PRIMARY KEY, resource_type INTEGER NOT NULL, + alternative_technology INTEGER NOT NULL, strategy_type INTEGER NOT NULL +); +CREATE TABLE alternativetechnology ( + id INTEGER PRIMARY KEY, product_name TEXT NOT NULL, product_description TEXT, + product_url TEXT, open_source TEXT NOT NULL, support_plan TEXT NOT NULL, + status TEXT NOT NULL, organization_id INTEGER NULL +); +CREATE TABLE alternativetechnologyorganization ( + id INTEGER PRIMARY KEY, name TEXT NOT NULL, url TEXT NULL, + stability_tier INTEGER NULL, country_code TEXT NULL, + years_in_business INTEGER NULL +); +""" + +RISKS = [ + (1, "Few alternatives", "There are few alternatives.", "medium"), + (2, "No alternatives", "There are no alternatives.", "high"), + (3, "Few supported alternatives", "Few alternatives have support.", "low"), + (4, "No supported alternatives", "No alternative has support.", "medium"), + (5, "Many resources", "A large estate.", "low"), + (6, "Very many resources", "A very large estate.", "medium"), + (7, "Many resource types", "Many kinds of resources.", "low"), + (8, "Very many resource types", "Very many kinds of resources.", "medium"), +] + +# With no alternatives, every inventoried resource type carries risks 2 and 4. +LOCAL_RISKS = [(1, 2), (1, 4), (4, 2), (4, 4)] +SERVER_SCORES = (91, 92, 93, 94) + + +def build_flow_run(report_path, *, platform_scores=False, **kwargs): + report = build_run(report_path, **kwargs) + db_path = Path(report) / "data" / "assessment.db" + conn = sqlite3.connect(db_path) + conn.executescript(FLOW_TABLES) + conn.executemany("INSERT INTO risk VALUES (?, ?, ?, ?)", RISKS) + conn.execute("UPDATE resourcetype SET icon = '/icons/misc/no_image.png'") + if platform_scores: + # What an earlier version stored after an upload: it must never be rendered + conn.execute( + "INSERT INTO scoring_data (exit_score, human_score, technology_score, " + "operational_score) VALUES (?, ?, ?, ?)", + SERVER_SCORES, + ) + conn.commit() + conn.close() + stage_report_assets(report) + return report diff --git a/tests/test_engine.py b/tests/test_engine.py index b142439..0668bd3 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -4,8 +4,7 @@ from pathlib import Path from unittest.mock import MagicMock, patch -from core.engine import _load_tfstate_scope, sync_assessment, test_permissions -from utils.settings import Settings +from core.engine import _load_tfstate_scope, test_permissions class TestPermissionsAwsHybridMode(unittest.TestCase): @@ -109,62 +108,6 @@ def client_side_effect(service_name, **kwargs): self.assertIn("ce:GetCostAndUsage failed", logs) -class SyncAssessmentContractTests(unittest.TestCase): - def test_offline_returns_success_without_calling_the_api(self): - with patch("core.engine.post_assessment") as mock_post: - result = sync_assessment( - report_path="/tmp", - name="n", - started_at=0, - metadata={}, - mode="offline", - token=None, - ) - self.assertTrue(result["success"]) - self.assertFalse(result["online"]) - mock_post.assert_not_called() - - def test_server_failure_returns_dict_not_raises(self): - with patch( - "core.engine.post_assessment", - return_value={"success": False, "payload": None, "logs": "401"}, - ): - result = sync_assessment( - report_path="/tmp", - name="n", - started_at=0, - metadata={}, - mode="online", - token="tok", - settings=Settings(), - ) - self.assertFalse(result["success"]) - self.assertIn("401", result["logs"]) - - def test_local_db_failure_returns_dict_not_raises(self): - good = { - "success": True, - "payload": { - "data": {"risk_inventory": [{"id": 1, "impacted_resources": []}]} - }, - } - with ( - patch("core.engine.post_assessment", return_value=good), - patch("core.engine.connect", side_effect=Exception("db down")), - ): - result = sync_assessment( - report_path="/tmp", - name="n", - started_at=0, - metadata={}, - mode="online", - token="tok", - settings=Settings(), - ) - self.assertFalse(result["success"]) - self.assertIn("store server risks", result["logs"]) - - class LoadTfstateScopeTests(unittest.TestCase): def _write_manifest(self, directory, payload): path = Path(directory) / "tfstate_manifest.json" diff --git a/tests/test_report_flow.py b/tests/test_report_flow.py new file mode 100644 index 0000000..0bb64bb --- /dev/null +++ b/tests/test_report_flow.py @@ -0,0 +1,630 @@ +import copy +import inspect +import json +import os +import sqlite3 +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +import main +from core.engine import generate_report, perform_risk_assessment +from core.sync_document import SyncDocumentError +from core.upload import ( + REASON_REJECTED, + REASON_UNAVAILABLE, + UploadResult, + failure, + report_info, + upload, + write_upload_record, +) +from tests.flow_fixtures import LOCAL_RISKS, SERVER_SCORES, build_flow_run +from tests.sync_fixtures import SYNC_ID, egress_row +from utils import codes +from utils.settings import Setting, Settings + +AWS_RESOURCES = [(1, "eu-central-1", 8), (4, "eu-central-1", 2)] +COSTS = [("2026-09-01", 12.5, "USD")] +URL = "https://platform.test/assessments/a-1" +CONFIG = { + "name": "Flow Test", + "cloudServiceProvider": 2, + "exitStrategy": 1, + "assessmentType": 2, + "providerDetails": { + "accessKey": "AKIAFAKEACCESSKEY1234", + "secretKey": "fake-secret-key-AAAABBBBCCCC", + "region": "eu-central-1", + "accountId": "123456789012", + }, +} +CONFIGURED = Settings( + host=Setting("https://platform.test", "env CLOUDEXIT_HOST"), + api_key=Setting("k" * 24, "env CLOUDEXIT_API_KEY"), +) +SERVER_PAYLOAD = { + "data": { + "assessment_id": "a-1", + "url": URL, + "risk_inventory": [{"id": 8, "impacted_resources": [1]}], + "scoring_data": { + "exit_score": SERVER_SCORES[0], + "human_score": SERVER_SCORES[1], + "technology_score": SERVER_SCORES[2], + "operational_score": SERVER_SCORES[3], + }, + } +} + + +def post_ok(payload=None): + return {"success": True, "payload": payload or SERVER_PAYLOAD, "status": 201} + + +def post_failed(status=None, logs="failed"): + return {"success": False, "payload": None, "status": status, "logs": logs} + + +class FlowTestCase(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.tmp = Path(tmp.name) + patcher = patch.dict(os.environ, {"XDG_DATA_HOME": str(self.tmp / "data")}) + patcher.start() + self.addCleanup(patcher.stop) + self.counter = 0 + + def report(self, **kwargs): + self.counter += 1 + kwargs.setdefault("resources", AWS_RESOURCES) + kwargs.setdefault("costs", COSTS) + return build_flow_run(self.tmp / f"report-{self.counter}", **kwargs) + + def rows(self, report, table): + conn = sqlite3.connect(Path(report) / "data" / "assessment.db") + try: + return conn.execute(f"SELECT * FROM {table}").fetchall() + finally: + conn.close() + + def raw(self, report, name): + return Path(report) / "raw_data" / name + + def json_report(self, report): + return json.loads(self.raw(report, "assessment_result.json").read_text()) + + def html(self, report): + return (Path(report) / "index.html").read_text(encoding="utf-8") + + +class LocalResultsTests(FlowTestCase): + def test_the_risk_assessment_has_no_online_mode_any_more(self): + parameters = list(inspect.signature(perform_risk_assessment).parameters) + self.assertEqual(parameters, ["exit_strategy", "report_path"]) + + def test_the_risk_assessment_computes_the_local_risks(self): + report = self.report() + + result = perform_risk_assessment(1, report) + + self.assertTrue(result["success"]) + self.assertEqual( + [(row[1], row[2]) for row in self.rows(report, "risk_inventory")], + LOCAL_RISKS, + ) + + def test_platform_scores_left_in_the_database_are_never_rendered(self): + report = self.report(platform_scores=True) + perform_risk_assessment(1, report) + + for assessment_type in (1, 2): + with self.subTest(assessment_type=assessment_type): + result = generate_report( + 2, + CONFIG["providerDetails"], + 1, + assessment_type, + "Flow", + report, + str(Path(report) / "raw_data"), + ) + self.assertTrue(result["success"], result.get("logs")) + + self.assertNotIn("scoring_data", self.json_report(report)["data"]) + html = self.html(report) + self.assertNotIn(f"exitScoreValue = {SERVER_SCORES[0]}", html) + ( + self.assertNotIn("Scoring #1 - Exit Readiness", html) + if (assessment_type == 1) + else None + ) + + def test_an_offline_report_has_no_upload_trace(self): + report = self.report() + perform_risk_assessment(1, report) + + result = generate_report( + 2, + CONFIG["providerDetails"], + 1, + 1, + "Flow", + report, + str(Path(report) / "raw_data"), + ) + + self.assertTrue(result["success"]) + document = self.json_report(report) + self.assertNotIn("upload", document["meta"]) + self.assertEqual( + sorted(document["data"]), + [ + "alternative_technologies", + "cost_inventory", + "resource_inventory", + "risk_inventory", + ], + ) + self.assertEqual(len(document["data"]["resource_inventory"]), 2) + self.assertEqual( + sorted(int(entry["id"]) for entry in document["data"]["risk_inventory"]), + [2, 4], + ) + self.assertNotIn("Not uploaded", self.html(report)) + self.assertTrue(Path(result["reports"]["PDF"]).is_file()) + + def generate(self, report, upload_info): + return generate_report( + 2, + CONFIG["providerDetails"], + 1, + 1, + "Flow", + report, + str(Path(report) / "raw_data"), + upload=upload_info, + ) + + def test_a_failed_upload_puts_a_banner_in_every_report(self): + report = self.report() + perform_risk_assessment(1, report) + info = {"status": "failed", "reason": REASON_UNAVAILABLE, "sync_id": SYNC_ID} + + result = self.generate(report, info) + + self.assertTrue(result["success"], result.get("logs")) + self.assertIn("Not uploaded: upload_unavailable", self.html(report)) + self.assertEqual(self.json_report(report)["meta"]["upload"], info) + self.assertTrue(Path(result["reports"]["PDF"]).is_file()) + + def test_a_successful_upload_records_only_the_link(self): + report = self.report() + perform_risk_assessment(1, report) + info = { + "status": "uploaded", + "sync_id": SYNC_ID, + "assessment_id": "a-1", + "url": URL, + } + + result = self.generate(report, info) + + self.assertTrue(result["success"], result.get("logs")) + html = self.html(report) + self.assertIn(f'href="{URL}"', html) + self.assertIn("Open the platform report", html) + self.assertEqual(self.json_report(report)["meta"]["upload"], info) + + def test_banner_text_is_escaped(self): + report = self.report() + perform_risk_assessment(1, report) + info = {"status": "failed", "reason": ""} + + self.generate(report, info) + + self.assertNotIn("", self.html(report)) + + +class UploadInterfaceTests(FlowTestCase): + def call(self, post, document=None, settings=CONFIGURED): + with patch("core.upload.post_assessment", return_value=post) as mock_post: + result = upload( + document if document is not None else {"sync": {"id": SYNC_ID}}, + settings, + token="jwt", + name="Flow", + report_path="/tmp/report", + started_at=1, + meta={"assessment_type": 1}, + ) + return result, mock_post + + def test_success_carries_the_link_and_nothing_else(self): + result, mock_post = self.call(post_ok()) + + self.assertTrue(result.success) + self.assertFalse(result.failed) + self.assertEqual( + (result.sync_id, result.assessment_id, result.url), (SYNC_ID, "a-1", URL) + ) + self.assertEqual(mock_post.call_args.kwargs["host"], "https://platform.test") + self.assertEqual(mock_post.call_args.kwargs["timeout"], 10.0) + self.assertFalse(hasattr(result, "risks")) + self.assertFalse(hasattr(result, "scores")) + + def test_an_unsafe_link_is_dropped(self): + payload = {"data": {"assessment_id": "a-1", "url": "javascript:alert(1)"}} + result, _ = self.call(post_ok(payload)) + self.assertIsNone(result.url) + + def test_failures_map_to_the_upload_codes(self): + cases = { + 503: (REASON_UNAVAILABLE, codes.UPLOAD_UNAVAILABLE), + 500: (REASON_UNAVAILABLE, codes.UPLOAD_UNAVAILABLE), + 429: (REASON_UNAVAILABLE, codes.UPLOAD_UNAVAILABLE), + None: (REASON_UNAVAILABLE, codes.UPLOAD_UNAVAILABLE), + 400: (REASON_REJECTED, codes.UPLOAD_REJECTED), + 401: (REASON_REJECTED, codes.UPLOAD_REJECTED), + 422: (REASON_REJECTED, codes.UPLOAD_REJECTED), + } + for status, (reason, exit_code) in cases.items(): + with self.subTest(status=status): + result, _ = self.call(post_failed(status, "server said no")) + self.assertTrue(result.failed) + self.assertEqual((result.reason, result.exit_code), (reason, exit_code)) + self.assertEqual(result.message, "server said no") + self.assertEqual(result.sync_id, SYNC_ID) + + def test_an_unexpected_error_is_a_failed_upload_not_a_crash(self): + with patch("core.upload.post_assessment", side_effect=ValueError("bad json")): + result = upload( + None, + CONFIGURED, + token="jwt", + name="Flow", + report_path="/tmp/report", + started_at=1, + meta={}, + ) + self.assertTrue(result.failed) + self.assertEqual(result.reason, REASON_UNAVAILABLE) + self.assertIn("ValueError", result.message) + self.assertNotIn("bad json", result.message) + + def test_report_info_and_the_record_file(self): + success = UploadResult( + True, True, sync_id=SYNC_ID, assessment_id="a-1", url=URL + ) + failed = failure(REASON_REJECTED, "nope", SYNC_ID) + + self.assertEqual( + report_info(success), + { + "status": "uploaded", + "sync_id": SYNC_ID, + "assessment_id": "a-1", + "url": URL, + }, + ) + self.assertEqual( + report_info(failed), + {"status": "failed", "reason": REASON_REJECTED, "sync_id": SYNC_ID}, + ) + self.assertIsNone(report_info(None)) + self.assertIsNone(report_info(UploadResult(False, False))) + + path = write_upload_record(str(self.tmp), success) + record = json.loads(path.read_text()) + self.assertEqual(record["url"], URL) + self.assertIn("recorded_at", record) + self.assertIsNone( + write_upload_record(str(self.tmp), UploadResult(False, False)) + ) + self.assertNotIn("nope", json.dumps(report_info(failed))) + + +class RunFlowTests(FlowTestCase): + def run_assessment( + self, + report, + *, + mode=("offline", None), + settings=None, + post=None, + egress_ok=None, + assessment_type=2, + **kwargs, + ): + self.steps = [] + raw_data = os.path.join(report, "raw_data") + patches = [ + patch("main.validate_config"), + patch("main.resolve_mode", return_value=mode), + patch("main.create_directory", return_value=(report, raw_data)), + patch("main.verify_credentials", return_value=(True, "ok")), + patch("main.test_permissions", return_value=(True, True, True, "ok")), + patch( + "main.create_resource_inventory", + return_value={"success": True, "logs": ""}, + ), + patch( + "main.create_cost_inventory", return_value={"success": True, "logs": ""} + ), + patch("main.add_run_log_handler"), + patch( + "main.print_step", + side_effect=lambda *a, **k: self.steps.append((a, k)), + ), + patch("main.console.print"), + ] + if post is not None: + patches.append(patch("core.upload.post_assessment", return_value=post)) + if egress_ok is not None: + patches += [ + patch( + "main.estimate_egress", + return_value={"success": egress_ok, "logs": "x", "json_path": "p"}, + ), + patch( + "main.generate_egress_html_report", + return_value={"success": True, "html_path": "h"}, + ), + patch( + "main.generate_egress_pdf_report", + return_value={"success": True, "pdf_path": "p"}, + ), + ] + for item in patches: + item.start() + self.addCleanup(item.stop) + + config = copy.deepcopy(CONFIG) + config["assessmentType"] = assessment_type + return main.run_assessment(config, "aws", settings=settings, **kwargs) + + def sync(self, report): + return json.loads(self.raw(report, "sync.json").read_text()) + + def statuses(self): + return [kwargs.get("status") for _, kwargs in self.steps] + + def assert_local_report(self, report): + for path in ( + Path(report) / "index.html", + Path(report) / "report.pdf", + self.raw(report, "assessment_result.json"), + self.raw(report, "sync.json"), + ): + self.assertTrue(path.is_file(), f"{path.name} is missing") + + def test_an_offline_run_forces_basic_and_never_uploads(self): + report = self.report() + with patch("main.upload") as mock_upload: + self.run_assessment(report) + + mock_upload.assert_not_called() + self.assert_local_report(report) + self.assertEqual(self.sync(report)["assessment_type"], 1) + self.assertFalse(self.raw(report, "upload.json").exists()) + self.assertNotIn("upload", self.json_report(report)["meta"]) + self.assertEqual(self.json_report(report)["meta"]["assessment_type"], 1) + self.assertEqual( + [(row[1], row[2]) for row in self.rows(report, "risk_inventory")], + LOCAL_RISKS, + ) + messages = [args[0] for args, _ in self.steps] + self.assertIn("Forcing Basic Assessment due to offline mode.", messages) + + def test_the_local_results_are_computed_when_connected_too(self): + report = self.report() + self.run_assessment( + report, mode=("online", "jwt"), settings=CONFIGURED, post=post_ok() + ) + self.assertEqual( + [(row[1], row[2]) for row in self.rows(report, "risk_inventory")], + LOCAL_RISKS, + ) + + def test_a_successful_upload_stores_the_link_and_no_platform_results(self): + report = self.report() + + self.run_assessment( + report, mode=("online", "jwt"), settings=CONFIGURED, post=post_ok() + ) + + self.assert_local_report(report) + record = json.loads(self.raw(report, "upload.json").read_text()) + self.assertEqual( + {key: record[key] for key in ("status", "sync_id", "assessment_id", "url")}, + { + "status": "uploaded", + "sync_id": self.sync(report)["sync"]["id"], + "assessment_id": "a-1", + "url": URL, + }, + ) + # No server risks or scores reach the database or the local report + self.assertEqual(self.rows(report, "scoring_data"), []) + self.assertEqual( + [(row[1], row[2]) for row in self.rows(report, "risk_inventory")], + LOCAL_RISKS, + ) + document = self.json_report(report) + self.assertNotIn("scoring_data", document["data"]) + self.assertEqual(document["meta"]["upload"]["url"], URL) + self.assertIn(URL, self.html(report)) + self.assertNotIn(str(SERVER_SCORES[0]), json.dumps(document["data"])) + + def test_connected_runs_keep_the_requested_depth_in_the_document(self): + report = self.report() + + self.run_assessment( + report, mode=("online", "jwt"), settings=CONFIGURED, post=post_ok() + ) + + self.assertEqual(self.sync(report)["assessment_type"], 2) + # The local report is always the Basic report + self.assertEqual(self.json_report(report)["meta"]["assessment_type"], 1) + messages = [args[0] for args, _ in self.steps] + self.assertNotIn("Forcing Basic Assessment due to offline mode.", messages) + + def test_a_failed_upload_still_writes_the_report_and_exits_with_an_upload_code( + self, + ): + for status, exit_code, reason in ( + (503, codes.UPLOAD_UNAVAILABLE, REASON_UNAVAILABLE), + (None, codes.UPLOAD_UNAVAILABLE, REASON_UNAVAILABLE), + (422, codes.UPLOAD_REJECTED, REASON_REJECTED), + ): + with self.subTest(status=status): + report = self.report() + with self.assertRaises(SystemExit) as ctx: + self.run_assessment( + report, + mode=("online", "jwt"), + settings=CONFIGURED, + post=post_failed(status, "server responded"), + ) + + self.assertEqual(ctx.exception.code, exit_code) + self.assert_local_report(report) + self.assertIn(f"Not uploaded: {reason}", self.html(report)) + self.assertEqual( + self.json_report(report)["meta"]["upload"]["reason"], reason + ) + record = json.loads(self.raw(report, "upload.json").read_text()) + self.assertEqual( + (record["status"], record["reason"]), ("failed", reason) + ) + self.assertEqual(self.rows(report, "scoring_data"), []) + + def test_an_unreadable_platform_answer_does_not_cost_the_report(self): + report = self.report() + with ( + patch("core.upload.post_assessment", side_effect=ValueError("not json")), + self.assertRaises(SystemExit) as ctx, + ): + self.run_assessment(report, mode=("online", "jwt"), settings=CONFIGURED) + + self.assertEqual(ctx.exception.code, codes.UPLOAD_UNAVAILABLE) + self.assert_local_report(report) + + def test_a_failed_sign_in_is_a_failed_upload_not_an_offline_run(self): + report = self.report() + with ( + patch("main.upload") as mock_upload, + self.assertRaises(SystemExit) as ctx, + ): + self.run_assessment(report, mode=("offline", None), settings=CONFIGURED) + + mock_upload.assert_not_called() + self.assertEqual(ctx.exception.code, codes.UPLOAD_UNAVAILABLE) + self.assert_local_report(report) + self.assertIn("Not uploaded: upload_unavailable", self.html(report)) + # A configured platform keeps the requested depth: nothing forces Basic + self.assertEqual(self.sync(report)["assessment_type"], 2) + messages = [args[0] for args, _ in self.steps] + self.assertNotIn("Forcing Basic Assessment due to offline mode.", messages) + + def test_basic_is_forced_only_without_platform_configuration(self): + half = Settings(host=Setting("https://platform.test", "env X")) + for settings in (None, Settings(), half): + with self.subTest(settings=settings): + report = self.report() + self.run_assessment(report, mode=("offline", None), settings=settings) + self.assertEqual(self.sync(report)["assessment_type"], 1) + + def test_a_dry_run_never_uploads_and_never_forces_basic(self): + report = self.report() + with patch("main.upload") as mock_upload: + self.run_assessment( + report, + mode=("online", "jwt"), + settings=CONFIGURED, + dry_run=True, + ) + + mock_upload.assert_not_called() + self.assert_local_report(report) + self.assertEqual(self.sync(report)["assessment_type"], 2) + self.assertFalse(self.raw(report, "upload.json").exists()) + + def test_collection_problems_win_over_upload_codes(self): + report = self.report(egress=[egress_row(1, "web-1", 10)]) + with self.assertRaises(SystemExit) as ctx: + self.run_assessment( + report, + mode=("online", "jwt"), + settings=CONFIGURED, + post=post_failed(503), + egress=True, + egress_ok=False, + ) + + self.assertEqual(ctx.exception.code, codes.EGRESS) + self.assert_local_report(report) + self.assertIn("Not uploaded", self.html(report)) + + def test_a_sync_document_problem_wins_over_an_upload_code(self): + report = self.report() + with ( + patch("main.prepare_sync_document", side_effect=SyncDocumentError("bad")), + self.assertRaises(SystemExit) as ctx, + ): + self.run_assessment( + report, + mode=("online", "jwt"), + settings=CONFIGURED, + post=post_failed(503), + emit_payload=str(self.tmp / "out"), + ) + + self.assertEqual(ctx.exception.code, codes.REPORT) + self.assertTrue((Path(report) / "index.html").is_file()) + + def test_the_upload_runs_after_the_collection_and_before_the_report(self): + report = self.report(egress=[egress_row(1, "web-1", 10)]) + order = [] + original_upload = main.upload + + def traced_upload(*args, **kwargs): + order.append("upload") + return original_upload(*args, **kwargs) + + def traced_report(*args, **kwargs): + order.append("report") + return generate_report(*args, **kwargs) + + def traced_risk(*args, **kwargs): + order.append("risk") + return perform_risk_assessment(*args, **kwargs) + + with ( + patch("main.upload", side_effect=traced_upload), + patch("main.generate_report", side_effect=traced_report), + patch("main.perform_risk_assessment", side_effect=traced_risk), + patch( + "main.estimate_egress", + side_effect=lambda *a, **k: order.append("egress") + or {"success": True, "json_path": "p"}, + ), + patch("main.generate_egress_html_report", return_value={"success": True}), + patch("main.generate_egress_pdf_report", return_value={"success": True}), + ): + self.run_assessment( + report, + mode=("online", "jwt"), + settings=CONFIGURED, + post=post_ok(), + egress=True, + ) + + self.assertEqual(order, ["egress", "risk", "upload", "report"]) + self.assertTrue(self.sync(report)["inventory"]["egress"]["collected"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_utils_and_main.py b/tests/test_utils_and_main.py index f39f91b..b89ce14 100644 --- a/tests/test_utils_and_main.py +++ b/tests/test_utils_and_main.py @@ -293,32 +293,6 @@ def test_unexpected_error_writes_traceback_file(self): self.assertIn("Traceback", body) self.assertIn("boom-unexpected", body) - def test_online_sync_failure_exits_risk_assessment(self): - with self.assertRaises(SystemExit) as ctx: - with ( - patch("main.validate_config"), - patch("main.resolve_mode", return_value=("online", "jwt-token")), - patch("main.create_directory", return_value=("/tmp/r", "/tmp/r/raw")), - patch("main.verify_credentials", return_value=(True, "ok")), - patch("main.test_permissions", return_value=(True, True, True, "ok")), - patch( - "main.create_resource_inventory", - return_value={"success": True, "logs": ""}, - ), - patch( - "main.create_cost_inventory", - return_value={"success": True, "logs": ""}, - ), - patch( - "main.sync_assessment", - return_value={"success": False, "logs": "server responded 401"}, - ), - patch("main.print_step"), - patch("main.console.print"), - ): - main.run_assessment(VALID_CONFIG.copy(), "aws") - self.assertEqual(ctx.exception.code, codes.RISK_ASSESSMENT) - def test_full_success_exits_0(self): with ( patch("main.validate_config"), @@ -354,7 +328,7 @@ def test_dry_run_writes_the_sync_document_generates_report_and_skips_sync(self): config = VALID_CONFIG.copy() config["assessmentType"] = 2 output = SyncOutput( - Path(raw_data_path) / "sync.json", 10, "0" * 64, None, "summary" + Path(raw_data_path) / "sync.json", 10, "0" * 64, None, "summary", {} ) with ( @@ -389,7 +363,7 @@ def test_dry_run_writes_the_sync_document_generates_report_and_skips_sync(self): }, }, ) as mock_report, - patch("main.sync_assessment") as mock_sync, + patch("main.upload") as mock_upload, patch("main.print_step"), patch("main.console.print"), ): @@ -403,12 +377,10 @@ def test_dry_run_writes_the_sync_document_generates_report_and_skips_sync(self): self.assertEqual(context.egress_names, "plain") self.assertIsNone(mock_write.call_args.kwargs["emit_dir"]) mock_risk.assert_called_once_with( - exit_strategy=config["exitStrategy"], - report_path=tmp_dir, - mode="offline", + exit_strategy=config["exitStrategy"], report_path=tmp_dir ) mock_report.assert_called_once() - mock_sync.assert_not_called() + mock_upload.assert_not_called() def test_dry_run_flag_passed_from_handle_aws(self): with ( @@ -662,7 +634,7 @@ def test_egress_module_not_invoked_without_flag(self): mock_render.assert_not_called() mock_pdf.assert_not_called() - def test_egress_invoked_after_report_generation(self): + def test_egress_runs_in_the_collection_stages_before_the_report(self): manager = MagicMock() patches = _base_patches() for p in patches: @@ -708,9 +680,6 @@ def test_egress_invoked_after_report_generation(self): config["providerDetails"], ) call_names = [name for name, _, _ in manager.mock_calls] - self.assertLess( - call_names.index("generate_report"), call_names.index("estimate_egress") - ) self.assertLess( call_names.index("estimate_egress"), call_names.index("generate_egress_html_report"), @@ -719,6 +688,11 @@ def test_egress_invoked_after_report_generation(self): call_names.index("generate_egress_html_report"), call_names.index("generate_egress_pdf_report"), ) + # The estimate is part of the collection stages, before the local report + self.assertLess( + call_names.index("generate_egress_pdf_report"), + call_names.index("generate_report"), + ) def test_egress_failure_still_prints_outputs_and_exits_egress(self): patches = _base_patches() From 97110c645cbe85aca0350322b0fc5d3f2ff6d6de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Fri, 23 Oct 2026 20:17:19 +0200 Subject: [PATCH 07/61] replace the pdf with the basic executive summary --- README.md | 4 + assets/fonts/IBMPlexMono-Regular.ttf | Bin 0 -> 173052 bytes assets/fonts/IBMPlexMono-SemiBold.ttf | Bin 0 -> 174608 bytes assets/fonts/IBMPlexSans-Italic.ttf | Bin 0 -> 207920 bytes assets/fonts/IBMPlexSans-Medium.ttf | Bin 0 -> 202460 bytes assets/fonts/IBMPlexSans-Regular.ttf | Bin 0 -> 200500 bytes assets/fonts/IBMPlexSans-SemiBold.ttf | Bin 0 -> 202632 bytes assets/fonts/IBMPlexSans-SemiBoldItalic.ttf | Bin 0 -> 211072 bytes assets/fonts/OFL.txt | 93 +++ assets/template/index.html | 529 ++++--------- core/engine.py | 52 +- core/report/__init__.py | 0 core/report/base.py | 223 ++++++ core/report/brief_summary.py | 378 +++++++++ core/report/context.py | 381 +++++++++ core/report/cover.py | 200 +++++ core/report/evidence.py | 147 ++++ core/report/executive_summary.py | 70 ++ core/report/flow.py | 115 +++ core/report/fonts.py | 64 ++ core/report/helpers.py | 42 + core/report/html_sections.py | 121 +++ core/report/palette.py | 103 +++ core/report/provenance.py | 80 ++ core/report/sections.py | 812 ++++++++++++++++++++ core/report/styles.py | 80 ++ core/report/table_style.py | 167 ++++ core/report/wording.py | 447 +++++++++++ core/utils_report.py | 470 +---------- main.py | 1 + pyproject.toml | 1 + requirements-dev.txt | 1 + tests/flow_fixtures.py | 4 +- tests/report_fixtures.py | 53 ++ tests/report_terms.py | 148 ++++ tests/test_basic_report.py | 782 +++++++++++++++++++ tests/test_pdf_sections.py | 194 +---- tests/test_report_pipeline.py | 44 +- 38 files changed, 4713 insertions(+), 1093 deletions(-) create mode 100644 assets/fonts/IBMPlexMono-Regular.ttf create mode 100644 assets/fonts/IBMPlexMono-SemiBold.ttf create mode 100644 assets/fonts/IBMPlexSans-Italic.ttf create mode 100644 assets/fonts/IBMPlexSans-Medium.ttf create mode 100644 assets/fonts/IBMPlexSans-Regular.ttf create mode 100644 assets/fonts/IBMPlexSans-SemiBold.ttf create mode 100644 assets/fonts/IBMPlexSans-SemiBoldItalic.ttf create mode 100755 assets/fonts/OFL.txt create mode 100644 core/report/__init__.py create mode 100644 core/report/base.py create mode 100644 core/report/brief_summary.py create mode 100644 core/report/context.py create mode 100644 core/report/cover.py create mode 100644 core/report/evidence.py create mode 100644 core/report/executive_summary.py create mode 100644 core/report/flow.py create mode 100644 core/report/fonts.py create mode 100644 core/report/helpers.py create mode 100644 core/report/html_sections.py create mode 100644 core/report/palette.py create mode 100644 core/report/provenance.py create mode 100644 core/report/sections.py create mode 100644 core/report/styles.py create mode 100644 core/report/table_style.py create mode 100644 core/report/wording.py create mode 100644 tests/report_terms.py create mode 100644 tests/test_basic_report.py diff --git a/README.md b/README.md index 3a9e84a..27f0c48 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,10 @@ cloudexit aws --config config.json --egress --egress-names hashed # send egres With `--egress-names hashed` each egress resource name is replaced by an HMAC-SHA256 with a random per-scope salt that stays on your machine (`~/.local/share/cloudexit/salts/`), so the same name hashes the same way in later runs of the same scope. +## The report + +Every run writes the same three files: `report.pdf`, `index.html` and `raw_data/assessment_result.json`. The PDF is the Basic Executive Summary, set in IBM Plex (SIL Open Font License, in `assets/fonts/`). It opens with a cover and a document information page that record where the data came from: the cloudexit version, the run ID, the data catalogue version and hash, and either a link to the platform's reports or the statement that the data was self-collected. The same facts are in the PDF metadata and in a `provenance` object in the JSON report. The last page says what the full assessment on a platform adds. + ## Reports and uploads The local report is always the Basic report, computed on your machine, whether or not a platform is configured. Anything the platform computes stays on the platform: after an upload the local report only records the link (also stored in `raw_data/upload.json`). If an upload fails, the report is still written, with a short "Not uploaded: " banner, and the run exits with an upload code (10 or 12 for now) after everything else has finished. Problems in the collection stages take precedence over upload codes. diff --git a/assets/fonts/IBMPlexMono-Regular.ttf b/assets/fonts/IBMPlexMono-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4254c37f20ba57e829a3315fdc91cda4e63589f5 GIT binary patch literal 173052 zcmd4434E2s)jxh_o|}!VH~V^Xm+X6TZ#J@$ge+tsKmq|-!j3G0ii(PgmMWrFRH~?` zsI*c=OciYmE>uuxMMb5STB^~~7OQP-Uu;p~=J!3%l6#Y&wf(>E=kpi8cb;dSSm}0Cr|su(hDyq`Ms4y zAv>F~=IgsBA_#U=&<&{fjM6@3yDvcx=K~2FCO+PsL>b>(qE5D!sGjWEp=tA<L`KB-#8sk zgTuE2948%^4b%b_pb97kx`AY%2WSL1-Ygwua5-(JWfH()*8vRx`}21WYq+_1jsy5x z3g7_Z0W(kla5w{;_XOSF4wuWww6R~Q{#*gq0GEaH=lE8D%f;c@<+AWI=Uoll?*)#> zARWg0$9T^H*gc{__`=--8~{E9rUDlM{GHta9UO>RywWrH9c&Ij%j z>|ggCk&keU?-D-4D_jVk+r98j_1c0kOMpaxW$yXFeSh}FTb zz2TcsfN$Pl8IT8b09*!^8_a9m&snxj1Xch%M%@JPn997y-?%Ru{e^W2ZZGCP?xTjw zaT)PIEzk_`H%@;(!1?n#^AGbj=U)uu=-_nu8>h+j#oxUVh<672tp<#=j4(_S$4>^B z=X!vR0L!HafZIj_+~(fx!E)IL;CKcfGEcC+!f}{ZZfmBK%VvPn;JPvDnEMPrGyObf z^E1n~@c`EWgTpg8EZ4CCPWvLjTV`@uj5?3jpSg|xW&A7YwkRV18j*7%ca=jTu}X&dYeW;yDVqAGjBI7`O+x z3)rE*?}WP@cmVjW;dUYHkANqE#{f>_Y5m=BpTPTf4Ht&646cvSF8hU!@;AcpHzN+W zF_)F$@B+uR12+QS18xJ>0^A?21+LU#@R7G`q{-=VU*tGO`uuz~!0Fxqa5@~9>AG;-iU9w|LJ!%PtI3|k-0+D)x(q|4vVm(~b>mZx_D zJAl6ccLUD=L%=@Z7B74s&;2_72kuM2uYfCHpMjle=rZ6HUc6BT=)g1~Dw9vG|beZWD({D_ln7;H$4XLpNTOur}mQqWVrODE2S!h{hx!Cf3%UzZSEnixL ztx?u^tHo-!rdczs1y-kZytUfeV4Y`OW4+RPv-M%?PV0}Yd#q1epS2EHU$nk#i?Bu8 z?6x#po^6V4uI*mimv&!!s6E^sWskR8?Ai7bd$YaQzBt8}`cUdG z(<9Q`vdsnb&*NW9_t4O}p&^u+OKqh>8c!4G0$N2^(+;)NJH%7s4e^zXRZG23z9x?= zr1%b(I!(z{OHHOkQ>v-Jw7|67bctz;=`GU<(_ej3Q0gE{xFy9>VyU#$S|(W*SXNrr zSTju5lyR1L8?zQ%hDs`uAnr(~iC$>J@ zbG8AmQkzlgIImJKN}*KJOC5qz|N3j8ms$*cfpUj{M?L}`Q)^+;P^#*xKW?}C6|_5g zAO9yV5Hm%Wn1ZK?qSn0){bhsudhMI{??2Xizj;6Z{w97KY|{J&$Dh0zZ@q&RgVw>? zlPw<~!oQynCJe?6#=<{xFk;X=7&;j8;e#J;|8V6;g&*Fa#rs$gee@6f`{<(&KKSWi z$6zDT2Wx;kKKR}TYd@IzLEQ%vK3wy`p7%dGaoq{zb?i|hpErDdYIXrwbIU^jYJ>y^FM8ej53oPw&Wg?aimhXO<62 z=d)qtS0CibINyIhkZC^4hQBCuB`v0V=y|%4x@Zxtr)y{{-A@Z>I?baSsFLQ>LVA>T z&`$7uH(g8LqwDBinq%t58i_CYkv}Z?<0ufk5=voYrf|rLO>{loXWBvQ=vnHgy)-~K z(`M6Dx|MFH%W1di8M@8XNq5i#O1U!L)Fr3UEwsxt7d5$su22>z^OS{jm3Tt@NbC|n z6+b~gdqM0MZ;8X=b#X+T5+8|^;t%3gDP(|Dq^}H>e)J$lC8x}l1+q{!%N%hzy+=ps zT{=O>=zVevDa>M=2o@nCPh^N}ks|^`C%AT+m@ejt8T62tBfbxLc7ynyxKCUwwu>j} zxcD)>E%wlF#7lz2&xImh5hn4n5aNLF5eJ2@cw0n>Ux+}+fl%>|h!npRLE^U}NxUx- z#qUHiq@qQ9C{o1-B1L>Aio~Bqf%sTticdwM_@hV{e-TdcxhNHX2e*}re~9topQ2j) zkEjs;5;fwCs1;v}Nn%KJh_j+qOp#Jdl|f>*G>I-5C@zo*Vuef;E2TwTB$LD{nJiXI zySPN!#KqDo*2rvejZ7C;$~3V~mWx|tiP$9b#0FU=Hp^mhlPncC%ZcJ1*&^ zSWXd-%1-f!xJ-tLUO8UeDjuVE#P3Cx^by@MUtBM1#hr43xJ%ZHAIdthRrJws#fuov zL&RK}BG$@GakZ((G{rOvlD@+<&9ua{*tFEtYnpGm5dC4HX(gopBGWRA0BcN_OSc?S z^5vI`P_odgaxqebE0Icp608I&HYHo$CVwDrms{i=@-Dem{!rd6?~&W&eHdphkhA4n z*((>wg>tc6Dle2P)ClUK^C<$8IoyiQ&(zbC&hZKNOezA0$M9W_(vGUi71!HZp{6MkF50zATQhqI;S2El6uMVk0RREg7~QhX_z#aE(5oD*%rE!wdNH(5Hwr7}ZYC9}kO*(C0j4dQOuD7ML| zVyAeVekFF(uf@;kWAQwl6a(}Jv5)?UHO=3Pr|EZMFMTMUr9tr=eI%Zt55$`yOdJ*A z;$0CXj)`dTo`@B{7IEUZh!?*SG2#=EFa9KQrN8KrFx8xk%Zn{7|_^xm)?M@|dzs zxmUSgxlh@yJfb|HJSwNtc6yASR326yQg$d0nyxloL%vwGn${eS;n zGS@&4n?Q9`PYpDY8mVcNthQtAm*sFcc} z)s3f$?;wb@IViH*>nXF_pWIskvmfHKpVaRAdSqMq|9%S(`NaOb* ziN`?}vn0-dEM`gE2`M}ia(D(L@pX{Gkilr1JJiu*GyQ~afi%AjqsI?ST^L0k5iiJK zdWs&PAJLDowl@v)iRrQ%y6hf|FRYQc#T;m->u5JUNk4;CqYIGGwDwa#XU|WC_`%G6G4%a@YDjt>yr9gRo7+z!$7-E; zVMCwj#f!{=N0tpwN{+R$Pf2Z@+T)mM-E7_5vS_ol(YkE@qCQiq>f*!V%`@|@eKf7- zLi~31*!pT`CJa9=o;hg7 zt8JK>V6#~#_U-TN>D%9sV4FD;p?rra1;y7~7^~Cl2bz7e@ZDcaWm->PE%q18+`O67 zoYv#8^=;U^Ibk!(p}+Uj{!t!+Mtjui9;gYVftr}uFE(@{F*pue0(&@Y4jbs4*?_zQ za$2YLOa!I2nYmo&+0Zrw1rdh60VwX0A6~{o1GqdJTp9uX@Xvl3`bsT$KNFlAuKGX( z2%1f;=mc3{Z!M6dDd=7J-PGL~XI2P(76y^8JM&)R&H-LO1NL)66FmTrnP>t@Y{&-)bzwgjpH5|gL z;o#PSZY`eXKBT|z(A~Fmca84$fgTS3x$fq(i*)iKKE$O1v|4wc(%qxFTf;8mU8lQi zP%;kxmhP@bgYx%Yh#oB+xOflccci~scOOz+R7VUn+6J`yEL7p;bP438(M(($#}n*e5Hwkup=Z$o&;jy|2u>+{)1i({dpL?2S?6^ zux|h`t1umh-o2m57y7zCm?50 zUuO$8Ce>o^5Mz)iY8z3sKY;Hs>xg2*0ECGHW5k<)KB9z50N)eKh>}u(V?>r_;3!e@ zN&w%iYp{QZfES4DUD%6+IF2*eBLsRCO5$SWP;Gw_^ogedbcQ5Mq48bp^{ z22qztl#4uakxt$gqI}Sle~PFu2slkt1UNq@a`h1vTLGkBg0xE!u530@`9|O@QAHTG z9wCn^ zAYMJvtM38!6E$Q2cyGXa!!lq4&<}i0G|?Zh0^5ih;Wi#8YC`(WCSVcJ2Y~LDlSGq{ zb}Rf_%YePWL8A5?qK?mqCMN=*vlD((K-1J-qG^cNh4*fxIURYb4UxfkW=Jxnw|13;MhNPj-R z4-zd{1Z*c-Xa%-_Ww#M6#`h(lZ|N?gWt)gD1eUKMS^>Y6kpS|)r~x=jv?}Y&;iM|i_Mgook z_;wSXZ;Ay9fM%c8^YX%Ft=3#2zML8-G*?t zA>8c^0Q_!;-|g_*vKu%EAnqL~(_JXzT^T?v(bh7c6Icdp0JZ^rz+qqzI79S9g#RJJ z-;FSLBh1|hb9X;*6gWkgssks1vqak>fgAudYy%D3K*KiBux%&Nz1xAkz!9SRDuFIw zC9n~|xBKwzKK_QZ?mJ6#en}8hvXnF`V zJp`KYN;{4NpA$Xo4_JXRpc7a|^a!3G!Sf^hyaCt-^Z|!~LEsG0qhY{jL_0y(PSCXz zbnOIPKLQ;;$^jaH*}z)hR$wR5kB;_OG9{fM)_2Ur7a0(KDnd_Axg z*bN*6P7uAqI8AgQ2uJ}cfi3{_9srF8wgaH?0BAgL5;#kA5cwWNzOQZoknXGfz)|27 z(Q5?60tG-b&CCc!v=05aJy|yu*lh81W7t13n}Ag$YOmTtFMJ z2v`qn1$F}miC#yT*AeD*ggL@MxFZO61mTV#+!2I3f^bJp0%wWdhy-!~yuX3>H}U=^ z-rsBhW)r=&3)l}F2RH1jGUbKr_$_tOK?H zyMX<`ao}^J4lKHe^Pvft4SO)c z4;}$d0G|tKtlAA zfX*r8UJ^a$ z1dvuU(rQ6DCLxd3HNXZEZ5v6nZzIuh9L7P=(CGqplbC|^ry`%JCrM1(L81#ZbR8kl z4NTuoVg};RI7OmoD~XwFNz94_koE;g>w>c+W&?8&cJ4tE^C|(P(~CUjuOqRb060Tp z;cOC%knUpSxdgN>0X<7oNGyv5`bk`fJeHRM+exh0MPeo1S8f6NfTO_YBvyq11wbdT z2G|1h0Y`z)Nn8{L6abyT8ej{F)$0gTJQ9~QU?N$IN#sgQAVKRDVIk~=bu!+P*l==IEByK|3O}j}z4;8oUAORgx`~Y!p zN7y^?e&<0FTboJz5b56i8HsHLB<`IJ;Qc-mfIRO*nET;>Kk~W%IEn2!zy{zLi3ciy zoxoWV4|V~_>mkH@Xbpg{51l5l1GMiz+=t;lyp_ZwvA{MGkHUTQ42hk`N&E;j{U`(I z0X7j9Jb-m1pi_y*ZY8lR1$cqPPkKo_4!_51f%QN?fIOZ6eNU_eb^#|z?6v~4fo;Gs z5>F!UC))s|{p3Ls(5J*6yzkiod`{vi7l3b19VgM32q2%noxm9qKSj8oZUIh_cpA@7 zBTeX5;u)m>%wZCH5q|F)V35SK`2H-?g#IL+TM3*X@v}-`JBfbe*}o1z9?v%e{Uio* z0DRjQ1gr->Bk{sA;1r1$ktXyP0ewZhgg7rF&dV>5*q;dC+kTYe=V8EX0JQ!bVPEM4 z@O&T=K>i2zlQ>uaYywV@c(o0{_g4}2HKhC6VG@V%{SeYUjC2ogB=HNx{lx|V;eWXX zfcttSfO>>pAdY-a;tizxW*C6?H~UGv1^VA=0M-E~NgRy@K=0A@z;0lW#M=n}Hp0BU z7I=ZgJN`f?upKxEd`9A3gn>>V-kl9>1Q7RKB}2et+))wgaH&ACUmk`^OFv|Fe$7Y0!Im7w|cWFV+Ca z_n!#!&n^J@{VM}N*fS=epTw720fhMqbbW>QukrosN}v;11R%|?LF?Cu^YvcfFmM7u z`e%{G*;oMiokiMbkCHeS2_TJgh<|Py33m|C3@iiI17}GLt%oTwk)*)cE3t#5bOHNG zVl0-*L6W8d0PjAXBz-f0QzZR%k@WWm*1%YdxB+W{y})Ubqt{vY5l$>>PJIWv^oN+ipa zAX3PW#t)E*u;^}xauNH#@UN%(=S-j?EY=CGcD?(=DPBNlw4<5OLK>_ygbv95t)``o;}rl zvavKY&Jo$2krtJa$@Nh=bXt6g`UnG&*AHM}9g8_C{btmMGF%_QT>N0K`Uqf;051=P zJru7B3S*Bj%_9H>O;n4l;K{63i-3ZNcQ_-q!rHfU|88}={ z1wL9igHg^{wVV-7v$KSoHWKAccSJc#i%O~lH+l%x*CQ=*UZ+<%kD+*tdQs&`_Yu9M z6>9N@3(Msc)P-`I(4Kax_q3+bbH*{L!>2qLR{>5Fx6|W>h9yE2o#X&W)}OnNZ*;O^b*~D@$=TM3%?Q4@z%bR8_U8DXp&9 zo{yI-KaEHfH) z+JZ*4qK`$=CCqzRz6F5>@9B-^=~*UjR+ASqMzTjFcO(|?TwD{1qNq>YbV0fw%W23a z)EyU_rN8-TL18Ictk|ha&nQQfGs*#}X`8mEzrVEN$+zBmtKU7TL9B0B?y76p-r(K{ z{!`E{IoQQB32V}g*p>9!0KB?sx>~86dIM5tw`sVA;+VoXFOqHSVe|6PrJ$MRhYd7a zLGu9GTcJ2L-R%4c3S>|=W&jg>u%47nYXCWCfT|ECTMbi{k53Q5<0(L1j0i{ZAm%)e zrNuzh3Nv)QS{K=PYEqx#@YI1PbXgWDJc^VS=V2Vt9qz(0M&(#URAdMwYFc`T5)~O; zA&R2?Txr?DS9>W@3}K{>HnQ-B8?ITElMx+ToKaAmSUf3rd`_Z$-r7VTE;7r7>5YcIgr{uy@t|&1n`Qt#IGe1< z8qz|o=JZmFa28=;Mx7gyk_Rqj^sx2i<*RBgNY0OM$h{MSJ-^kJnCNQF=Rxe4*BEAA zaaFptG$HA}>Ak59^Q)@nH>CDr>jleF>7;N38CVTExry)u9v(@9g*S}6wK za<6C~=G$nlh-j~h7{kc=K%nw1^HRK8a0gQ5KHN_Zjc*+W-#WmzxH15I+lVj@HB6(% zw~gT2MuTq~Rldzv`PPASi`7bw##5#G#2QdLo_MG^KhvTopNC&DaE|2C2yX@!xg!}i z+wtt&2=i!mz2QckOTQfE+hgbB)}Y$R;-rzBTGP=gUh*)h``0?>qAnl@7vSeC&hJwNv%qT!vq;IwuN%E4f z+~>r!(u(84u2Z~GHZ+`j+*PMkdk9u};zAHx4W0<*nlyig;Yei07EFUoOYr)k#i1@HZB;V))#>*sbq_)GU| zB52A!@MO}m7|j;ttj3TM9iutu!D?Uj^?E3oWD3g3&du>rLR@(@ z*^?4dtQnIM9M()}sJFGwYn0S?=!}?yy`6)8tb4`Q?(Qv}3gFF~!#_ zNM%Y^(0EeIN>sR4X^k>z6IqX78Y7pasq1`FiTgOu*`v{p5m2?jHP}I-k2tVOV5no8 zz_;qokt)})?wp`xIiQ(2yomvmD7F^BKnY5-wRn-fd z(k6CxP3r1Ye?Sz^G0QN={D!%-XO5}Yqo=0F^mbiOQAcC7c;1uF^UX0O+EklkhCs#| zbId63Ii}eWVHES<&M{5;WZHdFC}Sp>Y%?hPzqJ{t1wY@s4UDDs7wnEY|GbTD24j@3 ze{VBz31xU5Z5Ry%6cdcB(scg$%r}iFo@V{F`3y5-Ftnlph`V5;yZEwj7&)I|X;2}e zRO<&u#`b&e`IArB%!$qOCw4#VUdJ<<8>%LDc24-P0sWijnL*$`C+i&!sD{N}@@|YW z?A@JN*LA49Hw-SzDkg--aPHA2%vk*3#}gSpRhi*QtV7j_RmDwHgG+cinJ@AbkHtU% z;XLh378cAjd_~ce%Q8z+d#WqjL*uRCQxa-ij^who+{UG;u7HGem%VC2XuQolC9&G& zxFV=DFF!piJ1;UJEIuyZbg7O~!i z4a8pM*SnBycy^!WouNtSH^R zAwH`8(@#IGbBRd}QSmoDr_DF9?Gv`Pe}bQ4=|a}7M(TxQBv=%WB~e~#mQJuo2Z-em zvD#?E1c$1v1EXu2xG?4G5V}`z4|~wa4ODZ!Rm?fDK;tQ8;&B3Y)72(wW z;bbBHS(+*WdU|>SDke@|Hr2mtnKZfU#7m5GXGNbIHhoOe3G&|r*1KW=8eTX>c+oAn zv7}dXjv>>#o>VeDdj9}4AF1|BPS{^nnpj=)p%AsD+4Mb*t2QECuUn{o`N9z@s`l^e z?Wi_Yw$Fa?mEIW@rt&V~>s~c|`l@s8FRz?B^-6BvVvJi3mF^zq%aL^JM2zVs9uK`B zsw&ND8K(hTjY=5TE-R!se-xr*i(+S~+W4j7-OeEjh_+-UwXGX}bn1+DQ@b>syCvEd z71;0t@zwEz2SFE1x7Y*Ed^e4i+>v}YrrEu9Q-6@*F&}YF`I7>ISlq&(geTNhv4B<| z1-n?cXm`IP?sY#SX0(e-8`|Bsv95}?Qu%NIGB1!N3aVXKv;9#jpJClYAHF@3MJLVE zQ9UioDxVLT)Vdjm6#}Wwj9In>pq6au4nGH&PB;{&_^9HGiW%>%e$V~SfCX}T@i3H% z5^D<=sUS<6{`@#qkb-bLvrw(peo^|Q+@@QuvqhAyU*M_dFSC|Z6;!k(Iwxkh zzxc=ay1MZXZYi#;EZ!mq7EG;a4plVi|oP80kO7R>$E9Trde}su>sA&_M*n1fv27th|eiaw8dm5hhKZ)B^O>Bo}3wD zODf5UM`fYuaK;<ugn-~pfEx(raM!X!A#_%}=JasOC!xSz(!s)3eITTa9DtI-e+8Um=De@6D# z|C$D#LFoCKXNi9IcG2h_P;+%3(8^v0o*4(8$)wBKI*0>EXa%F}y8QB#eatYL$(Qq{8)8TQikM2KZZ5f9zFPO@RHB3cob9YK%daZ5iIIrrO(K9b~Okyx)g&8SSF5aXI=1k%Bpjk$79SYoo8mjQqrVKNr(_O_uUQ;!RBW^vLfafKCnak zs-+e-Oip>Rl}oqR{drfH2m`9z2jqbJ2o^(N{DWN`3)U)MqKAy8cZ}rp^k-g$V`DeZ zc9=VH`Uhf-$2k_1>e?WLHyio8_UsXTd-l{zSAG4tBlUP)5n~Yl~i7X)0*eL&S@@6 znAU2JN=*oxFi|{-?f^P?{Hg;Ts?A3|y6i>An6}c#FArg3+REGJ<2&3|te^@WVqxh) zQ>cnnghK=`>grNvzq$B}vkS!Z%TXQft!vM?-*>On_-GwyivevuRLoO$9I^JI?OXaP zFUasXfQ-;9*L8_j6#sUuycjiQB+?9{i#bgk()LPoO!4&6c*;9MEb^q@+gu-vQnH%H zL$89xem3v<;GGv}R0X5h5hxsNB~i6DaO5HXyYBLDUwO6f)yvw&+~2wH6-$4IYhex6 zMEiL9(tPev|BHP|QzHrV1%f_5v`&OWBaJ)Dqke5y{1uRG=k}uZ_QHh~T@uSNE96)N zq>*ap&?FffOATqJ&k{V9=G|kMXQ2mzdy~~R8f)j^O$6#p>E`LVw))pkL8t)#Vfw#?v$zR_KK=w^{NYiDd%V#X!EvhUk&RQn>YR%${Qxs_>KGl1c8mo@hD3r@8$K+V;#C?$@sz67IHp=v!!uyEF8TUJIlVS981???Wfi5x z?M1Oh)h}3n&#erKI4)vxq|>pG0;e_&t8j0etJ{d)bUsMuAcMU{$vdz_@4_n|V=56$a>anpy~hv$2;UTbTEBp?P|` zo}T;oYEKJwFY$N3BeI&*Hp0axf^xz47Vv#G+s6F}hljo7hQ4;?q1c<_p_zk6csVHv zPdqnD(m9CN7=n+o86;X2ZCJvBPYQY>bi!eKMVe-=;3Wuce*-Up8pevOx$DxaGB2oU z>l}|=G!wJJuU*k~siPuk>V&4Y@=2}E#_aIxf+m&}Ig1)=oaI5m0V&QZTknE0r!%df zvNFFS7(PYS_W2ia8RO7NYS2D@RK??`*(xvHSdZi-P7I`Th29#huxS`2sE6=D=O`#QM8X4N5_b+ zF|{dO-Xrl<;@f}&3NF57&g_&nt9V^o96Y*KKbi;Q)ZZ^ zcg;S2%eHN|h(%)hO}E{4lluwKZ3f*fB@QfS5(q@{E?} z$ZX=UnTKgI3;FlnMct+ui{}0vS3&H0?xBaCbN9Ia%5_)(S~66ggY7^WH5VDv-#j(M zOR+3dj9$k*78?=Kk+g2hYEnUArZ9{LGQ;gJZgJlz-gB=N546g(mezCcBSY`8yp-ng zss%1TKx7`xc|zUK=UVaLwR&+rme)o<&!|3t=?&}>YC>5M%7y9*<-Z^4OGmD?jvek6*O8Fm0KUuk|A)lL=*UF%2c;ZQb}R0IfGv#pGKdjnw|$qgYo*q-8lKbJ zR2iRy#Li@yys;xgTjkOAJVi#w;5G<-i6AW<_vV~O2SOd- z3LKlc-o@1+rTas3{g|4NH>(Nxew+Rf(YE{rSpLFWo`sdLL-rx zYX7)k>|E$ux`Uy4u!TUIsk8R44a^=x^@zEVUpn?fIitD{`}Gw*|GoYe>D*cTq@kxz z03C4{Z+k8QDIW6Z21}+itqFJ9PcxX=LDSfIorRN}Sj^xJRCyAb1WydPw6Jc9kd~IF z^0JB1HNk1l`t00}@iy$-O-w4uigVP=EXivs%?zsvv7{H-CQWW{pC;~{;PfjiE6gZ} z%Cttto_R$D`+sk#OF^mF>ow1QC!RK`5$b`qnr|6SZsthk3LZyE3q{5rzHK4sn%xm&m(n z*)sQ|BG`2EB=IMAjSLt`@s%x&sYs>Q5%dFPwp0b>Y z#Tlsbq_hHSb9+ZiQE5z59Bv=F*!_hgGpTH{)7f2RAFkz&iT)Ei>~Z7726R`{buG#i zi!$-9uca(*G|jZ1c;BXHKl-=K$=I%n>oGL8!a&ASOzT5teDUnxWR}w`4u;e!b%rQ@ zF}xK(EAOZ^>fF+vDqDWbtn8KLGaK?ked4B0$ZIQ0wqw=0dGh3DASiX>qUw&DFP@Q* z+0i|}N@kZ^$~p_VyiFZV{!Ja~50_Kmb{}l?M>+j@4~TlvnpfL>3nyu#B%83A5s`?- zh*))U!m6%nsZR>C`^{GIj{CH{t54SkvGwVdr(r1xjktUfm8437_E z)Ef^cV*-bIbEOv_3~7;m*=Qt`lAh51PowWpgkD_g|T09qG*O+m>eld_w;%>EJG zm7VkEbyjvq_?x?$vtj&hs;i8ytW$r)^zkJjCF9i}ZWHhxeq!}H8pC^W!?iO;tJj-L zojLGEKAx8{>&B6f+B%ke*8B|K3j$AC5Q^wRK73DZ8gHu>9aP zFSh<@yN(j!YB~|-#Hz`frE{dZHp~ZKV5tE|80^m*gbhifT8UUSiqQ7^v&JNHMwQjY zb>Q#*GOE&{KEeE08Dx~qaGTV^TNB)WjfobI=r1BXCK?pU;j%}88c3dK-Ez+pdT;q^ z1dYO`r;e0*krtCVg0QMzv_X)^s;k0SgLiM75%6Zjc1}=_7Oi zXl_xqp2w1088KC530d~ofT|!zVNLqNrAf|)^wna9yK&0ui1e(S*dSN2Il&Q;5r>_o z^QNWMmKDV|U_vgS3s1)#St*eB*fX3uN`KSUH4p7NU&nC^bGx!FACgR!U{TNwwBu7e zYHH^>^5MgqJv43}sg&vJnW`c1{@9U9oQTM&cV&cChFDVblba{Ew@z+pYH2JhX^5^2 zPA!^{nbTHoi-r!DSeg}Yub%NZD+{?%_Sle?YX8c*W+-r_<%Jn}ssfiY!BF7hvMQm# zm0+UAd~O|DfcrvYRh@0gH|uQrih!pUJeoJ!&gq~h8`K9r1ja2G0arWD$D9`nzv0$0?76xY7d1lD6RFvCuJo|Vgo)KntsTwz z#nBVv#2WWIk$$lgoUHU%E72^rtmxd#qA3%h+1O*piFJ+cuk0B~rJXKkcV)^bUG69U zr7j2Ash2a7%h?BEs+Kd^c=%9z2&64|*a2{m{lHPchnLbe-~n;?=tm^B(BtIe#XLr< zy%V)!obA!ZXOC?D|DX+abTl`2bbPZm*wO}Vuuc8Zcwqr-h-e*}{EfPxheJKQVCaI} z(x&10g$ZLXOE+EtiP3dJOxjT6rtb!hpFsu`*$POOI-Sg-zwo~mL^4J^jVAIv`%KnF}w_2lQj znP>Gz@U)6<&4id30Egl7H&t$`JR7F*A206e%=@zTV8$hOk+=XaQvLD}+@}Ek_!jUX z;D7N1KXlyNojvocqf z&YF-L;^WaQOTfR(!!m`ZG;KGno|%x*Ijy%soziHErTb=YE@qh<2Y#vqKb7IA)N?%I z;PO(n8dAZI<-;14ewM*AGu128vmWB&VbrCmmpsWRS6L|q1CD0R<6{lHt(Z@6CFly* zIOwBQ$ZY^3RSP!&LzuQLx}S2vklMkb;^7!Y2+6xO2cT4FPv|hZ8d}QdxHSDtU!+iv zh$c%+hf6Uz=ZT_&PY;7jS#@V6{Wp0{h4Dd_!nDNHq{6yXTWMC3UtO>(J-683oDeaq z#mCPiZ);V2{l^7dHE)Jb$n^Z|x`;h_Y0W{F;>L{lvf{#sl(yWo>fG1}OIn0IIkPm| zKg?oFi(Ne{eNjPuY*t2Q%$B9Dnx+)%J4r3sp(VAb^Xj23xE=d7*d%M{Mm7W2(gd~6 zc@eaV>wE&wfq5kJHn>l~S8;+(^?`bLKGGO-^m&}~;(FyHpt2YNHp>Mn4( zZ!M3{Ub=eqvY^P=;Mnd3H{Wyp_{FW+`!dq*P0J{+uc|L+S$jWj_I^>!g7k}Fy&qx| zA3-97xSq4~Kj%(DE6YArprl*7D&^BxA}EgA|a@PdEk8 zg?qL6EcVE9gEP>)YzflJjqvh5m9CG6{)L-)uLo5rTu8PsziQVvJxIu_DAgJ>oP7 z;_{?33hA(#@8AJ&#I)1-EpIFI(?k!Ba!pCm`1g%JY;#uY3cWCmcc;Pb$qP!jqk@hM zT`F#%O(=O3&sFp`;yUL;DVP&uiAsp;FRkC?{*=>E!>j2m@JZBJRIbdX`|J(wCN8izMNHk$4OCFs5^< z>c5oz|BE`jXy{TIL7R}y@<+HWmg8DoPG?|frOM9{+!ln9)Yzwvwg|ul7OCoiIMo&- zAp9{QD-pRFwlGUZY=;w!R8WG>zso4ZynP5bt&li$ zjI_V4kn-gD@kcMm&d70+#yaPJ+zAt65wS&j9LnhTHHs{i%VntV#K;P-;CS3 zwQNu4p7IACK)geUx5tPlM#l3+yc&^S8WZJrXzSLvH{ZPYfd|SFlj)d*zmgU$nBKKJ znBKMYeU-j?_=mzjoi4+DFkHUN=Ce-o@z5mHZ!k#1@W;!~8s}(B&T?2%dG6|9BYFUi zV&Xy-mW##MS{$W}kUO}9!K~)%m^D-_15y4&i0Vq_WaA!% zB(?0^O3cx^K@c0%d0PmVKM2|4Bea51Mt0Dl=}RyY@Rs0Fm6Ki2UShGAOms}1zMy`5 zWphaDxD>46>r}pyP}rEA-&BO*x~H(7 zW6;fatZ1||-TJ0%k1m?Ts%Mg>dh)Ff+RAhio>J6eC91VqT72HsHovHm75q2vA`}<} zOVP{n>yah!lx)zuDB$%shAtO>m1khALmvr(X2yrj&_{TgJ{-Ir0Zo->tEx&BX7rx2PFpcW?$B*`5sMCVORIoug(T=p&^6CDQk!OlIieeK)v@S$ie?wXoYzZyHEP&?^gi9oJy7)2) zzSV;_v|>LEK4R{QJhU^Hd2snavBu=ABD3v))gZJ+HD4#hXD_gU)s>c>#}+=^F@#k; zEQa&^7JJk*hx>vvO4MX!1%^cin1dS|IyxE}gUtbvVS!m$eJ{s`nJpDgXNAQa7W?w% zd`C?}hBYoQGAS(0QSC?zONtDPOU_KFapYIGXXZpj=UcN}ekMOxmNh>*DkrnOdUQEc zJ>{%GIqg6N$~j&wXE+VsShL}C<-pK;KOoi$(RRy zx!h{0J$GT0sLRu6hPdxkPI-n4nwdPRpw^6UE2!G0v(Tn)w5h6dDIT3m<2AO3IK?b6 zyV<=HOm&u(4D6xzjx#(b0AH$nfL28u@tk{SvzV<%l031LjgZfLZx&Yc4X)M}ay{qv z^%2-3UpyOyw742qDOe#gAtmk2#C;BGmmjNM?&clMy8&!lI<~-ljK?rNZ;Y)QLBB7B zW8U2eSr>?Pm?W5=bHUNi^ZZ-mXx=r?#%|3G)!iiBP1aq{{Cv!b5B(UUrw|@9w=Vwe zEH&*cHF=1z>6`d@ZlRk?!9x)$hhd8ceByd0)N{{p zJ@YLfoixajV+u`GJ55=bR?qBmJ+qsjyS&QE?;XQB!c02fANUs`@CQeR7DJcf{WPt7$}*hVs9kNFaMX1p^97ISsy?N6D7DM+wm7XD zad*+~w+n~MGk*R%8+QF@V(qYBJbY`_uPwu0OK??`CcINI1Hhi?5gT$nlA4WPBRA$6 zqgA>qEnndNA{P0{A{rYd#GjIIHBsJ7M}BN|T6t}$vv%5~@g?c0=l9JXUtME1C#UF+ zk}^IkI^I$i8IzxxmKzctGsRgjDat3dA>*4qoXB>W%_hI7Qit(?YK8pH9a za9XAP>`#@@vwvs1IsRk^Tx4{5ApOfzq-Ol2S9nsk>z@Q>Hc-RGEtd?TP8xS(%oUkRtPh@Vx0)wYOb8 zJuh$i)otxpP0uS$tDohpbk3^BdPkkY^u>{;$@~t0ZuK7>vzprM&l+H)^xXC;Is)Q8e5L9$|`62Yi2=*%{+huaqlw_@z5yqLxe_ zoW5bg1Z6^XfiO*;Y!c$pPxA6U5ivL3=>FuoY1_9?yG|{~X2I5p*RV*EP51H9FJm_c zA7D~{7FzUWh42b%IKTiSDRhzgvR*ENWIMW_jnqN{r zxw)m?ksp}l7iZ5&tZhl_%gU+Bs1%D6<6_cEa!ZO5JCb7T!Eup6bybg-{V+CWT%p5~ ziHg4!b#g@4Rl2#)X(tnKHfY4W-J^q`K86rXt&bhX!%_8tn~jGcmQuX;HNq&)H)vrZ zr>(2IwY9suH8(FW7Z7uLySsXOySjUeOS3DoOVuAF4LYwEYs9Zr`H-xgztVQb8t1Q! zcJs&s(An!nl}EP5>r{F6Q0e!~XzL9av_wuyb$)z&eszlB@`ZPRSQDSeaxgF6aBHh` zbE_Gs5&BfD7pqiVANE(TIfXj5==N9ij)FoyAMt6~q%wrBcE6(9mF~jxtN7cJX>_M* zf4#GZ%PqjEW%wI&88CsH*$%GlD1`>V^X<}f{EI=KV9S?Z`cviW4@DiC1gtN=rY!66 zy1Lrq$7}2A9?#mXrPYYL1j1nhRL=n5?@g&^g^-p?pT0Rwvo*5GL*Me?xl2Hwqo^0a z@HJdGxQ~YBT@8Ndp}fn1&j9#f5{)CEQGDly2sm({@xXz@^48kH!P;|c5yhY(g0AHa zFCP2q7<(ss=o_!kM}wwk851Fc8s3J@h5?i`DPU zdIx{@ce?xWWU=185fNSVD*h_&0~j`2cz?Wp_G=$oo-hcR@K^cxToPfKHGpT2$OMzK z0PVsdV?G>>kbGqc29s#MjVu*|1Yi2gBZGP;87{lTk~3fYduLj4QhY{4QhHdGxv?S* z*I8yn*wTWlqMKjMw%gOH+e2nA%Ce=TiPoBs72Tk(nyPWHLkf61oksv&F6;w0;ARe$ zKISp*w6G)nvPNtBg7Z?Nj>%7LT&`p$62`P}xkR)DJqUYh>R-9X4AT6uLhX zC$=4GIE4SVAzwi~Gza_MaF+t9Q?v8RLyzduEi&-H5RILH5WTA06*-RBvUMJnbdjwp%9U%!Nh0SXps=y!W5p#YzR|3>P z-}7)k>kxdfmFwgD{eTrOC{c*(K$@(SqUT_isVP>us{AZKPJCz;J*R+s4y#wAdyY2k zL9Kqn7qKn5EIz(CE-59rDx|UOyS<1h5y8Q+>6W;Hn&F2-!%xExU}m)sHiJ(Rp)+9> zBGecc_p!W(OknQkv4Q)6)(g4Lc=0+wcUe-h6yc$cdn*rh+*^65tKnNNFeKQ9#iGsc z=F%G;Yx=8*C}~z7+6AXN)SL~&}A5B_!I zwvh+KLJ`pU<+NY;{{q`!N5vhMo7!nTwtkeR7N-^?a<^EBxO?1F7jn!mJHJHQypL9q zQ&8GK%Hj1atyk(Ia(MjFbrE%E0dj!W>;x;CnIa^v6Xl*N_B1p|vCZ$EsV~pmuSyLg zeZDsc>GPk7)lN)vC-d9`=ozJXX@}D{h>pT`+hM|)_+RMB7dNoY^oWkC6`AV|bqh*D0jT zc~Yq_Ev*LzDsvKZD%BqZWcql&#+W?_602W)pB@{i19_FhxN!k>f_nLubwp1?X#B?J zW7b6Y-Upk4F%bhJM#l(YdjI_?@4uh=#1no`Jb^jK9GnS$v=t{q!`*-7Q=wWP=)m6t zp9UJ#y3+5x@NhnlJAB!zc-4KSIMVU+2KU!$yjaj)sp@7^Jse|<)B3E`Bgem#RYt*8E2ZITPHxb4(5AJ z0!HnF(PvK{-CCartJiX|mdEU^>2_FoaGJFxAoWsP$Fb6irx_dNKU{Sa&xN|`P-gBO z*Wz!TpM;D}gOt6Pcjy~ZmUrm0)t=o#L)vOG1bIRIU>U+!ETqGv8vwb<*Hal92g{IP zMCLY(fXnhV58~Cl;q^8Km3MzkQ5JF zdo|{T7D)NkJVU~tIR}~A4k5>|ywI=X^VlXlJA`;GkPSx`sPudf88l7k+3Bhu>pxsO zT9ayfR%6jp4t#}3fX=1gS2bcQn=KXByR&K*bw})ES6X#pMp$NeUS(Te_2TCA^ybCY z?7F9klK2j%vm<_$B{wpq$P!~K&xtGT-PqBwvA2}nj5m@qGn4-xYi|M{*Hzt*&zl+T z>u59@?b2wZ(Z28dE?crKkL8i=*owErP8?@*a0rxOLJ|@n4he+jPgt51%34TL3MmQD zLZD$OwCS&bhL(Q=Khi>gmZd3Mqw)Xyz30An-+1hVe)@MJy_usoZ|*(!oO929zlmvL zp6I=Wc-7DG&0VZ548U{SqXtXwWTP`r9j7*w<|c0Y^Z}~%cASzstE5P_;GxvNb#xLL zCeSR011h^w@@?6kONZi-mM4*IYc{@CSXC4$>k5UMha-zyuRh+{hQ?fD`H}94zPjeQ z#m4+Y+spF;IYWij0jp(bTX$&BuFF2LzjI-Dq(55I-&E#(d~j)V{`UDmKZFo{f$f;^ z4)2qo@d~U1ym$1w*}fV&MOm1^Ym>gVIP*7t3Xm`@NVm&L0n)NcwS{I36_^d3exf=eym=0n!m*2XIv zTh*_-M@JQ%NIzaGUG1ck)VfI17cY$;Ol=y~k8)j+#wd1^l1~gSNCtcfAmjvSQYt`T z`%%im!(Bc`{ZpI2d&}z4m1g7;wCx>T?V8$I;SWsiIy`V&^MTdDAM5VDR`K%5rH00Z zL*wIz=Nm_xy*<0PPE>^FFFTw3>UJDiuwDI9^V{wTp^xeZRc7EDxD(oac$95gs1$jm z3o6#2ZNPrc5|#GiW@&~`y(r^pSvsjv$%o+JDL2v*CkOJb*nt$|?jRtQ&gUsR!?c1L znqddPi%wer8#wI&<)Y1MK0hMaG0?U+xiU1gGP&3`;C?+cJ3EA*n@V3DJu`LU#MGJS zt4rOl=V#|;=4RC|HUGr9x1nd(t1+4eR;01jtDFhMx>w8L17oacK5*Boxb?q>(T}_YMh=<-Vs>VQd`MXcAnId<4<0*y>D266 z=?t=(+s6Z=p*;(KIx;jga>I#&w_Shz+X_xxkulrT5N*#$UZ2qwYwnrLxWd{xzB|u) zYu@g0OcVR5-D|Lg*~%X9p}S@|jmzCqjytL8H7FImWtb5gmC|^g&4BDILu{c8XC$zT zD<{r2H5DHJi?crD(^rUhlU-n$K3|DDlu%SW?evhNhk;%$vkADG7A~P_VR!x3c;53r z{NeL?@vZgUXkECJ(z>v|9IXov?poYf+;tGm3(MWgk=taJ&g!gQKTAdvnfoMWcuO+O0^JN%WqJ1qqELCNY5Mu+>6b1`mZ!A+DE^%Yv} zR^31Bemw7q@|=nBzsP^4{_(uWx8vnww_CqjxNl%`@vjz=!volS>DTa}&COE~&FBqO zfph{Y2)5ppCTHDi#KimxWD^Syw&9h86@+iu;gHo!Oz{;*UIWJ3k6bEdi$EQ-lkQ#d z$1qj`yoa1cDCpbTnta|Pt9c=I3Xh+NOjmCko!b%2``Tl^qVk$T>y{INg>IaVve>ZD z*0nlPH{IXa+BvtQb*_G~G+5W)TAPy*2k!dL-Q#WXwPO$JLP-3>RJMma!wQ{bp>_<8 z*M5iV(z3+NoTbkJgNcs=UozhgoyWl~yaZbbuM7-!;L3CWc{p2$mg+-QsFiWko- zSVUXjwwH_7YJ3~7_4;Zw6YWkn8mPO4Adkx9pfmNAlb|Ugr6_gOi4@t@VMpVm>+rWW z#UcIJnTnvp_p85i!lWF!=S7A$;~k@n9hiOwN-P4(ZD$uysa_fFmTwS}Ol#^s_QKPz zt);0)ADznHe|)C5va2D;ma*lNhu(wZgGMI~y=QuMcKXyQ?-Qp^9bKE>QtZpFY#xZR zbFB5&7hdS@Y3=FVe8xhsXke&gaF903lEz~T3q(ABLAh?%k#%60T%{q0mtMBF0J{vb zzknn;WZ*%f^ivt@23}~u7U{M&Y;#qF(55sTy!@%6k)cZt zFZqEJlh)5GuiNR8 z&%K^oIZZj4$(GM)z1xh2@LAB7A6lNj9ou0M-Y~++v;&eOh!ktp1_Wi{%8W5`8q0l| z0x8>z-SmxWgWazMZJB@@CA?oogaeJ>OV}Qvh_^D?8*R$C{4!r&*6L_RW{ND&@c+x# zv+}*4ek{AY9OArisByNerF6EWIpmV(rHz5v(w4H>uBl>>h!6bh@lHbikdLFDFOmKl zb<-L0E$Po#jA?{=23Dqp33+@L`xkt-%5b1QA|WXYU&6gvPP3w?E*D=(Eh`6C zmZQ-6X1JR{D{5W&xH9fRIQPOTT*?2upJgjeIG#|s;(=FqDQuTKMVm4=GGKOCZEs$h z#lP3D>+6YYvaEuv)4%!6i?i|B7xl&JVJPx>YYMhzFKp@^z|$yFyxz~+IlDe@5?^aL z)I>D3fHu3G2J|y8tzYue*!)$Zb@PzbBZ^70@deV?rypox?h+RVhb1UsR)zcI42F^?hGa8paY~dq5FCuC^8ON6uocUh<6R` z3v|XBTe1pE3xf4+)8iFA;rfEwKt*L{L1|G@aZM=mkw|Sp9vVUP; z;OXYZNTe_n4B|tRZM}0vfuY>|s`9*Gu#6w7Z>pGB?i&ABbTEH&Pghr0&yPWJK3Wp+ zS-9vB?JzVQRsh5F-SFDr$ii2SDjL$$s5j9z`t1E!Rq-6#fl}IY%zS1%N9c?6NV;Q; zO+3e<^^-0MCmoXhNQX2w8gON166uh9O|EY44;v>l;%;oMHyn%xBlx*wdeS#GlC{le zwfL5^M#g-T)7iJq7SG-qAKH;=Rc7uO`pxW~*&qMxXFtZ}H`Y6oSFczr$;bGE(Zei2 zUm7|-qMjRBC1qobL#=W!bup#EY`a=7HRitQw~jpmSRu&)iP$lyt4TMADp)=|w+Y@J zby`Cq;(~aG;#usLNOng*%`b0Hgj(u@JF5GZS|57L`&$=#tG5U1T0-mI=43^Fb!P;J z66E)-^r2}{IWEZuty7WC>io?+KoX#dHO#LA{=-Tr7Cqb>t5B*bwUb1;AIRpl)0wH( zhK#4Dlg!pB6$wGyT7rm?WM=1n7jpay5IeMHVrqJwn3^Qi`oBAyg?eH?>WTfRCq|cP zG=cEoHg+AfeM5!#u*7DWf=G(su3E0F;ws&ru##D4t`bfhlVqZV>8HhJ4I(i29)H=E zzV*Y0*Za1V`5$;-`{LsE8_V13gJ8k(sfC66txr82Yq{>q6W6uG-aI)mF`29%E+}sZ z-Gn9&1=B-|L(@+(;zZrD8u(-Htsqaz&Ho?kmS*$Q{9~wsRusBPDv@NYs0*o?)Ebzm z)Xz4cbooHhq?*abtDi}1-I^Hs)}d#g{rm49|L%yjWzp(Ft5y6gCLg!9s5wwBun%e2 z^EFbqWEj}{b($kfm-Hw%6}!WO%Mm3C;#HaAb#k~^eB6%ZOe=cVXiRcW_Nz>Mk_QRW zyvf$;G}9==0wYIA9YZc4Obn&#i^5uU00RXCG@MR4`8@S3c_t_V8&uZdu=-)wsc2|8 zgv`v4wRin2{{6_?^dnrp^PNANU7S6DpMPFn{&RWBb@i)trL3$4!?$?4tOwa1$M${F zCr4??YgAv2#+@b$+>6XCODZfhL>l$#W_Qo#fDN+22H9`}WP5&wAO9&h4D2`FOTjF+ z0D3Qe9>R}L`bA`;Uj(9a$OL452Dq&bFDx(;ll6sk{N4EIa{&AdKj-kHK3aqqRPT{w zm;-K=H{G~S;|Ga}9FMT&bGY)OetZc+fiEgIh7b4o;KmRunG&2fBPBSki7>9D$x5se zBhWICE_y(o1K?-)!G!n~U6^v01H_blC{FE?6t`RW0)j*AiEKQNI{?3jwgtbZ1y_Dg zr$hU~_|S|&{bi-0YKzVix)uVFF8pTLI1E>EB=o_xVf_2W>b}7IsAY}L2llN#XyxS$ zb-&LlO8(+~-9tIa|FYJhr`)%?IGY>H%M0etF0NXa#oE4~NZj2Pi&J)vz^=%Leb#`H z4tqc$HPA1l0{|n=O&A`p-Hz-`vo+n-T-4X3GnWSsqD?LPV=i_C)_3v1Op2IHWnYli zoAj#zz-rX}15jPqep$Xa0!tlDHdVC)_DXg1YyX&~%m$zGp+QRMWNYNAARS&cyF&6b zb(APT}hHC2uijqG%Hga$wW?5Hwe_=(Y_6*&y%s=0V}uWvTLs$^)m zHrm_OQr?--FB$hRZ?2%R%ehE^@G4Fzr0Ytn0ywvGV`wU`~x>z$@;VB zoo0X3kpkj+?$Z`Y$Mv zc%{9`dC_ged5ibI9xE)~|9b45{Yo=qJ0s)Qq}D-c*Q(|nQJxR+W-1T2Q!2|40M6*L z=MSuCOjy&4Q_Ks6p1P^<0NR-ae1)r9 zn->NfGG1l9#3QhJH@`i|6S1)Tpgi~rkwx=UEPTD^Zl(v=>(3=Bb1a**Vpi-^$~nEy%N2)aXe6}kK@)+-NwZF!opPpa|`dqfq7p@&bZHXcIJ!o5uXPymwMjD+6ns%5yrvI87MmL zXy&Ww-K5{7Whl7&4J#HgXLHgKVG9xlDaCO{i=POva4n=zcTE^-CZ-Fomzw13-uJ%u z#WBygJv*FD?R|c;8hKERlgE5{$hY@nf|>rr&v9|!h-+#1z7%+d1K;b0dp8~Uo)q{q z4t&jlClE6$*6=Lhd%x&BKhFC>mviTOz2n|uc&_I? ze@r#V_T3EuTgIc*Ljm67I`J^2K@JlqOcn%4jQ~xTD$WSQ2Rc>*o*+?k_Jkf%dtWt{ zX;N`-1t2VxzZshsGctSdmu7VjdkhZX751@0L?EDJP~U=Ij%pe*Fxf9l-iUnjRaqnp%7rfo;>f4xhO3+L?*|xzeHBhQ68R z11Gm8f46mRw6Cr|7AkM7D$NQNSGESqTUr_?t9-t$XlH+787GO1jX?Bb;Esc((o+*P0pHvHn2Gg?Zy10 zJxw95?>C=&jq^Fsi0==a=e;%se#(LWK??jW4*ZM*CvDoa9P^Ww-QOQpJYzn0i}N{t z{~70bZ%l#T?7(l+@CoEH{Z{cFBRAl^7lro(&iM;`UkaSOC-A*)xEim(_oTp|aqeGp z;0d0Mtl^wloxg7-bCl6iI=a0LX?~_$MEI90`TH3MW z?}VxJIn{~Hmt6d2<(_|*Gx$pIPred-?swx~@s+?2xZ#Sg1itEqE50I}d}Xf})?M)x z;ovLWuUFr-{^Tq9+-scAkxt210)K4^oO~tlKQM4_)JOgh_!$RIx+7l^4!%0?`(b6!L=x@2;&m}GUw|l-xC3uHtRg_>*;P}(fOmW@n zP@>*x4dLl9f&myqUlXrsHNX@+YU&of<#;uOS2Wo2bZu)UaHl@_0><3}VJ*`3aQgd`D2wEC0vQ;+r z4kp@Jys+2E-J>f9_D;9QV(o>4*;P%wwVhJ|U+HY~YCM_j?GDz31DRvlRXC`(e3)}JhHZD`oKh~)vDtMyTGF9`tT9)1&^z`FSroY~xfNR)oB8wStDg%cLpi*Pm(x#hT(%2%?n%s7(* zB<;^v4hs@I6A^`x0$PGstCTewy7J0q0?KOUlR|jA>coZ4k;3)w2aX*(aJ<_ekNdl~ zS9C=~p=ejd+}xbCaYgTB{@ZT3u&EyKd<(%7jeG#^AH_v zo=?*CX7|HlMADpLlBUJ$v@|ne6M)o`!-UJ3`vnI!7bgb>1yj)B8Udv-I4(|k4{l*z zBOR@cs?w5aE;%}hk7=pmYN11r1~OeHWv~UBQZ_Ui22zO{O~cej%|`aX1M%6}`0cJO zOz&NfPAx4>Z9X*aAZmSS-;(Hp6Xz(?p9d|mW{bNCqBm+Ut4V?1K)BM&U-XuFe*q@y zXI5X#v8<^ZnEKIr5>ma3kThOFCX8(XQHm^sKmew_O(7h-{6W2{e@+e67yZ^?n(r zY8*T@d-km=TldgH<4B8l>Tt_a=S*c+D6gV;c5|!A*abf3A=^Sqwz1v?vW;oD4xGB4 z!1txVDf0xr*9})ZEpT+KH_!1>*Aw_!3Y@$y@Ldi(fwOR^0}{@4gF)|%hxV7;zwX?R z@jKs39T4p*?E94tDEG&CKh}93vKi0EUN+CH8yu^rdmGa)p!)18DXpWohF#1$Yj)8B zLuq*Ai+Z)(49AIiRS5{?KEgXl3S;z(n{cnnL}yoldE8Iuw@Oc@J!R)<*Lu}{sm?#9 zXE)2G^aYtbA>jw>MA!5wZ*E=bz#cX^**iF}K2e{%wXAi#abSDtcveMiq&OTY&C%9Z z59+S!BO|S0XnzZXK7U4R%VkrOmoGKP<~N38`#-cZ<)i>!+_SwmW?n{{=ILB>rp)iQ?hDvDS1+Gpyl zbY`pP=w)PH8~9jOLznnUr*5v}q!rd@KTcW^>x=f1gir1m-ap+Co!U3Rwc2B^2>ifh z_5uvdV_hbvuv4ma*>zF2bP~MeqwfM!^`czACjx&bGSr zj%8LxZ_#GYPTQi*Ste?)3Sh4a;Nj@kfyge)G9{(lq$|_qN!KQ`HJCh7sg^hoS6$Sd z#{yJt-A<+!PrZO&16d?0>=z0g#a8sd1ODvtXixpFqw%SUavay;J`~HlRp6&B`mWK(5d_`f z^@Af&>1V;KMz!0c)b@#zT>-o_NB_eK@&w!MYjF zHY8_Y9P0bH2UyMce(VADxHxdon}+X8fnyKQ@V#z0=|sc#q`C z6jrLATW43YUI9$@iQDrlc^9(l+lkH8rC5B#zai-WUf`JW0^26ObhYHtxWv3KwdC}l zlx3g1OD&ztsQ;Aq4Qqp5FR9C#_bt|x6|?Cze%b3uyIkXGRpH6+SwlTTCh){+gi2&_ z+{g4>spw!R172S63$HxPnb%s!LcN$CZP-}5MhxyugoXK&b`A_T#{z|)Qs9U9V%ivO zSh_{wL81dyPT+o+}*in(=OQ$wVT<}T{H?IK)7g|1`eymL50r*esk|Mcouj%o&}zcXMv~VS>Wk-7WnyiE}@hxhRMfXb~c{V{0{Em zp`B+#T-lj=bPbC;p#XRjb)`TX&rD9s@D!?*u%qwH0nAhl5hTNjtq~}o3ROKQ;x!b7 zsTj6iJw9xscy8chDHsMGhd@saN*62zW;6Y}@SF5Lr1oaoVT4m-7kWJk>VHYuiy?J? z!YB6&oOYqW_qyTg`2yc%!kJw`S9NF#cRDlaaxHLDnjqECr5YG|7C--sAd24q3K_)hlsem%Af zYL!7aI?|L$#KE(oN8{&dlkq*54h(wziF9?+`#i&~M1v1Xdfl1S5bfD18 zC@ol_V~a}iONAmMfUJoiZAm@{rcuOcu=|15gdsVdDRF1`y@Y>{1=gTqQy$=qE-er2SC#r6@k2>1b%S$X6ucqX#_i6l z4*gfGv~~=`{$b&?9OgyeaaK?{?Zd&CxT%_bW!;PAm8}2i*jh8)>3aS2{94gZnr77R z%tZ9S5PiUv;T;WpJ$0FSZT)9FvCKc;Qs#nSHHvnCr1BEYTms|?KdGw)pUhun0 z^n&NP9@XB{z;7!(?*(uLNLe10Nz)rmoBW0$G>aR6o1={@0BujSD}e2Nxl&c|yi>6! zV&M)zic33A{*+Wv+ns~BU8*7-*`O%f`r!AY?Q zh#}5vQw*}Qr`R&C#^Nahc7o@lEuEb$__1#8?&#>gv~4czYuVj0#xruqS~_M+d}Rx5 z+qV6tx2vQ&RG2xIRngenvt`d!d$y=wzebP?p|0dNJ6hLmvg+<0Xk&P6>m^Qj41)%3 z?Eo`%E4f?&0BCE_4X6AS_+A?hUA`L;FM(5+&sWc(Z#iE*hdy>Z=bSu8;Czn2_Yw|y zM&E2c)^^o%KjUUQJ#nDT)~#B(Y^|S?w}Tx<(W>@A3kZBfw*7L{fpE(Wr} z!p(y^ia;H$Shha3wbkJ&lrs-lnS${sVM)Xt!>qfSBE?v&@ee$ zR2d2Es-7B%UVAXdj>Y|*E3`JBG1%JO+}7SzT2-9m%l8-74YW0nHU~SJd-KA<{I(Vs zd?n#?qI0gbP<$(EvBz1X1lK~Z5#@-)?&h)9ZupIaW9^nfGxgB7iakdudC|SwwRU|G#g$@o!0>_8P-}0x{-cMX{W7EPBoOQl` z@1@6!A5H$!doB7*y=Uk5vko5`h!0gS%xtNcA6TW>E-p(eTpZ;cqr!G3`>z*Ml!zbG2kffQUWF7^1QqJ?G_IvS9ztaWDt3XO zQ^$_fsm+raFAiHFKCQbXQ0s&r_V~(5)!_D?!^gWPW?CD|x6B{dHN3OFKRnUY*ebsl#StG5%>uvk)UX69ge4eI^Ab-%=Uc z@spo?;u9~-4ZLycu7yzLosTWtHTCBp2gq`n({I4X+>z*y_-juo;jr&8R|kIDx=HQ_ zf4ku~I`A6!HO&1t5Kf#yW)o-K^sq*Bg@lbWO+2P9B`vk8k<_FQpl=Z&-U7a9!Sh-! zAjBJiAhsQ6#hl$3MoP@s_Ng9!X~bWI!_F7hz~mr3`wkW%IHFljR)lq%TPznc-5IGb#q4xMQNDy1nk#?N+ z_l?6UApxEAd@8o?MA}JP8_0|LmD=A?-O2r3$B_h1`%U0>oJ!!__XWP!eZSg+1a8N# z)cq=UEbukw{seNJsc#U@y{T2v?)l?cckahHo#!)d;J%;sn>>Ga%Kfz81Rf_Gw7vma z&x3EW({l?m1jKf;X&BowC!0o? zC~#Szd#Y^^s(@ERD%A2|Mp|af$Vo@gV83@83^K=Op^o%?Ur@LCA4F~!q(<+3XkT=9^=Y2OhK0>?P{3isPS76o@b$F?^IAseF# zUa#)AeLf1Vo-c6w`2y$j1#UlI;FJM`6VKmJ<2nENDc|cnpE97s+^>8I@_gk>z`XD| zuTPCLg6A9iTHusn0=Ioo3a)2;~N>&lhamx4BDLj7~&!Mv;)BjfdCO!rJ zF$ey(6!;?!{O%O^haEVn#XN_0wLJgLHXP#_Q~NmK7az}E_Wf#{_Vcs+??@T{N$2@@ z@P6!lb&!!ep$}AhRNWPiF_VMnSmA5>)M{C1G=sGIhBP;;*$k;hvAzMg()A70x+_t4 zMAeDmN$z?N_iH!Rz{8*L^!pE|!h|*Z5v5kaP5#-X}Jf(uCqyzT!QS ze8k}PX=m>Da_aWl`42hpx4YrKTOIiAZn(c&!?XPN*|_Bz`D_}9`AGwMrCi@*KKD-h zb6hKbqw~B!Oo3N9@cUEX{SN%yHe7wbz2-4LX)68u&F9{0e@=aWi}SqqrNFsw2+ci6 zIA{ft1#b^%r3hh^v%Hk5(|jeaijM1gSJY~ep%r%pmU7sbxGJ?RJ7^LBQSeA?QI(MR z43f!1_(Vb~3};E7JZ z40#DWREb*eNqRBc(f*w$8rj&;J$no`+%cwpOof%TFfZ^5J+##;=*yy7mw{!Kn;Kx_3r;<^bB$n@Whq#V*9yo>N#(0g9V{=|7O zlcZ#dHd=v0gW6MIux^4VPvALz9S`LxVUl@6_eDtIpY?z=?kJh0XOze)$S7DNPEn|K z4yx+VXyAA}{<&MuoVn$5pGmB*CoaGHZYw@xwLSWYPdvJO`l_q1I{p2bG69r ztuL$hhrqj-kDe#FHl*P-DexOKd_U%&t!R^e9MCLF7C@T zuaSA*?!wccpE6tU`feLf#JA7JA7nl5*H~A0?<9U*pL?(UIlwFYl=bqw_qpKNe#&}* zKS((EYy^I(ddM{9XCl9vhqrYoHQnSArFFbB1`{1Bpmg0?PcF-P68r<(~VV# zD|^la^p#2fmMU{rIh9CvD`8(^5 zTW%3NGS@?B6m~oCYwNgiM^+H7H59A|YzoxtP*$|5`)%D?;FM_sw{>fQQ>F>r)~yB3 z^%J=Ly#l996S%Eg3!LjG@H_GCq>lu~=~pt1@QaVr*11!^*XY~=r%aRQ-@)fYrm^0r zLdmqN(q&rO&h@e~jZ*7;|9cjelABjHu11DQN-5K)+?k8Eq8&**{IK=t!w;v+ulwe` z1M~Bn-6aGL=M`UUt`Dh}62JabYI#aW@EmYc6Q?j&T z{WN(c28eNEeBMQ+`~5or&HlHZy9C!P*mWOKe_z4)zo%%j9dPWk1@bw8lg0#oR>Rlf zhea=5z^N-!M$D<_yc)C}R?q2_=bsb5?P~~Ed0~WS`Y*j)p8p!;orTqNQmcNum+>*;N^0?dW#SGi^p zZ#aqczQjNLgSE{ns$7ImEBVRA#l?RSJS|`zr9Sd*m%^XTy9>Zmslp%OhId6KQN|!| ziR+2q#H+w5O9Xy*3Y^auxcz)1!w7dhAL}oAkihLdLh=Z?4gz=b+c5rYWn;?og>R4- z2!9Kn1I6QJF|^$IYH3}1(o8M01#4qWx7aaGIqi~G8WDMsx%G5NOT>;nAlTU%dM^7j zsti(e#X-ZpK>Viv$l5+U{#&cA{Xeo`asP#P`K1>TxVzoEbMwy>E|t!pXuSuxF*p`D z<+#9YIWBPGUf?$F1x_9lxGl#8PB|`cTaJ%l&7rGkxLFgqpKC7n+wwZ~`9@v~{8P^T zZ_%_MaLRFcj>vK52R(!QpnpSjooB`%Kgi=$p+$gI%HU>ax7W^4!&~m9qCYeO9v~;7 z@08*YDuUubrq0x?{#pdSmGe_doL4pYo8%SN(#NGf+vB|I)qkayB^7HD^EN3$>|Kto z%B)ff1<}NZ5zS^_tz~r_819~!sH~`-*qGTrV!!<7%3^fGF4joLnY8NoGDHGZ!An ze&bYX4(L3fek*dz%{Sj-6)DeBaDC67b=vvLx3uW(dG*y-UyV(lc%x0O@DJ&r=sVjD zr`!|x-8LM1NR!&T2&WD)pq|qSJk~0?iudG`bMhR4^Em>)n{e#$ozO39VY@A{VzLa` zHCr)N+v;ediQMEd%;4T3rp50x4ZqVgHY_y5L=|yrUJe_Ou?Zf0QFtXsFVx^FE#Ld_ zXyGC5E#p9BMw%DO?UYi<0?b&%$tO-y0eaYEy8Kuc@?}gVd%@bt#>v(L)7#@?=r6O_ zR&wTK;#JY{nzh+2+ecTn@rZ@fS2;!S?!=q9jJf{4{=TK@{?Ysb9IY@JIec`azdwf4 zg1W~G0O^}*xb*6bJ`)Vk#MZZG}dqdG+gVs zo>ky-lC`}ino!M}=)`ohYu9qsYQ?cZ#Y)1*m3Xuyp|fU{J2@*=Sqv`ZEOHCpi!2;j z0i;6P*T^?%E757|J0HFL)Ttw*8}(!1+1Sv`%<#caB%g-zy!H=%?}^KfUensqTNjxb zZXBN3@$BY3LSq%6ZT8kJ@*Lr&B`1v9^U--LW>LQO}hWBhoe7o_Ct@V$!6rK?!eF zW9_TKTWCI*##_=Hn7l;?WHF{+egO_5G9Z@%?qrA&H;WVnh@8NM(;&R3mX|m0Ny&ep zoT|f|^1;}s;>m^cfT;+Q?_zosq-mccu0JoLuzdu9bUny zUkcpTFXev9M}gb4AaL@uz-|3f;M6Y#ZtIsu=O^6I&iUi9_t(_t8~sw?)Gy`vwtgva z>X!ny{RDB)8f}4Idb%^Uwqw(p&ipbwrj;54Xs66*cR53K%7aq<+p!uFVhQ3ZY1iy; zS|tw8fG&vE-PzqW?Y7H6*T4t z8#whtfg9WDMejc7hxML~^m^rbaSa>vq~WWQcT^Hzf*Lt4qVT)yB#Ur^BIi=%as*w- zfqP7)DkzTx5`qmjF5OZQ3lw$l-LhqG_j$9w{x~ufKD2szwYeF;Fk3y>eM+|*!z7I^ zOxnYoppy}vN_+16-L$t)>2`9zt=kco`r_<$ivz+;4yH`Oi=NUi2Q&#m@O-`CZPIx9I#n=d6wqm@MiiT7e^P%7ZTQN4p%e}|LaGpVR6E~H{$&gM(om$X+z zX-{-$&>_l-JZct5Mnr@6pX4^1ChMw0q$5 zXQ*g}-<*0@eVV?e+jJUzEyZ(>uf{RJ`05}X>-brF?*4!|E^uitGyXyk!8C18o8MN>pf?2_kFX9a*JFGl3 zgnuS^$AAB~^#@v(LT$f@_sMmwiihUv@{_Eh+ zz3B;n>`JtH$HSzV+T^Di{mi)dXb|RUO$}!^`XHE40K?YtKj#a?jG7(ufh}_A!EaW< zQi;>ido*o@pb%uKsu$^^f-{QEI+a9GNJ3MFAh^u%28lj=mWFnhOYb;nLSVZ=d zAr=#g#%T+hj4)5shTpg|7 zGO=R~=g=-pY+V|iTsXV1eQDcW%lUtBoi*9BbEK}Oe@pAwzJNdD(p>|)$Lp<6PYpEo zjxS8yn!I;zVt8`w{c~F>^RTCZ##uFu&;XD3;?aw?Y|&FQdzM?Oa}Lbg;QE9hQ17&E zNd8OQ>qmbG40To@;O@O`$0B(HTr_G&IPhhQ1P4(u-hi z07uoa8y^IdM5}4c6j|oDgG{UK*y(*o+fPl8F07RdG~KXt=;WS*ozp{0Q-ix(`*-K> zUK-m`oK-U4*wR+k+uX6dFtH<$o!8jV*45P3dz7?+T$gjF(GP{EL8y~UOTeHk|75Zd zMZ7&DjTkc{?@! zcC0TaPc2(FKT=z}(0|v#KS|C!efI3r*3Fk+`X=lSkX@<|k&;!w5#FmFW$Jwn=3&8a zfjl?Nqn;;EBdb_t7}}%Pdx#mmRz(aTt(|Mi=E_#e^`hGw5pA*)S$FG1nkerc1%O8b z$g*bPDu!w?+=5{sg{6OxDL@O;5}8>xj59w`*P@Su!(k3QP7jBPAUQDv-{$@DSpVRmG@8I9(E2}g7nZtkc!V3l`&qJPn zn2C|8xz-v5Mv|u?>6LL}jugNm6G^5rh&X}HAQOdzuu);@tSDA&5CJtnNg1TBR70ks z0*YNfP=PbqUj`F}O=*a*9#3xh&gc8HZA=gsli2HC1i!X<&<84@Gfiyz(4H?t4sL#x zYOZ2%tW<6lV>WWyNi8T`&S+}H4CtJ6yCSvwtG`e0T84?Wr_2iDnA`F4)zPgp{XKWR z@#yfv#Bz8fdiky^j`ej8481CUpr@veXWD)PbxIZSCGMQipjWS5L|D0?zaN9xxY^<7VPI z55{n>sT&ZDmZu)%ff*5z%<5+|`oJ)ff)B;B{g!VVekV7#q4&#gZ@2D3xBaZ!lQ%*J zNiB*W`V7)%aC)2P+bDv3%CQsR`sWh-hsd(wbO4#8Lf*;?->LaPq#{Jv1-I~J`1?fv- zqoh#!e2lzFn~yt4prnaA6Dfern;eEAgF1QUicEv9X|-zZ!7HhGGXAOu_q`)nUQ${U z_`|~wzABE=rLE6?xuOh%4wrxXM?X?)01eIh53E6{CqctVPDrrVAninU_b~@*h;Ao3 zTPsVe44!*Hfv2`SSQj+O)LzHqv?^zc!bREE-uUCK4Z*JZx`p7t)Kfbb)eWIRy z;e}__cS2#pj!GREI$|DJ-)*y`eP`PG83kC=quXMoqd5=d19vy>72O zR&B=^d0@s$y0Wd89NEorDv;(K;S;3j!s8Hvc=%l$|C1RuUjjja*H!-yrI(Z`oE`yv z{TeuZIUC=wv>Am)Vd^@HR|I2F1Qm!LvF?xm3SUp!fmX`zhgzq4;$(u9l}u7~hqU>^ z>b>e)t;Uz7#?1?Ky+-h;7gB zheeF{c8yLZ)DhYy9T;B+Z2tg!P&;^zQ3>24I~Ai-1EsTp*KLj_nigBE7sbpVQ9B3> zV4Qc1dyJpS?J-bnJjgo+#f?qJfuy_D}7?`ycQ&;ZXdF50^ zZS>c_cE8&^=iH?}a304RE><+R)SdyUEXMxLbIx3 zL|PAQ$H0eHs;kt^OM(RI>0wLo6x$B`ji=ZSgYGiy9vL`x1bEa)ge@6GVK356&`G}5 zJ8s$7M8%(#mD{spsJ3=!M^EmG`(L}GyW5p5Bh!{9tp$$x zel3iQ5=Hl6w$|hWkm?*%g}zo}aFGj^RmxB=L8)m&XU+D*@yVf3M?+bBZTSKPmG&Is zK;(~s!&1-b^!+)lHA7kfsoAjk>1@@RjR_gE8PMSiuucV%%)ApX=$Mkmbr#Gpe@?C-%mNCMUMVpx{Z$sY_v(ZCS=6zm(@$R2a|vFV7t@Sm!yInrq-Mjc0`fyq3X=+>dP|axYN>{n z%u)%dyT;hYQ=szBvr*GpwrNpnmPYT424KdR#=sh2RBqvDTDWvq7pWBzzn*j`Vfzlw zOMJI34wr+U7?zY}b6+8vyhX$Q%%ZaTaAzl$s;9TUwLEvFs%Z;Wa_h>_@@;&i?++JK<;l?7khVdlukl zYl(i5^oB8m?^-eD5HyAp+%@g9&rM!>n!tDS79%SnjCg9Tc#-pw1)YP@%RBlfrGpI9 z2W!-TkqD(agSZun^Ze$A4qSE5bNBkrEGxlnB2)SVBrS0G~aew>e zE%UuqwZq%H#%HD{a28|!^+&C#|9ff8z*1Z1%3$rnxJ^G}q6^`?9QdhXp1nNdNX}}p zF`MR&HI%>(*|haNxJr>Od+xJ5mzp_sMLW_EMDr)5F1!6v+&a{`HtJxZzx^H$u^F5x znF=4=HN1DC9_Q5zuO1DLm(C4Ehnq^jXT`F4>Op&JWMH9rNi@ENzSg!L9%q9DVlVcQ z7ZERJl&Ht^)+Z3F?Dh01w03~{^PROwQ$)0dZiKU0oHXHVg26D{#R5fH1T0V!wnzi50C0TW^X@w1vHgS5}VrFsE3{WLsHT+hj{wq%1EF zzw|XfG<^Zxz9xnI!ecQIKtZ%;bl{ZBq))yLE>RK9Q ztrL2F0R%{qWm*K6rbIKCi(BQ)Pphnk+|!&x#sSN{Bu_VPq3c=HM24j3f0D2=MHFUr z{+y!Lpq^3xhcKOq9h}asCoi#<)Lb5L&*Ups&R^(>z*d8Wrg~mvDlHVXWO$EWR!YY} zEoy!jo{WXBx1t#{F{f zO*7i`_@+IcK=gL&&g3_dFM`ZX$hpFES9xdfT*d|OBpusvLDI3&x%30gHyC`_jTW+= zCO#M!L}ct79*EPkGvF&P#J-F3pqUnMk3fBt@%Xce9~SG}5*}BZ7C&SwOdJDWIMLZ< zEv@fJUb(}1%R}uA|Gqr)wfB5;^PcDKzu%Yj)(5{j3_N)~LCj|n^PyW45d_edhuzRc zYweA_m3^yO(oh;cbx`CB2zC#OXtG5npiW-8Z~@Ne%Fw zD_z;0E6dEt!T6rDwtVF)$;atC*pET7wks1mB-a%+I^I1PZ?ord{%Y~GVzjpO(EFts zr4H2@fEFhPkm5*WsIHkVuVz=~W|V*^0TR!Y-PYp%^H&&8P$1G}~r)Q0j(ONz30?>)A-W=$oZv~tSpgSp;}j0PO_ko<-ALF0Ct1 z{+f4DMxp)?dvp$bik)2Pg7bQEX|u^-#_Tb6wrUaf8ZFeovk*>L^%{qxfg*QIOuPcj zEgrmZs1KC}g$Z>94YibGIQdJ1eaGe| zL;bVMH-vh|+8SF2M=nn+@65^Ym$i&HckLLi#oXWzQTe(r7Y8$QOJh&B16S$XMhw4P z94wwF%&=j}$vXV?&2NKt)tFf8?!6!Q&-633mg_)xGibQnbJs=V zTV||UeKi211Zr)-1iaGXTYm-lq$yM~j9`I*LURBXTTcRAO4`9?W>cXn&=rWr6R&&c zC2tDUmXy_&?4O;k?FwA|I>T8ppqt{iO{h;imA1k9@%lJyL_AH^h?v zE*ye{N{l-P<1S;&%d2-fTej#?nQ8-vimRQSvK z^p^|D8^fW2#=7}%B$R7imGGWAbM?EgS_p@N%VpD--FxL1H<$1zy)R?sE*|$XV>8lO zk1a*aK_J<2FT|DbKRP^Jvh!%p(Vcsl7HJh*yONJ2f3D&Z(A!i^ZzeNBZJJi{xK+JZ zbz`W-$y9Y?mS;Ea&j1czF0Nz7nZ|ah<2*libGP-$MDkZAcT?{VvhVk*@u>bmYCKTP z@b?Og2Q?!YA8Su6DB&J&AI2B(?4#nRryl_!~^$iO-6{X&>#-zK$HgtJ@VT0 z>aZ0Gy)i@pgDG?*l{gc};l7wS&un5;twv28N7f7aO4ovyfO*frN zs9)au?u*}dUsu+9&Ypcw*8cta>bY$8RyuSziXpPr{3^_odKcz+kr;>} zyR`C3;WJXfoec~B?wM4hY?DD_JIIW1OlDDmHkZ95k|npn3rEWb?Nj|PxZ{V2J`c+OCceN(;kc!`T3aK#p4TRXleV7 zwzAC~M#3|`KqQOUq|aLY7wK7hBmPT5K^2?KeNAQJv;W-a@qqCDOYp3wvwr#Gd-18jgB0Y6j zKS@6`Y`|CWGaLLFo5%Ju6NjlbplBg38}NdCz5*Y6=*8P;0siB+XaK$ug>=auxh+79 zlj+p5{^E;$-TA)QOmO!FeX(Z5FGIgt)EM6;-K??!AcC(1lowht>zwI&P486}pzF0~ zZHd!(*Sk24B`dME21P1mCW?f-o(;^U7C7fzc9EprzE^X`u2#~`4E`76+erMWGO{;0 zwvV1aF3|Ki?1vEShnVE{rur%)G?OB(+Ge-?kf$&OJ5hI4pzbC%EbHE+GBE>(I>0B% zwldPiTh7jb*AgJ+_*$XfE&Zw)UPD)8eeH)oTw9NHHL!ShR&e?}&ceWQgFBC4Cfema@WL&oUz)9$>9sk-|>pe9%8X2Xo=VcX`n&0o|lQZ)(N}gtVV+@KmeSQ6y7y1T6HK%T?;3|Jo;FoB))UWVhlQ*IPPf*?!6Z()ht&sH7825yYXzM==M;mywEnu13W8>zCLJHs(PSx zS(Oi*eL6YA?>Ki_<+J8^woxzopXB%9+0svv5gWz`#SfK*c(bZhpzlel71ozGSg&yV z(`VJZXcMr;rGTfOo!^%!EqY{#ff;*tCX^SSW(fu6$++ClKz`@&gN|?+*oVEIH`s5w z@nTxNv9luSAak@g+7H+QStb_3fJ4y-uDON<7GL~g{JE(gE_`50?a7wsTGgwFdHpzl z!2`j|&cU{Rkrp>{k6CeSJ*9O$Q@`w_E{2Z8Z(fEgo7#YvITf}*oyL!zN*SiYH!SI+ z=RJsb6m<}0+8$7zXVluUC#*i42X+IT?DL7D5|tWaE&WO|Mao)+8@g^Undv;TFj!ZA zKNjWtb#?1UZ|Ys_K$;$D@%%H_Q_nIdjkC@HG!Dn0xt}@30>6ZC(3Q^blQ>&|o9EpZ7|Irk8bC%3Grk-Zaj46bcE zST<8Q+Y=p*1;3|)ZvzXZqv0d@46gCquerg7mJSBjvgZrryz%CN1LNm3V5g zBBd-*`EfARDRHPV7g!=1U>qIMD1)1{223oG941w;s7lzlQKe5YmWPLkG1);4-D7?40 zWuNuJ-qvDO2m?%5$5)b{UwI1kFrS^9`z*>oo_Zettm=m{Gamx&NDT;S>KJGSy!1<< zok_@I)`oO4?>TUuzxem44S^wUA0&yH%8@tj+7PLs;I4qIQOuUt4lHvResL9?R+SVN zbs%fA{6A;+)m*$<E~Vmnj%oNMzq?&$q++Pw+|ut(>4I_57o>Gz~2yoSG4~ zmVwYxt5o64mn+Dc9-a~@`2=Bzc+;3iSHlbsW-(DXE2JaTR@u&zb(5uuf$6PV=8pW$ z(%)OxeyMNwcqB5u>zU!f?$-8|;pUNr`R6k-mX^QkuO3|O>t7pVJxCmT8P3jyrj7j* zMMWw3-+C`Id8lp+_u{QAsiNt=C+Za1Pg$S&6px_wZs1g79vuZ5Is{?!Tb{w-y+UY6 z*LWzn(j^4Go>Kb(9YfcC*k?S*b8w!BeokE9uWCZnbMOi@njjn#ybSU!j5VuZtq4o- z$tJdZOUoEc3ooZADP&zE`Kf+n>peX~q*sQ@HsHBL;?2lVi%q3Q@}eSLmMRTlwn1vm zi1oMOhQMXl#@#g|Gt0i}nvee|rDWu3&IuJvDzB@a=Tg<3x3{P1BWZI=@u6}~Ci74t zSWMxhoQA)^uE=B6G_(+?XsqvN5dyt2TBd%)X}|i_&wi=Osg-H`$c>mgbJ|#US;<_gFvKD| zjN9v(mdTcuDrWkKU@19wcQC=^x|KuPOGR$EV4+;CtR$A@fsUjH554%szzh^fmGPFk zrh1OZdOQ5a-8UB1mXy{NZo>py$`Xf}=>P1qPgaIeR8wAg&q8h6LsD1Mj`>x9KkGg3 z<6f7lx25rC+S+Qp&1^O9we?}(^=fT>kUt(z29}h4p6K|XY-RT~mW0SXxwuD8Ukl(W z+Kj!#GGQ(&mADH|`{c|_{QmndR07pL^}5$hojbn_iYhPWzJj%7*F+UNm*}m8jhmfV zv*t;&zowo2w)gx-tMls;NbG*3vc=Cp4HX8 z)|1OCH|!oM4i?SDT3TCTf30Mco$p@6vyk%6^95>wV`iJi;BM<$(X^4;4C8uq{TGtA z$`uVIKZwGYTE z&os@*GP*LC9V?}HGf!p;54^KZwi&6{M->cl4ny%zTN$5@e_E{$>g%~U*lVShinb4O zXsEB@H~pWw#tJmvjG7eH&p^&JWSWSGfEI_#UdHB;1kxv<@WK12#@vPA$pDVOko3AP z;9V4RMvqz)AJLN~-A(NjJ1B3GqTa>qsw~tA;4I{jG6=)9DC-#-9*9{T_OZV9b$%_U zxumkSJk%5{sV!cc$NLsEsj}ZYl_z%H$znW_n^RbjpO=?cn3ITG@7T4+e!IRa1t--| z=%Ck`jAI)oHUATw!0yvHsfYX&O`TY<^NcTmg@~SdKx6Dk=DV|8g zu{YXsB#*I6R?pK)xA(?0`ZQ8PZ-qwJvCAd0z{?rhEkh9r`jajMbV)`N*4yDIp=h9B zP?dD7s~+)=fJYiUH&Xbd>d0x8@oqt_m_Ln28t8~<5J!Zh@|8m-i2I=&7^ahfK{=@{ zPjNCJB*$T*sZXk#;Z^HJ!RlyTt9SK{t5!`$UPt>>pW|qF)m(pbe<2BV3$Rk{h0d!O*G}q@GuhA%%+A8@M}IzfOMi zHP*~)J{{k-EaS4vr+_D^Rpgm}^_XR*A2x@QO9H1{61b8}_WizG2aX&8bHDOO%Kf&_ zVg@wE8s9v|99iDrrgJ?rvtOpE3wj|gTuVo*T67`QYWVt5&^>MS`!Heumv!mymqGAf z|K9h$zWLHuK?C+!0-jfq=Th?pUU?nTbaBlQYH;kzC{#i%jf=x8mxUvwm7f8%(-|an zF^u!hJDri0(eqX*AiPs=ncu>POBCMU}S_A8itU# z6v&T<$f1)oM_EQB&3wH{XJgC^&y&FD*by@!q}(=AUL9=K_|6KG#C|olcicak*!aR{ z53f%7Cw6Rn{@(TF*RHL-*2+v?vuDpWn_q%8#QhHRg<3b-mS+Gk{)ugvE9(f$J$EqB zT9MKxAdMxoC1B?2o>~#ax}@wCQP(KKe~gu?13h-)1q0P&Vd6AXD_oqY9IQ%Psa#D| zmu0Cawp|y;x&C4!qk3t?E{$7XcGt!^bxtx*&gWhlpRX{VG>3#c)?SGDhz(Onbz>)# z8I=!!6g^A~>7|&Dl<<1+P8`={4ekh;*b%7Z73ph$*$@cNL*5QHsF{Uf88G*Q0s41m zlyp^0Jsdwc)iw~1|1?ycxv}TL#~#_2aqqnuwN)(qQ)@_lvO%rkn`8~~8zA^4J?Opm z6ey;e+Hug)GlLj`$uHKNW@0p&vCL|^^~_YpN;Q8x+R;YyN7NZ^E?SMG=Up2;+8!{~ zO|M;Fk3aN~yBY<8@yRC_Zc43j0==p;+f_WJ5_7aXN8m?et$Utl&;PC_-`=;W%~M<$ ztzM-gKs*dakD&7XL(3oLrp-E@vkY-yOjjxV{}g#p=%e7g2TxhexB)y>FZW%FUWQ-s z^1fmKEKdaQUxK~baB zcT*GPU5#b&+1dC557=kB4v#;3yY)q$-Wsb&zT10vYH4ZeQ90H%dD+06!dC>y03M(l zfMS8aqxO3Yi|?JjOI8&o#JpSASOBf=;>N|kQJxQiW)GtC39)i`C~7XuNE z3K3!QiTZdLnmBceF#gu2q^x9hxi!#cRQD*GR;uS~ENGyTQiB+TPnEo&CMaed>W`1q zdHb3wwzTJ7x6!*gP@R0ZxS>DVJ6bv(zGSt&ry=mtM1FK`W3;jAn(ME+IX1sB94nV_ zM9aXu^-tw(qYYVkiwQN~(`vq)D0<`}(C+C~17>uq>EiQkBt0}ry0ry?L-cqtKhC{L z&%H?>3GJSHyEFGj+^MziC^M>GZt_{K+26jr=b-<>lP}#e+9tCSp}}>R zLmcBTQrAcpXhv(?LEPDlJJ@y3uevJ8?%Shq(~SGa@rqgms=bsgCKehxzQ-D*K7?dp zA5O>|i>z8}+-gE@pGxh+De~Tlj?VS!KxgQ-GnE57`g4mjGPClsCTBzK^Ie6xZH=qz z>pKRAN*lr*$<3x%d-vMzj`Cne=6!D&J2>BDS##c9v-_H7yDGgM*UVmZ#T8fWx+#*K z@$BH9+S0vK6R&!ek})%2m;uPgLQkABrpUvPk*%|&>3eA#yI~nS>`Q|TRqdF%tvyX) zTA`@#x9IP(>R%eji8Rd4-3J%s3opE{s(vXcJYI)!jImEI3=+!3H{&;XTj1<*EpU~` z4LHK$JR1Qr;dAP~+wne+eP$6O=jY!_Q2-e#@Hp1<;&F4cv$-jPdfb>uGuFGjT-@Azhw)5M*Oa6A7-qAR30c398aG!}Fxz$@U1@pq+2@$gZt(;K zbv|GL`zW06FVX8}zEiK8l5A9+n6^kw(pJ3ksi?dG0Hvrrh!>CGhl+~yFsIaGy-YUL zkszYWDn70x{r#-adFrXeW!Ak`8@kb}zVsO1zH=MsJHdMT+qmyKwrYc&H>#zt8KZHv zVJseekVQ8Q7_1x=N})stB6l4e#H!>Q9LS83mm@|FuQCsSdjnpmtXv+vMa7Y4TyT3* zJSC-=G+Q|XTSBvu^)EjVuC|jLIU|+%xIP%ZB=PzKC0l3rNw#D1@392GkqpRvueqA^ zxgYD`K9@4}yhqYAYiB^uO#5)|Cp`=N9>Rey?qN01#oCzPb$%~6-3G(p%GmZ)jhW%Z zJ*h#Vrxg%sm)MGpyG_kMD4Wof7m;vv^g53CK(q^|PvD)z%HLbsI^7=WR_CJXoUU8# zoG*aer&~*ZXiZk|R2Fp%YRynbza&nzXAW;3|eXT{3i{a)Ly%3C#7js+aNaOvRdAbaMYc z_TD?duA=N8KQnW7Z+17?zI(T1cklMzv)S})l1(q9C)5y;R1_5~h_B{V@&aPVY`&k*%$#!X-Q6UL{_*=6xI5>bd(O-=&ph+=nE`Gy z@LZ*t=D5SXIyHV2LbVF{@OFiXPVO?1_fc6{*=gs=^7GQNbL=WDtm6{rZW(;v7H_GR z7&C9);G5y)SsGy%zm}5nnx;{`PD|77&A<=l?Hj}s*KeJz*Kf7aw>!pal%AYCruAE8 zPO9#WQK#A4#|`_KX`Q2MmNtkv=OV_DC|Ecrjq-B>xog08bqTbw>>X8TAhx>jw^ zFnL+T15&G@-C=FKgCG^vA+Ntlb26+3n9~)CD51^hVZ^Brz!8pdHfjGnZBxhLLp?(n zzS4x@b;7B)IBn&M1zX;nGx!H_=l6GBa-m+3wqSbcb{%9i}%faoJC>pUm3bzul0&s^z+xe$sdF> z5A@Iu{1iKnaeuHOW8VQsvZh#YBTmssj(x*DpmC>T-(P}WX88y&j7+1DK#HDziduwO zL_rrqv02kv%y-CgiotSKEaRoR+@wPxYH_|f6gN*=`Dr9zVgNk`9>M^6 zOyj5&I3nLMp0O&14P#5E4n}!HxC<256B^D4Z<}`ET1vGtUY(#0G7th*Q*ZS zs3}9%Z$-nm=hwEr%2kH~vcr+{(ASuAKb?~CarLC3P9lbw$%hOi$MMJ2z1Vv?6~d($ z^waU7`pP4hNe1yjcFAZ<$V=f3f85f`cW&Ic@*VFuFA)>xt&~lDOPBV26eFL63wdM^ zxS%{`oE@u+%Iqp>GW+nUh*mfT6cCPJ=s6P(yeF?{G{VE2-&cU9j6PfdR$GT254qma zi?aJ(wH(p8GcC6)FWOa;(Vkb59TBNQyUbZ5Q77VF_Lm+4M1V@f|MGDmVT zswN)KguoykpG4S6N>dV-3mm6OT#kmN?y+Hao}O_MLrov1mY z?6&30Z(Cl&F+TTPj&V)U*Mz~eq&Z1^#K3IYUqJQI~nnL`oSJ`xZ5UAP%yd6 zJSLEuPGV%5Tj0b8=*wlEHpId)dhQ)0^zrZE12uVuW_n|OSwl%dRbhT*rgyGp!aaQ^ zJWtl_JmsoYJZV!O249?!lbw^9ot3W*IoU^}Eyuf;!?QQza211hOULcKQ#U+Z1(v}v z;HuuoRXrG#as1Ow#1*f@VMn`74N5A9$HU;ayxxoPxEiLplj&YovI7K*~wn zInducQjL%2YmyDc^OuUd;lO!|%crr)2A6nZdUwD_(uVm}WzN)#*O@G92w(_oers`s z=T{X7(5(DP4B>McnGn84wsC~$102j$N($kxKPfABY(q{>CRSI9+LYYJ;yIHF8zxls zoQ$;spP1R5nGQDyIspCWOBvYa@CSYje-2{!C0ze4z6@Y^X5iWS%s?H(Ph4kfnL*G$ zD=@SgKyO$?a{!)`$mPOjK|d2DUFdGuXO!_4ofs=t1G6~z4=}+w#$*22b}rhU1Kehj zeiPS#Ch@Ky9O;GZi8flJwCjzm;d)OdXEN40V)*`VgCD*^oOZ?IT+hox2Y*a#{Q|s+ zYhAh&ZH&8tpH$bgG0c71D(1dttC;(r_3vMex5(qnefm+%^PW!>`!nb>zEbrWTmov3 z{*Dr7HZq-zT7qZ8CHV*~Ne2TGP8K6dgRRbjKt2ss@92TiFzQ5n#2krkLcVeO##Qc` zjigpIHJ9p^qf zGJE{WQ`~iHPWkl<>z1u?SFb$f$DdiY_}W`;xmIixtFOBD+N%a182mM9f_9Um?bQ2# z3&<;LU-keeT%)ct0KLepH+D%nb&`WHjDkG+b+x3!wuMYn%+nIq*y$49LZdvA(u?(+ zqX!G8cCwRWi34^W`}N{ucSh28>EPmp(WNNO+91`iC#}DNTnFEmrxf+KPF<-Nk@wVh zOe@2*7-AKpvU_`Gv4@165vLDTt8186Fbv*BaV10 zzzI%xO5$8$4KkgRTz43DkV3WGp@Nzle$cn|*70j`gRXUQR`S_Yt*w|7)Vx))o|YMJ zCM9Wp+fnmH}#@WSv-I3+WGqumWQL?`aEI1H^?{uhP_Ha-`uMt!?~FL zrT9Ffqp#5q=-UgMkZ8Hb1j`IVyjrCs@s6|)0}KHumK;>1iljcgQlzi^jJFTN9QcR; zd!lbif7d!Dd`+H?iRTtzKg?fh9~3gp(Z_VicaCY9b1etf_o0<}F%T=k16CHqY4rG& z>47p?)>QcmKqkiJgKc6tlP!ySmv_;pFU;00*mDGCG|4AX}OD+cw=A2yVtlFStSg?|6xq71W?W^K$j+)9C!hS{1BI16tEN?^myK_& zTyt{!nu|}UDLr@R&G*3eSv@X2UF-hD!Py!IJcs_B^v;d50er?h>W9Zc5aOU-;n;`< zalq&sabPycp^?O@3LsyIiLv-jOtfiETZ3=J1f4ffn36ETRIfJd(dXxpjDU;$wKEoX z%x*0_Dl!(W&A9g3NYCnX=OrUV(?tw96f;2=ort9(ha%PKb7p9nL6tkmp-9^0FRZmQ zhl0Agak?>Ck8Tp=P$=e5aK!-knl$M{CWoTj&!JFpITU<~ITSp%Lg!G7kU}wj_#6tF zIR$cN{ffD{t@&5(FYaE{onC+>iZPL?Qwm$>bfk_>X=-1!V#Ug-J(*1<^@B(1>*~51 znwlD0+EPb-@xZ#vmm`g0pbsZVE?Jr#p@SbG;7|x zS>PF1eW>@G8DsJ&;@2-E>Do!gPuRi0;JgvdWZpWDqEvL33T&E+G>RX8?ZkC6Q)Zoz z)4%qVhcNH&9{j5qC8{UQJA57e9Jh&C;tBa>P-8JDOxy0>N*^z|wZy0JBz?+)k;kR! zqj4g(8P7ai(UT2krb(g%Ry_O!q+aZi&rmWWHt~CU_mX_|PuRFARkSReGiM<`&DGV- z{N#)tor8~OY`O;e1fc7hlx60@hW^1YIG? z$rkPdRBQBs@kOPtm=HxJA&tjb^TeD;J(gE?7kp}4>Pat2r-?wIDHP7ET-f_Rlr|m5B93y zI#e#x@ON9u65ZmF!PhS>tE`MwSC1N>T2R?mK5b@C)B5j<`4cxo;-r>TR2Ajt#)`A1 zcC^fhE!24W0LK0kfVYX~4`qV!wuInKnz=T@0(cL=Nu6oeI5$vC_2I@+eB>^w%S0h3 zFSbh4I*6m!?_&uy?p5I)nV4QJZ2ExZB|4kQ?A^umCx1OLmJ^GTy4vdUvWCp&oT*(C z=eNbGx)#suotD?0*-%|qzqnOal||j+(xQT@;+n43nx=?KZE5J7+)z?gI5t+6+S?5R zxC!I(bb3W4k&|Us zXM0VGs;b6`Y)hZ`=&A`5rnr}0Ddx``_ou&g%o7v747#}!aU}_7q~?#bMacQE#`nZmCXs_~)t+3yT%qqVtDEtD%QYbOyK$H6ROjg8g#h*^tgHO!gZo)v9s>BJ6kv8g>1W_MIoBiBy^5I2*x0w3?z#H^M-HG(^Sgi=9qHu^80QZFS2hywbT309S3K#a&8B%9>;tu zhBnp7H44EQww?hQXjDnMe$XbhBEw-oe{yo;K+!U1L2Gq+ZU6M?{k7#aEeqz%#=p9HNWS`dF>7jDO>3H) z-W+QlHx7T&-L%%4wy7_TOB;v(A1p5&U0SaHXj=J@<&UHlNSXqhq09yMVS2GPd4O#z zb~MFvZN5hzpmg*U*_P~18!YmbyPf?}#P#f)yQ8#imw zjF}6rwL~L3LL^fdn+B3uyt}Fz(?|c&vh&E_#S>!RammgyKbzE!C;XQ~$xhgbx^IE( zME-+6X#bS#Og*aT#60*QCc_uSIq1{yvUBJ@j3hg`jx|ws29lHW?<4s02yi))>y2T3+TR z(!^mSXyl(y|K;Q(gl0 z#{m1$)=0IwdO%LSh^vD~5LrREg>g1k&Unz*FA#6Wx&{TNAJ4%LH1d5QBpf0bj6||9H*O+0&=b=BKuzqLv@e9341$#qyI+UcTbwuCC^(&F0U#%=9#x zqxi#d6wWy7KJNB>T?#C3U+6pniCrV2>hI);UT+qd1B+ZF(9~BpS@T5U+zYz68*nypFNg93JJj;?KpV8&P{V0V%uKvwJPpADO2*te0K3?Q zi>3O4d*-oQ6at3)YjB5AO05_NW~Vs^!S*cZn0MrP?YK$V?W3z>WtBCR)zz_z@{;!A zrrhx@+whLO$Ly0?+fs=OwG@5>WMwEo@+2Xdf_5V+N|iPZ<1DH z=hldrd-rwA+uNtAUE+bs&;O#MZ|D8@YnuHG#^gc37vuVZ%mf*mz&(OKF(^4{Gv9@XbVxXzC zs<6DORF#zHrj2@{dv9UM_^$4m8m|ulZqBu%%hv#>Vd&p3d$e^*f3&8ha80bct|T=z zqp`Ym^6bfFH8o}Uh=F-iYWjMbG7B3TT8c$}Y(hu(%#MoEjM57IhjaHcppSdek5SHa z3aNN|G|5w8O>9tw@f?$Hm>o7b;t;Ui1=!Q{>`Qw8ba9`&vTx7$!QX-(6ITyl-ci2x z7<+WQ(`(EQ5Dx>JN%D1QtTmuLdi=&%R}Zb0c72cbXgjJj_9(B6d<`?;qs!Msn{?WY zhS=DeX%i<-s~H<>m@%!aqN0qSimIv#e8fO+Q&qz#H?6L$uB!`w(%exERZYE56=T=7 zV*Tfq(r8+=RR7U*^8oNoT=1+z*rtUrv1>K3JfAU`kHcBz8uN^wVOgx!D#|QJiU1R& zdAgknwL=h`8%IEe%~HLKQM^g_!t^DhJ*qL3q&(u;(!99a(u^gekA+Exau#~ir^G5N zXOGRNTYAiPsqroSGj{1PLNxW5WGE)ZL*j%HWoXccK;<=iRAZjxxP zs;iD$ex@E>d{Ta5Tpk8J97TRwP8sw*h?rA;;=D{f$>Zgx@#%);Cw;n%Z*X%3&!w9n zKl?)Bll&WF@*v>q_vGil zYM+iIKU0q;K8b(O$Gzx>woiwVpFt0-{M5OG9BZR@D`pNRNKa8}q$llD;_3m+yW^3c zL7U+*_v9xmR85b^DnF0aK0QwPnR=Y!6Lj+c@J(DCoBSLK3q~*SRdC-NV%p`0TRRMx9SN47vSsN;%oC!*FkCbuX1$lr(jeXadHSO5NHyuUc^eeZYH zKYxh&PxRi`zw`YiaqoM-57+)l-uuI}pZt4#{oe0ce*aF0Yv22Qxb{zuYoGXA8uz~U z`*8R@#lAmW{inv&pNyaQ-;>)<{(X5IeBSTF!FL+l$DFTlUX@eC&!C5tK~uz9^T(o*t++SA0x zzi{Z$M-Lr(Z+Uff`FpP!*WEqtnnQ=4!43+Liv{TCMDa2@*MaRZ7I4*Kt+UWsq}fv~ zfT#ixEIVyE)9-OV*Jt%RL62wh#!N@|*g@}*dxhThX2IbDlBcj|YWaWz4|*N2v7YM}a{Y0vg+o{Q)C{sJT!Be$Tw7kEKtSG({+ zUs%hIBqHFeLg+)Ix`z+17!jZmd`4;A=x8)npOsr!R$O0IT%A#v7R{-_*Rdt_A8o9y zZRDpgC#Mjf!M@sB{QqiUZZy`Ao$b1*wb9Dz-0bM+teUJGHzlRIxU!*d@}$1LNt633 z>MP+Q(0`U}J~S|J2nYeaaUACBacIFfyqD)_PIvlrhwITjjsnYZKK7f5?B)>m>v^L- z5&O+}DZoBGa`d2tu}>VgXnZ#KjbM#2uKDhGlv9osx4p1HX@f>>@^i1hx!8#K{Cr*C z$G+7R&6oK__0uLb<>$%#g8IoLqx1XEJX2BeOgsLazq`4mrFl1SSna$H9CFU)fryb? zndwI!xib+b9?M|Yc$1{MF)G&lIN+hY#N@D8c0bEhLT7F61^iP;3mMg40sDi#t5Gx9t`cV zIXdF_KJ$kw^cAVDQD4<1U?)2K1hwR4uyjSjEPMl4vp8~xm~0OdZ=7o`=;U*9ro~k= zc4hqr-ALZ9tW3}&u2{xw+3VxVF186M#-6*QDTZSm;EE1~OjK}meDOW=a*hv9LGx*H{EiM9nu~G*Ua2!{D$KZGx zucuH9or$0`EQx290WI2)Mu6Y26g7V8RO|N?$ggA)BK_oCqmZF7LuVFgkH2_)&ro2@ z|Gmln-i)#XpC&vzj#P*PkgI#UqbRz4Dm3y#pG406aj{!hFEcSAZKsRwNVNLKtiG(; z$>XN1mPK8UG}hNQR>UGzDOouMd3gmnSt(VK*zM7}I#Jx-G;2=N>wSHP4n4Jb!}KYW zr&mQs=M-wx6=r7?Rqd)-aAdd^;7jIV;Ur`;j7a)j^S4`w>h{i&(TMyHJG?elxCr$Y1qo!E2+bHJd#qlyP#@* zSu7Vqx3YBE?5e6*X)IRyQOzP3K&|;%Wm$f4icBdfE~_i698-D1*%hNJ7F=c6q^I9p z>AWgl1T7jo_B*#70Kl`IIU3|@fJZE{BotYExuvLf_%;Mid z;KA@XgDnp$q|7ehVxoSYe1~{D0BPUl)ZvPeo^XcT5coM3{pQMKl)W;NGBXSs-#wx- z!+9>9%beH6D^}i~cK`&GZ$n7g=xVypGi_39fX84?(>IeAn$O(hD&;vdpN*VVxWggh zFO;%|N;$*%OD1Nm(Tll-I`v|sfE+X$*<%n1;zX@wX|V~IuLaC60A_{>H*se(vpSvO zYA_8S<|NI)5(8$ACkr{Aseps>KLW_84N&Dsw^}WaA|p>SoEg;YQ}?yCr%m6#;iQvZ z2a%FmHv_Ic7ME!VRh`IifOqEmC{&;6!(|g<0$c`gAtC^17zrYKC3M761Z5VW16+i} zSI9g}ePU8$Nsi1et)KK2EkE}yU%u~2p~xEP1n ziJ+wZ3AlhAGs1*49gq@_85UB*zq4_d@N=`%kpAT3@hT$mZi7jp(cn@!7h}%<0&~6u zT(raa1Q%?0s~L(=9hn}>AO|l=>eP~P)`l2o7|nVch8jLmW3_E=)Y8TqZRQ4(-LWDE z3J)1XzP_SSLqqNWK=L+E>;Mv~^t~2>s+jSFYhY&cIR%TT$yZBIDWl>$qhV`^T&I8_l4!jv#5bJgtd>e!+ z9r4C;icgJpk=anx=#4HlK4bX}WqWY!Yf)`3%4U?wmLEp`Sh(KsR^5{$F~3AhZe*Kd z2(41j5#sIp7H>G0p~|PWD&0LV5}$;vCX!6kNK|I&P|ItQl9@3hKDKct9Sc4&-Lxe~aXcah&uDMIyuE#7yt!jPHx~Z`S1zW&>#`U8mwVY30#)DR zS|hW<-8elIK&A78EP-Xzr?w>CmxNFomI>(5rmh+Mah6*BJ-A`l?&upSz*j_Lj#>~o zCUtOc0vF+`R8Q6llx8Qr3QXX!`Z=!jp@ylYctJjew+)dLP^}5Gf1KCEjbf%178Dc~ zKLA;vH5&1=bN7|kRMuD4e7fAqg$34Lsf0c~8}vn-H9Nh|cj=><;7rtvl)f3_j3sf# zGFX?Bq{G<$1}N$#5q5`&&@#gSMM*eo;VUhc_o3g$@+H$V(wVrRR%EbOtFKycuT5Wd z1HcYgIc+F=D8IYA67V$w2P9cC83qUqA@S8AS>VP=vOPL(VrK}mRYUMWx8mVR1%28b zrs?xlInBp)zw>S4+S3<@!u7=A=`#t}Q;4`JAzUY+Hi=-Z3O_7;4u$Jh;ChzEbsGp` z3cg`ik|8N`1WY6TW%rxq0u-B!kDQ;QqW#c~{n=PJo0Aett%+6EltnwvKH3OW&(1E( zaT^M9Mn|ja%gdVbnJ`3NK_9F~_p6-$;aIG8)(lJc3yxzfCUGn#g~lQY^+|&>m#@q< zbUzfY7-}pg0sFMi4>&7O&efo=0N0@PegF$`9 zUFnvURMeGZ7i8u($odMX+3`GVJTU(XM{}|>W!TXq@p8rm-j^f^kTkTJ4M=ptZQZA%&x{N6phL`b4uyOcc`lxmJab!ZF%2aqM_aJ+yzqp@;Fp)~P= zQQ!{PjT77HCazEojMF{Has;1ljChTOh&?Hm zl|<}zb!5OdtnH+43}tR}x8PFn4jy4)R*!LajTr9M06=b?Vf0eQDB5v{6Sm7(N5h)m zVOA2N|FehGKED@YE7y6XKa~1uILt}fvkr2HFNi*u7d5?)<=)Hr=!9`Ck{<`>uOvxB zyPz&0;3PC!64zLZr&r^Z3Fwv9CX!J>ms#)xn_V5UfI%+d;|>l4Wi0UpTQGa5jfe z%&|`ZD~UGvP<*xv&l$^mJOC!g?Zur{8lQ_%8u#;J%=6Xy-ZYepbw}smIim+2o1LDN z2rwBtN9Z7rj&>(Ikw@g#m-%@E-$~{ZBQx$d!s!J=a}oNO7^m#RIA=hETIl?i``;|G zU8fU>e4J$v=T4$>yHFsY@}x0M8bZ62I^Z6i9MeuSrpGo9ObjU(qh)e3$UW#-tknnI zkb8Tp%Q({0^mCoKnvJXYSrR=K|BLU>O7;R8UVhm~Zms)=GE9t-XbX*>*Lab@C9%tK>tC!LjJwU!vh z_A%jFyODo25?QH=B& z3vsDR-l#M;n8+B_jh<^Q%5G62e7xX5sWn z7a~NO9j&jdt86OHFC2T`Q3e2XsvIxg7lCCxiKEc#+nBu=1qqz645Vl~G1(rH2zHC@ zfWL%2``U<^z*i=OM#H9+>hWp^PAMV|p!s&EO*8dLVD!eZ$Sp^VW3dERCLWN7l8Zcf zT5^dFxgLPbia3+ZC>W=I!7(8lS<9iwz_W+}j2QbH{7Vob5Zf6KxO7AscY|mF^^{93 zz2^Yj+ZizKbtdTkQb`D6LrIFZMOT1f8%zmq*yR_&1hA4_-{A5KD%zA3USPTgS5%M9 zx-u^FWt^qPTfUYgC3Rx;(Ne|k^`)U0JtDRKEG+xp7!ieDoaH60>$C%9$d0J;y}2f0 z09^+d?C?28l3r6s3WU|}CmV*DsHV~5w+?x>aGhNY(*}a=&?v40Vta7~u6itT3+kX& zKM>W&m3BRi96msZDpuaa)jT7s7bLyPx^)+WNHNb%9D(_n^ofrJ0^z4Efhefa^j8Ax zspk;O*K}yg;6!1Is{naP=uJ{Fj_wn;+p-&q1W>fpK-@a`_a5LlYy*IQj_9Lt^U;8o;0l#!4lf z8n5U?>BX4`y?Iel-L`1K!ju&sojZ7NwfJ@6(?9;P_(1XDWr*B&Ia%0i_5tW}1zh0h z@71VKX9{hDJk?t&qAD+vA-cZvo#~Gsn6Cf!iUv{BF!+YZY8d?S;7bi6YY?ZYV82)A zI-J+m0tOjn_L@C&>@k2WVMV*m04ka#qP;k!sXkI6BK5CytnL!0i=~5~I$d2Jz>ZS$jTM>t+|}0tr|V5NQ{YD7HfEtI2&4nZP_w z`sxr%=ZP8Ti(0@r7Il_+b%w@t3~IzFwSbc=RqIhB5D>wRIcsK%q?7J~NVHb~(kSfE%E_!!gGsmsUliBG!A*R|!RZ8b3e*XHgF3<5qP>6; z1Xz!4PW~DP)j&cE8lR0oTq@`x+i4(CEhY z(*G4p;s^Aw9ez_hZK+(q(j1{3OVXgbX0#-ZIeMG`ZTq8>%;emrM@xj>o>4 znOA1O-81-{t#+K5eMNq?pp_ETJK3u@goglLQ!k*cv@`lc2HfYwp169S?;kwq&1&H+ z$2jurAWbjarki@qQ1p@p#F$Ag^dtwhAjg`fd*v9@G)NPD7_qG| z%>->#8ioQcg^8e9ifL=`Esr>LOLEL$uWbqIZ1Ru%34H2FtP^=3+PxJkY(9X!b2sVz zbNO4p{_O*JlE2{@{s#CQ@VB+#Z^fjwC{SKXTs(#>qBCrq2>^w4t2q#(T$EKlt-O}n~(rQ;TNwJscA z-d|ADRaVoTu`Up{_eYuDm&rC}KEicM)RE2!_O%*g0i z(pyp2x2o;#0ET>LF=%8vG(6xTW;HzE0eg*+f)1*c~4_fC!>t@I(tt04;UR}~r zQ&?EjQPQ+vVrAvT1x;6*CdBVGfSF@kD=J#YX7)$h=d`uYZjV~3piUBMUj*%whm>j@ z7&*&b&UNPtfC%n&J{fXCPm;X`0Iy*W5DUO!-F_5pw#{je_IrT%?U!I*Uhena;Vh#o z*5t8yJi%(R0ZoRtO{NDI(hHg;PpOUJ>HrKp0eCnOKG!8>6ZukQ&eT)__?P{ zp1pfTTl2DW=B~f3wX^Ah6%$YHtE%fid3@`#NGVTuEqxcQNCcN$#ITImq|0KB=hC6-&( zv#7moQE&O|nRN?JZd|Z&%Cv#G^(Boxr43^0ZV?l=?wKfRqMeiGwT?fzzpkq9HK(z`~W3&kcnNuZiKfF;a z!IR?1&z*yKZm0iTTGDenar}c2>4SHPCF&782c28N5aLE#4ttKBV<;)H0h>tg?^lof z{Z-=kAfDej!t>Zmy87V${pN90SP6K0HN4E;pUad@qt}~MO!`llk_rDw&>L__+;Z^~ zBe%>xbx|^sYaZ457!vD|pNmiBl~+Ec|3}aHk9b16=zp*7|FN`du=CHhyK_JPv0WGV z{1NbZ2_Rs*S)64N2w-EN5q#Q_EWPezJqK)4VN=Z}XO+7fr{IB9lxg^+E@o54@yd!C3tWWA(0oBP=|FA`A;g-u&}Gx&K9wR32zx zwoe24q!gOj1}9utK`LNLZ#_%k2*KPP!ra)fSO1} zJXzZ8mmN^k7~CT@k*ac0<|I%RM>L+Y`upXL-{`}CQ@-Io{|&~_4+6%W|6v%z)aJqH zQ{4Xj@gN%Brg>$GQ%c^Jsy7ybtV?MRs0;In)&*vf*f|I*VoCquTP$DH{B!7cJ#HHR z?#}&(u{W9Kk;l+(o^JP`xN-2$`)#xSyE}p5p4yE~7$-5d80{yDzgotT3| z!<^Wze%EUkLk3w0S)|{^F@E0oI<~*o$H4R4Acp!y%is;_k;Au8K8ke8$6?>4k)rLy zgm({6W;zFtsa;4yw0m$rrt@~&EZ;q*b`#!Zvm7Jzn6f$%S2A&Y(Q=h&=^wm7ERkm& z#xWvV9~j}?5T1i~cgi`3kvM7ErOZFJcGLF{ZjWzw_;(ZA)$e-kQa90K6mcpjBC)$N zbrVQw_yZ$DjIq{KFv`zETfs16B5VC-$T}kQQ|db6$2zW#1Dw!yKBNC>PIH{U(}TkW zwUAx{Jtd6}Pd)zhyl5JDzQ4*sg$Wm~vz|@(K2!;~(f}9zZmwUaF!k9Q4_1+u$u%&# zrk~WyhU}*YBj>-NX%8+9)t{lMnJVI78m^xPSI5v#Vh+q<_Ba;-0dBxc7M_^jR~TG`o~%{ry#rpW&B7)RAKQ#jRt8 zt^=Jx9l(vQgL5Oz0S$!w=!Apx&7QChhFt*}XaM||@MwtA_g7hv4zE0X3%R(St5!eP zbtceNe4R$ewsrcwtkT?>bY&z%xX$CjSNeWiQ3zkAuPt$X)v*F=cVZ_}M&i<7Vp2Qi zBpRZ_nmuUh3d5G>AVL@FZ$iD8>`WK6fpFar*a;f)8;Qq&rnoryrpLO5Czb?!h*gGR z2QeeCWnl&jHc0RW+#4UQ8z7!MY+cyph8uU%)J_E+A{q}=Qb36EcCa@JZm_q|m}^}n z&6$;qTf34jY3Cf^fc6Fv&W32+6C(s)`&*5i*W47%?W|&ZkoFemw!P`(%RcW4<2-23 z0-$kj2G_L3!;uwFVz)X)i&ntcwJI`?fSz?*BjHO|4VUpq_@a^&5C0O(^M85_{;a9{ zKQab4LJsQY{sUu>uC-C>XK(Wum=1);!0J#DGjE5SQvnUxd8;P|JY^VYGbz9gK|j=W zL_rVPD)-a9gDG=rhq58w~!L;JFGx$98D)NV0`m5}G@Y}-tLtcg# z4-Hoxv}GBF?i^EX$TYn9XXb=yugCUl@CMEa^z6X?g!V?lUC>^b=b5&K#GU4NzMg4y z3C&ag#j#0;2K=8G8@4m}XXu{)p|R2JZRcG1_l=FkU3hG)o|nP%MnW<2FkO!{Z@8o{ z!xO9l@1;U%7sQaO?w%XWuC9l^G^q) zY~}gza|y@#d7^lTHqDS3DHCpUc$_Bn5jVWKdo-My;Y*M|8mGgzcl=|h>G3FIi1rfq z5f7s<%&kOtxQS8{CQ z+aIp)0WsSw5-gKFJ`=)n zSa0(A|J%!tbq|e}rd6B&1V0Yi9F89&ZXS|3o^J&51f)*8m>z7#iPBMqwVLp(rKTPp zHR&vb?m5QBBhZ4h79GtYRx?8E!NeA7;U8c4{`ZR(d>C^=ng!M#97LUrw?!E3AF@v4 z5g=I`s7-V$xlV=xP^U;gay^ubr*)mRl!itV1B|EuIr22V=;`Og52AILCN?G{pB_Af z{}8v6&Oy|*^SJ7Q|Aa>tGnxnAx?hze5nFr`Hrb6Du8O$ml@}rAx>xGyo;3;GnXyS7 zpS)3Hj4(Y3U&W1(gTz_u5IFPt6^t_nC>8vfu<`R`fB$|Hn@q{q@kuZz_`v{M8fy&c z;%OL^Aj*~uCq4Ow&6!Am)`tXPJ&b6YAgri&3F-y9*RZBQy8^dL^&_KZC=(m>pyrr z{i5YyKj5bqM|Z*VS$Vh#*+dbv!k8&r;fW7{M*touYo+&iRyv&lbU+~LcsN_ucs`gL zJ=}!+0rCDOFZdbqWm()*iO&Kx2Tf2wtDc}S(gt9)xbZYWgJPGN%pB2F{B9y$JfDf; zi?uU2Mj?XS7NY|~hHC*<&6Z%jwV<&Z0sVtC;L!yg4**Sr7B$^qp_*^Q=@D>sD-?~0 zxT6(s1ay4dQjUo*JP*RT0Y252G?jyHtevarhFN(wSC+nK`uB|A*y9Kx`lh&p;01;E z9~@jnFRj+HG~ICi0al9}@1#=0zsyPmK&{YN;)oiD64$+$ku^S2{M9YN0kdd7T6-R? zt?ZYtqZ7mp=ev#_XxxP8CiU3p24Wi)I!H!GBtSQ^<}kL)MZK76oc|g($iVk-L%(p~ z`%J`*(wLz#P{kQFxX76Oz|Km;n{=HPH=aZ`EJUv%7^R`c$zh;&MvfVLrUnS^fRW|8 zT90$F4WoAGXxXBJn(pO;hqJ$fE?^3W;tLs0=K%oi=Pc9jo^i~y<3}Ad+p**W#$$Pe z=8+jrFL|VwJwZ~jV-1Q2#=LC7c=@8agW;Bp>KcxD@Sx7;NXfUZf6$8NZ@Qf{leLiU zN9ZYBi6oY3dJO^krqH(xv`H*_BgDlh;8qrk)MUv|7|!=OwpJwrI7k#eESxgvv(_aw zy=k0=uHzH51zL2bz6-kNKhoI?}W@KPAau>-l_ z$xL8Y-p)A=(ZD_+C;=T`U+VfYqbnFxm6S#JFR5ZST*W_rxRiRkfyZP%$uiFQ7ui;5s95{F}jo=7Pn}V^TwlI zGn&M(=hZYy6UNbq6vH>Ijnf@aFK|n~iF!~obz^T<8ku5xI*L8>_$TI~=AZ{X@0<}Z zz;8&tX#Bc9e$5Oql_5)qZPMgUl)_${gdvzUX3eXiT@6JgW)Gl9VQ)3r?xge?yut4> z$J}wfz>s6-PBLF$+vGq@EFvsSlOeB1)-T51jI-qzR*HxDh7nqZ zxQ*B{EJ~Aj96Yf7v*O|5STMJEq_N=1R)oU9$9+h&$sLT3)sGB^=?B-=Q^CvwGp)^( za9lIrK!22bIY@9mk85g+^w#KxiMVKn%!H_5nQKN5?H~6D=4kAV)|Ht3QwmUN7QRP4 z`tnDXy04u<;StsZ2Y;4I6uGFxxQ)IjT54 zoGHfzIU=3!S5>hpyndRlmrbxrGY_?#(shmr>dZu)%;&Xh4RrD_szkzQPPbSRf>EyZ zp%kZ~O6n6cO>4RhFaR&7mfRqmQ%i$K_X8P4OSJDGu^&MW0p$Vv!K_`p!yKez%fn?8 z+P?Affzd`>KfE?V{m_l*+zm)O<{E=;hUf<^aCiqyH#i>{2SYz1P9Vw6rpDG^YyZRaDnuKRy)$3=j87%Ui5iJSNcyJ-Y8iq?eJKS^6{^JKB zxa2v*JSWfjM`!3cl|RgLD*vtP!p{wlH+q0Pe2mff1>O35Apz544!`N;v;#Ndt4DcW ze06YI`1yTDdw$>G)#2w`j`lnVF7&+f@<`8v(qF~{q5eAWKg#p(#{=Q#7aryLg?PX| zkGTLk^q%*5M}SWOD#A4Od*C=tM-C4DuPjnO1%*ek$fX@?Eazg!!gbCTXCL=y47?-n1x8Fin;+1K9`Kfo@` zzZZW|A3h+?5q}kb<8%|}K%l@-(k0XoUE;tSUDntVhmPp)Gi^D~mM7VAvn{vU@?u+F zW6K+Cd6z9eZOhNv@=05M-?$y7}#jlI?y_@js z2lc&bCqp#qd!NU1Pw1;7c#i5Gk3Y{4Tl5tSCh;(?JO_g@;HZXD-7iH+bFva;J<4vB ztth!g9luYv-)Ey-i0^AqE<<^${l39|2VFQjaQ_07dr@9uzh7m)UyJf4+`j|m2T|T* zzdvBVgHD{!+FzenwN z9OEIXalaj9Bg!88eX{*N1La)YUx9Kl$`kGPb@uxJ$}PCR59K*1FG9HwX0?Kco{1VD<+wb3lFc#y+n@thT9tMnMerca9_gpgxebKnQ&XfJ@cQeM?6=Ml~usc zQ%M(MLS{P8Hc!udD(ONT^d2AF>0S9X?DQTV^soFHM9h2qIA2`rz2`S0Zt!jm{?cnP zXy*p+Rx*4+%gOKsAP128E&iKdRF9aU@Ve!fOUy8MJ@U&HZ`1P&{>}COyWtNG|oy^T7}V^{?m(W|N-!@eyMZ2m~~bF{*nBhdk zWPNott}fG8e}anfn!dUhR}bl{0bH%qS2?)in)Sz=r$G;2(^r?^>H>W=8&}iybKgUK z-^P{MO*jkUT;Wrhjh!!1PDj~|vL2t=xSoiz6`wV@-hgr;KBwY(GsrbQnAU@*2Ls zfct+!`7@L+pnL`8&rtsFe^rzr)w8hr@&8#B<%<>3Xazq{C0&SVo{aQmw0|Lfs7FyHL`7w@SKkp}^IU?z>ge z;oYHT!rTJnDCmeNzZu;`p)7^$=l4pK^(ebhwxXP9zfZT{XQNz*`)g1xLwTzGzQKN{ z{;~u2FF?5$l;6`&hP4@^-MohTKf~{T`mc8L|6lFq?)Ivxc7C2px)5_c@#>3r|3du4dwg)G zcjedcPVc=yjR;=M@g6_U7w3EL`OSzMyjz2>c`XL*_^Qu4y{G&agO+_r{r3Wpg%9a} z>P7X4843?zznnar7W3cdHI9bEkB#lAd>f03kBKq|&O}cc6cyMs@F+1W#L(mzFUqCk zqYPm|4kBjApl-rGSd4<2d>}?ag^7uI&xc~;UNkp|iG|{(Y{8484HK6%Q8e9#iP_?L zoLIv+rk{V|#hfJp)iGy@-N^`{iNl!~GV73j9n7(BWVV>MhQQ5c=v-%Wgmw#pTol+1 z2sJfIufrZlsL;ldJ(&DGb(kK&95BYgab@G+9!zpAg6T{uK6i+I`FqWH@k9a%Eb2O3 zzh7P*$Z5PrztOP@e1(o#na<)Q5Iw&KKvZ|!}kH`YZz4(&gF8WcwYV(+d^JKq}jb8FGA?8y=aC)z!VRiiVMB# zFfX#aJ12M-9lSUPKP}Z4$U1n5Dafh%L|oLA)?UOsQXU2-qM_(}u12y@UCpL(oGP6rFD>GYT;V<&6tO%kL?2doIak^7p zBp#Jt!4UPcKbFqm#cK!Rzl=M21egn> z@@(-{;C&4IomlyjfekIo^c2d#JkKF*M^K-at&IS*&U7t+wwPB{7pflzg@nF6TTxO{ zfzNHWlxLUZXXckA{2`xtSS}EU6mq=K&IDr0<1-5FV{VbK!&KZ3Xf(z=;&lm1J@(J7 z?~T6Fv1;~seB^?{`z!Cj2M=Q`j>!4qLkgMuZ7lx9E1+or-+;YX7M(i)1sYFXdZ*5_uyI^!~UNk4WC@*((L3gCE zwpXqwZ|#W|R^^S&O-suin^#pB?P)DXAFq%r#1GJJjuT_M=5+G-cDYE!G*`!sPc>8G zZswVgzTJIAW9Kzw#72*fWi-qiThu34w02C4p6yEMo*kXo(Mp|;wPK-aD0~`1yp2e;#2St{W#elapEw}%y_s`A594!nt zxfRVtURhdEQHqc3%c*E8E^eyG@ygD!lERWQ{RhoqupgD<0rTUy;%SO!QW$!tDUyb` zB!FcRd!Xs=1X7uy2Yb=(-Feo#*r6|QW(>NLX}a)~k^x;q$6#Z*$e+wKru>TO&& zWwX~Eue9A#Z6$kZ2k-Fu^3#(3F1aL zUzA7%mA}UO(>O1DUMx8OPQU{i4?DaIZceN3T<=|E>I+P5Y#jR5)W{}TkD7(^!y}#I zUZiiAI^&o%Z8#_Jn~hAE{Fi1#bZ&PA9%#o$oys(AuutF;0|sKyXMbqw#D=h-)b2>s zBtT?JF`=49sw#v&dtIw@yc(ydNF(W4nC!D6b>zAq2s#rlRKOL~_ z11vK@v4sxy8p7#2`os~=i_l&}In%K9K#p!3r?}vFxn7JPx244aB)Aa2Zks-7V(N@Y zeQPweB)g)ZA-(n4hK8D{rCE`@qMFkDNI`ZPG~?c0 zd69E1WM0Hw>fie+p6i30inz=Cdt2n$&IcfKBJOhk-hR2j`6}c<#9iUvdtT0Wc7g9k z+?D>l8*r~p-&^J1J6)dO+y(v`aaV`#t#R%GFO0ZrLibkSo`ZX9{d*_L70!M7-iiLb zYh|Ca7xO>jo@DMJNA*ED!&!$mGMx3m>L^mqbC6Yy+z%t)axwO_9|gW-slgsFT)~Z;6y`G= zTcAo%cYZ2U*n7Gc7uTJ0AgBj<~1!_}Ft~z4M+U_h34WVZAt~ zMQNrRfuiyrDk>+NHGu1mlsO+c@@>eZDm<4;yir*em<=x4mN*(QkZs&28?vp-h_)V2 z+OQw%j?|%^AAqpyOg$;Oo@~@5QI`YcQOHnn5!6!ZGQ8}N@wDc zFQSL+V+4H!=op`ZI960bd%oZPCgVI(~zNE!+k_BS6RM;=VUO7&aIRlVVN!-RTl z0bfwmSXtRvRM0-ExPC%K=c+k!eraQ8VKi2QQ<-AXqOOMG3!?W|LGJx??MF#DiR~*W z4WfQReetNE{YbQa!mV>wb;|k04P8ZE`-Pp2rJ|(j{;1#n`RIQIPI5ZN{*MI}m1Fb^ zH2Eb%5}HdxL3HtHc7AOd0im&L_fKqeE0718!itZ39gPAKMJrU;s?`YM1ao4bXk)C zjoO6CvO54ExY9IwI(DXZ^;o6jyeuoJtbp=F5!oeI4}w_&D3i8Q4BR%_ zx}X}AW2UTbX}V_|_&ls_XVpa)#dfyUjjsh8%g=8vD{IcrZ;wQ4$II_+s#>$AYEw~R zWn*r%tUNcjyeyjASXrpy8vKWx4H_Z6j)Vh>zGNJvYvT;I3Q&OmDUJsJfnoTLL4wA9 zvyTJ`dYtVXR2O3ss6&Ro6S52pAiPJU1F z<86#ZCMQmaYuSZIfU`uqArK#H=Z7VV1F#q zFaK#p{yAQpiYiaX1)UwN5-vDx>W62M?|xI6A@>{oZz>;ah%QDurOuim{BKqZLe*?B z+z{YcYVIoMPF~oi@rLFio(QB05e6lGLMM9aRDLjjgDX_8eRY3?ukN$JoZhQwIA5Kw zs2c2m$rx22-bMFE&qYW7+?(*veGc`GtZ{d4NZ zz(4ml__FQ=zX5X&`OUPJhLmT+xaHKnzVQOToD!)2lc*oN^@Qrz`u*v8=rEWG zXv3X5^-uK6`8YJ*IPLjU!fyie;ob5u+PEKI(!@djEC;p94TN@v5+zB+W?%$*8cuzAgv*YPfKLPo7G z`gma+u)KJCJ$S$Kyjxxw=jEdR?w8_DcOA%s{=4-@_unC%m|&+sBX`j&?7d_Yx85*WIM{`E#~=E<3OETz4<#ol`t`tJfK?40nn?lixe*Ft4yn6(qm*MA_2@tEZi) zF>f6x>!1?ZzKlX&=s*dyqT&1@>RwO(!HB|kebf1v+YUNl{+i{QNwoIQn#^wwZ89Hc zyT@-Tzd5w2?6+G3+*ZIE#SRrluNrMg`8ha4C=Uv?k~a$c!_zV2u^CM?+*kSL@aiFq zNf!ok;=#m=FW@9ODxTE%8ekdF5-|$Vu_zSr z@khwDK&^wu94LH|#C{F0?VUYDKh*=ts^6o@JvO%A5$ATd8}pX7@oe;3OAR9|G{?0Q z@=h~f6;uV@dArSKwQB^kS=-G=oIBl4$ROIygj3ti&uP0^!$|`g7k7GhPS+RzvPn(b zG0CJ>myKvrYoEivhS#^t+;($DFtk&<`xBuYW z;f}=^4rOnTq@Q54{}p>%U2qh8`!&!O=fFrf_^0e`jsAFh`!(mF+o0|3xuD`~?3t2* z91?_en>86*%lkQNdDEh%^-Y_b*0*P42ln`}S=(-d$ND^Lo3=J+RJ~&7{!DM~Cuod- zDt^kp-PZRj!&%>kC%*=sT&L~tGXddH_BSAW4#xA*4ROPwFC5Vje>+TXw=S9gdZssg z>wh^?IPmHmHFuDokr#GTe3H!Di;g0mKnU>nlVE(`3pth3FtR zDvCs>!8fLpIU)#>Cq#~!Y=m^9Hw$XV=FFQ~-d0(VIwhsPtgNQ3dq%9Nu&@Z93&s1y zeS>SORbEB&*zvP+#@6H&mey8|9aUQ|H(XbglaW(ch(AT>pHnBEfmLDtAt-LI1?<#T zd(C}{CfRFZW`@0XL(in(dY!$WiqYC?uOrAq-)FB!;r>(hI?d^JeqgUNoMM4Mkp^M3 zlPfCh^%&;{(P^(Uof5$u8{u*{nv=wf_8RAT${u@-v$*96_F6epkr!d zsZODM*j`5vSA5!Dk8%nX{S9m@%{fz_)yV4%r%Q1U6<&{aYTRObJx0XbsrEY48JFU< zo9FCFd9S&T&ef!**lU67Mtd!ty41z?S~*pzyY02>j7z=QUZ*%ssTMZisU!7Gdq3hd zMXK%fC}&(`@vI#?&)cg*sLA9cDBW4Y~32uKkSa}8rVIs>)e5j zZTxlV)`54#7Vp@;WBI_Qy<6As>T2ug=$Xu4_=msp$5#96>VaLmH}BXU(@%wd_iDN? zwt07KeQeLJ^&1Dat>3jJw&P46h88ruY4h$q1G@$`#x`${ZCk(nyx7@$cWvIiar1^f zsC9R2-;P~dH}2cKaiDGHj;$LB*2eYcE#ACkAU5NSGd6GE(h^&`e(%=Ux$C#bHV*8H zEn2^RJ8o?rK%M&L@*TUkt=~GZ{j5EEc1~(<-?wjH+Zm{6o35vA!;WpCA2y${jdxa_ z1&HVD*uE#WV#k?#_O0JFz%FdvykTJbZos*BI~&?_)}*0%Y7gA3U2uDK!ucPALwcsO7$u+Dh3DFEJp+?) zE6U)F-TK>rv)dWK-E;AGBcAl$U5cj%oOj@v#i(UFKFjfa6BdnZMJRk1T5Cgl9ZnCn zYVqFjuDo}>H(L|lU9DT(4cJ*TA@}MsH6_09)$iT^1m4*UxMP4MhQ9AYxe;}4(~xb! zy&cY(`0Ksr;h_chcIjU5?L8U__L{X34!*w~SdBSnqo2goZv46#M$I1lC9G_7AFg%* z3mb8@S-0B;eDBbAJZx=5P3Hk~o8kN(z&kUXGw>f<*#c1#g9mTD#$*h5XPYry&n}dU z@SS(eQ=9R&firk=IojH-VdV&I*Q37&qdW;FbUR?;e{E>ZK(r0zcGOOI+R&37c$4&! z{KjUyv(0~IC7v=ko}+6Zg{{EVnRs&_Y9k)KQP_$nH)vQ$OT_11-6rXZ<8zjNb_K>` z5x$=QSP6H`32I&xR|ET=NF$wqjGvejKo;Mw2hDno)6JSr&%j^8$&n%)hGOPmJeKL} zJ-}Kl0Ux{37moc-ys;ZCl1{gxXS ztA>E%!a6xx#0cve`5D5&dB0naB}aHW+S~?;F=KqbF3FSll$lLKKd~N97>Xv8ykftI zmw2eimsl$)b*OjZYiAEO0j8OHR&Bx?96^J%S-=Uuk^hlSiCd24HncEQ%|k)MUM0hA zW)ryvvB+5#(_^ti_iH!waHB{a+2c&{bt!X8|8+mly8`>QJN-hzI-i4GujXNzED)|p z!5IOIz(STdCqT|G1;H-I1YGH?!qiyftaVOwPI69mPQgwEHSioF>Cb6!8W9<92J3*D z3PPng=z?Z74orGH1j7VGYbSwSPk~=95Bq10f={3Tz9J-z!A?LZ%NZ-uuuouy7%j$# zOpztBMUKeDK3n<~MhbFg{YF0ote5qrhCVxM@2 zI8U7K{7qaSE)*Aui?Qd~yTm2pQgNBM9DAef5m$&S!6f#HtDL=JzqndlgFVsSBd!(K ziR;A;;=R}<|0dW2H;eaUr?gv~SHuU!t>QLuyZDg!u((6qDLx|Z5_gM_ihIPzkmdLe zai93ObDp?gJb=9_E))+s=ZlBLC&VY6kBCn>7l}`c&xnV`XT|5lBjWP{YYxQ$@tE^z z@wj+Gd{KPK`5)&W;>+SI;;Z6o&eh`U;v3>g?9BF*cv^hR`9J4k@on)PBtBmvzU#bG zJR_bJ&x!Ae?_=k-=fw}jkHib&$KogAr{YENlK7eUIrek=rFhwSRs701AYKu_7XKrD zBYx}rRvZ-n>pU%fCw}jI(>X)D>O3S4iT@L?IUB_5&L_kh;t%3Y@kj9|@n`WDZUiU( z4qM_a=dg3c*(m-Y4vRr?#Q9%rt}mPcDW!s4@-6Ag6zAJARYv3}nI_X^h8!)&$V{2# z{8eVl9GNTgWWFqLz9S1|)cLOSjPrL{QcjQ);-PLWe3 zlBZ>#oFQjApOUj=znm@SIKOt@l5^!eIbSZ23*{oYST2z#$fekJ?RmLOE_Z(DoF!K{ zKawluD!E#&fjxVoJV~A`Pm!m})8smNx?C^MkQ?MiIUvuJo1EXvv*c!EzMn0($gOgl z^BcKc?r=Wf+~?eiwZV5dx5=II9Jx#GmV4x0d9K_i-yzSF=gSM^h4LbKv3#d|m%Kz? zDle0l%PZuS@+!GsUM;VY@0Rb8*UIbU_3{S!UU{RuNxo0sEZ;9bAa9W$l()*;|B#2}pge-j(1nspDOaVaR25O9RGLax z8EUi|qcT;N%2qilSLLaERiFx0R28XWRiaAOSXHLVRfUSFN>!z*RgJ1ub*f%9s7BSK znpKNxRc)$Wbtoh`sBSe*^{DZxS4~h8)g(1pO;JHY&A#CRrAz* zwLmRYi_~JZ#CgN{gY%|3K`m9w)N-{#tyHVjYPCkKRVS*G)XC};b*ef|ty8C~_38|@ zL2Xn6>P)psouxLbv(*;0Rc%w-)ef~&ouhU+e^k5G9_LTapVeMLPWqdZ&7qxoAQE7XIU^*b)&jTy-(e&-mgBO zZc!gpx2oIJ?dn77!|D!or}~JxOWmzLs_s!AQ}?R-)W_BR>H+njdPsdjeNuf&eOi4+ zJ*+;fKBpd0pI48nFQ^0RG4;55LVZzvNqt#;MSWF$O?_Q`Lp`a!sh(0#t8b}qtM91q zs%O-*>N)j2^?mgN^}PC_`jL7;{aF1({ZzfEUQ$0(KUcp{zf>=)U#VBruhsvk->BcJ zgX(|P@6_+rtLl*YKlPe=UA>|Hpx#t}RDV)`R)0}{Rew`|S8u6*sKaVd9l=4x!j-Oa zT{p!|btCR5H_c6VGu+Yc7&p_+aq?XGdxx+l6P zxhK1)xTm_OIX^*M@~6&A&UMZ&oNHlB{ut~3Uvy7*e&()s&u}+5KX-oVZgdCSGu=(@ zS?*@{YC=?zQf9?)B~s?t9%E-J9I^xi`D-cR%3X;{Lzlt_Di3 zs=nX*=Iv~qs*&PTpGdnX$)|lXu5Qso`cC%l}WGBq-CLsib zBw~tDIVr6x#)uLTF(OUzqn5+rSWiz&m4k>DC`z=52oWjeC`E++?*IRPJ8uWGhPJ28 zJNeyr@4N5*Kkom2&imi}qTmt2lsZxw?wc`w$C>Kl{*=m4?-HAF%EuO6+8^p<=3MmRo`;e2`!YcvAbN$Gb>DrWIL78h6jiG%iX2n-nw^dNyL+% z40#h?Q>mFlqg%S|CQ`pJCK)pvrqUE9{Q`%{l!R%wFj}qHh*wAzDbdS{CbEIV`DHKL zCUGv*%Vs5>lQ1tK6Y+9VPENSYNqITpJ}2enq`aJzmy>dG#dxW^iaNB#ddYSwHd$cy z5T+VSTS`6E%1~pe(hn=y+*GPUfu)|&5qbq#NTOsR*J_JENE{&~@evXy$QOo08F9&< zCC(7fB}!B**Gq!g;>a{%j$@~kdIqaKqg(p0!N(~xN*f2UH8(ssT+5FVlN?Kw`l}@v zE;S)r6dt7{Cenp5mXTx4Bq`s|tVzuYiPFem8GC)vGKU2LIUY@wZO!1GO}xxdChsh+B@6A;$Pi{42Sv<5 z7BfCb)$I&!Y8>1&Zv38~YK?QWc~gx>Vsk{E#O8i5O#{=WqLPWj#NTQbeC#P zlq2uCW#}Qn`B@e`wpGUwHl)-9^GJz#v_#9{>`k_`u$bX;X?TDmtd2mcDM-bKn8Be4 zgMl>MpfxcRVNhPpH4l}xR)$Bam8}E4vEHHncqOV?>Xx8U5wg8Ftuio*{-Zj&rCb^v zX{@lGn=8Z=5pSprRH|a3d8v_t7*Jly$%~;Cg!6nDilOGi{K9*_HC|=Osw7#W8j&PE zOpls}YgNd?rBjLFNIsj0A0~=3`+KYS0RZETbT{q@tSmkf(OH`K^!%jo-%9*>$#g0{ z8dVSV*2zLa*etY88=at@Q8B|&Hp2$|;s6bgk})=n*7Q^mxr?HQLZ~;=jn$|E4+qEjAO(bbcP2qiy{u!v_H zvjvv9y(U&u`#8h#JXI)yYNa&7?0ZsJ(vKHNjCg@EH5JBR_{yl#!(N)^0%)9V zjVp!6mD1g<P}G2U3$kIM!1h(LXVm+sVR~LO8bdRBKbHm zQ*hVXzGZDhEol-DeT%T$N^GHT@#E{F+K?`=AY2vF)7DMU#yY9~dQQ@Fb?Iy#0naNS z7gtY^DoXz1#QfX_y?`eH2t_|ZoqhtcCp;H@1a$K7u-Z1Z);TAo2%w@&_V+ zP$z#N@&|SD2W(zJ(9UuN?R9eT5F7*rd0wb{F8T=S^ucRYFGz~LB*m(DIfrzSKUpU~ zuNU#0>=dEMo8+N3NYW6Ebde`nCr?u9Gg()kNvY4I$di=%O!AN(_}u&hU-RZetofDU-iqJT1j@(Md+i%?MpyMx%Tw&j=kXtEta1qFY`YdqF|?!x0PgoTqi3 z3G0g2%wCWcI$5E^YiztPba)!*1;vn#*bJ}Ly@1!~2<3TE^vP>@qzj)#3DaRYq25Dz z#CDAEZNzqr=-UXNST~Y9c`1)~A3#US;XME^XqEDLL`D5c{pO`UJ>erG=|!QR3F|k^ zC(p5C#&jj07uto?7q3rIKk_^+^}!=P(nTH~`C%7QE{~~Rzzb;TUFh@l5bsGj8KJ|| zTck@pc%dDH?}c_C{1t?sR(Z}tFzi5{^IR5o5WXjLcq_+|uzQwMltxQeK;s*Cyq)Wq1!28-G>WBc&U= zM`~|mkJR4s9x311JyObN_ed!p?~&3i?~zhQ+9Rbq+MV4vRM|o_=*H5pH$ubSG7Y}7 zG$@~?LHU>l-7*czNHpx7Z3o!WWcicaKO)ZZCk@M=~_+Y#biuYt#W1RmKF(wV-r zL&TYWz;@{evhOToIpS;wfo#oUEGNALvezqPH}VO69=j12`aI=ETO*#ab3AA9b0U9E zmiv5L$M3)2SA7Qk>q|FD-W>;*-cJz|Lz;9ZzJ_>q`r;R zw~_ibQr|}E+em#Isc$3oZKS@9)VGoPHr(%m9@n=Kd5y?xL=Q&l+lbzc)VGoPHau7J zjnuc1`ZiMEM(W#0eH*E7BlT^hzKztk;klS^q`nQ$#SrIumHr!X*015Y7vij6!*egh zxt!* zlM?zVu4j`H`n;k?xk8`U7KjUdUQ76f*AfVYKCc}R7y7(bKwRka+5mB(9|-+G=<{43 z&$->0Ke!HV|X6o8|n9q^m|77JtO^|k$%rezh|W1Gt%!F>FS=T8Vl{

nE%K*D|7npwE%Nhx1n-IddFhY1$e$MZ(?Vay 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 Jhje=PzW|`Rr{(|v literal 0 HcmV?d00001 diff --git a/assets/fonts/IBMPlexMono-SemiBold.ttf b/assets/fonts/IBMPlexMono-SemiBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a48e40a3c0759ed9c21793aa2d10362854f0eaa4 GIT binary patch literal 174608 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/IBMPlexSans-Italic.ttf b/assets/fonts/IBMPlexSans-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..efe8a1fb9de848891cd6e7d70280e1f47019c0ca GIT binary patch literal 207920 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 vVMp>3EBd=)-!xB%e7)cTd6p_kgWO_Jm;5z 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 H#&Z7)NtEp! literal 0 HcmV?d00001 diff --git a/assets/fonts/IBMPlexSans-Medium.ttf b/assets/fonts/IBMPlexSans-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a3826c4e9ab3f2d6e64e0c9aea7122ea3431e174 GIT binary patch literal 202460 zcmcG%31C!3@;~1FUM_N9lgT-mBbg+VYm!N3a^E*31cD)fBqR_{K@JhYgG)taSwuy| zV-Xb*@j_I@D2l9$f-bVI*SdU>by;=2zb-1PGWmb1-b@aw+Y!B|oN-nn%4z@>{=EHk0Lakw8od;W@9`!1jQIb-RMF#h!5 zoS6fIsdKviiD%1j-#!N!{-65?GHqZAAXrJDVDLY&*v|kG4N4WM-%FM ziZTDQ3kH@i@{5t4$Mak9yneyVr2{#CT$zFP0)N(Y$-sh{$!Dj%&-lr3#>RZKXyK(x z_q=wIALAdNWy~~T(UO^qjx3sg9b>wyP{tWhiZPit-ZW?0lheY=zGY+NPmsg@X1#r% zl3sbY^Ao4jm-)=c_c2VOT>KHax$_Igd`ofv6JL>|+)_T}AL$u?_GklD9MIt9GU@b(P;-)2b)LhtJ8u z!znz=hw~^sJvz*EM016xqvqAWSQbRi)HdjEMAIZM)5rv zX&vs5VRd}W@HxqZ>m=4DFJWzzFBORQ+UQxn3irNbdcFYf*Rf`_DpXF!btH?HBo@p+ zW+~DZ79-utI;AU^LwXqL1V9PkC44{1I{9{1Bt@|VX*2RT>y&elB42tH_b*k`n~-(_ z_i7d^J;o+VOIf764cDufMw$zu9M4igzg#cK#&t134=@3$07ZaGKt7-lkmm(uxNZVu z0TKYU06U-_K+hIPzh{;5YF5c_W)&`w#-V)xIT!$`k@A?18(EXw#+taDO~iE?(mVoo z4z&6`Ad%GqGWfSh&mh&Y6#gBckfj1_QZ(8U&1~Gl#tHa<89>J<-j2H4htKkQ(5#*n z@#!p@41N2py?E|Qymv8D%9{;27(m&paF5FAhI}ptP+$0e3d5hd z%TxLo^|Sw15b7#R9$@JN)PK(h=_=IM0jK~S(XW3MWR7}H8u1?K18+FLO?9Ac-SB(R z7o;;|e!#5yJVPk~u zA^oF*JdS0EYtl!*21>wl0(#c=GvFzQxyqF90G&zCtrGgKOz1n(dEStMa<>4g@!V4A zfQ2lcbfVCU(6zv``By=`*K>4zAvmur(zo6~diUpm^sx(sUMAiAe*p3WFj)Yh^GWY} z1KEJbS%?6#1^+upa=%A*f^3O5yvd?mwgvVj9bix)8)?zcK(e$Rx{848k(W&(yX1zz z;eQMJL^jGB2#^0mkWZp~vV$%ldw4z^!TnaIgB-@Xfov$*%>N@GJM0Y-+2fxBvePbT zM!(q1a-`RoSqfvxum=ulBXn1{&|z(oANqVg`e`8S)*fa-yYx~K%O|}cnE_*%uQU;9 z5F5h}ffm$vuR^XTnZ;w*$Bn-a?nzhd<@^ z;lBzz2`3l0@VR053z-jp!M_7+hpjv@{Dt%mfN1$4QkU=MKwF3g7FNKYfxh`~Ji8FI zBE1Zq4;<2@VYF3({y8=L6J$iuKd!V0-;)8&C^wjeONVhy_lSOtNJ+n{{vrPxixz$# zbc*Z?I(>{blW#W*&&+~;%4GR+p70HuWxOMAM&2Oq_pm_eES`x6gtHp(DnZucei*)I zL2swSCnLRc9q8!d!%xr&@4znrFXRuH1)zuhH@f@S-FS8j;P)(a1YfAnxp+c)hIlfa zMaZ*QmOKwQlaKhBsHaH&E9>+L#`PEA>$OM;ZeT|FU8I|U*G-_I%O_CnoJ_Ws=4ivepuvmzD-ep*MOK9D}A{^A12L43KN;IxH1sK^Cb#xeDJ2sGc1xf-i&J9AGhg3QOeO zEDUKB?`O$;Htfn+@OKgJ&w}0mmCwl&A%C+0Ei6$UkF*@TpNn(?zE8$=FN=asiIJOF zvfPLCa==XF&&T(A7AG&n_f2d$V7}am@kS>*rUG3*1=!$~-hpcZy5|iC@SVVwSB+~b zCrSlZ{xp1_2H*Wkzy`pbfPVsL%(@NmFTlfqt5vub>1x1kz(as<03QKf1K=JHMYJQU>+K9jtF>g)zkovzfoPReViF}MsY2r%|jpcSq9 z0q`KkJZLS(jEpT)!9Cu`clY1$_b5w@^nGI3YM(n;70N?YfypW)5|t3D2t*EDB{H9Y zFOi;RtC=4g#8n8hvSL=nrm)%U3bvB3;y3YI_`Q4&Kf#Bk1j!+FOV3NMNdG1K$iZ@? ztdSFBlbkNEl-J7}Kt+sNP{t5k4`u*p%vfa>O*kpLfu*a~^Fyz(B z2(&WJtCfRlD?>#q-~ExRt>nYsigvQ$Q;>V?siMEXhHsNk<)3nVNPj0z zMV<;j6^6W^Q-P-fPWhek{ou9_Zv9}!-+Vt zf^#JK;!h3$4^giAO@|Jl2UzGkead#&SGry#ACr&EZ_4lal=-xSqpJ~&Be=o01ockz z?ev|(@TYvzmFLg5#dGbs@~!c$qo;l6dGhGWce(;2bCCxvflufU^-pGt&@VT#J?v`M z!Dg^a*;4i!b~Ec|7qMQp0#TZ2Y=GUtwt#y}5Kp`ey>=P9iS@{LG9UD5e-?nqWFQM- zm$OxDIol+kW((OP>~Z!mdjh?FjeI-1hOJ`@*!}YN>{|IYb_2Uro+Mu+-yx+z7jJ{4 zcd+Z(CGuQ(7QCj#{I~pizMkL8f5UI)+xUIxGrRc1{1N^NKgfU2U*-?;zwp2Dzw(p( zWBvi#%>RL?qa^uCeo~Ue`Be5t_6j@7{>)xue`4RVe{(-J%q1>!BTwYXT+f``&MSE_ zFXQ#RoZZH2`5Zo-&*ZcC)qH^T5qpE*z+UB>*m1s{eb4V_KOk!LBO+aA`2*}HzLTBf zPw)W#5NG^R?!));F?=`Y{5c-Uhj;`(z@z!|Jc_@_=l`2l&SG>_dQT`bL)7D$VvCDNdDiL_Yillr9rX}UB= z%93VCv!$8RENQMBC&$YzaJkCEP#-iI~vll|oY>2v8zIZ*moIxYPu`^t5) zEbFC{a=!H6@&q|t)<}PrwbI*itn`kY3>#*YPRSY4C$dHQr<^5yCTB{Y$~n>(vQ_#@ zE|kv5?Q)oGm%f!9(sy#X^pjj6os%mir(7g`FIPju)W}>OCkM%Ga)>-$4wX}6opb`a zCsH~lM@WB^qohB{3DRHWR@llU>94Xu`am{Gf0NUskK|P8?{c2>wQQ5Vk&C6Xa*6bB zxm5Z=u9Al380js!R+i)f>0eSJze-s88ixA|k-7na+fKgk36Q#^?8<01SR9?JLgF#a?T=C5)S zKgQn~B+vx$rdRuz73)`z_lpf5ld^ee4;wpFPVSWkc*ad9l1qo-NOjFP7)a z3*<}Wh0xMVtM65 zldq9qkPk9HeinLl4EtVqM5JQLGy2be|5*!Huu6DM)vSiqvN~4J8qU+p24=*}R|-pI zW<=!ESq8JPOqRv6Sq{sE&y~mWnT-{&LS|G5$EQV=W zEQ@3DECD`U64S9{csQ!Yhh-#PeINAnU-$=zp1cAbd=&ck&(OgChW7On`Zp0882T65 zw;1}jjF&_IPKWM=<^^wV5H@2C+m5+|sq9+VjO*k(U^}+L2K#N4!9>P~Q9 z&g||kK{-BD4qn0I>$_3HJhcR*A}jYC9`el^tKTD=+q))o?O9!yu&1i7JHcSk*Y7#l z-nHjoU4o&z8&CPUF$Km~&y7`a_6N>>7Tga|O6lm@Q9fct=84@VNWH1?kXLlXy9+Ne;qpKd68oINI z=9%chh&nJSlMg{)F}s<`U&MtC(rKsAD(@>b zt)z4*c-jgtv;+Jc4^}jw|BglD(xE{r;Ky!7-=B(R2M-rv4!9Zmcro*n0x&Nz1UL;y zWWl&^RsS7@9(oK?vk11h!6(#bi*LX0KHnewTKu;79rADS-{${Kz+k{zV`9f_9CIQt zKX6{)(V*C%!JutHXM*Ph?+aleH6bfQ&V)`0-5mN(SV~x1*sicM;S}^B`;52oqR*`_T>G^N0UF)2V<<4uJ6&$(J$3+(%-E=Ztyci z8gzy>!xY0(!$!lMhMk5FjVZ>p#?MWOrg5evrY)wAQtT<6Da%r}rfg5GFvpm4%oEMd zw6e6b>Dx1WGH%KE)KX;Wv`n{bvK-A!%-oatPL?^VEo)ZR&aC6v`s|6>ec7*Pf1MMP z(~`3?XI;*&oV__Gaz4pT&#lW{m-~XX!TMO9HSewbru<{JcH4=9^n$w!58Klnenovn z&f;w)<4O}tuPuu&TU&Oxd`ZQw%KoYiRj*gKRo_<=TywIvzjl7@^4e=_H`U%$MKB+rf?^7RMpIDz>UsPXL-(KHSKc{|a{p$J+^;_$A)bFW( zzW&vQ)eRdOwl?f&*wb*Z;q`|18csKS-N+h)8)F+&nj)KYO_nBm(@@jlrZ<~THZN_7 zZqc`7wG_3~wY0bNw9IK)+OoQ3!`Q*Gi^g6xcEi}MV|R?*GcJ8x!MLh%K9#?P@zSo{bM4 zA3Hu}{IczohLg#?fh=CG&yu~{ABaw{K>~BpP2kfS6kPVu3246x>k0r@7mIJU)N(@ zLtTfv-t0Qr^=a34-BNdGcYL?GJHNZ4yQRCcdwTaJ-B)z4>%OIXTlennecgw;k4>33 zW!aRqQ#MYybIR^1`=%V4a%{@`Q$C&Y-Bf96=+xCyH%#3+bw^KnPeD&r&$ymLJx6=q z>G`nd%bp*4{dyyNb-k8edv8te>%H&wp6)%<$NGZ%V*57qZSC99x2Ny!#f@ZQHcn)Amg}H0{{5_osc-Kd*mT|FQm){h#)KHy{m!4#W?b2l5Bj4s0B_b71Gd z-szFkb<-`=?bB|A-*>}#~IeYKy z7iJ%w{mz^@bC%9oJ?EV{AI|x5&JT0_=0?ud&9%(6&#jr;Hh0S0S#y`nT{(CC+%0qO zoBP<@p}B|WzB%{g+)wAV&)Ycf*u{w#ubUq^f5QUff-4q$e#s@599x*RaNokyi%J%4 zUMwwcSbT8tt4ku6T(RV>OY<+Cf9WGjtxGpA{o%5Wmuh zI+yh=yK~un%XTk2dpWy2;PUXxTP~k?`IP19%dN|AS$^m8oy+$we_{F2p|ccV79!Rnk>~ zR~^2(?do?{wyfN;a@)$?EBCF+TD5M~TdPA?PhY)f_5RgguRgnmtqE9Dw5DoJ(^_fm zhP9j5zI9E_H9M~P^x8$&9=fjYx}kL`>o%|Z==zrHkKZuvhC?^z-?;V0?Khse@u&6a z>({RTWc}B_d45CNhNHiY{cXx`cW#t6wr}j(*uQbk#zh-bb%rr1sT zP5qnpZaRB&%gx(v@w;W>Esx!5zIDs3KWz@!9KBh;Icsyl=Bmw0Hb1iY>}_*yTYB5- z+csbG^)*7aMrY`t&mV_S!|9=@aIj$?PW-1+8R?RPzLx8?5D zcW?Mz?C)0p?&WRPZ5y|}eoy>8=6mw*sko=*o=x|Bvb|<|+xF-0O~1F`-be0z>fZ0} zb>0_vU*vsT@7sRguKV`fUvU4S9eq3Q+ws_rp&f^I9D9I0Q1ZaK2Y%S;w=;65Zs!#{ z*X_LJ!NCVVdhnZF%XYoF>-}9H?fPby^P#|pZrN?w{nW!15AS=#{K(!%6CX`~^r^=Z zAM1bY@Z50B4<~?!ziTC!#?=|kt*=ygs z+07d^TB$seA&_UZJe=Rdvg=`Z)y?K}8P;4`bA`EY;q{yU##&$c|f`q_7eCJx>G zT=;WKp4;(!`tuu~-}d}t&%f~evFA@d|K;<}1EB|W2XYQn9B4bxci@r(D-Ucuu7I_57+)-*B?$FE;zjM@S#^iU%BFyp;z8{wd&Qo zUwz?7;t}%^>yeTpbw}EcbRC&~Wd4z5M^+zMf8?zrCy#t`H1KHj(fp%5M`s;9{#wav z2Vd`befJ-I{@DJ<$BxAwTXyWjH}cf?_bKl`VKKdt-I(4W41)A;6;H}8J) zoj>RQ`OZJ@|MTZ>RlT+4?eMqneEak}MenSB=kUA6cjvwP@_XU$-S8{^-TB_Z_YVGL zinmZ1Ff;WLOvMZ;mVu%RJ%Sr~$e@>B6< zh`anaDj%aMDqmFSPZ|D7g`+U=4}TVj`<)?t^(;=57l*t^k(Y{8uhfxS?Ta{GDmw^} z7URoyTu_G8TP?G%alsekg3-m2XW=ROqf8Cn4-xM(l$nGwt$7BcIn5NxO-6HJfxVO$ z7MM*&KmW7}Zp)9+MA$T``Sx<$P2$lp{I$#+U*E9y;t7)%FX$_91TOmS0zwUtCw$nNx7nnl(4say!qR zotT%tH$AVpY)Tm>h^UQiXrlrC=LOps>~5nfcRIIR_)!b|yxQp9Bq1ol^hB`IMn89Z zWV9z(v@zJ-?l2S-CE6Hei?AuYjP?VM(@YwZV?-O<7K~`)-_g8cG*de5JeF&tRu<%R zP87`(4W%}6mWdTsr@==Zs~tiH0tBec)uTpn&%28eA0og9>S)hmSU@7^Ys`@f3o0by zf`1N#&5ww0%eOnUp`2@ry2|wWvaX`GLG6?vM|N3xd6~61q9cAzu(@$&Ma9fUb3=(G zC39?9Z`s(~w5+N+!qFvb3Lmcid0A6M%fp9J=#S!qNBI6%au)GFmouJ!E^(xwNyGz* zg9iy1Al^n?KqyFvL84p6Mm6PpP9(ZHp%Hz z1}-QAn#NZQVk^ZL;wvc`8lX}c-h96CfIh@|uXU~WzYqg|QT8sH>(JA>l|3K<-9MaEtE&5wHTQ~$A zfk2j*EK1a6utvq*%YrB)$SXrt#~%KN(-YxJFOwjnT+oOVbD+2i!c{1)ta(wULbL%& zQ)ANDG^PmC-0|aUYC0F-k3ZhsS=rcqO}7(MwlXsgXGt+q4*~&|7}LH$L~3K>1)*c1 z`O1&)XOvdCITuSv#CoAAQ-;i8kY z(Pdi41VxpnXO8iS8DH2wJv`Ku)}_%lXOABU@Q;huM7EAk&}-84X?$H~c64^8_;Bfl zEMbR|V6<-@0znZx6;#oI3$mLFS|YmEiwlXAk?55XK^YJ^w|sf{MDai}v?I35w; z5qO8ZP?V|@6>5=}D)QoyMhF6@DyrH+DiF5UWt>$bbsqhsE0@(z)0NvMRM(AEzF#;~|71riE@XtrzlCh46H}sG&*`(<&jRSU)7h)WWL7P2&k6jYdeL zP(LKIM#!uYGHZm)8X>br$n3KS?d^@6nP~Xe|yCF z(%H7%E?JX-R{$#2mJNM6?7TibiXLhY`Gw|tj|j*^f!~il z?Sqs?1T=W1mMKFycc&iNr-8WIqsy)U##-kNND}#$W572Wpa~Bj6OJfwKZ|zvp{-IW&A)fEBU?w`xipHg4nRgXV>2))xb zT!gXK&x|KMW35p`D^-p>l=UX07;C9cKNJ9_!q5(7?0Dg^7RH)_+EH#|t7EMQQ`87& zhbZqgo^+430yO$9S4Fib38@5^cQDI3`e4+`n?XJklBA z|9QM@OPTCqa@rJ+&}DC9NYBK0=_+qc6a)F7U<#S(B*sKX zq0ucF4n@uTBX0;gD26Fl@pz6plr>L_F}XvB9%{#EC>M)^Mh=AGGACnQ4)vLTwG5^< zBsJz*a}62R_Nj@s#>|FpQ*Cs$B|Fn>v38D6vX2Wc$f+`=r5fT)zJ7ttP3F>!n5?v_ zSW`-pPUr6zGPcrOo`oqoNHgR0SaTi4rVqhnLm>efv^6MK)vKd=jB0v3;^j>M>L9+N z%ZK19TwD{E1-v9pk&jWEH0Ulm7tlvV8P5%JFn=DO_;i|+qs);Ml<;E z9ppjF28f_oilE>oM0S)=M-yYBy%?kV>>j^IZ3;46J`YE+if|wTBva%Iwkz6}Um?+j ziQnksuQlcACv-0u)TAdzEm+VQm68xXXMm4&{+_%ZE$@N1K{A6mXZ{Uv#Y%T#G?vDB z;rc(*=RPplHrbZU$z;nfJ~4lMp^vdP()mX~JpMH9nInN$AK)l2} zvXHw7)DU@i#M3j_7Q*ie~#M~7^S0$%*L zbN@O`e5y96yGu%T9$UN^c%=Zj-wPRQBqwjAjHxQzgNBD9 zF2|LFeH0OuMYr@LF)QSTLW2eonG~*de#38a{(~F4_~hRZ{{lO&6 z-(yl;bu9zX5?{9upn9x{ZV%35QB*zRK}T^=De#IlF%-StgnG6Eq+S$>VoA&>-V%rr zAi~mV27gnOEsC44s4cM4`D6K|UvB);`I_&{V+RgML+2W#iO$!7tIV+e9V-bbxSb3? zbMX*d7RArp&kQJ*nop_LjkSuC2WxMkC$*I!#-LQ15(`VtB_ET_${3`{q6r#27b7r@ zG9XY6{p_#N$&{K54x#F3m9ZTzhW3JAK@Ox~W&1rbUh|pU~4Yp>llGRMX1fXZLii z>&b0zE66XsWMJOFCDk^^xK8kpc(9b#U4SNIsb{;!T_pLQxsXwmAK`WZH;urO;@B!} zZTUKhh?>dOsC6cf=={ERL94~mx}f&^@7wzO+tx`#S(C4sjQ?4K)2A((Hr+{TS>U7r zPC;xkF;0U12Y9mcZ=zP`aY%+LD_1~P(%4l)5Ev{G z^pX{IB<^YZs9u{!x-3l@RgvmQ6b%nm8AQvllrS;5G#W9PvY(kipcFy0NaUrUKGy`x zg@gMQ5<*)v4GNO!jv3etkC3G4#izTagIPQeW3v|i3&F$s6tbT)L_~9^5Vt3 z3}AI0mxi40bMv_d!M7qXZ;ogSdeQkZJ4)Mn`Z$fFsek*bl1sr3WeRhQ(D$en3ZERW zqG{8(wZFedl6rd1IeYM4g7AB3f~1nh{%&7T5F?5tA&PnE=j}~}h&rQCXQa}|2vUC@ zhZdFcmvoNt)~Rzt=ZvxCWZO@pwLUp5g-HW#+U$a&Vtz}XXgBGPYtU{H7onkmw_h-d zhiaU_gVd(t z=ljn2F5LLvftxSIMqosR3;!$h2w7Z3OOT%ET+9<;wsu1;facU*)B{%;_DY7I>^*1k zbItR#*L#XWut}9_;TVO8R+837&=wQf6#DWzt^{x?D#T6xK|Bq!6%jVrSCYs1zK%QY zaQMz&_nhBztLIBGi=F@E+QsKS1tEUNX9&?$ai)G2LVmgbc{~~=o2skk!Fd#qytNM5 ze+f7e?g50Oik}u-fu8Lo23|mm?Z4t&CqpGu-C#ex~%`GScL}} z@17Gaq?pFL$%0>=*+|9oq+wiu7-SA4n`W(|brK(}TR1*5bNs?OSDId%8y}xro1Rfa zX-zOGtgh>)=DO0EiG|~=msrOYCb};m2a4Tn1x*9d?Pint#PVyQZh{3Neub1#FQ);J z;+??17O#&XZFLHQFO<}3aY55ws?{Qg1u@Jx8g|ljH@PQS1M}t$Joi+l)tdQ~G_<6@ ze~I%`z9QRVwcrpY!qqX{ArYP9Fz44hgdrprnv2TffoquCyHbY~9$KrSA!IZ5ghx}6 zOQvY7c)@`%v?EpF0CY$>&yf`eg}C77ud$8hgA_EQ)KS`3lj35<+JzTCcpfVrEwdH+ z+o!B*cd_H{r=yy7xw+uJP})fSvPa0cmVHS4P)48o8Sx|3-9~k!;%TF5$~^-UZ>vI1 zEx9w)b+CRGG`k9to*P>|2J;son7;@Ct3t45BZT?%R{H~!5GV%PRv5x$TEx+g3o#Ab zfr}XcvBrZQ1AA#1NQfv1le7M8HNI>DYzN46mrAh$$QEIdv~Xgz!kdR1VTusWs~DLo%3TRlt-cQpO2Zqcm0(y6Q0&}Wma2icX4fJ#wWCu zl-1aa5vny-_XStko3nGuY#G+zps4;yIb&^!j)Df0#hR0q859!TKPj)hIGN;wLmU|P zBar$lCtpKZQRM2SqjaDuCPFGamYI=$gZ3qh%)1vKmm;LjbGxO^K{5@fV<8ko>kRv|$QAkd_=X1;H8$tT@MgFg-=JY{luiErAD1nx^{HF9 z9T?QT3@d#)H5pLpc(o{VW{)IuL5eA88aAg9wa* zp^B$TM%+L?@W7>wd2&(5%83}bH!pl-Rd<0@I2D-h`Q#Jc?%eb7$GrVq1E2fWTh7}B z53$Wanum6XI0ps`Bm9|BXsgP?gSNNI6!zVtU&vkXRr|&m$e}OP2}aB4zOo6y4zlR7 zH1E2HF#E)OE*)2bG)i*qfKj=Kd9nPuF&jcQ+z{4W&Kx{)kZ+tYW zKY6G?krwiI$!GBNEPpUC1N$hI;D%&5_and3c?bWg?&bYnLKIKNEGe)S!2UpGx)tkAF%7Hu-C^7yB{|I>6c(2kBxUL~elvp1CF2VVCl%}B zbIOf7c>nxYtdp%Fwvq^4y4GwcwGj>V>;% z7(6O1l?J+4UrV7(nj^*l2Y5P=rV83QKs%f}i9E#MnM35I;yGGY1$*wE=JRfWJ0b{^ z3x@?QQ@!m%?pIw=X3KAg9UGo*ugl7tRA$tq<|XI~)3qtpJwT9Tt;b89UIdq%vUuU|rDshRYJ#*`gftue)gmfQX9Mg71~Vzl5k z5Ap+w)7XM0g%uH}_0Jw(cGT$FBS{oDrbo%uK}b!sSppq`L?oG}2PnKpV@q0bK=K65 zTgXY6Od47<&1XBG=LwsqO>;hR+W&?D{wHTz_YHsJOMo9|Rp5^v_*vN6Aw)Bk)zdE7 z${==Rc*PyO7hz_A?uv{@;wnjSB>*`|0>c0bF1N!#^aIH2RiaWdzEM<)*50LvM~4vN zQm(*2gTGh-pokv%NL~yR3!MhM}a%6;03`tDMPO7M_ zt;n`V^~Q4?Uh}=i5F4T^nqVvFs>F(0VvaFBw5}|;tj?$j;nxv=iqL)&+Mf)~+D{q` zn|DP6^uJDXDG})y@K-Nhi=i=}*r|s$rwO7&Af=lgmF2L7%KlnUCp&K8d@vu)yXBDc6(<@a<#p()lk`6URhsXNl;Q=UIGYCwzp(9Toe%yHL)yR z2|T2iO^gbNxTql$(+5*a3Ze^2#0PK8w?~=Tt0nz~ z#coOwiR5>x?D2gNyskuV!5g1n&{biAOx`g<_9T`E8A!tp@&CMrMelB(H|PI{Ygn=z zJ+-N+wLK13THMxF3iy~_SVkW1ER9X6C_KU6m?qml<49)^anXi9+NV9CLp}0uymhS-G4#!b2O$39T28XzGP$ zCC0}m5~wXIcC}DV-%E%NjZP3B*L&L5>uRqn^=@kv*1={d{n8b!i$go9UykzIJni)6 zPBLuJ5W*?&Bt#$7LJul4bL6=xE4lGPf*1PBw$2weTr)geQrv}mnES?Q%!;(TfT zG5ma9z9fI1T9sr=)vTuvd>O^mkE6oAN{uuXn7*NpS@ex;Tp9YA75k8bSpDd^3Dt+Y zaQh@-W66gjJL|HqVvmaIul2kb@KDYlSMBQNGiJA5;Vh@M6HTZu4l|P)LLdOOfIZT>EYGR!hwaRrnHb;YgoKK(iWGS5;M@1R&CGL6iy*J zWexX9{=mDK#-phoU#-No9z}J}1nwn33um^UQ5+HSOH;+Vi~URk^+P+Q;HKjlYSIw8 z6y~D@>g2Y2S#{6stg_+B?RdJ}HXfPNSdToh3KG!OSY-Zm}MP)MRggM$SL$S+g;e3u^w=^y2v zJ;hD8_4hj;{-@uTExnk-ES8x(zNf5fLMUkZp7e?wz8-WqIWZpbU zMyyeB7-{lc*}X=)&uD!N0>5~g7St)-(TgXlr==lp48du|8{;Y2^>)DrQ&v(%O?`P* zepF99zsC7yl)t9FFngRsZ>a3KFWQ(DTNZ7M4Q(n9C~GuChw!U<&X}!9MeVkN&PsU0 zuzPmcgcQ~18fEtoiG>QHJ%PV4{wJNIjG7U{MAOm)t!Z9e*|zt;aJhP_j5*ESS({3G z>vPEEN-r>ED=t^DYVn23HTL=ox?EFgD?=)4#Rr-QzSTpfqQJKRT8ZuDbB%HFP4&A3 z-)M;}@l6S%kkTP3^i_FB5gk%J73*s3 zm2LWp-m=PuhDw5x@`~c(3Xi`PF`**8#u^`Qtx2z#5aIH-x=IVQ1*PJHH`%RWv=<=E zY6pEJRsfzDg03AxL0aYr?Bf;&0jP7t_5LWf_)5Pb)3KF$~xmM?}Om zV^)D;G*-I6h8>bbGNy`j(gHuXFQ#^>^Zl{&JWxvsWHP&`e+szuPkN2PWmKU$Sy-lAyGx1OGRD*bP0A=!F2&Ug#CU)(aa|eR z&!xldC(F5?rK5{0+jA_)Q=Ewk^$P17JRYsXwQ^I@Sr}t+2Zq6ZSV<@=`x%i#h7&7h z;R)k>IP9nWXGm&ZWx9P*agsL8mSo6D$ZyO{uggz}wK$S*2o0BRs|!oM@rEx_@(szB zvaCW&RqSqaZcSrIQe3#MpfMx0G&eQeG%?pyksTkCUTiWtveP1OO=-{Q8CSYlT9|1H zh%Yc|i&Fzk=5rq=#yz7c$PTdcn05N4nXW0lzrEncz zf;TtO1w^)0DGcv6h)slX$Y~Hf%TTWndIWBnsDZ*rRy+mk;XR%ZYp-aBEA5wZ4KtK& zy<+|Y#P^!RE#)J}VA75crfiz&D4Koil-3((mBnP&W!s8;e8Wm}jAhyJ@!4g@oPr?V z$#HR(?CkbSsv0lr$jP2~Y2(R{Y4ui))w-f|{_Q+75+4e^sFq+uYCo_En@_4y5#cI3R0ld@c&>Bw%)c0g}e;QYc4J_p)VL*rFw zVYFYIc?1ocY0Yy$`BrevzZc>#TZXeo)#oKYoZfh$?3kaQcjE=DBUHYle63gc z&*2VYJJ?N#i6YdHg7g1)AI1LY`A_(?QO_d^hP$5UUsj)IoXg7dLBN`7B&;LIWPBnX zL5^;?U5XlhOevEV*N8Sgho}A2GPQVK$G_bJ6p+(2e1^S`Gb%%vk%}BSkK!2)C_DAB zlo~vC?G?H?H6l`1amfUAkVF074!jDpC+ICf z@$o6YG3&zfE>6{#rTUs;!*ym+e;(>l>hJf|54Y4*Ut=V8+>j#Oqn?Jwbz%;SKC9oI)4Hl19=1TG1OF zg}o!t*)Rf~yGCIn>P7kd5tMJnNpw{2T(w@zNa6gv&(IqpXu%Nr4DXq|< z_1AuQ;DCP3n%euP-`}utBk{|Icfwuo@R9FS@-&B5<8NEDMt|VIjT<*M;5CwQs^@Qb z$Ba|)sQ!zwp_}rltgl2qt$p&xiCf5ry%;y##!jM7_<|z&o>gMXx^PKYZI`|9-YDP) z&Pk9EZ6W3*HS&o5In|JFDl?l*X29N8c&791SERhuq==+c@qubZy=PD_kz&N^G^JjT zzy5PGmcm;IA@QNEC>Po^vi}Gqt}-n%#HmQU$}?Yi#rgJI=i^`)uHkQ??TLs~SCKTh z&iP0h(e4!TNAL?R6iZ4%l|cX!l@M4ga>Xc|MB61gU%k@)C=} z+P=f^y9jrT;v(F*N1#6`_|tf2R0K4M8k3|rS1D+Ou=3l<%ES04AcN+4DcDKj0WwXl zpa7j5;x&*SN$uUSOY)NNk`4NNi|I1Q7N+~A_t4=U4Q}`9ikQ=tC}z zxME?=_cNLSrAQ(jRuF)c;tr9b4#G8A!BzAyv#C(Tca_r-C*=`;i^Y+f z-q#V9kfPCG(`3i`yo^&>*}d7Br!w*;d=_glX?+5Hv?fdJXVgrkoxI(JUzi6!1KJr5 z{IE>xg80!)9-T@-!wVW05iO&H35l_3gj!^Kc zc>hk<`;yBq`kD8A@jfqee%8k|N&y_d$;NPEF3q*3vXM1f(P*!K4*NVf8_yu#H=BH;m6N@Hvc1|E@Y;2V5-d{j0UNUoFU?xHB?4sHD@4WoH z4D@W!&X)iAlMZQr4_K;7dr-$!q zhLx1Nm6ZE`CNQ=NLc8u~7bY#)d4F1oz9D%%?@u10OY zn?7WX@TM%@?(OB9`JnUeb05+t=`{&+;L~x253Nxc$F86qeXXob&^r=TY$UFeRTCP- zNL<7DJmg={g{%8 zh7Rjm^7_(B;>y#lC3)8Rjs}O_lu@1Is0#=Wh)YBo77>>ikXD|lO)@rU<1$i==Fn*E zgxs7aZIHGm+gxsmNi@~R#AA1>IW#J^-I~)J6R53y)07t$CHczHc{-CdJkrNMIuHEJ z9KKkZ0DTz6%807&u|<@Nnx1~7>{11hVa~`aqCQEdk%hT^fAnPZC$lg{}{Kc4f21lfkamHsOI0d;2}M)EA#Qyk$9I@9iX)Pu4u zKi{9dduudM3dG~dNTcaBinR>Gl~Y$_;erllT7;a{0NO~GjK{1LcUgrVeiJ~%U6OGj z_N{zP7g!$Pz_}&r#K zH)gmm`Y-A2y~LiC7N4A#)|i%Oh)=U1rsacpg@r$bzC?6)AS87UQ69&rkDxs1D&j!V z1vo$sOc2aUCgg=CyRJgGDK~Zq-!j$1XrgI}MKUrdWQBk%9?qigqKYh7DEzV(@+y(i zK^Oa3CGs31k4}D4Rt=GYEc6yGH6}ZH<)DK$O_M%E5HStQv!N8RIQw%;vpqT4-fVHD zhN29uHlxU(rm}6CDMb^JTa=b$j*ByMOLEcJtgNv`$*we0n_<_-Cp+jZM{>O0o}n%N zQ)*INvVVJ%IVLQo6itI}O5*om?vmCz$C7S}ffJzEf*3TwvjM;4InY5Y(?We{sYN_a%O<@bDCQ1CHNxZ(=uKNfY}etQ@Fax~7ez;6#+DIF9u&JU0)>z;8&4W8L% z^_)hugIwAWurUBB&3VDqrI`CdWUK;62z1i$hjvw_iU)m>(gq${%N{3&?3k|4t@dXJ zk#QVw3V_A>xsjyH{GYj|UEO{@8t7F~iNk9Q^{C#E>8jsk6Sq9!BJQd_mN zJ3Cmb(Ir`kHWtukDQFV}yfGif;7}z9v__qs0lLtAoKGn3GwKP$$PjAf9_7Lp+J&LZ zp=9(AELcRV=xBf_T#+*M_QS+VI5dlz@~emL8apN zhWq(lqK_w&W@jL%5~sh=ULTrcq$8oIRkR8Ua)$1RAcjE36(LsQ^#}$lCtt$AeP*u8 zO-RVCGN)EkTAiAimzN3P{n)o_HrH4aTxnZLW@ZTi`4G8$K079SaKz!f#{eV!U~66i zwt?rC4wmxyyv?~+#9i<%-;8(7>_(vLk545QXTDcdn z24jvO+Odmc)Wx0 zt6}Myq@`;N;|yNc+4$kH7him_d3Q5DMZyDi?-uft%k}DB7$fo09eePW8$lfj2ypCA zNTcoie}2$+=fu|rU*mcT`DOBj&YSUa3HuPgPqYSFsF?<_iWXweK~DTo7oBuSM%(WM z%>p9_of)eE$Ixm((k~&x0ggdRhmOH)$K=MM%}}DW9@}-8tyY|}B90WjFSXd1SeUFe zY15)ga=pKN7!?s39U5L9uXkTiU#Z0}x@)8q^p!NPzVaNT#SiEbek67Npg2igAR6o- zsZ;ua=Ewc}r|;iidAR#9{tMdGa$o$$QXk$OITz!Z2cfx`TKr0sZ+ACpsfA2xq{TED z%vuNo{9udFXdXT7*-Gz?7Hy>`@_>MFB7X@*JRpzOeY>I+MJdh)4DfGBacH4Ty#<7fI_RTBbp;|9Y>Q8K!0Skab!dlwESnf z!I16+MBBd_wOXxFeDLc$*Q5t(@e7n1egyboJb!^P-g7|M4{;KoOb>p-J;ZW*vh_OL zxoytv^(T06{xsX1t@Z!qJdbf|nN$N(a8ejbD$LZT|^UUuiIu||Z{}dcM&PJ5urGIGA zijd0@<)jv-7NVej(mE(>&YY+GpIqde=p;5~ZzvM3G+m76-ImES zK%+(I)U#-w&jtoZ5TdAua6QS5&N+N*cQ^lXmEYBK4|Kgaq8>U|1@++28TDKhQq)0Y zTin=tW)zhuI|W`r549Z{EVt`N9Ve+R1fO5+w`%T-T?Z`PG>NBu9#i{2DR;il4nXEj7Ux z>J7Xa_|g390q`gvj55P6`=V^@77KFIE(o;yg}ERP!xZgI=gg2bp}p`188%6V$};(D zxzC$#w!isi>P8MGZ}b(1OIh3l*+c_uGP)oR zW1>BkpRSm)v!KGSYVP!-IDYux7hmutBDM?M$?v)mepd+TS(@2n6{zS%k!%*$4t$OIgdChGJCjb> zEmIFArW1Bk!2(K)Nw=q?y9K(ns1a8bW`-9g3>ht2`^Xju&rp5UNb=or<=qr}c zD_?VK!Xm12({gk9b@)gVUuPv2G-qWs7bMm+)irouPli4$&$E~CE3M*=pr4!*OrZ#x+1(y> zUUAgu0jgb@J~Ru@%?IRAxJ|Q2%1L@bD3tc|(12aJOFM;>Jc>ydcbuKyp_ks%F?n3~gY+%!{9oc55&vm(%Ssy6-n7#)g* zu|b&F&$VdQi?HFcG-)kR2p>lHY<*$_Y1MN z^cFmj*42{t-z9Z*B>*1Qba8_*wQiuSxUV)P%_x~0Ybp|Bn`htX{IH?Es=h&dfZAwh z7=GowSGoooESs$&6Y1V_Lv5s4OmgdUL^q;&Lz=s!)x|U*!a609Pac>OFmtUprlTU1 z;06WG0-+(J+&L7+jzJEsHCNtGLQXt%C5?^XsUsjm-a0AE81mT>YozEw=y_}7xILpj zexPE8UO%H^Aih3h>1dcr7wE}cRm29 zKhdB?@R`2+f3$rGU}RO5cD+~G_jFR(t5TIps!~ZRsicxr_I>Ro=`CCD-K1&g?sRvW zO##_O8Bh=u1R5JRbd&+bWe{;daTFEkMhC}n5D*Y`W)yS~LG8-_efQp1?^RcW&OiU( zaPwYHUcI{a+;h)8`=#|u!^d)f25Gcr-V zJBVUGO+B$KN837laBU#4_TccgL!Ql!HId=DoY&ua>-C(u;m)bz&AIj6YqOfRUpqH< z?Y7WVZ)d1|{qVZkb)!>V;cjP}AEJ^5d)UR=)8GdV+j?@;SdB}EA=jHC($}#xnBGhU zoKc}tSYb@N^o~(~q(6ttCJ$s^94_!i!jV3$TT6xYBh_oq!NJVw*LW_^xU4W`V&wdc zzbm~h{k96+E(NvG?T>b^Uih!=(O0@wqhHM++&aux9rMkr!SEY^6l1;;7`Q36eVvjM z>=r^-!zqnY$KuJc45ncQYbo$7Z8)^sq@lA_xN-(+MJ{0&;We;M`XTBWQO3puV8bg0 z{)LkOI>DMiwXUBvlAgyCHKvxTH9n8kda`7^L zBo`ma#Yb}S5me0PS0Bd>bl+rrI2RvAod|l?25_@p{*sCp@|@gUe1ERZj#o4GrAhGT zbPih}h~-lAo<(Q*XeC+`T0a^(n+jy)0U3EfMjnThi*L;XM)H7>JYXab7|8=h@_-S9 zI*6~J{X?d+aV#VZ5(PJfG6>l`kAqpvr6M`?A?zr*!u^1y3KoXE8m%~il_3mA)KkLA z27bg7V@EG;!znw-YtQjbR!)Wv?%Ka~W?-jxby079_f&Jyxx05abT_w0f72WewQihE z=?f0|8dF{<-oAGIj*Pjvl-6Lc)1Ufec6G4Zzu{okyjJFG&OMpdp)J$l!hxM4%#nyvSE>RVdsJv00DoA&M7wr+SZEycdd+fiFP8g^KiV|fPPD^l zm!sW?b{y?~v`5fRpgoWFD%uK~;%T6N@H8p|)M1mK#k#)<JBjuJ+G}X3 zoKn&oHgc5d!A%~T8!d!3fR;>NvI{py(5^tUgP5db2!b-$nqXiSZpN2_#~lSH9Ka3P z+Ag#sXjh=!ga%G{CN_uaXXrdxTQ)m?91HkE0_}OUSJ76`MCWsHgYO}~%Lbyd zfhbXnjK=G1U^E*Tg&_iM8Z9}-=S<%(qhB20bjI;i58A5n8x=#jnth@_M)Do0n>>*S z{u&6CcidJ<@lxw)7I(CTx8M$(0L&2%5pJgKsJ5LNZYhQdQ^|UEDeIxEb%DL8m?d58 zg~bJs`LzD*;>?WEtnumd_GOHurxq2ak7U&K6mQHdD`@t$GV1MzGzN{roE->(Z_d$ zwXZt14}!rNvPQ^;{kL9e*M_X|4s}fS$MEGY`K2hQzt-*9jk+(`e0`DlA&}vbGD+O!H11Y%H9J^Q`X+~oT{n_OR}?L3k{S;$r;J(3jL0CjidP~T<>|Kq`*+IM)vAySc{3NHH>O$s zuRM9Q#OsE1x0BeS-5qiPZY1$xB?E2RNq~0>p-=o>N`Dkp;oQUDxnUWeJTLCg?8%d; zr>Bq3NH;e%W@Q4yq9H;;xtzGOv38jw8Gq4^e&(D__;=;y|8VwZ{JZ*7SK759`*pwm z^)Iox{oCJ_ zNeDplnOlth`( zUoH$_uz=QUFg|A11}aR{cBB*6)7CjI}vCS5w5DRYrS1khN~ft-PY zfDzfs2w`qj)wohY5fEZsHmI#p^lVa#Hx-gj^J zW$l+8D3~8>Khk_x*JT~Y!-qP@w-g?_vg@+w&(khVYqo2*wWPf#tt0x{`!XYX$FG0= zt96@5@~^z|GKl_H+BsEKztUc*_Wb&b4KKeWX&(y!RSB^*m9zGJ9@wZXEX025vAIYP zQWHvYDJ!38n^1+f1%TQE%ywA$v{3Na4EPc7n*$bHjWpdZ>NTc#J+ek8!Vg)eHDAl! z_eEdpzI_{_&wowxZrHo8+5aUyJGwp?)=ovU7E!Em&tmjz+WK(t)F-)??O0|uy&Sgx zGAx<~+kIq;L1ktl6Ct(n+7KI=suN*aZ3A4-0j_6bdre{_Vy=)o9N!ML>U|`_DVlWa3Hg|L!)4uXtb;H^F_MP2O{r<`B?#XCm zqQSGn)6f=d4YobMc{6ZDCn5xK%zc6F#<;jq!Nn46vI$0-1Z$Leq#3Pv*b@X=paoFS zwnIR&8-h07vs7eA0~FpVa&e(B6po>&DAkIwBaoN{9>LvNu=V8f+}v{S$;*HG_#>}e z`)t3~u|=zm{)BBy^l_~N1|h#t15W~rO#*LCf~*M}5o8%4S^i0S z#+v}>M@zy^&5e!wD$PX{YbauJX^;@9(U7@uKFG|-*mxh5@wlvzD5X^74VYqblU)j!+1^_uF+VMDLk4ZX&VcS6W_ z8hVXW=ryz`pUGZB)dg}Enj0;IHh>1z<*(Unz*zW?IuqYbix^}l>NOeAl`^nC@yiRi zc}@Pp$00&Vy(R;CO@?h3+7YxX&~8FgdJTCHilw2^e3OBfXGwiV3c-&++@NIqJQ_vi z6*Q&KKrp5gPW&H$I? zLrBI_gJD@@8kM*^GD$<2qa!j_u^SZy3?&5=C1Updsdf=USzM4JL>h1EGpoAR1p;dh z3@7Ex<=4-<7Yf>n{r}Z6wY@$!H+N%WL-ows&Exa8xHq0tGJoal%vJLZn|Ean4mJgY zsn4A^xx2emo6#?6-F;|izDRq;9!*|5QH>b}FX5SR>tX+FxBUwpkP#uL89Iq7L1oGv zru~yUDeOOj^JSCs8Re;k$-{=f0=d+KWhrc7vfhPXj-WA)6jLtE)0kzfGYHPW4mV7M z(&COqt$1t*lfZBlUy}p;Q^KH2go~H+K|O`i2_+8diSj($Fy6 zH5NQ3i??AFry}lx=h#Xwqo$37H*`3Y?GqQ=7JXeiVo&M_`$o$9eCKvGt!;POlh$7H zVJ$a$W9C4+Z`|MOTgq~^Pt@g=XF>+aaAdVKmb*vl>v$SWPI(q!Mdj@gSKVkM90aVl zexE)n?66JRV-I1+G%~N~_!ibzCgrFVjbmv**-dEuXwzsr(GH_sj&>v3akTr<9zi>S z_B`6FXe(&SQsEY>&W+cfX+{6d3l}Se{}R`}Ocx@H|Npk4Gx2h@vj67w3X8~bEZqCi z9zi>S_B`6FXu>v}iyM%r-S!vWfORn7#9ZD?!6_0;){VPjamS_2f+Xw=Po{mlWz3SG z%>Kn$$*FdEx`2kI{LHHEHFbft2L_X|hW+bYoAQH2a|MxU$Hv^cuGOKO+}v_^u(Y(M zaC7y>b4%y1+`Re9h2ZAhnVsz&)wSu-RgsnsXRE&$EBWr6@ao`^;ADLk&amLD=rNgq zu};%wM2eKv%{ZC_txkzQLVD%+P>uoPOUJ$|YcaFHw}`@1waxty3Pg4Dy)!ePkB(^% z&7e*N_(&%@F#=hj()Mx2g~Zn=OOUEfJ|ZpdtY61(#dO>#wP_w!oepl1E*xAi599B4 z`FGk`!CH1cOX)Sdo7@P^!&CDce83pc=nX!fR z=$!-Xr88%m9nwG2g`Cr3oe7&1`I@CXm%pQk5A8Md}WMwTOC+7-CvjiD)=E z0lGvOyG6Zra6A4TeHi~XuG_d&Ik0>1#IJ)>!3}6H-?wFI%LcS>?mIYecvBPmsSCMn z0sSn(G+jgv>rwJL6Mzw!2~I)hWP%0XDoHX@(*~T<98FsY>4ipno{lXs&KaPQo`!p} zXwC%Vx!q#jL?C-o?5FrEPb+}dj!b?G3ua^JL{f^^_3q!fb3d+04RtMbsP6$Pm<3;@9p3|)DXf*!Hq@%>Y18mR+fY46mICgYEREz0 z2gt~m!$_;|ln{d;QCU()CxH&9;Vd@DU;nzMYZVVNyP?=OH zj!0>XYr{jCUs#r;)gC&R6l9g*9tY?E|H5idRaMS|Hq$?M?zwaQbEE5qA{A|clCs9W znz>sZ+OXlF@6ufVUB|wCwAb%DCu3n$n(V71wSJoH_h~<0Te&vc7^tnR4S?>-Kzt6I zC%p>Y2A4>W(H+U7C`NZmjflF85QT?IZyZx(KJ8`O;C(_Ja>9}XlQmxYg>AJNjs>{h(?UwZ2$Pe z)u-On?vHNJEoWJSxDani9Lzy)_dx<;oaQPGIx65ib0f`Oj+o&jQlb6!pUCG0T>oS-4$c&hy$L3L;9&mFPn+0X3Q^ABhc)s6nc zI&1X*BYYlnUc1sH&hl-y4Qd(8P93rh3u!I_G}pxDyqU&`X3K$}ra|jL$&FN`kZE^O zn~KO;A>*>094x?B=3*LiL14L%ta7m$6P+UISoJj2;21bgJk}_s@?NkdO2ECUN^3J< z^g6K5bt=j+LP7Tn06dB&c*JYC5$g}t!==P~H*WX{x1|~pmlee4Ys8pK@Xj{D{yaIb zTN?7F3|c|~F*q<{mXU4-2gF3xB1e9>#>^1Y)2af+sYq!k%?M_OYm5A@{JxOx^(80g zuJ7D1x4g8YHPYBsI9Rr0?cRft729$XN?Vpym#s=)m6My);3}%^sz^$1^ftF_ z9-o?O2({K$b~kkHn2-MXhOt#%Z_u|UHm~;f*t{N(qXTok@3zAA7h})gVujo9jM2?Q zF}flB8T9S5lfD%dnsjCP+}C5DBOTrtd*8!x@b|~y->|^TE${he436)&8|%eq=X$Yx z|H^-gb!qndvDoM0zyI5D@Ec;^{~ZPAc@Mf+-(r`s-~MO0z6D&2U&8OQ!o~WQ@MCds z_E*C1w8F*umhjtR@CoF2aD5Ywk!jG{uwGB^&mFPn*-x%-`TPT#&wO62Z~6X@@OiB7 zCamvr)X}MfMe=ESwK{EGg4XFQHuLwab#w$jR@O(8g}cZBTo(6&HRelv#Jb>FvgCiU z;9XdV(sf>rs|-@B28@nGhF-L&*r+ovQ%Ej3%;r>S(y(2g7GtQNwmO~py-k`s{*>^R zk&%|pk&!bjO8abM>y1}ld1JU~Ho7|QSnb%naXrd;I371|T#pidw-qkdqlDjLg^Tq- zc&h!O*gTV7xE=||dc^aJYD_w?eD3S9&yjAp9_9NUj)QYOO87S{@IK3Xz8Qn#`^9>c z-%t9set(WwM_iBceUHUH_qy)1eE+vCaNW-JD4+ih;gA=xuY#);Vi$JUrZw#$>=f&5 z8wBMJfVR?4K4|0B5!<1kX}h8+XW1Ec=ouqQPo*@=qD(+lVC-_hD0 z>J-~kb!$y=T1uKb*zN7u+*uL5bMjaE=qhhb&^I-a(KA_C-Qr4pvCi9v$KC#dRerZ) zc6559Kh)~4>}u#-Re_^RycPMi<2%{|`8^Ejk+O)rUGQ2ac|exBSi)iJ(q0yD21Ee< zD=VC`l!PB691>#vN|VN#eH}b&q)Bl$LpxF`tjWNX2qOg)%tNY}ABquDyKNTa^q`H` zG6)?_XaVTi$c%hzP;m;_e;ES&yo1K(~39!=P~%L3eLUdQ6b}|0gtKn zPiXVF$}^eGJ(8dN;|1}@~iZWirAe7C=16MVUZQth|cRgUc(2k&8fkt(TRW9hUBcXbgxt_|7 zRH_DX1)oNC*B}+B6X@deXd*u8e%xTwO9oX9VmlJhWbx-$`3pP+tUpK6M9hyMySmsn zaPMmoN?aTCb>sXl)?qf&KwQ|yxGyP=nrhX%3Z#aGgT1yo{D=l{`W<>sWnssBugBdt z*U>fC<%}L18)+JG3}m=USNYuF!?|S{4Vf+Jy^+q5r>R60d0nMhNy#a`@x6V0i{pM{ zC&HGM>negCN(kJOXoJl~pW{`9dy|@NZk-nvjbm{5ymTIs)B+j>>^?;2c1h3k%eB5r*|RJIoItx+83ep@!y&oLP0}Sf2dQ;4>mM0 zKO2=`fqAG}X+WM5<=t$IKgE96i*kMhT;N;6?~H>xV$Xlv3Kx5!eEwDiZ^HYfJe?)q z|GIp?fQ$D__?>ZZzF)#WZiS2YOZcsXV?)cuk3VBib=vM?KtR0BS!qn>YEMWCwHo5Z z@>iio*jfiksirlB9?)FSfG&4P9Xli#4rWbNqdY0?>oj%JxI&K!xk0f=Nnt=&Y@c+9 zJWg0N5d6Y}^&I(A*corWIXQo{XT$u`9!J1kpwDw(yr-_?+}+WyYQ68yEUGGseiHQz zf5P$FKMVY@zICeo&XujmhDN<){PuM{4ZUfiV6AFUE3T)!9wr{lVT(|+{HY~9?bMrG zmk=SfRPGz(&v^gsD_gj}R-OaAk#wTqcPaS40^Y`bL&1;5!Le^B_?-&=GM=yHzMCizVHu=0?3lMJegI4Bog{(o8qvq_Xl@39ZX*^?|`3;mkjh#5bsI44CvHa{%b1EZ) zc%-*|!&2{1|6%>2QxEiv;nnCf-mT~~0DwuS{|a~;`HCjtitcPv(J0P+c^UMTFP?u; zL*~6)H-M*s`{AS-xcuUB$d#OJt{au4Wdafc5zX*8Iw8|l1~Z)?8odv2F`(dWnBmz4 zUd=wSw3NK&y_Y}o{VV^l=5u=5e{8ww2hqm_j^#Rh3H@Ca8{gx=J=VFX$tvL~_D}H7 zgh!*AU5rmY|Df&fB^=LxK57bXC*ML(f+ZuGwhxt5RF%iM}LYIP);Eh_%WOYpG)$k;%!W zefuUl!%vjVmT0rlr>bVFqNmhY|14zKLJSgg^(Qq}jOoupR+aE$1{^r}vyd?fPqja& z)x>b{XJG@9&)=e+2Zh+}sFUuu6>8MfQfz1ACwM7n2=9Tf0=U6uEaZO!H!7~$Bmw1W zF#)E=p*V!n&SI1eCnGRI34uwANkW}~{Dv9?ayt+tBGOkKg8a4K4PsLOv0#f>ztA%@ za9Fz+dxEDp`bj-)R_qdodqznIl#kN|&Kw+({r2DBV!*{dFX4At;R2Tuek=}7xnIKX zw8DklFX6Yv;1hbfz&+uV%u3Afwe;tX*z@cs<$L-3198un#=iHX*5`%XFTeLAgk!fW zTlp8=t+!&XyodumPN%LB2U-LYq}!WUGuk*|39Ir1*zt{^Pamc{T~Ih9*<~TpQoLB6 zi%5l#iC)~@(}kqQY|?bRtGm%&A0K^ zQ8;~Q;!jU>il>lhyH=olH%FWBV^)a~d5I{bdOCp;4_jzC zBy=1}``?VwKGu+si3x{}gXh(^i@CCiwM`vIe(tf@=U&%yg^nZP-?qThg^nZP-ys}p z26Jma1pda9#Ua~&(gHGU8xaK0;6(dL8KhVelZxsn71dcxy$SA0H=l4@Y`<3T1tW-n zUrciy?o7MjDgk5GdXM0RCD`kFL^n#Lo?HS&xWv$tOAI}^1bVW7Qco^HL=%2N^-0zP zVDJD>08bUw1HNYK5E@lKu6!@{U+L9X1yf8zFXXC(6L!dePt%^YGEkSGuu^H>y=G!( zVTxn>fI)1sH#)Mn~sKclhz5VbYC$yCYDK@Hr^WShLN| z!8YH>&Mum8>#CuxVJKLe-0KgL-Y`NzpASfQ6Zdq%vnBkPg7;wmI|y&0d>;Fs?OO33 zoEfcsQ@p2CzUOiI9to%2SHh1Gj{VOKyYDwaAEUPSlAp@j-p2k{!V(vUx+u*0 zg$V-+aT8BSvHogs)cCX*vJtEK7?qCNG6TXET;5Qc-81hVbB;HKnmmEf`mGh=@%pg^ z?{MK@U9jF)7n+^ky-8S+UA3?xKW(uK4@yxwc;`vSk|Cle}S;0A9uM7N_VG@QRn`PNHlk;W^gOY)NB_^A) zS%M&K<+x_32MfD0I4Lf$Bj2lnIF?l0#;FT1|EPnbU#hx|H} zCFlYs(N+}qh7d*7T1KzPC5-hB6Ii9@()3HqL!)(l3$CHisIS50t8|9Eg>!2zd(YDJ z^rcH}Gd=EbM@NmjrY8-hg>?+de|;zKkj}=1u7mCf?LUnrJeW@&~1{Qc0FVv!qpDa!yX9 zv?*mkVUef9Ql3OrNuZBG@T?aJ7LG_SO5^m2G3At+z7fvj5VD6jC89{hIiUb?{?x$m zWw&V?@7l8Wx(oU{*N(5=Sa8$J`mV0k3s-OYY381-ONSQpuKt3)*7@&8H-JY!5BAQ_;M?+YgdV(d`cLmSxH*)=e*zP_Sx&(hMu!rt!o zOQz3XC@J6i!G-guZ=t+|aUHW`7v>#ggHN=|xPdaP*-`ygtXaajW}g*!C!m+Y31`9H;OqNlVHNq%7 zN}A8*RWJn@PLy#CLL~R}d~ZSAmOlj*YF+DD2M?arx-LI?=_N!UbiyU2zZ`w4Da@dQ z*2d_ITG?dJmYzxBlHxu*iT(2pv47Gg208H$xEOGuQ%U$;R=C)oCHz<%ocn-;-)V&l zol3%Qi@_(fH^hEUI3+>IIL7x{`g2F@dG?b!m3;mI%^iFGPj>25^8FvRJ}-1C3I7P; zSksg6rR2dT(q#Jzb8lj_ET$!a-8YiEH(CS&tLP^_k{}%ZKa6CdY$@G(<+w7E1uOr| z#xq2%2p2w$N;ID%6;XRbTF~-K(+vw+Z%Febp)`H7d&9!gqNBF5U@5_f)>UDjEvPQf zJ7>4HCi)nSv&Gsrpe{Byj8Bm`JsyW^>RA$gOAObq>x0mcxV+@auEus2Emf^qWwKaua3aK2x{jaXIz7w?zw+pNzEenB|#e7or9>F>Y8`n=#5 z@_8ecRlc9JD&K#LdcGRW;W5#Fx9sQlWIrXGdW3`@v%-bXM#67Za2)%qUt#|yp7)6N zJV`VGpA!Cw82srt_=7R{$vF5IV(=4j@M~l69~khJ3Fd1mIPo)S`ChX>PZ`gPets$T z{Ey?FUyi;1dwd=~h9dCIkBS@+ukD|CkdslJ!HtDz=Afv((99}Ji1q#d=VOp+vJ@Jf zl&x+8?cR;Xs4B)xF+HCWV-+$oz#YN`XXd4TPpTIsM8aCq2Y>5L2*qvEYWt&q?mbUy zOWupa15S-4a8S4*zVQuxWAq7g-k*)(^G82?1pg+n(x0@BVrQg!bYf+krNq-4W3{$12-zG^pF;EhX z^rC@YG(j)aN%BO#R4O9vGO@cz{XP>3@-lmho=}-@O&>tMpea`EM@T~Kzf2Sc&_4Im zZAY~odV2H$ttt96PLLSPzw_RQAJ!jQ{MO**o|ka)4hcU=IQk#KTAfGUr-*H%Q+Wc3z| zL}rOeYZmjx>acR??clWlJ1*tw0^3!rQ3oZ z(x$}Ei8#D{AO`=wf}=jHe!iXf1lI?iPtpyIS;@wacY*fuAi~@ti)qOMo;2d*$1I5n zw5LQIfmzZMs|J$jN|l@fqO;qs06+xEd;vF)qKVvt^%BauIQ+Ab1!pbF!D^$d_b81X zNa~SM6!n_4wO38bDxvXe~-@t*RxnV zO_*yhYka!YE@#*|5_UJMo+}oU(hr1LgCR_IG36-PuGHLAu#Y6bP|XN`m5js-D$0D2mT4fkqJh zfZ{|68(CLbvIV!?lO(RbYr*Y0vA0OvK4;*Tb7VK>8hZksS2HEvhb=;PSU&fH@j1X7 z#GWMI_j3!}k;FYg!e1gB>yrHML*R3$ZO7ATN)YbP3ZK#kW9A>JFgSBhu%;U^x-D5u z2xHo6#B{lM6)$oj;3N%yXEjwLyHOrWA9rl7j9l_0yAUjmb4m5m^5$Xx$Y#e`xuKEy zw$UTgb#>E6Mn?}%)oCwAKcba25BdGW&1IVQq0qW^=WutxKhok1ZF}GB?EAKb8n?c0 zp=x`>>UO8IeRYGu=Pwp}ydUFEu|M5`i{k$m3p`8s2?LIJfQ$9-0AGaDR}2L%_B_{= zeE$1<9(Y77p#BJGzQ9(j;ld|n@i(Q9nRrxIJ`<3z*{X0~a6s}sMDU2$ zip_Q>0Ef{;l-P0Hi12t;7FB7HvMj0iGZ!X<$cn}E@C1rt@ggL9rXp>gCw=k!Jk-@= zJ-lh$7(j7`*9riF#pdFs5=}qKFnu1V$B-s&ZglC>z?Kh#lO{3(MdYu?i2lt-BC(zt z{r2+s__Ef+pV4n?q3B6nn_0xanNus;hnJ`)$~}a%Uklu(*iUrg0yt2uog?^YK)|2; zl!W6c?OkGSCH!dxhxDgiF7_e`KN$z-`z8E@f@2HQ&a;z`O85^HTz)U-T*AL^z*i=; za|9nH9IF$dn`i3hQ^xb6|Ky|c`5(tUPd+N&|2;mB>FR*Z@G8Ma;kQ1Wk0$Jt@5o2V zDWy=tO_UrJ22sovMS2$A`Csx+aY7xBi^TcfXG+}jcI@1#g?8<-#3QvLE6U(vB^)|nCi*iNh4_)?RH5de*qQJ)8-68PL=ds3cU1Ln>1f-~U6XoF#$b2n#HDX7 zqmWY60|%eGylKc^9PH>G)UMqbJwm!*e~0bgL?qOcE4wfqxZ?a9P`LJl7-zZg!*Tz` zF?Ey^euE1>NBHA{M@jez1)l|vx?0d*Jsu>FS`g1;{b(9O^C;ANHw%)?~e+j&2GyVnQdt6LZLcJ){e9vGRWPuB| z!wN&RJ?S7m-ZMssgvi7qvP@CZ0{7Gls3C-!16qD8Fv#K2ei=P)_wMD-e_pH5?Cv?Y zwk!J2oeLe0iSd9JpD)JWD8~6Dc{Il<;FPN*{GhtKEttLR<#qOheM5sYt8fEayzIkz(J6c1__l-ERUba}AH;*$^96pI zBz__iKN3!PRKlN*gA;EOeliYDJ}TiS;^5??68?i2e1fuuf>W9~OU%pZ{dp?(JY*9) z`6b_zYX5QE^N+;d|3uvLh!2IeHEU}g=P9~tS#ki|gpP)Be z=7q@Op=KNq>al7_ct$+1r`4~C0Pi+!apy?q#Guz-KYe(_y!~^rmKE#|jD*UxL&ee8 zu)-Z)=m~Rtxoqv>U6O?dxz8T^2WjMu0QYi1rJh3%n=}$6ON<$KMq= zmU84VyB`{DGvk4>p&rZX>Dj=pd46CqHRocB=B$K$b9I;jk`&Ux7{DZWEytDUXPH+> zRJijCwm?UIJv_HERH{)|Xk$PSBj8aOlQ=`tm`m4>Rffu&8;Prt!_)P=HrHM8@3q5C zWo1pnwSggChwT2{HFYf?*h)M#ZGGQ7uZRw6sc2gpIuu&lRuQ`~*3vP7r(d0hrwJYS z0R9M%r*-S|R$4zsT363oY5kb}y$YUUf806`LjIR)K1>w*e@VxqVbFEoE z_d@J*SZhLWknj6>9GrTCgug_%)EgjieFs}^t?koP>y7v?@+M%pTI#Els+R#a?idV&hqtgPF)d)Kby)W&VsF3i7gTVran zus|)Oq-p<^n!2>>(5|J}Ub9W`*cM3_w1cQUXq%8pC0y7?2*)0@P3%F0a}Qc0o@afF zYVn@1d{0!qN5aXICHy4e*qc~Sq8jruN`H~QliQVk_zgrlg3-zzSI? zECo-qP#6LvfltQj^2*Z({b(Lou*zV;VhISFh<+5dEEP&|T7Zo=!3AvyQ^vvTv0BMm zea}#PXWzNIFIqCAY>=yIiO*THg_xGY%ip+>GK2As*AoW5F+aeof^&W- zi&7SNT|W+4z+*c?<*!QEC=6L3fw@_|Z(^sns?n@-!)O-@7NsJJ1rB@=JDu3lU{NGU zoKAfw_J0}SqOd57(X>UVci~J(RIJgDS9Wg-Uo*VAq$d#W=pDG8 zHm~ojoEvj_x*FY$ksS}8`i%b2{(nr;%34-qOfD>N+HZ1ghp5RSPXQ!gj7i1EnhaaT z7YQ0-JS+_|^597BfDm42p<~7gs|ndaLexN##h8qmKhzwvsWqy&99e8L?9g^D-+nvG zY~HR#qTkaZGv8la{QiuXrxNrrAl6YUUJg6s8@OQpN-$3Wv5pAmJkbZB*-Ef30EYyy zm9aN9HlJloY6#|-loMNwNrp&q)1i!7jjmF0;x$XG0d7s?NVMdlCJwbAYb?d-bnpEY z2UKB3Qk296TU6+?i;Jf|XU0%iD0eL;gEH(oj3W;#?*kMiWb`=$U8sc{v!sM>b3|6I zN&!;T3S${iRk#T^ER%G_~^vkZ4a97WMB!+`3L z(Iu=@HM{)v(3-aL^0qahuYb8d6srHS{?N6q$W&9)RK#`De+TPAbx=ewchrw0&!lY% zG6$Df3{JUJ!k>i|&fRjx^1al@c5#dgrb)0`V+K{&ggJu=mZDm6b89^X zRgO}q4XmO-BdJJItpK@Hz$97$wq5~~Xa!876)=fbz$98>y8?|SQCU-xCeaccwpW9X zGqVvyoiEZuMP+83Hit6-V(FqFZpvG+l+yffj%?gGayYX*=&f&wmEWl!$amM3*$eAS zCeG?DhfcC_Vat{U?Y`(@c;i@~%NHy`Rz0ik)C3$=&hnh(l;rBkBkQzdvAL)3PvY)J zaX6(MD&gOa;q-O=ZXt)x(l8KiHWoND$J2C?eW&>(a)|sGFdH07Nf7bxwq@Npa(0xPjCzh9#4Jc(eUl zA+t~}fDg_FgAIAJfOB0+_=z|;*QJF2AP&y;C*e;iI4W)GJH-1tCEn6;!TTkg@0W1H zKavSs`VO%t5KeqzZ-_n5@0HIRev{MRZ}>qZoaUI z`NdCuuY{9tN%+Y)IPHuQ{(Zv1CuF`+8u%>i4ath*8+<}>dNT$nA)r#Re(6tPl6yi< zyOrv>Gjn@^xfiRS9)V<0{QyCPmcS?@B*^bAc1)=-D8vFG>!N(HI4SRO{bbaLdph-| zhz1HBxDx1~B}G3QJTDo6!#MSvhoCZESB7P3Z@+(8B`5A*+=D4FQQE-ipE%GKHX*Ow z)A-B>pE>oo=1l&yHnmgVdg{|V5k;^{;?W1Y&8P9*;!IlOBwYiK%Iz0i;I7G(eC=oi((!t5y$hN_9iX$&h&za4r*GpnOkEA8FDjVdr=;>N7y@->W% zasVrUP-tM)7FG&p0g-AsH(_xampl!bS~_ZTqPS(BJzhC4c6cMlN9JnQEU#U=yr!l& z&{=i-jgp3{!qUd!`ugFflESKnk~bDJs;)n$|AGai^<2>3Iyc&ypRssbny zsjO@nz)gRvFYPu=59_QPK%N3K3*dA1#4^7;=xBP3)|7+ATtP}~HvvlKH7$0gO%pG| zNO|BCiP@!`KCGfy0JuEqwFp;f^)w1#%Xn)>Amc!Xc)gUlpQyE(+=4TDQ1DJ^rTT$z zb8|*^QqjT`YAm~4?Q5H+wiPFplpa0O7m40p-BMIuUer=u*U=FOuiLt%W^C_3|K9PM z`RzN_L|5)h4|cCz+Z`OASTixE@X`sq6k&$m$EGW^q48IG}1wKG8tU#=t^nYq&i*sc>yu zEJ8ODeMOaw2?d&CWx+P2o?b78vG*S2EVxi82GRF03aS%V>=?5@HfF^I%s6CKcFkm- zR*j8ZfVIq7GRki&5qzwjQ7|C@2GC@E4nDvmpxOmPtpH1vKD-K02|cE=QgQ(cXG-yz z>YI`J#}H)^s4cL7giq!(NNsQG@KWn^dqttQvCQ4wP*%}4)!e!kQOjQmgj$;0+ae<^ zJ;TG@P1+wXyt=%3q_(=Nt~@0vskC9Bp?OnJRnQ-OO@B{Yusqm4I2^s9CeYtq(b-2{ zkPbfeCgOYZZ09q-BR=<6`JGH6P{wK#piDd_%B;GFAQS5bL~T$O8zuWl2jdc_d$$x- z>S8H4yia|oH56>lX$z!;=Fa&BGf}D<@A=rr-Z=FpI2ZR6k<+Tb3}LT<=S*kE);VpD zW@e`fjy31jYHDPH3HE4kGFpsePA9TQRZeFW{+5l|gNo{R6dRMHjE!x6Fj+ecjVP?5 zic_^|mD88CPBauS^N`73o{q`d>$L;@=_PfZ{&ma|)2~lW+Ez0$xvsAN>@lBzbXQm3 zu0c=AdCLpY{oamn*qOF%dV*bwe$SD!!kYRve486#y zIX%aUXz11)-YTP5m-gD|@%@K~dOEr)+NWC99;g~CnrLnd)%%w(nvZn1uPI&FyLl|q zm!6y&3Ax*Ai$lQyx3{J=tHQH>Fw0xxt0oTWR<>xL!8}*lW@sakr{@|tP()_VuIV@D z&Lq=EY0ki0iiJx4ER&09E6kTsAkc2uWu^i-3UiT1=4aMWpWd&vrFwg{{r9!`n>#0c z)oHamYbV=Q2MaUGoBcP8-><*C`T5c9{+wV(Zb4?ym0$L*%8LtfYQlp7XQX#^=-yFKJAwuj8GE0TCV#emnqj_=SVKOWYJi@FT>=V>CjT=#j{&L>1 zvN<6;`DcVPc*O3ZDaL6Scnf6p8wZU3b`&X=J^+VS5t-|8yL;v$#X% z1S^~kt~~uU-l*Lkp_CX-fF z`V0V+Q*dC1m^xW^LOK^^1%^nh0tL?tMpjUPH#&^M0-YBZ`kXC2?|SO0{}}zWp7y)( z_ZH}$$t{!qn=%yp?M=$MF!Td34eOiH0I*w`K@QOXQA8bEnI z4=YfEs0$!YLXgtExj4M=$LH?6^AEr& z`%HPD1^4B)zoRk|ZzuS_(5cKS0pKKTpov+*z$GGwGjLBNQ;qk5A9fGsllLc#^3>HjJyek;ET)R%czB7{3)Vk2o zTv*>K*gH-!k^H@7sBUmaMpEhFy8fKBg$3yLQWh!HxeSPd60D7_n2S6ap%O1kDFMV> z3JJ^Bx)eAUrida>31rBGII2oe3zGVT1s!^*_9o&{y6#(=*N4wqSmbDJ5Uw`#A9}N1Uztf5) zM`ng5CWdZV@zC#HH z=(Chrn?V;2=wQ&ei()z$tC1AM<{&`_v#O$$U{p0*kOB2jrb9zU6~`9Hh$Z0;hYcVW zVoue;uRRc~i;1TFTf4Qq=xe=W6idsU+Wl$8n})l0jrx3JdwP1$8m(Cv8CgF%GBUb8 zIDkYc4Se3FeGh%lvW;_b#^;|WByL*S2`vJ(6gv4?)JxGSXo9xH*cPC`= zDvj9ei4`4U5n1`30I0IaLVscmS`GS3KJHLe@`*ONw#Wo&dSK89l@XCY0QuFWL+AnB za;f3ayqFH{(Av6okNU<&heCD5we7Blcb#?fRR<1C&e!>T^;tDzXZ7v9!8=wlH`wm) zs9E)+|9;o?*WdiGRy4ML^JHh|q*!aYz=I9*P)z@FKG>V}Omh|8VGcHd7!*C5x}h2B zB+C!b5mKzG>oCZJ)&YKzLmhlt0L2hlM`63#^0mH-+J4~$_g^^DA8g9m zlyvC@Us)c})7<_?wbbaRqQ6-1_@0TrtR2x(!2?N89r`iYyBxHDXjn#!hBfFZf%7F8 zz!EA4s%bR`W{P_tbP5D!}fq0UAFZ81MgqD z?4rjXzV2!*E&8+D^mm#?NYsmLS@gK(;(lQtKB5i0vMZ&)JzIBxDo z6D87j;^r`ap%&GIn||>Ni)YY^Zd8ah%uxOqO^^*p z&w#Ne6XFg`TccWFR-*?`%%Krn!-=7OZz_0%cHOToJhVD#^3c&=GviQO_~Rd=h4zp=$aw?>UA z_zd$+E}Vc<9+rrb({(zBppur%4F3fbFEb6s161~5-CI$HOem$m1k zpL_9l+TOi?dSmb6>(NE@sRZB2n2Z!0ImJE+f0_8bQ{XqW@-w?v|99-II39y%4b-ce zGB}e8cxB88ubsrNM&Yt)FcY3@4W4T)mT<__uVfxVHGubvHGl_==mvGO(|{b8H!NkI zX?p=ptOy%!^7sqQ4o7gqWACz`9ZW}@em3l4*{CIzjq`u9ads+lBpBLRiPnV1vo-0C z;o+%tPsNhVp2dTYVq_=LUO;1+Sk5t{Naa4j*im`HDvySa0HZaA83RWNpeTtMQ-Sku zjabHR-3V{w@yN@$6Gx{5f$5_YxyyMIN2daTsiPBl%X(g5tfiu&Wh{{Qp4eSw)9gj# z;}^|@+S@}jyqj$r()T-CCmZ@2CR?4cn`k@d3R;Z5U(8j9pr2v<&U2#6Z78XQ*b}3k zi`e24=tu4D=0Zp~soRu($R4wlo@axemnrbV#1b+}R&1$ZaY#7bA>MF{WOyfC@DOpd z?bm*Ixir{Uvs}{`EM5Nawabs(aNR4fgvaU&w7a5b71WP~UwP%a8y>+|U`)!_g*5`c zgWrXph4DMTnjw61P#vrvMN)b`^6Y?$;bB$et=n)=1A#{Fbb_I_CR4 zmBafc?zuM*@vcfrE*$M{T-Q;_!iSwhL!CYSLs>QJFPYna``-4H+yi+@CCwXpT3fwC zJ9~T28uM)#9~~SWA84N&9p^qro1+fdp0Q=swwt)V4bLF89I}31&1_vYHdB-|jb5fU zZItG!##?2bqH5ZBpT}3ciYA!t{kQ?oX6^fG@fVtQONF~31rSP)F!zab5aAn>VxvbD z>qCyNnXNmeIff_C?eBH?JG{}Cv?OO$mGk!ND_pLM>-DsWo;}&g$^BtZN1%9<%USNM zy!mGpWu;{m@5S6={H>UO)_Y~1KuFqMjdP{R&&h_&^Wv2}a^{JqF5ZCYrPCQaMTiO> z@HDB)t)e_AV4Qqf9XRC>{)K^r6P?0QBFj`)^NleUnra*h@;eS*P}w=%9NN(38SkE$ z(Y$?gZCwLheHWeG+T9gu>CCzmDRxckA}(imtgd@DD=BH$hStqJ?yVhd?QJdBU#{(J zYpUk)gzv|;N&fV4%zLs8@g7Xkp&8AP2@>Ym8i^rg8TKF23T#hP+NM`fDtv)fY*f^P z&i*9y60dC&SC6cAVZ1lNXJsxF)1MAcvA`33SX?*^N6At?4b-%P#@ny4xSO{s)er9} z72aIU3{XpXYxjXo3*MQAx{Z;c4VUlOy=hxjgRiNfVZuGMA#3Ag&+5X|yq-$Wz|icP ziJ|Gjlni%;ySmypum|1M;Riim-vYekfZyVTn^jCrBoKP!5{S=tSsaTP8E zuUq^`bi*%hxZxMt?9jRI^$EVk8v5j0zzu%qKEbmzSYMj+fJ{|;62{SHdx61*@}v}F z9un+h3H8?!iVC$+rEIg*>r=6crUvQb6DUF5xA35?4I?EM9>SW+R7Z}6$N>%ma5);+ zI**p5?;uZwn^Q!R1>BfVKy8T*TGoQ4_tSXF2N;*sgSHT!Y7j__^l7l}BCLYG0hkIX zK>;Rw8Dd!hCj14OIO!7(zk1Ez)tOmY;Bz#0wRxNKhc_&37|w6@wskc-dMWAl6I+4bn2+q=iLVSr`YeD~x&l5?S8JRYie7(Sf7qP3@mL zdh|f4ca=Ny{EIHaZx>y7-m}4!r0l>WPiUWw&Oh-8aAGejdiEv!dsgBDbtAQ>zz^ze zAErqs-iNJ<+?acb$q%S`A%Gk`G_3zTkdi)a2CAj1z>7T6)CmBdM^g@LRDR&DG62YW zLXoqaD|VF%P!U}UjB~|#m}2l|kvth*0Rl3DPg4D=HlDx-*5zwci`C;LTROIOG?xaO8+?KOQD=D6?`f~ANKUp_1p9ml@J0xMkSo%_e{wm_WOUD(Pmr*` zB&-9g$y=xyX89SZeAA1Wg~bn(U}_klfUqP-sPj)9$Vlq@@>%kyo)*32u$FP^O9ZF}n?0_YXf*Q?YQUtS~t#$x(LMg3EUYl^bKM zFXQ!yUC4tAfG1)Zw5rBx^0EX6b%KxAoI|pLBvlzGAJog?$*?2lk8mjYg z=`gVmQSWp&G%;%)iA+&MY}kll6on2%SR;3m%#o~BS*hB^OP|UtD9?4Y`)U^Qf|;amrzQ8l2D=hCD}TUYXFwyp%)Nmv4LjzW+>ZC!|g^I&GI$=a-%qbI8N8 z4IU-Vut=d7jywx~43Fp!v~(%^@k~baFICJreRWTY{`Jys8Q(u;F*WxXYyZs13qf2e zy7{4dw6ER67(B}Ol4sfj;`_-8D);c;bsEa`0WYns0ev|TZ30DdTetH-wE=+ z3Q91Ud4ea)>YzN`=t;rd4XjDqrZjQX;!EB5(k3veAbHxIWyA8zx`%^jJZ9(n)B)YM2`R_(-MPtW2+?a7nI-NTwgJG5!6XKa)B5xp67 z$#oDFd%a8YLg;oI1dqYqkbD}CNf1F27K(`zc~FZiUcqo?7*O88<0jSjdu;Ef&n{j!NYIUypl>czYv`)jtjrF{2v%7?KNYX0uq9`M zRl((*M|9G|I1Ciaw-k4Ulqd?{ihjSEUR|HA>sm6(-G*51wmIzZR~8)Fx~IA_IX#&b zZqMDt3b#G|qQvcv=*CyL&meVbc#Rl9Hre%!031gX z;{7ykL_DBjh^HbcPtDQrC$8++>J*k@DFTDHYe$lcMk1SLmKww1-le5qI;%I&{o@xJ z03pbzAW+E1g8gf9Q}&C}eJj5>5o8lh~X{_{*NeFp!X!geUBM7!VU z3*=9GHTvSW-AVpgD}s{Wc0WvTO(&!?sBOt)kBWhsfH=hxL@j;Q9^A>uQYWrdn%JFm zg=yMjFc!LZB?%ctl)0A*7RmVj|C*Xv3br_W&Q*tY6#88SJhNz^yLDrsp0hsd+}+yh z=wrQonZ=$WZD;hcOXLkQhytDA_WC}~%gXuoZr~!9G7Lo-t1gkSRs7#Bn$F8Gj>x+ z4JI)ufOYkg14}z&Dz4I3&no!RctV#w&(V-$W9EUp3!TOhxTP8S7-ybUa5j5Iy0<<< z*EDku2Tq^gEo2rMNpb?;W`YIZ(8;6qVc;zXx;^aeh%Hf7{#n$)w6`TNV{1BrA+<2A z1&WavX)#%c4S+l}gGtEL6dDn*3}GyCNIx5DiI~>pk6!b;8~p=~WxJ>T@+)6MgYOc^ zJ@*v{2WmHOneli7UiTO9THTfbds{F1%|sg1(94W@`MEiIV>@jE*RZZbp4!0ZR;qJ| zP!;{ofi`5E6iY1f4K(tOHIO-ks(CmJ_b zs1gf#{k@#7T~q)49H(npyJ`v2GiNNY$(S+iOJJ)wmjFzja(f1-ZV%^7R&kPETzhO( z34>59M)bJ=pO&*yAp2Z^J{MT}OtF%o?yAo;Pg}`rYU#$Q-MgkvN!mh3w{DDelXNu% zd)e>MM+cY)@>-a#CEW&$5rjusp)rBUnRLZaxfIpMEKn2U1Iac~Ju*g35~(8g1H1m) z`Rb84P5nh+VRG&2)T<|N0H zfuKyoi37GtJROEubU7MVlibS5MtB6R6c&vbLU4R}{-iwHidKzyhohdgcyapvrKJN| z@7mKYb1B+8`uaN1vMxFN92Dhq&o^=pw+@ZZj}M6-YTUnoPQr0RDtx{8$m)qyZN|u*Y^W&Fe;dHT_1O*t$&^nqr~m(5x002180WiU%*2(MNPCG7^-l z3Qk3_q?x`ZU5b$~&G|Als~e?N+PuQqp|vSZG#uz~I{X9UrHuprQ%i><1tCwLHgO3# z=fXfka#HG=L4R*U@u@c-y{N8IE4bU^E%P7%K*s{MX+OpIcskb+n(w6L6P{s=&lwwE z!peR}BcK!OQ5{T#5zsZp>2xaf#pM3NzY{kouQ{T3XIB&sj)?&sI_JQJ+Tdq$R(6kL#`9-4Y9E)qBE@& zahn?kAWLk@Zp(T&_$Dzyq)Z25Tfk@#H3hLlgy~wm*|XX|MZfge?*03BN1xDwESSPN z=l7x9;dy5@Hr6#ZzKDLapRw3Q@L2rL*hLTmIGVBijU6JPw~0Pnqd)oHS?#*$uMmcZ zDm1`H_R4-lkZ}7iBR+WWd4(7TlW-BbT^>T`d(_0~UW zzkkn~^A`q75XQkdT!lVAhCb88`F;viGFn`Iy&mhck|WJNCs{9~#GIl7h~Kk+1!IfY-k?LZ!`6w}Fy>hip*bRD z_c8%$37UWim5qqSms?AKwW>^rfRKD5f?-M~)PfZf5rojf2-I9C{bGyViJQY{VoN@b zoBR0-im%HRor0@8ypKymreBh4uzUcGYfOx1(1h_6%h*UGQVX7H6nIn|062svi!NSj zWQL2^a1#L2BDtPcXTkRB`gZ136+5bOHuOb&ZH4;|Eg#xf*yfA$ZOEx|6j$Z#>>Kk2 zi;n_)v^eM;TYT8SS3gVULsoa}NxyyywO6g{CezZ4|f zz`VFIFaLyjsg^a*JBpYf4KT%ZC-vQ4Z^m9b`!iBBov%Wy>U0ht1d+UXtTv}fG zmwiHWyIjdBsjhOZAo>&JOR)W>!kL~ck4U~y;BfE?ne z3bIcXY{5;89kiT*CJb0=d?p60`?}Tt%dyexD-S8j!+2%6Ug;C&U(e*+HP48AXXmcS zyVuG$Ued76y{U6_b>GmD{e4}%eeKhw>jFpDU-+JNy{<@IY4ymSte(htprN%bGt{~m z4rc_L{GP#Z=h?FpYcrDVMgC5I(^QAFHIi4eUhF&hKh1F~0mlSbxsX7W={q~!|CtZ} zCl#T5_&+u8?4eBin3i=mFVWXgbtQV8i5d8dso34y5#`w$m_T%t*ei3t&V*g=> zoGbSEDEjQQ-Sf7-O|y#$?Ein_+dPgv<$g2~2}b*bVkG5YB!=O647Cm*ZFs&8z zN*r7W9K4gy)5L+Y0h)k!6nFCR4N}~ZK2Hvdr4~$|=jrQlcKM<&UUW}hWl^!W;J(Yh zb`iVI_4ezsvb2=s3Rmgnqx1-_z`-sc$KP+Ej7nxy!P^3$ZT?F&o=7 zI7H0vnX|6YRgj@Qw)~6i60f7Ir@nr%uxrcJJDxl>rq5ou_d{n7IZBJUpA`Y8=s99< zp@^yd?aZf6o!DT%`bDC;;n%#D^r zp@Wf26Fwyh8jT^>gJ;8~N(!;pYwtZ&_Lp~E5lL!VTK+?3iM!slW^q6-ivH%xtF`=7 zpDeF-q~jG0h_UZ7V(e{KFy2I-+bY9=oY;RE4kTjivk+qs{hCZSOGePcIw)^;i5o~b zc$NqG@|ug!(x=3;)OmFRPW_SxtMCaU*u4XPm*4SDA?}fUGsa!B#<*YX9X-6RE--a? z)O!@SQ-Qj5ywz)19H_K$r0&7Gk;YP13#y3(rw;V@9heSo*%F*S(AR%pD!AnzwVTR< zebx9cSQfiMU3TE6apiG)4{94oe;I58u-K~V#myKGeU!Y4EI*!_Dwwd+-k_gBM|Jo7p560bnBTp7zRKOO>4J%g3pO>(&&Te*sy)28wQuX9_z@k!?1Fv~Ngfy9 zKMUFUNzkvTk#)LXEJ0#U*sDxKbWBFZXek3{BBfS%#Sr00?~=ZF>i5f9aQU2bvD`k# z<%Vc^oOyu$^y2#qZ95WuM+tJ2S}x}Lc{|^c>8Xj#3~l47G2l${AXH=~j?Um4O3<|u_JxE|nC5(= zsMzVgq1ol%;=jKVQ#K6tB(vGehEz^+z*5u)KU@)?Js3EUgU$a1`8z?b|n?WqtfkoV)|!_x9Txx4dr){%_p-f0%m@ zz&eU+Z+vIw>R$COU3FbGt4mkXRkLh$$wjs$_l~V*TegZV$+*xQ2!tebLJh%$5J-Rk z2@nzpH3bNrkc1?ZKmq|0$|D340@C+8GqZPZkqs~Jec%5t`|i%p?#`Y$b7tnuX|rcw zd|;0}sQN;V$>)Q9lCgG#9FxLDVH8|IVKPE4uD>15a3PQ85Sth6+H zNXX7L0~1*Fq&{1c$qv1w0}jK#2lZ=2{o=7=gd8&#v$%3MAkCJwXMrA`t>&{RQg>=g z*hP!a2x(V`5>PAJ{*VSL#NU-QCFQIV{z)lD9tkparG$?h^VXQm6tRXXRJQN5E03@% z?kKCMD0?eC)gEFWTGzjqDl_vJs!OrEq=M#g<;w(LGJK5IahE-hlQ*AiS5^MiI$!xZ z(etDl#qtnRgULObOicCdm9l z_jtMPP~Ng6)}mby$+ZUcB&|#@GPdDI(lKRQN*q2#bdmfLar%&OkuOLRfJ(~S-rWtb zF1l#!_q{J~JgfKj3g3v;M|#XGl5`pYV`-2FfiGN=i?zZVD#4!8iYr+iOoudAo2*4J!XP?4Q+1(1G!X8y*VlRLMA&ZIAk^gSf~nO$5-Dn2t`?s2U0FZ0VM z`c?T?Q$EmJ0v+VLQV01p8p-^1kb1(i=u#7rY(G-snw~nQfRX}BQ?M_st)4)iJ`F#u zZA?-Uj2Pr(Q!XK_5{VY|2zez~Fn-f-M4Mv)}#*X@mg5sX0kiM`5%SuYz&8>th<^Mx|rl&|8 zUN)j|sPcg;oqsju16PHxvB?2V)fsP5A6Ahv&G|XV8z{(rrASGTEo@rDqaH8)x;R66Md~NW||;ue5xFj_*gbevoBp2chFQ5xs^s;Na&C z{s*){5=3A}X>L-U*2?N4-K)b0@;%cWs3q~RBnle&))+uu1}eBS7VnbxBs2-+D#?@? zx`~){XlQ^?+Q*mlekvLl#Z8Q%VG3$S3IX6qf;8UiDg^B1BsmW{ix2P})5S@<4~(rC zSuu7ZdvwFhS2jLf8fv>~-MX7>p`}m%7Jsbx47>dd(5mtgB3eBSc)+~$h@#1~*=gxJ zR6#aXap+F8Es05!|Do?t9A+sMv^9XjovY^FMIx) zN9yVp&MR72}zLNM9|8&>S2`OEg-;-1h_t2(j zXIxTxbW3APjyrK#{?bw~sn71;e@=0x_Ml2khf8ST<vh< z_qN?+W|_Ku#rt0~z1(;o8QSdA&TiV`((-uQpsi)qn7AEvk1@_7X{^8fww8-nH4b9T z{R~%#bF}1@G({kCheV`MlrKQ}LROsCD73CpVFXl48FuH$UQMGP#hZt>#-{_)>1)H_ zOJvXX47qEd%`k@u8p{(gntz-4F*TYie$0yhr)MRr20=?hOG2&rG0Vt{hAC4P7_Vv& zj~c{#M>kT(#_Ap`JGUV-ROhzKDykjrO_NiiW7AZY1~026SByO;jb*77pBv-ZviYdK zy6s+i$+U1*Ma*t;k6zX0c2 z&E>NmpJkW{H4Z<=R=tK=5YTW&gL|I2E)0|FLhvp$*OTibh(O0>Mb{{V9~yiI46h#= zMHe9ct-;Gx2a%nwB!(bm&`TkzOxxd5j5}4>0W2NNAqA?UXnT~Rbz_67;YxqA*viGdC$dD=s!IdXfKCW8)(4I?!AXuIug& z4~+;94+{^A4DafG#Z23~y1Tpl@sfe@8GPdBh*4)zcQ9TuffwbllZo**)r$%8xbt1+ z`j}kiV3k{_1-5h6dDd982oW3Ss^9wVu5i@rPc~H_HNGgnu1!{)wqw$&rFqe$Y4Kbp z{n20vJs294xF<3x1tqE|9VC4vA5BP#$V-!=awtDd$@?{kEc6){#mS5c;Jt%FY#e8i zrU7#iQ{*xvcTU10a*v4D;+Yg+S`BdI=b_?w`v_quqBs2m&^}@q-nuR-y0qHe$i^NT zV^@VFwR%>qS+%lB3u3a;zfN^Qt=`bW80tq9Yj>LbOEC&UKNl!(3gT%k`hZ|d!=?~P zR_Bx=L95Ckc2q8#vdm?Z+RD`wh)@hdGBe7k9(AaY)nuiz7>z*&;?rvSsQ(0KHC-F>zJrS@!fi{USpbZK1D-Pnnd})KYE1F!D z2!hy3HQG`NhUAKCprJ`h52cSRh(dbFYWX3)4XX#5_jZs>#ed!lnk0e-AZwZ<3qvn$ z0(CT}sm2d-SW+&#nAHd@zJ6xaCG7G`Uxp*10!;ZUVO^tqcCoI(q()=rCC}sJ%_kYL z%D>DvU!;2TFZa!-_zC)XDQgDa5i^#(FY%7}4>m|oNds&4NnJB*ObKFpbVFF~y}ndM zk_n(J^zu>`E7Y>`Md&>F)Ul z#VONw{ovI7;^6cT4sIE|_g>~;?9Mx9jay$Mx>5e0wBEj4($i%+{!2CbKrQN$LObxp zU~(X@3zH6lNM19yR$Z(Y~;pR|lmVq&=i8J9*usEZv#zKvt{zDhvHPkRA0>U7IN1(7M9#O2p$l9~npS zyA{9t@v{Lf%Db7=i!QXQ|2obaLeui?+qQD$*kzYV_de+_;C1l3@4k&&ouw^B#m!~O z;DKFvIo|;uT+kmpDD_{d07dc{QtuK$c3xyB27?7|E?1;O#^7qHg5NE-0ycRueTeW9W-SzVSc;Ia?%Q_3p$=WVK96r8o~6h9wazV=d4n zLxWARNUyYT!gD`8Bo%tPY3u%*qy_Ld2DGwouNJaaAI(7SM9@AW0v+h%5B0k~hn zAuva{;C6;~RrIc|iNbO1jits2cieEocHtE8KY36mz>g_SOQOl5>qkb3cLfo|;aPK* z>4jHw)i$CqSO=ET3> z9GTa;slKFO$CzhrldEXa33bJB3znDUw}4B0EHS@2%Mrd{3D6+%FfH+r4+LOuJq~SP zm26d#y_Fst5BWqZ`6S#Ur6;k~6#j#wR{5xv@=q+)n8CnaiG@}ResW-+K;krcq1IVy zbwQXZMHwb$$oCoal1H}f$3cz52RDxmt{bw?jhd8|b+rmN=X^2x$iN)9DfEQ1hkcO4 zp=*4OUjrJc#1iv-l7KEWnk8;r0MqTojqYjIQ0QwG!Z3)_ZV3t_B-tQZITcD}Qx ze4)Fh#toW+-7*L(40C9-(MNNr|GqUrd}0v8;M>KNq?4B5wA&8tDW|wD6$nt=|NYTE zkG-ris-m*B>x(?}?H}HGb$Ol)TZukTHp)k29N8SeWfzaZU6#fIl@D8aOm>w2Y=+9` zSV+U}4P(gF@;TU%v6tn(zBkI}o_Gf*)KBI2VFEw4ZG<&bT1!zi4F=do^aGQ&5yyxtWqrev#LzcPuQg>GLGnRLA<#9=rtno$s~hPbHnMv! zJj7fDdQh{I7kt18J4fk9)6uU!O* zXkk|X=QPMmN8xvbrD&ptMPEh`;*csja1kM2DlZ%L2 zADXu{v8QAGX3ipJ{=qk|@9Im~oOfy@8RYf$)K||R+s21tw(V;1H1@2aekpz8sK4@C zP!ETA%=oG7hbo_ZTU7qD#zV56C9s+OBj~l!n4vYGvV%9*Av*MlRnHQdu}TapBabrj zd@_8^+az6C+?^&gEf~S9N33jyj&=uDmNsZmY^?Tv!Lo+TMYbV#0zZy$@x%v{Hjiw{ zlvpuGA~+etkkaeX&h4}@1oT+wDFt}G1UcpMsVeVNG#%8*r(*C_BQ#zV07wR8(>`+A z=s{XS9N)K290>LGmisKpmu0}|B5}H1hx8?BCFw+HjhHEe8!D<+XeAzlLSKi1Ck@AQif>Co4lS}0 z$WlZZKx8o=?>S`wac5kf^H!04TS|W1H%`7`eBZ-Lu{phmwBJ%V3 z(NX^b>bp!nBNN)E=%V7R-9qYyic859|E06D4w{|S*Jc$SLCnN?7cEq&F z3avoRejH?qZH-qnr%_AKW#6Fe8mKFgT|!U0&KgFqu}D!gD(&Mmfi7i#hR9a zuPW!j22VnrXWL!&(4pz;Y|g4>p4|s&ylOxr2JVA0E!$c7;62ueX4d2SwB#dH{*!+B z`!KGl{6~HAC*||EOL}>Nl;yw^`E+~(c*-_zA->9A@2pt>Yj_KCMYRSmG<%@VjbcpG z)P~-oMl(BRL_68M(QIfpM>9J{Gvs1!SKB&}nu<)_se{|KV=9eL8FG9I1(9X@jZYB6 z%%O|r9*6Sc3-s>WH6p$Ef}Hzka8Z7J-v&MkjEBe%%3mL?$;85kUhY~Mmik%a<2d#S zM=?>{il4?uo{5h<;KQ_9AYu!)T2Oogf-OkoDys#0GLLYB4oGKZwGj82Rtw}eP_wpj zGP$Opu4QXXR-&^YZU`QbmClnY*Ve$_ZRYnEUif@T{?Kh188Da5cHKi>kQZL4{UDPu zApb$q&uz!FTd>d*L{Oi@Zh1@IY>uOeB>8$EYDClh#bQ;6f^|wBELe?5Mp}=+B5`7z4V-8At{K4EF%Z9 zSukxHl+6NFj?ACbWG^sNVKx9M2#^3V2Q(&==t!CT(9_8%Lp`nR8Z;_C)2<;d@04A; z_MLOiu~3q;uA^h!xpVu=f@z0c1I3^qKe|)NN>4?qQ*l8Gz3X1Ru(6O4dc#p9 zZpBX;6>mdgf-cm4UIL;Lx=_mwB}$~OK`9ao6T?Stt`I3=Ehm*iQ+sEA1~<0_aKj_- zVHye8Jn4!D9t`UQ@h0j={t@>Or(ya>keJT$j|dtugUJ854ih9-qd>%vYN#0`78OLS zcEtwAqJlb{JLCZh2bD;)$Xs)&NGmj&I!Lu<)2*YOgRtfn^A>ZXYY! zci;9sd$!L!GxPjD{s8%n{7@l6H9aB5bP_Y?4{;E9K`hE(dx)UQ2cgGW0PFd~T7?JL zLr}ZZE>v>hivU{W-c+7tCNu!H+3_JIbeE{X3k2PTey4ZYWH_>D)kj-V|aCo!BkyCWu05!9vR#wZN&D?e-aV6As8ZgAc)s>_|7 zTAp4sZ`i!3NKeX79A1r^=9YEldHcjbRCsK3bVPJ!#J~hwpwiJXVqjuoAP_H~-)_R{ z`oVY!w3{$7k$Y@RkepX|?Iz}^^vngr!)))dY8W1lsx_-Bsj*D?{T|zH!h%0Y1z-uQ zOd<@ln~)vRw3{Ho6_439vR8TABZ!amx0k%?YVj!gmdn_6OuGpS4&RAFvZlU$%LU?6 zGe@AQ1R+CP^_wU>0@s-KI zff;@3J}H+um2B5Zp0b-_1%U6C```HU-I}Ym>SED^X)8fsl01dHUDYg4Zu&Ia6LTD* zf?!!5BVGdusq$k*lcyEI-*59tdE4T97Ovc~Wo1im!se{O?vc>AJCANSYfEL{3Qu*@ zc>nJH@wS@!wv~~zrb+?K#lXL`k)RD_eDzvEtevmKTFD9}q=1rS*O2G}Zc0Jv4@5I1 zK!pAVyKZlU2=Qqi$Jdb%5ze?n)UV|C#X5XHhIZUJhMk0kGhacAGK-)ulIEQj6C`{i za|sHpM>tiwc)H__>8ea5%-QhmPv3rseefWwCa-jWG!w17m@z&=#>rO-T65h8IPKz5 zgsY@An#w2HlgfY0FaLgT{^Nf6B$HClKTE!)C`ayHCgVKOVvd>DfOPb8;30^Ze1#mu zIx3}iFzZMxBq4{0Dhdl;@;IY_KnN`%%@h88#TD-zxk=n`>ibt*vG3k>?1q^~Si{W5 zb@vjCNuZUB&{oox;uy9GR$B$FqdtTGnaehT+LL&hILXThx%aPvmO^Xs;?D1i8%}xk z_h%mE-#q1oZ-b{vo_UK6v$ZpyFx$+{Gc#EK0oM&^%Y|~i4LKprbx=G|eHu_7S{J5} zH|R0!6Ri4Z!IG6>5u?9-0vR9B)Cc=?-3~;8$^e%tlcE^2IpPseofho%3rT=F`Nt%n zun7b^>eL0;o#d%&Llu1YWe%E`gSKI`#AeF=K<#ry0caKMFgeqnF0|XJ9Z-kNVQs+j zNX7%jo7dW)PK8x?E~KD&?bPVL@wKf*6jdR{e&!cLPd@PEGiStJb+w42&(p_g>TYT}K#**xuW_eb7H{ANlF#%lI+0N2Ho} zhf%i(W8K}z@E>or9f3t+LE=t}=FIglQIxVHP%$^a@+@JFxkYvfVJSkxoGKMLqFYK8G7NZoOG5JE z#bwZ`V8VGiByCaYX@_@gtLlz#_nay6WhirJA(PI!{+d_8FMbYO$H zyf;*3e8QD@YjNKLs#-)}sK;j1Bax!bl1D0e+?z7epruKWak|Cm(JW+W(_Sg+y8s#2 z;D<#BRRf|v+7!1Hi9YtVxK@)b7SKDNWQMz) zHN{sgugmCYDlY7-&MaMXctvUcNn;m&qhj-yHZogA%@UMYv80Cd&k_f_B@XhT3V=oc zu@iOlB8t;c;9Z>sX$CC6TpD_?Be53;w4PQtpj521)1XCvoWzFOkk7&jS@4qkNkI@+ zr0ton9metSlD2p4$lfzUKL=8}2BXhAFZSE#9TO=Wbn=|&16_EGHnPF68|_AitRBr> zwO*VAjZnTEt!aZnYcQHru}?gTfDMMtw821(c~Aw}V3fcHqaK355j4TiRGj)v`yq$fWsj4W(j$b8K(L|=W|l)Gi+y0cHHF6`OU6_p+u z9_kD&Y$;F4YunP0mR{d{^7zClOO`t4yXt4ooNVc8ZE9*Q%S>~+PdIPu$}eoFn2t+hlz7`iT>rwwAsqPsOS=_oM6I@b?c`Xvw^-pMDE8p3gej zD*gcG$yB3*$TdwnAxC=KGH7ztA5SJ*BTTGlDO!spAcf(_)I>vz;~(Sto)t@$te{^( zMn(aCtaEVl=0WWK_DZKOkl zO9Hi5FG7fABsUgj=TrY*K zw$QAB{9YQqM>6C%ZF)z7Qi6eWV3>AL42@>Bkdtd;AMb{T(L9FLu%?+e59MTMr)Fh^ zc13qKw|2*7%u8DNAY0PCJ*_C!mXea?OhYKuDrZq{b_^CUu(M_z%mY|URU9S%y&i+? zE_9q)z^be*91=ei5H?prS&Y$0nU@xm-NjQzF+8=45$?TMO)|4ckb|}%pN0>W+#&^7 z^=`K0tq?d=+CUiH&k6`6-LkL9ab>5a701*hwbr!sl)LiWD?6Lo6Plt6U9Q}DIfcA1 zCC-+ZmXcJSky~4yR~jPhW%H_=N;1lvnHeb|&g51aixbdB-JpqZ^cZ;4VCAK!7Fvc{ zv>L>#TC_@}Wb=s#6(w(Hlho{z)JPeSCLdS&8O75uKr7`dLG1ujH0lJb^v*4~^n3i^!8F&xhQ=j8eR7s7-Inf(3k!V)jSkG^pstHiSK2cHT48h7VMMa@ zjuSzPWefKtYoR*ELUy1+6OmpV!Tpej-85x~(_=?7udjii1@>_&+L-?!a>t@&X+rFK z#Ym73JS{{2OohUvmV6-$*oLTp&gNd%MP7_)wtcQ|QW!(R4w_x>o0bNQRmwaEax3M0 zn4nW+*-LU>YXnvS>amZ6PZR`yR64zVm;_uuTC}_@FSDS#t*yHtGp}sPlTD;kK~yyo!Y{rzC|YrO3l|X~_}EY4U(t17BG{Oeb(L z=ROGiaW) zW=YJE_*N4;aNIf4`j+qV*&MizH%AsDvc-RMj>HUEoDw`k5?*4^Rvy55ygAZ>)prj= z#h+6@|DVs1+9g7cyyjr`pU#mlefAUy8pTeLtQ++3`Q}Iq1A3&BYqrljM}k`Bo+Eu` zNNS60v_%_Wl5-^X((tn-(#m-Y5{pJDq^;sRRJaNFWMVW70o^N|CZcoD92tT+G6ZvE zh@2x+bV&8{OtMBs;z~0 zb38l#{3yqa|8#z|f5!Ak^CKF}jXE4}ew+*X!1)pT?vdFAU5D<&yQCBopXNF^CBilIZxW> zo*tX5Iq^Dcrlh$Mxb6fVKHuDEVZe7C{dwocg8$jv_}X!&M#zpZ4~ZJ^RrADZ^v^=$ zB-W3!L!7|=Ddn5Vvmc9gzx#XT{cRiy0=%Dr`vu?==7}}>{Y~mV`!DrA%5R$E{p}0^ zgz!Fmjj&fPy}wq!e}a5}JNuLO`%C9|zn_PBzh6Da_xtttF`tNrIm+*6AIbLK6|B(ECKl{}Cea{@6_{n`s3V!7jev;9^3 z4ikPct{epXDg3u!xa)}@_QsVAHT?yGoEI(rfjq;>zDCr;1-U-K$$ovx9|w1>w5@t% z2m4*+>kmD|?nFR)W{esPdwG%>WG2}RdFB0fe1pEj>06a>+zexdVXQukIl~zL-ORf) z@Ak0xnRj}yCvIHN%sVWe`CsX;R@`SSuY20MgIM0 zg~AqA=*X$fan@I|dMB?6fcS_%s!-+~#%lgcJ_7iY{Ct?Asco?)V@Y|14k!GjafI^< z``hkj-od(BXYOtiA?x|b`Vi4Hb59%V>fp`0S%aj@b5Q2BW|}h80th;UI9u&C&MWXKKpv-H{GJXBGP$ol}1+_KKdO!*CD})0N z+r1lQ%-jPw7XYj+8&GxD)i!fCKCx$pRDdY+3eesapuJdH3r9nMMQoL%XY>=&v}oqU zTATWsE7N+IVm^7P9;68TOS<3R+*8RmGW+J6HnPf|<`?)c(3m3R(>K{qLPT-T!WY!{ zL62nT8TkEh)Jc9nNY3Z0`XOPbie;;M9i)44X1`P4ziIQ#$Ljm+dkcGtMF{)Jr*DP` z)%7He9zcISi0`MSu7J$uWBxla*10QDxZI7cOinsnz=zSI}{X!z!)JF29v?2hp9yr7OQZ_5Bbvf|0qW-}h%eHt9Z6Qi?{m zB74lmi)a6e_IsKaijy&S?!hd22yk60=TYk(^5i`{BX#u*+Lj~GMa>7_jQPh2u@8j` z)$IX( zI=S8s8>>ij&d_x2l#`VvLN$G;iOTnTY9@I>O;|6==|Eoq1)$3WqZB;Q)sS1~k-*jK zTOR?s%*8JbKZ+Wox3X2V>1l=*5&FVBPX#9@_e_ksG>0WDq&cjp?_giwp}wNcX$|Wa zE?D1?rm5D^g6VT(e%Hxc>+83k+*P$@QEADdE!FuTt7R=O1j52BSdx8Up?C?j4IA1; z!=gX{ED9(S7WNfMCxBtWLcp-O2p|ElB0>>{7Y$7HmPI9{i?&pmK$*Zu+$=V_KvVmH zn*u-Fgh;%Qo{?ez(3nlzzh_;D`pCzrSq5?m$8WxY*#c}=}#?<@LY zvyx0AQw!763sWQ4r??lp-HWSIEV}W7#|J+&eo#DI72}8W!H5(Kcoa9_)!Sh&Irso& z@g~e?nmZs)>U|RqCxQ_qxQd+`o-PSaN)L2MdWANC{gDJA5upWm{+rg|f`{A{UPv9@;e;jYeu8$DT-^E2kL zwjHd2oj%;k3Q{U+n&!DzEy&4TxVC2bp0f7(3s2~nURzbUYO=k4ZAY!sC2Yhlp)M!! zZR|zk6s(Id+LS0)^Y7;L~e!Z2>bPnto{N`sLzC-lUa_oRO36b#g) zNhYmil#EzOI`ssk$b%zFs!gD6POeNrUx2zw1&i~G6h}_8C#L4dIrC!U(qf>zD)7DF z+hRi_qN1ZbxNRm-mvi_4dz`tEHnhL#~Jl(Hk`7sgd?VdhOe$qF-Z$+bA15_2eqYm#Lcp7!1w)Ewqn}nvvf`efr4+4p01sl+Dn+8&9 zx(hMU6xKUmXx_YWA%1*O=AC8N<44aJf9Kuodm1E13g1NlXR>(o`L67vZhD0J=n*sVw3+Z?#*6!F%v*hC;x^rCr)?5|RN&(u3O>H-(vs=v5)I!M`EvFg|2IafWzYUfe zmuHmJIJ0sxa_~RPSyPfh6)}zgrsw&;Fk4Ft5i7QvU(fLZ=S718j@Bat6B)LWDKxq+ z&FWbc)ts0yj7nt-79}Na%neVEj7$&D-I$oPh%c`yEz92|Y&NkezpS(haB$;B-oS1V zb-+!g(Ms4ceb*C@(^q<_3K)bE4I6+62bH8Er7)rbA(|mGF`(q-VmM)C>F&J5#5{Mp zd9JUo=M4!6$CO@~n_!+7)MnJ;KN|VFyq4W6XpAK1$^&GbU_wTeNx_rp9Q(9^nF%&d zXq)pD8B=^U3Se%NB;SH_FMiwbo50UOv~(*HlBk}N3F78dMNlv>N-JSRbD|2NeZ?`Y zZ&+o;K>MMFo}LE!rDtWO<0mSLrWhuxw{SYDQ^l zVq=^wA}J!XI5DU6nT%+6WL$JiB-YPEt8BuSljn3Lg-cv~5p`Mz?j@CC!6f_eNj|a~ zE0Uw7M{9$K7=N+&x$tx2*NR^se%tZei{BCa!Z5)qCW(kE8BONIh(UUi!WxVgR^+R9 zlM6HB7S!0g!*YwAj>PEL)bNT3&of~OS;@uovy+n3^3&qO;-h0C$`Vu1hAbNM#n3pvE4-n?ym!1xw;7et)%=1uWM#_z#1IK<`NyhnJqaXRqh5LbBfjv#No%)8Q? zw~8+@Zo{1G5MTGnTZO!M>+rXC_-@zQ@5Z{=2PQ!c}Z!pfl*y|8S)pG`${RVF~ zc8e%5RYSmhXeIg;DKKf%Y&U+b`1Rqp9Y4F`d_z^U^dd)&X_aQ;2qoy5_spE9WrBtf zlYvASHZo=*QCZ&LNfr4SDQWYo4~VFOwAA@o*%^Zpx4+;$#!`$~4)IMBc7yRO;|z== z4sq=)MeqY%&K_kyGy1V+CU8Ls)QhwQkc=BfaRZ18qroeIqTo?zy#DOU>EW$Bp`fI5 zjN0~^*-GOqj57}LEiX@%xd*6DpW~I5T6+ zpM3=5PX@t+cf;kV0%ueg#xdf3Ie0QhH4UKB*o8boJgmtaNE>P+ielF|I;a$*Sd{2BG^QaT!8OB95p3|5h+hBqcH20jZ5)pui?3nQ!el7U* z;uk_s96>_9Lj#LU#L9#pBXSxsBIt#*UwR_aWOj8jjGbwz5*Nsr5N}XCzbGwqeo{iI z%T=0?G(R-0Xnt1nrVYF!t^y7u-WLNiIVpYYEIuyI|mBBwzo1bDE}U;qEO(X&mSqrpji+qdSM!75Hh)>80_dVnZ!v@ z@Q@tI+a@Aah!pu@=NN^GzDwq$h5BO(9OghzHi8>Db4dhI2oZ$5h>1&!EsU)WM%N?J z5Jf~qxt+N&p%IZ$QI%N1i`h+lrExv_I@u^^!_!=^uwqQEQZ1u^47IRQEE^f<6UR!E zBdwF=I9dSv9WbxYt^#bg05&^_Cl^{n z8ZQA!48}P*{M=?HE|3YuWEr3}b6o`}0U=G_~Zejg^1(q5xp!C5R;mpLL%PpQ(V3#E1%~fUq9AV!tWDAG|MY+}yl105&mE zc1PC1V*tj1%T|^m+d16$67`Qz$)oFemAVD(j}wX!JQO2%C`Ry5HGiCO7Iwzty zpb$+s<+JP1w{H|zf)3JPD*)Kl8mW(Hl*X*{O=6u;393!-s!7jB`W?6V@_X_f;2&*3 z_&4PQR2vdN)6z7GW_bx{OVC2O@}%-pPANaBvaqo*CA2;%u^>0MATg;vG_|mSzdY(* zx6VD9mNKs*$(5Dna;0Utk}Bq<7#zU_TP1A^;O75zd(byYoPzd{QBBSEz|88ve-e(& zya5vXm1+=4S(V-fk+fudB+ds16K`Bd?GdYKNyZ5w?9(vHB#+Nbq3;N#TszlpicF za5>XzYY*9UmCn~p;f|%=dYNNkpB@WaKVRP^dLaR(*?X-KQ9z&xle}Y&h!>Htvm~$^ z_pQEp?oQ0HR%it;eh$`nE;YwxX}n|S214Bg#0nCo|ho7jVGQtQu8fn_hOSvxvPm+X5agrppJ#FT=Wx`y-d369{pVk4qju$4yxmYg^sU@onX{Kpg6MikOCL{7Y0$l0FqGRa|0>6w8nT=-37e2q*ATu*JH#75k{OBw~v!OP~4mc>-u7(G7h`1k^ zb&xO4vGfMiBhVNj7#P&bO?ni2$!i=juNf%1q^gF$_8o^-Zr%fq)T`kx`YSu z{T_e09C2^{#nQ0l{i6O*06^-iTJ9rN4R5=kY5>jMgzpTS-wC9-ZIZAg_?qTkrX2d< zQFV*HL!=|)cv0=>Hfn|`kW>-k;)*+5RAcBO-h)oyHjH}hemVi@A}9=$4I`?J;%50M zCZ0Y9SAl8-XdiAfrTg@qf8smn&?MUr)IB^+pinD#X}yJV=&KS++AsBwGRL3)l}+WV zPWY$(ATz$m|IVHh*J9LergrkxB~ZQ8QeM|fe_TOft%PXNjyeS{(xvtw#jzkUj@Y5N zM5R6WJ82KHb24-AKU-=KNO$me_PjU>MyrZo@Yfx9YpCk{pJ@vI9-t{8J;6)R6TEBmqU)PV(t^fwrBIPp$hPPV+Lq5BPzh&HPYXhw~5z|4>J9?lpX&{h9 zTJbH}F>yx1zqfpT5mzciT=690iie0RRg1XZN-e5%=HzB(WMt;%I4g@%&mc3C zW@ABHiJz`&n(C6g+{*iG5`j(PCRRJp)wdR;aNlW;ji<}g{ zbc@M1O-PYbHl$5pp8O6#Gs&DjGHwA(O6Jr8n54|9Qu2oY?rE9$zaeT0fb+{*)D!@u zyab&7xGO&{?*23Jl0Q7QBoZ&p2@LQ?GJ)4wifqepV-ui}BeHkw-G&aQJGal!+)BBM zzSYo75|{r(WF)9jPdtZYbLRk-b^J}^Mq3NoFwNMaFhhOKXZoW_QFbx!JlIM(3KmLy zmOO|gis(VdlnIrcdTQB8mBnO2QJ)lFn44P|519~5D8yM~Boi7-O)02I&dy28$Vkh{ zPOd0WGNIQc?U8)ue*uU8O)-ybVYf!2*`~%vHX5hnjWOU+#)&lrJ9^C00-bH99H-Tk z<3M{V@fd?PolA6sz9i*`e#0=SYd^0H#~g1nb}sb-t^0-eP>(6~s`ZaCru>gZH~w8j ze7O4NV{{Ob-MntxW~-61o6`YRmKy(+L`LIMOK-N3NJg!!AI;_)VImUkKzdHnRSI}l32dqs79TGnKS8Lm*`s)-AG&37Z9==_ z8L~)r&@>F8os`m(co?rNi(I=_|6dl*uU#Cma;5&i_>1M=6WV+fA0 zl|^p7yiS(KOdN?V$&?H&#b{6|wbxdmyjuh)+9ZJFm)eIsETg2It+W?5?Q3aTmRq*K zF2oU+8{g zO{>gJoVjE%t6X^F*^Y(VPwL)yX?07yyQw3;Zg@py<%;3D{EjAfd`tDE8@o^1zR+g%dVEz5~7?%CDSva6>!KBqLT zqN}fJ{d6aq25r+i`+=Xwv_moBNx5_=eH95|z==Zt|y#`cKW+AZBhftf|!TM3^S)@LikOM)Zi z0_pCi6>O#9mKY^*5}B>!&`MVNH2T~BkWUcTL2jtSIl({Ycyn@)Qv_x!8@V!~y=$ zMmf)gG)Da)R-#TbOfTg+&B2|u=(K=ju3M)OgVOs<2xy~U=To5Dah-Nx$T(G}X|Lj^ zbUMtaF@CSph=RqUbUMU5l8XXGiW0FQ@ntx5xa z_~EKFVl?nnofgP1(`lQL23s2SOo)-fH=ymAjpXl}_+Fi6M!vMerhM!Pz~9mN!l>h) z=(Nq}nGhpQRO@`Z;S$Sq+F@KPNX9_rAR3P?TBpN|9X2dLWPXHEZTpH&M;iIIpXhWH zbJ_l=)6s?p=Jx7&Y&~iB=rs1XwXe|W1Y_F1Nu^P_eEX9+jk?(1*J*Cdb6~MY-(`po z;8>v3HpAoCuG1lg+wrxIiM@v=M|SR-c2&Ep8eB`-m%BP9Cihmj+D1oR^2U^Fa(HTZ z^2Fhx3cA}jI(*Qzx_^AiwS0JJY}N42{iFSpH5G1mZ4=$513f}dl+zRIh9{>+CdOUz zk-(c~f!}eBOu719)06!}!(;uEdt4Jcyr7if`#VRbriUkohg>7$uCe~{L$2NXCr74+ zMh2%*^pxws#N_DEfsvu%iuQ@oA=mPW@uB`h%SZMMyV?c@M#lG)x%&F|kGf9mA9oE6 zPr8=%_m3lMWEf@2%vBRpWBsGUR9_m>QfM**iT|F*Pz;F)_KbvbTHL zvG15TmW_rM8QDKJH8Qprg^x}g7@i#LpBi@c2j%RX8ttFjg_?}-9~(eb4i8UG3{6Z= z`{(qJ0)^xK)4pX5PaPhfo^Ibiy0d?B1Q_oCeT|}Wt{oGTV^f&MCSV|W$e1)njGe|V zW7=>b1f&}dHw{QF!ECo2XL@Q9&s8AZ20!#soUJ#e!0kr1@?io*stI z+hwfAm^F^4Tqu*C88cSlywli^C;IVb4c>C&U#-#PecMc#rI=-ud&^jdGA2>#2w)sX z*;XkEf@8lK2;nVwdql$5kJL0u?FW>@fRmuxgS-i22ae`@CRWOjHz{jEZ%<2jsD4x$ zK}7G51II37H|k1wo5I}@V-U|Os0k+rB#uXM=Kyl)yA{Cx1aLzoEtj<$LP>{^9sxAN z$ZazQ@Q=Q-2YYwAz=QQmY`V}2^i7v6XAb-_5&`$4Ap2FCDF61(H_fi?ggv_w+k~SQ392`%&!Ehdmyz`0W$iz z3=1;<@0!r`OPr2KS{;xyO>H(J;cx+R`feNMUrJ8{YuJSVXA2)ws0+3IUgS;Ti$t-b zI8)Es2@Ln*{bm2rJ0?Avly3Dj)z3zNw=virO#oh^)PCcz{OSNwgP@#pNta`Qj3{X* zp4ow?CXgb!9K=(ERiX;&+oMPm?9`*_jeRIxwa!k^<`|ys$9<|}j(m>FrFW?Wf`D3& z%A__WTv1uae}>?o5n@WVIMKnltkHlh%Z1udnG>Ll35jt9&D_uQqa;Q71Sh5DhKois zDwkTY9bX#-Hi$Y$a3pMW=rNPVLZVW_8MWjX-lO$`{G0tA`sy5i;@`+*jJV%U@B>|F z`9)}-CB{-_V=)pLOuzVQD8$pfOcm-PtCons6kdh3dXD1dh(%~7gK|@&>3uh55 zl0~s-7QcAZZy6H*85tlm9QB(Rwu2-3C4DCuPw$;SSt9jUqH?A1|-O58(%fPg2)fw zFs?SvU}@NSJjys7`)u0LkNUx|ziV6!TWJV;jT@i_$_71s&A5bR7(Zp1#+O)@@nhpU zzs;DC3tMmC={8RKk$F$UY?X4b-55#OsFQ4c#=7ov|XLKLhe zY$@wOl&9rv1?y#fY$YP+t!8W3TDFd@XB!Y4ViViUwy>?nHS7dqh8e~L+s3xDe#VTq zjJ<4t(P%KmhS;#NkL_SP*)BGM0M2{ZC>vwrY{K{#+spQ`NjAl%*?x8+JHQSazcr@W zA$A!3@Bll>*w0R8r?6AmY3vC50y~|Z!M@1OWM{E2v9sBi*;kA|v9Gdo*ty0*b{;$5 zIK(bs7aAuSC$o!;!|Y=AHFk;dO?Ihq3cHM5&aPlrvahqN*f-eK>?pg2ebe|pyOw>6 zUB|v{yk>mDu4gx}?-*ZZ-(@$lo7m0ld&WD)sq7YZE0hyoV7D2kvG21Vu-n-k>`wMW zb{G2*`!Tzl-NSyu?q&C}``H8RLG}>)DJ;_e#(r+x&3?fiW{j4{ZbHEv^0*t_B#6udxO1+rP*KE-`Lyi9jsga$=+lC#olLsXCD}k zvwyG;jVBPF=3_)SdW!v%{fm9f{>?sNpRyS?%MH#r=YrdeS9u7xV|BBGL+OG&ro)YA zjGa8f_!W=jQHbtzA&)U`<+0d~B%UYmM4rTxxs#{xRGwzs#?yHQ&xCFMpN)U>Z0_PY z*mW`wQTz*xw~gnplSiTPYhxELGJb=dQ%ZO#FXQFBf>&}kuj19bhI@D|uj6pd;*ESh zZ{iD#zw(7xN8Q6)cq?z??eHM#FfKE`V_b=NCr5cFc5>*3&*fsigfHbid>LQPSMXlm zhuw@<@zuuH`5L~KujA{nyUa$u3Gv;x@U8p=zKw6^{fO;6$cOkacJkfHckvOvoA2SH ze2kCt3BH%_*PX0rF7yl9eG4>OPuCVz|nmH!QvDDUuh`Fs4o`1}0t`~&_E z{vmwr|H=QwKj#1DpYTul44;L)E)!e`VH1e%D;y$Jgo$twAtFVTh!!y-R>X;TksuO9 zl1LU#ks?w>nn)KJB2#3EY~d0)A{X0|7Y(9O%oj~!fmkS-MT=+^ZK7Rth)&TZy2T=~SS%4sMUPk}mWvglSM-UMVwG4e z)`+!Yomek6h>c>C*ete)t>Of+O>7taVn7UvAu%j=h@E1W7%~3Kc;EQD*e&*mQ86aQ z#e~=^_K8U`C8ouGaiTaN4vIs@2jZ|e$@quyp*UHbB2E>ji6i0*;&gF__@X#doF%>_ z&K6%5UlCsw=ZJH~dE$I=fw)jyBrX;?vfQI#D<6O+S&l|58e-Pg`UKH1h8^m{vmyAD(?}{76P2y(pJ#mY;Roo`N zFMc3y7k7v|#Sg_@;z#1g;%;$|_=&hz+$ZiA4~PfFL*l36XX5AL7vf>@h1L5`Px2iPyzn z#2eyG@s{|j_?viJyd&Nf?}`5s?~A{S55zyjhvFmgPw_ADvG}+6M0_e{9Q(&d+-`TL zqrU=6HF&w~^gCCF)3Q!wS;^`!nL9l(wIjxxQV#`OSBFlF4_DBGq0vJug-8J=H z^18Z8URS$o^!=I|o!=PNKRz)%JUTqmuX1YYvvuvCoS>WTu9|N7 zkh@KPw#}oj)ywX-27TSA&u#kLuFswNtUlsy*X6Wp@Y;2G?HcTMU0%B`uU(hduFGlf zw)YQA0%26FyQ)JLTh*n&!@0)MKh{4uIWg|&pV)~e*%OYX_xR5K!Tr(o-@z#%edChw@Juw-5%L5i&gXxslZYfQ5raP71uvFGC8<^YzKrv znt1w$Mj*tQ8kw@5M~PMHDzbm)WIw1x(kT^%TQ&Mh*Ur3BbyQaovZg8>Yt>q1MD|aQ zj6y2pTV}WKReQbPl?Fw9z1{q}LRhWulWqQ2oqqTH%4}=&yVtI+9R0%*ixGnslEMce zU+Eu&0BPDjG}w>Uv=7Thw-3v!@L}(L-G;hN+S?rvn#txLR;bbUbsM(pw(rnw*`eFM zL$`g0Y;$*YZDYhv3!KQEkaCTou?e8?o!)Op?vy~Odc^s>pmO4UUsGAJ*2^lxwrik& zGGdqYE>Mb$7}>U)9wiI}^^0juBnG5mi0=h-_9qvfDAT+e_r`>Y94Hx{TOul^waq zryh}ec4BlIAMGE9?9e_c-?fiQ^hS>QJZBq4eeL7w{eB-MYSACVn(tx+GrQRh@El##xT0ICrQ@bhg|)gwhVCDSE&0d)cqAc_eB>-w$}$fvr~3x+;ntB_4-kLuYxwB z*QfLMTEyO|>(!-kr--s4Rh zXx-GE+hgD8+t)QEcQ@EK$>PE{dFeBxe{}Dz{s?*0^Vz`gbbrXseoU7kL&Kxf{f^Q7jzSvpy__w2qyTDJ^g$4_G9iD8yM=BNUdq_;N$!G!4XUhBQ%YR$z2on zsga#y{Z%3IB&PfKJNBzG#ok>bVh}%EhD?kN@9Yn0BYe(4)NPFsb)hjJFJknC`EFvs zwR|XC;zuPVl2kbwE%G81XpxC9!iu_2DYM*2jS+Q6V?kcT=nM17@Lp;Rc}rr(Q{$E# zG0qAXI#Ngp)YR$wbpiM5b$)$7 zeuK_$2*_{L`HcbjZ92a#AirJbw+H0Q9*g?PSsG{6b3HZP`uT4E=RLB=yFDI_ACJb5 zCjdVlIn>}ejSo)%K0F#99*qxA06siw#PE1@{XGHo_vre20_yM4_4fqS-=jtzkEcV` z%hM51FI|66m%iT>a9`uY6Mzr3^mTixG(M{20FP^3|0-Sos(|{dr7)h;^{)!3f0eF( zmEw{-Rg$~H^SXXj0rjiW?OPSlzE!$?t91RUbo*8*Zp>4y+p}83U#;P<*6>$r_^Sip zuh#8XEr(#Yr&_~Xt>LZK@K&pt$WyKHU#;P(*6^qWKHk^(tq#Di;+EZ>&MMVQJAIaW zlU3Qcr|bfe9AJ1#tDwf*qsqIuWRom%LQ)=aa`x+;Wy4*&M8zneE`MSJX{T&DrO zZcmqn40o_bxr zVzNLTx_vu!d%86ojXJ+uf4|XeU-LPAAA&gbUHzQfqzm0%YK4yW(f8|gdnhIl`MN%e z83bMEauxgK_CVmL+DU)kZPI}*r%_+46$;AJ?cg@)z2D+ z>d#oj==y6pYgK!Dgj|WJF+W@`Y$^s1O^$~7t<&Ardcde|?+B5MUX;oFS!`d89t>)9 zr>=1;;;YfbSL0ShSEC1k8n;A(yG9dbjqa>9ns{s6ia2X@=d96;K#iX0YqaqNQi*y;5`#=e(lCsclNYK03j%d*ff4O( z5TC~_Jj9zAvr6!0Spa*pta1}A%v-PeWLj19$+TWiuwJKzldvV2u=q5E&&w9jk_6pq zo~d>#lE+z(cZ&3Jt;a#tWpJ&>Kh*_rt;avrg>bFM(`q@+<68A+x9VEmUEMlg@r>1O z#WUhLHExkYj2gzR_>``0C9LV{Q1fYbcek{`XRwwDg~ds%;gI_w!K4D#CF+mE+kv)A zrOgiJ;M`+-2T*KGkxQ?&G5juDrH}lOnZsYhmbnnAB?&kY&gsvUOgZv2%j=vFqBKv1FW4Lh+ zJK3;xb}w6Fq|s<^6hS+%$LJpIpB{$=l#QAiEl;*p4m{1UMt8xMN#!xnhDvc*Em_Z! z16i!Wm-jAn8ELC}m%Cs$rJl3FI!UEMh>m2*c32=q?|}`iu}&Vh%j0f&oR-H^X2n2SKB@I`Sw=(tBxa%YaLHJe&cw-@oMNbq1T699(H56 zE4(87kKyk`Tov(7#0L={M;ei7k*>&jk!6w9kqwc}k)4sRM;(cJC+dUf@#yQK-;XJZ z*&TCs%zZIGi+MdZH1^rp=VM=r{c~JJoF{I6Tx;Aw+^)FsxaqjVaYy3Liu+F7EpZRT zJsbDuxDVrhlyG8VL*he;k0d^s6q$5U(sfDiCuby|kbHUalg>zIvvaTWht3yL(ozCq~4eMh7p5EoYDNS5ydYy!uhrMecN#Gn{a(U(vQx5%71Mn@=s>} z#Xp_>7kU&I3um7a&9kqHuGtwQf*+p!kl!@>E}r}tPui^~XWz$@&*I7F@#KeM$?O~O zwAerU96vPsCw#A&pEUajKVp>e^YEREXJ6!3&)(0ko&7Vu-O6u`D2FQ zzXlwi%>Do{d@O9Ue-NRD2e8D5Jd|7pSQgISg)iNOFFl1XVT{B$P@#`p*k}KN>wLg& zpM4XlOqmMI!^klEs$oOfDJWau>IW$6RX}$H7&?3QAJ_vW4t4#QKMIIGF(Ub=Mx;oX zeFNi3h-e1&hofFOczOZq;6xp&IYpIq7=QClm2xqJ#`_pd>7rz{-CCEAIlA z-{(hWKf$+N;O7Ca7tj74Sos84d7giJ_91-x34Sx4yMOi#VCF;q*z8|{ndkT`v#$d? z?*KdR06Wj2)DjUo`<{rH{S@&1N`E;PUycQa-bJlmHaOsH1dMft4ds4{)}}hyQ77!> z07yPU?nlT8qa2j-0AR<6h}4}Luf8}AH_n{>JH8tRti6Mlct@D`oemXHsx--Xvr@VY6i zjP^sXtYH;#Bn&S?6EBAEs*ruY7 z)A*f^|2)YX7uA8)1Kvz1y&-zt`wnMsVoWTfN5M`Ow*k#)##TeLq6IlEM+;iOtOa?M zA+Iu|v=+=(8D{4J`TS@p*foV0dnX{3a=!N1Exgq03zjD!ogU!?-cp*#`AOcua5Cqo zV6~S=8^SAqb0xo5MT^6$qt)RxoSzzP4X5!tJz9w-Pl?`!cP;R)4(tl>B3LUjT!&N> zui^MK4{Y8Bi#qtx2rnAoMM6JcG&K4{Mh}leE|;JwQ-JPhuZH$wtfU?L%16e9(7zk2 zXoCJ$Y%L#)!21{vc3}an*l!;6Z=l8v(B4X|TcEuddMlwfAA0klH-z4NXl)Cx;P*=Y zUq#ug`F{=DRIr@J?{siM?@ar(9}LY4(5QV#X9pOD(7y%xx0v4C0#Vk;*`78G_#6f55mNj9b8XJG!(JU1|z1VY`%VJmoLr z+yu6X{GY_{WVR_#b~)Hjh3aYeq2s~ud9d3KX77SoeQ*Q(kyd|;-&^n~zsF`|_0Ikp z4a-GQBN*m^;g%f9b&vMM7&e39YA{?6hD~7D42Ju_uoVnj!LSt!Tfwju3|ql)FBmq1 z;ck<()A9i6y%Mbm7PGC4o(tZL<^}KI{dYz4@XihRkfCHiQy4WqBig_y_u*LQ0PK7h zwp)&k_ecJfaCmueAGkimHTj_)wm&M~e*hg?j}A4#2ff*XuZS3fO+F<(x{=y$q4po~ zZK6A{?BC1m zY$_j{D#oUyW2NX=F*;U^j^&|a#n@CKdbSvwT8d3A#irgs*X~ExUP0IXg03~7YYWk} zrA9-McP2D!f`--5Fbf(&cu@@j5V~H2t~u~% zJ$~*r?Br!=d;uC?fX1cJ_&oNq7XP=G2(lkq7b5e=koh}k8_RYc^q)_3zZjn}&Z~-PFGv23&|VGQwK3h@p!)^ro(tVCLic>=o(<^Etip8?5ho^j0Q*2*b;Pt!kx&a8$y~bvdYNbgKJJ2#t?+T1X((UC<==Vfcyb9ii;pv-J@9g6R!qUK;y-T$_dD`up%lw}gnz|%$`H-h8Jn5P4?;z>XFIuJY+PX>Z#c073%9e4*FScwj- zgx~Yffmh-At1<4cg8MrbPd0*KyI~@K*@ho&Cw8XT#zc##U6HIVwzL9jlO#hV*^ZYf zfExwG)&h&I)1lgB44FMS4xXh6Yf9l+Qb;jEf+bL&WD6>reVhdYD|C@yFj}z%jy?je z4KX)sU6can8_}OT&;q9kd*SAKB)`w3-QBX)b%go5%o=%je2RA} z{O<|>kMX_(uWwfF>-8flj%ucS%ZNx%l+KSn=a6=aG4s|MCD_>b_QSWuEG|A_bIaDHx?omWtYP&2$ zK5f*s7K>~q(lx}fxP>U!f;G0FcMVwMeynjD_~d~{VET6r$J5Cx%kX5qfL0H*`k0UM zm(@tQ8Y!0}BoF$x89cy?|DU7w(N0?hUkK z(V0O&J0565fz|_P#{;bfXr}?~SfCA{_uyu%V+=lGESvM$)!bFiUHELnp`5$QxvPM? zdT>`r`}-#ByiPNA!rKveAMzz^WiN7goBEW+)rIH6*NezzT(m!Y z4srMr!_kS?@$4(n3%yTQj-}j*KFYf*!m2&?EYWl!IKBjqi}8#L4aYgrCU9)A$k~Q% zw`0>OqD?*;PyRyNYbQSBRVY>T{4Me-Kwc$yceR5T$66o>lsPRx@0=Fk$IygHoJR|= z^@Ui|Lab>a*0c<3dJ1b=h&4Tqes92{=3`Ox3sS4gfRcT;IQyYJg$!>ZQqCdUu{I2K z(N6_lv__HXShkC?G1pEb1Ljz563fTUe-_a8Pz~Cv^;HPJRw3C|bTUAyE|XOIaUJvx zq{m97d*|Gf+@FDUc=_la`pgc>3f|wDVMu!!Tv-P8k6TXY@~Ov>_(nJsBK2CNPTxfI zBvN0F)aN1fza#Z0k@`G1xEK!ZM)JF2$*)85>yZ2sBwsI@k$gR^ZnX?I!QqFI_&OxM z5GyS~(kbG26Os;{rC`a}8B%@}DbGU6e?-c&kn&wfc@|Qhg_K`F%8w%DM|p?x$SA0X zf>&Y+-iCs=pt}6fEzAf_f-;6bd#$!SXB$V!xNhek0g#1pAF( zzY*-0g8jxg9&H2rjbOi-_djVP;Q2?QIW1Tlxh!qH??k)tGHdWME%09Ly8*Oo3*ty{ zv|+pu53T;6b+jDX=pRzcI|bhgiI6ULs)$=pac)ASxCrgO7>`W;h<9oXFXeYU5%MyQ zCm@rF{GY^idL;1Ly=Xnv5JhV6-X~!J1BfC=V+FP0RJ`sqey7vtFaZ9yK}8!Hv;@1_ z2Il+WbvwLOFPU-`*Do-J7%WeF9$BaQ1un*Kj>Gd^LamqbYgsbqCP2wVekZX_=Gqi| z+~w4mZjEw$HNN8-ey0Lw8o$%g)Qo-g=IO$e=t*G%Ch7~=4X>07*CDliNTn62v?7%j zq;deMw8U9LJ5p&T#>^nQwN^Im_?x2zNTA4C)ULlnG&RC;_2oSQO-o~X)8rMh~FJ&tZwrp~v&k<9SdxAFWWkdLNefF4Vpl>|t}QmsRNU9JFFB9Qak(3(AQb z>N~J#v!1*6&~C^VkH~GS=_{#5kIE^%0Ddh{R*&}GNDKH5ex->IMz32>sP%%N+ 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/IBMPlexSans-Regular.ttf b/assets/fonts/IBMPlexSans-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..bd6817d5202895da5ac4fad88de3da71e652881a GIT binary patch literal 200500 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±Lk;941wN9j(O#6{H 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 VkO`yz&)k7yBj|jf8c=D*002BjV;}$k literal 0 HcmV?d00001 diff --git a/assets/fonts/IBMPlexSans-SemiBold.ttf b/assets/fonts/IBMPlexSans-SemiBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..09ec8cdae8208c26165db0c87da409cda8c26ae4 GIT binary patch literal 202632 zcmcG%31C!3@;~1F-Xs%pUz0nTE167YCdnj|WRj5kfDm#Kt^~pz!YM=q1XPTOimZzW z9*792sCaUCVN_&Yb&+)yZ`KoC)*tJ->bl6HE0h1H`n}0a2)pj@`~Q8>o_??2d)?Jl z)m7Ei)g8teV=?&AFlXKPF{5$EnH=ATJZN-X{iuBr`%Fvf+tXyu8 z{}q0kF~d!GKB}j8-X(#ZJKT(=2H~5_=FPgSmnASWlS013^T>INSNDWRkNt=->+6g^ zbz=V9S=|}SJHE!dWq4jR9|hWT+8Ep~$9?+zOO~(lH17P2F)4wuh_4qfoippbgEc>+ zy>}VYp1Wk$s@}jj>Gybl8{RiuGI#l`oIkA1LVp22YgjVtlDT=CK03_!r+XM1@k8&@ z%a-qdXJQoNr*|-B9@o2UZf|$szcw?Lb|u;f2c{U4`RJzG-fNo?QT78HA){}MeeS%2 z`j7mI9iERoX9E&7D{wo4NmPn|gl_Kn8)E^jc>Ym9qNquECVu6AQ3-x0FfE(M0@UZh zXqDR4;9eummOAi+NgI(fHde{`bk@O3g18i-l>;Q1N%C$cecQ$iBfZs+scAM4-~;F7 z(1CRB&~P4OV0;%nmx85!WCkYj6ZoZnz&0f*mt~-xY@|ksvrKWF!!prcykul4;yw#` z6KmwLtdhqL{3PY$+QJ&;Rrm(wl1qHoNbm9+@$4Tgm9J!pcvc4%3zc1{%fMoAJ%XP_ zd%KxVx`VY#>sWzw0C@*eHPTVszscJ9gRBs3CrJ;WYy@kUoybuxo?oNp+mO#eyLEW) zAZwP^vPk(UT;I&1rBy6gn#?Sy*Lp$9$9)2l0m+S&hcpt2`tLxp`6Vi^L`p}ZZ_1G9 z`zjgyN;_GG9EaWinM;E3>6>zBs+U3;I+LD^seFU=6Cg0=8HtcLJ_=PsmTq-fsHta1>G zmEHhd;+Rg81ll%%CcP|FN@aGySHMp(S!xvYu}Je6m$u@a#o!kz`wp~k0H1utEb;=B zSF;>x8`DZ%z~lX>cPHMv8hx0HoXVDf=CfI(v=Ps?vTdk{L(? zCy;P2(npFMfw+u?oQMVg#zNZSnPkFvGqF*){z*;mkgV~_8m@DUlxbPeLD)PQF+oJ?%~7FfMjOw;l() zX^_`%p?|kRcFjSWf@e|SV;ZYr(4S8rpA6kphU+RkD-h2Eq-n@wU}wAlc}VYGhcQ^n zT>K2K$6(z44dcBAdiD(JjA3EY-*9~n`a-oyxCgi)7>EZ6E`^>2#2>;Q*~)Z-_Q+)D zQKIP)yh}Kp1Q|>CML*?nun%C1$R*H~{`~;#1UJFVs|G$tdPl%XFnN;~p8E#Ak&_0# zks=1ZLHp^_m$*KQM0orbxz~0pLc1f;Zx{6Wv#9qU=xZNvMRFN3UmnG>ltb;smlm-L*@*wd zcXuON{qWbX zkt*H^_suK|{3h@(Xz&B{0Q923v6lvv6CDWG>yd5-&T3hR+ya`c1)hlie?%gh^*Gn_@qfuK_)%5K3ke>r*%Jt3%Y~m(z{5H zfc8Z111KYzi7~9|fOL}cG9^3e+M%>3Ph!a;T?>BN#Ue2?)$=aoQ$fp47R;x>j-3si zG8d_V>G*s+p9{PCzkE-g0ULHE66%938!3;(clJxFL* zMw_x5ygv{3o7oDax$24mIC~{6RI*Gyn7)?kV|~Q9W^L@|O4Dz6Ug3 zjfl=BS7Git25B+U6G-Eb z<|6$DX(bXpqtv3_Z$SX}BO=*e33Wwn=kv$QqHLqnJp|=+#e155iu84*+vU#()8l2Iu>@ zAN)7)PqZs$`kFY_r@50=p-y-en5?E0My>%iA{ZrfmBKVZ-ylE5`dA?A##I<|vJzIs zrob<;hOOmS^PBk=eh=TzKjs5cvgDRJrRSyJNuS6XIaH38b#k(7maXzyd82%jyji|S zen>tbzb3z-N!Fxmay2tFb2N)ID>N@@{;2tj=Es1H@XEC7(sri(Xb3at3`vGGgUMhq zWEq?Wm!Z^9XXrJoHr!~q$FRrnfZ=h&LBmsqe!~lf7mYE-IHSpEG3FS@8K)cXG(K#6 z!uX8wIpfR5S51;B&=g^cHt9@BrZkh?RAg!}%{0x;IF=cc*<_FU`sBB-pFQy}$ulr; zeqaE7q+S|W9xG+lY$}`2u3)=GFL(2O{15ymDM9q|Iq6mDgbe4`U@tA`<$CmTlYEPO zpZu_VP(G$%niNfj#;KXDS*Tg2*{pd}^RecefOPb-553%NV1`ITf+59VKrgL^TtmL0 zMD%jGp-=7QUc&*ylZHbV^s>!3-niNLka559X=A@%FQd@QM895ktGx^tz5L)`T^-&Q9=3~ZXSXu;^C|G=pOBvNJc(a;!ti^S z2OhEu|9yWMV}F`Xr6)%z&rTNq3EsDplTYTKbbmqrK0X{@m`yM)~byK23B8{5FP$`j;?@|{v9TgP^S)5o(L*b;ex+#@e!m-1ip8~8@P zmH&$0!gum}F=qDiNBE=scl-$dEq|Fm$p6Sc=O6J;`Tyacv0M3{r65U?0;E7GRpQW- ze_+33$JjgU_v|h91N)H&vH>n}nVWbD*K-5&@FHHxOL!Tt=jH4+Ud!k6*?cbV;aBll zB#+ped;>emH?!k>H~S~QkNpI1(!cmU>>R(J{mdU==lKC1#2?~}KgKouARoaW=A1vr z;bG!Y`~@D%pXV|BMV`bD^F;o8ZsD)-bp9Kj%8&63{t8d#uk%d)Cb#qB+`+T?TRexq z!=3zXp3C3mdHg+|&)?@Reu5YB4|oB0^ACA3KgDYxBTM<8cs2hAAIHz|c7B#m;6L#3 z{GWUx|A}|-ANgc{j!%*ppUFLZIzP{+N#T5n6v7utGM^)b@k^v&zEBF~i=`yKMoQyr zC4-dC*Gm?Dy_CVP<5T!)K1zzNjlTq*|Adc}G`w5V@fA`WUnwQiKPc~&_d{lrG&>+aBJYzA$xqAA z%J<6;NDHB(7fFkyOQc?Dnba*UkuH^{OEaZe@J-K`9MT+Vo-|kLkrv2_a+2IA$I2;k zy*yfOkjKc`a;`iUdch)>$)ltPrM=Qa(!AcI?f#W?le9^?S=uaZk+w>=Nw-Veq&ua%q`ReE(r)Qq=|1Ux z=>tgU5z^bzhtNiWvQ`d~zLvg`gQfo?os#|~2gr4@EE}Xx<$USy@;EtC)=BTk@zQ&8 zg7m(uhYmAIC*>^Zv}}|9B0HqN%6921IY;`N?3BKh3#Bu1n;an*Nk7PL>8xBX{VZ2V z=jBStBNt2ml&c|OYGf`q$*|7lFu7F@m(%4m>0`*AXz2|(O8SEwBfTXjOMjG`p(|6R zk7T3tnQWFmmoud=0P;2mgEBIA5sdx zQcC2jrDT2uzlpuhH?cSPo$QbNF7^@M!9M19vrqVT_94HOy~}TB@AEs@2U!0&!Ea;l z@yB@pG`E&N!GrmeJcK{Z!}v2ioFC#5{3#yFk8(4Al^gj>*>R3G+b%9(qG7~eiba>+}ER$JT7PB!sbFgfd!*XGB<*|I` zVg;;_6)`s}h7DHwA7ptri(ru~ibb;+7Rz)jj>WSCmdKJ=GHkk3md5n3a8!v8%}BEP zUdZV`^3S9|$lpIe0{myd3g(He@d(FKDwt=!|RF zZnh3Gd_8o=b@H9i9owJ-{>&a=TiIP~2Ri_r@vvl*vfz)oA6n%G%x!<*wd_*pocq`w z_Bdt;R_K}~Y`T03nq(_<1Y{-a6!zwP!4J2wd->Ip0an>#Y!O?`USY3-Gj@Vc7Q!Cg z2wignw9P(M+x(6+4SM*w&@n+gS-uO>?KV}?-3K|hM~3Z!34vqmAvU6|;{fNIIuG%I z>khHH)Mvp9@{H-(hZuJl4D}1@_VbyzksK(n8F3{$45RkT8Kc@e%$)YAT@So6VB}z!5q9Q*?B)07VDLE{vs|4@Wcd; zg=NTs>@R|o?HC&=;N?)YBpRc{0FI7^Lq3_2`wp%(8~S`6S43{E2knDSK+hNlhp z2`pSK8KJgr1lY3%0i`nq*F*~fnch*QC3!OMf+4Kal* z3E3NRHgrkok+9&frmzj+n($TO2gA=q)I=d8SZ;yX9K}y(?s7dTdd?TqQX-(3*$xX@IQ#2`SQl3n$ zOZ_nIUj2N1pMHaWvwoX?zy7HHL;d##gTZMiGpsbMGi)+EYkWP#%0Ev zj1L$O8V?(PGL@P3n3K$n<~8Qs=9klB)4S8xr|(IBHT{i@#g;ltmt}osMdp=Or}acu zP1c7tr)`~Wx9w5ehxS-|kNqo0q+_OIgJZYjWOi`&l~OhM{!+zPm7A+JR~@fzs@`1_T=RYH^4h-In`*b!?y22h`+V)u+IMSD)_z@g zwC>%ylXYL$ovRP3kF7V42v$hqqRkby>b+mQ2^|oC(E@)irIK#Lt<93dFc-+(DUK;nt zxDUsDHSTP?)E?gcVf&Zu-?w|l2ak^%e`NgY<4;UTn9x5lf8x%G4^Mo0QvRepllD(~ ze$vrN?@l^7>FY`7CTk`~PEMKJKl#j!hlgJ09pb*m1by zSjYPvUvzxa@l$7DXLM&;r>(Q7v!=79b4q7V=d#YVof|uE@4UD3@y`A!EmNjU>6x-@ z%GxQLrfi?`z?6ei4o`Vw%7;_FobvtD=~EX@T{(4Km!>PSE2YcYwX^Giu7h2NyN-3e z-}Ob;H(ftX3!D}`Ep6J~X-`f&GVS=ZkEflUc4qqQ=}V@snSTBBEi;N|)Xiv{(KTcK zjO8=>X52Jm+l)Ok_Rn~JX4TB5nR{kFIrGTO*JqxXd1~f&Gk=~HG^=A)_pIJoSI*in z>-4NMv)Sy>*$K1LXFF$?&2E@IVfM_~XXdaup>x*G**NF+Irq+ad`|zISLeJx=ZiVt z%=xK%L-(!SySpFlKGgm4+?jJ1&s{b5R8LV)O;1bD{(07U1@o%rHO=do*FCRy-j(w< z%)52o?)e4ttL8V&zjyxQ^ZVz&I{)qYpU(ek{@Df6g75`N3oHxr7gQ{0Trg?D>;+2} ztXXjVf-MVnE_iss(+dp?r!U;O@asjh7k#z3d-2&zS}r-V#IfY2C0{LFyY$gsskf{5 z_@%LzZn<>(rKgsSTDD`^PnQ*3w(PPq%R838w4!ZA$BLON=C9~ov1&!%iVZ6*D|1#B ztz5Qp&B}EvkFPwj^5n|XmzyrPU7mmW*O#AJHEGrKRSQ?GT(xf1rd8WlJ+SKFs>7>} zt$Kge7puNm_0#IW)zPcdR@+t=t*%+!vUKDB1{noq9?zGC4Ohp%|`iZ`yj z>8jYPHm#M`Hm#kocIMi}YfoQYb@isJkM^bX_4cjm+uwJn??~U#zH`^GYl5yhvaWGm z`?@XbJlD2ed;9vd^}W}HU$^AC53g^z{(&2UZ&-E1xebdqe0*cgjg2?%x$)5(Pi)NJ zxNYO^U#+?+_@;F?y>Zirzn=f=mp4UkO4?-FWZRU#Y2l`2o7QYvx9N?WXWsnj&0lV| zY+kwf`CGzonSaZ>Thh0z-Ewa0!mV#_{dDVBThHDq-5Pvr+^r3_uDkV(+lp_iyRGfE zuG?O|?f7jU-+u2MhC3X09NLz!t!!Jvwh7y2Zd<%<)wZAS+_XJ<`}Vt1?pnJeaL4Q& zOLm;Qd*w^zJ`1Ib=z1#Ng-uvj@LwjG|d;Fp9hl3v8`H1zA2Odp%^x$LZk2xPZ{CN7~ z^B;eGpJiXqzJvP??>n~d{e55T`)YsL{@eH8yWex5=fJW9pC0(~p!J~hVA;XCgAYFu z^u)p^mOXLi$-buyPjx@__)}j#UH0_xXOf=T{>;yZat=NDY{Ij%pWXKCH~s7SUw^LY zxyPS7`F!{DN1s3O{ORYyF)Y?7QDj z`u&O5dtZO)4~9SV{^95w1#fJ6qr$>*}%7Xj+{O%hyHw~L+V2P8HnzPLhO+qrrzOx*j$*RiTX3} zW#VA{iBx|gOQ!logIX%kDh&>UKpXii01rGv@WP}O^(CS#T9joV*D7t~RtLZ;%D~Jq zgGra-W*08@d2h%B)GIeKTzMIcU-~R;M=k2rp^h+72SdH7sMnciG+8pu;oNMp6c!YX z$>no?)BE?iRJ z`Ln&Oy*n!}%jKFdYFw+u;mk~~%XZJ4a<*rjC8NG7%Ag0HC6*1mg#&NV=-@SEnqs;u z;E5s358^GD&=l;4w+JeT@GHe3&cz6%apW4&KrqUZL|GVWPC}V8 zFJrLZ=xe;I5R}fO-U<|m4sQwwZz-6wV8NV%mhgbSzQs8e6*>9k<&(3EHm$$**M-@W z&Yzv`$a=t%Q(HWxxCUKB9~;p}JFL))_Azv@k1Fr!2YDlqdJyQ>NB=GfW(lP?LX|!S z4)#aJN7SD~EOfBn5$JcQ=wpm4$|Y#2iwy*gGtD})dq^J}`!wid_kso819McI?5*Pwf~IhF^q;8w@bVwnSH_-W8Fjn(#}08Run<{eeT zsOLY-hz?Pp0}ZxkF;O8F3^3(Lg#{H7Q9+vnW(y?byYh?N@!^~&luju(7|N%Vw$6)R zFe1-ZR9IM)lOH}iX_;&OO6jn`_RkuO}G2yr$6Ms(;UGDr@=Z zFbw=*bnr3X|A^0`{_pvWJI|*KWi*LsAZbB^WK0M*_k*e7G7@YMjIoq5$Y;a4b0IZS z2C1PFDajX_tH=AZ@DW{;Eo6c|yHm=~!3U`-P#IqvuvubdS!oHOwfog_YD> z_08EX{!(30(1@a1qA$lRk?~fHvq-kIAF=^XE1UTUa2F zlE#I!$qYw6lIO>13oLdX2qwTyk(>_+D-?=kOs$zewx(-jQl7(Lj!ZViICGaQOvsIG z9Je&fddZ?3Q$$?dO*1F2vgRF3&M&K~EZIJNI)*`|EiOu*lj_LRh6LprV)bzm8ckeF zLEEfAZB*0j@c2e&>&y|__^8a*iHRw(SOhij1#MAn_+OC=4xxh(Da>x^hX{jnRUjk{ zl#mB05l@JZ_oGA#6{PqTL{UMMpAM3#Kqgv|zmOEU7S9#KBMNj%Qz?+fqC|u7D9aFK zNoXlU;5S1N({2)f(7Rs6tSTsZi0GEWue!2fcG@Ucb47VTcyr;PqWUWYcn$=4%CfaK zLwiw8*|_YYoBH~0f++U@e>*HWb?#Po9gQO|kH*2TTn0zgy9llZBaIaRT7eN4c$D(d zgP0k{qXz1?!4ES&rd3$Ac#u^`33jT6pKFw8p;}?pYA>te0!){yA}Uv9Xsg1iRMQT9triy<60lwUIjSv1CBrS^HR~@uRCcdVEj~BXk(yFwBpxBdA~m74(A& z0jiNNp|c^!6{=Qpmo~sQ?J_aNXwjA!z*`;X^e#>22~`_Bldmyx1J8t9$K3wnfMJUE~)7|#HE_-Iz_%TT~d zlQd!FV__1N9%8YBEkkg zWC|Wq4Sx)NESaGn%KY({=36QYdFNYE=9nQ$``^vCf+u;WTb|FkWypk!<}$^D^X6Zk z%lNu9j6nWf5`FytIhUb1^H8IN|MHwUhg*hBnuR<>R7x=TcLJ?14YlQmjSE%U`MT*p zE=GV*p)4H6mj?1nnFK2XTPEAo6sU^={|D$yx%w1qb&V9fSeKa=z1m{;905CFjHY64 z`#Si^!4~ynvV(o4eECbSc8zC>Mc5%7i#gQfI_4p7k7NO1SjH*q+Rm)cWk4tWqzG7{1iz76(soSNwq6NV=1-|tvUw^3cwpAlNSv7i)O185?z`3x&UpwInMy=$CeY5 zkrLh4H#ydv61i|TZ}j}Os3aKH5ATH0A#jORX^8{g5v+~~j&M3Zc!%+^YWZ9oj~Xa$ zm+aDW7gV?0|M=Q5F0E_yntf96=l%Vkiw4CU81X{E==4gW9l|^O+8ZX|h7nSgcs>AB zXNXUYswfo&^6^6~0w%=>m-(j=!QXOXDnGUYV!KD!BaSjkTGw zrDlT^{J|4Xd_d{^*)KP3`V#cTqPY|X_`}G)B}MFS2MwFU4})Lj03WzMK|tu2#2Cby zuzCfA#>lRCm)j-76f$dL4nDqRv1ZYR0|&P((JVpuAAjsI&w0;}Pa>d|vk{QF#CuV! zgLqG|WWBccFw6}@b`bGikpE;bM3qM{%7DEHU=M^1m&t~7%dQx}#W${cJ2=tgG&E07 zc<$<{roeOTra_p-|$R>MCKS*3^yb8O)5U; zzW%E{r#-I(bRIf*Q0hNFN~-aEj$R-V7Ey-%Fs6){(O=q+IiGh%Z$_QeUCMo4c@+j9 zl9n6DT#TAg}fHoAp?qq(CZ z>gcMS*V^lyi8^z3LVRI%TC%ey)3bB^A&bpsIdosT)tY{v)Sr}7Yqh%5qj_L-W_h-4 zOp(4fGsBc&U7Tswn==&|QvjJbM|$-?`d)o~^C4WZ#i9VSB9Im6eOB4Ue6O?Wm$uq$ ztxM~tUzu@fMBT{7$&(vLj*VJmSsR)?<+_fJ8>czj#+BukbWd3_byihwWy@rOQ4%z{ zilQ+flExC+yz?*O{IEfO8AgdACKxcR6VQ@hJ5Is1OGUArt6pq7k81bSEp4^iTbI^( zJOtnxsoydA+R6Cev10O+B~vDQoHU+^PtpKW2%AhZLWP69<5`u>7sne%`LaD^9!7{D-sHPg?~vHL%U0VCMk za#TD2jpw&K*7LP!)$>Om&s+nZ$z<2jDiFd7{q%%7yYlsY*r?4US(d5jQ4$>~0vVig zF&JnvMWmv!r&AYP%;edrLIF8+(#6AQJWR(2-j$b&d;KdU_`M2^QD!MZGh#+-#e!x> zcKfnX)7M)T#f+_NnKr#uUt~?Vkq0eX?5fY1dVNR74PDOm=F04{8Pj^ZW=5nr(z2I& z)}!MJ&4~{~*ddZPLusyd)7NiRWcv#I-6g{0P(6H*6cn$fVY28xCqMa{DEJIXnueRD zxVV~bXmzd<7p=;TEMDQ40yP6D;q>kdT+sXC!(3d@`{Yv(6imf|2^b@(L3z#h}e- zrHfI*sXS-^qf!2jzNFnXYjMPy5xH5Ju8+~z;M}I7w7C-!v-9#?{F=ps^t}Tx2tP{D z1@^-*`l|B=A5g>mDE{;f!~j+3OU4c-bq((c$RG?d1Vp>0tzRE7=do@7;M%)+*^)N^ zFNG$JWc>dZ@`f}s=@m4DQQf|tLtvbL4&nniCmHrB>Wm~k zCtom6`gh&)_1AxiLy$@3c%k{iL@&Vs$Ym^oldCN7IIfYC7m1s-Av_bS9Z@bQWpIWx zOH*|7%|)84ZhJy=;O49N4@*7gc+gT$2oUiIp9sbrgqg-*7}@ab0vZkDP1U6H!90vc z{?dtz1_>|{>_G&h3ST@S&&-n=w8-rj@I7#`rbCXy{)%?Y-i_Kq|ltM>pLOBa&o$E=;*w@ zD`!Rbl*LoJ#ShS>=+ddcUpVZI=}PpavfLYqPW${Dzo{=sjoom06Y?i&el{Fwi1! ziq#A@jn)GZ#4bc+0$}y&eU+v@ysB&<_%QBc@7fztS9e^IP6R#CnP%7m+nB1A`%RPK zZqe#oLw!n8?5z6gWjA<{zV?zu`&FbLtSz@iJ-yS5_PY-1s`m{+yNY+RD{0#KcS1jc z=0#t~CmlIx!>F?qUtd&0DApuJnfP-sS+@!yB9L>`cO|9*xe3)HK#B;gZ$w~yBLb^% z5lk};^uhU6h~I#g@IDb5g-RolBwF7Suh6>&PeQc_@0Qsf6f+|nxxX)k;3Sv1;!82X zrckaD@g4<}&`2CqCFDy%V2gijf$*fz3T3#&?NhGG?o972DzC7*Yu+-~OdHv{(l#Zj zvACop$5p*>uDNzrXjQ=^o1-w>>ruT}v$K*x$I*NqTLtWU7db4uu*vfUORrAI)&aFy;&>Y{6fBZ2| zcXMx}+1%LMjQvC$49Kw4i_F-6@9XfB+-@wPI{JA*&(~e<9X8usI5-^q2L+j1ymn z$<;|MlRXfxXeeJ)6O_|X&BPtGv#a#_s@b*IU48X+wS|SXNTGRM*H4;wLs#D6M;$F3=Y060r3ro#0sku1~ z|MaP;L%W@nS8rcyug^;wym&#M5EgGR#D^tN452?R;$aJ5{|JNTQ=Br9AUok=SoG7y zSk#|lil(9MIVocF=v19UCJa_3L|LMGH5>`>Woy%Trdx!r>4tW?w(yHESIg&N`L^0& z1pd`Ii}k>{^vt=Fi05HJNoC;q5GEFJht5@p$pwGT^F}IB2qjI$w5q-g9Q08PnbB!8 z-LxPw^~i{A?T`I#_agq?ll5<$Kd=Ze8zqw`<)h+)GJ8>R=&}%Nd3&LI(nx)Z zvnqWnpSb2oUTaQRPF}g)VXkzh=!=`O9SzkZ(^eDi5-@h70Cx$^F(R=gawGuo!mSE= z9}i@ZJ0nAJXG8)nKK=cX?jsaraG~>UrXL&<(x(w;or|4S(ZwHVJ2L&?gJ<+1PFU za0U&v1G9)6SHW_iGg z12~~2#%>}VHu#E8QT(Q1kY@i;nk(VraroFEK8;htsp3Q@GX=6DVJXoZ%qOAS6YdrzSlx+nktNDTpp;yx{g^)~SBDry`nmDci}1lXNk!6O znyO4bhN3-YP^yT)b4U4%YSY-tf@*!)aPWyn2R+9%MU5qr!H?k{<9~G5C)^>CbqU6fjT_IAawQ+uR)q+N=wQ)gJ_54Qb zqRNV%ic0Y#c-0GM7%-hnYoRG%r#!){DWC=ezf$g#k*d2#9}BQyykl{8mBu*k$%CFvR~g4p`O zi@_{baHG$N_4m|eYU46v|Td_YywcTbv6STYN^)0)5c*CqImwOs0 zRsyk?I621#9SPel5zB)#d-2*ps(kac=L?uq;+lxbXiXzZ-Ah7JEJ(<>MC7Dxoq3hm zMLooF6Y(f8+;qm#&lZ#yXF5wGJHs;FqwMpS8;fd9E!^p`EnaGA?ST;vS!o8EI1zJ98+SCoSIl&>@Kewt|Ue23|4+BQldo}Xki3wR-#1+9Xz7YBFhVr zx=b!0BC|}P1sRAm%&;8;?;m1%&_dad#OR}3w45q3XyJOASiKAjo3E^Z{)f!+| zUA^FRRzYg=7YlQKg-#D-Z{B(1#4E((f}j4-d-$G(VP zw6m&B*`qrh50=fS&k4~6r&wG@$JioaXO&b`lpqN^tMR(tj?@e6tj;R)ydk=dvnudb z66hC1k=K59)(9{CR9j2XkD}y=eu|sKYh(qeG$fA=i5)Vr;=Lx;-tSKL@(=%Vt2B1) zpL@O)poGUs707 zQiv3)cQ@MWCqxBBwNzSb^OBSEYOR$mxSddMhkerR#xV(Q@xv=z_DH)+{0RPYfF>5m zH?r~fk_z(LSO#Dh8y60-94~cL#U`ke1L}l;I+(Fk;(}u-B@Xo|gGq3q69r-!`7`B$ zmXYJIj2uU^AQ|n^6aW_y%3~RmB=v(M;zHqCg@3&UoI#x@aBn=z1qRy%CTdpM^lFo% zeqz>xrCqi5i}SB!qm5i+u3a)I#oju;vp|aV+G}87&rUxMmRT`)=|%981Jn0YDWui|qC|k%XhMb(F3h$2;Wd)F5 z3j~Sx$~5?%mDG}`#R`e1Y~!(^Off(z)GLHK_$*#Hh`dpqWK>}e z#@(1c2O_{wR`xuigj5tFYGEnkoH^{dJsn}mE4LI+E=`NG6r}2NQwke%vPQX5yQ?Gg8}I(oU`y3o^6a+s(zspb%(AL5*kWk~qq8zfax)MQyySA3f^Cqu7|&o;;B8v_ld^FPPMJgUo04{;Qc?5r5LL2i&<0WHR}1O1@I z1U6ChcNC~lM^xZm-t{@D!E(ExOUPYFnyC)k0%3E0EEpMwie9QL=+RcX67k5QJhCX< z`XWqP!L|mvz_#UP<$Jt7N^CGpL984#&7_+P<_Rf99{JmQ1Tn z&$ro}$px(i1#Jb%*@imLUs99WT8-878#0{}mg5YRW$oFo+f!GjWVo_M+u)Mp7!iNR zserwZxH|H8Y6Sm^Gm{{N?an+m=D?*q?eAA#ji)%fRDE9x#OaS0yH967Nmky2=biWf zc9?RH`s4lTe-2LqsX4WUbV>8mVgz0&@1N#9!`_G23{QRUzpTEG^J!(}{SZJ+H4xP0 zupH&n;uVyn4UCbZ2A)#tq~IDFqwG1n?X5E&y-ef(ct(Mt9w(RnnQg}cwwVeX6#Fy|^9}yN zX;P_p-io%rm2~tR#sSK4qVpAmmw`(KOfk$wFb$1~@m~jK3QWKRMhZnQ1jD)R7Q36o zOqoMg5S#!oA~_>|swpQ_pJLe#2$M^ToYBTgOF(8aKnAUI(53>*Odl-&t{gCq;g7QC zu*XMcX#}H+XQ%>Nt5%In?D3(sL08n6WjI5v*mD&1;XA$w-&t_FDD`;`&V?gNR`cI6 z56WYhWq6;}m=tBt5T>7DgL}DzeMEk9+R$}#Rz_s9C2o?z5t5pe!GD*T8k6iU$cfgM zWdvp<#HD7U9SeVp{Ym-;?bwF5qtaWn6O49f7Ae}g7)moDQU=?SzVWt|R-O@%k%+d? zSM&)d;89=aioVWep9{JJ2L3W&u;4sG!a)y}6Kn&&Vt--}VfK_tu=!$zAt8fQa6$$t z90*s)Ac1Y^#n~Ok?2y#t^oKI_u|*3q^ktR+OG2!k;ynQi>gF$_ZVOJfqc#_)ZD!!F z2x;;62?ALZd2eulD~%V#>*RuyG7H`D+W60(f8KJ{Ri*bXy?5M=Hxiur_$Jo-4OOOo zBjKBvOn1Cao4M~Q3%=qpH;%(+ZD^m``4-<;aLOKivWT4)a8X@9i*ky^(&CgZltXVc z4z#hqV*f+{D3$Mv3{zspC852&dP5x%B3d~RXp(@U_(H4=>f|BLiAmOCYpva4v16$2 ze}$)eK7K_?x9W9iR%@D0kA)MZ-7{#HP%$JTO=(xyT7tv>-72QA8p4QHL?^k>rIWQo zfauHGkeR9i@isTV@`~q^Pp$e`y;b}uFr*FC@lVkA6nMJF6DN4j1xX#!?{ueIltb%zNtdE*xU%=npJ_5m;GFVMmaTBb`LjK=dW!A*~Jj+zla^ zyHH#VDYXmS4a)RTs~bv}J))29kVPX*MNRpx_EPG(sSK=QLx;cEA-hEvZK8{VU2p$p zDD5yd23GQ^(m9yN(6OOFy1F+n0Nff)t0&Yr*pJ#)=gSy3+O2SKL6n!3WNwe!aJTZ$vc4YZU4aMEzbLsW3(| zU1>7cCp)V$R$Hrc6BBc)vsPQGa?!w;8&*u$r*|z_QPEhwd_kARIQ_Egseaau`eRYQ zmSxc(7>qv=RvOw+qL|4ZBhG@rz;ryGmT!>SUutgND6i`}U(_Y)D#Qu;{}gp)&}!0P zU0T#bt3rq~5FRRTJu!$_!5UEEKT(f#?&q~#(lJ!TSu9{GLOn)SO`}2CL5MyJFGnuT z=n`NFz?cI&k#~~9#7JI}e*?$esIcl4SjA|xm&Us0Mz;b2<}E=e)e@g{0iQ=at`oC5|whoALWEU%9GTb z&UB!AF^dya9frPP2=<{*#ix(%YJrmw?ELQ#jPUXQfM6gV&=mnnCs1A{kZ(sxH7)u5 zU!oqtfEOXtFA#*kL6jj1{nylKx{z8l{;r1{{T}jV=)8^0Y~TzsAM!Wq`o)mEKd;5u zko?|9EP_w+DvR^NhM_MtkW6sZi8&M*>9VM0@nZY|jvmkU^QY(+k5OOgz${4veko_; z*v5XGH`~s}3%utEY+_84b`u<++9ViJ#N=dlL?ie4Nyy1eVP(-`Pc7Azu3puv@6oDN z9;-+FbcTpstY7tG=Yw5+ZzR5L#Z&l1M4-fwAk7>ZRPvrEL#Lf62SF*pnux3r%chyi z)>v__47S6jarjI4DZRTT*EKts(1*3x^gLDa$M+LWenr%>q4uRvsyx^`^i+OC3vuCn2RNZhF2qugn@ftGK3k*66Iki_SE6b8c?4JFV1dPck^to6;S|Bzrav z9nj$KvDx?&DxTeN8466}Soa~ACizA4(Bg|=oWj^cFrMt-4_c?Y65cCvXyH$!u_~n& z3gw{HOTs36zj)a}_u`=gTB|~oRU)^GvPzV>MVSi_DN#!#Gz#bAg zlk9ci?;vcDj(}!1_6&uRdFM_^XuvSY6RKVnau<*;e#Sjhi$I%KAaeAFV=jc+JSvdWr}Vp~D^xTZ3_*=P$Zh#E7p zzAf{G`qZQ}8*fW88_dP|?joGRkQ|$xmQ`1C|K#k@xY(3r2k>PDzWRW#5D8Ck-PzeTYFIqV%u~N&R?t+|;S# zC}mi!8A!ZwPDjTaN~5xEW$@(E4^@i4c|4i#660S_tNRS-Qq}^7osA;snj}OZ^@vso z!H)!MD?X!Gv{-RO)u6Z^9HDd&1jQo6#{Jb&<4j3$)>yo`)#vCqDS367nYDQ- zDS5S-nRR(7lZr6~!ZfHDwug<+VIKUAR0@w+y{W z^G;!oe1`A6)o@S;P$28A<=itFh4jw!zT{7e3$r z{1I)&umv=~8r0R)CY^sV*zOQN=6`M%@wXU5%4vagMf>L>AXiA{(2)u8Bv<^PA{q1H zLHq-3fbQfBojnpL`q^`s&-@ zs3KGi?-JfGf;0g7o~92Jd#Ch-%oFS;Ku;(zF&ximIzdu04LR-43dFS@c^nc&AS9xf z8Q8n;Mn^@$$rWxSomnJv3(ooGFignCpF?~4>8INt zZpW|4c+kTSgE$VJEaD}#%tW*r^hx;hh3L72<~jB}WYXCIuihEBeb%uhNAbs+5zzu;klmQ>nN(+1wR|TWPZsID1&vj=G9Oyl8pz_6e zM~=)Rtw;5F@OPi);@hF?JHE9QT0yGiZ+qs=Bc7|pk4{>N_9Iv$L4Y%PgyizcV&7JM zf3Rq)KA{H)1S9z$6p%`z~3efQPwC1Wr07f z;WY-*5fBAIBcu#>+|qE1_y0d{8*|4U4dVYXs1iM|WoGoL5?4_eNtHCa@{S}5mtjCA z<gNpn%zu!^p#{hcKc6Ta zI3jpSjl+ZZBa(7m0REf9w|Kti;XhC0Y~B6Z``3wj{^hI3Pxert%y4&l0|+r-z^F)%LDBJmp2dHt4(-w8014An_bRw&x(^zB3M#fIkodZB+8Y%tAs2 zV*BO{3{Wp9omZRC_kc~nKQq`T_`6W+;M?0N_(r{l2oUSk>Y6MG5Uuc54BZ~IPhEZe z#JBp2I$N;L{SDSuX;pOuR#k}~YuG*b8&l9WO16g|{=^ z=7iwVDtZK*MHF{@^Rdl#t};fH)>d#sI`YPqat#lU3s%99g7wFIjSo z?_PNP_;KFn-S1B})fU)PVI*TO+}}9Nep8nK#Q~epMo=7Nc~ImjEG1Wza>}weY16u5 zRO8$gqsPphEj?S&+H-z%1zFHCgwu5C4F47`WEa)-pG6beik1_MAOYotB-hi0 z1S9!HnlDX}pnYCR)Q9I3Yc&En>3n5KlI=z`)FD2W!fNTXFxWmQp?H&Yc#g&Z)mpNwfv@Z3kJKyHSNkPdpJHd}D%UKRCF~AS>IOZo5 zVLgLZv%C@3p#ax7IQLXfv-8=DKBW%%3r!bCufqyT!3qu?r3Zr(zQ;h^D<@-QkS6rn zIYT1*b;b-F4~_YYR`+^pbKaDd=>@SRW2Y95zPuwZujBI3qgHj~dD3`MTC+2+Idytm zQhRNhr8+m+HT}Br?blCtxu##=?*6?lIa&96^hbwvNRRX#A~EtQVqAv-S|GsD0YK|u zf7Bh&!)6DFvWRkW z2X zWk@F!kW-w3ENPbo<_L6@1O3$voV$Rjy;(etMo#D2DNDS*xU{%<$OT&H9eRMKl$u^O z)jhJS+LT&2HpewOJ}CLF%MRN+MrWTZDlacW;!$H3jW%V}%`PvQR+FBSDYx8Enbb7@ zde3(?m1UK+wfLz)7tzO1mXE*3xPQ;fZqG z>;FgFn*c^uRe9s}sL_6tRopi zgB^J@8to913=f>WI=W-B{k*ZwPUq&a^V%oP*MZK?0FM1tb0eEWD_4d#kIYq>ujjft z@Spexzi+kFE#9L234RP$hJ1>>-m$f&7%auN7L^;y3N$pM3XK~8l@1evM6iO|+C>0p zREdHv^p}sCvMHqXIY#MV+ZrK$eM1*|%8a*B7a%l%m!9IL~h@!)@lcIDoj5hE55zpC>W!}`FS07oJBRpDA_DM3;C zU_)57Shw!QMIw^A#Rx(di#}E;JO-0sHHK)j%w#|FuvS(#XXAp5QjS|Bg*gSwxB+nr zmNBo@Q8^)6|BUBE^^P_-v~Oiy-O7DK%}YbIIh*Z+fv#0q@BH!I-(*d82SP>l8yltb6X(k`V(*cH6SBX>{ggm50+W z%ukvaKbU@{<0Gjzl_akm-;E&puE)X?^QU)3&cMP!91nynd#LP*UppIDd1E%=SX5Ia z)?YKOF6S$m^F?!8?3YE?ksMtY+TV^(c3lTad)*B*V%U> z2iG>7lftT2&*I-(Yq4LG3tFvFdyp}3Y<4R+l^FnPGoI_ov8)|I$2AvaEZ9cFom~7R zd#usv+mGW49Jk^)j^hy=-@XM0u`lCz5{Hek=+n6P9S#wTK8_3F z_MVg%$#{Tl=HtX!+L3U%hZ!N zUdMsgWy&B2sL0`flJSrn;426CLaGq&%&+F1V(TAL0fm?opX$XAlpu4-UOjA9oGq#Z znX4++nG#}(E2`qfl~~&fn~&0}EeX3Pc6uc}LW!7(>Jxg~x5`s8N+Xi;^_iQ?InEB0q?w61V_GCr3b zxv`=l>#~0Z?1LLSRCsJH#JFoft z=Rbeskvs3S-EqsKM?U}g8*aE^_43tP8123*BTwNHzc;n`S?hN20b{+i@7lC3nxWY0 zS@1$Vexnq>#dDcDJJnfOdi3fUyVjWSx)q}${R_Gy!3(;Z(=QHiWMqLwUNC-hT)$}2 zFXr@%llsMjyx_O`hlJAD%xv{OPazpeH&Lkls!6{y>{h8 zu4=!(+O=+v715TBD~9`1l5Ba^9X{`HutHn-%x8uwh9g6N|M%5yS5-A+S}SDQIOfs^ z=@HU?#F_}*FZBUU^_VxFTXmU+q!48qchiC`h9iuFF76zT{Wz|`aVw7FI3B_AEgUCt zypH3yI2LgznFjuVOrtYEvpex8LUJWSEG0rLB|>shDkVa42|1UD`AdYHOT?QZsz|}H z85aj|SiuU^Vo9e4G69f+>7zJ5h~vvRp2U&FSyO^7M%1o56OV@mOXWB?= zYFb`O>PUKBw_`)9Bd0N3xiPc4xvwU5q^PB^xFyA&a@^~y@h52s*101!#}D89MaSHX zb@qbn&YJ$b^0dfM!Qi07UsaqqYke@{Pt#C=Y>oVAqDi~WKGg}NgW+`>qS5kQE}D_L zRhN}mpLX%SD7~86~!673OCL)N<26-B3z3zm287BPSDK>cCoNcXrY|d;f>{>unuXY z&~kQJT@g=}Cvn%-#O&nJW?PJ%^3r1|*@^exomB78)?Sj`U9~#DCT}`BSDGpV9(Qkh z=5$_l!D@eJRtj;+c1RZMY2i0kTHKa@r43!J8plv3li}P^&*FcTkz>5wsw<{BqKdd~ z5*Lyj5#I;SH0q`~p_|0trIMmKnkN39h1b$^cEszsb9NH%%;_<)=%gxRa}A(Z94LZN z=_Tqj_CB*G;xF2V?>n*<|GWF52M_an&nFJrw07G?|MqWhL8URGiPKwJ+fRS`(Km=c z^s_|$rw{mRhYGo$mfnCU43ZXJ7cp0?TikI)h(?K_QtM`Om*f!!SDrvS+^|KOyKN|n z=@+mRoMp!>))dMm)AU57*2u?ME;6lwl@r}HLRG{FPb0c718Y7V$tpLGDm2yi2b~7p zxoV9hN@u2iJh!{9cA29cMGtu;`L*8Jr4@Z8b!AnK%F^OIdwG7Uqh&>C*#*wkGo#-6 zdhe+9#Ll^{*^)aE$Fmny6yJ=B~*{q~GbtDX4PQ6goR7*2$@#f!2V1Xdx;=DT=#J*2?Mr6zc3$3bXXF8A^-Qkujn1~gNp@a$hil9mqL1`BodlriA zfY3W#a~yf5+J9tzRpcAr)y7xtI5P2F^f|hqzE1m7BxCzl>xr$~(N26|=FC?x7#qrr z*+cyejKl}C^kD|iczaaq&K^Ou!IjOJl&XN_>aAxyr6Q40}?tarzm0q%B$^$RLYlOtJ_V zLW}}U)`Oh2+bcti+SbIpaL4$@jpH5Ryu_=nx}vxDiuL~HW`9Fd(=u&Mr2;3F=^8W(UeD_`*y z<>CWlELDIK01gmzwTVb0CqGCkJ1l~u zDMntR{lp4Xw15S!KsD^u2%*nDu)nx^-MXuT&)oTgZ+-3c2Y%S5HSW~hk)QI|8To+* z@{qk7Se5f_Cv+%QSC+`5M%OFB}?ZJ{y(u=)Y!Csyg1XjBrVfab>QF>HQ-DU$m>S>df6CYB@Q@HboAwiCY)_} zBbvMVbPDo^0HASHSYR(h?wnQPFb@1rjb_WtxZu>jOsLr+Y<(tvuR?}v-70iua z_k41#^OBcdy2R;EdS2^YzGC^u$0nDj=B1_Qr!8N;Lc6%!{p#w~*Hu?8!FcHZ(3dR( zwi>l>G9;|WbQlt*HsaYsZNwfGB~1gRKtu_Ao)TCyB{2L;VEC87@GpUPQ3AWa1a^N3 z?EVti{UtCuOJMhxSTc$IY+PWwQ(~e7m_U4y&_+Vx79cvC3~SXQqB{#X!;v^=geMa$ z6CdIE9boh>_`dYNVQ|s4Nre%dDu3v>w7%#gv6FKuxGxot;-a@y_(-TC;>1{Rk%PmD zLxqM?VI}h!Qq`sXMEEyp7-t&BnTBzuVVr3gCpMv#5uG$3C5>h`sVfa*O#@!jfVnjK zOE$|bxL}Zisx#x1Y{tS@*Rx;w_&sjF2S#V40C1H#k@Q^Cbd zba6x&&V> zDxVl!{-v_Sw8}Pb;nW`IC$F5kVy4kLv)#ETP|=Y2?9t)5?&2BklCG77+S5tfRxNy5 z%dHAlRRV92E9`+fgSd6ja)PwfDy%tOt{7FcafYGPywV&*&vOP3q<1Mim*3$7{SaVO z2Be8R7QE50rW!@vEUt(?HhgEnEY1RbSwJwUn7$_Mk2IWXa3Nu9x(C7$7$eHqTsGJj@4>gwgw*@aF1u2sJJ@|Go@lA`J5t8)ulTV1}X zI&2le-uHiRT>{>$LX{}GDObU8WV8>q7T(>fbEHc~BwU~|Hqmo|#$4b|7r4^}?sS0= zUEod^xYGshbb)GJpjsEG)&;6{foffPM8b8>DC_$LRL{wrz&73vy!U}z#z+JmAWZaW zTuAPUW!JMAG0J=AIXXo1|6GM{nnvuKvT8fV8axT&TPts{;ES2(n%lDABdm#p*|M7=$0kS2^u5ouo=^(}G! zmoi_3#YxElvB*Kef|IEPHu7e*S+eS3?tzjPUCcMk&3!epPW$ZKoZ#hd{Qfv}bD8B6 zv?0qy!&se{aM4${ zCPm)VN^Nb;U$tgGRZySy;R{oj4nF()xwUi6HFI+bR!{BRz-)E|GK{fz9$?b?l1)Ot%TmY#8&bBZB(1#4JTpN4c(DX7dO?1liFKPZIsuJ!oeu`l3%OU9NR ztqgBj@}t-5rt6l~PuKnG^Q)#;jZdy#_2~ND{rgupN&2Ae#6I(d&<&!Z#-)sGR??tC zQVH3(D7)F3W5uKecw{V?aSGB1pu9F$o6Y7^tJrB-U9sO?`yA5_>fGAUqcC zwu`i%-a4#KQx;0Ymc4toeD3YH@AuZ#d+tAQ;J|rn=d{qSEt_|*Jx|N`dzw5o7?jPj zO4%bWORXiS4KZ#bb%?Y88Mmq9$daHHxhhV>+wkoK9X&|Ho7y!RhB+A3HJJY94!;cXqw85!IXaKTyLdGRo62-XW2`E3Auf@LH zGKe08bPCcMV7NR835*Xkg9yEXA4vR7xCrwDZ_CC-B|mWUh(%loS$Pr{uMgH>p@QBxJ#0K78 zgA3nY(5_PZM{5u7ST;O9f8m+;w1*=b%==tpj!?fy9PEWZ?!oUeshg+42?e|cwoKn%Pz3co)Fo)?ARXNFtfio)-X!u!yNyHLTCZQsz6&iOv~ zM(^i)I->VKszEpF?{9rM`usb2Kjyqy6YS%_Uzo^ z^p=c{dWYYc9}a46e_~SRWcTFS3wO^p1{*^8%gZ-U?Aq5F`OEfNPhC$@RdHTwURHK` zQ(;MJzSB|LS(=d8R1*wNkF1*T)Hi!ehZ;hg*S~jZf3CCAQ!}j3r)?lQpT}e7|CK2G zK{MR?b`*Yx8E*Tsf+yQfMCk=|XVdB0VNDuc^u5QU-y{8fD*C)9V&Hd1;omgD z>rKyjDhlWC>wMy|kxxv&zxWHmkH&jH9sOSH?|&x-{y_BipHc7zJSR)=wNNg$d&u6v zuY_~_68=C8oO~_e_s78bz7l?)87}x*!taj4`?&wKx}PJ1Qx^SQ${Bh8z0v#mp5$x7 zlWmV`&BpzLujT!B@_z7j1NfSIc>9qeeVLU8ot7@aBX!^@V@;j8Z;s$v6-_imcLh1W zg5gEQ=;k^t@f9=Yv+aoG)c=^_E5L>_w_S>}-1DFsuz+BGFXA2n4C4Km6xNN-qf96u zD0x;LVAi>K)1R2NvUS_eo!eShW+g8Cy0NdXG0@u^&ypnxGY##x9J~7FHk4RQ$85J9 zoj3BMoQLBv^G1G@@CVIs!H*JthZ!#TfpGGpKF^>R!H8;3YuqwJ8YJ z+}XPu)WyWJG+x=(9|TN8FdMJKI0*wuUfjejEgrKB;!2t!)EG@dchJ~o6L2;NrzUyK zvJ~Z-Md%a2eZLtCNLHONL-;A~NN@s6%)3RsH7vep*Qs8uMkb7fP1;jnYxsAn)(otl(f-Wc{z%s9KI zg0qc*MyY;!#tM=O)`xr7b(TgR8hTT6Idhyf-sqIUK+EZThm9J}loM!HnF@tfY+eRGW24_N@oP+ebZ7yph)=e(p2l&HC#o{4NEj zuKJnv4Zu@?$7I_lwL5T@_1Mc*Zs{LnS+-08F{5Pcw-=XDimTOCowE%cU~XMQZ!|XB zG{DzpatWJg;IP52V~GFk^b)-?6xz{cLOu#n1lj+|$T-m*iWTTI`OleY(rz(J@sECV zp|5XYj}qlkv3-2Dqht2Wm(Gik-M~{F@K0D zA)v+tQ1z_9lU`QH9e+IFcmx&l*^Pp0jk&~?b*D9A_d`Zr^((dkWfvd4uP$2&G>E&) z)n7okD20&(YBcg-)jKM|@h|G89HCgPr@c5nJY7AoKGeMl1@P@Fn%bAzlQMei);6wm zukb#dRgyJ*SzBLUTRXZYJ>ER_#D~WBiczdHs_UXbv+ zqVPUzvaqWNr-m#wy$|gwdH=o9``^WT(yo&CKgzzO`uhvJO5T4b?+2|wM)CRZm%^5} zDdqJZC5+=3os^468bu~0WcF{(?h*VYVJ$&#;Q3A*)PwyvCUI=WaRA2x4!TFiYJzl& zTLsOFz*^lxgN1RHD-6O?gN32N!qWdaiHq0yfzgQLxRCXIY|iDuUHlq16Ve|_?ba;( zupnG*y=0s%l#SIETo*{qql9NNQWntfF%ha`mn6>|Nhg z9(lOGzhTfmoaS@acG0o{o6cc>`HHNn zQfGI0u5_2I$x>Yzb4DLW&eMoTndYo{Y}nx-ysv9?sGZIZJi3?So4~$>5=~ z#d@nt$bd}XA<6c@n>a<`_J(Gg7`$1)t);Z z4lmq#YhuoD@2a&6zD8^+m1kW|E%>0nbJy0$*R|e`j&ysaJ@OgS`zsu;pm*$722Qwd zaSp4HHOn|YJ#U2~w}|CU=j?B6;K3L+-!7OtzFZi7g5NFSmhfVq<)5wpj(Mk14-U6{ zM9?8VSY9x_&%Nq?a_#@2PEgNzRNlY%9-i-{PEhbWc|Z8uw%A~K3vpAfdxQPJ)~>QF zn>!4pMfn@6WVja0upi<~T660Vp(IqLiU_LFXtPS!Iv2_!7s?_RGCCK^A{WXc7s?_R z$|4uaB3CF2E>=MX8>X;m!i%q$F>D&lJ!gc=ogWiC9bc$M9IG%ZweYECRU8fXqQGP2lVEwe#Diu=7(^>!wI(-H3CDa$1ge$tUj7Q$HHVFGWSKR-YhHRYV8^F^b<*+ji zwo#4QmsZ)-2Hz;-(E!911{&pROfaJ0LCAh4J2a*al11a$AS(*;-CEYr!b0NMrMExv z-Mijidfb}!#>VTvANjVxvE;+wfFJUr<6}Q^!tW8|AUw(T$(1+}jxEWvqwr+gW0o&Q zpYvG}E0@o?%hDM6I>wFA0D9EE0V2g7)!Cp%CfM}3QNuL2z$nk6YC@t}jEXr!u6sy4 zCG&?Yr%K1cbPjV!Oy_Vx7;~nmmuKJ>E*rKDZ3|&yS=SWQ3Wf%;@$$m?=MN9|JX*Y^ zSX&)=wQ5V%yJAd2*3lkBW{z}qMvYa#X`f2?{W=_ToFeQ>!hr+Luj2r4+8px!JJkKG zAuU5+bFZa96MwPH<9m>$pl>_}vI4kCgsk93P&#hZ1RErvTr&d@w}4oQj3{NxT9ZVf zCAdJ4?IvNsVcow{32aWRD+ zf%}hV2z%cy@%E0yn}pNem+%K-;KZkd-yZ{~j+XHI%y41vOZeSUc%L;x;Gb|>X~pM! zpL?VC^F3+T%ljXVxxXssVw%P^~MTX zo{jo+C2EW*GpsmSPs(n%XvZh0oNJ=w>R<=XGF+tL?+*Nx^_lWMX1DdfvO1F_!zw)e zK9_>A$%rc4=TczZPxK~-m@UL+Y;r5Mf+h7;;n|Lfj^2@>LPu42eaFhqo~K)TPLvNk zR^AZoPMjXv$w-7}Y3p>=OfbA-)KfEdY`E{DpO;+lvy$OHfp>uyp*xoVPqBVqhTsQ@ z$2$Zb={LQLI#;e6_F1s%G9#LG<)fEhMby<(hc>>m8d9hqsT^Wb8TfW?#>i?!Lb6Ej>Vq&Kng1CnFLkQQGR`*r7KFkELDn{bt&&5cY?> z-%Ps|!oHEu|5)_-?^^qW-6m=MsVJ?3XM}%4IQ$#juYR5HyZEl)WyWmf_nwY^?_F!D z@NXpiJ0^IB@NXpi8N$IMm|NTVkZGByZs=#swlul|fkulBGsLrzLH5xS@|nEjCIXsF zfVixK+L}m&pqM29&0(5-xRQa{CR}loG`TfemymyjfXJi=c6ee$w-7cw_H$wa|0FKB z)00TxAIHTb;xBx6i%2{$XM%Wgu23yq?*jq=F3t9qW23BM%J>H1`$_IrnZbXzj<+;f*}m zQsZmX$+5P<*XSAE((TXe9QIYhBU3s~J1k^%5%Fca=W94YhWnt1K2EzDQjPHY6}$&} z{;)Mo!Kvq^om`GM?)SxWINx~A3-UP<&gV$@{e(lul_QS(ebB?GTsWI zWkQ##2bK{UR!3vGSX06{nQXBF_qR#Y7h_jg%^i|O5ZDwC)@&`I%=$DZg1co6j?CV5 zRV&Mf8yaiN-K}e8%UYK1eTXBZ{ zy&RA&H(GxOrn&b7YBuFQ!TRqvklh)Z|BG4a046=PXVE)}|^~Wl!IyYX=*PSB{q# zyPPG?^wboGyV)6DnQrUv>FGc74zQNkI^9)S**V?1wxqJGI4v!ASj-`9#SYA&lY4xZ zs%jh^hsu92<}l9iFdXs6H8)^zQZY8@n(zU`?W~PZcJiw{PzF$kI!1(N^8DX6_@0``bqbmMpbj^|Eya zy{0Za?^NcN4SV;^H+7VRTDCnN*#Md;1IB-JDdZ(r3d;yFqlnxKN1Cr(X33u9l^CldPFl7x#uObjH#kX-=E;83+>ueIi^ZIV z#uYxGVfXHB%hGLyMdiqfHJF9pMEXFRRw;24`SyILNyZ_LLVB+Qy|e!8N>+2~^=J8b zHKR|aqHBZlRSm6y4l|((!ZC|876GEw5)?Yu{xIDl4M$1u+4xzxSP_8|Fsq#6X%Y$UxSj zgp36ox(th}K8dpf~mY&KKiF3Z`qRU3~y-QJmofUGtfr3u!Q zNSq#z!8KzJ5`IS%*Y8@#;A2&ARiyo_c(rO=Y%qz692fvIlboeur2$Vvo-UT$2|XES za*F(0Iafu3Q6MVLftt3Ub*!{*Au$#e>-Sg-J%PyJIYz%n=ZP`lPwfXSfmi+uCxMqf zjuZYA;iP3(ls3)x-y6LjcoF`Ty#G-X?T-n6N zKJX`F;9rS8=UWO6+ssxjWB?=07}q{IE(s@3OZa_exZr6CzuOEqJ?CBpPlX(nGNw{K z|7Y^~63*vK_3u7rOv3V%5U z{;eqdR1EyEEc3p#wU4>s-z1t)%1nts=KpO^Ie#d|&;z5hos_g@%&{`Yu4 z^r0QH^Rrkpfb^{8F@}xwt_V)(Lt{;e(uYPPp19iF|Nrs|3{9aSd@+o3E->% zmrIrz+Rqg<#J&S4HHiL~j`jE2mw&Y88g0F`D)J4nV0vk!wczGYfF@2H{@me54_k-1 zh#DgTeTZ>UK4^c3%VgV&+EM{;g)O#C$PL~Ixe>q#_e(hCgM^UM;SY)s@U*)PiN$1D$re3vrJU#66%(|GyNb z$zX=u0b7)_X7a#c@Lr-2*NqQuR~7@XY^g+H(0Qy{F%ZNz0Y zFpy+Bl}B7!;c;24SAy1ZV6EK2oT<#Nvyu`t5RC`N(V8-21Xk(876X?gvSP7u2J%6& z5<;^)2%xBhcoG+4mC&Sq;S?A8sx@=t3+_T#AxMs^OR&mD7Dd!)B7)BKHp1syyL|cL z(0(>Z+>iRlr86^2FTLzCt!@3sTertLc&kRz$xD({Dl}e=Saez*Wr*a6+({@PRY_? zdLQaBdH+lL{o?(p$K?G#in*V9Oy2)J-Va<)fp2;+*KW&aWb=R;lMfK5z0AI>Fhi9O zAS`^=CE3K3qy4#ZWs@O05(G=p(Unrf4Jt~L$#PHzsKbe)8Am^kNgSJT9Kf-FgOydX zizp+G?C+5u6*cyADlgZR_Wj6XT2A~z^8bS}MR!mTk!&zf+%N$3)Z z+n?#UBDE!=5ZYCtwQ4_{SvIsh??_h5$cB!Q11tS?69rp67|&HiEI^z=r&jFRhD3O$ba3;6EkPBWaimGF}~968@B ztYqioO3SWClwr8N;@WOLkWLD!R7CA z&L#YL9lqG79TKvWaBw8|b;A3f{hlxB_lx(Z?3DNaDCU03PWk-r@qWxs8)AXC3fYO= zGaIj%WoMjr{6N`B=_rj5Do#pKT4B;=XL-f{Qf7(`-MFtzEk!CA;F}WvDDc;8V^c<)y^WvjRU>%ZkN6TEoDP9ddjl z%VJ7IZKDj`7Ln(oz0cyh^2IeGHx}cwsD${DVN!PQ)pA9$01Gly?g)_%7pm{9VfVm> z&i3gZPx2*cJ;COoU6H>YJBDWIWv#2bD@y}I{^EvUYqxe}PvkJ@g73S=_B~WIy|_4! z>A+d+^pT`}UyQS4@sHXog1%8-Li+v_PWT?-KN0*O;U^V*3Nq?$L4S3)kuqwtxF5EJ z_ABw6dI5iNai;+nwLF9)7R{F+zabX=EAbq8Ka`v4e)_l)ewSru+w`ZJ#Y8{j?D zvX}X#*xDwAM?;_)VR;s7$&O2M9Xn#MK(iI($BsO#_8}>z;-JuEzJa2Jt^l2a<`(oR zG)jD2+HWE|H*Grh@WWdE|EbtkscndyY~RuLWsV23_;NA+MlsG8yVW=aoVJvNpVHyL z*L3BNO$u7XL&#PatbjP+>aRgWkiBqBp-EO;QhvLAVgU`!i*J0j@hm? zhpdIw!vvBd!cWD(DMuyzWDJ~mlkgX! z@IKlX3Qh}VyXkjnYs&jyirx=<#722Zc(UzBG571wNwR%E=6=di`JC?&4q?~2_yT_S zw@}G0B=#XoEg)#Ft&rWi8MiRHRmEv&^K@|%=p#_USC}jrpMaHuPFWsAy;*9pimR+B ztVVr<{n)aED5vYAUooD`Z!lx6I{S0~9ix_q%}%-U*zQ6bXwlrHP20Y1IJ7D}UO#bQ zcy=f>6?S{-CJ+23?~k6&mFu-tX({j3bgkTw;adgi`|!H!Ha7CAVPSgq+Kr7`OUSQX zTzdY-vSxeaI-JG)15cz2?M z0Af_sN`{9I zOwE!(!=z<(znPXF#t!Zho@9H@Ov?$32T9uhS(Ns{LqZo24qpKG zt6w*G$@IO`(eHtmgfAeU_wyJyeE|voCE=iM$O7Bv;0u(%&V87!y3bN8IEM15JZjIX znKPzR$ua{FXV04Xi9R=WOa(tVaF(8d11=q#Lp_FoqU^ZO78xw=B8;v`Imm-)!3vy7 z9xg!?F7x`7LWkkS)dWJh(baz1>qTMb$f3#l`pH8h&i*p(yQqX`)4lOC-|lWJ=`3Hp zZe3E6P7g^*BA~Q0Lpzz2yd8wGUFb_7=mH@>1Cl22>IR&0Qo>K^aOjr{gnl8M`ej1g z&$_r4@tmN14$K`sN5c6W2|q`UN0G6 zV`FY7tYN#6#^AuN9Ty*n7RVGecbJN6R_yc5R?oKg_W44CFS`dfbx$1euP+(5PStsR zp41F<&pYqKDbe~EQ*BLuQg8EGZcXm4PfK%szG-=Td24Wu(-*}j^_0ZtNgdCa7vNLD zIWOb^>i2i8+^@CDa)3%+?sB5zIj%RFQH%nLWg}5@-jcm$ko?&;RSZLVoY8=6A|Lvy!B-a2M=-CQ%)QT2nC zRo$*S-|CWzR&SNJW#{M5eBFBDl7|wk#m!4OE@WV=O&C|UWgit7))8R(^>L}%Y=d!C zU67z3HrirvMQ)@>Jq4pg?tQ^-_ZT=>k)8#MkB#0t^awNQ4XSDy*?2N&*LEJe{dTsT zyj`n_{Hs>8?VE=We{-9leN;nO{laH!#atj}sPb|pm@B{FA;LLVuu(Cl67U4zn4}rj zd&K%k%5^rzWVW=*s9bOu^=52No+qY*ZY%{|5nlf63NofS#V+zjA%xasu7t?0;?Szk zSp>1nI(+!>na9n=59r~HF>k;aazG;3MJ5lTjvF8$G>lisiptu!{$XZGN~2KpR$}u@ z^dPdKc@ZxtMG;ix;-Y9z`Td{(ZmJ|gq_5*bgh&^8!7uVp;-XSq$UwZ5!6m?sFg)#g zqy|w@Ss%gx>C~~WH?L?fD{EiT{PoX!>gzq9x1PAd**VeNJlW~|NW|x9^pGD{fWLMK zxd%^~b_U&xD4a1+34b{TPI)Ndr()pbYY9IY1E;+r;V(qteeA=f;MAJyOutL}Q{Mkl z^nSi4?N7pKf12;7+>rNwKjwbgpA!B(!a<`eK%?yc)yxjv31`(oG*fOjo8)#}M99xO z{HC%8RUj+e13ze%0Z_4!v;ri=((npAuma)E3WPf=5bmr%xU<5tfP>*qme4vxfn~{0 zn9E2|J3Qozat7JiILn%RPDftp*xZv?ToJx1y{rj+Y)f+x0xe?8n>w_-$s1_1<$CSo zJNnDueXO3s|Fkbfwg*>__LS$86cuHv#ha-Gm4&sfB}s`k_xOPo+Wpb_XD(0T@0A$5 z8uR&V6tC}EKVhTgyq0*4){F@$tn!`)@5kZQT%0ds$SGizG>8PA*|XQc2x3?TNy&N` zpiPc{YEP-Fy#M^Av18q{#o3q?`O=C5<8C>!D2;uBG;hrYO_*uEk7H2qSei87Z>Gsl zkS4`*VrlXdv?JB?pEKhwAvsFJzcACVuoES1|Lgm}fBHV~pC~xwwe4QP%OSxdFQS;5 zye#45O$k341Ls^y_zN*`@}Pvjq~Oe->=VytpcQ!IGAle^!ufm&KdHkJE8i!0oN(e3 zPJ8rz{;s_L1^xcBp8rw|oV+ie^CPYIoX>ey-S5G3q%GGa-&2$CDdCh|5`HQM&X}Tv zKTkMhgvKvZ8)tz*6pKN-czqQ}gXER1vtEUwY`JGf!&H z#5=X7-PVaSU)hb~0GfKVc@V4l3VvJc;i~UEtK(DoCWefNL!S~F0-tn35a@?}!?08m z)@F$uqyJ+k-i|%SVZsNLjtNJ{Hl2iy)0JsE0 zIv8AiLzhk(9u1JDY_lzUfQV@eie35S=E4-J$f~;9`fRXu8_{_siyIE393Nqh z+RLXax*N*M8oDc{%i9~Ok{`viu}{h&tV<|{Ba57KIYJh)A4)k8nHr@*RlQ&^R9v~O zvXc!o81YT^-PG$#8I0zdF3b>0V_4WATcG6OELS<^0nWKC5?L@-8DrA~>epyFGAd%<|0#4s2ew$&p|$y6RAGN91D_jrqmJ`HdBxhK9OO z@0zu@Tz&N|Q|neQi$p$|QrA8@++H6ZT(P7N6D9F62z(U6WWJ8>x1Q^B03Rt)e8eq6 zRLMUBLFVBL#j98b2%?$6ic0LeEH*uZF^loCB%eY@&kzy^tSXe1)<&y6)y6BA2%saR zkAE=O)svEtJ7M(gYK@#$?Op=@mXO}2Yh%jUyXGT}+V;W3Gvg zSxE(>##r@?GK8v`@+lSM+5&wU3U;BQYlL_z%xJk;61llttW05Xd!fqVv8J2s!cF2} zY_NGH)1P1o+4NJ$T2vmZx_Soxq=_c|!tsL#JElXWL!qipzoWEuc}x3>tb~Pc*Z7(m zS{i~q&FwusZFSmzTz-9dVAwaZCOaXqxUSFFvbwh_;E%j*y{yGoR@d6y9l6I-+tF6p zOg|qRw?SSeqGmV8av{6n#ID~}IVWTCRj||mCo@(7dHVOv0CF`6W+fNhv32D~U3EtX7N!HxCW9s10us3Z1u z$4g;*3+psy%&wvBkN(=J|UDl&%2@~$o@yWXI zjxmpScuQx`rm!n%@6q|l#Z_(1jU_2l!&5zdeaw3TL5*00^+)ikpS4HW_Dysb*X6Wo z6JSh%Ip8MutmMc@re48$6_7Zx*I5Mu5!_Cg6I~(pYeoP+LQ> z+8=8^G`Z{WVE4hY&}4A=ZqJIMq59^!k-bO81D(Oq(y4})HDjT^)P(FgXS>hQ;9pi= z=`2obpADrutK2S(%e%N%`vT^!%ChDJUL-coq6o*}LL(1O)uPK&E9XZrF}-)W7>KdK zTv^G1#x$H3uG_H}WMl1@w-Kv+*m}P$+1;b<|3XuBee+VEH?3)R{nFrQeSTVLi|^VM zpR~TQ?H9dksfPay26?C2YB{$wK7s#{#(m3*O7_A~J>vyb|` zsA(|fC9bnp9BY`%*`=|;nWO5I>l2c2W~&2w;A9iI!Z4=k$C}ph&8dkwBSy~;u*`;r zu3cNtykkv_HvGU|4&a+CtkGhVss?uaO+%7Ph68C@a9;+VT~VnRHy>&|QW4@E<>Fhu ztxM1u)_4krKb!)- zE4O#&(o6kaRjAP5Rpn$Dh>2ld`Xy%SF%}t;dgRJ9u1Mm(HLS%$yS{uZJc5B_b!TJy zzXvz0Z*9%~T3;{zZK+DFnZD#H>(O~n^O@K1&)qlO^c&!ma+CVL2iN76j}jrV{o(%y zzLa?#vOu_!y@Capos#u16uN9f$ijpqwVWT!A+F$AmBxEkVsKt}l2G_2=c=`}6Q)kt z+Uh`VqASL~ifo(@_%Cp|Z#{@{ri!^q!;el;l%uzhX>Q`=P@K*-%OOMToLeJ5aV7L| zXI6tJxnb&(uR*r>+W+qO@xP+S58k5&^89O%=Pjg>GPP@(P9w^FFlfYFm`?YBD}5o^ zQ5sB9REi;xN_2WC#T_y>%jM+7B5;)^mS~H-1ckiNPNJow6!%C`MCHBEX4nawp=DPxX7=Em80LxG zYmbEzVp}IcI#{G9BJ2oDTdjlLgTr?%tg)`xIzJC-QyJNaX~J7tEkW=@CiGP{I07D@ zLvaK%t8^;$nTn$rvtAN(Z}>p8xb$W;nJ{PR0LdPyd~2Hj%CJSUR+9JGE9rtCy6Eyl z7ilTe?HRV5nEiz2wH5uDpfB<(t-8N&V^=ubb)%;ic%{woI>z9#46z)p zO4Nbk&7Jsh@ovUC(G$R0x>(AEKjF_@>?zv>(0nKn@0O_S0m>MoC1})K(E27@m$f*Y zed{`wpYOOJHQ)=hg?dJXy^vwg@`q=7x;78h42RL>b4f>U%Rr89EfC+Ni2!K84j&6U zR1`*itU};BKZ@@-AIm_m?C8Sv?&J)KUT(e$niu;6uve+FMB(#C%f{&+c(upsYIPIT z72epT*(3ka)=l%Xq*!}6rD$ciYi`)%8QIp`w{^H?M|aOGEzVhAXCdjg0RNoAyJuKN z$&j&Y$W`3JpkK2HVMIcvfmq2yRC9$bG$9;Y6)!;ss0Bo-J@M&1q|o_!tOvz!4gg;o zely!LLWe0fKNaVAs;vl&mrye=98Z=OAw(n%az?S-RnWpuH3WkV3#U&n{J6ibuU~74 z{M(r`qK^gr;SOX13n8|=mfPv1>G^K%Qf1`3;}o0nc8#|%qe2j*xnHUPnBB<&poiO^ zQEnGls)f)a$|MibF_F_PyD*Bzz{1Ut`|pvbu#;TWKelV_JzGXR1H+Sk&+^db?K{rD z_3EQX!z-$*-0qB;(H;GpZ?2vwn+o+0FM0XkB^TX%EJ>qaFAw zWwD^64(k^Y*K#njp`A63IGat8jS&Ql^QhF68Ur)TJSB9@0RhG{|0k1llg9PRF2IC{}@PaL~e%ZR)-GKhv`9~&Iee35srJ@$kg6Ycfq zL7#rhCz+Fu^#Rq^Hpbk<`5JMii;)RsIVj}|=RpQCDnN8BHASkZX&@VBJblgzOE->X zI5yzejpI@r$;cL}bte4aUf7G!=b8F=)V=cY*L=~LD$%&yQ zJEnV!)_Tu}9&8z3J zJ}?PFLDrMKYrPNj#YO5hmhW>uP!28-MzfWIn&+yhgjdsHkeDGP^ja5MzN~OzyrEP$ z0(bz|vz!FrbsVBuO&Avw{6KGM4j23RfjiVR;evk^w-I^;KX8Mb*Kwf|ytv?=HPJ6J z+k0C4S+A_pyS?h^U2jyjaLT+$gJ~Cx9$Zydx9Z?1s%Yx!CJ&9ST)%$h%GuefpJ(W0 zGmRUsTgS4h!=hwH{PRzT4+~r(+azkHlRz>$Zdt>c1QH|faxRY|UY=?hqqvZn0^Kr+ zt5GmwjfxRtixy;1T*FqISa1kgqJ8|WkL>QZ_3ys+EiNq7rhoOT$QSWrYL0&<-lHM< zUUJS^`)OmA##)YBZSYUhE$6e=0G(CDoU>sBL-IVQN9SBku`%a{dXievSjr-kA;wY` z8Y--|fawIS&Pt|a99`NkBjxSdha)$%^GYo;;k&DK8^9szSJYUT153jD>Yq_xfWL$C zfn#9W>crpDsoSk;96FBU>dMWuDl-il2FuW$T+3!-*sd+aK$y#c7p;5qC)%$gU;mf? z&^8?T)tg5y{N-;i#5_@j%gftM~;HJX!8KVbQ*4C~( zFq&~qCf2|3awPK_Yo@!Wv81H2$DMggRWGl4t1_L9YmSU9{qUN`o*w;b%(}Uxc?d21 zhnh>G7m;bqK_2En(s+lUizWE{9_XUfa*+ST=F{WoBCb$Y*+vG%8y+6$5M(BOItzTd zbb*#EpoMNGyR6i(fg_Ua6;{Mba>ekOa))`Nt-tZ6V+Ed2)iqTiPr@dD@+k-FdZ30&l)~!^iL_!3!7za$2B$P&vjZ@;+Q=Ze>C|$@cJD@;<@`1swcN z_-`d##99TsMZ!-@xQO)$xU3<27+^jhF&wQyz#-X?Z+>_Y!5#Eh`#InPTvu2_2Z#3Qi>BY6;Il_M=oDz`l^QL^hu!+U}0}}q9Cb)RMgx~!a6WnzFr~gC3h5aDl z!}2+!5>9!0qxfCuPQ3p=FXD{%e+TfL?8R!SStj6-e~ZGO6L8{EIET|3&7b1utCa5OFpwYF9boY#ZK>7MiZx|a3y zb+@kW2~&rGR^k6MPE-V?|FIKDhRT>bSP-M8Sm)VTfvd)5jNVAz$h;-3l_C{?7uHIQ z8FCVTrS%bOlZeG-Oghj!lQtu9#Go-19=G5?0!)~VF4b%gMP|k#+;OdPWPDG5|MW5} zA4@AKD=WGAsH3#hanzbN+OsV?A*ru*Xw9b5;WB**@D0Y-wim?a8e7_@x+{E~l zMdiDt^rjdR`xTnw8|R*6hB)LtMNXV$?!gI}CfzvpFEZRX6=b0CkC?TQ-qVt!bB(bT z7#>`?rWNnMh?}(pR`*nm_l(bI*uJ5yt!v-zndXkx`i4NpWrwupz*J{td2qzvyCyZk zwslo-O^@q>*5=l^qZeyyTKv_%#zx;27$MXl<;DDqyyqFcW*p{S?FaBj+?LfeBx36><63hWOrqyQlc0Ur%alcqGH*k2 zOc(eReW>8!hBfTyi_DYSYz^ulTSFH%xE9s#;}(!-Fg~44R4!>j|hg0jde8mVh8e zN-VI|Bi=#GXCu89Zk8U#p;*5W?g&4~e$A{1z^>}zZu&iJE65rN2E{0X^csqa=v~!- zxy4QU;pXRTFu@I;}R;S>UNDXbyFF1NnpF z3rm;e2Rt32rh*zzL1!rJ2^I`5T^JuO2zbKthkYrDskK|@ou2DneDOMubAD@WYGR7- z@VwK1%WJRQ;&%e~DZqUW;@vK;Z7+i~V|z87w-oLT84?#SiHm4CiCJ@hEAwVmLNFOC zt(gc;Qo&8;ak*o#T$Pj}@R|WkRsdHeaL$>Ulzq(<&VmjaS%|a%Edo_;uf&o&v|#sE z7wx^~;P53Q2d>$dQJUwmUwqS%kxPa@{E>@J`%)6pYM*{i`()(2=bkPoNlZxbo&G8Q zcN#CfH7L!H3G}Ma@e}W@NBd=oDo2z9FGDhB4(aMR z0FU5c3%_iUx@L5OjoerQj$I~kWo~09DHc1y-)Zm>2)N*r82~(ictfU0=BOd5s0u2rF(SPv#@O#1 zMZ2nUyAwnIMTOg6`vh}Sc(9f;E-akOp@ zX$wkLIR%kAqw~xxWd`^fp}%C-ejrjDVzO`i^7Mq>Pkl)KiG<~Ntv6B82(iZ7gZdT)D8 zA9oGpR-OfQ3vBpf&O57d8*(cy5pATI)wm6-ko<~LY9WDgK*d%urc^-X+D7(OJWJ|~ zFH9_4e_3{Ufv;%cmZK93XrXxCb5A*n6BCmCumAL?eBaE)zlykYA^X{qO}PDu&SP=Y zV$6biJtc_+sS&BN;)p1xtuB-^st-lO0-E4LRb&pO<=O<45JW97$fo^kX$EN?S8m+jOFhaiyK?;QeL0t91?RWK6*|cY9 z9CH~@KJ*4x`sd=^@PaT2@Ayc<^|xM@P<_!w)d`nb)82Xu=-D&$%rirgk1775%t*If z!5or|m+82PBdfUgj0u>Lzl`@}d{Eko2tP~yN(FzV>im@o{^AEofkLXlK!8kvnj2sE z-U9v)?s)Ocd#}C<`EYjbmU1Kk@11NJrN>pNuMq#H@7!%UNnfhR z(kqzWt;j{h>4Z>J!oi3NDIPEkfw-KS>S1T(TVgOuN-*ctm}qtiIiuk$_eG(PCac{t zKnHqgUT*VD8C4+6WIPA=$qwUXIMa+^fR2f3nPB5Svigo0%wN+?irqZG71hHGkZa=! zp5-%BQ#0O<{Dg&s{0=WaEcYZVjCh<*PlMO1b>g1}dK#%|q;YYgG&$7s;L8G8d#xg*Y2p>__z@OVBWDRsg;#smL&K!wj5(6h(PQ~G}!h#W8tQmlg zsMTmN^FIV6e-+sR7M{J(n#S4+IBfX+A@%!C%f+nwk==`RnV}@EF()cAY5)wkQV9{` zg`{`M3@L?ChN>#(tI6i(ymE?{M8?eHH6&zwgF7>7me?>jCfYm9Ljbzl5;vT7%u!Ggd^09YSQbTdtxAv^%`A2Wp_ z7%c5w$Zc(JV!=?`=dkFcsdZuDmlZW%{l=I5o{Ey^P_OW_{pT8AxD)@KS*ufk_7E$eNQbaVZH;)BTFZUhT`|@Niu-A01snK^^T(CC z5xQB(Q#YMNx5*!maS}lb(wlbUO7G&1yD@2e9qc42WSk?Kj+X66RSgww6M43*sf^uQwWX1#J3_K&apdX!@&ZeL?A4{& zt!;IzDF9#ERsbK_v{h0ddd+@ATzukx%BbugLywodkbM@zN~X2`_{>2uX$IR$))jMh zq3wFSW5Dvr2@5xZcWVbfR_^*omogOTTyydL%P+fnddQl_AIYI z;C3daCF+y-KGRI`0Q(-ch;=n^$F<`8=<;Y!U&-B6c7cX zRc8@^VqwThsmz8EGN_8eP?ud#pSfK|eE7~QHSK2aKvPLc(||Yfhd)$#y;UEAar1Cd z!(d(Al7^x=r_<|neun&tUX+LnPvZT#00uqp*r;Dl0TrP;`u=fJ#LSo6MIl+tRu)KH z`tVuse&o&y$aSeExs0gF)jmuBV24ePKyDIqj@bWR^pJ95EJ?XzLGNI5iK}OIYhxhL z2zNfCqH{9v+K!XW!HV`}4S}gH=LJpm&icj%e{~&Fz_ih5!#;y?G1gzkj;;u|ioI>* zs`5-Q3nQIImzMXFJO-T`Zl2OnhU8%}Av__l4eChb%sDWXj;IMPm;T7X!^J<>ywT?$ zU%1LM&{QgBF7h^K?gH(y1z5{xPoDM#A#ky80LGc3#<`W=Y_V$V&v2qq(->#98-w|O z@$TabH-X|x;p%&Iii>||L2qHu8+{8qWH{EVaQT%iwP8)5k#~v{ga*A)ktQizXMoD2 zoJa*!$_Y{#D+X1X39A47Y0QPqN1O|Zd8DwWQczf1V=&N!Dmg)6TJQT&7<#TE)(4*i zLC*_mZ^f?NP}Yn=I)+a!-&fi+tP-R#Tdp>2v5<0(1AryOi15+L%Mc64C>RRGymHw` zGV??z|2N`!decac>dyw&`r_9+J6c+^wwKPY?cLbtbcQ$ePH%B-%WiM%?)bXao)8@L zR);zVJ)_&hT!Yg;5cGP&-QZrxgmLI_8^%_EAA=Uhwizm=L!;N&(=)D$H_rMoCQWgS znpS8ABN+oR$#FNq5DMc^5%UDNv;3gzbIvMlK)FUq?!l7CwYYNQM@kXV4=FZ*Xf2$- zIDPBF!nVwdwja3M*}JYI(AE~fk#W)PSK$Glf9SR=`mx8``Mo_`dwRt`a{QCUmrz2=2)>kz#>$ttVSPX`oh+k6 zkP(FK!h>ankP8E3s;ipTj7>erLU27Tdr$C>vYKj?RACAT@9_VP-M&yDfDgYo4-10ctp z6V(6=@)(jo&SU(*0ACq&B=_{h6EFf9rncb+xaL4+z-2LK+?d?PfG#Pl?i-XE55_&EQA9rVi2NELDUbq1dmQ(0_RYxLc0TUG&gE@ z4`@7z)-ss)?8RNyKN+|wh~kD-iAhnLM)9xl3x-Z)kd#ZfS;3u!FXbR2Nc);W zQ@OaCWlyZzL1t3~PZ?Z8W)q!$L=4rQy)3`7FjM>Kp35THmuY|gacYhu`>?%d=E%w) zpLsm;$Gh&*(zR2E$M#Nj+fkrlwdCQQe}s2tIP-dj!Sj@_$e*`H-&xs_#yiJFRLv!U z3jny?Yk3$>Fi>B#V+bICilO1rduCBSQ~*5t7ZDc0NVPl^{{j`o`vTS&`c6PVcE@k&=3 zBZ_NWqI_uMju0J=kjVw~5#Tccnh9u@fDnc$1rmG)OGvP3fEs5GPQ-p3&^TPJ9FWDK zCvow*giS54j>r{z`bc#y7hjV9?7$%x>`+Zn4(fNY`IrnL z1#nlRz%X*Q6n?-ia$3yV(qGdPxo2lP7<-nf+`gGCcd?@?YiVCsZCAn8?Z>unE9k84 z>Ko5;If|>Z*7hy)v^p*Tc&8)iS$6o*^3uelr1FY;4}1JCX%&%QyyW*BzPF-0DJikE z{L#Zz&96pY)oNdDM!~Sgh?4Ecn6GN)bt~Z*unV^C4=VaHW+QF_m54F!h;!LLV3Q82 z+Z5H@oK+>Plw(jsU&xY$e6Ah$$eK|aB;|}vNF;h~Bv<^GAGAr-IZ*owoaM5T{>j|x zB8NNs`DlUUyHntFXwO`>ER>-f?XE!?%A@bC*{nCM8#tYPpf0W7!ao-nIsuI7o7Y5#|pUwrMV-inU863@td#@@9xwatyG4Rsf;Pw~}NJ0}C7^Vf|oOHQ)d zy`7%s36cLKPoq{6@9_TlPh-5w@G$^psU#u*@1Ot7O%9^93Fa)sn&qj5Oxy5Zj>vz} zQ0bQ2uGSV%x_s!it0LTAPP`X7HF~UHA(z{J0LzB+L->dBzU7uD-#52uyk49t`0wO4 zPeQ-Ejzd|lD$&Uz+C@NB#d)bnfu1A%Ni`$WpCpE})1N}ceV{;?I=26iqrJAXZziX* zu-Kiuxo_MPDE{z%wqX`r{ywzz2B6S zrQp5GWs`KyX)f}heD4p+gBq`C_+08W&(4F&el0Q&N<8bT9MU412hE+#E6q>UUOo0o zR#87{_x06v@=iK%5KJoXMnsqfN zs98rG<^6MR#@H0S8WFab3=xC}w;Q101~$SmWi2}v`85#Bw}y$N_$FEtkI#G`?C%1d z4FQMGumX*Gb-Z6TKA(s8%eAbd-jmjeF7MQ@8}FyA5(8kY6s9sJQCK0=84HDV10l`; zGyDKs5DGAi&KFs>I_-)J3;uM|p$=Qi)~nx3FG1XO=5WxO_g~js_s262msUGi|Ha(U zYqkmNY4ADg=5JsPie5L*@*lQJSWs&l`{Gqi7Ioqy&2;hJk} zhWBHihn4%6c&@eiYI}WlyRWzQl&_B$eLi2Habj;cyl=8`{rbkqec|xliN^KMYL^t% zb-DUnU3Ep#i^#RWQQhLRwkhj>0!PfB9U_jR`7;cMYYX_=B8Mh6vSNx+d42G1DL>1V zC&!j88dPu&be;^iaD!5|DyIv<#;Qn-;S&>!YzmF7eK51{U-M67h$YMBB-v$^!w1=* z_TWgxHI*X=C+l!ITzQSvxq4=1>glOWwpjUAb8Si3fQ^|bcThIM`GHi&7%lDPbTr%$Su#{P;y<@_?#v&K zX$8mjVppj%kCW{pe*_<)yPI_xem~!GUVJ_&&bEp(r3~52Z=+{|;2?&DqzJZ-${Emv1F+6s>_dE+c@Ajc5E>{Ub`+WOgHV1)Ung}@2_9e(F9cK@3OcnAsz3MZP^{4JNeslrR2RKgw8#A zy7R+H%+@DSSdnrfZMXjQY4M82Osd($R6-Y7tS{mnl^^~$;{V3GnEh}W|0lD=`r&$? zS-H~_#5r8d=)O#DQ*rp$AJnS98+F6Kq4m%We+;cwBY&;zTGbMm?4lpu=tDms%1qcs zwn4nJjE!IVw`1dR^R9yB#&C>KIOGY%huN1d5~Rs3VPw3^3UX!JUyKNN)*G9c#seqF zJ#{QTu%Nvyc9%GNP}WgWjO-)3Q;loY*|G4R_$o^_Dgio0gD3Lhpn?NJ0t$8we!{36Ky%fP9k> zAcO?z|C!x=_pZo>&pnT?SkUI0dD^Nd ziBsr;?+483*cgH?vt03q8Odk7Mqi~ zY;8_mw~NgQNC!BHenyh@38O2Zk7II zXUN}Q3l%5LpE93c1v$QzdNt5CO>-V=UnoG12XS#NS<=ps9L?CY#;)MoW8`^q$p=!X zkv0X=rzO%@NcjCvY%b9S%G;fke`-saQgG&_`pC}(HP%UAms7?NzXg#gKDdP1m5$2; z(51&xsMHt7g$^8cHB>kHc87}?d@>Ex!{5WFRuCR<}=p7laTNY+c4TsggZWMr3La-Jwh#f`@q3FVYmaX#h7(f~&j@lJP{|2H;E5zeUGrq*(O;S#C+eC=Q3yu(Xvi zv1P6r&jBc+&4iDll_n3B2uEiLfdYj3P8EN!djYa$xjOUmo=-FXSr{&L7IInw9z zOo^APcPqTed}=>e`9~-p?JtMzO$%tlYy5-8qKbBD&bJ}^{Z7&uJN?X9N-3k%FT;`( zG=C2mz9jWnI3%qHW-m$YC$rEclV?SA6q`&bMNPUta2 z#K_4m1gBR=VZ>z92}FU`7}j2#mJ+NH8=}Rr#3J1ZRV01F@ z$I1hE$&?r(oRfi01eUTCct#yJV8&dN(-Zmg2l@s8i5R6&zhRWdh4>5MASQ_zQfgEHB}78gKsZo{ zkZ&hj55F5&SDEtT36#y+mB^l2_;|x2=Lx-uPF*@?kEkv=u|rYY%IJdK3k^MU! zv2EK_yA+yoIVa&t_R7Mjp{)y>H19Cs*Wg-K1D5Jeu7_X)1Rs#kvOzbHCE#;JbaM_$ z1l^Fr%FLIthswWB=SzPeId3RGpX)yH`<_4VE9XkTa5}Dfi&b(=t)JS zD%K{cZgT_p92xsev3JIP=BOc$pkPlS@Gq7Nr=Va@Aw7Q*{!arQij7BTTS{5R1@)ta zhY&M@VwyjS{dOfSt9{zdF|^@Rp~6EU_7)3GJQM;C^kCZG+8`GyHK{T&P~jDm=o^U> zQnyjCIdza_9_YLfLpq;gzyc%wuA#v@iZnNnPe{dz{I)b#X;VXOoFg$MGiG{Y8~j1L z@>f)lr^x8&fr>)qNfnlqIkvg5^s>>>SBX~0{&6dCl&5?^ozl9)#F5s~SU3vteF)hJ z^j||+c9A)lLGolva|l~C&_}l5Pw%+o0Cn`>FVT`~6KQ2~q*b+AeksEYWjHwnmB;|( zsfA=e36HWGc$789Wh5u(#LaGOD|S0TKd_=yE@c^M0WM|01H5r~HpYB0onTWCL@Oaf zL<`Io6VTk=Qs`U6Lo<|3(@G?1u+h6Dvxgf3{Vgzq(axZf6bixbRO~JPkyEHg4*6Qr z$N^Q5U=7M3IrYStnBUTJ;LxEHdDrEv85&yi##A15Kn3}%2{-71P8_^|wrtH42U9_q zR-^&VD=Z4hr$LkNH)yO=NT1(qGcFf=C{6LlHXlJuy~Q=Oj&+``}s%~ejt6jH&d2wk&3 zpOTTdoz>Oz4?KC`9O`Cear@)}UM7uhsr|mredQIO#k5@C;{;h+2V<`Z;h+M}v(R(< zNw5&I0JZFQXivhl2t;%znj7Ibh(A3yMgt}E;F}ZBNRoFnkEgY}=H*XPqzbeJjetkn zE3cSq#~8VGglt5z1OnWs{j(2wG}eg8U|CHnE{M5*mI6jYl3~5CmqSR@TJMd6Z5Di==C%YxVm@ePG`2}vWs?p}E8a)UK*jy`)6!tELV#j(++R<_8=$8t zshZ~I2W~&WZVyYDk?ktQI*x3?^Jq_x7-c?Knc1!o zdD$8>Sbu8R6e4w;TMRkcwkFi(nTs)mAW(}jsSEZgSacjo3I~H0{dUc1&RM5UU%bUJ z5b3U{udA!Cu8rH2vaoI}Jn_;YQmk|}BT#Qg!-j_Tn!=ifw&*WO_)`Es`O`)l*fk0E zQx1{g{ON-vG@6i=7K3~x6$7RejMtT<;nONgKU)xn^t9D_MbyoIfV$(2&7`9VoFhXf zk&wY=O^GTbA35BEVu3!I@)QAtI7UfJO}V*;X^$_SSaLDD@}g(p(5L`YeoQPR+6qsH@aok zcnA@dbVpcj!v2(HlN3Z2taPY|X;zV1`1GS^b-V1Y`FkJ3=zR0G+uoEXiCC~C8l%J` z0_{6KzaOkctY#W|^h)5!Ls?M?9W+CoLwD*HnqKIjIS1rXACM%K7Cyaaj}Ov4dk(Bw zV?w)TSmG`jbW;x8B@!nOX!QszgcG`3MEZ6hb z5I)4!Z{Y?ys6|^k(G~~kuxV4)KkjhIk|~5yT4R8Ije|L)AQh*4Oyj^f)TmT4qf+=# zJ4idGhKWPB2(FL8o~P8V=llGePfr>1^FEb6aDX3;_!r>MG|nSq?!YtKLYBoLft3B1 z#T0*%+|aQVuHa9Q@GHFz$VChNMBNl_G#u+HvjG2|`Z7(HUFeKS2kEDH3OhXV&l_;EEbD6TMsDXq|? zqX5XEP92%#L@IC@=aXa*y20 zCEJwL!av^vuku^8SNUfjW7`QZwV>jc_`9%6gV7e^GX5HX&7gi^q~o%St0y+yp_Gs|d2&~5ojT?z31LL!4=436uKi?m{kYtTQL&_S&%UUTEMHUD$3-f)JArJ?e zEWVXXS3H=63ihc?dP(dx!+ViEe;Rr=okj}qNt;KBv~v{4Q%NMT7Us&QDN$}9H7+Tu zPASewIv|(iVF{f$;HGu%oK5Vgd8Q3tP6F|b6;0bVP3&r&L%1WI1aQE^8TwPxw0s7e zpl7+yME%VOXRu|#zSCjIG5J_YW3N9QOZQ4S7U|IO65lF7@Az7zY|T4Lzd)YWK#crp ziCW2&U?o#xu4GEktMouCbV3bj<4aHlIc1Usj3pPB!kw2!u@usBaWr1OZ#RhvG8`ij znSG;vb#t+8NojF^WAy&5vre6nJ@NI~Ev{MVozCF}o|!M##bCA2P+ieGIIyQ;?uPn` z*rv|1yoFUB_EblGL}Y!tZ2OpOdjT4a)jId-D@g5Ddg8Vh5M30IzJfL}NhB`74>(#< zfR>Q9g@S9E*eF%2+fpE)RAFNniL>Q{KQ<@`r-==U`vxK@gp4 zk-5mzLio7k{Dl1*L-0{p6UiRe2$A$qI{6EJFX*8L;Y|*aAHyu8S$1kZz;m4#C;p}$ z44j$vUDTcyX)Q=owI3otO}grBrMD#RU5ZmpYVjpGAX6|uM6Hz75xY>QqtL!rlg39u zDa}^;^aqlNOA53tp=Ua4Amf61=@V$j!6?Nk4w?ew25l<8w|e_2j+Xq^?cLLy&8r(D zk|LtQV(gx#Qy17{!i&o`uV1%$&{_--h)VmJP~{|!B|4PC-Cf(5VN4rQSgMZiyw3k4|joV9kEk z5>icVL3R%L&_I!yGE3br-MFrr_y|_Y_(x~aZ<9V^C$n|1>anybRFCQ1rfeO^l1SM) z#LMMsDj8}*V{71iZL_+`( z7=K!e!!Y#eftJz)krhXr7>&R21La;6oaifZ8z2R&&-S6Pz)tIFf|n*PZTK*^CpF`oWz`1>_OOihicj}#u5C*0^t4@& zGJ#`Z$DfOinQS|7Hh=)Fg4TGhJnOg^8R6nN3?wU`&MsE@&jjQX4XgYYC?D7*TlP`d ztmTlModJeITL-4xqM4J`ch+!IoC-(1pkIi3NPkQ%1&5OOponhd{f9RD0wk$JbEGES zv~yr=+ZDIma>b^7$)WiTB;j1t{RJ4y?&;vZT?Et zMzUdeoW?1?6B%~#jPYaHc9l>5Eh_&7%10Z^C%1|BK%b2)oz~4w#&o|nhPcp`(=3HE zDw@}5s?gJxm0|6o>o5G#5~K#AbrDR|Bu=Dl!!cyMjz7>iIlW*BLdPr&BXKt6s>2#0 zWriLGW`Ht(^9OT)lBu6^@}giZO0LS3o#Mn0DLRgGEzvoZ@+%jlFZC65Se^WeB&98YG{hW|eL;S8SYjE9Z{+>99i=Zt5Nb8s?(Irr$K9v{fA~QqP0d%Gq*+NiU z24&Vy+ce|?noKxTwh+0@&znveLp+dBNUFkme9?O{cqx`$z(Xg;bG1jI3oTT@=ffLM2SauOz=fOUo{OW2*0*!;gmD`W5 z^)8u3wHA@m>eh;(s>UsjXORR%;}*ViXse1gN^h;OsYMv!iru1WoRz7;q%v9eIzduvT7ApU_ zfc!D=JeB`bzx>_u`}}&VI-vSg7WnXQ*3Voa_Nocbp9p{M1pE#mbxg|q=DX&QX`jmmo(hMgo>@XkPTWm zmy{Q|$j2|(_iu1NDsdUtV&F^q<6DrSu`nGk!)UXMy^DtrxM-re#tq;|th4P*a0!9*!a7b;?vztG0Pi zl$T>tUJfv-Ke22ily8s{eqp(bqJzyMq$`GWE(xJE9YZ{^kbF!|r7U&`&nJ zZ}Y2Po%kgn;^fCF-j%$48bu2K9NP$saPmotJGpHHp@5XuYLX{&5z7#Q7C-7d9x{TP z21@XmMEH5(OlcCK-C!TT-?flpeJpfM{NxeSA(l^&x6wDXU##W$V)@7J_W2&k^>$ z*7VG^UE8}zY_E5h`<{)h%j)53n$DbZC+CV~$7-5bWff1uW2{fA^&O(X@ z8Zndt*$`ir{l*i(5vpv|ck+#B#khDVJ#wzc(O3wiCMf+K7TU?=j@QZoa8OqD4NKoL5;- z5GB#!F;0ola117LawTH})pno0nO5$WMa8m%EdHP$nN0lcApc%zWo7AY3N;pe5V}mS zG^tMzyMU`$crVa|(->k!YyZk`fS*hT4fPf*d4$zNIdvhB%XWrV%$#pY{dE zLZB4!gb^pwiYIK^8#MY>1tCMiU-nCp?0p*Kcx7)e4Gh37Z7>MzX&~o(NzP=0(I~rf zO6vp51_Od^N}8~YFl3-6sM+kSuXE?(%F4w%J3DvMF?c&W7p;$}aO70xI4Uwy3X|3c zJc)JP3)YK}^YMl@pJt1Ui;0PfiH_r^ZD#f6L)a-jn@$PBO(tNMwisrVDib$Z{|Rn# zX-h?tG`T3H%lUz@pOdowY_MI z;(f1y#-oiB+hJHF?XRZ^T7a7)HP{YAj!&)DqG7p==RmPF@g8~pQ_vyl{7-N!62|~X zp0Sm5TY>V6-W##tupGSf(9#+uX{(qA;!^(n4%!%%h;;z5>S#Q55;#GnO)rRY)KDQN zBFRcAiuiY-DB4pp-EnYY@*s~4%a~QVf7kftMx`r?ih{BT9@H39kCV@G+c{?uKE06p z5wig8z!^M7P&-0Ak*#rJ+Fj6?AhiKSTA`F_cL8w{tUgTpr=7sqf)(vNK%RxC?Jqt( zzGKGx)vM>vSdh3qZ)3-Pd(z!Eobu)Mt|d!n)-?5ZMl(_!?IGci?>_YW)7Yx)luAwb0*^}? zMEznT>W`;We@^|J${;?HQ)CDXSnz&GKgoYeA0`l{AU94pZ4YdJRqO+W$ODEbUsGC z-+7CSxBxw)d_-;iTs(vjlfR;cL{6nZ2o*U=^z=VMfIQkLND+ddNHd3jUViyszIv0m z{_tNfzx@2Wdf7K+l%Z34?;`r9m|#Z%qqNC5fsKRJS0U@Q&ykg;vT>mPBvB_(iaLU# zCiyHJAnX**zEK=G{JU4L`>MF=@UO0g$P+X1A=}JWeDcY}xBoWr5&BEUH;`-nAIN@u zCKWfdr2%bu2yJnYw`s7AL&%(_V>& z#6lq1ZN`{?IE2Cd0r3zhTm!)kI#~m_)0J(5imqyE=t5Agjg>99Epv7vwP@}48&8UL zXq_N=nEG@EmZ^xycHFXUpiN_nJxBK>Kih_CvS&z5A=3sO+2N(I#PW>ZvEhRodTUD% z+QA;h&pkWh%snr@I+}3RH6mU{ctHO^2O!#H|BxP;^vp+aNBlWlE=4;@R^3C<7EHSa z8rBZq3>zYeS)wUv4{bhU1rp-$EGL9Ot97{|LmO$kJ~Nd+IS=#lCFrxeYCNP=8P$A8m#UM*(|W=&|Fp1 zaG^yd-6;kLRtQQ$hD8G_I8Grzj+-Gsy0E!~J_y#26@kzWO1TjFGLj3wi5tGlocxHr zDD`L+686l=MRM(wOEFwhfIO1TL5sQOUQfvK*?+1g-6zc$GzMV-uBU1c;%sD?gs2wB zDhOE;6vD}P7qrbIXK-mk7qHXw`<{fPb1GnEBje@t6F{~E?tPhxZRS%@z(@eHy1 z8?o7Q)}Av|TR?9gN`3(QEjxynzs*yK4kbkMZSh~y5}8wBm{ehNt@e(hQ8t^H1xA^2 zMi0jm1@DxmGyYz>h3cTa#KJce_?=bfovEUvXu!`t@c2`*A;2N&(fR>Wv7kXeV42}#VN_=qHUkq$x$T~0ie;0Zzu zOgwN1uJRbB{rw=Lhty)okg55wh7e^Fl^Ipd#U0&A<8iL8wT-Pimbk-~)m994CLBGp zYvMA)0Nj4e#VH#A%=nx6w>? zAav0BY}v#DKws;_izWpNFaFZ2X$PV<_bAvaF)!GG*ep8`A_mhAqz+W3>_Dn8tJKNr zXli#!k|8^g8KxabNhwaI@v{S|hFJ*3lp-o>&3_;Qy=D~5P;bQ?YQUGdj~A9iAMqU{(Ii6`u6W!E=U&&S{(LXvl7w_;RiHjE2UB#>$-RjGEIf zUBCRCHPsxO9Bl1^gYJcO+2XXjE<1GSvUP{2Mc6-TT3tAM^pu(1-L!83egW$$ei`^h zoU#Xrm$8CP8A8t}7S=+9A#34c&N(!7(l)g;?M-$dVwyVmQGEE;1>2i#)91!Dd*&^8 z1k0swzw@V$YAd&YhK&?>(6M%`Q=i1#mtnLMsUljDL~M+&4?`wAUAdzf_Dz^9(L=P} zNJa{L$J_-6b_0X3Ji55Md-0O)?j`x@>G{r#4A$Omm$}6f~(V16Mm>Ja`TauTZTbNtKD^lWZ ziRqQzoc#K#!iq3kcxAC?dQ5U=yvLE7nJQ9}=Te_Sc153dfrjX`2WW_}8q?9ZX=!TF zc8Kh1(KeBuP3wgyI#sO19qk#=78Hqhswko{9Lxyvf{nT;w0oZT&b8az?p5I%`q{iW z3tsIBf9N}!CQ>DaG4GrE z_9P~lk^#CZ(s&ZQ^f>C! z`a8mtng+v&xgLzL#T1riR23vISumqLsWrAJFDExQ%aLoJAO8s-8qO|E$gRwtzF=C8 zCoL~8!{*3JjEH)H`h?m)AMH;C1`tV(4oX%rlmgFr2w7NLeL{K}8eIukjHs9nw2!Y^ zhEqXh%QO#a<_#VgeLJK=Nirin1j#?<{&|Zt$~~E9&R-K17FAl}Zd=;bl$)8Am&@9h zwwH8FuTHF8UdvMpo9mk9x7S5v;$K82jEjMB(3Kl)h%j)@93uJ1jU|(B>(#Z@hZf|p zsX))c2Nr#3!;&CNp23@+Dps5~zK~^~iDE0-FY2E7HMj}!~WQJ7skr>&!_D5tPu(cH|etW5gL%Eo_L=>D0mf~rVcRGD*H zW8*YuS(GiZs=zh#jkM&5WQPNnv?Gp`sFbucTpVf~X96*EfeWG;jN=?1&Ex7>g-7`I3(QY|Ie%_b7vu(=s! zp3Jl6pAsDwRZ>c1Gk5VdAe*e|dAt&IQ6!)K7Cds5mF2hX!~T9r0t4eFENc{Y6NX%@5$8H9DR(*e_o=UEFHYl))fxWIW9 z+oK`#tToGGjzwL~X#dIPSgS7oCG{7XW6RlpJjY^&Epdd-ur$V_&{vIsb((Ppk&N99xb#7V+2A99wSAvE`U!kxSF?P9zS{ z!w4qayO9W-XUiq^Y99~STq*}y4Azi&mRKNygMTeIUI}E5cIJ z%sCdw>H__Iu{qYlm_^rLe2xWmO+Cl@&9KyW+334DfJx4=SwVBGQ>|1jNc^`5X}L+V zi|WM`$uGi?qjt??WRls?&MzS|9yP~?VU7*M92^--4mJ&BdM zG6mlHc~fla-jmI+toQ^oY&T%V9EJyVMo_vm#{O7-$W5Z86#nK#$ zj&!39C!1rZf;@PR#hJnI`SP1%^$bh=gQuHXAS?E-9<)l`InT2Li zQel-zw_nlJe+U1-)bf z*K>h~FE+ng81P@mf6@81`2S>n{ryR%SLj7Cm&y3GC1N?oL8-BiZO7dX9R*}~dO!Io z;=VzN74&`sx6Ah#ct0I-Mu}LVf4^D&ego&~_bA^x#qT#VI6t6#>>Y_x`TLdn_ovC< zZ)9)!>Zkaj!S(m(@~QrgDeCWGpQ-Zke&!VA_b_Zpp#CJhr}(`kVwJAHPuAbV{^hHm z_85cf-^AXK@4+`jiG06W|9*@7{U-LB`aRk=75;j8oUi=3QdGy#gG``FZ!a1Ue7+&s!TxaF%e&SrwXL{+FZ*TV@fTlYM_jVpe!w*i?UH-9 zbYQE$KV*pJ8NHa*#i>`Rp2UN|nF6_@-kG`C;t`MABnpT&)h2=qd$7YM&(ZU?|W`qNi-EXs|cG9f0FqC}v9qSo3KREaE=D$($+2D~GHH?T~C zm$t94mtc1^FhJ^nnj`;FDpG<;a7{eMt`C44MFOwa7_0exW|@9CpeWT0Wfs?Y91VaH zWLOHHCN>lR*6N@tRGGJd2JZz8CeV2pb+yCX~w2ej`t(CEFO(FD>1_`wo|_W4`pSn&97 zPm!`##q(7oQQ8d#RDXEer>c7Pq^ur?d%owZUefJOjPZS_KZ|T8eYl{b7sxPA1z=HQ z5B^usL8{T07gQR%QnuDtof;EF*TZ(%i?+J#7cz-Rv*|=lJ1rpe&xfg)}#^Q@l?Kf zE-gA0R<*vP!N;R zY|&Tqr$LaZ@$eXE@M)6<{a}%M>wZysEE=?=d}aErst=PKPWlxDRPXD!7+JlAdB_{f zCx3%}{4FmP`!WA+#!S2iKz&Wl#nwCI$#?jM-buRMEs8KVWzgK@fcQxZC5&O3n#0{|j*)~n{7bf`B$ zRi~{(reQwylWJZ{EzU_y%qdQ-TH0Jx)V#FnC4~U4s5;6~n2}NFi0;d9&v(1$xi$6_ zt@^>^haci2Sz10&+t`S(>ys3G`;Ms;z5zRqGcd>bNe(ZWaF}pucxEg70R#Y&(HE`Y zic9$T;E3UAJTF4Aqd{C45}=Np%lIFuoWQ}w2lJ#qz|YNa5i zaNN-F(hjLYD2Z(lOZfksHj6tm>!xMZow~QPYu~!M?nP}g>Wf-dxZEpd7R2Y2r56fY zABR{VvK^k;%e)<<%c^Tvjd!fQbmoG}&2wB!XB6Z$Ev&)*e1*fwXPzcS8PFfK$1dB0 zIn)`{o*HTo99DeVP_4YdxKxhs@W(@|s(ms~9NJ<6FE=i%rZq#XlHw5i0e~#bD9ed2 zn6=#PTG3KeKcj8Y!n$?)yE^xtT9-AgE-R1E0)TA%w3*!LsHm>XuUXiXS1@C#YtH7% z1v4*Q+cCbXwtCrUhj-a*k0V=15ejV?AVTu966axxLtgT?rK~2E&!fZl>*@PHh6Ov>TQm`f=$qh|N@o$2E3y2ok zjudA?a!x{KMs=}TBO+tc8ZwJwBBEj(4VlG&0PtVU$JqDz^8kIWa{8fQ zm>3`M{16x#(!WFbb}*%D)j4;oS7F8hBo5<`T=CH+3~4&%_G(r!@iKpYy{zN6d==Zv z-$yTaX&q_BdJMpk*{k-~puGj#0(IL_s_ZW4U`gFWHqNYaT4CWd{P9)Andwp zE!^0TzIY9N5ep6C95PK)Ui>sUkaY-+dcPV&JHrkOLE0#yqg!b&JeBssDR7Ffa!

R`tEypug7icv=;;n|9pO|AA77&M-M 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/IBMPlexSans-SemiBoldItalic.ttf b/assets/fonts/IBMPlexSans-SemiBoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a9c14fede24bcd9ba7f974e3ec18c491df32ee30 GIT binary patch literal 211072 zcmce92Vhl2+V;$xThi-IZ?~lP=WRq9R6Q)wQpMRg`=G=bdwI?uDr9_x=C==-inz=bSn3ynWs&VVp4* ziXR=zt)0*?3Rj#dB`cA8jjF91`H*Apd?w5%Q2qL-=5Z6=JYRY<6V?YA*S$V!!larW zQ)VkuQr0sTw0GQuoc!y9RxQM{2k?GJ=fYWi`_JApjj`Y}jQNVr70b*4&nuTOX70uP z+Md2S3k~ge6f+k45#G6E&a6xNSR}LIdoSRA(3}OUdPaZwQ8;6%+Zcc8)86h`T}flx zKF71=xL?$Z3a{-xx8r<`mC~E?fD~HH(HZCL$RNJiK6W=d6z=ww}ZH9%Rh;(ZX3P z`wRi%D?Gm)&zl!^FPoM9*R^TrZ!WIu7tLDOecN59mofhJdd7UN=v#crvS&lCn98_A z$C&k*zNOuLM<>}|VJsef5`MrGV+tP?)Uf#8nStfsGH(U_VC=oz+n!MK%kOY}>^Q4C zty{qa^F~tt5xTkKGsg55+<#Pe8t+k$2mdKwQ``9MV@9@&>E)LI623~`)!|wvT0|S} zFtJ&*IseHTDQC5O25aM`UR?MZ6}?cHP@(QF>q(^Z5IE&%8;+w#RxC-U(Sd0>jd#D$G#Qnuuel>D% zwJ2oM#GSy>0yamvmGy~b&UeIYHdB;3zX9F~FG``fjz9`VG9wir^&ll9H6W#UBsL>x zJ{Hlkd5WLSSx>#%>i5`{59Y@l}!;kRxV7=@5C17jeeIP)gaYsiSSvC zRELy{l&9shkP~jxDDg>bwoJp>GJZQ?9xmT$!aZuo&i6b2D!yf*;uX-u$ikHGLE93R zC>8+kzTmBH!1K2AInl`CL=kAdfR&1MEJ1XzVp;bgi&9F!CtLAulYBP>{1t|_Cx~63 z)mA(=lf}#Do7gDPj_V#~5}QEVYtYUt@cJAi;{E?rA{qM+C8ZGkMw3(^_x6t0z{J?d~De2r(IM`Ysu&fTs=x<(|c`i684 zlESwjk7NE)_h{duvN92}Ig*7-eMCCx-zMk#!b0iSJnMc&Y0>l?>ADS&Q_^=Arws7Z zEi4sy3F9*%`_P9-(MTq#E1?@nKmL!>2t5B&>7sT>-+CsgcS-mDRDwPxojj1pT|&9;zz`L#)`XCq`Hd*or0$S%2( z5Y8W^eIgs>nFx+wmlQll>DN9lpHmLtd9s79ME3CF^jF+3WaFisBwI;o9_q+u{)ZCT zVb3J(G1=swN@S;9sm}SSxY_wV@DL{gov(_0@D;9r+>uTmhI>JZG_ z8+si!F$20aO!zvF3u+@AHizz$TwexVpX7XtzYqOa3b}oo6^R`91t)ON&-ti0it^hi zYd#7847ec}hzAKSqURH^Kjf3Z9}z2^?+*GSjo=BQ*?p)dzhsp2UlPCQr;-JG2ww!x zc3;pBz)o-z%v!QZI0+_Ka>28S6)Kz+^7Baa{de%p021NxG;|B$g=~r${!1eCUM6(R zgDjlC3tO6w{*q4SEJ?|9o>dAUwJ*abX@QxMwsM`4k@cpiM64#ZQ{*-#v z`I)pQ$5|2idZa&;Abj^-_$Bb?L>Kf?B6Lj)%Tbo2JRP{51s~uuv=NSEVnzH1r$Y&3 z;fezKqyn#m=Y_x({6C57bIw1Dzas(P%39Y^^UHbGg7d-=ia?rSz$+Ec=% zEA_Jg&?(&0e)1nQ+-mq0r=2GyjtS47!|(G!pFMRm;kyK>4{4?3!*I~$4Y3ILDMa0I zBu`sZ2b(q)cqhAl49}OqpT1a!!@q_e;-KF#q6Kg;T;wAW4bosIVV4MoL3>O7dkx@n z*-g%Z#A@&Wr3b-JA@JKY{ZIOuXv9C)Xyp$bO86t$aHT6)iKH9R?p+W15e+9|I7EEw zsS}BwcOo4|`Um{8t>B-tpzX(?|JO*%Q17V+K||#W)M+$UI#oIotp}5;SH_7g@clt= zjCE$f_Rf&BzKvB$op>+$py_?mCO#Ry>I9a` z#{&P8Stk7LQrPl(-pI!AURK3tuu44Vi+c*jOCRyE&iDCb=O^&V|Aps_crF0XaX!Q8 zR2HxrWf9T}mZ~g4J`KFH82P2Ro`P~8%Y=+keW|h@`6{F?SzoG@vsk=W1OIk8Qa9R~ zfOJGlRPIN*%_F}TWlD6j*q`Pr{ycomFaQ}IXO&1_ti}Whea-_?Ujv{SCnu3J! zzOxVMCZzpHuOq#Uvk=7znAGe8mb|-SRKY$b`I%Y z$VMCL(okNZrNQw$t_S})|AqGCNM0At*6V)Ds?Z*y229aXtQtx1_CXCtT`bdiA)dm{ zu=UKqx=`|GxvZ2`u_+keUB=e(b^Jzt6Tge^<0rXO#E4?iE}jz4i+2>A;-`ctVM>f* zQ&N?+%4TJYvQ@cDxnFric~N;q7o$tm<>+SWI(74P%XQD{-qig=_q{$Tpfdid_?_|J zoBhpU=4f-g*=kNUr(8R;j<@x5DusFywiz3QZLOckCm}%HkI|V%h@j3%U%3o{#SlZM9N-1DGrGf3R4V& zy-Y?gH=>s}D8EsDuRNeUs=TCQx>#M3E>|~OH&3@zw^jG5?xgN>eIk0f9=+UUX67Js zq&e1XMlVy%Ip%zGsqE!4^Lnk9_n99t?>8T~sF%%_36`yv`z`w{k6HRXdKrvfMtStI zOY3ET?ByFjaIKfz`K|0HL;Qd__fV%Aqu|;}-2XkL)W%^4_086$2yH>VcB?{~K4W1Nj5Rr{BVV#|A)PvT-OYWu1BPf(lz=re78kESwDs0pZZHB z_doq;ciCOi*XV2MY5hER9hLO6)pTJk>RRO8iG%eDVt`%j2DXo_VG~#)3Bt z2b;vEvsG*uo5^Oe-?CfT4z`p{WXmD7E7*;&S~oErWZKBQ5P9@rzU)%Aj;&-{m6L2S zdyqZM{=gn#>)8h7Ms_8;nk{6%hyC2BT+gmyH!Drb809x2grzn8zs-{WudQ~X1CnA`Z@g_jUQ zFAO41a6T0r_dI)v9c3@G*Vwo0dv0J(F1W(2JeDVLGjs4FUdc;&Ij=(m=oUVL_ww1i zoA>ZFd=}{=_A0-Iy}-A!BYYS87ymsw$M0r8@VnS|{2ulr-^0%HN4OWipELdt*YQWW zH-CV0{v;3K{XCfe7dP>zcqsofkLFMFDE=}}<}dO@{tS=fFYzS)7aqe8^A!Fn&)`RR zCePxp@oavS=knKi4nM~8_}_RwKhEv^1TW-o@B&`U-{K|w46lKXEaUIWzeOz3!*2;<8|IA0+m`AQMN zFXcC}*Adk{%Rk_?{0H97oqVQp3v}0Q${ose(A4)RcPo38JC$9^ebCywq0jDzR=Zc( zr|g9W|DCcE5-~$`h*@H`=oOiwQ_Kh( zxKgQ9Db-4nlA?@I%u2jcpj3!m;x4gU{9fEG_K17M{o(=f2l1fTD=rbs#R{=fTqahF zHDaCEAg&NsimSvXagDeZ5$)^57ID3}QEU}AiJQeXaf`TB+$Mf2c8EK~@5G(rhL8ms}zf~O0M`q zDHY!-W#W6KR#B9Zie4E7OA)Jti@%At6(8}u;w=s-zTzb%RJ^X#!CHoiV@eb(N{o0* zNf4)$IB`-*6CWuV;$tOGe5vG%e=2tIl~N-9r38q-D#OG%B~yGNV)<$j#aD?KemNpq zhxrZc75-cHCL*M7^E=o{{yX*#-@)GE+t@L9uE+Up>xzwy0X56f-jf8;)V zKlkO2aew|e58wxQApaBh<1cU zpYR6$DId*0bN<+Sp?az^>P@`3V^@(<-R<#X1}de~fe8og{DY|r&vuV!vZ|!)C8m)+m=Nk0B0Xz!-=@AO1^vM5JWNGy3=c{=F8iV3qKis#y&i z!D?9@8+nmlwlFJZwh~zqOJ*r7m8G$CmccSv7RzQi@VW9>KC`m|R>+E2F)M)&R`v_J zJb(qVAQsF*SST~GFc!`tSR{*L(JTf&T^x&N3Gi?U%S z(7~^<@1cDSQvb$614I8p`<6ogmh<7zzq6rxp?N`@YoyKCz;>}Kpu;!9W?Tg?=tj03 zHsJ4U54)M&&hB83z-Bxk(nXpmXZOIWY{FRoTRws~|S=R6+LXEvJmDap-k|^bl zH&dGv+H976>o;$X*^GW@*9X|aiz+y~xS~p{04C4|Fj04auWv?U$ZVDvs<2sX7QoqF zi*I{pHcn`(1EiMrEW&vPB$$I&MF#H&!(#R_gZ~+Y0^G5JW8oPxuZ%x~ld~Whh~dLC zryBNWyfy-YkO83y(;gD6ft5p8Y+!WtH3ips5U3b5pw{T%B-KKQk|7XkjUIwofa}=7 zXABUrOlCnVuD6U3*HVCBfjFsec|jP{aUID5v>IaqyB|tN%@Hh;*SoYli1OvyedLSL9zBoAKg_igHPq`s%5lF^%jI*pzEivY5=upV z9y#96rEBEPTAqvk(fwDo{0l7yr1H5+nS&fH==fA<$jOlMF_7yVK{4SGVU_I z?sb{hN8UBw&-v8)Z1EMoZN4w~F~1JKy?#IXm-t^Dzyc-*+#T?7U~1s*ppc;Eplv~C zf`fwR1s@2B3|SWPY3QvcKU0-ytLde%+_3dwhY+Id33o(njCdh3Ir8qvZ=x1Nor$(a zKNXW7vo$s(c4O?*aj|i?#(fk&G5&Z$NWuf=T=O~03Cow3@2t$~Wwlz%trM+_tUIlH zt^L-sHem~}W!g$?OKp2?{kB84bBTsTb7EQI$i$Y!TN4M8Mkeh~`XD(nxhlCQ`Cv+P zN?FR3l+`I4Q=8Jd(pIPKNuQj4YesKoeCC#{?5tf`XR}S&=IpWATe45*^yX~M8OZg? zP0XE_yDj&dyxhFb@D- zGir9du0E(fwm!AKpuVcUslKhgtG=&(bwhK*w1(b>Wew{awl>_+a9_jzhJy`98csHR z)bPz{Hrj7=h!x}&kFv8}PIv9ED;D(B@m}|yt8?$T7gJWlm(~S!n7dtL>T*0`iaZTge#&wPB8@IZ-tGTavb@P_y z?ag;L?;GDTe#ZED<5!HoV*Cx`cZ}aN{?V4&mgbiAEn8Z)x7^*bujQ$h7g~VclRlmF-DKV5J(C}u{B&zkYfbCe)+wz$txH?iw%*XXqjgW~qiuJ$?Q46g z?S-~uZ3FFBx8Kyhv;E^K+opz3-8}Wysk^5oPTMo>(P>XlduiJ7X{V=sKJDCe!}O5p z@zY`!NZ*Qx6a>WuA7?JVf5>TK#f z-g&z7^G-*XPgi)Cwd;y<>Z)^!D^F?Oofux%bxI z-MxF~w#=O|ci!BO=6*Ae&GVZVIWKWu?!2mbP4n93bEgAEH!r?*@$SWY7xyndwD|SKrxt&__-vo(3+RjPOYY0>tLPiu_tFxtC8L+z zu{3(=eM|RUGV+q$%goDmEIYS+-U^=;Wh-u4@%f6gmsVYR@X{AnhOgYX^7N|uRoARK ze%b8H4qaYz`RU6)zWl4p&#e}#eO8C8j$VCW^}*FItTC(!SQEacXU(EDE7q)C^VFK> z)*N1Y_1YWOez%UT^II3WE^%G%y7G1P>n5)2Shrx^%5@vpZC!WAy8G7cUw3fb;dLk0 zomuzgx*yl;*88pBx*=r4?hQw-D7fOOE1tXJ@RjQ~=5E|~RrXaySJhnAe|7BD3$EUC z^^cpHHci|#ZBy5#1)Fwm+OuizHCJ34eXaG{?bn{&y!*O|*B#iBx@G5<@2;PH{fQf9 z+;Hl~mK&$sxc|ndZv1#_{nk6S-u;`aZ;HNY$4wvKboS;OZvJ#z`L^0^P1`1Jo3`!N zZ9BK^*|vAvceiZ6HU8G*Tj$++=(d2{x^6qTJ$L&R+dug2qThb7qh-g89rJdq*m1>< z%{y-0@zU*v+sEGi?Cr1I{?_du+|hE!j63H2F8Oy)|L&!o({?_)^P`>L+{y0ryEF36 z?YmNU-FH{PT~F_h-@RwIIIHtyTH?~X^x9y$H! z6@Lu*Q7C7I`i1X$L@GM=<&YCA3dNu5Og5+K`lhE({5k*6hYkiE+)``Syd=f8f#>-&zz9!)-)d$jCm?a{GE+m6mYy5Q)Fqw9}uK6>ov zsiPkq^E(!Ptn66dvCEE~{@duky>h(w_`x?)-?-w9w@!>avGYxzH+$dw@~s(f9e(S} zw@1GH;M<>`G@qP!a>vOd?_|ES=$#{{YER8Rb?d2*-YtK3|3LY`wt=(nHNAJkdmo%m zJ-znyiT9g+#eXy2-}wHkNQ2a1DGqSRN1hNps3JxiS9~}f2=m@!% zb#3^`iFpEEK}bdW0me__8h3TbIdbF4+&p`*B{<0*Y%}xij)Nb4r1<^#O`Dj8_LDGU zn}fN>csOs@^~1x&;7hh2hBwm&+Yh7m!&o%6FFzPf7116QzEt6>LNEj*-=HTj4l{hy z5AYLMnD&7uF!rOmNVJ?L>lmJkMO|*5#hP4LP*lln0o-QI7KH^B!k!-%^W<|+1Yaj;kVWx1tFELx;irzf7Jh3=2B(ZvGQDMDNAF^svWvJuBxSU{9v@yiX zo;xQxKV2pOI%26J_Gi!$if;PJu4j-lC)>-&sa6n|%wT*; zwH`ia1_F~AOwh$R6lS_E0#Qhi7f(=76X*-1wqzTzXg3P&68)cmUrorhAP>de+`I^h z(qX)~sIb6hH5gMWI8Yj93gmX9&6+%%aGjE0A%JwNQ6lx;y3wnb&on5lxg`;!Eb|&? zb{ewELPsWL=_AMKOv&bOo&V&i0SZs@s~>v+&5jDMjhMHcWCTTiV zRKp}+2BAjPjX^mO`&Yru?;?rmQU?S<2jGGf0@Z$@UXacRQgsFe!V=V4JqEc!@{W8t z2N#ueXF4g{Ao&pz5Tj%vC|XjkBm)Mc2m2^iLm;nE3JbEuxq0(b8|G9cR25k7dPK=B zN~y9o=FK z2!E{Jc0S}R;d{VaVGJW{UkGr72mc2U#sZj|fxM~0+XICmR1xA)p`!{NGgY)dfi>WS zPj6t>OO^r#B_;$V!Z9Q!0FxQT_E1}~9VTQLH`gG$yPw z5F%jRnaWsu{*rks__LYGddT{$Whb($LYiDzSJOLz`xWr8ZGn(HmnzgM+K5L`3^p zgQG3!d}ex*DM(+T{z@CfKu7XRI|c zno$CM4=~c#xspo$_(ZP!Ksf5+WF1+@D5OCY(jW?HkbpE21>*WNdX;1%4Ke}kjdTP_CnZW1g(A%t zO#rQ|0N~#U1+>Rdfl%q=7J<+U1mtG~A|f?-U+@V33x(mNhXg$07L1=42BW?;GQm?4 zKJes!$3d4|z!yO{S%g9t+t_0A#4sfT{JDIQfA4|Wh>*5l@W5guVHDU&w+H5rI=9~i z%)!P;_p`77{`{cRWmIhUud30`^CClUxFGoFO?au8QGr60p5Ob1EsP{5txgWRdvXt%}D zB-pL8J>AN1yvs1EB3Cj}?FjkQFeK8EhO?;1Yw3_2w#4fVwhZ~ZCOJ!)xN2ghNlcCo z3yg?LQh(1skY@D>cIR8cV_~@Lj(_m^I3{K@28B1bdiG z%~_U4(Fi!n$$(+RI1rF&2Jj-lH>|qUzpl2?H1=f=u!Yb0y;W76;z>`Sp|~{ODFN>U zv$;kgr->y-riyZ+(Qi)n6D1UJmo$87?}-YQo=d z9%?trVGqXB8tZyWp~u(<)sxuAJ<$_UZK}#Xu_%JqT?bmf=zMy z;#HM&5uJ_;L$UB#Hhe^F`LMeB%CNNX6RNNV#`-~8O%cQ59G~&BfH+)5n<7f%A-gWU zMD`haDd8f$Gz6dj9(|g8NM<8!k>5?&!bF(FOfc#KRB8`m=I0fYih`e0Oz7>;t0i@8 z#eW2^1fqANXzoeXx};M;>_Hj-g|Pz-a!I6V1Q9KvmIHekA0f{Sw#Pkw)qI05rY%aX zpFNCzJMCGOej!3+Woo%!-`u*D?RNFItERHmC(t*&pr*3TD+oF1rO%ut7_(!2IxHo| z>_LO$9fH|CW*;&{AEKx{b?Fd)>DQ0hRp#{=vnw_}bj-eXRpiBE_OeynGGx&1bE)T; z-SIBBTs&&0xJ43hKt3bkU`;r#JVr;e#ok=t4ar>6%&NQ>D=W&5s2@1Qjj%1vM)LPoePzf-VYVEYjc| zo069h6J@Jy@GBKQE5p)l;g^;Urxlsd5eCFjj({HNY<@r7G%O(TfM_`U4)Fi(9k&n%g{&&k5(?U zjEShs%FBzii;#BZU!XOj&Z95egpX!c=L@E;m9Z zLiVmQ_^gbk)MgaVRb_-WnB_Tfsx& zlqK7|f(-t{N7UDZ*i2ZwgZ`01G^2msthOINSWfD}cSGdh7j2UcE)C#J;CA4duc})a z5p0+;?+VAK!sqy*L&wjbrSA>LpeqUA3u7y2>BAuHrRo@>V27ySAqdtu!|ix!k$qXA z9D$<+eddkafILJxT`GX;uVTyLHaK3xR~mJOy7^0kd<`|TSAKD2h@ZZ?_iEvDbpJ^t;Ek|ywxI~+SggVHJBIU&Z zP;ABAR%|hHi_s^Pmwy+^uQNIRn|qr&d2Q!x$CFZig>wM(s=`_mBRbRD4@X?}m?<Fa;Uh7^+3RsC+MvA3LM2 zju+;e%iBsFPx87HyO&;PwLOyjS{h1<+5EdC(ce%%qBlezFv6Zcq1?JKIW;=U7#)9* zr&?1ZBaKn;8jAj{^I!p zbVf$;v>5}QRWzkKv2ezQrm4%K`U1+z8ajd_bfTuRqA6x>&JBKvqvq93+jVK(C?!8fqu6ZG+c|I$d(fQ6Wn>Ld;P=c#gmkOSM{5^9Q{!4Hzr z0>V3Dj)ZW{x6RG3$E_dptR)>@OH1>v=6s5LwWryg$>dN+=E9&~DPVEXZ`_@%C zk_nFl+jPL@%UVfsTsRKY^!88Vkpm8R1>gxUhz)u-$X+H+XSa$2dPnv)3{9o=f~w(`Gmi*%+W7lM|HRzki4Y5medg5@rfrOvA26nDLT7N8 zp`p}Lo?|h1T@~hNnztY-!xCy6)mu0Hu9bC@#unR)x?1PQ#WjoywdTg9&Uakx#`m8k z?f>A`Xf7Gj6qmctnp$^PxI+_~YEuXa3L$~Iu49U1a<0zVaLmqdnzK=40kgRZbE1aR zEKVf~<*0FkWI6Cud14()|l- zo81=1vt`!HKkn^ygk!TH@v;v56ozkyvP#m&Sc2`rPugh6jY_wbcmf^)eUYZZL)88v zGDXaxIoRZaim;y0r5!n=@?x=`-IkVC=sWgr_?SaXpC6kyA~iCj&|b)|n(LCOX8?yk z>m(ofLO){&jkWQK8!&$&slP{r#goPcNUBO>a*8;ab+|y}W0K<*YU9P1rhqE7#=ci> zcbs_WI$pBkSwQ)Rb;lh?9oQPC!N;Efe0qvQ36Piu87?0D1^q*eMxuiOl43E&Y+1XU zdqbO?f6T2*uwR2v4}0i)(YWmAy6NuskP*4Y{ZiunWp63cfJM&;dwL@`96=emFJS?m zAlyM>Q~D2kMy@nWeW)c_-?eJX=6SlWZr-wrf3@L@?^inffsQ?V3b@rpE0RZlHmjeQ z{Q{qG2)_<7azkk4<|@^>5P*?j_aYcmRQMt!fIKr>-?MVd<~3@@=icnl0kqq0=08f< zgtWKYp&vt`z~FhApt;ngDJC@Nj?WAM%2TRLlC#Nv#Y@_`t*#og_^AjD&$myhOD&kW zvDrSQHnm{J#!*dOAz?Z(qDssCEOqnh+wWR6%33#9=1aP#^#;fJ)i$_t;8wN4D}h@- z7~eTG+v|!UM*@DIK^y!F97h^}f{UcY$%LyG&zGiN-bIB}XzQr}O+_*+?T%`oVU$D3 zlaL|93=S)#bk9w$n^|^GQPZyEVlTbXmhwB%KWk#~w3;NxxBT+dwCH&6XdB=zagG&) z_jpX_V3s;|aGgyo;7%BXTbuuNGq5MW#+`I&G~(FUBWjE+K~jTefXTCi59AsbW@BpG zHSrxFFb(mOba|k+C{rhVQby#*x~P*iVcAH(1y^1~phq*4eZ&27ywhu^m%HdR;no#n zeX91lDCEjL=oARgW3!4hBLM(Cda%FR2+7@Fjc{&@>?sZ8QAbO{(XGt8Oc~Rqac?aQO%b~(nc{9g6M+gp6LQ+ zjF!}B&u&Fw7u_Z+CvTHsqoz8jmJAy`n=3C=CzO~fMoK&w$ELc}0m2vxF)enpj`6o^ zw{Y*-JrP+|>Ft}wtbZ)I$eXxlXPW4rIX-oCWsaZYG!I;mnjS?Agx&FE2VslGOWy`D zA=^d1jTRGfW6~|Lm=FTu$j2u_Am}0qp~i%$&K(o74aS6`@thiW_?ehc_|Txx`AJtM zb;L9k)i>s(7B;pfjp!&Jx5_p(y0*ByHa$Im+_a=obNotcM`xu~=h;%de1j%8XEzVC z7UYh%S?rm%ByXSKwsBdLM_>|@O@pxaLC$d^8QFYwRiMkB4AG$)8%eHlTYE_#g(nke@t7EHNBoFC(XuW!%&_A2 zs~xujhPNGGI(d<1zknL$D84CUGRBKyGD9@$kZ)=d>jq+o5xx*AbyVvEb!LQa2nEsL zc?2tV02-=DSZxXh(8`seYYbis*KYCQUenjDcO1XTS251q0^pB2&hk*6=s4|o?|WW* zek3pZ`U3|o7e##L9pLYe=!=Zch>L9T5YRPO*bTbIX>N7o)=$KiMy3b55OvfNhWe0m z__K-((r}TwX|D0vo4DmSn|h1~Z$j{$eC0*F>0keH?2`1I3HbI(`eGLM;&|H-?Wm1c z-0-wx_+G`;H-KfX zgaw{~*G1#DAu{L21kGcTuSq@JAd^hVzpVjbMnSWZt~I zIw98@=^d$8GVSIZt>l+Ea!O9?eOI?+j+~lJ`P|roQJE_u5<^R?Vhids)DpPCvme9R z+J0zgY!;QuHWQr9fF8tWN*BW=S!_|5GTjNt9#(C0c1hOe8k?o8#1M2!eX!?DZL!5`wDTrRO4p^}$}@ zaawi(zRXyzbt$do#THqKFN=mR%6}EU)OZkirZh#(m`OI1f!E`X0QInc7I`loSfqKT z11ui*(!rw_BP!IYvWGD#n=5j;a0M^c$GZ^IyxherA?93Ll(z{-9dKV`_Ugea$G67p zAt({^;|l6Cy9@#Pe8|d5VB~d zIFbUS(ZrC;?wZIRnmp{Grw&m4PyrYeJd+?{Fahrfii0Nc`#B+ksEowdYG4znVDA^IzKpj_?Xcq8xJpy{f4%iE6+=XLa*I<3?ht2!j67$~i&?1x+KCA)lp z*?~VM>!OgWn+B|SE(3Ki^YG<{bio}u^%&>VQ;3lB^pZ+1B{w}|WI{t&ZfRY5c2iMY zdp$2GPps>#${k&l=rcX2VpwT~w^EQ_AI9&@N(=N$Oij)j8<`d#6p=A3v3`1x-lwTN ze?o;dEHNj#CM~ZZH!vV2dp_wuQEX3EW&nv2?7e0PV;` zs{Wxs|9vHI(X~gr!BAedtjUQ_smhE@sqZN) zMmGf_V>0u*Y<|;?YkF+y&6B4UDd8zGK{5HGGKyx^XY>p&@-g}56lfWZO?uGj`RK0~ z8&5y*WB`>*Zw&Dr+`Uvi1_X@IQ`dag8uDNwRbC@6!p>>e0;YDB^Ulwr=jZU_;f_6G z{DzPF&OZiMl_dk$m5}>*Ai0Y8E0vCCkSI%)*v6a*^1MO2J0uS5hR3y!ClAI{)nR!u zyi4$TV-FFrEiI2wOX^a&Qb6yrBZ%KiI>-fcgZf}76=*uer9UV<%$4E^#j#VyiOJ@w z=@nhw<3{kjqNKW>;-VTKAqw)H=iQb(6s+eTH+4)+RZ z7?#yikl@^onx$pDGA}JW(3YnDO8#sBy=s6n;?J3+o?ZM&W-)jWQ!yl&SXwwZ z0`iQQ$3PxcHpIRSmLd@z3~Z@<>x-NQQ;4sh?<{a z%*r_n9Y0>cocuix-i%0g^X6rrT*RDMD+&I{#VH}QCJ7Pb3)duh4fa)&Me|@^-CUtY zTGVw(b@d%bykX#RFHN$&B-g4NNl3L)Yyb|VLPWYyOh^ zo7%c|8S2n>_P}4@@I2h}$mB0jQ@&Dn=H@{RltDF@Qsm+287&!yLo!q2^-4xkMq}i} zG52JQ%8$vaZirpT?IuUT;$=Qj2}zM50rAO2IYl>zrb{HtYsDUEkv@%rn?bLCC>I>5R zhu-buA*5mFrg+CQID2ec`{dBc@j7qEx!1OBo5%M%YC5)wG{?(C7pz>IEs2=j#W(UlerdPQo>th=m=R{njVaH}sVVXg_07t&Mg?#= z$Ex~sd)G*n;`hJGpHn?KVk!)2$&-5+uAQ81plL-pem^J+>he8x(3&K< zF%aLQy$e6#<;jHu*vST6tVJEAJIlR#78OldQT1ruCGAxqMt^fkUi`SJaPb05sO$Op zYUX#@yv7^W^d@GsOqy1pgeS)a&uuH}sL$X%6~(?LpX?$nlQPE|!GEJbTQAmhfsg0y zqOIoVNv@++kSdc=NXX^j=`~YB5h_wu5-S5L@b9uPm0TqeNhXWpDwM|!>VIwc;PzVFmfW?o4AF2_B%6qX$C4${Y(!3z z6htYtq%t89K|}pdyLT5ma}h)7pd%?T^GPAiNiT~c77U6G8SAS;~=K2MBr)O&!(Mhm8(8&Vb zNW8l7CwVp7MKKLAl49AQlQ+}?bstBkN#uZ0X(Hc*iF}hfym}V}$+a&~fuc@ZWz8>f zEw<3npv&z8O8)3G-+aiDg-)}c~Cmnm?YS{}E9Kw-pvTqa|N&xRrUOvb>+Im4Z@3?2m!D_Ty zA)S2UbTx1g2AokqfZ`1q+9bMyIvNsCFd>YUN`@M)-K9XZoeh_D34ZeoVWGT_;jNZY_bna+?X^x z`tsyLFU4MM$&AVxo|G}7AU!m;HZ^@zVO$7y<|UQqr-oc%X-b`2SGG;`B*ys#n^NMU z3)ADw=g&rlJ!HyB@e4|$$eduqotu;f@f^;H>SlKnMS9qrL5fV1lyLEEE z(^bXTjLp3VnDr#8D-p%vOK4UR6~O%jdCMQA8d=IvAHvB4#I1kE_c&%?6Yw1E%zpw? zO?1itj^Ag~2BFABThVBt2=@kucEzq?HMA{lSd=FU40^yA2FQh(K)7@V3FowZ(G_ft z7g+tR1{CRB2U-?nqJiJ4(JRJh=ncWm%eJ-FEuWmH=zUU4>(lH-x`67eVfBhWth+iZ z*GuO=yI8^bQNcz<;Pi*$>Px0n`g`m2mddH6F9zKiVu=b?!t|+0$vIv@!m=u7(yGys zMG382d&@GKFR2erjmU4UO3F&{OD-ML7-$LW>`Ap$Ilc(?w~T2{@{cjuTS^l$FjC15 zOARiaR&5>2qXQ%30_Vk~<)x1G^3j`ZKtAky0Z#er;S1oEhpCH$XaYo@{RT~)K^~vQ zSP;phBfq`$Qid~_wdaKar`Z0~wG}6W=i)q615O5{_O)xYAK}sdlek02v+1-iSWWz# z>3k1@4z71T<=DY|(L1zE1}k_5-w}@uzTnHp3hD)>x3aaMa}#y;a(BGaj9BHxJ3KnYqXz?^MM&g;?sFFawD5A zu!HzilYfhKLcRuc1D&fNJ0@7PJP?Bhm#gLF*DLiLnE=A!n45>0LS;_n(n>sE>UduEo!oUEV)|y zk`mN@Pf0B@MTUb+Y5aL>cu+({fgK$mo}`ZpHirAgQO7y{e%C8JSA33d0^h+GeN*ED z`AuJZlXf!7@BS3NKOOpA@%c}G7xYJ85%>5IIOvu*=*9vU;tRkd0s&7lMeTr_9;%mo zaV>k2-48h9F60YnV^7e2k`*o;3flcY1sUjl+<7hEf$t>aq)+<(T=n~yY$)TY`1=wD z7EH9kfz~QtDmh@klu909jCktyh*vGQ&nmmS@6HL=Ukls@;+=5UJN&|TDtStAL?92m zeO5kRyZ-uXN8>g0AKww7cqbXBj?(w%vri;m&=w|hsD6R0UqJO3cm3dO=DygAL+p}I zqjT4~UkaKVF0fag8&eFRrT_;SX}g9|^3etItxxSJ4oS^SG7H6!o~~y54sn~~okPMF zA7Ka$icdji3XDgLm+WQ&q~dc}ca z2oY}g>`zeUu!d}{qg@yD?6hZ(5{G3(xO)2e#gyW##<_x@7T z){}g}g`~LyErc0i%T{V~ntMw&*rzuZ_?i?kvTR9Imeuq|`|d4EQ&XmN%`fQ)j`N?l zuP$!P6x9wpx8fVQu5ZxNI_(=F_=W-BaK(YO;Ao0HUg5bv^$~vC&wPbLPFLWR?C&78 zY2+Uat_$MiF(Rp}*~vGgU=EZB+_x}nzN1ZE`(Fs2a?_OH&B_(6=kdo(@sB&-M0=BD zdr23xXGB{x1BNvoXp45$s%>FCkz6;-CuyyT?|xj{DvnVMFA{C-KwC+yn$~CG_-7Tz z)PI&k1F;zR7T^!C6Y0|!pXOj?tq!FT11+KKEV}*&* z6gDj+!@D?TVaWJweQkBD*DYbCT8I@B5d)fKhYW-pI+B_V);EF%(|LykxIn zuhUpzrS*TR3ucQO=3u~_1eocs3J}aR3kO{ZT+m!;kSx(E#5llDbIC z?XUxblb;KZ*eH+j$lzh)vvnaJFm_(lhwQ(HXLtg6VHZ@I9dOR&-$=gzeibcHm%f-v ze>p<)^bL#%T-EcxE_T>&Q!fAUDze8n;r$t|_r%s;bI!iydr(8g^FWQ#x?INz&L4 z)Q(4qjsxa$_6tYIfHzW6Um~B#MTrJlG*5>WM(Q-AHYf6vD^CCI6W5BW#pe$iM~pKU z)u+*KgEh|PgPF;w2rcJN{=!YdF)tfCwy>?rrv5hOwFdybZ1gJ#?op zpvP3iKt89r#Xhz~(uC#|NySqhsO3Z(x~|jmaaz7`&^rGwXw*bRYx<864a5Yr69Hut z$Vek$T%pwS|CFE?PMiD`Q~Y0&od4!O$`6gd%q^r-@}c`DK#$6C@!6i2()_^!?A?|kw!K5)1qpraDe@EE8EbG`~l0b@wp|PmIhNrW?HE=siwNPx<Tgo?y0C({ zxHPqd#aTq-2IqYIRTA0@R89+R2KNGy4<)hcUe7mplp)MxFtG0r2@6Fok9l!Tfz#e2 ztWH*JA6m-NqmpW;mGI_}irc2&)_L1>9ujXKk(@WVI(hne{3Y}A0pP6R81H{9{(|pk zfwa$4%xau=a#S|jrZ_m|zjD2tiNaz|R}fbg5JaOR!Yp*>OZf6KU?vN=LyIx9>CSq5 zSKb+*0`8EjUqpAlK)g)GhHs<-*5C}6M_o~1d89n*sth$0eC@{p|7yt4;8* z7F@wIIRuCoa-kwUs5H;mu$VR7+?Jbw9d$LYCKZ@h6Tf+7Ncdwn#&QygSA_+5|FSQLLYMv{ldqH?ZR zY8>r63G|H)Kk^69cp+C;nt2iB??i>w z1)riuumG7FF|vaw96=gDQV>L^la190TN-y+(F)!`DUtcWAH?bqj}SQy<|U~{IWvO}O^Dk-~D%*}CP9 zqFz3llgm`>LGRPvp9I{|(8z#B;s?|Y$XarhspIN~IDd596*H7xFqDk+c!~IyWrNuCJEhseIRR*fbsjJ9Fv zkqJfh=~`~iDojhtDoW?AQJLk58Ka95D9;>S6hE;x&z3`pV8vd4-YMf(GM?+N!@F3kLK9@yqAeqSkIFWu@F-CtJ^?=>MogNJRz$c)W!hpj z86yr&DMmC9vRRBUqA}&M=FPWe-4ffEc4ze7mg%E-)W~acyK5eKxT3S@`m$S9c*}8q zMmWZazO-u0V@-|%dr$EJ3AZ-qt;USWwbDj@@{CMXnYbbdKGLa5K6Sql?E=P*XSse2-001(2*WnNw5tcgi@AX7?{Cx30B9E83W)!Z!qFrpiK7>vBcFXs?Zn!?ptY4qC|z(h ztNNDe#~**Z`N3xV%8VNyd{FXu9M+sReyPWA>4@YoJjN>&UrSJvr7?2#ADJ)o~X@lPRWmp&Wevnin4^) zgu| zV@X=^N7JNsqZqwAHm>m>n8_1F3UUytfJ`vgF9D1tQN}a2hfNRL7Rq18vnKwM>ETDZ zAHn|*OJFz_ACJdbYQca-&Rbo$mQT|ch=hX>*o;9+i8K<=da&x@r4yFQ-w$pXv;CHl zx5!`WpMpgTi6TiP-OG7BAIRd;%2n5HDnRs;eaPc48z}u52f|uII&vQuTK&i zfQbnTTGJe)LP6)j$&cSj1zJV$g)FFJJAOohB~K}*J1Ed2cgljgG*zdOM~~bOWTKt> zsvKZl`=H_hPmi2px_Rj~y+ImrNIejL{s0V+|3g*wmIQ}O75p{l|^V)m7?zv31Re-h(uC`oML0hyyYe-wg zNnlrQ)Q{sCXopV;c2sVpmK=_WPS7q5gsu>iKuKR#;MNADsv%Wm2L`Q{J3Q)I1?UB~ zw1bSHdVrf$oRntbJ53$fpol+D;J7-BKhxdKPcJntU+|}%N40N7<6Cr&6TZbY&7<;@ zJO^N~2k!GtT!fWR_C7s; zABN1Qpcnl^Od$u*o)Wj~&|}9kA6MRc=!?JmopOv0eP#Kd z(A)T5p(_?qHlJH(oTDS^X-%TF8eSF)549O4Av21Cmb47^ppd*-3BKvJ;`!MvK6yHz zeqnNE>FmVh4A%s&ZIAjpFQsb>Q0fdDKhhn+ph6yUx!r&>xt2F-5iJa4!78<=E{ur+9Tq=~(OXw~`y$+2r3aZI)GO%1 z3+w59{9LE}!_3CW@O;O}4v*cxuh04+&^%G$L>PRaKy@fLiPN+I?b$GgZe0x{bF0V9 ztNp1&Te`K??Jt;+|MD{s&ib;C!VW^BEbCd^dL&DV+i;eCbGjs$3IRkER!7n96xvh` z%tbmjv82KW>%?c-A*X?&!_b$I1ZGsTAS*oF&@JD-{2R4vJ9eCicKP~ecXqG3Y+Fai zw#!ycU%ss)Q>XOcZyjrE_ml@VP1H4xB?GBF*UxXdVNWWx=Y~zaUoQ^?%D;~H!9Fs| ztLm#*>pN}Ob1@Dgm;3V2cT}EzA3a+!NBbArGLsYILIZT-EF(LNiprgeN}x&5xkSBP z{*`V936oyAVf6N+t={Vq`GpMTxw7GSO{;g-qL6b~>1tHM(czXVdvR&0BUwG@scadH zuHAUyh0$Q7r*d0SV`{9WGMpNyb62*c{j^9emK~KtpY5wpYA#b27I~TUVQED6j6AZqd(wea|g- z9BOc$f2lmF&(y~A@-?{C#OZ@2N^6O7$lK6eADbPB80XA=XqfQMyOqBnMt1P-3^t8! zinW#$R@dsM=hv(O-c?%^{8R#&S_NM-VhMS0^~%a@`qrXZ9OqQkn!;38N%5=Ix+*Rl zQa=Ge=ttMc3wBV+gE|J!6xx)0Cpiq0GvN~H_j$aini&+bbPj9j=e+1J*!b9TTt^yrCo*YEA!H`$YhKxs)D zXVzNSIqitn&!sR(k{mxa*B?gykk`~7-Af{?on^)VO=NJXPwL8Af1G&CFpQzB6YE)D1F z1=f`MwW;GH$K7_#RqflbVeQb`b3RX}CJWuF+LqAIN9*^@Ee7MB`qIc)^?759VSWz9 z%+GsPXqOkLSgMojqfDhm}MqS*WaW54UbZNuBI%c?o1!qwQfY`q$rk|7%^-2X#(GEw!$IyrH~?QM##g29-;di=+SJ#CeNqXwV=bp5 zaAj0!MgU=Tu{923gjfuJ2$ws=WPmQSf9B?+Rr`xU0Xr&x?fISO_nw#S6FtF>Se<)) z;?isz&B@W>MBAz~Hf9~2F0G0BCP!weD}8183bhQfMf7`i!aYHy1J*&gVRM)&nluaT zLS5l_Mi2YkIP!&9pImGbwh-rA@~Rpu(L^6&0ASX@`yvu7$kJTTrE**H@i zC_ETOAf_PiB4E$9{86`zyUm* zIMO)Q<51c(R*K% z>l@#L=wd|(`85D(lo0$%c*76H+cn_18X%M!_;wE4UR)f*A^L|sgp0@df$#kyE|lX8 z{8fwYTMWM=*GljwRBN`5^o!(cnINglGLe?dGJ^>A;fc9M6wT-FEml4?w6|*BHy&@= zvN1Tct*>m++r8euP~2JHIKNOgw0*E_@t1Y+#(~bKrXxM6{DSVpn)-O&^WKH2=GDFR z+Sc9q$;O$=hP=;v=ck(1j>H|?l*ZcT!YA@Zl*vKgN1Y6cE3i9k>W9J0ergFwfirUc z1b^hTQ_5i&-JXPS{~Xl;7|=9KD7YPvTWs4Y#MO(0$(U&|t{H6e;7kT8=i+F3a%H0< z(U&6SQg+sys(D{2|2ux@hAsAI?)uHeo<~YQ;JN9hn>MZ8q@*)1D>a#?aEW(uAiwrQ z>P6s*N~~gW@I@bYJFmnm%H6ZrVXz9n$}^kF@SM=kt>hDW9O$1{sPvu2+q@fxnAc@o z2&HGyxHu*R7p8of7t&v(UnC94qH*zpaq$o@NGPN7g1I4db=vVWQuH|)hrb;{hEkET zkbz1z%4VT6r6PlUB-Ca=xTgT>pIGB{I{**i5R8_@MVcS@T+6t?1F{UP8LS|n+`O+D z+@lX+?Gql$0cLB)qXCLov$+R>+yIjTnTj%%b)t{ik8zwC&}_w-WmtWfOb5L3UOI^N zJV&YWq4gqs^4xw+L=n+Z<=oFoY_a@_>z*iIEZ?$i?XZ&UtsmLeTUS`WHvDc^wB6;< zB0=XzqH7}$hQz06D6sX2ddIc(_>zSLg0D%CVM7*(!*^wCJf=gkXLXGTB%@o2Qt zmsgE#6u9E_LS`NXeMG_QUCK*b54!at!XO?#hL_;EjkMqf9fYOK3}ajvj2s3|!;qO_ zU@{C$h9OkKpp!7@Bn+_>hFA&%v0)%K46zgj>cfW2L^1|tCgsUqJPca2LHgmLc0*|K ze^59eU~Ck36k0`?OdI5i4Kk7Hs!hZyU&Wd7k?~JTcz7-;Ro#%P=rAt@ubY$Y#VooZ zc-{ChH-5~GA9I67+_Veye$BB3V@vgV5h*4w*^2=0Z>yQT&8eI`#;e(KD; zuRW~1yKH{+5;u%tt4`8HYd8#IB!d|`Nlph*J+LbDXIdzgLMLH*w(Jvy&Zq34Q;4Z9 zlpOyGE`*eMoEK#MpWxzE`DfZ|g&;GIr81*ymvqxpXj5EMI|muzis_Jrm=SuA7=xor zib6YzGchmQW}{1752J8c;P7vwp5?s?it4mXA z1U2<*4~F+QZW$bXy7V#4q1;<~x4XAWRn}frJGaT#=-Jq{eY9bCU~J9A*hYNZl;|%8 z!)Wa3p#BN#7TX>)*nU7$PUdSvK;hxU;+?f}|F5E!pCgjQ!UkcwNXbm8niLnrih(r| zMp(o^{qMYxR9PT!Ne(dLnhm%%;+mv(Gp;Eak6I<=YlMsAiZ~~eQVhKwE=IIBcG0;R zQ7(B`@@U)o(hVgzT5q3vsP$;;y3+L}INEOC6N=iCzmD76ulV(^e}UEV-(G(C-*Ebc z(tZBhepPV#_jtbm@|Syo&mMTK-*pBFMict>p2K(y7cp54i72!JT}d{z<{sv^_E_>; zlLRDn01z|=wmbvZ$(Sx}yP!so3z!RMskJ6CPPVI}Nk{{b`-y~v*MeyS)nX?Tgo{K{ z2C8F)6AvjF<~BF5dj!#$+uR_H5g0=A1H&W3%@01mZO`;bsw^3cG?W&X`D?4&(v5Xv z{=TY6b=X_#sjjK)+B7h^uexu;z$vAyu>($-NN6gqp4m9J_C(;;&gMu}Xv&ZntkuDfBnl%bEI=p`rM{p-uH(;{9)lY zEC(a4V?O3FA0B7|ms0C(ur&%ziqBi0l^UiMt0OX}aA?JL9n2ju+sVqxAr^q+%kXNj z>H;x!e5!#5nw{5ZlFJUWVZF^THkOKbnWzKhNEv9K#h84>$>8Q2h`W<_eH{IAA_7M!wVlo zT%!hlzz*9-$y1$rGrTgqEoY5qv$s}`N4ky7+(u?5PxB4z)KNnqYy}Qcxm_#`ZSJ7L zK{SIKpUV1lji1n!MVEb>Da(Pp(}80>9y_??t#*Q?I$LE_0^?XiKDr- zIhj`w`&OCq4`sh?=v=eDcjsuNHFD#Ifq@N~$&E$6>f+g&L^7T%4Mktsg(4f-XNSG< zy_AeDBi5`FMNUiC9LJD#0T%+)BparLU_lYYr_n#O41id%7jbbEhp;h|xJZi+w5T`* zvq>CMpDIUmMOpddBa5=tR0Ik}6PwA?5&X~A(R<+(A-w>dretQkm#PZ5HOx;TSr2&ahU zT)P;Wk^!KvpV|<)7Ag`w3n<{kk;K7(6}RMtQ7HeU;7{0F3`ZJ=9qT^DAQcf#5;Z11 zrje^faY;>p;-(2_Y684Y$R}d%Zvr<<*z_lo06tL_CLc>6*+8%Y-Rjh>s1Y#Qh)IVQ zhBN-|?}XsIV!n8D$$ZIt@TaqT^Uo{Ys5%|%OLj#rnYl82>eo-J;ozk$&v~9(Gg19? zkuownH}}rf8|His-r4ER?^A1Q8(x}!X?z^>A#JBiU^|5{37yI(&QOZzD|aUzor)sQ zVip3>6c8(*zlkYj0lZ59hzY=U3P6JjfUpBF5CSj|0Molcoe0w4JCAQ4h7fD6Ez|tf6^$9F(_7!$*WAX!zto8_t6^oCm`v4~9=344*t0 zK6x;F@?iKNr%QUH@}Ltlp%zbzd&Ah_EdP)8jBr7{kx3+P<$lp{YO}>&tB`hV1IfK? z-~|DC_%b2>pbf9Uc4+*s9fxlFh48E)JZlKHUkK`Z2#*S#hz8qUTpYu34GwyVYMn{}dtY|scJp~OHp^r8zb-RT#(ypa#5#yWRgy|yHD zUaT!$e8^E(z2-+%#bcWnTQsj0Ixku7{b2sof~UfHapB<3;=GfQy2}=~-gj(t%LU#^ zD1a@+&s{scr@s>Ut!o;4Jj&CKeH)Ji9vzzmXWJB89pY1m;g{{UeVX!SWv|zqK%eQC z9&6OGKgB2&^(C8JWO z;mSy|AcGRcNnv(%;(m;y8SZw$hLCj3jE3%M=`f}%=?If%L#~QaoLrUkmrj;mdE1{$ zmBqaL*2L7{XyT&&w(0h&!n{=io_V0am1+%7H>X=Kb62&FM4K8nt=m*wTM}vLn@cqL zn|i~(<}!kUC5c2$)7oS^#F6RSNGhi(D~*1Tl#L0>#ypT+P;zAuTp7e01;Ld;aAgo& z83b1b!IeR9We{8$1Xl*Zl|gW25L_7qR|dhAL4zwH@xYZt8#t0Crom2_6p^(q-4OpP zZghbrtPHKQ5#`}gOzYJ+PUC>Lg#KSQ9?Ol#a^tbwcq}&_3k6WpAK}Imy76vqyc-h1 zbmlI8k+V*QV22rQGfZvNa5_*gjndcg5H!NaaeNWSi#UFQ<5e6w<3+)EQ7~TA@Y_Ve zyHW6A6zGnMA0w$q9|gIQKIJUur3T>qO}RvLCZ<(%>N%S^{Pz!CW9{3oUa#fbL+3Tl z*Dt!mY#W#_61-w@{+{FG%GN{Sxq<3%VJ4;|+tXu{ z>NaInq}!u>#j$hqac~T1WEK8F1uLX^p>6%SIYLTy{WLSVF}2ictvy3&^)rYACc$nd z%y@yihIJoVd?m(S4M)7nviJXNIkn|Wyij-V-u>~+g7V?ryTSLpc!s<23^g{aAw|#{ z8N4!lA35%2lkb`3V)VMpr~Y}`u(X$XAr}fzWypM zsJ=>_oN5$v{OFl7Y;>;56ae&jG722PTVf+X?mNm%Ceaj{8ji69`;|3!yw&??&rnh3 zS01Ijx%~rZ7FzZtr8WOb?~&p?8}EN@S7+m{_QbATj?$@J>BZu|QVyb3mpUi(9Lz`o z(830020s3VCL`mgG+y$=R=3O&mRC@+MV7Fxh9O}dDp(0BuX@-5rCGUTuV-(`Ue99k zRQ*4C{;>o{bZE^k|HGTt-0d4MKKCfEqKOB`0EF)fAbPi}4H;^x_G{Fbyw&FN2pt_;qGOghb=^mFB<5fe@Z75gW*f z50t=7V@xCMU(2-;PWu*p`>)%N&_KF;v7)<2ga>N7*0lQe#vjpR1CMNfLXQia@tuD@ z0|N(-mCke+p@He12G?QbXW7tz85mHPKZ06~kM^atp|$V_$6 z9RwyICr>a&+c{=4)4O!dMiiF_*=lXqWaJP^Pq%c2F)%KdD=IEB2*fppJ({JW!hCSx zR1c7cjg5t|Kb=N)XGI7K2G|2hwx&XI-=^8p;!Sg_%6C^Pt(()n&5f0 z$`sg=AYSA>AosQbdR`Jc1UJH_15~>MscI&?FbltD1-v>7zk3C|EDOKO3Ri!gg`dvC z)7%eThr6`L6zlIsv*Yi~j^}gwv*RCCkWp(qzxrZ!{H+|1Rik0KP3*Vcg;f>a=N8+H zZMBexEufinb}elo0kl|l5bhwL!vd%$K;OndP=ucVzr~xXuZn_ZWXdPvJ7~=bp-}=} zliOE~;%w||bqv?n;K+w{Ae-abK`ZQ7LglAha4ADyGJMzqSX#i6c+#|Z2_~MT0K7!nKd-aF_P+EPa5^G=q*CHPW$8Z6^I_k7vL4 ziR|}CZx3hTpIiaICky|S1%AKfKA+9P`TGW6G-~9FzxDf{%YN@Q^(~g)|GWjB)ZUf- z{U>yICGuJSL-4U!u(Uf0B|ibKy2SfS_&pYQfkr--@ViYo?!)Jm@Vl&V!N(GQItx!L z{~`F8aJ-ofJ5OU?<@1n_<@h_Z$j&1@0|lyx?Ow{#K3$A6G87@%~uoS1BHL zrZxDu6?{CJ4Sp zzp#7J>j{Q7q!U9ZNZ%~XVPX7!U~0`nA4epuYg*|m8CA0Cmsn*`Dt1D`Lx|Ayn)W(pfyM1SYuhu&~ zT3l+ctTyIb@V%VxdoA?wN9>Mf(9L}o_ZK-&)Jv?HF|fw>eT_lT_twUNJWYuzkENyy z?V}r7ON)G!nc>;>S6;g4(zYeZC`Tp-=e(}aLT_EVyZ+(FwDz9ZX#H%*k+GS*epg^| zJ?VYauKGg7Wol)$a(H6Es}+~mc)XQGj?B=^=!T6Bd$hEeywldx+}PM$67G#}TCi6Z zC%WnrBk2_SJ#-3M6>}_P`+o`=zr*%P0f+9U{6@g(pMma1wL@Q1%YZdvCcLd3fBQ|Vg0RCs=2wCOy5nWH5c>!{qFzQ&`L zd#fY9Xh%&`ue;qz9N>h~qOTbona4 zd=2(C`=9pPKF2VKQ5i(faZa?(T$glSGA)jrh00n^$K7AxW3$1;D8&)N!L|^2P~%yz zjtRhY$Q>0b@i*C*f|U{hTBLN*)5TgNm@M?7bZaxv%TyMEC`z?tw{WQ2$hSdF0sci<1!@UIl7sGu z1}liQSNU|Jy>0#Bfqh4!>4o<0iQ>GxSk3u$JL-l5A1D zfA7v+H*RmaHH_GvMX2-v#{6XlAa{QypxH(?vjdJ|09548@jrWI@G^SM0ojGos5FEqwNVA4X zx7|$sG)sGR=c>-1P!olg2G+s-Lpa)TjN;%zv=;~E!ZkPytFB8(NiK3SQ`3)yP447^ zN&^R6HGZs#e(IBqYI5fte4uBEP<;*cV+|Eh2mCyKxYG3xW>EsZ`~sl-s|v{w#96kO z!FF#pCN@iEZxt$IJ47^1$39UcC6&isv??(B+sjzJToSbvDx}zcNiM6uY8xNwSN89Z zuRb^$9PO%q&&Tw|&l`=@mql8u3Ot^Yn!T!-UDQ62xX5A`bc@B)zaV_ToZ#9j`Gi zz9=vFFlJz;9oGm@=7Szr23PGQvJikAK`o47N7=lh6ld9rhyjt1P8oC>m>Z}WW`$lE zM$4j3eL6VW9nMZY^qvcjfFSxTGwy~))3j-)!&@LGIh{Ijm{Y&UI<=5Z%fDD|QM-f; zD#GhHwR^bYW#P0%CH$@x@L(4HP7KeE7qUiLLJf!c6YxxX)%`5kd5a3uJR`wY0WyOz(O z+FyYpW3&C2S^#fkH_L!gU2egh9@sK*XXP5kwvbfAr)Hj3%5??Yq?=E|Un)GIkH960 z-^Klv16=nMCii09BeCf4dq{IS{2mMZKCXK@{B9kN3M1td*F7D6mu39bT=#VNX&qj@ zjK&b^uRud3;&&grBKy0xWew|_KK@S2eKv4?6L6RIsO|IF=e&^Xn?C+ljz_%bFVN+2 ze~?AH?NQcG8vVnweFu5pY zq`Okgp(bY2bc8<-$E35gOfDvHOisWtsn2qS#R;D4b1+Ci`46ijT%Y&9<2s__^JWA8 zh%pQTpK`ujwo(NK0LKG%^GEdTqRoPqiFnG#ZGXa9%BfX&#xnN2DnYs&mm5P)>8xcw zquEGDU&E})lS1(}Efuz8aELgk5p!llmicCA4W43IDD$A0?29+j6c{q5TC@D9k3B&L_YVtY=XzgNV&5$@FPAHWH4 z4SUo_Ebzy#l5q0>2Zfy`?{k~2Ci7|F0xN{F8SpaO5e8{j;zEBhQ$(97g6so@qi?Hg(E3~{VHNMpF-`{X*(bPDz*yHehG9N6w^?%tQRG72EF8H&U#|1 zNkG=nnV^>tWlu=0?T^k|A@h(u&8RW89Ly-MG_xdL5& z=%n9RpVpIida^Wk^ap1T|5u%9y$Kb?07yW{pE7}qsp)` zULDlxv*T~Kju-Zjyw9zK+qC8Xv;1B34(f(^(1^&=yJ_*toGzK2$-q7{kz!eox-O#3 z=`?#c$g~`$80P8P1yx7-D(F@M6@fd*RGqd8aYd=fEpV8YVEnZRD}F@4qSY5qh)zZMm?wNmXop7rT zho-3CM3GwSSdr?qBc!FgSXf`YwPk4P^6jaX*}iRw5plKV)Kt2ly3(UMeA>5*N|f^^ zNAa;t)qIZcxVeNw-U=KFIXnRXXof0cI9G_8U;d6Vv{sph> zx`>!dL^f3%@#aTc3PM0%sx|FKa!hNBes&)_OeDE4+?#0aUdzk<UM2^JcSr?{nGjy{5iN*nskWpSQqkg$*d-PY@0tJYw24TVS)B^A3S$HK?5R8 zyqP>VDfloA$fS6LwDdNl;a5OjwDdNlA^MJdTLt_YlB0oS(_kSR zeNh%c&JhR+_9k?}Cy$JM&`AU6L~f?Yj#edtI%F3GN^Y;MR+rZuxTlodag38rE4NkFI>kU2u{F%o@zYnTSKwtQ(=N9^Iv@QaVw z9^`^6yPq30(Mpzclx8lkA^bR_^xdXk~>DqQsl;ZlY( zcQ4?+urjTTJ=YBtU=TN_6^~+*H{7Yf5D2WOVx)oE9Yb<359)QQQ4iI0&n8FqRIl@O zhhxb~^ioLe-Jz&zXZz4}@dYP0ul21}D{ni}zP6_}uXy5uKy@t9`H9Bqu3#cQT^mRs zL{wbn=}ArO%ySf$6(pMaC(r&~`M^^Ck&CNZ(~Y(9>4EYZcbOL%ufSKGIvS&xQXAFcdy_l7kmy*F1Xv%XJHPc_XQ8K3c{Ce{?^ zj|^0lzyIqC>h_Vf=Uu+%o5gdh1{WuzOUb@)Wjwm{WJd1Q1pc=vC)9W1j0|;@0EqTw zz=7CbiHXK~osmg7Ic`nUP%_+_`7pJl+dH2NQ3&=^I}R=%dBSrg>yx;S5fifJoR$^y zdYRxS@_Hln@D$)BWnm4M6y1Y`1mBe6+Hs+(mJgSXl+St_g5{2~<28qUhbwlL?hGt# z&nquHG=F5NDzNkP(vkTaAcH$$H>pC$XWlhz$hEK%0k4J}cr&_Ev6xlLfx885_#lG; z$8)1!MS<>V0bXh_OJcHhABu?=D+4vXt{j$BP=XnkX}WZkRA8p*pqF(tqEp`MNL*%o zAbF6JuVX=Ux-XG#7aiUp=t;uwvB2YEO_%VyE%17=4oUc3 zR=BVkCH!<2o>uFHEVoutKUJ7k8K~Bb1BNrYxbg~PNUJ5N}fjLpKbyFj* ziZS6L-iC4(NI-1b4DJge-WEi>Er|F=5b?Gk;%z|@ZK_hx1CEMZ*&_<7E+_Lh~^@f6RT9c)xeWd@#pR!f(#b$7|}_ zVNWJ(S2Ch0t1XTB(7kge`gP=*fSfhT^xsJnl8FUX!iy(M!NZVTMoBo$OxDeulQAHL zQECO6b(0g=RvwPFS`l^9#Oq@Dg}%JTmb~dS+I+0QjTM&{?r&D9E zypTb1{G&?994~w&a{SHucw~+0xl9+uVxFpm-)+M6d>skDO^5#k<43jU zFg|SKeV)D;C)|hdquNKa@UL3n7i(Y0!k@Fiw`rfu!k=9Me|r}GWdja6997TgaN?&t z`@5ssv)S>_7~{orn&Y2W-jN+YrCpL8{}jh#{rw4K@g~&w)Z)E9c?KZqjRk|AyY`xW zKg{N;x}KtI{-yx-P$Hw7Atf@MQmRZv^+4!)fN&3lt_MQb1EK4I(Dgv*dLVQ?Lg=z( z0y+vo=oUfflB`)91rbcO2se{WQfMEr^&VPPzn<_#e+gHFwI#~zd*{y)v`9nm^!{%9 zjmigIw|$Je4(7_$PkvHemHD1NzoXjEvUvNt1<$+fkPkXNe8U1iPrE4#f6@X!Zr`lK zUD^xAykLG!`f1ciKl&_Z=k2(CKKs2FjqhRdj@zfR@Ncew_hsSVvcP|0`Tg%2aQwbW zkBu7X@!a2Euzc@(#`p01Pg#EdB@6t7J)Qmi9}o^Y`v&CEM?hx`58c8(D6(@uf*F`m zdzRL8i^!xkvx$jLYo;rL+M-==?<_Xm1Kl8^4Z3k5@3Ro5k#t2Aq8~+`fYUm5iz^vA zA^ory37Ruz|MQ}af^zz6$KH-jxU2n{uSOMLMdmkAWuhW8vge6+4u9ek>X~DoIQEan z)Va*Jl~&3C@otn2%7b`Um-e*sR?eNR4{OUcLS{q&hs?MPr!1VZLBgLS952%cUc3gi z(L>CmEI==w5zxJ{4(rW!Ow8ydCW7gD@M1RcVyx;Y(66K;mxU?iSaB0LqVUKQsU~_ z6*#;-3;&W1hqzF$7jlBr&oxf z+?uM)ExJg`UB@iA;+iCJ^`r&2Ulp=l;`X}+ZaGJqG1piVFkYW2^M01^eb4wF=IT+g zCdm7}WPvY=H9^9EKsfk=^6@^%yLy-$7c+=w^u}Usu^GqG=fs>f)8JrWiu{<5GdC?q zyqx?fy0M{TmWbwxKoh^zmM;eY#y&(wCkL>e(ZzvvE+{~yzmOSey|Rac($T$hqS0y3 zLn{k)W>x>Dx{JyOXLqFf_e?aVc3;2ho~F!GsygAWpE@{I(OlkGQC~DRH<<2dn(Pcj zrw@$ndictj368@|KN6h_x^%=~agg>qz z;V^q2@|T2viQ|FO5dQfu&_8?{N(m8DX&?qTt;otb-KaKks(T(yK+eLemqKB^q5MPp z6+`|ps-qyP1F^>_5F$NjEpcuGRsLCsNTXSf7`q7QQI2ITGTE1jXMQnb+=Gk#rCDx0 z=E5CWiOlp?2b(ak?}P)z{OC5z59|V4$`6pc(%5It1oI_Wqs^*v4?JE9oc z_>68v%712F9z1n$m3LLysWPPo|4fxlA3U|GBJ*;E63e`#YJ0Zhe|yfJRqnm;Li`x; z9{`@86!S%c&ZRwTyFtMD7GD$cmc}*YZAOkK{4ZkOCH$)vct)&O68@Y8{<65ggg?6i zPI)WgU$(%{Xru=T|B?YmzStQdZwUt%+wRRi=NVx?$nnn@1|KHw4b^E}bZY%6>?RRBaz&tHGU0FU7#LA5J)F6ebj{~|LVFIG zXitCHW{YW0+x~!8b1Kkr1s&M&_i{nssLvvOuQuUHL9Y`2tPT%Aemy1VuNecGGgK$; z1O8DyEAG=G?}J}6_(~P|RuXyorcMoGsQ^vbKDR9sx-sx!@lK--@ zg*upoKWD&!OT1GS{NjU5=JGR=}y( zNcgiW;FPHn{^cw@P5mSbe@tnz+@HFt9REyqJoFhMJ0<*inj{nMv@zi1E z_@@Ym;M10$Q~yc*1J;-+?P9TUE|a9gVohg%Oas=Te}gpS5fOVj?2W5V{To!J&{~-#Nrs__9ZqC?9#W~? z=U3RD)#WwpoS0IfbhfMyt)JTN@pdg#%bo{73E zt9B0^3a#(h{+^Sotk>A&sICa%-3!C#@5y}Q{5=NB^YJJDy}-RSlGQnWVtl9@9qDqjDy;a}-oPo+(Uoy1ytmcn(0AFf3Uk zWH&=|jZ$`tTq=n$v#|pDD!g84m-;7Pers1CP}jBRx(%rvQ*k1&Id*+T=CQf`W#x8t zVpKnCjZ1Cu$l@?@7M{9bhG+MMXVV9E9r6a;ldFxh!83mnxcVh<1-TACkc6jI-~>1% zoK^s2jLu_!qW&P@PEGee3f}t@c|aep`yX{UcuXJv#q9lGQ~%4pIm-jz&GHO*Ppl`L zqZsrn@&9}q5W5ba-JgH*!?c|WsJ4I6|3 z3UkhLI${3L``=o{Cu5CX!Eb(?jeTKcLpmxrOnIWnWSjqhxMI>IeFkiiN{1)Ord|*W ztP+z(D*UGP$ZAS&8~$i`c9eUoQwwdxeAnp$nRseJE@IxP_g*e!aVPKxI@ybpz6M<`^jHZOeiy>AW?e4U zEW)||SBmjA=n)&meNys1Bl12HPT4Kt&k-*5Om!pXX_}pTy~qQbfo)){Wur9XM{OoY z>OB|)1C9c7Qmtf>0;V4=-5&_W2n^C~cx-T=>ggKkF#R zStfS-KoI@h7S(W_?Hig*Bs$mcii|G~ml?tGm2R`?i$Z$?b3HS&$$0nd)`p3_L+ejQ zc2-ZS3#pb!Lw7*QF?or)!)xl7pgW-T9et6qQ?h~Y9O5@C`9aOh=rFysvMD7<1?Vh< zktT_sK3SP$NY;?LEE~@IpfAX(V9^daEljp>iCTy94jGU~akui`w%&ZFJ>GVp{8O2q zpEz}k8mRoJ5*}!-svlh(n%!Ns$J?=(idR;-LXC$$eD+i7nJeDsP%GktvE;7Rt*O?T z+K_n1jiBXYc*hdkMQk{L*o1|q^t3CJru1S9K~pl}it8u0aaDzxYNzLcEo|BT-E4Qp z)C;DYxIttzMvG{O>UOAXm5X@hzM)e?cY5!LKUJY*DxQqr`%9U(o-cgRFVb$!o7uU-C zKEue6mNXT;O1#9m8GB5)qIrc)LycnmZ&_1DYQiVv>zxR_ZlcHivG(- zalsFwV)n;`vUGM?{u!;}j0>Ci6MGgIa+{uEq3pW!Rs?Y7icBhzlh&*RZaGzb{ei=c zQ;P%dujzcKu{Y0wF6a-aXRg_|xp&ui!_9w*H&xf=S4YIW&Vv6wDCA?enD3{rlJhO$ z)OjWRt1IA?n-c!q3OM;*!k=9Mr_L+kU(UkQ>IVfq5zh4%IZL23`McD4<@jf^#CaF_PH@-73PfP)wG@n1B@s~@zxbbPwB?^|hG#AJzQ zm-g*9fPZHNoI0)?|Ia!cmHpbc1&{ZN`#f#?i+COhC(lawvn$}_SqcC03OIRF!k^LM zi;$fk75DF#c-t)TCgHrlggYu#Ug%}?~aornq6du`D#%2jMT6#GQlZ4+8l0hI28YL z>bEjqD-IPh8O%RWr<5TZ4EaqZj*?aVjgh-(OsUI%#y``Lp$e)vf~HFm7R26l!gPx|~Q z-ZVS{OQP(26yPLZ2tWrO$`S5Wx;3agVXsRGEB9vlDwWWKmC9EtGhev31Yz5u=Pp-f z?aIXE$1-1^$^1alDD#fq1v{qGc8IldvX{Prr<^S$I!SPymiVM8i)G(^1ya~3!I$&Q zUK~~8q1lRuDhYC%4abZOZHcOUq{fS){5F)pv5Z2{soxcGzYB@*ox9$;zHi59q_T5; zkNPhEe@FUf3d zjbK$5RyC78)!xabBr?aa=V#s8&C&6ly>oLedn|V2`|1WW_Z0f8YduW``SpRwP;s#{ zoNS%w9FLXm-o5U+y*(Rt%+F@dzArz&B33suJ(x(3&(sB55EF%LWZx(DEp@_xATLvv zb)Xib8f(U_%z`qiD%l6h?7F6NmYElw(`eQ#uaxcuZm!0Pss_C&NEgkPWdLMHCuyb` z9c7p~-y*)iT0=(s5Uc)NB_W0wyVWodK zG&r9cSnYBkW4gC7(OKZoqLHTY($T`U{?urJUE7sV&R%)>_KhI5P`<-GJK45%vZ=2% z^JDeOwwCfhVX$F%F7s69Ku51HP_zKq(*#__A@{wuOX$3mnMDRq=@>GJC#Tpzr#DmX zTWtp+_w{r(L0*(jD4&9KqVbRqL4YU=^!$pTiq!GmloM!)JTNfen`~^>JcX{FU3J$) zZaf&j0wp31kq6(q_1Oz9%lz@}&*}Woj@vQ}1uV2NR_Sj3|VLCmI8uvi4$GylJiN?QzjX&Gj<@Y`a0YHU}Itw@cQcXC3N zH}6t*#CyU;@l{I$yDw^)-qNgGTBW?%`>yg}dR=j0eoIU15Kii~dG?vm;P6<(^vUfh z6un(E8+RVLW-0Sxs5|Uh7;o)E2fFl7a**o<_knOh7IZS#DRPb+%qjmcgV7x9(__yO zI1$iS4djdsg+$e_to5iC_~1k(aRrBh;W0z3-;jiQB^eyA;ORjHO$1mIuB-RT_uJpR zsdKioa`m**oTwYv)_?8fI{#3jv(u?4b@fNeE{nC{Dv?U9itHWeEN@8F_Rr!LpE9=|KiYf<7Ys;=|y4)SyH&;J4HQVw|M;@O)2tKRAoJCo; z??1;Ul*4Db3&>==9JbOO>6S&Gbj&0@xw%v!&U6>isunCJUP=^bi0#wiMWL5XPMu2& zZXWSLZ&FmvsM$Ys@_e(HAN z8&kUv=6k9_$WPgnYMbe-wmTd{-hYdCuXorn6q{qInsQYw?dimf!O63NbJ5&MBC@Sxii4RToY+)mBh(PE&4`!^_St85e{LPaj+9WKAkk&N5xrK<8~Mw+;K@TL zldg@I`QpLR@`0O9-}2+B`_+P9ZawBLUV2OBJ9ysZ@8X}A!S6mkuXaxSRd*-m@Vof} zx;7|Q9kC4Q3zYz zWzPK3E3Z}J>a|Pp$l0&qpTEER?wObhcCTSc$@!E&lx-ab*FsvAap>xM@2?D5X%Y9V{;w{EOVPPZwGUG3@#pK`rd zy|y!z=PR69>TE433$=L)4Jlk$yJvNDv?FY{*KBK>^5iXI0o-Uf2_?u zb)DQRdcFfum%Kf0rejn?Gf^~!YE}lS3{lcm#2r1F*1;xq-T$Mx3{cqX~jdInc^KDC)ZP6QqtSJc;S4B$sE(**e8~So0^WCOb^T66^DL?_Y5ukQ{CFHB~FzzOVuO_X$Cx?Y0}4@?qHB zVa&6cMUwNB`T}G=r4?;L&_*a%p^(cuA7jhALL>sRRiVVD+{J{(QW*GU+FO(^9ih}< zD^&C;Fz6zy;i#y|@EQzD;{T7-x>U8leO3ENdym^~S3CM+)kn@-x1o9d$_)*dJw7>K zT4L9FgV=euu4_8pcfsy8RU2yNhx^JZ3llA0e(M#xHf_CWZ|d_(aC+UGx4vkweF)^T z{Bhu=1#{+SOxdny<{5LAQ+u6Lahzi}n(0uy9U}d}fz)ldY(m2jI58@+g{l{Xb1kLm zD>Zy<5ifR{x+C7~sIfnA!*5el-nEUbj#|efH~g_<%BvPco6ab%%sVrGT8bvVp817x z@{dcP{cgO+Gw>-zIJ@=d$e^4_)F0b)#_8K+=1j9`5SywRrj*eYYa1E*%%mJMwphdL z_Ubxo)&YtObK_grHyfH)rD{mFeNKB@$5oeI6>P7s%I`lpRMa-TINb2u=c|)rG16~w z<`>m<#Vv6(t-GTrJlLw#W_~cXb0F|}yf=L&p9Wp`F!wZoC9u!J^ZFJ%=3C|Xdvi8S zHl1sd30b9Tw84%;M#_wVIQ95h$V$Wu^KDo&grbzs4Lmz_3 zGHTo4?`P&0eNc?G9v7eMv!4_EtUfHp>;7eYhB>Q`XDx>l3p(#7{P^NC{POzWnXl*s zUYOU$yu1fNr(AEEx&Natn{C4thYJYLP1%)4r(Kg&O#x)&&QSZU6Y;$`lopv>fEi*= zdK@|FahV@}6c<~!C{TOrC4&QgaZHE6y&%Ytda&j4R^uLwK z4a``>67G!@D*V5q{BZNtbrsIb@4rDg!g@)i0Rf2R%-8UH`m>-yn9tH`d=`1CPLYqQ z(W8kyKWPHFp!^-AaFp=@nN}-vCFt!po>!k{bKX~If|zl$ET>TLm|2!XwUA{wkgiZU z{CalO?aE93Ow6ww@Mr#hkGR$=X2kP~d{IbHC#KuL?c;nO^%L38%wclE+90m~OMjn2 z>Kdy4F2=LxQ5mLFyn=(-6qJ+vBYRmg=2WepYeiUD*2zg9J=sof!pD@n zZeXjy$bS=i+8T_AbPHG|+2prDH7jnpt$NJfyL~j$xp?z-&3(#k@7*z4-?4b}4z2jq zTeM9V`3h8RYL#|X_G)XWKE1Pl+g*nThlY%+8Fl}zrDSofXJ*_uXXZdJ%Rd4=pehvf z(MS5Q9e}Xw0KEt4)vY@x-mbHrN!$9mWCHYM$OM=R6;)gGtq?SCgUd)}pOgW2)URrZ z8zG+9z(wefa@mz1S?HQhZS1Qnik(X2X%}xgwJF-)5^}ekYOVYG+Zs-NCfGOE(lPQC zEY78emHRX2m$j_!{o%jeeamO?j(Xe*JOOzF`NOsM9`+YdWuE>6zsIBLaO=Kr5ByHT zg}+yf?*PE5J@8uz7d}M+mw9OqF#Li0(3dnX;4)_Zz%sC^!*66C0rZ^YeNMkejz`vv z@~~z6gUjF)`8+H7aX#>`a=g&j#rQFCp9fyGz{UL~{4~WY?!)nImhlh%Lc)a(BH&Z< zK5GPg`6r->QNp>lB9{J7%Wd*LfIq^%zqW?e0-ljEf4q&BFW_<90pm@$8WQlP^Q~~) zRC$>>3tSI?2cr&%&-o=WKAEtN=lxp*y#JIHE@Se!v3;!A>hl;MlTTv9ym#vOG~-mj zOC|cwai1IIanYm6|1M&p_P=Uv*VI?d zpp$HZz~n?WL4brk3RUfD91zP)UBhlHsC$cxNQeucnB@cm9;hK8JG%=i9R7+MzZ0mz7JcQutAT?~&_WFU^ctvn1C)ggwn5vFcsTa0eed;&fSO(}Ok>^#-Y$6?$UwQxdO4t`%_)5l$WRm}ywL>Cj97|}rHCEiRfWQq zH8$ZLDpCHtcu2Dk_^#R0Jd>*NCkCQQ|7dL0-emhwe!jMAsY+Gbb|eS#oQm2U+fr8P zUfNL-_9#sUj#alI&ua()a=W@=s%OtsY=1|4S82(rs|(BW60OR%uI4~qu`ATD<0 z(undk(1^o^P@M(?k^5@AAw5{LG)VmTg4A+gHJ(!DeBW9?yXz%-%6V@u`10$pjT&w9 zbZ)QAh0#qwlh#aTld*HUFuZ!4+}Oj88|Aq5c#OcTr7M=Pkpn$zy4M1B{S))sz8Gq4 zk3}LQk^b)e!}Hq@?F((5!y+B^^>!R_kEZ`F=_+grR0e87v#qi5k;$bBmoJsd^8|dW zQ`EKOUOf)bP(7?Xk%=s=JeibXQcg~cG{@{UUy1Y2G&L0*bA?0EH*dS>A1)oQr4 zCtR_9$h{&cZ!s78`o{fkoKX5P?_8&~CeTzrcMWL=+YMT1D#stGXA78wkV6^$^za}` zmF=yi+tntZgCjcRh<>0>MxzZU@)@iHh5CMnDJi>B5LqcstYZbw9FSbBnP9bmr5qmi6LKuZ)tjS zAn@Xg#}_5U>#ZqVTD7@2TKt0_49$0A8y$h?PQ)L?E?wv1Sr6Tsax5q3Ajjuo#iCdt z|0d#1JTvOg9fOJIqAZ>bj|?lH0~jIKfxm%T_{f>VK-@xQT#4J)L+u{o&}Db5^|g); zYugqsS=grIwifXG?Gm-yTi@KWrMRJeAi73Jtv668aH~9;*&?tzk{I?y%LQ^ve)z+o z%|Nc8<3F>XE#y}_*Nu{M;yb$g$s}8oxhY7Igb$*ZmEiQep{bEzaSv9DQqdcdo+^@? zY(LL@AQsu7JrS;i8WHQwX0KpZYBPtjwMHtyVKUZP`P$E!R)A3$m`(b~~p%+gDWZkB$

e_P{_uX|HTomcVr^IN-p-HmOM*a@T1wz@uTI? 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 - EscapeCloud Community Edition - Cloud Exit Assessment Report + Executive Summary – {{ name }}

- EscapeCloud + cloudexit {{ name }}

@@ -68,6 +68,79 @@

{% endif %}
+ {% if summary %} + + {% endif %}
@@ -177,7 +250,7 @@
{{ timestamp }}
-

Risks

+

Risk Inventory

{{ total_risks }}
@@ -202,7 +275,7 @@

Risks

-

Costs (last 6 months)

+

Costs Overview

{% if total_cost > 0 %}{{ currency_symbol }}{{ total_cost }}{% else %}-{% endif %} @@ -226,118 +299,13 @@

Costs (last 6 months)

- {% if assessment_type == 2 %} -
-
-
-
-
-
-

Scoring #1 - Exit Readiness

-
-
- -
-
Exit Readiness Grade
-
-

- This grade reflects how ready your environment is to - exit the cloud. It's based on how dependent your setup - is on provider-specific services and whether mature - alternatives exist - the lower the dependency, the - higher the grade. -

A - Seamless -
B - Smooth Transition -
C - Manageable -
D - Challenging -
E - Complex -
F - Very Complex -

-
-
-
-
-
-
- {% if scoring_data %} -
-
-
-
-
- {% else %} -
-
- -

No exit score data available.

-
-
- {% endif %} -
-
- -
-
-
-
-
-

Scoring #2 - Vendor Lock-In Score

-
-
- -
-
Vendor Lock-In Score
-
-

- The radar chart visualizes alternative technologies - across three dimensions: -
- Human (skills availability) -
- Technology (maturity and vendor stability) -
- Operational (ecosystem and support services) -

Only where viable alternatives exist. -

-
-
-
-
-
-
- {% if scoring_data %} -
-
-
- {% else %} -
-
- -

No vendor lock-in score data available.

-
-
- {% endif %} -
-
-
- {% endif %} -
-

Risks

+

Risk Inventory

{% endif %} - {% if scoring_data %} - + {% if summary %} + {% endif %}

diff --git a/core/engine.py b/core/engine.py index a0bf940..54233c1 100644 --- a/core/engine.py +++ b/core/engine.py @@ -16,9 +16,11 @@ from .utils_azure import build_azure_resource_inventory, build_azure_cost_inventory from .utils_db import connect, load_data from .utils_tfstate import build_tfstate_resource_inventory +from .report.context import build_context, collect_provenance +from .report.executive_summary import generate_executive_summary +from .report.html_sections import html_sections from .utils_report import ( generate_html_report, - generate_pdf_report, generate_json_report, ) @@ -421,6 +423,7 @@ def generate_report( report_path: str, raw_data_path: str, upload: dict[str, str] | None = None, + sync_id: str | None = None, ) -> dict[str, Any]: try: db_path = os.path.join(report_path, "data", "assessment.db") @@ -459,30 +462,32 @@ def generate_report( # Generate Outputs reports = {} - # Generate HTML report - reports["HTML"] = generate_html_report( - report_path, - metadata, - resource_type_mapping, - resource_inventory, - cost_data, - scoring_data, - risk_data, - risk_definitions, - alternatives, - alternative_technologies, - exit_strategy, - alternative_technology_organizations, - ) - - # Generate PDF report + # One context feeds all three reports, so they say the same things tfstate_scope = ( _load_tfstate_scope(raw_data_path) if provider_details.get("tfstatePath") else None ) - reports["PDF"] = generate_pdf_report( - provider_details, + provenance = collect_provenance(db_path, run_id=sync_id, upload=upload) + context = build_context( + metadata=metadata, + provider_details=provider_details, + resource_type_mapping=resource_type_mapping, + resource_inventory=resource_inventory, + cost_data=cost_data, + risk_data=risk_data, + risk_definitions=risk_definitions, + alternatives=alternatives, + alternative_technologies=alternative_technologies, + alternative_technology_organizations=alternative_technology_organizations, + exit_strategy=exit_strategy, + tfstate_scope=tfstate_scope, + provenance=provenance, + report_path=report_path, + ) + + # Generate HTML report + reports["HTML"] = generate_html_report( report_path, metadata, resource_type_mapping, @@ -494,9 +499,13 @@ def generate_report( alternatives, alternative_technologies, exit_strategy, - tfstate_scope=tfstate_scope, + alternative_technology_organizations, + summary=html_sections(context), ) + # Generate PDF report + reports["PDF"] = generate_executive_summary(context) + # Generate JSON report reports["JSON"] = generate_json_report( raw_data_path, @@ -510,6 +519,7 @@ def generate_report( alternatives, alternative_technologies, exit_strategy, + provenance=provenance.as_dict(), ) return {"success": True, "reports": reports} diff --git a/core/report/__init__.py b/core/report/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/report/base.py b/core/report/base.py new file mode 100644 index 0000000..d8c5d9a --- /dev/null +++ b/core/report/base.py @@ -0,0 +1,223 @@ +from reportlab.lib.colors import HexColor +from reportlab.lib.pagesizes import A4 +from reportlab.lib.units import cm +from reportlab.pdfgen.canvas import Canvas +from reportlab.platypus.doctemplate import BaseDocTemplate, PageTemplate +from reportlab.platypus.frames import Frame + +from .cover import fit_logo, logo_reader +from .flow import KeepWithNext +from .fonts import FONT, FONT_BOLD, FONT_MONO +from .palette import MUTED, RULE, SUBTITLE +from .provenance import attach_metadata + +MARGIN = 2.5 * cm +HEADER_TOP = 1.15 * cm +HEADER_LOGO_WIDTH = 3.4 * cm +HEADER_LOGO_HEIGHT = 0.75 * cm +HEADER_TEXT_WIDTH = 16 * cm - HEADER_LOGO_WIDTH - 0.5 * cm +FOOTER_BASELINE = 1.15 * cm +# The body spans the same 2.5–18.5 cm as the header and footer lines; the +# vertical padding keeps the first heading clear of the header rule. +BODY_SIDE_PADDING = 0 +BODY_VERTICAL_PADDING = 6 + +OUTLINE_STYLES = {"Heading1": 0, "Heading2": 1} + + +class ReportDocTemplate(BaseDocTemplate): + def __init__(self, filename, context, cover=None, **kw): + self.context = context + self.cover = cover + self.allowSplitting = 0 + # Every page starts in this font; the default adds Helvetica to the PDF. + kw.setdefault("initialFontName", FONT) + kw.setdefault("keepTogetherClass", KeepWithNext) + BaseDocTemplate.__init__(self, filename, **kw) + self.normal_frame = Frame( + 2.5 * cm, + 2.5 * cm, + 16 * cm, + 25 * cm, + leftPadding=BODY_SIDE_PADDING, + rightPadding=BODY_SIDE_PADDING, + topPadding=BODY_VERTICAL_PADDING, + bottomPadding=BODY_VERTICAL_PADDING, + id="F1", + ) + self.cover_frame = Frame(0, 0, 21 * cm, 29.7 * cm, id="F1") + template = PageTemplate( + "normal", [self.normal_frame], onPage=self.header_and_footer + ) + self.addPageTemplates(template) + + def beforeDocument(self): + # Each multiBuild pass draws on a new canvas; the last one is saved. + attach_metadata(self.canv, self.context.provenance) + + def handle_pageBegin(self): + # The cover page uses the whole sheet. + if self.page == 0: + self.pageTemplates[0].frames[0] = self.cover_frame + else: + self.pageTemplates[0].frames[0] = self.normal_frame + BaseDocTemplate.handle_pageBegin(self) + + def afterFlowable(self, flowable): + # The outline is what readers navigate by and what the structure test + # compares. It is added when the pages are written, see NumberedCanvas. + if flowable.__class__.__name__ != "Paragraph": + return + level = OUTLINE_STYLES.get(flowable.style.name) + if level is not None: + self.canv.outline_entries.append( + (self.page, flowable.getPlainText(), level) + ) + + def header_and_footer(self, canvas, doc): + if canvas.getPageNumber() == 1: + if self.cover: + self.cover(canvas, doc) + elif self.cover: + draw_header(canvas, self.cover) + draw_footer(canvas, self.cover) + + +def fit_text(canvas, text, font, size, width): + text = " ".join((text or "").split()) + if canvas.stringWidth(text, font, size) <= width: + return text + while text and canvas.stringWidth(f"{text}…", font, size) > width: + text = text[:-1].rstrip() + return f"{text}…" + + +def draw_header(canvas, running): + canvas.saveState() + width, height = A4 + top = height - HEADER_TOP + + canvas.setFillColor(HexColor(SUBTITLE)) + canvas.setFont(FONT_BOLD, 7.5) + canvas.drawString( + MARGIN, + top - 8, + fit_text(canvas, running.title, FONT_BOLD, 7.5, HEADER_TEXT_WIDTH), + ) + canvas.setFillColor(HexColor(MUTED)) + canvas.setFont(FONT, 7.5) + canvas.drawString( + MARGIN, + top - 18, + fit_text(canvas, running.project_name, FONT, 7.5, HEADER_TEXT_WIDTH), + ) + + logo = logo_reader(running.report_path) + logo_width, logo_height = fit_logo( + *logo.getSize(), HEADER_LOGO_WIDTH, HEADER_LOGO_HEIGHT + ) + canvas.drawImage( + logo, + width - MARGIN - logo_width, + top + 1 - logo_height, + width=logo_width, + height=logo_height, + mask="auto", + ) + + canvas.setStrokeColor(HexColor(running.appearance.header_footer_color)) + canvas.setLineWidth(0.75) + canvas.line(MARGIN, top - 25, width - MARGIN, top - 25) + canvas.restoreState() + + +def draw_footer(canvas, running): + canvas.saveState() + width, _height = A4 + + canvas.setStrokeColor(HexColor(RULE)) + canvas.setLineWidth(0.4) + canvas.line(MARGIN, FOOTER_BASELINE + 11, width - MARGIN, FOOTER_BASELINE + 11) + + canvas.setFillColor(HexColor(MUTED)) + canvas.setFont(FONT_MONO, 6.8) + canvas.drawString(MARGIN, FOOTER_BASELINE, running.document_id) + + classification = running.b["cover_classification_value"].upper() + classification_width = canvas.stringWidth(classification, FONT, 6.8) + canvas.setFont(FONT, 6.8) + canvas.drawString( + (width - classification_width) / 2, FOOTER_BASELINE, classification + ) + canvas.restoreState() + + +class NumberedCanvas(Canvas): + # The page total is only known once the last page is laid out, so pages + # are kept until save() and the counter is drawn then. Each multiBuild + # pass uses a new canvas, so it is drawn once per page. + def __init__(self, *args, page_of, **kwargs): + super().__init__(*args, **kwargs) + self._page_of = page_of + self._page_states = [] + # (page, text, level), collected while the document is laid out + self.outline_entries = [] + + def showPage(self): + self._page_states.append(dict(self.__dict__)) + self._startPage() + + def save(self): + total = len(self._page_states) + by_page = {} + for page, text, level in self.outline_entries: + by_page.setdefault(page, []).append((text, level)) + for state in self._page_states: + self.__dict__.update(state) + if self._pageNumber > 1: + self._draw_page_count(total) + # Bookmarks name the page being written, so they are added here and + # not while the document is laid out. + for index, (text, level) in enumerate(by_page.get(self._pageNumber, [])): + key = f"outline-{self._pageNumber}-{index}" + self.bookmarkPage(key) + self.addOutlineEntry(text, key, level=level) + super().showPage() + super().save() + + def _draw_page_count(self, total): + width, _height = A4 + self.saveState() + self.setFillColor(HexColor(MUTED)) + self.setFont(FONT, 7) + self.drawRightString( + width - MARGIN, + FOOTER_BASELINE, + self._page_of.format(page=self._pageNumber, total=total), + ) + self.restoreState() + + +def set_pdf_metadata(doc, context, b): + # What PDF viewers and document management systems show; it follows the + # cover. + provenance = context.provenance + doc.title = f"{doc.cover.title} – {context.name}" + doc.subject = b["pdf_subject"].format( + document_id=doc.cover.document_id, version=provenance.collector_version + ) + doc.author = "cloudexit" + doc.creator = f"cloudexit {provenance.collector_version}" + doc.keywords = [ + keyword + for keyword in ( + doc.cover.document_id, + provenance.run_id, + ( + f"catalogue {provenance.catalogue_version}" + if provenance.catalogue_version + else None + ), + ) + if keyword + ] diff --git a/core/report/brief_summary.py b/core/report/brief_summary.py new file mode 100644 index 0000000..c172d5c --- /dev/null +++ b/core/report/brief_summary.py @@ -0,0 +1,378 @@ +from dataclasses import dataclass, field +from itertools import pairwise +from xml.sax.saxutils import escape + +from reportlab.lib.colors import HexColor +from reportlab.lib.units import cm +from reportlab.platypus import Paragraph + +from .fonts import FONT_MONO +from .helpers import ( + cost_problem, + format_change, + format_money, + severity_label, + strategy_name, +) +from .palette import ( + HOTSPOT, + HOTSPOT_BACKGROUND, + HOTSPOT_COUNT, + INK, + MUTED, + SEVERITIES, +) +from .table_style import data_table, summary_strip, text_style + +# List lengths are capped so the page never runs onto a second one. +MAX_HOTSPOTS = 3 +STRIP_WIDTHS = [2.6 * cm, 2.9 * cm, 3.9 * cm, 2.9 * cm, 3.7 * cm] + +FINDING_AREA = text_style("BriefFindingArea", 8.5, 11, bold=True) +FINDING_TEXT = text_style("BriefFindingText", 8.5, 12) +FINDING_DETAILS = text_style("BriefFindingDetails", 7.5, 10, MUTED) +CAPTION = text_style("BriefCaption", 7.5, 10, MUTED) + + +@dataclass +class BriefFacts: + scope_kind: str + scope: str + provider: str + strategy: str + resources: int + types: int + regions: int + cost_problem: str | None = None + costs: list = field(default_factory=list) + currency: str = "" + severity_counts: dict = field(default_factory=dict) + most_severe: tuple | None = None + types_with_alternatives: int = 0 + alternatives: int = 0 + hotspots: list = field(default_factory=list) + version: str = "" + completed: str = "" + + @property + def risks(self): + return sum(self.severity_counts.values()) + + @property + def critical_high(self): + return self.severity_counts.get("critical", 0) + self.severity_counts.get( + "high", 0 + ) + + +def collect_brief_facts(context, b): + severity_counts = {} + for risk in context.risks: + severity_counts[risk.severity] = severity_counts.get(risk.severity, 0) + 1 + most_severe = None + if context.risks: + top = context.risks[0] + most_severe = (top.code, top.name, severity_label(top.severity, b)) + + with_alternatives = [entry for entry in context.alternatives if entry.alternatives] + hotspots = sorted( + (entry for entry in context.alternatives if not entry.alternatives), + key=lambda entry: (-entry.resources, entry.name.lower()), + ) + problem = cost_problem(context, b) + + scope = context.scope + if scope.kind == "aws": + scope_text = scope.region or b["ev_dash"] + elif scope.kind == "azure": + scope_text = scope.resource_group or b["ev_dash"] + else: + scope_text = "" + + return BriefFacts( + scope_kind=scope.kind, + scope=scope_text, + provider=context.provider, + strategy=strategy_name(context.strategy_code, b), + resources=context.total_resources, + types=len(context.resources), + regions=len(context.regions), + cost_problem=problem, + costs=( + [(entry.month, entry.amount) for entry in context.costs] + if problem is None + else [] + ), + currency=context.costs[0].currency if problem is None else "", + severity_counts=severity_counts, + most_severe=most_severe, + types_with_alternatives=len(with_alternatives), + alternatives=sum(len(entry.alternatives) for entry in with_alternatives), + hotspots=[entry.name for entry in hotspots], + version=context.provenance.run_short_id or b["ev_dash"], + completed=context.completed, + ) + + +def _count(b, key, n): + return b[f"{key}_one"] if n == 1 else b[f"{key}_many"].format(n=n) + + +def _join(items, b): + if len(items) < 2: + return "".join(items) + return f"{', '.join(items[:-1])}{b['bs_and']}{items[-1]}" + + +def _capitalise(text): + return text[:1].upper() + text[1:] + + +def outlook_text(facts, b): + parts = [] + if facts.critical_high: + parts.append(_count(b, "bs_part_risks", facts.critical_high)) + if facts.hotspots: + parts.append(_count(b, "bs_part_hotspots", len(facts.hotspots))) + if not parts: + return b["bs_outlook_good"] + single = len(parts) == 1 and (facts.critical_high or len(facts.hotspots)) == 1 + key = "bs_outlook_attention_one" if single else "bs_outlook_attention" + return _capitalise(b[key].format(parts=b["bs_and"].join(parts))) + + +def lead_text(facts, b): + if facts.scope_kind == "tfstate": + scope = b["bs_scope_tfstate"] + else: + scope = f"{escape(facts.scope)}" + lead = b["bs_lead"].format( + scope=scope, + provider=escape(facts.provider), + strategy=f"{escape(facts.strategy)}", + ) + return f"{lead} {outlook_text(facts, b)}" + + +def severity_breakdown(facts, b): + # "1 Critical, 3 High": non-zero counts, most severe first. + return ", ".join( + f"{facts.severity_counts[level]} {escape(severity_label(level, b))}" + for level in SEVERITIES + if facts.severity_counts.get(level) + ) + + +def risk_finding(facts, b): + if not facts.risks: + return b["bs_find_risks_none"] + code, name, severity = facts.most_severe + if facts.risks == 1: + text = b["bs_find_risks_one"].format( + severity=escape(severity), code=escape(code), name=escape(name) + ) + else: + text = b["bs_find_risks_many"].format( + n=facts.risks, + counts=severity_breakdown(facts, b), + code=escape(code), + name=escape(name), + severity=escape(severity), + ) + if not facts.critical_high: + text = f"{text} {b['bs_find_risks_no_severe']}" + return text + + +def _type_list(names, b): + shown = [f"{escape(name)}" for name in names[:MAX_HOTSPOTS]] + if len(names) > MAX_HOTSPOTS: + return f"{', '.join(shown)}{b['bs_more'].format(k=len(names) - MAX_HOTSPOTS)}" + return _join(shown, b) + + +def alternatives_finding(facts, b): + if not facts.types: + return b["alt_no_resources"] + if not facts.hotspots: + key = "bs_find_alts_all_one" if facts.types == 1 else "bs_find_alts_all" + return b[key].format(n=facts.types, total=facts.alternatives) + if not facts.types_with_alternatives: + return b["bs_find_alts_none"].format(types=_type_list(facts.hotspots, b)) + key = ( + "bs_find_alts_some_one" + if facts.types_with_alternatives == 1 + else "bs_find_alts_some" + ) + return " ".join( + ( + b[key].format( + a=facts.types_with_alternatives, + n=facts.types, + total=facts.alternatives, + ), + b["bs_find_alts_missing"].format(types=_type_list(facts.hotspots, b)), + ) + ) + + +def cost_trend(values): + # Month-on-month changes counted back from the latest month. + steps = [float(current) - float(previous) for previous, current in pairwise(values)] + if not steps or steps[-1] == 0: + return None, 0 + rising = steps[-1] > 0 + count = 0 + for step in reversed(steps): + if step == 0 or (step > 0) != rising: + break + count += 1 + if count < 2: + return None, 0 + return ("rising" if rising else "falling"), count + + +def cost_finding(facts, b): + if facts.cost_problem: + return escape(facts.cost_problem) + values = [cost for _month, cost in facts.costs] + total = sum(values) + latest_month, latest = facts.costs[-1] + parts = [ + b["bs_find_costs"].format( + total=format_money(total, facts.currency), + average=format_money(total / len(values), facts.currency), + ) + ] + if len(facts.costs) > 1: + previous_month, previous = facts.costs[-2] + parts.append( + b["bs_find_costs_latest"].format( + month=latest_month, + latest=format_money(latest, facts.currency), + change=format_change(previous, latest), + previous=previous_month, + ) + ) + else: + parts.append( + b["bs_find_costs_latest_only"].format( + month=latest_month, latest=format_money(latest, facts.currency) + ) + ) + direction, months = cost_trend(values) + if direction: + parts.append(b[f"bs_find_costs_{direction}"].format(k=months)) + return " ".join(parts) + + +def footprint_finding(facts, b): + if not facts.resources: + return b["tbl_res_no_resources"] + return b["bs_find_footprint"].format( + resources=_count(b, "bs_count_resources", facts.resources), + types=_count(b, "bs_count_types", facts.types), + regions=_count(b, "bs_count_regions", facts.regions), + ) + + +def finding_rows(facts, b): + # (area, finding markup, details section, attention marker) + return [ + ( + b["bs_area_risks"], + risk_finding(facts, b), + b["es_heading_risk_inventory"], + facts.critical_high > 0, + ), + ( + b["bs_area_alternatives"], + alternatives_finding(facts, b), + b["es_heading_alternative_technologies"], + bool(facts.hotspots), + ), + ( + b["bs_area_costs"], + cost_finding(facts, b), + b["es_heading_costs_overview"], + False, + ), + ( + b["bs_area_footprint"], + footprint_finding(facts, b), + b["es_heading_resource_inventory"], + False, + ), + ] + + +def _headline_figures(facts, b): + costs = ( + format_money(sum(cost for _month, cost in facts.costs), facts.currency) + if facts.costs + else b["ev_dash"] + ) + alternatives = ( + b["alt_stat_types_value"].format(a=facts.types_with_alternatives, n=facts.types) + if facts.types + else b["ev_dash"] + ) + return summary_strip( + [ + (b["res_stat_resources"], str(facts.resources), INK), + (b["res_stat_types"], str(facts.types), INK), + (b["bs_stat_costs"], costs, INK), + (b["risk_stat_unique"], str(facts.risks), INK), + ( + b["alt_stat_types"], + alternatives, + HOTSPOT_COUNT if facts.hotspots else INK, + ), + ], + widths=STRIP_WIDTHS, + ) + + +def _findings_table(facts, b, appearance): + rows = [[b["bs_th_area"], b["bs_th_finding"], b["bs_th_details"]]] + extra = [] + for area, finding, details, attention in finding_rows(facts, b): + index = len(rows) + rows.append( + [ + Paragraph(escape(area), FINDING_AREA), + Paragraph(finding, FINDING_TEXT), + Paragraph(escape(details), FINDING_DETAILS), + ] + ) + if attention: + extra.extend( + [ + ("LINEBEFORE", (0, index), (0, index), 3, HexColor(HOTSPOT)), + ( + "BACKGROUND", + (0, index), + (-1, index), + HexColor(HOTSPOT_BACKGROUND), + ), + ] + ) + return data_table(rows, [2.9 * cm, 10.1 * cm, 3.0 * cm], appearance, extra) + + +def build_brief_summary(facts, styles, appearance, b): + lead_style = styles["ContentStyle"].clone( + "BriefLead", fontSize=10, leading=15.5, spaceAfter=12 + ) + version = f'{escape(facts.version)}' + return [ + Paragraph(b["bs_heading"], styles["Heading1"]), + Paragraph(lead_text(facts, b), lead_style), + _headline_figures(facts, b), + Paragraph(b["bs_findings_heading"], styles["Heading2"]), + _findings_table(facts, b, appearance), + Paragraph( + b["bs_caption"].format(version=version, completed=escape(facts.completed)), + CAPTION, + ), + ] diff --git a/core/report/context.py b/core/report/context.py new file mode 100644 index 0000000..32ead79 --- /dev/null +++ b/core/report/context.py @@ -0,0 +1,381 @@ +import os +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import Any + +from core.utils_report_common import summarize_alternative_technologies +from utils.data import CatalogueError, catalogue_info +from utils.version import __version__ + +from .palette import DEFAULT_APPEARANCE, SEVERITIES, Appearance + +PROVIDER_NAMES = {1: "Microsoft Azure", 2: "Amazon Web Services"} +STRATEGY_CODES = {1: "repatriation", 2: "hybrid_cloud", 3: "alternate_cloud"} +COST_MONTHS = 6 +MISSING_ICON = "icons/misc/no_image.png" +DOCUMENT_CODE = "ES" + + +@dataclass(frozen=True) +class Provenance: + collector_version: str + collected_at: datetime + generated_at: datetime + run_id: str | None = None + catalogue_version: str | None = None + catalogue_sequence: int | None = None + catalogue_sha256: str | None = None + uploaded: bool = False + platform_url: str | None = None + assessment_id: str | None = None + + @property + def run_short_id(self): + # The end of a UUIDv7 is random; its start is the time. + return self.run_id.replace("-", "")[-8:] if self.run_id else "" + + @property + def collected_date(self): + return self.collected_at.strftime("%Y-%m-%d") + + def as_dict(self): + return { + "collector_version": self.collector_version, + "catalogue_version": self.catalogue_version, + "catalogue_sequence": self.catalogue_sequence, + "catalogue_sha256": self.catalogue_sha256, + "sync_id": self.run_id, + "uploaded": self.uploaded, + "platform_url": self.platform_url, + } + + +def collect_provenance(db_path, *, run_id=None, upload=None, collected_at=None): + try: + info = catalogue_info(db_path) + except CatalogueError: + info = None + now = datetime.now(timezone.utc) + upload = upload or {} + return Provenance( + collector_version=__version__, + collected_at=collected_at or now, + generated_at=now, + run_id=run_id or upload.get("sync_id"), + catalogue_version=info.version if info else None, + catalogue_sequence=info.sequence if info else None, + catalogue_sha256=info.sha256 if info else None, + uploaded=upload.get("status") == "uploaded", + platform_url=upload.get("url") if upload.get("status") == "uploaded" else None, + assessment_id=upload.get("assessment_id"), + ) + + +@dataclass +class ResourceTypeEntry: + resource_type_id: str + name: str + icon: str + resources: int = 0 + locations: set = field(default_factory=set) + + +@dataclass(frozen=True) +class CostEntry: + month: str + amount: float + currency: str + final: bool + + +@dataclass +class RiskEntry: + risk_id: int + code: str + name: str + description: str + severity: str + resource_type_ids: set = field(default_factory=set) + + @property + def scope_wide(self): + return not self.resource_type_ids + + +@dataclass(frozen=True) +class AlternativeEntry: + name: str + vendor: str + country: str | None + open_source: bool + support_plan: bool + + +@dataclass +class AlternativesEntry: + resource_type_id: str + name: str + icon: str + resources: int + alternatives: list = field(default_factory=list) + + +@dataclass(frozen=True) +class ScopeFacts: + kind: str + provider: str + rows: tuple = () + region: str = "" + resource_group: str = "" + file_name: str = "" + locations: tuple = () + + +@dataclass +class ReportContext: + name: str + cloud_service_provider: int + provider: str + exit_strategy: int + strategy_code: str + scope: ScopeFacts + provenance: Provenance + resources: list + costs: list + risks: list + alternatives: list + report_path: str + appearance: Appearance = DEFAULT_APPEARANCE + + @property + def document_id(self): + return f"{DOCUMENT_CODE}-{self.provenance.run_short_id or 'LOCAL'}-EN" + + @property + def total_resources(self): + return sum(entry.resources for entry in self.resources) + + @property + def regions(self): + return {location for entry in self.resources for location in entry.locations} + + @property + def completed(self): + return self.provenance.collected_at.strftime("%Y-%m-%d %H:%M UTC") + + +def _icon_path(report_path, icon): + relative = str(icon or "").lstrip("/") + for base in (report_path, "."): + candidate = os.path.join(base, "assets", relative) + if relative and os.path.isfile(candidate): + return candidate + return os.path.join(report_path, "assets", MISSING_ICON) + + +def _resource_entries(resource_inventory, resource_type_mapping, report_path): + entries = {} + for row in resource_inventory: + key = str(row["resource_type"]) + info = resource_type_mapping.get(key, {}) + entry = entries.setdefault( + key, + ResourceTypeEntry( + key, + (info.get("name") or "").strip() or "–", + _icon_path(report_path, info.get("icon")), + ), + ) + entry.resources += int(row.get("count") or 0) + if row.get("location"): + entry.locations.add(row["location"]) + return sorted( + entries.values(), key=lambda entry: (-entry.resources, entry.name.lower()) + ) + + +def _cost_entries(cost_data, collected_at): + current = collected_at.strftime("%Y-%m") + rows = sorted(cost_data, key=lambda row: str(row["month"]))[-COST_MONTHS:] + return [ + CostEntry( + str(row["month"])[:7], + float(row["cost"] or 0), + str(row["currency"] or "").upper(), + str(row["month"])[:7] < current, + ) + for row in rows + ] + + +def _severity(definition): + for key in ("catalogue_severity", "severity"): + value = str(definition.get(key) or "").lower() + if value in SEVERITIES: + return value + return None + + +def _risk_entries(risk_data, risk_definitions): + definitions = {definition["id"]: definition for definition in risk_definitions} + entries = {} + for row in risk_data: + definition = definitions.get(row["risk"]) + severity = _severity(definition) if definition else None + if severity is None: + continue + entry = entries.setdefault( + row["risk"], + RiskEntry( + row["risk"], + str(definition.get("code") or f"R{row['risk']}"), + definition["name"], + definition.get("description") or "", + severity, + ), + ) + if row["resource_type"] not in (None, "null", ""): + entry.resource_type_ids.add(str(row["resource_type"])) + return sorted( + entries.values(), + key=lambda entry: (SEVERITIES.index(entry.severity), entry.risk_id), + ) + + +def _vendor(name): + # The shared summary names a missing vendor "Unknown Organization"; the + # report leaves it out. + return "" if name == "Unknown Organization" else name + + +def _alternative_entries( + resources, resource_inventory, alternatives, technologies, organizations, strategy +): + grouped = summarize_alternative_technologies( + resource_inventory, + alternatives, + technologies, + strategy, + alternative_technology_organizations=organizations, + ) + entries = [] + for resource in resources: + found = [ + AlternativeEntry( + tech["product_name"], + _vendor(tech["organization_name"]), + ( + tech["organization_country_code"] + if tech["organization_country_code"] != "N/A" + else None + ), + tech["open_source"], + tech["support_plan"], + ) + for tech in grouped.get(resource.resource_type_id, []) + ] + # The report does not rank them: alphabetical, then the vendor. + found.sort(key=lambda alt: (alt.name.lower(), alt.vendor.lower())) + entries.append( + AlternativesEntry( + resource.resource_type_id, + resource.name, + resource.icon, + resource.resources, + found, + ) + ) + return entries + + +def _scope(provider_details, cloud_service_provider, tfstate_scope): + provider = PROVIDER_NAMES.get(cloud_service_provider, "–") + if provider_details.get("tfstatePath"): + scope = tfstate_scope or { + "file": os.path.basename(provider_details["tfstatePath"]) + } + locations = tuple(scope.get("locations") or ()) + rows = [("file", scope.get("file") or "–")] + if scope.get("sha256"): + rows.append(("sha256", scope["sha256"])) + if scope.get("lineage"): + rows.append(("lineage", scope["lineage"])) + if scope.get("serial") is not None: + rows.append(("serial", str(scope["serial"]))) + if cloud_service_provider == 1: + rows.append(("subscriptions", ", ".join(scope.get("subscriptions") or []))) + rows.append( + ("resource_groups", ", ".join(scope.get("resource_groups") or [])) + ) + else: + rows.append(("regions", ", ".join(locations))) + return ScopeFacts( + "tfstate", + provider, + tuple(rows), + file_name=scope.get("file") or "", + locations=locations, + ) + if cloud_service_provider == 1: + rows = ( + ("tenant", provider_details.get("tenantId") or ""), + ("subscription", provider_details.get("subscriptionId") or ""), + ("resource_group", provider_details.get("resourceGroupName") or ""), + ) + return ScopeFacts( + "azure", + provider, + rows, + resource_group=provider_details.get("resourceGroupName") or "", + ) + rows = ( + ("account", provider_details.get("accountId") or ""), + ("region", provider_details.get("region") or ""), + ) + return ScopeFacts( + "aws", provider, rows, region=provider_details.get("region") or "" + ) + + +def build_context( + *, + metadata: dict[str, Any], + provider_details: dict[str, Any], + resource_type_mapping: dict[str, dict[str, Any]], + resource_inventory: list[dict[str, Any]], + cost_data: list[dict[str, Any]], + risk_data: list[dict[str, Any]], + risk_definitions: list[dict[str, Any]], + alternatives: list[dict[str, Any]], + alternative_technologies: list[dict[str, Any]], + alternative_technology_organizations: list[dict[str, Any]], + exit_strategy: int, + tfstate_scope: dict[str, Any] | None, + provenance: Provenance, + report_path: str, +) -> ReportContext: + csp = int(metadata["cloud_service_provider"]) + resources = _resource_entries( + resource_inventory, resource_type_mapping, report_path + ) + return ReportContext( + name=metadata["name"], + cloud_service_provider=csp, + provider=PROVIDER_NAMES.get(csp, "–"), + exit_strategy=exit_strategy, + strategy_code=STRATEGY_CODES.get(int(exit_strategy), ""), + scope=_scope(provider_details, csp, tfstate_scope), + provenance=provenance, + resources=resources, + costs=_cost_entries(cost_data, provenance.collected_at), + risks=_risk_entries(risk_data, risk_definitions), + alternatives=_alternative_entries( + resources, + resource_inventory, + alternatives, + alternative_technologies, + alternative_technology_organizations, + exit_strategy, + ), + report_path=report_path, + ) diff --git a/core/report/cover.py b/core/report/cover.py new file mode 100644 index 0000000..4d70657 --- /dev/null +++ b/core/report/cover.py @@ -0,0 +1,200 @@ +import logging +import os +from dataclasses import dataclass +from xml.sax.saxutils import escape + +from reportlab.lib.colors import HexColor +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import ParagraphStyle +from reportlab.lib.units import cm +from reportlab.lib.utils import ImageReader +from reportlab.platypus import Paragraph + +from .fonts import FONT, FONT_BOLD, FONT_MONO +from .helpers import strategy_name +from .palette import INK, MUTED, RULE, SUBTITLE + +logger = logging.getLogger("core.engine.report") + +BAND_WIDTH = 0.9 * cm +MARGIN = 2.5 * cm +CONTENT_WIDTH = A4[0] - 2 * MARGIN +COLUMN_WIDTH = 8 * cm +CELL_WIDTH = COLUMN_WIDTH - 12 +LOGO_MAX_WIDTH = 5.5 * cm +LOGO_MAX_HEIGHT = 1.6 * cm +LOGO_TOP = 2.2 * cm +TITLE_TOP = 8.1 * cm +FOOTER_Y = 1.5 * cm +ROW_SPACING = 14 +LOGO_FILE = os.path.join("img", "logo", "report_logo.png") + +TITLE_STYLE = ParagraphStyle( + "CoverTitle", + fontName=FONT_BOLD, + fontSize=30, + leading=34, + textColor=HexColor(INK), +) +PROJECT_STYLE = ParagraphStyle( + "CoverProject", + fontName=FONT, + fontSize=15, + leading=19, + textColor=HexColor(SUBTITLE), +) +LABEL_STYLE = ParagraphStyle( + "CoverLabel", + fontName=FONT_BOLD, + fontSize=6.8, + leading=8.5, + textColor=HexColor(MUTED), +) +VALUE_STYLE = ParagraphStyle( + "CoverValue", + fontName=FONT, + fontSize=9.5, + leading=12.5, + textColor=HexColor(INK), +) +MONO_VALUE_STYLE = ParagraphStyle( + "CoverMonoValue", parent=VALUE_STYLE, fontName=FONT_MONO +) + + +@dataclass(frozen=True) +class CoverCell: + label: str + value: str + style: ParagraphStyle = VALUE_STYLE + + +def fit_logo(width, height, max_width=LOGO_MAX_WIDTH, max_height=LOGO_MAX_HEIGHT): + scale = min(max_width / width, max_height / height) + return width * scale, height * scale + + +def logo_reader(report_path): + # The report folder carries its own copy of the assets. + for base in (os.path.join(report_path, "assets"), "assets"): + path = os.path.join(base, LOGO_FILE) + if os.path.isfile(path): + return ImageReader(path) + raise FileNotFoundError(f"report logo not found under {report_path}/assets") + + +def _paragraph(value, style): + return Paragraph(escape(str(value)), style) + + +class RecordCover: + def __init__(self, title, context, b): + self.title = title + self.project_name = context.name + self.document_id = context.document_id + self.appearance = context.appearance + self.report_path = context.report_path + self.provenance = context.provenance + self.b = b + self.rows = self._rows(context, b) + + def _rows(self, context, b): + provenance = context.provenance + run = b["cover_run_value"].format( + run=provenance.run_short_id or b["ev_dash"], completed=context.completed + ) + left = [ + CoverCell(b["cover_doc_id"], self.document_id, MONO_VALUE_STYLE), + CoverCell(b["cover_classification"], b["cover_classification_value"]), + CoverCell( + b["cover_exit_strategy"], strategy_name(context.strategy_code, b) + ), + ] + right = [ + CoverCell(b["cover_run"], run), + CoverCell( + b["cover_generated"], provenance.generated_at.strftime("%Y-%m-%d") + ), + CoverCell(b["ev_language"], b["language_name"]), + ] + return list(zip(left, right)) + + def __call__(self, canvas, doc): + canvas.saveState() + width, height = A4 + + canvas.setFillColor(HexColor(self.appearance.header_footer_color)) + canvas.rect(0, 0, BAND_WIDTH, height, stroke=0, fill=1) + + self._draw_logo(canvas, width, height) + y = self._draw_title_block(canvas, height - TITLE_TOP) + self._draw_details(canvas, y - 34) + self._draw_footer(canvas, width) + + canvas.restoreState() + + def _draw_logo(self, canvas, width, height): + logo = logo_reader(self.report_path) + logo_width, logo_height = fit_logo(*logo.getSize()) + canvas.drawImage( + logo, + width - MARGIN - logo_width, + height - LOGO_TOP - logo_height, + width=logo_width, + height=logo_height, + mask="auto", + ) + + def _draw_title_block(self, canvas, top): + y = self._draw_paragraph(canvas, self.title, TITLE_STYLE, MARGIN, top) + return self._draw_paragraph( + canvas, self.project_name, PROJECT_STYLE, MARGIN, y - 8 + ) + + def _draw_details(self, canvas, top): + canvas.setStrokeColor(HexColor(RULE)) + canvas.setLineWidth(0.5) + canvas.line(MARGIN, top, MARGIN + CONTENT_WIDTH, top) + + y = top + for row in self.rows: + y -= ROW_SPACING + bottoms = [ + self._draw_cell(canvas, cell, MARGIN + index * COLUMN_WIDTH, y) + for index, cell in enumerate(row) + if cell is not None + ] + y = min(bottoms) + y -= ROW_SPACING + + canvas.setStrokeColor(HexColor(RULE)) + canvas.setLineWidth(0.5) + canvas.line(MARGIN, y, MARGIN + CONTENT_WIDTH, y) + + def _draw_cell(self, canvas, cell, x, top): + y = self._draw_paragraph( + canvas, cell.label.upper(), LABEL_STYLE, x, top, CELL_WIDTH + ) + return self._draw_paragraph( + canvas, cell.value, cell.style, x, y - 3, CELL_WIDTH + ) + + def _draw_paragraph(self, canvas, text, style, x, top, width=CONTENT_WIDTH): + paragraph = _paragraph(text, style) + _w, h = paragraph.wrap(width, A4[1]) + paragraph.drawOn(canvas, x, top - h) + return top - h + + def _draw_footer(self, canvas, width): + canvas.setFillColor(HexColor(MUTED)) + canvas.setFont(FONT, 7) + canvas.drawString( + MARGIN, + FOOTER_Y, + self.b["cover_collected_with"].format( + version=self.provenance.collector_version, + date=self.provenance.collected_date, + ), + ) + canvas.setFont(FONT_MONO, 7) + canvas.drawRightString(width - MARGIN, FOOTER_Y, self.document_id) diff --git a/core/report/evidence.py b/core/report/evidence.py new file mode 100644 index 0000000..daf91cc --- /dev/null +++ b/core/report/evidence.py @@ -0,0 +1,147 @@ +from xml.sax.saxutils import escape, quoteattr + +from reportlab.platypus import Paragraph, Spacer + +from .flow import chapter_break +from .table_style import FOOTNOTE, KV_MONO, KV_VALUE, key_value_table, mono, text_style + + +def _short(value, head=4, tail=4): + if not isinstance(value, str) or len(value) <= head + tail + 1: + return value or "" + return f"{value[:head]}…{value[-tail:]}" + + +def _section_heading(appearance): + # Not an outline heading: the style name is not Heading1 or Heading2. + return text_style( + "EvidenceSectionHeading", + 10.5, + 14, + appearance.heading2_color, + bold=True, + ).clone("EvidenceSectionHeading", spaceBefore=12, spaceAfter=4, keepWithNext=1) + + +def _mono_value(text): + return Paragraph(escape(text), KV_MONO) if text else None + + +def build_document_table(context, b): + provenance = context.provenance + rows = [ + (b["ev_document_id"], Paragraph(escape(context.document_id), KV_MONO)), + (b["ev_document_type"], b["ev_executive_report"]), + (b["ev_language"], b["language_name"]), + ( + b["ev_collected_by"], + b["ev_collected_by_value"].format(version=provenance.collector_version), + ), + (b["ev_collected_at"], provenance.collected_date), + (b["ev_generated_at"], provenance.generated_at.strftime("%Y-%m-%d %H:%M UTC")), + ] + return key_value_table(rows, context.appearance) + + +def _scope_rows(context, b): + scope = context.scope + values = dict(scope.rows) + rows = [(b["ev_cloud_provider"], scope.provider or b["ev_dash"])] + if scope.kind == "aws": + if values.get("account"): + rows.append((b["ev_account_id"], _mono_value(values["account"]))) + rows.append((b["ev_region"], values.get("region") or b["ev_dash"])) + elif scope.kind == "azure": + rows.append( + (b["ev_tenant_id"], _mono_value(values.get("tenant")) or b["ev_dash"]) + ) + rows.append( + ( + b["ev_subscription_id"], + _mono_value(values.get("subscription")) or b["ev_dash"], + ) + ) + rows.append( + (b["ev_resource_group"], values.get("resource_group") or b["ev_dash"]) + ) + else: + file_text = values.get("file") or b["ev_dash"] + if values.get("sha256"): + file_text = f"{file_text} (SHA-256 {_short(values['sha256'])})" + rows.append((b["ev_file"], file_text)) + if values.get("lineage"): + state = values["lineage"] + if values.get("serial") is not None: + state = f"{state} (serial {values['serial']})" + rows.append((b["ev_state"], _mono_value(state))) + for key, label in ( + ("subscriptions", "ev_subscriptions"), + ("resource_groups", "ev_resource_groups"), + ("regions", "ev_regions"), + ): + if key in values: + rows.append((b[label], values[key] or b["ev_not_determinable"])) + source = b["ev_source_tfstate" if scope.kind == "tfstate" else "ev_source_live"] + rows.append( + ( + b["ev_source_label"], + b["ev_source_value"].format(source=source, completed=context.completed), + ) + ) + return rows + + +def build_scope_table(context, b): + return key_value_table(_scope_rows(context, b), context.appearance) + + +def link_markup(url, color): + return f'{escape(url)}' + + +def build_provenance_table(context, b): + provenance = context.provenance + if provenance.catalogue_version and provenance.catalogue_sha256: + catalogue = b["ev_catalogue_value"].format( + version=escape(provenance.catalogue_version), + sha=mono(escape(provenance.catalogue_sha256[:12])), + ) + elif provenance.catalogue_version: + catalogue = escape(provenance.catalogue_version) + else: + catalogue = escape(b["ev_catalogue_unknown"]) + + rows = [] + if provenance.run_id: + rows.append((b["ev_run_id"], Paragraph(escape(provenance.run_id), KV_MONO))) + rows.append((b["ev_catalogue_label"], Paragraph(catalogue, KV_VALUE))) + if provenance.uploaded: + text = escape(b["ev_platform_uploaded"]) + if provenance.platform_url: + text = f"{text}: {link_markup(provenance.platform_url, context.appearance.heading2_color)}" + rows.append((b["ev_platform_label"], Paragraph(text, KV_VALUE))) + else: + rows.append((b["ev_platform_label"], b["ev_platform_none"])) + return key_value_table(rows, context.appearance) + + +def build_document_information_page(context, b): + heading = _section_heading(context.appearance) + return [ + Paragraph(b["ev_document_information_heading"], heading), + build_document_table(context, b), + Paragraph(b["ev_assessment_scope_heading"], heading), + build_scope_table(context, b), + Paragraph(b["ev_provenance_heading"], heading), + build_provenance_table(context, b), + Spacer(1, 6), + Paragraph( + escape( + b["ev_stored_footnote"].format( + run=context.provenance.run_short_id or b["ev_dash"] + ) + ), + FOOTNOTE, + ), + chapter_break(), + ] diff --git a/core/report/executive_summary.py b/core/report/executive_summary.py new file mode 100644 index 0000000..9e96415 --- /dev/null +++ b/core/report/executive_summary.py @@ -0,0 +1,70 @@ +import os +from functools import partial + +from reportlab.lib.pagesizes import A4 +from reportlab.lib.units import cm +from reportlab.platypus import Spacer + +from .base import NumberedCanvas, ReportDocTemplate, set_pdf_metadata +from .brief_summary import build_brief_summary, collect_brief_facts +from .cover import RecordCover +from .evidence import build_document_information_page +from .flow import chapter_break +from .sections import ( + alternative_technologies, + costs_overview, + full_assessment, + glossary, + introduction, + resource_inventory, + risk_inventory, + severity_levels, +) +from .styles import load_styles +from .wording import BLOCKS + + +def build_content(context, styles, b): + facts = collect_brief_facts(context, b) + content = [Spacer(1, 0), chapter_break()] + content += build_document_information_page(context, b) + content += build_brief_summary(facts, styles, context.appearance, b) + content.append(chapter_break()) + content += introduction(context, styles, b) + content.append(chapter_break()) + content += costs_overview(context, facts, styles, b) + content.append(chapter_break()) + content += resource_inventory(context, styles, b) + content.append(chapter_break()) + content += risk_inventory(context, styles, b) + content.append(chapter_break()) + content += alternative_technologies(context, styles, b) + content.append(chapter_break()) + content += severity_levels(context, styles, b) + content.append(Spacer(1, 18)) + content += glossary(context, styles, b) + content.append(chapter_break()) + content += full_assessment(context, styles, b) + return content + + +def generate_executive_summary(context, path=None): + path = path or os.path.join(context.report_path, "report.pdf") + b = BLOCKS + cover = RecordCover(b["cover_title_executive_summary"], context, b) + doc = ReportDocTemplate( + path, + context, + cover=cover, + pagesize=A4, + rightMargin=2.5 * cm, + leftMargin=2.5 * cm, + topMargin=2.5 * cm, + bottomMargin=2.5 * cm, + ) + set_pdf_metadata(doc, context, b) + doc.multiBuild( + build_content(context, load_styles(context.appearance), b), + canvasmaker=partial(NumberedCanvas, page_of=b["footer_page_of"]), + ) + return path diff --git a/core/report/flow.py b/core/report/flow.py new file mode 100644 index 0000000..b5fd59f --- /dev/null +++ b/core/report/flow.py @@ -0,0 +1,115 @@ +from reportlab.graphics.shapes import Drawing +from reportlab.lib.units import cm +from reportlab.platypus import ( + CondPageBreak, + KeepTogether, + PageBreak, + Paragraph, + Spacer, + Table, +) + +# Room a section needs at the foot of a page before it starts there: its +# heading, lead and opening block (summary strip, bar) or first table rows. +SECTION_MIN = 6 * cm +# A Heading2 subsection that opens with text needs less. +SUBSECTION_MIN = 4 * cm +# A table with fewer rows moves with its caption; longer tables split. +SHORT_TABLE_ROWS = 8 + +CAPTION_STYLES = ("AltCaption", "EgressCaption") +OPENING_CAPTION_STYLES = ("ResBarCaption",) + + +class KeepWithNext(KeepTogether): + # The group keepWithNext builds from a heading and what follows it. When it + # does not fit but there is room for the heading and a few lines, it starts + # here and splits, instead of moving a long paragraph or table whole. + def split(self, aW, aH): + if getattr(self, "_wrapInfo", None) != (aW, aH): + self.wrap(aW, aH) + if self._H > aH >= SUBSECTION_MIN: + return self._content[:] + return super().split(aW, aH) + + +def chapter_break(): + # The only hard page break in the reports: cover, page 2, chapters and + # appendices. + return PageBreak() + + +def section_break(min_height=SECTION_MIN): + return CondPageBreak(min_height) + + +def section_head(*flowables): + return KeepTogether(list(flowables)) + + +def _opens(item): + # Summary strips (one-row tables), bars and their captions open a section. + if isinstance(item, Table): + return len(item._cellvalues) == 1 + if isinstance(item, Paragraph): + return item.style.name in OPENING_CAPTION_STYLES + return isinstance(item, (Drawing, Spacer)) + + +def split_opening(items): + index = 0 + while index < len(items) and _opens(items[index]): + index += 1 + return items[:index], items[index:] + + +def _is_caption(item): + return isinstance(item, Paragraph) and item.style.name in CAPTION_STYLES + + +def keep_captions(items): + # A short table and the caption under it move as one; a long table may + # split, with its header repeated. + result = [] + index = 0 + while index < len(items): + item = items[index] + if isinstance(item, Table) and len(item._cellvalues) < SHORT_TABLE_ROWS: + end = index + 1 + while end < len(items) and isinstance(items[end], Spacer): + end += 1 + if end < len(items) and _is_caption(items[end]): + while end + 1 < len(items) and ( + _is_caption(items[end + 1]) + or isinstance(items[end + 1], Spacer) + and end + 2 < len(items) + and _is_caption(items[end + 2]) + ): + end += 1 + result.append(KeepTogether(items[index : end + 1])) + index = end + 1 + continue + result.append(item) + index += 1 + return result + + +def _is_short_table(item): + if isinstance(item, KeepTogether): + return any(_is_short_table(child) for child in item._content) + return isinstance(item, Table) and len(item._cellvalues) < SHORT_TABLE_ROWS + + +def section(head, items): + # The heading, lead and opening block move as one unit; the rest flows. + # Without a strip or bar, a short first table joins the lead, so the + # heading never waits alone at the foot of a page for it. + opening, rest = split_opening(list(items)) + rest = keep_captions(rest) + if not opening and rest and _is_short_table(rest[0]): + first = rest[0] + # Flattened: a KeepTogether inside another always reports itself as + # too tall and would push the whole section to the next page. + opening = first._content if isinstance(first, KeepTogether) else [first] + rest = rest[1:] + return [section_head(*head, *opening), *rest] diff --git a/core/report/fonts.py b/core/report/fonts.py new file mode 100644 index 0000000..568a0d3 --- /dev/null +++ b/core/report/fonts.py @@ -0,0 +1,64 @@ +from pathlib import Path + +from reportlab.graphics.shapes import STATE_DEFAULTS +from reportlab.lib.fonts import addMapping +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.platypus.tables import CellStyle + +FONT_DIR = Path(__file__).resolve().parents[2] / "assets" / "fonts" + +FONT = "IBMPlexSans" +FONT_MEDIUM = "IBMPlexSans-Medium" +# SemiBold is the report's bold: it reads cleaner than Bold at small sizes. +FONT_BOLD = "IBMPlexSans-SemiBold" +FONT_ITALIC = "IBMPlexSans-Italic" +FONT_BOLD_ITALIC = "IBMPlexSans-SemiBoldItalic" +FONT_MONO = "IBMPlexMono" +FONT_MONO_BOLD = "IBMPlexMono-SemiBold" + +FONT_FILES = { + FONT: "IBMPlexSans-Regular.ttf", + FONT_MEDIUM: "IBMPlexSans-Medium.ttf", + FONT_BOLD: "IBMPlexSans-SemiBold.ttf", + FONT_ITALIC: "IBMPlexSans-Italic.ttf", + FONT_BOLD_ITALIC: "IBMPlexSans-SemiBoldItalic.ttf", + FONT_MONO: "IBMPlexMono-Regular.ttf", + FONT_MONO_BOLD: "IBMPlexMono-SemiBold.ttf", +} + + +def register_fonts(): + if set(FONT_FILES) <= set(pdfmetrics.getRegisteredFontNames()): + return + # A missing file raises: a report must never switch typeface without + # anyone noticing. + for name, filename in FONT_FILES.items(): + pdfmetrics.registerFont(TTFont(name, str(FONT_DIR / filename))) + # Families let and in Paragraph markup find the right face. + pdfmetrics.registerFontFamily( + FONT, + normal=FONT, + bold=FONT_BOLD, + italic=FONT_ITALIC, + boldItalic=FONT_BOLD_ITALIC, + ) + pdfmetrics.registerFontFamily( + FONT_MONO, + normal=FONT_MONO, + bold=FONT_MONO_BOLD, + italic=FONT_MONO, + boldItalic=FONT_MONO_BOLD, + ) + # Mono has no italic, so the upright faces fill the italic slots. Map them + # back as upright, or markup inside mono text would count as italic. + addMapping(FONT_MONO, 0, 0, FONT_MONO) + addMapping(FONT_MONO, 1, 0, FONT_MONO_BOLD) + # Table cells and drawings select ReportLab's base fonts (Helvetica, + # Times-Roman) even when they draw no text with them, which still lists + # those fonts in every PDF. This package is the only user of them here. + CellStyle.fontname = FONT + STATE_DEFAULTS["fontName"] = FONT + + +register_fonts() diff --git a/core/report/helpers.py b/core/report/helpers.py new file mode 100644 index 0000000..9e4f65b --- /dev/null +++ b/core/report/helpers.py @@ -0,0 +1,42 @@ +CURRENCY_SYMBOLS = {"USD": "$", "EUR": "€", "GBP": "£"} + + +def strategy_name(code, b): + return b.get(f"strategy_name_{code}") or b["ev_dash"] + + +def strategy_definition(code, b): + return b.get(f"strategy_def_{code}") or "" + + +def format_amount(value): + return f"{float(value or 0):,.2f}" + + +def format_money(value, currency): + amount = format_amount(value) + symbol = CURRENCY_SYMBOLS.get(currency) + return f"{symbol}{amount}" if symbol else f"{currency} {amount}" + + +def format_change(previous, current): + if not previous or current is None: + return "–" + change = (float(current) - float(previous)) / float(previous) * 100 + return f"{'+' if change >= 0 else '−'}{abs(change):.1f} %" + + +def severity_label(severity, b): + return b.get(f"risk_sev_{severity}", severity) + + +def cost_problem(context, b): + # A message in place of the figures and the table, or None when the data + # is usable. + if not context.costs: + if context.scope.kind == "tfstate": + return b["cost_tfstate"] + return b["tbl_cost_no_data"] + if len({entry.currency for entry in context.costs}) > 1: + return b["cost_mixed_currency"] + return None diff --git a/core/report/html_sections.py b/core/report/html_sections.py new file mode 100644 index 0000000..045572e --- /dev/null +++ b/core/report/html_sections.py @@ -0,0 +1,121 @@ +from xml.sax.saxutils import escape + +from .brief_summary import collect_brief_facts, finding_rows, lead_text +from .helpers import severity_label +from .palette import SEVERITIES +from .sections import glossary_entries, scope_boundary, scope_strategy +from .wording import BLOCKS + + +def _version(text): + return f"{escape(text)}" + + +def html_sections(context, b=BLOCKS): + # The same wording as the PDF; strings that carry markup are already + # escaped, so the template may mark them safe. + facts = collect_brief_facts(context, b) + provenance = context.provenance + + if provenance.catalogue_version and provenance.catalogue_sha256: + catalogue = ( + f"{provenance.catalogue_version} · (sha256 " + f"{provenance.catalogue_sha256[:12]})" + ) + else: + catalogue = provenance.catalogue_version or b["ev_catalogue_unknown"] + + return { + "title": b["ev_executive_report"], + "brief": { + "heading": b["bs_heading"], + "lead": lead_text(facts, b), + "findings_heading": b["bs_findings_heading"], + "headers": (b["bs_th_area"], b["bs_th_finding"], b["bs_th_details"]), + "rows": [ + { + "area": area, + "finding": finding, + "details": details, + "attention": attention, + } + for area, finding, details, attention in finding_rows(facts, b) + ], + "caption": b["bs_caption"].format( + version=_version(facts.version), completed=escape(facts.completed) + ), + }, + "provenance": { + "heading": b["ev_provenance_heading"], + "rows": [ + ( + b["ev_collected_by"], + b["ev_collected_by_value"].format( + version=provenance.collector_version + ), + ), + (b["ev_collected_at"], provenance.collected_date), + *([(b["ev_run_id"], provenance.run_id)] if provenance.run_id else []), + (b["ev_catalogue_label"], catalogue), + ( + b["ev_platform_label"], + ( + b["ev_platform_uploaded"] + if provenance.uploaded + else b["ev_platform_none"] + ), + ), + ], + "platform_url": provenance.platform_url, + "document_id": context.document_id, + }, + "introduction": { + "heading": b["es_heading_introduction"], + "purpose_heading": b["es_heading_purpose"], + "purpose": b["es_purpose_block"], + "scope_heading": b["es_heading_scope"], + "scope": [scope_boundary(context, b), scope_strategy(context, b)], + "method_heading": b["es_heading_method"], + "method": [b["es_method_lead"], b["es_method_phases_lead"]], + "phases": b["es_method_phases"], + "method_catalogue": b["es_method_catalogue"], + "limitations_heading": b["es_heading_limitations"], + "limitations": [b["err_limitations"], b["err_limitations_data"]], + }, + "severity": { + "heading": b["es_heading_severity"], + "lead": [b["es_severity_lead"], b["es_severity_note"]], + "headers": (b["risk_th_severity"], b["es_severity_th_meaning"]), + "rows": [ + (severity_label(level, b), b[f"es_severity_{level}"]) + for level in SEVERITIES + ], + }, + "glossary": { + "heading": b["es_heading_glossary"], + "lead": b["es_glossary_block"], + "entries": [ + (entry["term"], entry["definition"]) + for entry in sorted( + glossary_entries(context, b), key=lambda e: e["term"].lower() + ) + ], + }, + "closing": { + "heading": b[ + "fa_heading_uploaded" if provenance.uploaded else "fa_heading" + ], + "uploaded": provenance.uploaded, + "lead": b["fa_uploaded"] if provenance.uploaded else b["fa_lead"], + "points": [] if provenance.uploaded else b["fa_items"], + "how_heading": b["fa_how_heading"], + "how": b["fa_how"], + "command": b["fa_command"], + "sites": [ + ("EscapeCloud", "https://escapecloud.io"), + ("ExitCloud", "https://exitcloud.io"), + ], + "url": provenance.platform_url, + "assessment_id": provenance.assessment_id, + }, + } diff --git a/core/report/palette.py b/core/report/palette.py new file mode 100644 index 0000000..72504c3 --- /dev/null +++ b/core/report/palette.py @@ -0,0 +1,103 @@ +import re +from dataclasses import dataclass + +WHITE = "#FFFFFF" +INK = "#1A1F24" +SUBTITLE = "#4A5560" +MUTED = "#6B7680" +RULE = "#C9D0D6" +HAIRLINE = "#E3E7EA" +HOTSPOT = "#E8A33D" +HOTSPOT_BACKGROUND = "#FDF6EC" +HOTSPOT_COUNT = "#B45309" +PROVENANCE_BACKGROUND = "#F5F7F8" + +# Fixed: the pip count and the word carry the severity without colour. +SEVERITIES = ("critical", "high", "medium", "low") +SEVERITY_COLORS = { + "critical": "#6E1A14", + "high": "#B8322A", + "medium": "#E8A33D", + "low": "#4A7FB5", +} +SEVERITY_PIPS = {"critical": 4, "high": 3, "medium": 2, "low": 1} + +# Header row text sits on the colour's own 0.9 tint; checking the slightly +# darker 0.88 tint covers it. +OWN_TINT_AMOUNT = 0.88 + + +@dataclass(frozen=True) +class Appearance: + header_footer_color: str = "#005470" + header_row_color: str = "#005470" + heading1_color: str = "#005470" + heading2_color: str = "#005470" + + +DEFAULT_APPEARANCE = Appearance() + +HEX_PATTERN = re.compile(r"^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$") + + +def normalize_hex(value): + match = HEX_PATTERN.match(value.strip()) + if not match: + raise ValueError(f"{value} is not a valid hex colour") + digits = match.group(1) + if len(digits) == 3: + digits = "".join(digit * 2 for digit in digits) + return f"#{digits.upper()}" + + +def _channels(value): + digits = normalize_hex(value)[1:] + return [int(digits[index : index + 2], 16) / 255 for index in (0, 2, 4)] + + +def relative_luminance(value): + def linear(channel): + if channel <= 0.04045: + return channel / 12.92 + return ((channel + 0.055) / 1.055) ** 2.4 + + red, green, blue = (linear(channel) for channel in _channels(value)) + return 0.2126 * red + 0.7152 * green + 0.0722 * blue + + +def contrast_ratio(first, second): + lighter, darker = sorted( + (relative_luminance(first), relative_luminance(second)), reverse=True + ) + return (lighter + 0.05) / (darker + 0.05) + + +def tint(value, amount): + channels = (channel + (1 - channel) * amount for channel in _channels(value)) + return "#" + "".join(f"{round(channel * 255):02X}" for channel in channels) + + +def contrast_pairs(appearance=DEFAULT_APPEARANCE): + # (what, text colour, background, minimum ratio): WCAG 2.x asks 4.5:1 for + # normal text and 3:1 for large text. + own = tint(appearance.header_row_color, OWN_TINT_AMOUNT) + return ( + ("body text", INK, WHITE, 4.5), + ("secondary text", SUBTITLE, WHITE, 4.5), + ("muted text", MUTED, WHITE, 4.5), + ("chapter heading", appearance.heading1_color, WHITE, 3.0), + ("section heading", appearance.heading2_color, WHITE, 4.5), + ("table header", appearance.header_row_color, own, 4.5), + ("page number and footer", MUTED, WHITE, 4.5), + ("attention count", HOTSPOT_COUNT, HOTSPOT_BACKGROUND, 4.5), + ("provenance note", SUBTITLE, PROVENANCE_BACKGROUND, 4.5), + ("band and rules", appearance.header_footer_color, WHITE, 3.0), + ) + + +def contrast_failures(appearance=DEFAULT_APPEARANCE): + return [ + (what, round(contrast_ratio(text, background), 2), minimum) + for what, text, background, minimum in contrast_pairs(appearance) + if contrast_ratio(text, background) < minimum + ] diff --git a/core/report/provenance.py b/core/report/provenance.py new file mode 100644 index 0000000..e5dc208 --- /dev/null +++ b/core/report/provenance.py @@ -0,0 +1,80 @@ +import re +from datetime import timezone +from xml.sax.saxutils import escape + +from reportlab.pdfbase.pdfdoc import XMP + +CLOUDEXIT_NS = "https://cloudexit.escapecloud.io/ns/report/1.0/" +RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" +# XML 1.0 has no escape for these, so a name carrying one would break the packet. +XML_INVALID = re.compile( + "[\x00-\x08\x0b\x0c\x0e-\x1f" + chr(0xFFFE) + chr(0xFFFF) + "]" +) + + +def _utc(value): + return value.astimezone(timezone.utc) + + +def _xml(value): + return escape(XML_INVALID.sub("", str(value if value is not None else ""))) + + +def attach_metadata(canvas, provenance): + generated_at = _utc(provenance.generated_at) + # The Info dict dates follow the generation time, not the render clock. + canvas.setDateFormatter( + lambda *_local: generated_at.strftime("D:%Y%m%d%H%M%S+00'00'") + ) + # Bytes, because ReportLab would encode a str as PDFDocEncoding, which has + # no BOM and no non-Latin text; XMP is UTF-8. + xmp = XMP(creator=lambda document: xmp_packet(document.info, provenance).encode()) + # An empty filter list keeps the packet uncompressed, so tools that scan + # the raw file for XMP still find it. + xmp.filters = () + canvas._doc.Catalog.Metadata = xmp + + +def xmp_packet(info, provenance): + generated_at = _utc(provenance.generated_at) + stamp = generated_at.strftime("%Y-%m-%dT%H:%M:%SZ") + fields = { + "RunID": provenance.run_id, + "CollectorVersion": provenance.collector_version, + "CatalogueVersion": provenance.catalogue_version, + "CatalogueSHA256": provenance.catalogue_sha256, + "Uploaded": str(provenance.uploaded).lower(), + "GeneratedAt": generated_at.isoformat(), + } + custom = "".join( + f"{_xml(value)}" + for field, value in fields.items() + if value + ) + return ( + '' + '' + f'' + '' + "application/pdf" + "" + f'{_xml(info.title)}' + "" + "" + f'{_xml(info.subject)}' + "" + f"{_xml(info.keywords)}" + f"{_xml(info.creator)}" + f"{stamp}" + f"{stamp}" + f"{stamp}" + f"{custom}" + "" + "" + "" + '' + ) diff --git a/core/report/sections.py b/core/report/sections.py new file mode 100644 index 0000000..f0246a5 --- /dev/null +++ b/core/report/sections.py @@ -0,0 +1,812 @@ +from xml.sax.saxutils import escape, quoteattr + +from reportlab.graphics.shapes import Drawing, Rect, String +from reportlab.lib.colors import HexColor, white +from reportlab.lib.enums import TA_RIGHT +from reportlab.lib.units import cm +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.platypus import ( + Flowable, + KeepTogether, + ListFlowable, + ListItem, + Paragraph, + Spacer, + Table, + TableStyle, +) + +from .brief_summary import cost_finding +from .evidence import link_markup +from .flow import section +from .fonts import FONT, FONT_BOLD, FONT_MONO +from .helpers import ( + cost_problem, + format_amount, + format_change, + format_money, + severity_label, + strategy_definition, + strategy_name, +) +from .palette import ( + HAIRLINE, + HOTSPOT, + HOTSPOT_BACKGROUND, + HOTSPOT_COUNT, + INK, + MUTED, + RULE, + SEVERITIES, + SEVERITY_COLORS, + SEVERITY_PIPS, +) +from .table_style import SECONDARY, data_table, header_cell, summary_strip, text_style + +ALT_BODY = text_style("AltBody", 8.5, 11) +ALT_BODY_BOLD = text_style("AltBodyBold", 8.5, 11, bold=True) +ALT_BODY_RIGHT = text_style("AltBodyRight", 8.5, 11, alignment=TA_RIGHT) +ALT_MUTED = text_style("AltMuted", 8.5, 11, MUTED) +ALT_SMALL = text_style("AltSmall", 7.5, 9.5, MUTED) +ALT_CAPTION = text_style("AltCaption", 7.5, 10, MUTED) +RES_TOTAL_RIGHT = text_style("ResTotalRight", 8.5, 11, bold=True, alignment=TA_RIGHT) +RES_GROUP = text_style("ResGroup", 7.6, 9.5, SECONDARY, bold=True) +RISK_CODE = text_style("RiskCode", 8, 11, SECONDARY).clone( + "RiskCode", fontName=FONT_MONO +) +RISK_MUTED_RIGHT = text_style("RiskMutedRight", 8.5, 11, MUTED, alignment=TA_RIGHT) +COMMAND = text_style("CommandLine", 8.5, 12, INK).clone( + "CommandLine", fontName=FONT_MONO, leftIndent=8, spaceBefore=4, spaceAfter=8 +) +GLOSSARY_TERM_WIDTH = 4.2 * cm +RISK_BAR_MIN_SEGMENT = 62 +ALT_EXAMPLES = 3 +PRODUCT_SITES = ( + ("escapecloud", "https://escapecloud.io"), + ("exitcloud", "https://exitcloud.io"), +) + + +def _text(value, style=ALT_BODY): + return Paragraph(escape(str(value)), style) + + +def _percent(part, total): + return f"{(part / total * 100) if total else 0:.0f} %" + + +def _count_phrase(b, key, n): + return b[f"{key}_one"] if n == 1 else b[f"{key}_many"].format(n=n) + + +def _no_rows_table(header, col_widths, appearance, message): + message = [_text(message, ALT_MUTED)] + [""] * (len(header) - 1) + return data_table( + [header, message], col_widths, appearance, [("SPAN", (0, 1), (-1, 1))] + ) + + +def _group_row(rows, extra, label, columns): + # A spanned label row that never ends up alone at the bottom of a page. + index = len(rows) + rows.append([_text(label, RES_GROUP)] + [""] * (columns - 1)) + extra.extend( + [ + ("SPAN", (0, index), (-1, index)), + ("TOPPADDING", (0, index), (-1, index), 9), + ("BOTTOMPADDING", (0, index), (-1, index), 2), + ("LINEBELOW", (0, index), (-1, index), 0.6, HexColor(RULE)), + ("NOSPLIT", (0, index), (-1, index + 1)), + ] + ) + + +def _type_name_cell(entry): + return Paragraph( + f' {escape(entry.name)}', + ALT_BODY, + ) + + +# Introduction +def scope_boundary(context, b): + scope = context.scope + counts = b["scope_counts"].format( + resources=_count_phrase(b, "scope_resources", context.total_resources), + types=_count_phrase(b, "scope_types", len(context.resources)), + ) + values = { + "counts": f"{counts}", + "regions": _count_phrase(b, "scope_regions", len(context.regions)), + } + if scope.kind == "azure": + return b["scope_boundary_azure"].format( + resource_group=f"{escape(scope.resource_group or b['ev_dash'])}", + **values, + ) + if scope.kind == "aws": + return b["scope_boundary_aws"].format( + region=f"{escape(scope.region or b['ev_dash'])}", **values + ) + return b["scope_boundary_tfstate"].format( + file=f"{escape(scope.file_name or b['ev_dash'])}", + provider=escape(context.provider), + **values, + ) + + +def scope_strategy(context, b): + strategy = f"{escape(strategy_name(context.strategy_code, b))}" + definition = strategy_definition(context.strategy_code, b) + if not definition: + return b["scope_strategy_plain"].format(strategy=strategy) + return b["scope_strategy"].format(strategy=strategy, definition=escape(definition)) + + +def _method_list(b, styles): + items = [ + ListItem( + Paragraph(f"{escape(name)} {escape(text)}", styles["ContentStyle"]), + leftIndent=14, + ) + for name, text in b["es_method_phases"] + ] + return ListFlowable( + items, + bulletType="bullet", + start="•", + bulletFontName=FONT, + bulletFontSize=9, + leftIndent=14, + ) + + +def introduction(context, styles, b): + body = styles["ContentStyle"] + return [ + Paragraph(b["es_heading_introduction"], styles["Heading1"]), + Paragraph(b["es_heading_purpose"], styles["Heading2"]), + Paragraph(b["es_purpose_block"], body), + Paragraph(b["es_heading_scope"], styles["Heading2"]), + Paragraph(scope_boundary(context, b), body), + Spacer(1, 6), + Paragraph(scope_strategy(context, b), body), + Paragraph(b["es_heading_method"], styles["Heading2"]), + Paragraph(b["es_method_lead"], body), + Paragraph(b["es_method_phases_lead"], body), + _method_list(b, styles), + Spacer(1, 6), + Paragraph(b["es_method_catalogue"], body), + Paragraph(b["es_heading_limitations"], styles["Heading2"]), + Paragraph(b["err_limitations"], body), + Spacer(1, 6), + Paragraph(b["err_limitations_data"], body), + ] + + +# Costs +def _cost_table(context, b): + currency = context.costs[0].currency + appearance = context.appearance + rows = [ + [ + b["cost_th_month"], + header_cell( + b["cost_th_cost"].format(currency=escape(currency)), + appearance, + TA_RIGHT, + ), + header_cell(b["cost_th_change"], appearance, TA_RIGHT), + ] + ] + previous = None + for entry in context.costs: + month = entry.month + if not entry.final: + month += f' · {escape(b["cost_provisional"])}' + rows.append( + [ + Paragraph(month, ALT_BODY), + _text(format_amount(entry.amount), ALT_BODY_RIGHT), + _text(format_change(previous, entry.amount), ALT_BODY_RIGHT), + ] + ) + previous = entry.amount + caption = b["cost_caption"].format(currency=escape(currency)) + if not all(entry.final for entry in context.costs): + caption += " " + b["cost_caption_provisional"] + total_row = len(rows) + rows.append( + [ + _text(b["cost_total"], ALT_BODY_BOLD), + _text( + format_amount(sum(entry.amount for entry in context.costs)), + RES_TOTAL_RIGHT, + ), + "", + ] + ) + return [ + data_table( + rows, + [5.0 * cm, 5.5 * cm, 5.5 * cm], + appearance, + [ + ("LINEABOVE", (0, total_row), (-1, total_row), 0.8, HexColor(INK)), + ("NOSPLIT", (0, total_row - 1), (-1, total_row)), + ], + ), + Spacer(1, 6), + Paragraph(caption, ALT_CAPTION), + ] + + +def costs_overview(context, facts, styles, b): + head = [ + Paragraph(b["es_heading_costs_overview"], styles["Heading1"]), + Paragraph(cost_finding(facts, b), styles["ContentStyle"]), + Spacer(1, 8), + ] + problem = cost_problem(context, b) + if problem is not None: + return section(head, []) + entries = context.costs + total = sum(entry.amount for entry in entries) + latest = entries[-1] + before = entries[-2] if len(entries) > 1 else None + currency = latest.currency + items = [ + summary_strip( + [ + (b["cost_stat_total"], format_money(total, currency), INK), + ( + b["cost_stat_average"], + format_money(total / len(entries), currency), + INK, + ), + ( + b["cost_stat_latest"].format(month=latest.month), + format_money(latest.amount, currency), + INK, + ), + ( + b["cost_stat_change"].format( + month=before.month if before else b["ev_dash"] + ), + format_change(before.amount if before else None, latest.amount), + INK, + ), + ] + ), + Spacer(1, 12), + *_cost_table(context, b), + Spacer(1, 12), + Paragraph(b["es_cost_note"], styles["ContentStyle"]), + ] + return section(head, items) + + +# Resource inventory +def resource_inventory(context, styles, b): + appearance = context.appearance + head = [ + Paragraph(b["es_heading_resource_inventory"], styles["Heading1"]), + Paragraph(b["es_res_lead"], styles["ContentStyle"]), + Spacer(1, 12), + ] + header = [ + b["res_th_resource_type"], + header_cell(b["res_th_regions"], appearance, TA_RIGHT), + header_cell(b["res_th_resources"], appearance, TA_RIGHT), + header_cell(b["res_th_share"], appearance, TA_RIGHT), + ] + widths = [9.4 * cm, 2.1 * cm, 2.3 * cm, 2.2 * cm] + if not context.resources: + return section( + head, + [_no_rows_table(header, widths, appearance, b["tbl_res_no_resources"])], + ) + + total = context.total_resources + rows = [header] + for entry in context.resources: + rows.append( + [ + _type_name_cell(entry), + _text(len(entry.locations), ALT_BODY_RIGHT), + _text(entry.resources, ALT_BODY_RIGHT), + _text(_percent(entry.resources, total), ALT_BODY_RIGHT), + ] + ) + total_row = len(rows) + rows.append( + [ + _text(b["res_total"], ALT_BODY_BOLD), + _text(len(context.regions), RES_TOTAL_RIGHT), + _text(total, RES_TOTAL_RIGHT), + _text(_percent(total, total), RES_TOTAL_RIGHT), + ] + ) + table = data_table( + rows, + widths, + appearance, + [ + ("LINEABOVE", (0, total_row), (-1, total_row), 0.8, HexColor(INK)), + ("NOSPLIT", (0, total_row - 1), (-1, total_row)), + ], + ) + return section( + head, + [ + summary_strip( + [ + (b["res_stat_resources"], str(total), INK), + (b["res_stat_types"], str(len(context.resources)), INK), + (b["res_stat_regions"], str(len(context.regions)), INK), + ] + ), + Spacer(1, 12), + table, + Spacer(1, 6), + Paragraph(b["es_res_caption"], ALT_CAPTION), + ], + ) + + +# Risk inventory +class SeverityBadge(Flowable): + def __init__(self, severity, label): + Flowable.__init__(self) + self.severity = severity + self.label = label + self.width = 2.3 * cm + self.height = 10 + + def wrap(self, availWidth, availHeight): + return min(self.width, availWidth), self.height + + def draw(self): + canvas = self.canv + size, gap = 4.6, 1.4 + color = HexColor(SEVERITY_COLORS.get(self.severity, MUTED)) + pips = SEVERITY_PIPS.get(self.severity, 0) + y = (self.height - size) / 2 + for index in range(4): + x = index * (size + gap) + if index < pips: + canvas.setFillColor(color) + canvas.rect(x, y, size, size, stroke=0, fill=1) + else: + canvas.setStrokeColor(HexColor(RULE)) + canvas.setLineWidth(0.6) + canvas.rect(x, y, size, size, stroke=1, fill=0) + canvas.setFillColor(HexColor(INK)) + canvas.setFont(FONT, 8.2) + canvas.drawString(4 * size + 3 * gap + 4, 2, self.label) + + +def _segment_widths(counts, available, minimum): + # Proportional, but no non-empty segment narrower than the minimum. + fixed = {} + while True: + free = [key for key in counts if key not in fixed] + if not free: + return fixed + rest = available - sum(fixed.values()) + total = sum(counts[key] for key in free) + widths = {key: rest * counts[key] / total for key in free} + small = [key for key, width in widths.items() if width < minimum] + if not small: + return {**fixed, **widths} + for key in small: + fixed[key] = minimum + + +def _severity_bar(risks, b): + width, bar_height, gap = 16 * cm, 15, 1.5 + counts = { + severity: sum(1 for risk in risks if risk.severity == severity) + for severity in SEVERITIES + } + counts = {severity: count for severity, count in counts.items() if count} + drawing = Drawing(width, 36) + if counts: + widths = _segment_widths( + counts, width - gap * (len(counts) - 1), RISK_BAR_MIN_SEGMENT + ) + x = 0 + for severity, count in counts.items(): + segment = widths[severity] + drawing.add( + Rect( + x, + 21, + segment, + bar_height, + fillColor=HexColor(SEVERITY_COLORS[severity]), + strokeColor=None, + ) + ) + label = f"{count} {severity_label(severity, b)}" + if stringWidth(label, FONT_BOLD, 7.8) + 8 > segment: + label = str(count) + drawing.add( + String( + x + segment / 2, + 25.7, + label, + fontName=FONT_BOLD, + fontSize=7.8, + fillColor=HexColor(INK) if severity == "medium" else white, + textAnchor="middle", + ) + ) + x += segment + gap + drawing.add( + String( + 0, + 8, + b["risk_bar_caption"], + fontName=FONT, + fontSize=7, + fillColor=HexColor(MUTED), + ) + ) + return drawing + + +def _risk_overview(context, b): + risks = context.risks + scope = {entry.resource_type_id for entry in context.resources} + affected = {type_id for risk in risks for type_id in risk.resource_type_ids} + return [ + summary_strip( + [ + (b["risk_stat_unique"], str(len(risks)), INK), + ( + b["risk_stat_critical_high"], + str( + sum( + 1 for risk in risks if risk.severity in ("critical", "high") + ) + ), + SEVERITY_COLORS["high"], + ), + ( + b["risk_stat_affected"], + b["risk_stat_affected_value"].format( + a=len(affected & scope), n=len(scope) + ), + INK, + ), + ( + b["risk_stat_scope_wide"], + str(sum(1 for risk in risks if risk.scope_wide)), + INK, + ), + ] + ), + Spacer(1, 12), + _severity_bar(risks, b), + Spacer(1, 12), + ] + + +def risk_inventory(context, styles, b): + appearance = context.appearance + head = [ + Paragraph(b["es_heading_risk_inventory"], styles["Heading1"]), + Paragraph(b["es_risk_lead"], styles["ContentStyle"]), + Spacer(1, 12), + ] + header = [ + b["risk_th_code"], + b["risk_th_risk"], + b["risk_th_severity"], + header_cell(b["risk_th_affected"], appearance, TA_RIGHT), + ] + widths = [1.6 * cm, 9.5 * cm, 2.6 * cm, 2.3 * cm] + if not context.risks: + return section( + head, [_no_rows_table(header, widths, appearance, b["tbl_risk_no_risks"])] + ) + + rows = [header] + extra = [] + for severity in SEVERITIES: + members = [risk for risk in context.risks if risk.severity == severity] + if not members: + continue + _group_row( + rows, + extra, + b["risk_group"].format( + severity=severity_label(severity, b), n=len(members) + ), + len(header), + ) + for risk in members: + rows.append( + [ + _text(risk.code, RISK_CODE), + _text(risk.name, ALT_BODY_BOLD), + SeverityBadge(risk.severity, severity_label(risk.severity, b)), + ( + _text(b["risk_scope_wide"], RISK_MUTED_RIGHT) + if risk.scope_wide + else _text(len(risk.resource_type_ids), ALT_BODY_RIGHT) + ), + ] + ) + return section( + head, + [ + *_risk_overview(context, b), + data_table(rows, widths, appearance, extra), + Spacer(1, 6), + Paragraph(b["es_risk_caption"], ALT_CAPTION), + ], + ) + + +# Alternative technologies +def _examples_cell(entry, b): + cell = [] + for alt in entry.alternatives[:ALT_EXAMPLES]: + vendor = ( + f' · {escape(alt.vendor)}' + if alt.vendor + else "" + ) + cell.append(Paragraph(f"{escape(alt.name)}{vendor}", ALT_BODY)) + flags = [ + b[key] + for key, present in ( + ("alt_flag_open_source", alt.open_source), + ("alt_flag_support_plan", alt.support_plan), + ) + if present + ] + if flags: + cell.append(Paragraph(" · ".join(flags), ALT_SMALL)) + if len(entry.alternatives) > ALT_EXAMPLES: + cell.append( + Paragraph( + b["alt_more"].format(k=len(entry.alternatives) - ALT_EXAMPLES), + ALT_SMALL, + ) + ) + return cell + + +def alternative_technologies(context, styles, b): + appearance = context.appearance + head = [ + Paragraph(b["es_heading_alternative_technologies"], styles["Heading1"]), + Paragraph( + b["es_alttech_lead"].format( + strategy=escape(strategy_name(context.strategy_code, b)) + ), + styles["ContentStyle"], + ), + Spacer(1, 8), + ] + header = [ + b["alt_th_resource_type"], + header_cell(b["alt_th_resources"], appearance, TA_RIGHT), + header_cell(b["alt_th_alternatives"], appearance, TA_RIGHT), + b["alt_th_examples"], + ] + widths = [4.6 * cm, 2.05 * cm, 2.15 * cm, 7.2 * cm] + if not context.alternatives: + return section( + head, [_no_rows_table(header, widths, appearance, b["alt_no_resources"])] + ) + + # Lock-in hot-spots first, then by resource count and name. + entries = sorted( + context.alternatives, + key=lambda entry: ( + bool(entry.alternatives), + -entry.resources, + entry.name.lower(), + ), + ) + hotspots = sum(1 for entry in entries if not entry.alternatives) + items = [] + if not any(entry.alternatives for entry in entries): + items += [Paragraph(b["alt_empty_set"], styles["ContentStyle"]), Spacer(1, 6)] + items += [ + summary_strip( + [ + ( + b["alt_stat_types"], + b["alt_stat_types_value"].format( + a=len(entries) - hotspots, n=len(entries) + ), + INK, + ), + ( + b["alt_stat_alternatives"], + str(sum(len(entry.alternatives) for entry in entries)), + INK, + ), + (b["alt_stat_hotspots"], str(hotspots), HOTSPOT_COUNT), + ] + ), + Spacer(1, 12), + ] + + rows = [header] + extra = [] + for entry in entries: + index = len(rows) + row = [ + _type_name_cell(entry), + _text(entry.resources, ALT_BODY_RIGHT), + _text(len(entry.alternatives), ALT_BODY_RIGHT), + ] + if entry.alternatives: + row.append(_examples_cell(entry, b)) + else: + row.append(Paragraph(b["alt_hotspot_short"], ALT_BODY)) + extra.extend( + [ + ( + "BACKGROUND", + (0, index), + (-1, index), + HexColor(HOTSPOT_BACKGROUND), + ), + ("LINEBEFORE", (0, index), (0, index), 3, HexColor(HOTSPOT)), + ] + ) + rows.append(row) + items += [ + data_table(rows, widths, appearance, extra), + Spacer(1, 6), + Paragraph(b["es_alttech_caption"], ALT_CAPTION), + ] + return section(head, items) + + +# Appendices +def severity_levels(context, styles, b): + rows = [[b["risk_th_severity"], b["es_severity_th_meaning"]]] + for severity in SEVERITIES: + rows.append( + [ + SeverityBadge(severity, severity_label(severity, b)), + _text(b[f"es_severity_{severity}"], ALT_BODY), + ] + ) + return [ + Paragraph(b["es_heading_severity"], styles["Heading1"]), + Paragraph(b["es_severity_lead"], styles["ContentStyle"]), + Paragraph(b["es_severity_note"], styles["ContentStyle"]), + Spacer(1, 10), + data_table(rows, [3.6 * cm, 12.4 * cm], context.appearance), + ] + + +def glossary_entries(context, b): + # The AWS entry is only listed for AWS runs. + return [ + entry + for entry in b["glossary"] + if entry["id"] != "aws" or context.cloud_service_provider == 2 + ] + + +def glossary(context, styles, b): + entries = sorted( + glossary_entries(context, b), key=lambda entry: entry["term"].lower() + ) + table = Table( + [ + [_text(entry["term"], ALT_BODY_BOLD), _text(entry["definition"])] + for entry in entries + ], + colWidths=[GLOSSARY_TERM_WIDTH, 16 * cm - GLOSSARY_TERM_WIDTH], + hAlign="LEFT", + ) + table.setStyle( + TableStyle( + [ + ( + "LINEABOVE", + (0, 0), + (-1, 0), + 1, + HexColor(context.appearance.header_row_color), + ), + ("LINEBELOW", (0, 0), (-1, -1), 0.4, HexColor(HAIRLINE)), + ("LEFTPADDING", (0, 0), (-1, -1), 5), + ("RIGHTPADDING", (0, 0), (-1, -1), 5), + ("TOPPADDING", (0, 0), (-1, -1), 3.5), + ("BOTTOMPADDING", (0, 0), (-1, -1), 3.5), + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ] + ) + ) + return [ + Paragraph(b["es_heading_glossary"], styles["Heading1"]), + Paragraph(b["es_glossary_block"], styles["ContentStyle"]), + Spacer(1, 12), + table, + ] + + +# Closing page +def full_assessment(context, styles, b): + body = styles["ContentStyle"] + provenance = context.provenance + if provenance.uploaded: + content = [ + Paragraph(b["fa_heading_uploaded"], styles["Heading1"]), + Paragraph(escape(b["fa_uploaded"]), body), + ] + if provenance.platform_url: + link = link_markup( + provenance.platform_url, context.appearance.heading2_color + ) + content.append(Paragraph(b["fa_uploaded_link"].format(link=link), body)) + elif provenance.assessment_id: + content.append( + Paragraph( + escape( + b["fa_uploaded_id"].format( + assessment_id=provenance.assessment_id + ) + ), + body, + ) + ) + return content + + rows = [[_text(term, ALT_BODY_BOLD), _text(text)] for term, text in b["fa_items"]] + table = Table( + rows, + colWidths=[GLOSSARY_TERM_WIDTH, 16 * cm - GLOSSARY_TERM_WIDTH], + hAlign="LEFT", + ) + table.setStyle( + TableStyle( + [ + ( + "LINEABOVE", + (0, 0), + (-1, 0), + 1, + HexColor(context.appearance.header_row_color), + ), + ("LINEBELOW", (0, 0), (-1, -1), 0.4, HexColor(HAIRLINE)), + ("LEFTPADDING", (0, 0), (-1, -1), 5), + ("RIGHTPADDING", (0, 0), (-1, -1), 5), + ("TOPPADDING", (0, 0), (-1, -1), 5), + ("BOTTOMPADDING", (0, 0), (-1, -1), 5), + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ] + ) + ) + color = context.appearance.heading2_color + sites = b["fa_sites"].format( + **{name: link_markup(url, color) for name, url in PRODUCT_SITES} + ) + return [ + Paragraph(b["fa_heading"], styles["Heading1"]), + Paragraph(escape(b["fa_lead"]), body), + Spacer(1, 6), + table, + Spacer(1, 6), + KeepTogether( + [ + Paragraph(b["fa_how_heading"], styles["Heading2"]), + Paragraph(escape(b["fa_how"]), body), + Paragraph(escape(b["fa_command"]), COMMAND), + Paragraph(sites, body), + ] + ), + ] diff --git a/core/report/styles.py b/core/report/styles.py new file mode 100644 index 0000000..23db8f2 --- /dev/null +++ b/core/report/styles.py @@ -0,0 +1,80 @@ +from reportlab.lib import colors +from reportlab.lib.colors import HexColor +from reportlab.lib.enums import TA_CENTER, TA_LEFT +from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet + +from .fonts import FONT, FONT_BOLD +from .palette import DEFAULT_APPEARANCE + + +def load_styles(appearance=DEFAULT_APPEARANCE): + styles = getSampleStyleSheet() + + for name in ("Normal", "BodyText", "Bullet"): + styles[name].fontName = FONT + for name in ("Title", "Heading1", "Heading2", "Heading3"): + styles[name].fontName = FONT_BOLD + + # A heading never ends a page on its own. + for name in ("Heading1", "Heading2", "Heading3"): + styles[name].keepWithNext = 1 + + styles["Heading1"].leading = 1.5 * styles["Heading1"].fontSize + # Space above a chapter that follows another section on the same page; + # ReportLab drops it at the top of a page. + styles["Heading1"].spaceBefore = 18 + styles["Heading1"].textColor = HexColor(appearance.heading1_color) + styles["Heading2"].leading = 1.5 * styles["Heading2"].fontSize + styles["Heading2"].textColor = HexColor(appearance.heading2_color) + styles["Heading3"].leading = 1.5 * styles["Heading3"].fontSize + styles["Heading3"].textColor = HexColor(appearance.heading2_color) + + content_style = ParagraphStyle( + "ContentStyle", + parent=styles["BodyText"], + fontName=FONT, + fontSize=9.5, + leading=14.5, + # Left-aligned: justified text leaves uneven gaps in a 16 cm column. + alignment=TA_LEFT, + firstLineIndent=0, + # No single line of a paragraph alone at the top or foot of a page. + allowWidows=0, + allowOrphans=0, + ) + reference_style = ParagraphStyle( + "ReferenceStyle", + parent=styles["BodyText"], + fontName=FONT, + fontSize=8, + leading=12, + alignment=TA_CENTER, + textColor=colors.grey, + firstLineIndent=0, + ) + tableheader_style = ParagraphStyle( + "TableHeader", + parent=styles["BodyText"], + fontName=FONT_BOLD, + fontSize=9.5, + leading=14.5, + alignment=TA_LEFT, + firstLineIndent=0, + textColor=colors.HexColor("#FFFFFF"), + ) + table_style = ParagraphStyle( + "TableStyle", + parent=styles["BodyText"], + fontName=FONT, + fontSize=8, + leading=12, + alignment=TA_LEFT, + firstLineIndent=0, + ) + + styles.add(content_style) + styles.add(reference_style) + styles.add(tableheader_style) + styles.add(table_style) + + return styles diff --git a/core/report/table_style.py b/core/report/table_style.py new file mode 100644 index 0000000..5b54247 --- /dev/null +++ b/core/report/table_style.py @@ -0,0 +1,167 @@ +from xml.sax.saxutils import escape + +from reportlab.lib.colors import HexColor +from reportlab.lib.enums import TA_LEFT +from reportlab.lib.styles import ParagraphStyle +from reportlab.lib.units import cm +from reportlab.platypus import Paragraph, Table, TableStyle + +from .fonts import FONT, FONT_BOLD, FONT_ITALIC, FONT_MONO +from .palette import HAIRLINE, INK, MUTED, SUBTITLE, tint + +SECONDARY = SUBTITLE + + +def mix_with_white(color, amount): + return HexColor(tint(color, amount)) + + +def text_style(name, size, leading, color=INK, bold=False, alignment=TA_LEFT): + return ParagraphStyle( + name, + fontName=FONT_BOLD if bold else FONT, + fontSize=size, + leading=leading, + textColor=HexColor(color), + alignment=alignment, + ) + + +def header_cell(text, appearance, alignment=TA_LEFT, split_long_words=True): + style = text_style( + "DataTableHeader", + 7.8, + 9.5, + appearance.header_row_color, + bold=True, + alignment=alignment, + ) + # In narrow columns a word slightly wider than the cell runs into the + # padding instead of breaking mid-word. + style.splitLongWords = int(split_long_words) + return Paragraph(text, style) + + +def data_table(rows, col_widths, appearance, extra=()): + header_color = HexColor(appearance.header_row_color) + rows = [ + [ + header_cell(cell, appearance) if isinstance(cell, str) else cell + for cell in rows[0] + ], + *rows[1:], + ] + table = Table(rows, colWidths=col_widths, repeatRows=1, hAlign="LEFT") + table.setStyle( + TableStyle( + [ + ( + "BACKGROUND", + (0, 0), + (-1, 0), + mix_with_white(appearance.header_row_color, 0.9), + ), + ("LINEBELOW", (0, 0), (-1, 0), 1, header_color), + ("LINEBELOW", (0, 1), (-1, -1), 0.4, HexColor(HAIRLINE)), + ("LEFTPADDING", (0, 0), (-1, -1), 5), + ("RIGHTPADDING", (0, 0), (-1, -1), 5), + ("TOPPADDING", (0, 0), (-1, -1), 4.5), + ("BOTTOMPADDING", (0, 0), (-1, -1), 4.5), + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ("VALIGN", (0, 0), (-1, 0), "BOTTOM"), + *extra, + ] + ) + ) + return table + + +KV_LABEL = text_style("KeyValueLabel", 8.2, 10.5, SECONDARY, bold=True) +KV_VALUE = text_style("KeyValueValue", 8.2, 10.5) +KV_MONO = ParagraphStyle( + "KeyValueMono", parent=KV_VALUE, fontName=FONT_MONO, fontSize=7.8 +) +FOOTNOTE = text_style("Footnote", 7.5, 9.5, MUTED).clone( + "Footnote", fontName=FONT_ITALIC +) + + +def mono(text): + # Inline monospace for identifiers inside a key-value value. + return f'{text}' + + +def key_value_table(rows, appearance, label_width=4.6 * cm): + # rows: (label, value); a string value is escaped markup-free text, any + # other value (a Paragraph) is used as it is. + table = Table( + [ + [ + Paragraph(escape(label), KV_LABEL), + ( + Paragraph(escape(value), KV_VALUE) + if isinstance(value, str) + else value + ), + ] + for label, value in rows + ], + colWidths=[label_width, 16 * cm - label_width], + hAlign="LEFT", + ) + table.setStyle( + TableStyle( + [ + ( + "LINEABOVE", + (0, 0), + (-1, 0), + 1, + HexColor(appearance.header_row_color), + ), + ("LINEBELOW", (0, 0), (-1, -1), 0.4, HexColor(HAIRLINE)), + ("LEFTPADDING", (0, 0), (-1, -1), 5), + ("RIGHTPADDING", (0, 0), (-1, -1), 5), + ("TOPPADDING", (0, 0), (-1, -1), 3.5), + ("BOTTOMPADDING", (0, 0), (-1, -1), 3.5), + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ] + ) + ) + return table + + +STAT_LABEL = text_style("AltStatLabel", 7.2, 9, MUTED, bold=True) + + +def summary_strip(stats, widths=None): + # stats: (label, value, colour); values are plain text. Equal columns + # unless widths are given. + cells = [ + [ + Paragraph(escape(str(label).upper()), STAT_LABEL), + Paragraph( + escape(str(value)), + text_style("AltStatValue", 15, 18, color, bold=True), + ), + ] + for label, value, color in stats + ] + strip = Table( + [cells], + colWidths=widths or [16 * cm / len(stats)] * len(stats), + hAlign="LEFT", + ) + strip.setStyle( + TableStyle( + [ + ("LINEABOVE", (0, 0), (-1, 0), 0.8, HexColor(INK)), + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ("LEFTPADDING", (0, 0), (-1, -1), 0), + ("RIGHTPADDING", (0, 0), (-1, -1), 6), + ("TOPPADDING", (0, 0), (-1, -1), 6), + ("BOTTOMPADDING", (0, 0), (-1, -1), 0), + ] + ) + ) + return strip diff --git a/core/report/wording.py b/core/report/wording.py new file mode 100644 index 0000000..d2ad9de --- /dev/null +++ b/core/report/wording.py @@ -0,0 +1,447 @@ +BLOCKS = { + "language_name": "English", + "pdf_subject": "{document_id} · Basic assessment collected by cloudexit {version}", + "ev_language": "Language", + "ev_dash": "–", + "ev_document_information_heading": "Document Information", + "ev_assessment_scope_heading": "Assessment Scope", + "ev_provenance_heading": "Provenance", + "ev_executive_report": "Executive Summary", + "ev_document_id": "Document ID", + "ev_document_type": "Document Type", + "ev_collected_by": "Collected by", + "ev_collected_by_value": "cloudexit {version}", + "ev_collected_at": "Collected on", + "ev_generated_at": "Generated at", + "ev_cloud_provider": "Cloud Provider", + "ev_account_id": "Account ID", + "ev_tenant_id": "Tenant ID", + "ev_subscription_id": "Subscription ID", + "ev_resource_group": "Resource Group", + "ev_region": "Region", + "ev_file": "State File", + "ev_state": "State", + "ev_subscriptions": "Subscriptions", + "ev_resource_groups": "Resource Groups", + "ev_regions": "Regions", + "ev_not_determinable": "Not determinable from the state", + "ev_source_label": "Source", + "ev_source_live": "Provider APIs", + "ev_source_tfstate": "Terraform State", + "ev_source_value": "{source} (data as of {completed})", + "ev_catalogue_label": "Data Catalogue", + "ev_catalogue_value": "{version} · (sha256 {sha})", + "ev_catalogue_unknown": "Unknown (the dataset records no version)", + "ev_run_id": "Run ID", + "ev_platform_label": "Platform", + "ev_platform_none": "Self-collected data: no platform evaluated this report.", + "ev_platform_uploaded": "Uploaded to the platform", + "ev_stored_footnote": ( + "This document was generated when run {run} completed and is stored " + "unchanged. Later runs are reported in their own documents." + ), + "cover_title_executive_summary": "Executive Summary", + "cover_doc_id": "Document ID", + "cover_classification": "Classification", + "cover_classification_value": "Confidential", + "cover_exit_strategy": "Exit Strategy", + "cover_run": "Assessment Run", + "cover_run_value": "{run} · data as of {completed}", + "cover_generated": "Generated", + "cover_collected_with": "Collected by cloudexit {version} on {date}", + "footer_page_of": "Page {page} of {total}", + "strategy_name_repatriation": "Repatriation to On-Premises", + "strategy_name_hybrid_cloud": "Hybrid Cloud Adoption", + "strategy_name_alternate_cloud": "Migration to Alternate Cloud", + "strategy_def_repatriation": ( + "moving all resources and data back to a data centre or on-premises " + "environment" + ), + "strategy_def_hybrid_cloud": ( + "a mixture of on-premises and alternative cloud deployment models" + ), + "strategy_def_alternate_cloud": ( + "transitioning to a different cloud provider from the current one" + ), + "scope_counts": "{resources} of {types}", + "scope_resources_one": "1 resource", + "scope_resources_many": "{n} resources", + "scope_types_one": "1 type", + "scope_types_many": "{n} types", + "scope_regions_one": "1 region", + "scope_regions_many": "{n} regions", + "scope_boundary_azure": ( + "This assessment covers {counts} in Microsoft Azure resource group " + "{resource_group} ({regions}). Resources outside this resource " + "group are not assessed." + ), + "scope_boundary_aws": ( + "This assessment covers {counts} in AWS region {region}. Other " + "regions are not assessed." + ), + "scope_boundary_tfstate": ( + "This assessment covers {counts} in the Terraform state file {file} " + "({provider}, {regions}). Resources that Terraform does not manage " + "are not assessed." + ), + "scope_strategy": ( + "The scope is assessed for the {strategy} exit strategy: {definition}." + ), + "scope_strategy_plain": "The scope is assessed for the {strategy} exit strategy.", + "es_heading_introduction": "Introduction", + "es_heading_purpose": "Purpose", + "es_heading_scope": "Scope", + "es_heading_method": "Method", + "es_heading_limitations": "Limitations", + "es_purpose_block": ( + "This Executive Summary gives decision-makers an overview of the " + "cloud exit assessment: the cloud resources in use and their costs, " + "the exit risks identified and the available alternative " + "technologies for the defined scope and the selected exit strategy." + ), + "es_method_lead": ( + "The assessment was collected and evaluated locally with cloudexit. " + "It combines automated discovery, structured data " + "processing and rule-based evaluation, so that results are " + "consistent and repeatable across cloud providers." + ), + "es_method_phases_lead": ( + "Once the scope is defined, the assessment runs three phases:" + ), + "es_method_phases": [ + ( + "Discovery", + "identifies the active resources in the scope and classifies them by type, region and cost.", + ), + ( + "Risk assessment", + "evaluates exit risks by comparing the discovered resource types with the availability and enterprise support of alternative technologies.", + ), + ( + "Reporting", + "compiles the results into this Executive Summary.", + ), + ], + "es_method_catalogue": ( + "Risks and alternatives are taken from the data catalogue version " + "recorded on page 2, so the results of this run can be reproduced." + ), + "err_limitations": ( + "This report gives directional guidance on the selected exit " + "strategy and its risks. It is not a migration roadmap and not an " + "audit. Business or technical circumstances specific to the " + "organisation may require further validation." + ), + "err_limitations_data": ( + "Results reflect the data of the run shown on page 2. The analysis " + "assumes that the discovered resources are correctly configured and " + "accessible. Dependencies outside the defined scope, such as " + "on-premises systems or other cloud accounts, are not covered; " + "results are indicative, not exhaustive." + ), + "bs_heading": "Brief Summary", + "bs_lead": ( + "This summary condenses the findings of this report for {scope} " + "({provider}), assessed for the {strategy} exit strategy." + ), + "bs_scope_tfstate": "the Terraform state file", + "bs_outlook_good": ( + "No critical or high risks were found, and every resource type has " + "at least one alternative." + ), + "bs_outlook_attention": "{parts} need attention before an exit.", + "bs_outlook_attention_one": "{parts} needs attention before an exit.", + "bs_part_risks_one": "1 critical or high risk", + "bs_part_risks_many": "{n} critical or high risks", + "bs_part_hotspots_one": "1 resource type without an alternative", + "bs_part_hotspots_many": "{n} resource types without an alternative", + "bs_and": " and ", + "bs_more": " and {k} more", + "bs_stat_costs": "Costs, 6 Months", + "bs_findings_heading": "Key Findings", + "bs_th_area": "Area", + "bs_th_finding": "Finding", + "bs_th_details": "Details", + "bs_area_risks": "Risks", + "bs_area_alternatives": "Alternatives", + "bs_area_costs": "Costs", + "bs_area_footprint": "Footprint", + "bs_find_risks_none": "No exit risks were identified.", + "bs_find_risks_one": "1 unique exit risk, {severity}: {code} {name}.", + "bs_find_risks_many": "{n} unique exit risks: {counts}. Most severe: {code} {name} ({severity}).", + "bs_find_risks_no_severe": "No critical or high risks.", + "bs_find_alts_all": "All {n} resource types have at least one alternative ({total} in total); no lock-in hot-spots.", + "bs_find_alts_all_one": "The only resource type has at least one alternative ({total} in total); no lock-in hot-spots.", + "bs_find_alts_some": "{a} of {n} resource types have at least one alternative ({total} in total).", + "bs_find_alts_some_one": "1 of {n} resource types has at least one alternative ({total} in total).", + "bs_find_alts_none": "No resource type has an alternative in the data catalogue: {types}.", + "bs_find_alts_missing": "No alternative in the data catalogue: {types}.", + "bs_find_costs": "{total} over six months ({average} per month on average).", + "bs_find_costs_latest": "The latest month ({month}) was {latest}, {change} on {previous}.", + "bs_find_costs_latest_only": "The latest month ({month}) was {latest}.", + "bs_find_costs_rising": "Costs have risen for {k} consecutive months.", + "bs_find_costs_falling": "Costs have fallen for {k} consecutive months.", + "bs_find_footprint": "{resources} of {types} in {regions}.", + "bs_count_resources_one": "1 resource", + "bs_count_resources_many": "{n} resources", + "bs_count_types_one": "1 type", + "bs_count_types_many": "{n} types", + "bs_count_regions_one": "1 region", + "bs_count_regions_many": "{n} regions", + "bs_caption": "Amber marker = critical or high risks or resource types without an alternative. All figures are taken from the sections of this report and refer to run {version} (data as of {completed}); the summary adds no findings of its own.", + "es_heading_costs_overview": "Costs Overview", + "tbl_cost_no_data": "No cost data available for this assessment.", + "cost_stat_total": "Total, 6 Months", + "cost_stat_average": "Monthly Average", + "cost_stat_latest": "Latest Month ({month})", + "cost_stat_change": "Change vs. {month}", + "cost_th_month": "Month", + "cost_th_cost": "Cost ({currency})", + "cost_th_change": "vs. Previous Month", + "cost_total": "Total (6 months)", + "cost_caption": "Source: provider billing data for the scope, currency {currency}. – = no cost in the previous month.", + "cost_mixed_currency": "The cost data contains more than one currency and is not totalled.", + "cost_provisional": "provisional", + "cost_caption_provisional": "Provisional = the month was still accruing when the data was collected.", + "cost_tfstate": "Terraform state files hold no billing data, so there are no costs to show for this assessment.", + "es_cost_note": "Costs are shown for context. They do not affect the risk evaluation and are not an estimate of exit costs.", + "es_heading_resource_inventory": "Resource Inventory", + "es_res_lead": ( + "Cloud resources discovered in the defined scope. Each resource type " + "appears once; regions are counted, not repeated." + ), + "tbl_res_no_resources": ( + "There are no resources to be found in the resource inventory." + ), + "res_stat_resources": "Resources", + "res_stat_types": "Resource Types", + "res_stat_regions": "Regions", + "res_th_resource_type": "Resource Type", + "res_th_regions": "Regions", + "res_th_resources": "Resources", + "res_th_share": "Share", + "res_total": "Total", + "es_res_caption": ( + "Share = resources of the type as a share of all resources in scope. " + "Region names as reported by the provider." + ), + "es_heading_risk_inventory": "Risk Inventory", + "es_risk_lead": ( + "Unique exit risks identified in the defined scope, ordered by " + "severity. Risk codes are stable across assessments, so a finding can " + "be followed from one run to the next." + ), + "tbl_risk_no_risks": "There are no risks identified in this assessment.", + "risk_sev_critical": "Critical", + "risk_sev_high": "High", + "risk_sev_medium": "Medium", + "risk_sev_low": "Low", + "risk_stat_unique": "Unique Risks", + "risk_stat_critical_high": "Critical + High", + "risk_stat_affected": "Resource Types Affected", + "risk_stat_affected_value": "{a} of {n}", + "risk_stat_scope_wide": "Scope-wide Risks", + "risk_bar_caption": ( + "Unique risks by severity · colour, pip count and label carry the " + "same meaning" + ), + "risk_th_code": "Code", + "risk_th_risk": "Risk", + "risk_th_severity": "Severity", + "risk_th_affected": "Affected Types", + "risk_scope_wide": "Scope-wide", + "risk_group": "{severity} · {n}", + "es_risk_caption": ( + "Affected Types = number of discovered resource types the risk applies " + "to. Severity levels: see the Risk Severity Levels chapter." + ), + "es_heading_alternative_technologies": "Alternative Technologies", + "es_alttech_lead": ( + "Replacement options for the discovered resource types under the " + "{strategy} exit strategy. Rows without an alternative are potential " + "lock-in hot-spots and are listed first." + ), + "alt_stat_types": "Types with Alternatives", + "alt_stat_types_value": "{a} of {n}", + "alt_stat_alternatives": "Alternatives", + "alt_stat_hotspots": "Lock-in Hot-spots", + "alt_th_resource_type": "Resource Type", + "alt_th_resources": "Resources", + "alt_th_alternatives": "Alternatives", + "alt_th_examples": "Examples", + "alt_hotspot_short": "No alternative in the data catalogue — lock-in hot-spot", + "alt_more": "+{k} more", + "alt_flag_open_source": "Open source", + "alt_flag_support_plan": "Support plan", + "alt_no_resources": "No resources were discovered in this assessment, so there are no alternatives to show.", + "alt_empty_set": "No alternative technologies were found for the discovered resource types.", + "es_alttech_caption": "Examples are listed alphabetically; this report does not rank the alternatives. Open source and support plan as recorded in the data catalogue.", + "es_heading_severity": "Risk Severity Levels", + "es_severity_lead": ( + "Severity expresses the expected impact of a risk on the effort and " + "complexity of an exit under the selected exit strategy. It is " + "defined for each risk in the risk catalogue and is the same in " + "every assessment in which the risk occurs. Where a risk depends on " + "a threshold, the threshold is stated in the risk description (for " + "example ‘>30 resources’)." + ), + "es_severity_note": ( + "Severities and thresholds are expert-based benchmarks; they are not " + "derived from an industry standard. Severity is not a probability " + "and does not mean that an exit is impossible." + ), + "es_severity_th_meaning": "Meaning", + "es_severity_critical": ( + "Major expected impact on the exit: large parts of the scope are " + "affected, or a significant redesign, rebuild or specialist effort " + "is likely. Should be addressed early in exit planning." + ), + "es_severity_high": ( + "Significant additional effort, cost or time for the exit; a " + "specific mitigation should be planned." + ), + "es_severity_medium": ( + "Moderate additional effort or complexity; manageable within normal " + "exit planning." + ), + "es_severity_low": ( + "Minor expected impact; usually handled during regular migration work." + ), + "es_heading_glossary": "Glossary", + "es_glossary_block": "Abbreviations and terms used in this summary.", + "glossary": [ + {"id": "aws", "term": "AWS", "definition": "Amazon Web Services"}, + { + "id": "data_catalogue", + "term": "Data catalogue", + "definition": "The catalogue of resource types, exit risks and alternative technologies used for this assessment; its version is recorded on page 2.", + }, + { + "id": "hotspot", + "term": "Lock-in hot-spot", + "definition": "A discovered resource type for which the data catalogue lists no alternative.", + }, + { + "id": "run", + "term": "Run", + "definition": "One completed assessment of the defined scope, identified by the run ID on page 2.", + }, + ], + "fa_heading": "What the Full Assessment Adds", + "fa_lead": ( + "This Executive Summary is the Basic assessment. It was collected and " + "evaluated locally and covers the resource inventory, " + "costs, exit risks and alternative technologies of the defined scope. " + "The full assessment on a platform adds:" + ), + "fa_items": [ + ( + "Exit Readiness Grade", + "a letter grade from A to F that shows how ready the defined scope is for an exit.", + ), + ( + "Vendor Lock-in Score", + "a rating of how easily each resource type can be replaced, in terms of skills, technology and operations.", + ), + ( + "Scored Alternatives", + "the alternative technologies for each resource type, rated and ranked.", + ), + ( + "Egress & Data Landscape", + "the data held by the resources in scope and an estimate of the one-time fee for moving it out.", + ), + ], + "fa_how_heading": "How to Get Them", + "fa_how": ( + "Connect cloudexit to a platform with an API key and run the " + "assessment again; the results are uploaded and evaluated there. For " + "example:" + ), + "fa_command": "cloudexit aws --host HOST --api-key-file KEY_FILE", + "fa_sites": "Platforms: {escapecloud} · {exitcloud}", + "fa_heading_uploaded": "The Full Assessment", + "fa_uploaded": ( + "This assessment was uploaded. The Exit Readiness Grade, the Vendor " + "Lock-in Score, scored alternatives and the Egress & Data Landscape " + "are available in its reports on the platform." + ), + "fa_uploaded_link": "Open the reports: {link}", + "fa_uploaded_id": "Assessment ID: {assessment_id}", +} + +# Labels and headings that follow English Title Case, for the wording test. +TITLE_CASE_KEYS = ( + "ev_document_information_heading", + "ev_assessment_scope_heading", + "ev_provenance_heading", + "ev_document_id", + "ev_document_type", + "ev_collected_by", + "ev_collected_at", + "ev_generated_at", + "ev_cloud_provider", + "ev_catalogue_label", + "ev_run_id", + "ev_platform_label", + "cover_title_executive_summary", + "cover_doc_id", + "cover_classification", + "cover_exit_strategy", + "cover_run", + "es_heading_introduction", + "es_heading_purpose", + "es_heading_scope", + "es_heading_method", + "es_heading_limitations", + "bs_heading", + "bs_findings_heading", + "bs_th_area", + "bs_th_finding", + "bs_th_details", + "bs_area_risks", + "bs_area_alternatives", + "bs_area_costs", + "bs_area_footprint", + "bs_stat_costs", + "es_heading_costs_overview", + "cost_stat_total", + "cost_stat_average", + "cost_stat_latest", + "cost_stat_change", + "cost_th_month", + "cost_th_cost", + "cost_th_change", + "es_heading_resource_inventory", + "res_stat_resources", + "res_stat_types", + "res_stat_regions", + "res_th_resource_type", + "res_th_regions", + "res_th_resources", + "res_th_share", + "res_total", + "es_heading_risk_inventory", + "risk_stat_unique", + "risk_stat_critical_high", + "risk_stat_affected", + "risk_stat_scope_wide", + "risk_th_code", + "risk_th_risk", + "risk_th_severity", + "risk_th_affected", + "es_heading_alternative_technologies", + "alt_stat_types", + "alt_stat_alternatives", + "alt_stat_hotspots", + "alt_th_resource_type", + "alt_th_resources", + "alt_th_alternatives", + "alt_th_examples", + "es_heading_severity", + "es_severity_th_meaning", + "es_heading_glossary", + "fa_heading", + "fa_how_heading", + "fa_heading_uploaded", +) diff --git a/core/utils_report.py b/core/utils_report.py index c10b725..396b48a 100644 --- a/core/utils_report.py +++ b/core/utils_report.py @@ -7,17 +7,12 @@ from jinja2 import Environment # ReportLab -from reportlab.lib.pagesizes import A4 from reportlab.lib.units import cm -from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib import colors from reportlab.lib.colors import HexColor from reportlab.platypus import ( - SimpleDocTemplate, Paragraph, Spacer, - PageBreak, - Image, Table, TableStyle, ) @@ -34,17 +29,6 @@ transform_risk_inventory_for_json, transform_alt_tech_for_json, ) -from core.utils_report_pdf import ( - transform_resource_inventory_for_pdf, - transform_cost_inventory_for_pdf, - transform_risk_inventory_for_pdf, - transform_alt_tech_for_pdf, - draw_header_footer, - draw_risk_chart, - draw_cost_chart, - draw_vendor_lockin_radar_chart, - draw_exitscore_pie_chart, -) # Configure logger logger = logging.getLogger("core.engine.report") @@ -74,6 +58,7 @@ def generate_html_report( alternative_technologies: list[dict[str, Any]], exit_strategy: int, alternative_technology_organizations: list[dict[str, Any]] | None = None, + summary: dict[str, Any] | None = None, ) -> str: # Transform resource inventory @@ -155,6 +140,7 @@ def generate_html_report( total_resources=total_resources, resource_inventory=resource_counts, alternative_technologies=alternative_technologies_data, + summary=summary, ) # Save HTML report @@ -177,6 +163,7 @@ def generate_json_report( alternatives: list[dict[str, Any]], alternative_technologies: list[dict[str, Any]], exit_strategy: int, + provenance: dict[str, Any] | None = None, ) -> str: # Transform data for JSON transformed_resource_inventory = transform_resource_inventory_for_json( @@ -200,6 +187,9 @@ def generate_json_report( }, } + if provenance: + report_json["provenance"] = provenance + # Add scoring_data only if present if scoring_data: report_json["data"]["scoring_data"] = { @@ -403,451 +393,3 @@ def _build_scope_section( content.append(scope_table) content.append(Spacer(1, 12)) return content - - -def _build_cost_section(cost_data, styles, content_style): - """Page 1: Cost chart and table.""" - content = [] - content.append(Paragraph("Costs", styles["Heading2"])) - - tablecontent_style = styles["BodyText"] - costs_block = "Examining the costs reveals the financial impact of the transition, allowing for more informed decision-making and strategic planning." - costs_paragraph = Paragraph(costs_block, tablecontent_style) - - months, costs, currency_symbol = transform_cost_inventory_for_pdf(cost_data) - cost_chart = draw_cost_chart(months, costs) - - costcharts_table_data = [ - [costs_paragraph, "", "", cost_chart, "", ""], - months, - [f"{currency_symbol} {cost:.2f}" for cost in costs], - ] - - costcharts_table = Table(costcharts_table_data, colWidths=[2.58333333333 * cm] * 6) - costcharts_table_style = TableStyle( - [ - ("SPAN", (0, 0), (2, 0)), - ("SPAN", (3, 0), (5, 0)), - ("VALIGN", (0, 0), (2, 0), "TOP"), - ("ALIGN", (0, 0), (2, 0), "LEFT"), - ("LEFTPADDING", (0, 0), (2, 0), 0), - ("RIGHTPADDING", (0, 0), (2, 0), 0), - ("TOPPADDING", (0, 0), (2, 0), 0), - ("BOTTOMPADDING", (0, 0), (2, 0), 0), - ("BACKGROUND", (0, 1), (-1, 1), HexColor("#115e59")), - ("TEXTCOLOR", (0, 1), (-1, 1), colors.white), - ("FONTNAME", (0, 1), (-1, 1), "Helvetica-Bold"), - ("ALIGN", (0, 1), (-1, 1), "CENTER"), - ("FONTNAME", (0, 2), (-1, 2), "Helvetica"), - ("ALIGN", (0, 2), (-1, 2), "CENTER"), - ("GRID", (0, 1), (-1, 2), 1, colors.black), - ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), - ("VALIGN", (0, 0), (2, 0), "TOP"), - ] - ) - costcharts_table.setStyle(costcharts_table_style) - content.append(costcharts_table) - content.append(PageBreak()) - return content - - -def _build_risk_section( - risk_data, risk_definitions, resource_inventory, report_path, styles, content_style -): - """Page 2: Risk Assessment chart and table.""" - content = [] - tablecontent_style = styles["BodyText"] - - content.append(Spacer(1, 12)) - content.append(Paragraph("Risk Assessment", styles["Heading1"])) - content.append( - Paragraph( - "The Risk Assessment provides a thorough evaluation of potential risks " - "associated with the cloud resources utilized in the project and the " - "alternative technologies available in the market:", - content_style, - ) - ) - content.append(Spacer(1, 12)) - - risks, severity_counts = transform_risk_inventory_for_pdf( - risk_data, risk_definitions, resource_inventory - ) - - risk_chart_data = { - "high": severity_counts["high"], - "medium": severity_counts["medium"], - "low": severity_counts["low"], - } - content.append(draw_risk_chart(risk_chart_data)) - content.append(Spacer(1, 12)) - - severity_order = {"high": 1, "medium": 2, "low": 3} - risks.sort(key=lambda r: severity_order[r["severity"]]) - - severity_icon_map = { - "high": (os.path.join(report_path, "assets/icons/severity/high.png"), 22.5, 12), - "medium": ( - os.path.join(report_path, "assets/icons/severity/medium.png"), - 39, - 12, - ), - "low": (os.path.join(report_path, "assets/icons/severity/low.png"), 20.5, 12), - } - - risk_table_data = [["#", "Risk name", "Impacted", "Severity"]] - for i, risk in enumerate(risks): - impacted_str = ( - str(risk["impacted_resources_count"]) - if risk["impacted_resources_count"] > 0 - else "-" - ) - severity_level = risk["severity"].lower() - icon_details = severity_icon_map.get(severity_level, None) - - if icon_details: - icon_path, icon_width, icon_height = icon_details - if os.path.exists(icon_path): - severity_icon = Image(icon_path, width=icon_width, height=icon_height) - else: - severity_icon = Paragraph("N/A", tablecontent_style) - else: - severity_icon = Paragraph("N/A", tablecontent_style) - - risk_table_data.append([str(i + 1), risk["name"], impacted_str, severity_icon]) - - total_risks = len(risks) - risk_table_data.append(["Total Risks", "", "", str(total_risks)]) - - risk_table = Table(risk_table_data, colWidths=[0.5 * cm, 10 * cm, 3 * cm, 2 * cm]) - risk_table_style_commands = [ - ("BACKGROUND", (0, 0), (-1, 0), HexColor("#115e59")), - ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), - ("BACKGROUND", (0, -1), (-1, -1), HexColor("#115e59")), - ("TEXTCOLOR", (0, -1), (-1, -1), colors.white), - ("BOX", (0, 0), (-1, -1), 1, HexColor("#112726")), - ("BOTTOMPADDING", (0, 0), (-1, 0), 12), - ("TOPPADDING", (0, 0), (-1, 0), 12), - ("ALIGN", (0, 1), (0, -2), "LEFT"), - ("VALIGN", (0, 1), (0, -2), "MIDDLE"), - ("ALIGN", (1, 1), (1, -2), "LEFT"), - ("VALIGN", (1, 1), (1, -2), "MIDDLE"), - ("ALIGN", (2, 1), (2, -2), "CENTER"), - ("VALIGN", (2, 1), (2, -2), "MIDDLE"), - ("ALIGN", (3, 1), (3, -2), "CENTER"), - ("VALIGN", (3, 1), (3, -2), "MIDDLE"), - ("ALIGN", (-1, 0), (-1, 0), "CENTER"), - ("VALIGN", (-1, 0), (-1, 0), "MIDDLE"), - ("ALIGN", (-1, -1), (-1, -1), "CENTER"), - ("VALIGN", (-1, -1), (-1, -1), "MIDDLE"), - ] - risk_table.setStyle(TableStyle(risk_table_style_commands)) - content.append(risk_table) - content.append(PageBreak()) - return content - - -def _build_scoring_section(scoring_data, report_path, styles, content_style): - """Page 3: EscapeCloud Scoring (exit score gauge + vendor lock-in radar).""" - content = [] - content.append(Spacer(1, 12)) - content.append(Paragraph("EscapeCloud Scoring", styles["Heading1"])) - content.append(Paragraph("Scoring #1 - Exit Readiness", styles["Heading2"])) - content.append( - Paragraph( - "The Exit Readiness Grade provides a consolidated indicator of how prepared " - "the assessed environment is for a potential cloud exit or migration scenario:", - content_style, - ) - ) - content.append(Spacer(1, 12)) - - exit_score = scoring_data.get("exit_score", 0) if scoring_data else 0 - exit_score_chart = draw_exitscore_pie_chart(exit_score, size=5 * cm) - - exitscore_table_data = [ - ["", ""], - ["A - Seamless", ""], - ["B - Smooth Transition", ""], - ["C - Manageable", ""], - ["D - Challenging", ""], - ["E - Complex", ""], - ["F - Very Complex", ""], - ] - exitscore_table_data[1][1] = exit_score_chart - - exitscore_table = Table(exitscore_table_data, colWidths=[5 * cm, 10.5 * cm]) - exitscore_table_style = TableStyle( - [ - ("SPAN", (0, 0), (1, 0)), - ("BACKGROUND", (0, 0), (1, 0), HexColor("#115e59")), - ("TEXTCOLOR", (0, 0), (1, 0), colors.white), - ("FONTNAME", (0, 0), (1, 0), "Helvetica-Bold"), - ("ALIGN", (0, 0), (1, 0), "CENTER"), - ("VALIGN", (0, 0), (1, 0), "MIDDLE"), - ("SPAN", (1, 1), (1, 6)), - ("GRID", (0, 0), (-1, -1), 1, colors.black), - ("ALIGN", (0, 1), (0, 6), "LEFT"), - ("VALIGN", (0, 1), (0, 6), "MIDDLE"), - ("ALIGN", (1, 1), (1, 1), "CENTER"), - ("VALIGN", (1, 1), (1, 1), "MIDDLE"), - ] - ) - exitscore_table.setStyle(exitscore_table_style) - content.append(exitscore_table) - content.append(Spacer(1, 12)) - - # Vendor Lock-In Score - content.append(Paragraph("Scoring #2 - Vendor Lock-In Score", styles["Heading2"])) - content.append( - Paragraph( - "The following radar chart visualizes the assessment of alternative " - "technologies across three dimensions: Human (skills availability), " - "Technology (maturity and vendor stability), and Operational (ecosystem " - "and support services) — only where viable alternatives exist:", - content_style, - ) - ) - content.append(Spacer(1, 12)) - - human_score = scoring_data.get("human_score", 0) if scoring_data else 0 - technology_score = scoring_data.get("technology_score", 0) if scoring_data else 0 - operational_score = scoring_data.get("operational_score", 0) if scoring_data else 0 - - content.append( - draw_vendor_lockin_radar_chart(human_score, technology_score, operational_score) - ) - - vendor_lockin_table = Table( - [ - ["Human", "Technology", "Operational"], - [human_score, technology_score, operational_score], - ], - colWidths=[5 * cm, 5 * cm, 5 * cm], - ) - vendor_lockin_table_style = TableStyle( - [ - ("BACKGROUND", (0, 0), (-1, 0), HexColor("#115e59")), - ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), - ("ALIGN", (0, 0), (-1, 0), "CENTER"), - ("VALIGN", (0, 0), (-1, 0), "MIDDLE"), - ("ALIGN", (0, 0), (-1, -1), "CENTER"), - ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), - ("GRID", (0, 0), (-1, -1), 1, colors.black), - ] - ) - vendor_lockin_table.setStyle(vendor_lockin_table_style) - content.append(vendor_lockin_table) - content.append(PageBreak()) - return content - - -def _build_resource_section( - resource_inventory, resource_type_mapping, report_path, styles, content_style -): - """Page 4: Resource Inventory table.""" - content = [] - content.append(Spacer(1, 12)) - content.append(Paragraph("Resource Inventory", styles["Heading1"])) - content.append( - Paragraph( - "The Resource Inventory provides a summary of the cloud resources " - "provisioned within the defined scope:", - content_style, - ) - ) - content.append(Spacer(1, 12)) - - resources = transform_resource_inventory_for_pdf( - resource_inventory, resource_type_mapping, report_path - ) - total_resources = sum(res["count"] for res in resources) - - resource_data = [["#", "Resource type", "", "No."]] - for res in resources: - resource_data.append( - [ - str(res["id"]), - res["resource_name"], - Image(res["icon_url"], width=20, height=20), - str(res["count"]), - ] - ) - resource_data.append(["Total Resources", "", "", str(total_resources)]) - - res_table = Table(resource_data, colWidths=[1 * cm, 11.5 * cm, 1.5 * cm, 1.5 * cm]) - res_table_style_commands = [ - ("BACKGROUND", (0, 0), (-1, 0), HexColor("#115e59")), - ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), - ("BACKGROUND", (0, -1), (-1, -1), HexColor("#115e59")), - ("TEXTCOLOR", (0, -1), (-1, -1), colors.white), - ("BOX", (0, 0), (-1, -1), 1, HexColor("#112726")), - ("BOTTOMPADDING", (0, 0), (-1, 0), 12), - ("TOPPADDING", (0, 0), (-1, 0), 12), - ("ALIGN", (0, 1), (0, -2), "LEFT"), - ("VALIGN", (0, 1), (0, -2), "MIDDLE"), - ("ALIGN", (1, 1), (1, -2), "LEFT"), - ("VALIGN", (1, 1), (1, -2), "MIDDLE"), - ("ALIGN", (2, 1), (2, -2), "CENTER"), - ("VALIGN", (2, 1), (2, -2), "MIDDLE"), - ("ALIGN", (3, 1), (3, -2), "CENTER"), - ("VALIGN", (3, 1), (3, -2), "MIDDLE"), - ("ALIGN", (-1, 0), (-1, 0), "CENTER"), - ("VALIGN", (-1, 0), (-1, 0), "MIDDLE"), - ("ALIGN", (-1, -1), (-1, -1), "CENTER"), - ("VALIGN", (-1, -1), (-1, -1), "MIDDLE"), - ] - res_table.setStyle(TableStyle(res_table_style_commands)) - content.append(res_table) - content.append(PageBreak()) - return content - - -def _build_alt_tech_section( - resource_inventory, - resource_type_mapping, - alternatives, - alternative_technologies, - exit_strategy, - report_path, - styles, - content_style, -): - """Page 5: Alternative Technologies table.""" - content = [] - content.append(Spacer(1, 12)) - content.append(Paragraph("Alternative Technologies", styles["Heading1"])) - content.append( - Paragraph( - "The Alternative Technology provides a summary of the alternative technology " - "landscape for each identified resource in the Resource Inventory, based on " - "our dataset and market research. It also includes a count of the available " - "alternative technologies for each resource:", - content_style, - ) - ) - content.append(Spacer(1, 12)) - - alttech = transform_alt_tech_for_pdf( - resource_inventory, - resource_type_mapping, - alternatives, - alternative_technologies, - exit_strategy, - report_path, - ) - - alttech_data = [["#", "Resource type", "", "No."]] - for res in alttech: - alttech_data.append( - [ - str(res["id"]), - res["resource_name"], - Image(res["icon_url"], width=20, height=20) if res["icon_url"] else "", - str(res["count"]), - ] - ) - - alttech_table = Table( - alttech_data, colWidths=[1 * cm, 11.5 * cm, 1.5 * cm, 1.5 * cm] - ) - alttech_table_style_commands = [ - ("BACKGROUND", (0, 0), (-1, 0), HexColor("#115e59")), - ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), - ("BOX", (0, 0), (-1, -1), 1, HexColor("#000000")), - ("BOTTOMPADDING", (0, 1), (-1, -1), 6), - ("TOPPADDING", (0, 1), (-1, -1), 6), - ("ALIGN", (2, 0), (2, -1), "CENTER"), - ("VALIGN", (2, 0), (2, -1), "MIDDLE"), - ("ALIGN", (0, 1), (0, -1), "LEFT"), - ("VALIGN", (0, 1), (0, -1), "MIDDLE"), - ("ALIGN", (1, 1), (1, -1), "LEFT"), - ("VALIGN", (1, 1), (1, -1), "MIDDLE"), - ("ALIGN", (2, 1), (2, -1), "CENTER"), - ("VALIGN", (2, 1), (2, -1), "MIDDLE"), - ("ALIGN", (3, 1), (3, -1), "CENTER"), - ("VALIGN", (3, 1), (3, -1), "MIDDLE"), - ("ALIGN", (-1, 0), (-1, 0), "CENTER"), - ("VALIGN", (-1, 0), (-1, 0), "MIDDLE"), - ] - alttech_table.setStyle(TableStyle(alttech_table_style_commands)) - content.append(alttech_table) - content.append(PageBreak()) - return content - - -def generate_pdf_report( - provider_details: dict[str, Any], - report_path: str, - metadata: dict[str, Any], - resource_type_mapping: dict[str, Any], - resource_inventory: list[dict[str, Any]], - cost_data: list[dict[str, Any]], - scoring_data: dict[str, Any] | None, - risk_data: list[dict[str, Any]], - risk_definitions: list[dict[str, Any]], - alternatives: list[dict[str, Any]], - alternative_technologies: list[dict[str, Any]], - exit_strategy: int, - tfstate_scope: dict[str, Any] | None = None, -) -> str: - pdf_path = os.path.join(report_path, "report.pdf") - - def header_footer(canvas, doc): - draw_header_footer(report_path, canvas, doc) - - doc = SimpleDocTemplate( - pdf_path, pagesize=A4, title="EscapeCloud_-_Cloud_Exit_Assessment" - ) - styles = getSampleStyleSheet() - content_style = ParagraphStyle( - "ContentStyle", fontSize=10, leading=12, spaceAfter=10 - ) - styles["Heading1"].leading = 1.5 * styles["Heading1"].fontSize - styles["Heading1"].textColor = HexColor("#112726") - styles["Heading2"].leading = 1.5 * styles["Heading2"].fontSize - styles["Heading2"].textColor = HexColor("#112726") - - content = [] - content += _build_summary_section(metadata, styles, content_style) - content += _build_scope_section( - metadata, provider_details, styles, content_style, tfstate_scope - ) - content += _build_cost_section(cost_data, styles, content_style) - content += _build_risk_section( - risk_data, - risk_definitions, - resource_inventory, - report_path, - styles, - content_style, - ) - if metadata.get("assessment_type") == 2: - content += _build_scoring_section( - scoring_data, - report_path, - styles, - content_style, - ) - content += _build_resource_section( - resource_inventory, - resource_type_mapping, - report_path, - styles, - content_style, - ) - content += _build_alt_tech_section( - resource_inventory, - resource_type_mapping, - alternatives, - alternative_technologies, - exit_strategy, - report_path, - styles, - content_style, - ) - - logger.debug("Building the PDF document...") - doc.build(content, onFirstPage=header_footer, onLaterPages=header_footer) - - return pdf_path diff --git a/main.py b/main.py index 8ced425..b5cb01e 100644 --- a/main.py +++ b/main.py @@ -936,6 +936,7 @@ def run_assessment( report_path, raw_data_path, upload=report_info(upload_result), + sync_id=sync_id, ) # Handle the result diff --git a/pyproject.toml b/pyproject.toml index a43592b..5bbd5d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dev = [ "ruff", "jsonschema", "PyYAML", + "pypdf", ] [project.scripts] diff --git a/requirements-dev.txt b/requirements-dev.txt index fae7aa9..e0c0617 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,3 +3,4 @@ black==26.10.1 ruff==0.17.1 jsonschema==4.26.0 PyYAML==6.0.3 +pypdf==6.21.2 diff --git a/tests/flow_fixtures.py b/tests/flow_fixtures.py index 90944f7..faf7ad9 100644 --- a/tests/flow_fixtures.py +++ b/tests/flow_fixtures.py @@ -1,7 +1,7 @@ import sqlite3 from pathlib import Path -from tests.report_fixtures import stage_report_assets +from tests.report_fixtures import stage_minimal_assets from tests.sync_fixtures import build_run FLOW_TABLES = """ @@ -65,5 +65,5 @@ def build_flow_run(report_path, *, platform_scores=False, **kwargs): ) conn.commit() conn.close() - stage_report_assets(report) + stage_minimal_assets(report) return report diff --git a/tests/report_fixtures.py b/tests/report_fixtures.py index a3ff6d7..0ec9977 100644 --- a/tests/report_fixtures.py +++ b/tests/report_fixtures.py @@ -124,3 +124,56 @@ def stage_report_assets(report_path: str) -> None: report_assets / folder, dirs_exist_ok=True, ) + + +def build_fixture_context(fixture, report_dir, **overrides): + # A report context from a fixture, for rendering the Basic report. + from datetime import datetime, timezone + + from core.report.context import Provenance, build_context + + collected = datetime(2026, 5, 7, 12, 0, tzinfo=timezone.utc) + provenance = overrides.pop( + "provenance", + Provenance( + collector_version="2.0.0", + collected_at=collected, + generated_at=collected, + run_id="01939b8e-7c2a-7d41-9a51-3c8f0a6e21d4", + catalogue_version="2026-11-02", + catalogue_sequence=41, + catalogue_sha256="c" * 64, + ), + ) + values = dict( + metadata=fixture["metadata"], + provider_details=fixture["provider_details"], + resource_type_mapping=fixture["resource_type_mapping"], + resource_inventory=fixture["resource_inventory"], + cost_data=fixture["cost_data"], + risk_data=fixture["risk_data"], + risk_definitions=fixture["risk_definitions"], + alternatives=fixture["alternatives"], + alternative_technologies=fixture["alternative_technologies"], + alternative_technology_organizations=fixture[ + "alternative_technology_organizations" + ], + exit_strategy=fixture["exit_strategy"], + tfstate_scope=None, + provenance=provenance, + report_path=str(report_dir), + ) + values.update(overrides) + return build_context(**values) + + +def stage_minimal_assets(report_path: str) -> None: + # What the Basic report needs: the logo and the fallback icon. Copying the + # whole icon set for every render would dominate the test time. + report_assets = Path(report_path) / "assets" + shutil.copytree(Path("assets") / "img", report_assets / "img", dirs_exist_ok=True) + shutil.copytree( + Path("assets") / "icons" / "misc", + report_assets / "icons" / "misc", + dirs_exist_ok=True, + ) diff --git a/tests/report_terms.py b/tests/report_terms.py new file mode 100644 index 0000000..6f356dc --- /dev/null +++ b/tests/report_terms.py @@ -0,0 +1,148 @@ +import re + +ACRONYMS = ( + "DORA", + "FINMA", + "PRA", + "ECB", + "EBA", + "EBF", + "ESMA", + "EIOPA", + "ESAs", + "CTPP", + "ICT", + "GDPR", + "NIS2", + "FCA", + "BaFin", + "MaRisk", + "BAIT", + "SS2/21", +) + +PHRASES = ( + "regulation", + "regulatory", + "regulator", + "regulated", + "supervisor", + "supervisory", + "competent authority", + "financial entity", + "outsourcing", + "compliance", + "compliant", + "article 28", + "art. 28", + "circular", + "european banking federation", + "cloud banking forum", + "european central bank", + "control demand", + "rating grid", + "concentration risk", + "third-party service provider", + "regulatory framework", + "2022/2554", +) + +# Branding of other products' reports. The platforms are named once, on the +# closing page, and that page is checked separately. +FOREIGN_BRANDS = ( + "Community Edition", + "Generated with exitcloud.io", + "exitcloud.io document", +) + + +def find_terms(text): + found = [ + acronym + for acronym in ACRONYMS + if re.search(rf"(?]+>", " ", label).split() + for index, word in enumerate(words): + core = word.strip("(),.:;·–—’'\"+/") + if not core or not core[0].isalpha(): + continue + if word.lower().rstrip(",") in TITLE_SMALL_WORDS: + if index and word != word.lower(): + return False + if index == 0 and word.lower() != "vs." and not word[0].isupper(): + return False + continue + if not core[0].isupper(): + return False + return True + + +def retired_term_hits(text): + hits = [] + for pattern, allowed in RETIRED_TERMS: + for match in re.finditer(pattern, text, re.IGNORECASE): + context = text[max(0, match.start() - 40) : match.end() + 40] + if not any(phrase in context for phrase in allowed): + hits.append(context) + return hits + + +def american_hits(text): + hits = [] + for match in AMERICAN.finditer(text): + context = text[max(0, match.start() - 30) : match.end() + 30] + if not any(name in context for name in AMERICAN_ALLOWED): + hits.append(context) + return hits diff --git a/tests/test_basic_report.py b/tests/test_basic_report.py new file mode 100644 index 0000000..77c467b --- /dev/null +++ b/tests/test_basic_report.py @@ -0,0 +1,782 @@ +import copy +import itertools +import json +import re +import tempfile +import unittest +from datetime import datetime, timezone +from html.parser import HTMLParser +from pathlib import Path + +from pypdf import PdfReader + +from core.engine import generate_report, perform_risk_assessment +from core.report import fonts +from core.report.context import Provenance +from core.report.executive_summary import generate_executive_summary +from core.report.palette import ( + DEFAULT_APPEARANCE, + Appearance, + contrast_failures, + contrast_pairs, + contrast_ratio, +) +from core.report.wording import BLOCKS, TITLE_CASE_KEYS +from tests.flow_fixtures import build_flow_run +from tests.report_fixtures import ( + build_empty_report_fixture, + build_fixture_context, + build_report_fixture, + stage_minimal_assets, +) +from tests.report_terms import ( + american_hits, + find_terms, + is_title_case, + retired_term_hits, +) +from tests.sync_fixtures import SYNC_ID +from utils.version import __version__ + +URL = "https://platform.test/assessments/a-1" +COLLECTED = datetime(2026, 5, 7, 12, 0, tzinfo=timezone.utc) +BASE_OUTLINE = [ + (0, "Brief Summary"), + (1, "Key Findings"), + (0, "Introduction"), + (1, "Purpose"), + (1, "Scope"), + (1, "Method"), + (1, "Limitations"), + (0, "Costs Overview"), + (0, "Resource Inventory"), + (0, "Risk Inventory"), + (0, "Alternative Technologies"), + (0, "Risk Severity Levels"), + (0, "Glossary"), +] +OFFLINE_OUTLINE = BASE_OUTLINE + [ + (0, "What the Full Assessment Adds"), + (1, "How to Get Them"), +] +UPLOADED_OUTLINE = BASE_OUTLINE + [(0, "The Full Assessment")] +SCORE_PATTERNS = ( + r"/ 100", + r"/ 5\b", + "Ø", + r"\bgrade [A-F]\b", + "Exit Readiness", + "Lock-in Score", + "Scored Alternatives", +) +TFSTATE_SCOPE = { + "file": "infra.tfstate", + "sha256": "ab" * 32, + "lineage": "e1c2f0c0-0000-4000-8000-000000000000", + "serial": 7, + "locations": ["eu-west-1"], +} + + +def provenance(**changes): + values = dict( + collector_version=__version__, + collected_at=COLLECTED, + generated_at=COLLECTED, + run_id=SYNC_ID, + catalogue_version="2026-11-02", + catalogue_sequence=41, + catalogue_sha256="c" * 64, + ) + values.update(changes) + return Provenance(**values) + + +def uploaded(**changes): + return provenance(uploaded=True, platform_url=URL, assessment_id="a-1", **changes) + + +class TextOfHtml(HTMLParser): + def __init__(self): + super().__init__() + self.parts = [] + self.skip = 0 + + def handle_starttag(self, tag, attrs): + if tag in ("script", "style"): + self.skip += 1 + + def handle_endtag(self, tag): + if tag in ("script", "style") and self.skip: + self.skip -= 1 + + def handle_data(self, data): + if not self.skip: + self.parts.append(data) + + +def html_text(html): + parser = TextOfHtml() + parser.feed(html) + return " ".join(" ".join(parser.parts).split()) + + +def page_texts(reader): + return [" ".join(page.extract_text().split()) for page in reader.pages] + + +def outline_entries(items, level=0): + entries = [] + for item in items: + if isinstance(item, list): + entries += outline_entries(item, level + 1) + else: + entries.append((level, item.title)) + return entries + + +def xmp_text(reader): + metadata = reader.trailer["/Root"]["/Metadata"].get_object() + return metadata.get_data().decode("utf-8") + + +def metadata_texts(reader): + info = reader.metadata + return [str(value) for value in info.values()] + [xmp_text(reader)] + + +def link_targets(reader): + targets = set() + for page in reader.pages: + for annotation in page.get("/Annots", []): + action = annotation.get_object().get("/A") + if action and "/URI" in action: + targets.add(str(action["/URI"])) + return targets + + +_RENDERS = {} +_COUNTER = itertools.count() +_SHARED = None + + +def setUpModule(): + global _SHARED + _SHARED = tempfile.TemporaryDirectory() + + +def tearDownModule(): + _RENDERS.clear() + _SHARED.cleanup() + + +class BasicReportTestCase(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.tmp = Path(tmp.name) + self.count = 0 + + def report_dir(self): + self.count += 1 + path = self.tmp / f"report-{self.count}" + path.mkdir() + stage_minimal_assets(str(path)) + return path + + def azure_fixture(self): + fixture = copy.deepcopy(build_report_fixture()) + fixture["metadata"]["cloud_service_provider"] = 1 + fixture["provider_details"] = { + "tenantId": "11111111-1111-4111-8111-111111111111", + "subscriptionId": "22222222-2222-4222-8222-222222222222", + "resourceGroupName": "Prod-RG", + "clientSecret": "very-secret-client-value", + } + return fixture + + def tfstate_fixture(self): + fixture = copy.deepcopy(build_report_fixture()) + fixture["provider_details"] = {"tfstatePath": "infra.tfstate"} + fixture["cost_data"] = [] + return fixture + + def scenarios(self): + aws = build_report_fixture() + for name, fixture, extra in ( + ("aws", aws, {}), + ("azure", self.azure_fixture(), {}), + ("tfstate", self.tfstate_fixture(), {"tfstate_scope": TFSTATE_SCOPE}), + ): + for state, prov in (("offline", provenance()), ("uploaded", uploaded())): + yield f"{name} {state}", fixture, {**extra, "provenance": prov} + + def render(self, fixture=None, key=None, **overrides): + # Rendering is the slow part, so tests that read the same report share it. + if key in _RENDERS: + return _RENDERS[key] + directory = Path(_SHARED.name) / f"report-{next(_COUNTER)}" + directory.mkdir() + stage_minimal_assets(str(directory)) + context = build_fixture_context( + fixture or build_report_fixture(), directory, **overrides + ) + path = generate_executive_summary(context) + result = (path, context, PdfReader(path)) + if key: + _RENDERS[key] = result + return result + + +class SmokeRenderTests(BasicReportTestCase): + def test_every_provider_renders_offline_and_uploaded(self): + for name, fixture, overrides in self.scenarios(): + with self.subTest(name): + path, _context, reader = self.render(fixture, key=name, **overrides) + + self.assertEqual(Path(path).name, "report.pdf") + self.assertGreaterEqual(len(reader.pages), 9) + self.assertTrue(all(page_texts(reader))) + + def test_an_empty_assessment_renders(self): + fixture = build_empty_report_fixture() + + _path, _context, reader = self.render(fixture) + + texts = " ".join(page_texts(reader)) + self.assertIn("There are no resources", texts) + self.assertIn("There are no risks identified", texts) + self.assertIn("No cost data available", texts) + + def test_a_tfstate_assessment_explains_the_missing_costs(self): + _path, _context, reader = self.render( + self.tfstate_fixture(), tfstate_scope=TFSTATE_SCOPE + ) + + text = " ".join(page_texts(reader)) + self.assertIn("Terraform state files hold no billing data", text) + self.assertIn("infra.tfstate", text) + self.assertIn("e1c2f0c0-0000-4000-8000-000000000000 (serial 7)", text) + + +class StructureTests(BasicReportTestCase): + def test_the_heading_outline_is_the_expected_list(self): + for name, fixture, overrides in self.scenarios(): + with self.subTest(name): + _path, _context, reader = self.render(fixture, key=name, **overrides) + expected = UPLOADED_OUTLINE if "uploaded" in name else OFFLINE_OUTLINE + self.assertEqual(outline_entries(reader.outline), expected) + + def test_the_outline_does_not_depend_on_the_data(self): + _path, _context, reader = self.render( + build_empty_report_fixture(), provenance=provenance() + ) + self.assertEqual(outline_entries(reader.outline), OFFLINE_OUTLINE) + + def test_each_chapter_starts_a_page_and_page_two_is_the_document_information(self): + _path, _context, reader = self.render(key="default") + pages = page_texts(reader) + + self.assertIn("Executive Summary", pages[0]) + self.assertNotIn("Page 1 of", pages[0]) + self.assertTrue(pages[1].startswith("Executive Summary")) + self.assertIn("Document Information", pages[1]) + self.assertIn(f"Page 2 of {len(pages)}", pages[1]) + self.assertIn("Brief Summary", pages[2]) + + chapters = [entry for entry in reader.outline if not isinstance(entry, list)] + numbers = [reader.get_destination_page_number(entry) for entry in chapters] + # One chapter per page, except the severity levels and the glossary + self.assertEqual(len(set(numbers)), len(numbers) - 1) + self.assertEqual(numbers[-3], numbers[-2]) + + def test_running_header_and_footer(self): + path, context, reader = self.render(key="default") + pages = page_texts(reader) + + for index, text in enumerate(pages[1:], start=2): + self.assertIn("Smoke Test Assessment", text) + self.assertIn(context.document_id, text) + self.assertIn("CONFIDENTIAL", text) + self.assertIn(f"Page {index} of {len(pages)}", text) + + def test_the_fonts_are_plex_and_nothing_else(self): + path, _context, _reader = self.render(key="default") + names = set(re.findall(rb"/BaseFont\s*/(\S+)", Path(path).read_bytes())) + + self.assertTrue(names) + self.assertTrue(all(b"IBMPlex" in name for name in names), names) + + def test_the_fonts_ship_with_their_licence(self): + for filename in fonts.FONT_FILES.values(): + self.assertTrue((fonts.FONT_DIR / filename).is_file(), filename) + licence = (fonts.FONT_DIR / "OFL.txt").read_text(encoding="utf-8") + self.assertIn("SIL OPEN FONT LICENSE", licence) + + +class ContentTests(BasicReportTestCase): + def test_the_findings_use_the_local_results(self): + _path, _context, reader = self.render(key="default") + text = " ".join(page_texts(reader)) + + self.assertIn("Key Findings", text) + self.assertIn("unique exit risk", text) + self.assertIn("EC2 Instance", text) + self.assertIn("OpenStack", text) + self.assertIn("OpenInfra Foundation", text) + self.assertIn("Examples are listed alphabetically", text) + + def test_the_resource_inventory_lists_each_type_once(self): + _path, context, _reader = self.render(key="default") + + self.assertEqual([entry.name for entry in context.resources], ["EC2 Instance"]) + self.assertEqual( + context.total_resources, + sum(row["count"] for row in build_report_fixture()["resource_inventory"]), + ) + + def test_documents_are_identified_by_the_end_of_the_run_id(self): + _path, context, _reader = self.render(key="default") + self.assertEqual(context.document_id, "ES-0a6e21d4-EN") + + _path, local, _reader = self.render(provenance=provenance(run_id=None)) + self.assertEqual(local.document_id, "ES-LOCAL-EN") + + def test_costs_before_the_current_month_are_final(self): + fixture = build_report_fixture() + fixture["cost_data"] = [ + {"month": "2026-04-01", "cost": 10.0, "currency": "USD"}, + {"month": "2026-05-01", "cost": 12.0, "currency": "USD"}, + ] + _path, context, reader = self.render(fixture) + + self.assertEqual([entry.final for entry in context.costs], [True, False]) + self.assertIn("provisional", " ".join(page_texts(reader))) + + def test_mixed_currencies_are_not_totalled(self): + fixture = build_report_fixture() + fixture["cost_data"][0] = {**fixture["cost_data"][0], "currency": "EUR"} + + _path, _context, reader = self.render(fixture) + + self.assertIn("more than one currency", " ".join(page_texts(reader))) + + def test_the_pinned_risk_numbers_and_names_are_shown(self): + _path, context, reader = self.render(key="default") + + self.assertEqual([risk.code for risk in context.risks], ["R1"]) + text = " ".join(page_texts(reader)) + self.assertIn("R1", text) + self.assertIn("Limited Alternatives", text) + + def test_a_catalogue_severity_and_code_win_when_the_dataset_has_them(self): + fixture = build_report_fixture() + fixture["risk_definitions"] = [ + { + **fixture["risk_definitions"][0], + "code": "ALT-1", + "severity": "low", + "catalogue_severity": "critical", + } + ] + _path, context, _reader = self.render(fixture) + + self.assertEqual(context.risks[0].code, "ALT-1") + self.assertEqual(context.risks[0].severity, "critical") + + def test_names_from_the_data_are_escaped(self): + fixture = build_report_fixture() + fixture["metadata"]["name"] = "Prod & Co" + mapping = copy.deepcopy(fixture["resource_type_mapping"]) + mapping["101"]["name"] = "Bad name &" + fixture["resource_type_mapping"] = mapping + + _path, _context, reader = self.render(fixture) + + text = " ".join(page_texts(reader)) + self.assertIn("Prod & Co", text) + self.assertIn("Bad name &", text) + + +class AlternativesTests(BasicReportTestCase): + def test_a_missing_vendor_is_left_out_and_alternatives_are_alphabetical(self): + fixture = build_report_fixture() + technologies = fixture["alternative_technologies"] + technologies.append( + { + **technologies[0], + "id": 2, + "product_name": "Alpha", + "organization_id": None, + } + ) + fixture["alternatives"].append( + {"resource_type": "101", "strategy_type": "1", "alternative_technology": 2} + ) + + _path, context, reader = self.render(fixture) + + entry = context.alternatives[0] + self.assertEqual( + [alt.name for alt in entry.alternatives], ["Alpha", "OpenStack"] + ) + self.assertEqual(entry.alternatives[0].vendor, "") + self.assertEqual(entry.alternatives[1].vendor, "OpenInfra Foundation") + self.assertNotIn("Unknown Organization", " ".join(page_texts(reader))) + + +class ProvenanceTests(BasicReportTestCase): + def test_an_offline_report_says_it_is_self_collected(self): + path, _context, reader = self.render(key="offline", provenance=provenance()) + pages = page_texts(reader) + + self.assertIn( + f"Collected by cloudexit {__version__} on 2026-05-07", + pages[0], + ) + second = pages[1] + self.assertIn(f"Collected by cloudexit {__version__}", second) + self.assertIn(SYNC_ID, second) + self.assertIn("2026-11-02 · (sha256 cccccccccccc)", second) + self.assertIn("Self-collected data: no platform evaluated this report.", second) + self.assertIn("What the Full Assessment Adds", pages[-1]) + self.assertEqual( + link_targets(reader), {"https://escapecloud.io", "https://exitcloud.io"} + ) + + def test_an_uploaded_report_carries_the_link_instead_of_the_promotion(self): + _path, _context, reader = self.render(key="uploaded", provenance=uploaded()) + pages = page_texts(reader) + + self.assertIn("Uploaded to the platform", pages[1]) + self.assertIn(URL, pages[1]) + self.assertNotIn("Self-collected data", " ".join(pages)) + self.assertIn("The Full Assessment", pages[-1]) + self.assertIn(URL, pages[-1]) + self.assertNotIn("What the Full Assessment Adds", " ".join(pages)) + self.assertEqual(link_targets(reader), {URL}) + + def test_an_upload_without_a_link_names_the_assessment(self): + _path, _context, reader = self.render( + provenance=( + uploaded(platform_url=None) + if False + else provenance(uploaded=True, platform_url=None, assessment_id="a-77") + ) + ) + self.assertIn("Assessment ID: a-77", page_texts(reader)[-1]) + + def test_an_unknown_catalogue_version_is_said_so(self): + _path, _context, reader = self.render( + provenance=provenance( + catalogue_version=None, catalogue_sequence=None, catalogue_sha256=None + ) + ) + self.assertIn("Unknown (the dataset records no version)", page_texts(reader)[1]) + + def test_the_pdf_metadata_follows_the_cover(self): + path, context, reader = self.render(key="default") + info = reader.metadata + + self.assertEqual(info.title, "Executive Summary – Smoke Test Assessment") + self.assertIn(context.document_id, info.subject) + version = context.provenance.collector_version + self.assertIn(f"collected by cloudexit {version}", info.subject) + self.assertEqual(info.creator, f"cloudexit {version}") + self.assertEqual(info.author, "cloudexit") + for keyword in (context.document_id, SYNC_ID, "catalogue 2026-11-02"): + self.assertIn(keyword, info["/Keywords"]) + # The dates are the generation time, not the render clock + self.assertIn("20260507120000", info["/CreationDate"]) + + def test_the_xmp_packet_carries_the_provenance(self): + _path, _context, reader = self.render(key="uploaded", provenance=uploaded()) + packet = xmp_text(reader) + + for expected in ( + f"{SYNC_ID}", + f"{__version__}", + "2026-11-02", + "true", + "cloudexit", + ): + self.assertIn(expected, packet) + + def test_the_json_report_has_a_provenance_object(self): + for upload, expected_url in ( + (None, None), + ( + { + "status": "uploaded", + "sync_id": SYNC_ID, + "assessment_id": "a-1", + "url": URL, + }, + URL, + ), + ): + with self.subTest(uploaded=bool(upload)): + report = build_flow_run( + self.tmp / f"flow-{bool(upload)}", + resources=[(1, "eu-central-1", 3)], + costs=[("2026-09-01", 10.0, "USD")], + ) + perform_risk_assessment(1, report) + result = generate_report( + 2, + {"region": "eu-central-1"}, + 1, + 1, + "Flow", + report, + str(Path(report) / "raw_data"), + upload=upload, + sync_id=SYNC_ID, + ) + self.assertTrue(result["success"], result.get("logs")) + + document = json.loads(Path(result["reports"]["JSON"]).read_text()) + self.assertEqual( + document["provenance"], + { + "collector_version": __version__, + "catalogue_version": "2026-11-02", + "catalogue_sequence": 41, + "catalogue_sha256": "c" * 64, + "sync_id": SYNC_ID, + "uploaded": bool(upload), + "platform_url": expected_url, + }, + ) + html = (Path(report) / "index.html").read_text(encoding="utf-8") + text = html_text(html) + self.assertIn(SYNC_ID, text) + self.assertEqual(URL in html, bool(upload)) + self.assertEqual( + "Self-collected data: no platform evaluated this report." in text, + not upload, + ) + + +class BasicOnlyTests(BasicReportTestCase): + def test_no_score_or_grade_appears_outside_the_closing_page(self): + for name, fixture, overrides in self.scenarios(): + with self.subTest(name): + _path, _context, reader = self.render(fixture, key=name, **overrides) + body = " ".join(page_texts(reader)[:-1]) + + for pattern in SCORE_PATTERNS: + self.assertIsNone(re.search(pattern, body, re.IGNORECASE), pattern) + + def test_the_closing_page_only_names_what_the_full_assessment_adds(self): + _path, _context, reader = self.render(key="offline", provenance=provenance()) + closing = page_texts(reader)[-1] + + for name in ( + "Exit Readiness Grade", + "Vendor Lock-in Score", + "Scored Alternatives", + "Egress & Data Landscape", + ): + self.assertIn(name, closing) + self.assertNotRegex(closing, r"\d+ / (100|5)") + + def test_the_html_report_has_no_scores_before_the_closing_section(self): + report = build_flow_run( + self.tmp / "html", + resources=[(1, "eu-central-1", 3)], + costs=[("2026-09-01", 10.0, "USD")], + ) + perform_risk_assessment(1, report) + generate_report( + 2, + {"region": "eu-central-1"}, + 1, + 1, + "Flow", + report, + str(Path(report) / "raw_data"), + sync_id=SYNC_ID, + ) + text = html_text((Path(report) / "index.html").read_text(encoding="utf-8")) + before = text[: text.index("What the Full Assessment Adds")] + + for pattern in SCORE_PATTERNS: + self.assertIsNone(re.search(pattern, before, re.IGNORECASE), pattern) + + def test_the_html_follows_the_same_sections(self): + report = build_flow_run( + self.tmp / "html2", + resources=[(1, "eu-central-1", 3)], + costs=[("2026-09-01", 10.0, "USD")], + ) + perform_risk_assessment(1, report) + generate_report( + 2, + {"region": "eu-central-1"}, + 1, + 1, + "Flow", + report, + str(Path(report) / "raw_data"), + sync_id=SYNC_ID, + ) + text = html_text((Path(report) / "index.html").read_text(encoding="utf-8")) + + # The findings table names the sections too, so each heading is searched + # for after the one before it. + position = 0 + for heading in ( + "Brief Summary", + "Provenance", + "Introduction", + "Risk Inventory", + "Costs Overview", + "Resource Inventory", + "Alternative Technologies", + "Risk Severity Levels", + "Glossary", + "What the Full Assessment Adds", + ): + position = text.index(heading, position) + + +class TermGuardTests(BasicReportTestCase): + def test_no_regulatory_word_or_foreign_brand_in_pdf_html_or_metadata(self): + for name, fixture, overrides in self.scenarios(): + with self.subTest(name): + _path, _context, reader = self.render(fixture, key=name, **overrides) + texts = page_texts(reader) + metadata_texts(reader) + for text in texts: + self.assertEqual(find_terms(text), []) + + report = build_flow_run( + self.tmp / "guard", + resources=[(1, "eu-central-1", 3)], + costs=[("2026-09-01", 10.0, "USD")], + ) + perform_risk_assessment(1, report) + generate_report( + 2, + {"region": "eu-central-1"}, + 1, + 1, + "Flow", + report, + str(Path(report) / "raw_data"), + sync_id=SYNC_ID, + ) + html = (Path(report) / "index.html").read_text(encoding="utf-8") + self.assertEqual(find_terms(html_text(html)), []) + self.assertEqual(find_terms(html.split(" Date: Sat, 24 Oct 2026 05:43:24 +0200 Subject: [PATCH 08/61] replace the v1 client with the api v2 client --- README.md | 15 +- core/api_v2.py | 733 ++++++++++++++++++ core/upload.py | 144 ++-- core/utils_sync.py | 129 ---- main.py | 56 +- tests/api_fixtures.py | 217 ++++++ tests/test_api_v2_client.py | 1358 ++++++++++++++++++++++++++++++++++ tests/test_http_fixtures.py | 24 +- tests/test_report_flow.py | 321 +++++--- tests/test_sync_document.py | 1 - tests/test_utils_and_main.py | 62 +- tests/test_utils_sync.py | 92 --- utils/connection.py | 86 --- 13 files changed, 2683 insertions(+), 555 deletions(-) create mode 100644 core/api_v2.py delete mode 100644 core/utils_sync.py create mode 100644 tests/api_fixtures.py create mode 100644 tests/test_api_v2_client.py delete mode 100644 tests/test_utils_sync.py delete mode 100644 utils/connection.py diff --git a/README.md b/README.md index 27f0c48..41eef0c 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,20 @@ Every run writes the same three files: `report.pdf`, `index.html` and `raw_data/ ## Reports and uploads -The local report is always the Basic report, computed on your machine, whether or not a platform is configured. Anything the platform computes stays on the platform: after an upload the local report only records the link (also stored in `raw_data/upload.json`). If an upload fails, the report is still written, with a short "Not uploaded: " banner, and the run exits with an upload code (10 or 12 for now) after everything else has finished. Problems in the collection stages take precedence over upload codes. +The local report is always the Basic report, computed on your machine, whether or not a platform is configured. Anything the platform computes stays on the platform: after an upload the local report only records the link (also stored in `raw_data/upload.json`). If an upload fails, the report is still written, with a short "Not uploaded: " banner, and the run exits with an upload code after everything else has finished. Problems in the collection stages take precedence over upload codes. + +| Exit code | Meaning | +| --- | --- | +| 10 | The platform rejected the data, or it is beyond a limit the platform announces. Retrying the same data will not help. | +| 11 | The API key is missing, mistyped, expired, revoked, not allowed to do this, or belongs to another region. | +| 12 | The platform is unreachable, busy or in maintenance. Running again is safe: the run keeps the same sync ID. | +| 13 | Credits or a quota ran out. | +| 14 | The assessment named by `--assessment-id` is missing, deleted, archived, or does not match the data. | +| 15 | The platform does not accept this cloudexit version or the sync document version. | + +Before anything is sent the key is checked on your machine (prefix, length and checksum), and a regional ExitCloud key is only ever sent to its own region. The key never appears in output or logs. On failure the run prints the platform's error code, its message and a request ID to quote when asking for help. + +Connections always verify the platform's certificate. For a private certificate authority use `--ca-bundle` or `CLOUDEXIT_CA_BUNDLE`; otherwise `REQUESTS_CA_BUNDLE` and `SSL_CERT_FILE` are honoured. There is no option to turn verification off. `HTTPS_PROXY` and `NO_PROXY` are honoured. Requests give up after 10 seconds to connect and 60 seconds to read (`--timeout` sets both). The platform's capabilities are cached for five minutes in `~/.cache/cloudexit/` (`$XDG_CACHE_HOME/cloudexit/`, or `%LOCALAPPDATA%\cloudexit\cache\` on Windows). ## Data Landscape & Egress Estimation (alpha) diff --git a/core/api_v2.py b/core/api_v2.py new file mode 100644 index 0000000..b1c9dd9 --- /dev/null +++ b/core/api_v2.py @@ -0,0 +1,733 @@ +from __future__ import annotations + +import hashlib +import json +import logging +import os +import re +import sys +import time +import uuid +import zlib +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Callable, Mapping +from urllib.parse import quote, urlsplit + +import requests + +from utils import codes +from utils.settings import KEY_PREFIXES, Setting, SettingsError, normalize_host +from utils.version import __version__ + +from .sync_document import ( + MAX_BODY_BYTES, + MAX_CATALOGUE_TYPE_IDS, + MAX_COST_ITEMS, + MAX_COUNT_PER_ITEM, + MAX_EGRESS_ITEMS, + MAX_FLAGS, + MAX_LOCATION_CHARS, + MAX_NAME_CHARS, + MAX_NOTES, + MAX_RESOURCE_ITEMS, + MAX_TIERS, +) + +logger = logging.getLogger("core.engine.api_v2") + +API_PATH = "/api/v2" +CLIENT_NAME = "cloudexit" + +CONNECT_TIMEOUT = 10.0 +READ_TIMEOUT = 60.0 +PREFER_WAIT = 20 +META_TTL_SECONDS = 300 +POLL_DEFAULT_SECONDS = 2.0 +POLL_MAX_SECONDS = 30.0 +PENDING_STATUSES = ("accepted", "processing") + +BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +KEY_BODY = re.compile(r"[0-9A-Za-z]{54}") +CHECKSUM_CHARS = 6 +KEY_HOSTS = {"exc_eu_sk_": "eu.exitcloud.io", "exc_us_sk_": "us.exitcloud.io"} + +EXIT_STRATEGIES = {1: "repatriation", 2: "hybrid_cloud", 3: "alternate_cloud"} + +DEFAULT_LIMITS = { + "max_body_bytes": MAX_BODY_BYTES, + "max_resource_items": MAX_RESOURCE_ITEMS, + "max_count_per_item": MAX_COUNT_PER_ITEM, + "max_location_chars": MAX_LOCATION_CHARS, + "max_cost_items": MAX_COST_ITEMS, + "max_egress_items": MAX_EGRESS_ITEMS, + "max_name_chars": MAX_NAME_CHARS, + "max_egress_tiers_per_item": MAX_TIERS, + "max_egress_flags_per_item": MAX_FLAGS, + "max_egress_notes_per_item": MAX_NOTES, + "max_catalogue_type_ids": MAX_CATALOGUE_TYPE_IDS, + "clock_skew_seconds": 86400, + "max_collection_age_days": 30, +} + +# Written by hand from docs/api-v2/errors.yaml; a test compares the two. +ERROR_EXIT_CODES = { + "unauthenticated": codes.UPLOAD_AUTH, + "invalid_api_key": codes.UPLOAD_AUTH, + "api_key_expired": codes.UPLOAD_AUTH, + "api_key_revoked": codes.UPLOAD_AUTH, + "wrong_region": codes.UPLOAD_AUTH, + "principal_inactive": codes.UPLOAD_AUTH, + "insufficient_scope": codes.UPLOAD_AUTH, + "key_binding_violation": codes.UPLOAD_AUTH, + "assessment_key_v1": codes.UPLOAD_AUTH, + "idempotency_key_missing": codes.UPLOAD_REJECTED, + "idempotency_key_mismatch": codes.UPLOAD_REJECTED, + "idempotency_key_reused": codes.UPLOAD_REJECTED, + "request_in_progress": codes.UPLOAD_UNAVAILABLE, + "invalid_json": codes.UPLOAD_REJECTED, + "unsupported_media_type": codes.UPLOAD_REJECTED, + "payload_too_large": codes.UPLOAD_REJECTED, + "limit_exceeded": codes.UPLOAD_REJECTED, + "validation_failed": codes.UPLOAD_REJECTED, + "catalogue_mismatch": codes.UPLOAD_REJECTED, + "catalogue_sha256_mismatch": codes.UPLOAD_REJECTED, + "unsupported_schema_version": codes.UPLOAD_UNSUPPORTED, + "client_version_unsupported": codes.UPLOAD_UNSUPPORTED, + "assessment_not_found": codes.UPLOAD_TARGET, + "sync_not_found": codes.UPLOAD_TARGET, + "assessment_deleted": codes.UPLOAD_TARGET, + "assessment_archived": codes.UPLOAD_TARGET, + "target_mismatch": codes.UPLOAD_TARGET, + "insufficient_credits": codes.UPLOAD_QUOTA, + "quota_exceeded": codes.UPLOAD_QUOTA, + "rate_limited": codes.UPLOAD_UNAVAILABLE, + "maintenance": codes.UPLOAD_UNAVAILABLE, + "service_unavailable": codes.UPLOAD_UNAVAILABLE, + "internal_error": codes.UPLOAD_UNAVAILABLE, +} + + +class ApiError(Exception): + def __init__( + self, + code: str, + message: str, + exit_code: int, + *, + status: int | None = None, + request_id: str | None = None, + retryable: bool = False, + ): + super().__init__(message) + self.code = code + self.message = message + self.exit_code = exit_code + self.status = status + self.request_id = request_id + self.retryable = retryable + + +@dataclass(frozen=True) +class KeyInfo: + prefix: str + host: str | None + + +@dataclass(frozen=True) +class PushResult: + status: str + sync_id: str | None + assessment_id: str | None + assessment_name: str | None + assessment_created: bool + url: str | None + replayed: bool + pending: bool + warnings: tuple[str, ...] + request_id: str | None + + +def exit_code_for(code: str | None, status: int | None, retryable: bool) -> int: + if code in ERROR_EXIT_CODES: + return ERROR_EXIT_CODES[code] + # An unknown code is judged by the kind of HTTP status and the retryable flag + if retryable or (status is not None and (status >= 500 or status < 400)): + return codes.UPLOAD_UNAVAILABLE + if status in (401, 403): + return codes.UPLOAD_AUTH + if status == 402: + return codes.UPLOAD_QUOTA + if status in (404, 410): + return codes.UPLOAD_TARGET + return codes.UPLOAD_REJECTED + + +def checksum(text: str) -> str: + value = zlib.crc32(text.encode("ascii")) + digits = [] + for _ in range(CHECKSUM_CHARS): + value, digit = divmod(value, 62) + digits.append(BASE62[digit]) + return "".join(reversed(digits)) + + +def _base_url(host: str) -> str: + try: + return normalize_host(host) + except SettingsError as exc: + raise ApiError("invalid_host", str(exc), codes.UPLOAD_UNAVAILABLE) from None + + +def check_key(key: str | None, host: str) -> KeyInfo: + # Local only: a typo is caught before anything is sent, and the key never + # appears in a message. + prefix = next((p for p in KEY_PREFIXES if key and key.startswith(p)), None) + if not prefix: + raise ApiError( + "invalid_api_key", + "the API key does not start with " + ", ".join(KEY_PREFIXES), + codes.UPLOAD_AUTH, + ) + body = key[len(prefix) :] + if ( + not KEY_BODY.fullmatch(body) + or checksum(key[:-CHECKSUM_CHARS]) != key[-CHECKSUM_CHARS:] + ): + raise ApiError( + "invalid_api_key", + "the API key is not valid (check it for typos)", + codes.UPLOAD_AUTH, + ) + expected = KEY_HOSTS.get(prefix) + hostname = urlsplit(_base_url(host)).hostname + if expected and hostname != expected: + raise ApiError( + "wrong_region", + f"this API key belongs to {expected}, not {hostname}", + codes.UPLOAD_AUTH, + ) + return KeyInfo(prefix, expected) + + +def resolve_ca_bundle( + configured: str | None, environ: Mapping[str, str] | None = None +) -> str | bool: + environ = os.environ if environ is None else environ + if configured: + return str(configured) + for name in ("REQUESTS_CA_BUNDLE", "SSL_CERT_FILE"): + value = environ.get(name, "").strip() + if value: + return value + return True + + +def request_timeout(setting: Setting | None) -> tuple[float, float]: + if setting is not None and setting.source != "default" and setting.value: + return float(setting.value), float(setting.value) + return CONNECT_TIMEOUT, READ_TIMEOUT + + +def detect_ci(environ: Mapping[str, str]) -> str: + if environ.get("GITHUB_ACTIONS") == "true": + return "github" + if environ.get("GITLAB_CI"): + return "gitlab" + if environ.get("TF_BUILD"): + return "azure" + return "none" + + +def user_agent(environ: Mapping[str, str] | None = None) -> str: + environ = os.environ if environ is None else environ + system = {"win32": "windows", "darwin": "macos"}.get(sys.platform, sys.platform) + python = f"{sys.version_info.major}.{sys.version_info.minor}" + return f"{CLIENT_NAME}/{__version__} (python/{python}; {system}; ci={detect_ci(environ)})" + + +def cache_dir( + environ: Mapping[str, str] | None = None, *, platform: str = sys.platform +) -> Path: + environ = os.environ if environ is None else environ + if platform == "win32": + base = environ.get("LOCALAPPDATA") or str(Path.home() / "AppData" / "Local") + return Path(base) / "cloudexit" / "cache" + xdg = environ.get("XDG_CACHE_HOME", "") + base = xdg if xdg and os.path.isabs(xdg) else str(Path.home() / ".cache") + return Path(base) / "cloudexit" + + +def _version(text: Any) -> tuple[int, ...] | None: + parts = re.findall(r"\d+", str(text).split("+")[0].split("-")[0])[:3] + return ( + tuple(int(part) for part in parts) + (0,) * (3 - len(parts)) if parts else None + ) + + +def _one_line(text: Any) -> str: + return " ".join(str(text).split()) + + +def _warning_lines(items: Any) -> list[str]: + lines = [] + for item in items if isinstance(items, list) else []: + if isinstance(item, dict) and item.get("message"): + code = item.get("code") + line = _one_line(item["message"]) + lines.append(f"{line} ({code})" if code else line) + return lines + + +def limit_problems( + document: dict[str, Any], limits: Mapping[str, Any], *, size: int, now: float +) -> list[str]: + def limit(name: str) -> int: + value = limits.get(name) + ok = isinstance(value, int) and not isinstance(value, bool) + return value if ok else DEFAULT_LIMITS[name] + + problems: list[str] = [] + + def over(name: str, actual: int, what: str) -> None: + if actual > limit(name): + problems.append(f"{what} is {actual}, above the limit of {limit(name)}") + + def longest(items: list[Any], field: str) -> int: + return max((len(item.get(field) or []) for item in items), default=0) + + inventory = document.get("inventory") or {} + resources = inventory.get("resources") or [] + egress_items = (inventory.get("egress") or {}).get("items") or [] + sync = document.get("sync") or {} + claim = (sync.get("collector") or {}).get("catalogue") or {} + create = (document.get("target") or {}).get("create") or {} + + over("max_body_bytes", size, "the request body (bytes)") + over("max_resource_items", len(resources), "the number of resource items") + over( + "max_count_per_item", + max((item.get("count") or 0 for item in resources), default=0), + "the largest resource count", + ) + over( + "max_location_chars", + max((len(str(item.get("location"))) for item in resources), default=0), + "the longest resource location", + ) + over("max_cost_items", len(inventory.get("costs") or []), "the number of costs") + over("max_egress_items", len(egress_items), "the number of egress items") + over("max_egress_flags_per_item", longest(egress_items, "flags"), "egress flags") + over("max_egress_notes_per_item", longest(egress_items, "notes"), "egress notes") + over("max_egress_tiers_per_item", longest(egress_items, "tiers"), "egress tiers") + over( + "max_catalogue_type_ids", + len(claim.get("resource_type_ids") or []), + "the number of catalogue type ids", + ) + for field in ("name", "group"): + over("max_name_chars", len(create.get(field) or ""), f"the assessment {field}") + + completed_at = sync.get("completed_at") + if isinstance(completed_at, int): + max_age = limit("max_collection_age_days") * 86400 + if now - completed_at > max_age: + problems.append( + f"the collection is older than {limit('max_collection_age_days')} days" + ) + if completed_at - now > limit("clock_skew_seconds"): + problems.append("the collection time is ahead of the clock") + return problems + + +class ApiClient: + def __init__( + self, + host: str, + api_key: str, + *, + ca_bundle: str | None = None, + timeout: tuple[float, float] | None = None, + environ: Mapping[str, str] | None = None, + cache_path: Path | None = None, + sleep: Callable[[float], None] = time.sleep, + clock: Callable[[], float] = time.time, + monotonic: Callable[[], float] = time.monotonic, + ): + environ = os.environ if environ is None else environ + self.base_url = _base_url(host) + check_key(api_key, host) + self.verify = resolve_ca_bundle(ca_bundle, environ) + if isinstance(self.verify, str) and not os.path.exists(self.verify): + raise ApiError( + "tls_error", + f"the CA bundle {self.verify} does not exist", + codes.UPLOAD_UNAVAILABLE, + ) + self.timeout = timeout or (CONNECT_TIMEOUT, READ_TIMEOUT) + self._key = api_key + self._cache_dir = cache_path or cache_dir(environ) + self._sleep = sleep + self._clock = clock + self._monotonic = monotonic + self._meta: dict[str, Any] | None = None + + self.session = requests.Session() + self.session.headers["User-Agent"] = user_agent(environ) + # A truthy auth stops requests from replacing the Bearer header with a + # ~/.netrc login for the host. + self.session.auth = lambda request: request + + @classmethod + def from_settings(cls, settings: Any, **options: Any) -> ApiClient: + host, key = settings.host.value, settings.api_key.value + if not host or not key: + raise ApiError( + "not_configured", + "no platform host or API key is configured", + codes.UPLOAD_AUTH, + ) + return cls( + host, + key, + ca_bundle=settings.ca_bundle.value, + timeout=request_timeout(settings.timeout), + **options, + ) + + def _error(self, response: requests.Response, request_id: str) -> ApiError: + status = response.status_code + request_id = response.headers.get("X-Request-ID") or request_id + try: + body = response.json() + except ValueError: + body = None + error = body.get("error") if isinstance(body, dict) else None + if isinstance(error, dict) and isinstance(error.get("code"), str): + code = error["code"] + message = _one_line(error.get("message") or code) + retryable = bool(error.get("retryable")) + request_id = error.get("request_id") or request_id + for detail in error.get("details") or []: + if isinstance(detail, dict) and detail.get("expected_host"): + message += f" (expected host: {detail['expected_host']})" + else: + code, message, retryable = f"http_{status}", f"HTTP status {status}", False + message = message.replace(self._key, "[api key]") + return ApiError( + code, + message, + exit_code_for(code, status, retryable), + status=status, + request_id=request_id, + retryable=retryable, + ) + + def _call( + self, + method: str, + path: str, + *, + expected: tuple[int, ...], + body: bytes | None = None, + headers: Mapping[str, str] | None = None, + auth: bool = True, + ) -> tuple[dict[str, Any], requests.Response]: + request_id = uuid.uuid4().hex + send = {"Accept": "application/json", "X-Request-ID": request_id} + send.update(headers or {}) + if auth: + send["Authorization"] = f"Bearer {self._key}" + logger.debug( + "%s %s request_id=%s body_bytes=%d", + method, + path, + request_id, + len(body or b""), + ) + host = urlsplit(self.base_url).hostname + try: + response = self.session.request( + method, + f"{self.base_url}{API_PATH}{path}", + data=body, + headers=send, + timeout=self.timeout, + verify=self.verify, + allow_redirects=False, + ) + except requests.exceptions.SSLError as exc: + logger.debug("TLS failure: %s", type(exc).__name__) + raise ApiError( + "tls_error", + f"could not verify the certificate of {host}; for a private CA " + "use --ca-bundle or CLOUDEXIT_CA_BUNDLE", + codes.UPLOAD_UNAVAILABLE, + request_id=request_id, + ) from None + except requests.exceptions.Timeout as exc: + logger.debug("Timeout: %s", type(exc).__name__) + raise ApiError( + "timeout", + f"{host} did not answer in time", + codes.UPLOAD_UNAVAILABLE, + request_id=request_id, + retryable=True, + ) from None + except (requests.exceptions.RequestException, OSError) as exc: + logger.debug("Request failure: %s", type(exc).__name__) + raise ApiError( + "connection_failed", + f"could not reach {host} ({type(exc).__name__})", + codes.UPLOAD_UNAVAILABLE, + request_id=request_id, + retryable=True, + ) from None + + logger.debug( + "%s %s -> %d request_id=%s", + method, + path, + response.status_code, + response.headers.get("X-Request-ID") or request_id, + ) + if response.status_code not in expected: + raise self._error(response, request_id) + try: + data = response.json() + except ValueError: + data = None + if not isinstance(data, dict): + raise ApiError( + "invalid_response", + "the platform answered with something that is not a JSON object", + codes.UPLOAD_UNAVAILABLE, + status=response.status_code, + request_id=response.headers.get("X-Request-ID") or request_id, + ) + return data, response + + def _read_meta_cache(self) -> dict[str, Any] | None: + try: + saved = json.loads(self._meta_file().read_text(encoding="utf-8")) + except (OSError, ValueError): + return None + if not isinstance(saved, dict) or saved.get("base_url") != self.base_url: + return None + fetched_at = saved.get("fetched_at") + meta = saved.get("meta") + if not isinstance(fetched_at, (int, float)) or not isinstance(meta, dict): + return None + if not 0 <= self._clock() - fetched_at < META_TTL_SECONDS: + return None + return meta + + def _write_meta_cache(self, meta: dict[str, Any]) -> None: + path = self._meta_file() + saved = { + "base_url": self.base_url, + "fetched_at": int(self._clock()), + "meta": meta, + } + try: + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_suffix(".tmp") + temporary.write_text(json.dumps(saved), encoding="utf-8") + os.replace(temporary, path) + except OSError: + logger.debug("The meta cache could not be written", exc_info=True) + + def _meta_file(self) -> Path: + digest = hashlib.sha256(self.base_url.encode("utf-8")).hexdigest()[:16] + return self._cache_dir / f"meta-{digest}.json" + + def meta(self) -> dict[str, Any]: + if self._meta is None: + self._meta = self._read_meta_cache() + if self._meta is None: + data, _ = self._call("GET", "/meta", expected=(200,), auth=False) + if data.get("object") != "meta": + raise ApiError( + "invalid_response", + "the platform did not answer with its capabilities", + codes.UPLOAD_UNAVAILABLE, + ) + self._meta = data + self._write_meta_cache(data) + return self._meta + + def whoami(self) -> dict[str, Any]: + data, _ = self._call("GET", "/whoami", expected=(200,)) + return data + + def _check_meta( + self, meta: Mapping[str, Any], document: Mapping[str, Any] + ) -> list[str]: + warnings: list[str] = [] + versions = (meta.get("contract") or {}).get("schema_versions") + schema_version = document.get("schema_version") + if isinstance(versions, list) and schema_version not in versions: + raise ApiError( + "unsupported_schema_version", + f"the platform accepts schema version {', '.join(map(str, versions))}" + f", not {schema_version}", + codes.UPLOAD_UNSUPPORTED, + ) + policy = (meta.get("clients") or {}).get(CLIENT_NAME) + current = _version(__version__) + if isinstance(policy, dict) and current: + minimum = _version(policy.get("min_supported")) + blocked = [_version(v) for v in policy.get("blocked") or []] + if current in blocked or (minimum and current < minimum): + raise ApiError( + "client_version_unsupported", + f"cloudexit {__version__} is not accepted by the platform" + + ( + f"; use {policy['recommended']} or newer" + if policy.get("recommended") + else "" + ), + codes.UPLOAD_UNSUPPORTED, + ) + deprecated = _version(policy.get("deprecated_below")) + if deprecated and current < deprecated: + warnings.append( + f"cloudexit {__version__} is deprecated" + + ( + f"; upgrade to {policy['recommended']}" + if policy.get("recommended") + else "" + ) + ) + return warnings + + def _target( + self, + whoami: Mapping[str, Any], + name: str, + exit_strategy: int, + assessment_id: str | None, + ) -> dict[str, Any] | None: + if assessment_id: + try: + return {"assessment_id": str(uuid.UUID(str(assessment_id).strip()))} + except ValueError: + raise ApiError( + "invalid_assessment_id", + "the assessment id is not a valid id (expected a UUID)", + codes.UPLOAD_TARGET, + ) from None + if (whoami.get("key") or {}).get("kind") != "account": + return None + strategy = EXIT_STRATEGIES.get(exit_strategy) + if not strategy: + raise ApiError( + "invalid_target", + f"exit strategy {exit_strategy} cannot be sent to the platform", + codes.UPLOAD_REJECTED, + ) + return {"create": {"name": name, "exit_strategy": strategy}} + + def _settle( + self, data: dict[str, Any], response: requests.Response + ) -> tuple[dict[str, Any], bool]: + deadline = self._monotonic() + self.timeout[1] + while data.get("status") in PENDING_STATUSES: + platform_id = data.get("id") + remaining = deadline - self._monotonic() + if not isinstance(platform_id, str) or remaining <= 0: + return data, True + delay = _retry_after(response) + self._sleep(min(delay, remaining)) + data, response = self._call( + "GET", f"/syncs/{quote(platform_id, safe='')}", expected=(200,) + ) + if data.get("status") == "failed": + error = data.get("error") if isinstance(data.get("error"), dict) else {} + code = str(error.get("code") or "sync_failed") + retryable = bool(error.get("retryable")) + raise ApiError( + code, + _one_line(error.get("message") or "the platform could not process it"), + exit_code_for(code, None, retryable), + request_id=error.get("request_id") or data.get("request_id"), + retryable=retryable, + ) + return data, False + + def push( + self, + document: Mapping[str, Any], + *, + name: str, + exit_strategy: int, + assessment_id: str | None = None, + ) -> PushResult: + sync_id = (document.get("sync") or {}).get("id") + if not isinstance(sync_id, str) or not sync_id: + raise ApiError( + "invalid_document", + "the sync document has no sync id", + codes.UPLOAD_REJECTED, + ) + meta = self.meta() + warnings = self._check_meta(meta, document) + whoami = self.whoami() + warnings += _warning_lines(whoami.get("warnings")) + + outgoing = {key: value for key, value in document.items() if key != "target"} + target = self._target(whoami, name, exit_strategy, assessment_id) + if target: + outgoing["target"] = target + body = json.dumps(outgoing, separators=(",", ":"), ensure_ascii=False).encode( + "utf-8" + ) + problems = limit_problems( + outgoing, meta.get("limits") or {}, size=len(body), now=self._clock() + ) + if problems: + raise ApiError( + "limit_exceeded", "; ".join(problems[:5]), codes.UPLOAD_REJECTED + ) + + data, response = self._call( + "POST", + "/syncs", + expected=(200, 201, 202), + body=body, + headers={ + "Content-Type": "application/json", + "Idempotency-Key": sync_id, + "Prefer": f"wait={PREFER_WAIT}", + }, + ) + replayed = response.headers.get("Idempotent-Replayed", "").lower() == "true" + data, pending = self._settle(data, response) + + assessment = data.get("assessment") + assessment = assessment if isinstance(assessment, dict) else {} + url = assessment.get("url") + return PushResult( + status=str(data.get("status") or ""), + sync_id=data.get("id") if isinstance(data.get("id"), str) else None, + assessment_id=assessment.get("id"), + assessment_name=assessment.get("name"), + assessment_created=assessment.get("created") is True, + url=( + url + if isinstance(url, str) and url.startswith(("https://", "http://")) + else None + ), + replayed=replayed, + pending=pending, + warnings=tuple(warnings + _warning_lines(data.get("warnings"))), + request_id=response.headers.get("X-Request-ID") or data.get("request_id"), + ) + + +def _retry_after(response: requests.Response) -> float: + try: + seconds = float(response.headers.get("Retry-After", "")) + except ValueError: + return POLL_DEFAULT_SECONDS + return max(0.0, min(seconds, POLL_MAX_SECONDS)) diff --git a/core/upload.py b/core/upload.py index 75e8f13..6352b23 100644 --- a/core/upload.py +++ b/core/upload.py @@ -10,12 +10,20 @@ from utils import codes from utils.settings import Settings, load_settings -from .utils_sync import post_assessment +from .api_v2 import ApiClient, ApiError logger = logging.getLogger("core.engine.upload") REASON_UNAVAILABLE = "upload_unavailable" REASON_REJECTED = "upload_rejected" +REASONS = { + codes.UPLOAD_REJECTED: REASON_REJECTED, + codes.UPLOAD_AUTH: "upload_auth", + codes.UPLOAD_UNAVAILABLE: REASON_UNAVAILABLE, + codes.UPLOAD_QUOTA: "upload_quota", + codes.UPLOAD_TARGET: "upload_target", + codes.UPLOAD_UNSUPPORTED: "upload_unsupported", +} @dataclass(frozen=True) @@ -28,55 +36,83 @@ class UploadResult: sync_id: str | None = None assessment_id: str | None = None url: str | None = None + platform_sync_id: str | None = None + error_code: str | None = None + request_id: str | None = None + warnings: tuple[str, ...] = () + replayed: bool = False + pending: bool = False @property def failed(self) -> bool: return self.attempted and not self.success -def failure(reason: str, message: str, sync_id: str | None = None) -> UploadResult: - exit_code = ( - codes.UPLOAD_REJECTED if reason == REASON_REJECTED else codes.UPLOAD_UNAVAILABLE +def failure( + reason: str, + message: str, + sync_id: str | None = None, + *, + exit_code: int | None = None, + error_code: str | None = None, + request_id: str | None = None, +) -> UploadResult: + if exit_code is None: + exit_code = ( + codes.UPLOAD_REJECTED + if reason == REASON_REJECTED + else codes.UPLOAD_UNAVAILABLE + ) + return UploadResult( + True, + False, + exit_code, + reason, + message, + sync_id, + error_code=error_code, + request_id=request_id, ) - return UploadResult(True, False, exit_code, reason, message, sync_id) - - -def _text(value: Any) -> str | None: - return value.strip() if isinstance(value, str) and value.strip() else None -def _link(value: Any) -> str | None: - url = _text(value) - return url if url and url.startswith(("https://", "http://")) else None +def error_failure(error: ApiError, sync_id: str | None = None) -> UploadResult: + return failure( + REASONS.get(error.exit_code, REASON_UNAVAILABLE), + error.message, + sync_id, + exit_code=error.exit_code, + error_code=error.code, + request_id=error.request_id, + ) def upload( document: dict[str, Any] | None, settings: Settings | None, *, - token: str, name: str, - report_path: str, - started_at: int, meta: dict[str, int], ) -> UploadResult: - # Transitional: the existing upload still builds its own request from the run - # data. The sync document is only used for its id until the v2 client - # replaces the inside of this function. sync_id = ((document or {}).get("sync") or {}).get("id") + if document is None: + return failure( + REASON_REJECTED, + "the sync document could not be built, so there is nothing to upload", + sync_id, + ) settings = settings or load_settings() try: - result = post_assessment( + client = ApiClient.from_settings(settings) + pushed = client.push( + document, name=name, - started_at=started_at, - report_path=report_path, - meta=meta, - token=token, - host=settings.host.value, - timeout=settings.timeout.value, - verify=settings.ca_bundle.value or True, + exit_strategy=meta["exit_strategy"], + assessment_id=settings.assessment_id.value, ) + except ApiError as error: + logger.debug("The upload failed: %s", error.code) + return error_failure(error, sync_id) except Exception as exc: # Whatever goes wrong in the upload must not cost the local report logger.debug("The upload raised %s", type(exc).__name__, exc_info=True) @@ -86,41 +122,59 @@ def upload( sync_id, ) - if result.get("success"): - data = (result.get("payload") or {}).get("data") or {} - return UploadResult( - True, - True, - message="uploaded", - sync_id=sync_id, - assessment_id=_text(data.get("assessment_id")), - url=_link(data.get("url")), - ) - - status = result.get("status") - rejected = isinstance(status, int) and 400 <= status < 500 and status != 429 - return failure( - REASON_REJECTED if rejected else REASON_UNAVAILABLE, - str(result.get("logs") or "the upload failed"), - sync_id, + if pushed.pending: + message = "accepted, still processing" + elif pushed.replayed: + message = "already uploaded" + else: + message = "uploaded" + return UploadResult( + True, + True, + message=message, + sync_id=sync_id, + assessment_id=pushed.assessment_id, + url=pushed.url, + platform_sync_id=pushed.sync_id, + request_id=pushed.request_id, + warnings=pushed.warnings, + replayed=pushed.replayed, + pending=pushed.pending, ) -def report_info(result: UploadResult | None) -> dict[str, str] | None: +REPORT_KEYS = ("status", "reason", "sync_id", "assessment_id", "url") + + +def _record(result: UploadResult | None) -> dict[str, Any] | None: if result is None or not result.attempted: return None info = { "status": "uploaded" if result.success else "failed", "reason": result.reason, + "error_code": result.error_code, "sync_id": result.sync_id, + "platform_sync_id": result.platform_sync_id, "assessment_id": result.assessment_id, "url": result.url, + "request_id": result.request_id, + "pending": result.pending, + "warnings": list(result.warnings), } return {key: value for key, value in info.items() if value} +def report_info(result: UploadResult | None) -> dict[str, str] | None: + # What the local report may show: the status and the link, nothing the + # platform computed. + record = _record(result) + if record is None: + return None + return {key: record[key] for key in REPORT_KEYS if key in record} + + def write_upload_record(raw_data_path: str, result: UploadResult) -> Path | None: - info = report_info(result) + info = _record(result) if info is None: return None info["recorded_at"] = datetime.now(timezone.utc).isoformat(timespec="seconds") diff --git a/core/utils_sync.py b/core/utils_sync.py deleted file mode 100644 index 4e6f4c8..0000000 --- a/core/utils_sync.py +++ /dev/null @@ -1,129 +0,0 @@ -# core/utils_sync.py -from __future__ import annotations - -import logging -import os -import time -import requests -from typing import Any - -from core.utils_db import load_data -from utils.settings import normalize_host -from utils.version import __version__ - -# Configure logger -logger = logging.getLogger("core.engine.sync") - -_ASSESS_PATH = "/api/v1/assessments/" - - -def _assess_url(host: str) -> str: - return f"{normalize_host(host)}{_ASSESS_PATH}" - - -def _build_payload( - *, - report_path: str, - name: str, - started_at: int, - exit_strategy: int, - cloud_service_provider: int, - assessment_type: int, -) -> dict[str, Any]: - db_path = os.path.join(report_path, "data", "assessment.db") - - resource_rows: list[dict[str, Any]] = load_data( - "resource_inventory", db_path=db_path - ) - cost_rows: list[dict[str, Any]] = load_data("cost_inventory", db_path=db_path) - - res_payload = [ - { - "id": int(r["resource_type"]), - "location": r.get("location") or "unknown", - "count": int(r.get("count", 0)), - } - for r in resource_rows - ] - cost_payload = [ - { - "month": c["month"], - "cost": float(c["cost"]), - "currency": c["currency"], - } - for c in cost_rows - ] - - engine_version = f"v{__version__}" - now = int(time.time()) - - payload: dict[str, Any] = { - "id": os.urandom(16).hex(), - "object": "event", - "cli_version": engine_version, - "created": now, - "type": "local.assessment.succeeded", - "data": { - "name": name, - "exit_strategy": exit_strategy, - "cloud_service_provider": cloud_service_provider, - "assessmentType": assessment_type, - "started_at": started_at, - "completed_at": now, - "success": True, - "resource_inventory": res_payload, - "cost_inventory": cost_payload, - }, - } - - logger.debug( - "Outgoing payload: %d resource row(s), %d cost month(s).", - len(res_payload), - len(cost_payload), - ) - return payload - - -def post_assessment( - *, - name: str, - started_at: int, - report_path: str, - meta: dict[str, int], - token: str, - host: str | None, - timeout: float = 10, - verify: str | bool = True, -) -> dict[str, Any]: - if not host: - return { - "success": False, - "payload": None, - "logs": "no upload host configured (see `cloudexit config show`)", - } - - url = _assess_url(host) - headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} - - payload = _build_payload( - report_path=report_path, - started_at=started_at, - name=name, - exit_strategy=meta["exit_strategy"], - cloud_service_provider=meta["cloud_service_provider"], - assessment_type=meta["assessment_type"], - ) - - try: - resp = requests.post( - url, headers=headers, json=payload, timeout=timeout, verify=verify - ) - ok = resp.ok - return { - "success": ok, - "payload": resp.json() if ok else None, - "status": resp.status_code, - "logs": f"server responded {resp.status_code}", - } - except requests.RequestException as exc: - return {"success": False, "payload": None, "logs": f"POST failed: {exc}"} diff --git a/main.py b/main.py index b5cb01e..46a5b1c 100644 --- a/main.py +++ b/main.py @@ -30,9 +30,9 @@ generate_egress_html_report, generate_egress_pdf_report, ) +from core.api_v2 import ApiError, check_key from core.upload import ( - REASON_UNAVAILABLE, - failure, + error_failure, report_info, upload, write_upload_record, @@ -51,7 +51,6 @@ is_azure_cli_token_expired, ) from utils.aws import is_aws_cli_installed, is_aws_profile_valid -from utils.connection import resolve_mode from utils.data import ( CatalogueError, CatalogueUnavailable, @@ -574,26 +573,24 @@ def run_assessment( sys.exit(codes.CONFIG) # Detect ExitCloud Integration - mode, jwt = resolve_mode(settings) - configured = mode == "online" or bool( - settings and settings.host.value and settings.api_key.value - ) + configured = bool(settings and settings.host.value and settings.api_key.value) signin_failure = None if dry_run: print_step("Dry run mode – no remote sync.", status="ok") - elif mode == "online": - print_step("ExitCloud integration configured.", status="ok") elif configured: - # A configured platform that refuses the sign-in is a failed upload, - # not a reason to drop to Basic: the document keeps the requested depth. - signin_failure = failure( - REASON_UNAVAILABLE, "could not sign in to the platform" - ) - print_step( - "ExitCloud integration configured, but sign-in failed.", - status="warning", - logs="The report is still written locally; the upload counts as failed.", - ) + # A key that cannot be right is a failed upload, found before the + # collection starts. It does not drop the run to Basic or discard + # the local report. + try: + check_key(settings.api_key.value, settings.host.value) + print_step("ExitCloud integration configured.", status="ok") + except ApiError as error: + signin_failure = error_failure(error) + print_step( + "ExitCloud integration configured, but the API key is not usable.", + status="warning", + logs=f"{error.message}. The report is still written locally; the upload counts as failed.", + ) else: print_step("ExitCloud integration not configured.", status="warning") # Without a platform only the Basic assessment is possible @@ -892,17 +889,14 @@ def run_assessment( # Stage 7: Upload, only when a platform is configured. A failure never # discards the local report: it is written next, with a banner. upload_result = signin_failure - if signin_failure or (mode == "online" and not dry_run): + if signin_failure or (configured and not dry_run): console.print("Stage #7 – Upload", style="bold") if upload_result is None: with console.status("Uploading to the platform...", spinner="dots"): upload_result = upload( sync_output.document if sync_output else None, settings, - token=jwt, name=name, - report_path=report_path, - started_at=started_at, meta={ "cloud_service_provider": config["cloudServiceProvider"], "exit_strategy": config["exitStrategy"], @@ -911,14 +905,26 @@ def run_assessment( ) if upload_result.sync_id is None: upload_result = replace(upload_result, sync_id=sync_id) - if upload_result.success: + if upload_result.success and not upload_result.pending: print_step("Uploading to the platform...", status="ok") + elif upload_result.success: + print_step( + "Uploading to the platform...", + status="warning", + logs="Accepted by the platform, still processing.", + ) else: print_step( "Uploading to the platform...", status="error", - logs=f"{upload_result.reason}: {upload_result.message}", + logs=f"{upload_result.error_code or upload_result.reason}: {upload_result.message}", ) + if upload_result.request_id: + console.print( + f" ↳ request id: {upload_result.request_id}", style="dim" + ) + for warning in upload_result.warnings: + print_step("Platform warning", status="warning", logs=warning) write_upload_record(raw_data_path, upload_result) console.print("-------------------------------------------") diff --git a/tests/api_fixtures.py b/tests/api_fixtures.py new file mode 100644 index 0000000..b41a719 --- /dev/null +++ b/tests/api_fixtures.py @@ -0,0 +1,217 @@ +import copy +import time +import zlib + +import yaml +from jsonschema import Draft202012Validator + +from tests.flow_fixtures import SERVER_SCORES +from tests.http_fixtures import make_response +from tests.sync_fixtures import DEFINITION + +ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +HOST = "https://platform.test" +ASSESSMENT_ID = "0193a000-0000-7000-8000-000000000001" +PLATFORM_SYNC_ID = "0193a000-0000-7000-8000-0000000000aa" +REQUEST_ID = "req-fake-0001" +URL = "https://platform.test/assessments/a-1" + + +def make_key(prefix="esc_sk_", fill="A"): + head = prefix + (fill * 48)[:48] + value = zlib.crc32(head.encode("ascii")) + digits = [] + for _ in range(6): + value, digit = divmod(value, 62) + digits.append(ALPHABET[digit]) + return head + "".join(reversed(digits)) + + +KEY = make_key() +EU_KEY = make_key("exc_eu_sk_", "B") +US_KEY = make_key("exc_us_sk_", "C") + + +def minimal_document(sync_id, completed_at=None): + return { + "schema_version": "2.0", + "sync": {"id": sync_id, "completed_at": completed_at or int(time.time())}, + "assessment_type": 2, + "inventory": {"resources": []}, + } + + +def corrupt(key): + last = key[-1] + return key[:-1] + ("a" if last != "a" else "b") + + +def component_errors(name, value): + spec = yaml.safe_load((DEFINITION / "openapi.yaml").read_text(encoding="utf-8")) + validator = Draft202012Validator( + {"$ref": f"#/components/schemas/{name}", "components": spec["components"]}, + format_checker=Draft202012Validator.FORMAT_CHECKER, + ) + return sorted( + f"/{'/'.join(str(part) for part in error.absolute_path)}: {error.message}" + for error in validator.iter_errors(value) + ) + + +def merged(base, changes): + result = copy.deepcopy(base) + for key, value in (changes or {}).items(): + if isinstance(value, dict) and isinstance(result.get(key), dict): + result[key] = merged(result[key], value) + else: + result[key] = value + return result + + +META = { + "object": "meta", + "api_version": "2", + "host": { + "product": "escapecloud", + "region": None, + "app_version": "9.9.9", + "base_url": HOST, + }, + "contract": {"schema_versions": ["2.0"], "current": "2.0"}, + "clients": { + "cloudexit": { + "min_supported": "1.0.0", + "deprecated_below": None, + "recommended": "2.0.0", + "blocked": [], + } + }, + "catalogue": { + "version": "2026-11-02", + "sha256": "9" * 64, + "collector_min_version": None, + }, + "limits": { + "max_body_bytes": 1000000, + "max_resource_items": 1000, + "max_count_per_item": 10000, + "max_location_chars": 255, + "max_cost_items": 12, + "max_egress_items": 1000, + "max_name_chars": 255, + "clock_skew_seconds": 86400, + "max_collection_age_days": 30, + "idempotency_replay_hours": 168, + }, + "rate_limits": {"per_key": {"requests_per_minute": 60}}, + "processing": {"modes": ["inline"]}, + "features": {"upsert_by_scope_key": False, "content_dedupe": False}, + "assessment_types": [ + {"id": 1, "code": "basic", "name": "Basic"}, + {"id": 2, "code": "standard", "name": "Standard"}, + {"id": 3, "code": "premium", "name": "Premium"}, + ], +} + + +def meta_body(**changes): + return merged(META, changes) + + +def whoami_body(kind="assessment", scopes=None, assessment_id=ASSESSMENT_ID, **extra): + if scopes is None: + scopes = ["syncs:write"] if kind == "assessment" else ["assessments:create"] + body = { + "object": "whoami", + "api_version": "2", + "request_id": REQUEST_ID, + "tenant": {"type": "organization", "id": "org-1", "name": "Org"}, + "key": { + "id": "0193a000-0000-7000-8000-0000000000bb", + "name": "key", + "display": "esc_sk_…a1b2", + "kind": kind, + "scopes": scopes, + "assessment_id": assessment_id if kind == "assessment" else None, + "expires_at": 4102444800, + }, + "warnings": [], + } + return merged(body, extra) + + +def sync_body(**changes): + body = { + "object": "sync", + "api_version": "2", + "id": PLATFORM_SYNC_ID, + "client_sync_id": None, + "request_id": REQUEST_ID, + "status": "completed", + "assessment": { + "id": ASSESSMENT_ID, + "name": "Flow Test", + "created": False, + "scope_key": "aws:123456789012:eu-central-1", + "url": URL, + }, + "timestamps": {"collected_at": 1, "received_at": 2, "processed_at": 3}, + "ordering": {"latest": True, "late": False}, + "content": {"payload_sha256": "1" * 64, "content_sha256": "2" * 64}, + "assessment_type": {"requested": 2, "granted": 2}, + "catalogue": { + "client": {"version": "2026-11-02", "sequence": 41}, + "host": {"version": "2026-11-02", "sha256": "9" * 64}, + "unknown_resource_types": [], + "inactive_resource_types": [], + }, + "warnings": [], + "results": { + "scoring_version": "1", + "scores": { + "exit": SERVER_SCORES[0], + "human": SERVER_SCORES[1], + "technology": SERVER_SCORES[2], + "operational": SERVER_SCORES[3], + }, + "risks": [ + {"code": "R1", "severity": "high", "impacted_resource_type_ids": [1]} + ], + }, + "links": {"self": "/api/v2/syncs/x", "assessment": "/api/v2/assessments/y"}, + } + return merged(body, changes) + + +def error_body(code, message="The platform said no.", retryable=False, **extra): + error = { + "code": code, + "message": message, + "retryable": retryable, + "request_id": REQUEST_ID, + "details": [], + } + error.update(extra) + return {"object": "error", "api_version": "2", "error": error} + + +def reply(status, body, **headers): + headers.setdefault("X-Request-ID", REQUEST_ID) + return make_response(status, json_body=body, headers=headers) + + +def meta_reply(**changes): + return reply(200, meta_body(**changes)) + + +def whoami_reply(**kwargs): + return reply(200, whoami_body(**kwargs)) + + +def created_reply(**changes): + return reply(201, sync_body(**changes), Location="/api/v2/syncs/x") + + +def success_replies(**changes): + return [meta_reply(), whoami_reply(), created_reply(**changes)] diff --git a/tests/test_api_v2_client.py b/tests/test_api_v2_client.py new file mode 100644 index 0000000..5b20136 --- /dev/null +++ b/tests/test_api_v2_client.py @@ -0,0 +1,1358 @@ +import json +import logging +import os +import re +import tempfile +import unittest +import uuid +from pathlib import Path +from unittest.mock import patch + +import requests +import yaml + +from core import api_v2 +from core.api_v2 import ( + ERROR_EXIT_CODES, + EXIT_STRATEGIES, + ApiClient, + ApiError, + check_key, + exit_code_for, + resolve_ca_bundle, +) +from tests.api_fixtures import ( + ASSESSMENT_ID, + EU_KEY, + HOST, + KEY, + PLATFORM_SYNC_ID, + REQUEST_ID, + US_KEY, + component_errors, + corrupt, + created_reply, + error_body, + make_key, + merged, + meta_body, + meta_reply, + minimal_document, + reply, + success_replies, + sync_body, + whoami_body, + whoami_reply, +) +from tests.http_fixtures import fake_http, make_response +from tests.sync_fixtures import DEFINITION, schema_errors +from utils import codes +from utils.settings import Setting + +SYNC_ID = "01933b9e-7c2a-7d41-9a51-3c8f0a6e0001" +ERRORS = yaml.safe_load((DEFINITION / "errors.yaml").read_text(encoding="utf-8")) + + +class ClientTestCase(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.tmp = Path(tmp.name) + self.now = 1_800_000_000.0 + self.slept = [] + self.clock = [0.0] + self.clients = 0 + self.cache = self.tmp / "cache" + + def client(self, key=KEY, host=HOST, **options): + self.clients += 1 + options.setdefault("cache_path", self.tmp / f"cache-{self.clients}") + options.setdefault("environ", {}) + options.setdefault("clock", lambda: self.now) + options.setdefault("sleep", self.sleep) + options.setdefault("monotonic", lambda: self.clock[0]) + return ApiClient(host, key, **options) + + def sleep(self, seconds): + self.slept.append(seconds) + self.clock[0] += max(seconds, 0.001) + + def document(self, **changes): + document = minimal_document(SYNC_ID, int(self.now) - 60) + return merged(document, changes) + + def push(self, replies, client=None, **kwargs): + kwargs.setdefault("name", "Flow Test") + kwargs.setdefault("exit_strategy", 1) + with fake_http(*replies) as fake: + result = (client or self.client()).push(self.document(), **kwargs) + return result, fake + + def failure(self, replies, client=None, **kwargs): + kwargs.setdefault("name", "Flow Test") + kwargs.setdefault("exit_strategy", 1) + with fake_http(*replies) as fake: + with self.assertRaises(ApiError) as ctx: + (client or self.client()).push(self.document(), **kwargs) + return ctx.exception, fake + + def body_of(self, request): + return json.loads(request.kwargs["data"].decode("utf-8")) + + +class KeyTests(unittest.TestCase): + def test_every_issued_prefix_is_accepted(self): + for key, host in ( + (KEY, "https://platform.test"), + (KEY, "http://localhost:8000"), + (EU_KEY, "https://eu.exitcloud.io"), + (US_KEY, "https://us.exitcloud.io/"), + (EU_KEY, "eu.exitcloud.io"), + ): + with self.subTest(key=key[:10], host=host): + self.assertEqual( + check_key(key, host).prefix, key[: key.index("sk_") + 3] + ) + + def test_a_key_that_cannot_be_right_is_refused_with_exit_11(self): + bad = { + "none": None, + "empty": "", + "not a key": "not-a-key", + "wrong prefix": "abc_sk_" + KEY[7:], + "too short": KEY[:-1], + "too long": KEY + "A", + "bad checksum": corrupt(KEY), + "typo in the body": KEY[:10] + ("B" if KEY[10] != "B" else "C") + KEY[11:], + "symbol": KEY[:20] + "-" + KEY[21:], + "non ascii": KEY[:20] + "é" + KEY[21:], + "trailing newline": KEY + "\n", + "prefix only": "esc_sk_", + } + for label, key in bad.items(): + with self.subTest(label): + with self.assertRaises(ApiError) as ctx: + check_key(key, HOST) + self.assertEqual(ctx.exception.exit_code, codes.UPLOAD_AUTH) + self.assertEqual(ctx.exception.code, "invalid_api_key") + self.assertNotIn(KEY[8:], ctx.exception.message) + + def test_the_checksum_is_the_crc32_of_everything_before_it_in_base62(self): + self.assertEqual(api_v2.checksum("esc_sk_" + "A" * 48), KEY[-6:]) + self.assertEqual(len(api_v2.checksum("x")), 6) + self.assertEqual(api_v2.checksum(""), "000000") + + def test_a_region_key_sent_to_another_host_names_the_right_host(self): + for key, host, right in ( + (EU_KEY, "https://us.exitcloud.io", "eu.exitcloud.io"), + (US_KEY, "https://eu.exitcloud.io", "us.exitcloud.io"), + (EU_KEY, "https://platform.test", "eu.exitcloud.io"), + (US_KEY, "http://localhost:8000", "us.exitcloud.io"), + ): + with self.subTest(right=right, host=host): + with self.assertRaises(ApiError) as ctx: + check_key(key, host) + self.assertEqual(ctx.exception.exit_code, codes.UPLOAD_AUTH) + self.assertEqual(ctx.exception.code, "wrong_region") + self.assertIn(right, ctx.exception.message) + self.assertNotIn(key, ctx.exception.message) + + def test_the_client_refuses_before_it_sends_anything(self): + with fake_http() as fake: + for key, host in ((corrupt(KEY), HOST), (EU_KEY, HOST)): + with self.assertRaises(ApiError): + ApiClient(host, key, environ={}) + self.assertEqual(fake.requests, []) + + def test_a_missing_host_or_key_is_not_configured(self): + class Settings: + host = Setting(None) + api_key = Setting(KEY) + + with self.assertRaises(ApiError) as ctx: + ApiClient.from_settings(Settings()) + self.assertEqual(ctx.exception.exit_code, codes.UPLOAD_AUTH) + + +class RequestShapeTests(ClientTestCase): + def test_every_call_carries_the_agreed_headers(self): + client = self.client() + with fake_http(meta_reply(), whoami_reply(), whoami_reply()) as fake: + client.meta() + client.whoami() + client.whoami() + + meta_call, whoami_call, _ = fake.requests + self.assertNotIn("Authorization", meta_call.kwargs["headers"]) + self.assertEqual( + whoami_call.kwargs["headers"]["Authorization"], f"Bearer {KEY}" + ) + ids = [r.kwargs["headers"]["X-Request-ID"] for r in fake.requests] + self.assertEqual(len(set(ids)), len(ids)) + for request_id in ids: + self.assertRegex(request_id, r"^[A-Za-z0-9._-]{8,64}$") + for request in fake.requests: + self.assertEqual(request.kwargs["headers"]["Accept"], "application/json") + self.assertIs(request.kwargs["allow_redirects"], False) + self.assertEqual(request.kwargs["timeout"], (10.0, 60.0)) + self.assertIs(request.kwargs["verify"], True) + + def test_the_user_agent_names_the_client_python_system_and_ci(self): + pattern = r"^cloudexit/\S+ \(python/3\.\d+; [a-z0-9]+; ci=%s\)$" + for environ, ci in ( + ({}, "none"), + ({"GITHUB_ACTIONS": "true"}, "github"), + ({"GITLAB_CI": "true"}, "gitlab"), + ({"TF_BUILD": "True"}, "azure"), + ({"GITHUB_ACTIONS": "false"}, "none"), + ): + with self.subTest(ci=ci): + agent = self.client(environ=environ).session.headers["User-Agent"] + self.assertRegex(agent, pattern % ci) + + def test_a_push_sends_the_idempotency_key_and_the_document_as_json(self): + _, fake = self.push(success_replies()) + + post = fake.requests[2] + headers = post.kwargs["headers"] + self.assertEqual((post.method, post.url), ("POST", f"{HOST}/api/v2/syncs")) + self.assertEqual(headers["Idempotency-Key"], SYNC_ID) + self.assertEqual(headers["Content-Type"], "application/json") + self.assertEqual(headers["Prefer"], "wait=20") + self.assertEqual(self.body_of(post), self.document()) + self.assertNotIn(b" ", post.kwargs["data"].split(b'"sync"')[0]) + for request in fake.requests[:2]: + self.assertNotIn("Idempotency-Key", request.kwargs["headers"]) + + def test_the_body_is_utf8_without_escaping(self): + document = self.document(inventory={"resources": [], "note": "é"}) + with fake_http(*success_replies()) as fake: + self.client().push(document, name="n", exit_strategy=1) + self.assertIn("é".encode(), fake.requests[2].kwargs["data"]) + + def test_a_redirect_is_never_followed(self): + error, _ = self.failure( + [ + meta_reply(), + make_response(302, headers={"Location": "https://elsewhere.test/"}), + ] + ) + self.assertEqual(error.exit_code, codes.UPLOAD_UNAVAILABLE) + self.assertEqual(error.code, "http_302") + + def test_the_netrc_cannot_replace_the_bearer_header(self): + session = self.client().session + self.assertTrue(session.auth) + prepared = session.prepare_request( + requests.Request("GET", HOST, headers={"Authorization": "Bearer x"}) + ) + self.assertEqual(prepared.headers["Authorization"], "Bearer x") + + +class TransportTests(ClientTestCase): + def test_ca_bundle_precedence(self): + environ = {"REQUESTS_CA_BUNDLE": "/r.pem", "SSL_CERT_FILE": "/s.pem"} + self.assertEqual(resolve_ca_bundle("/flag.pem", environ), "/flag.pem") + self.assertEqual(resolve_ca_bundle(None, environ), "/r.pem") + self.assertEqual(resolve_ca_bundle("", environ), "/r.pem") + self.assertEqual(resolve_ca_bundle(None, {"SSL_CERT_FILE": "/s.pem"}), "/s.pem") + self.assertEqual(resolve_ca_bundle(None, {"REQUESTS_CA_BUNDLE": " "}), True) + self.assertIs(resolve_ca_bundle(None, {}), True) + + def test_the_chosen_bundle_is_what_every_request_verifies_with(self): + flag, requests_bundle, ssl_file = ( + self.tmp / "flag.pem", + self.tmp / "requests.pem", + self.tmp / "ssl.pem", + ) + for path in (flag, requests_bundle, ssl_file): + path.write_text("pem") + environ = { + "REQUESTS_CA_BUNDLE": str(requests_bundle), + "SSL_CERT_FILE": str(ssl_file), + } + cases = ( + (str(flag), environ, str(flag)), + (None, environ, str(requests_bundle)), + (None, {"SSL_CERT_FILE": str(ssl_file)}, str(ssl_file)), + (None, {}, True), + ) + for configured, env, expected in cases: + with self.subTest(expected=str(expected)[-10:]): + client = self.client(ca_bundle=configured, environ=env) + with fake_http(whoami_reply()) as fake: + client.whoami() + self.assertEqual(fake.requests[0].kwargs["verify"], expected) + + def test_verification_cannot_be_switched_off(self): + for configured, env in ( + ("false", {}), + (None, {"REQUESTS_CA_BUNDLE": "0"}), + (None, {"CURL_CA_BUNDLE": ""}), + ): + with self.subTest(configured=configured, env=env): + try: + client = self.client(ca_bundle=configured, environ=env) + except ApiError: + continue + self.assertIsNot(client.verify, False) + self.assertTrue(client.verify is True or isinstance(client.verify, str)) + source = Path(api_v2.__file__).read_text(encoding="utf-8") + self.assertNotRegex(source, r"verify\s*=\s*False") + + def test_a_ca_bundle_that_does_not_exist_is_a_clear_failure(self): + with self.assertRaises(ApiError) as ctx: + self.client(ca_bundle=str(self.tmp / "missing.pem")) + self.assertEqual(ctx.exception.exit_code, codes.UPLOAD_UNAVAILABLE) + self.assertEqual(ctx.exception.code, "tls_error") + + def test_the_standard_proxy_variables_are_honoured(self): + session = self.client().session + self.assertTrue(session.trust_env) + environ = { + "HTTPS_PROXY": "http://proxy.test:3128", + "NO_PROXY": "internal.test", + } + with patch.dict(os.environ, environ): + proxied = session.merge_environment_settings( + f"{HOST}/api/v2/meta", {}, None, True, None + )["proxies"] + direct = session.merge_environment_settings( + "https://internal.test/api/v2/meta", {}, None, True, None + )["proxies"] + self.assertEqual(proxied["https"], "http://proxy.test:3128") + self.assertNotIn("https", direct) + + def test_timeouts_default_to_10_and_60_and_the_setting_overrides_both(self): + self.assertEqual(api_v2.request_timeout(Setting(10.0, "default")), (10.0, 60.0)) + self.assertEqual(api_v2.request_timeout(None), (10.0, 60.0)) + self.assertEqual( + api_v2.request_timeout(Setting(5.0, "flag --timeout")), (5.0, 5.0) + ) + + client = self.client(timeout=(2.0, 3.0)) + with fake_http(whoami_reply()) as fake: + client.whoami() + self.assertEqual(fake.requests[0].kwargs["timeout"], (2.0, 3.0)) + + def test_transport_failures_are_exit_12(self): + cases = ( + (requests.ConnectionError("refused"), "connection_failed"), + (requests.ConnectTimeout("slow"), "timeout"), + (requests.ReadTimeout("slow"), "timeout"), + (requests.exceptions.SSLError("bad cert"), "tls_error"), + (requests.exceptions.ProxyError("proxy"), "connection_failed"), + ( + OSError("Could not find a suitable TLS CA certificate bundle"), + "connection_failed", + ), + ) + for exception, code in cases: + with self.subTest(code=code, exception=type(exception).__name__): + with fake_http(exception): + with self.assertRaises(ApiError) as ctx: + self.client().whoami() + self.assertEqual(ctx.exception.exit_code, codes.UPLOAD_UNAVAILABLE) + self.assertEqual(ctx.exception.code, code) + self.assertIn("platform.test", ctx.exception.message) + self.assertNotIn(KEY, ctx.exception.message) + + def test_a_tls_failure_points_at_the_ca_bundle_options(self): + with fake_http(requests.exceptions.SSLError("x")): + with self.assertRaises(ApiError) as ctx: + self.client().whoami() + self.assertIn("--ca-bundle", ctx.exception.message) + self.assertIn("CLOUDEXIT_CA_BUNDLE", ctx.exception.message) + + +class MetaTests(ClientTestCase): + def test_meta_is_cached_for_five_minutes(self): + with fake_http(meta_reply()) as fake: + first = self.client(cache_path=self.cache).meta() + self.now += 299 + second = self.client(cache_path=self.cache).meta() + self.assertEqual(first, second) + self.assertEqual(len(fake.requests), 1) + + self.now += 2 + with fake_http(meta_reply()) as fake: + self.client(cache_path=self.cache).meta() + self.assertEqual(len(fake.requests), 1) + + def test_one_client_asks_once(self): + client = self.client(cache_path=self.cache) + with fake_http(meta_reply()) as fake: + client.meta() + client.meta() + self.assertEqual(len(fake.requests), 1) + + def test_the_cache_is_per_host_and_holds_no_key(self): + with fake_http(meta_reply(), meta_reply()) as fake: + self.client(cache_path=self.cache).meta() + self.client(host="https://other.test", cache_path=self.cache).meta() + self.assertEqual(len(fake.requests), 2) + + files = list(self.cache.glob("meta-*.json")) + self.assertEqual(len(files), 2) + for path in files: + self.assertNotIn(KEY, path.read_text(encoding="utf-8")) + self.assertNotIn(KEY[8:], path.read_text(encoding="utf-8")) + + def test_a_broken_cache_is_ignored_and_replaced(self): + path = self.client(cache_path=self.cache)._meta_file() + path.parent.mkdir(parents=True) + for content in ("{", "[]", json.dumps({"base_url": HOST}), "null"): + with self.subTest(content=content): + path.write_text(content) + with fake_http(meta_reply()) as fake: + self.client(cache_path=self.cache).meta() + self.assertEqual(len(fake.requests), 1) + self.assertEqual(json.loads(path.read_text())["base_url"], HOST) + + def test_a_cache_from_the_future_is_not_trusted(self): + with fake_http(meta_reply(), meta_reply()) as fake: + self.client(cache_path=self.cache).meta() + self.now -= 1000 + self.client(cache_path=self.cache).meta() + self.assertEqual(len(fake.requests), 2) + + def test_an_unwritable_cache_does_not_stop_the_run(self): + blocker = self.tmp / "blocked" + blocker.write_text("a file, not a directory") + with fake_http(meta_reply()): + meta = self.client(cache_path=blocker / "sub").meta() + self.assertEqual(meta["object"], "meta") + + def test_something_that_is_not_meta_is_refused(self): + with fake_http(reply(200, {"object": "whoami"})): + with self.assertRaises(ApiError) as ctx: + self.client(cache_path=self.cache).meta() + self.assertEqual(ctx.exception.exit_code, codes.UPLOAD_UNAVAILABLE) + + def test_the_cache_directory_follows_the_platform_conventions(self): + self.assertEqual( + api_v2.cache_dir({"XDG_CACHE_HOME": "/xdg"}, platform="linux"), + Path("/xdg/cloudexit"), + ) + self.assertEqual( + api_v2.cache_dir({"XDG_CACHE_HOME": "relative"}, platform="linux"), + Path.home() / ".cache" / "cloudexit", + ) + self.assertEqual( + api_v2.cache_dir({"LOCALAPPDATA": "C:/L"}, platform="win32"), + Path("C:/L") / "cloudexit" / "cache", + ) + + def test_the_default_cache_is_in_the_users_cache_directory(self): + with patch.dict(os.environ, {"XDG_CACHE_HOME": str(self.tmp / "xdg")}): + client = ApiClient(HOST, KEY) + self.assertEqual(client._cache_dir, self.tmp / "xdg" / "cloudexit") + + +class PolicyTests(ClientTestCase): + def test_a_blocked_or_too_old_client_is_refused_before_the_push(self): + for label, clients in ( + ("blocked", {"blocked": ["1.1.7"]}), + ("blocked with a tag", {"blocked": ["v1.1.7"]}), + ("min supported", {"min_supported": "2.0.0"}), + ): + with self.subTest(label): + body = meta_body() + body["clients"]["cloudexit"].update(clients) + error, fake = self.failure([reply(200, body)]) + self.assertEqual(error.exit_code, codes.UPLOAD_UNSUPPORTED) + self.assertEqual(error.code, "client_version_unsupported") + self.assertEqual(len(fake.requests), 1) + self.assertIn("2.0.0", error.message) + + def test_a_deprecated_client_gets_one_warning_and_still_uploads(self): + body = meta_body() + body["clients"]["cloudexit"]["deprecated_below"] = "2.0.0" + result, _ = self.push([reply(200, body), whoami_reply(), created_reply()]) + (warning,) = result.warnings + self.assertIn("deprecated", warning) + self.assertIn("2.0.0", warning) + + def test_a_current_client_and_a_host_without_a_policy_have_no_warning(self): + body = meta_body() + body["clients"] = {} + result, _ = self.push([reply(200, body), whoami_reply(), created_reply()]) + self.assertEqual(result.warnings, ()) + result, _ = self.push( + success_replies(), client=self.client(cache_path=self.tmp / "c2") + ) + self.assertEqual(result.warnings, ()) + + def test_version_parsing(self): + self.assertEqual(api_v2._version("v2.0.0rc1"), (2, 0, 0)) + self.assertEqual(api_v2._version("2.1"), (2, 1, 0)) + self.assertEqual(api_v2._version("1.1.7-dev+abc"), (1, 1, 7)) + self.assertIsNone(api_v2._version(None)) + self.assertIsNone(api_v2._version("latest")) + + def test_a_schema_version_the_host_does_not_list_is_exit_15_without_a_push(self): + with fake_http(meta_reply()) as fake: + with self.assertRaises(ApiError) as ctx: + self.client().push( + self.document(schema_version="2.7"), name="n", exit_strategy=1 + ) + self.assertEqual(ctx.exception.exit_code, codes.UPLOAD_UNSUPPORTED) + self.assertEqual(ctx.exception.code, "unsupported_schema_version") + self.assertEqual(len(fake.requests), 1) + + def test_a_newer_schema_version_the_host_lists_is_sent(self): + body = meta_body(contract={"schema_versions": ["2.0", "2.1"]}) + result, _ = self.push([reply(200, body), whoami_reply(), created_reply()]) + self.assertEqual(result.status, "completed") + + +class LimitTests(ClientTestCase): + def refused(self, document, meta=None, **kwargs): + replies = [reply(200, meta or meta_body()), whoami_reply(**kwargs)] + with fake_http(*replies) as fake: + with self.assertRaises(ApiError) as ctx: + self.client().push(document, name="n", exit_strategy=1) + self.assertEqual([r.method for r in fake.requests], ["GET", "GET"]) + self.assertEqual(ctx.exception.exit_code, codes.UPLOAD_REJECTED) + self.assertEqual(ctx.exception.code, "limit_exceeded") + return ctx.exception.message + + def test_a_document_over_a_limit_never_leaves_the_machine(self): + resource = {"type_id": 1, "type_code": "A", "location": "x", "count": 1} + cases = { + "resource items": self.document(inventory={"resources": [resource] * 1001}), + "count": self.document( + inventory={"resources": [dict(resource, count=10001)]} + ), + "location": self.document( + inventory={"resources": [dict(resource, location="x" * 256)]} + ), + "costs": self.document(inventory={"costs": [{}] * 13}), + "egress items": self.document(inventory={"egress": {"items": [{}] * 1001}}), + "flags": self.document( + inventory={"egress": {"items": [{"flags": ["f"] * 11}]}} + ), + "notes": self.document( + inventory={"egress": {"items": [{"notes": ["n"] * 11}]}} + ), + "tiers": self.document( + inventory={"egress": {"items": [{"tiers": [{}] * 11}]}} + ), + "type ids": self.document( + sync={ + "collector": {"catalogue": {"resource_type_ids": list(range(5001))}} + } + ), + "too old": self.document(sync={"completed_at": int(self.now) - 31 * 86400}), + "ahead of the clock": self.document( + sync={"completed_at": int(self.now) + 86401} + ), + "body": self.document(inventory={"note": "x" * 1_000_001}), + } + for label, document in cases.items(): + with self.subTest(label): + self.assertTrue(self.refused(document)) + + def test_the_announced_limits_win_over_the_defaults(self): + document = self.document( + inventory={"resources": [{"type_id": 1, "location": "x", "count": 1}] * 3} + ) + meta = meta_body(limits={"max_resource_items": 2}) + self.assertIn("limit of 2", self.refused(document, meta)) + + generous = meta_body(limits={"max_resource_items": 5000}) + document = self.document( + inventory={ + "resources": [{"type_id": 1, "location": "x", "count": 1}] * 1001 + } + ) + with fake_http(reply(200, generous), whoami_reply(), created_reply()): + self.client().push(document, name="n", exit_strategy=1) + + def test_a_limit_the_host_leaves_out_falls_back_to_the_default(self): + meta = meta_body() + del meta["limits"]["max_cost_items"] + document = self.document(inventory={"costs": [{}] * 13}) + self.assertIn("limit of 12", self.refused(document, meta)) + + def test_an_assessment_name_over_the_limit_is_refused_for_an_account_key(self): + replies = [meta_reply(), whoami_reply(kind="account")] + with fake_http(*replies): + with self.assertRaises(ApiError) as ctx: + self.client().push(self.document(), name="n" * 256, exit_strategy=1) + self.assertEqual(ctx.exception.code, "limit_exceeded") + + def test_the_messages_never_contain_the_document(self): + document = self.document( + inventory={ + "resources": [{"type_id": 1, "location": "secret-loc", "count": 10001}] + } + ) + message = self.refused(document) + self.assertNotIn("secret-loc", message) + + +class TargetTests(ClientTestCase): + def target_of(self, replies, **kwargs): + _, fake = self.push(replies, **kwargs) + return self.body_of(fake.requests[2]).get("target") + + def test_an_assessment_key_sends_no_target(self): + self.assertIsNone(self.target_of(success_replies())) + + def test_an_account_key_creates_an_assessment_with_the_strategy_name(self): + for number, name in EXIT_STRATEGIES.items(): + with self.subTest(number=number): + replies = [ + meta_reply(), + whoami_reply(kind="account"), + created_reply(), + ] + target = self.target_of( + replies, + exit_strategy=number, + client=self.client(cache_path=self.tmp / f"c{number}"), + ) + self.assertEqual( + target, {"create": {"name": "Flow Test", "exit_strategy": name}} + ) + + def test_an_assessment_id_names_the_target_for_either_key(self): + for kind in ("account", "assessment"): + with self.subTest(kind=kind): + replies = [meta_reply(), whoami_reply(kind=kind), created_reply()] + target = self.target_of( + replies, + assessment_id=ASSESSMENT_ID.upper(), + client=self.client(cache_path=self.tmp / kind), + ) + self.assertEqual(target, {"assessment_id": ASSESSMENT_ID}) + + def test_an_assessment_id_that_is_not_an_id_is_exit_14_without_a_push(self): + error, fake = self.failure( + [meta_reply(), whoami_reply(kind="account")], assessment_id="not-an-id" + ) + self.assertEqual((error.exit_code, error.code), (14, "invalid_assessment_id")) + self.assertEqual(len(fake.requests), 2) + + def test_an_unknown_exit_strategy_is_not_sent(self): + error, fake = self.failure( + [meta_reply(), whoami_reply(kind="account")], exit_strategy=9 + ) + self.assertEqual(error.exit_code, codes.UPLOAD_REJECTED) + self.assertEqual(len(fake.requests), 2) + + def test_the_document_is_not_changed_and_an_old_target_is_replaced(self): + document = self.document(target={"assessment_id": "old"}) + before = json.dumps(document, sort_keys=True) + with fake_http(*success_replies()) as fake: + self.client().push(document, name="n", exit_strategy=1) + self.assertEqual(json.dumps(document, sort_keys=True), before) + self.assertNotIn("target", self.body_of(fake.requests[2])) + + +class ResponseTests(ClientTestCase): + def test_201_carries_the_ids_the_link_and_the_status(self): + result, _ = self.push(success_replies()) + self.assertEqual( + ( + result.status, + result.sync_id, + result.assessment_id, + result.assessment_name, + result.assessment_created, + result.url, + result.replayed, + result.pending, + result.request_id, + ), + ( + "completed", + PLATFORM_SYNC_ID, + ASSESSMENT_ID, + "Flow Test", + False, + "https://platform.test/assessments/a-1", + False, + False, + REQUEST_ID, + ), + ) + + def test_200_with_idempotent_replayed_is_a_replay(self): + replayed = reply(200, sync_body(), **{"Idempotent-Replayed": "true"}) + result, _ = self.push([meta_reply(), whoami_reply(), replayed]) + self.assertTrue(result.replayed) + self.assertFalse(result.pending) + + def test_completed_with_warnings_is_a_success_and_the_warnings_come_back(self): + warnings = [ + { + "code": "unknown_resource_types", + "message": "Unknown types.", + "details": {}, + }, + {"code": "tier_downgraded", "message": "Granted\n Basic."}, + ] + body = sync_body(status="completed_with_warnings", warnings=warnings) + whoami = whoami_body(warnings=[{"code": "key_expiring", "message": "Soon."}]) + result, _ = self.push([meta_reply(), reply(200, whoami), reply(201, body)]) + self.assertEqual(result.status, "completed_with_warnings") + self.assertEqual( + result.warnings, + ( + "Soon. (key_expiring)", + "Unknown types. (unknown_resource_types)", + "Granted Basic. (tier_downgraded)", + ), + ) + + def test_202_is_polled_until_it_completes(self): + accepted = reply( + 202, + sync_body(status="accepted", results=None), + **{"Retry-After": "3", "Location": "/api/v2/syncs/x"}, + ) + processing = reply(200, sync_body(status="processing"), **{"Retry-After": "7"}) + done = reply(200, sync_body()) + result, fake = self.push( + [meta_reply(), whoami_reply(), accepted, processing, done] + ) + + self.assertEqual(result.status, "completed") + self.assertFalse(result.pending) + self.assertEqual(self.slept, [3.0, 7.0]) + polls = fake.requests[3:] + self.assertEqual( + [(r.method, r.url) for r in polls], + [("GET", f"{HOST}/api/v2/syncs/{PLATFORM_SYNC_ID}")] * 2, + ) + for poll in polls: + self.assertNotIn("Idempotency-Key", poll.kwargs["headers"]) + self.assertEqual(poll.kwargs["headers"]["Authorization"], f"Bearer {KEY}") + + def test_polling_without_a_retry_after_waits_a_default_and_caps_a_long_one(self): + accepted = reply(202, sync_body(status="accepted")) + slow = reply(200, sync_body(status="processing"), **{"Retry-After": "900"}) + result, _ = self.push( + [meta_reply(), whoami_reply(), accepted, slow, reply(200, sync_body())] + ) + self.assertEqual(self.slept, [2.0, 30.0]) + self.assertEqual(result.status, "completed") + + def test_a_sync_still_processing_when_the_budget_runs_out_is_pending(self): + def still(): + return reply(200, sync_body(status="processing"), **{"Retry-After": "2"}) + + accepted = reply(202, sync_body(status="accepted"), **{"Retry-After": "2"}) + client = self.client(timeout=(10.0, 5.0)) + with fake_http( + meta_reply(), whoami_reply(), accepted, still(), still(), still() + ) as fake: + result = client.push(self.document(), name="n", exit_strategy=1) + + self.assertTrue(result.pending) + self.assertEqual(result.status, "processing") + self.assertEqual(result.url, "https://platform.test/assessments/a-1") + self.assertEqual(self.slept, [2.0, 2.0, 1.0]) + self.assertEqual(len(fake.requests), 6) + + def test_a_pending_sync_without_an_id_cannot_be_polled(self): + body = sync_body(status="accepted") + del body["id"] + result, fake = self.push([meta_reply(), whoami_reply(), reply(202, body)]) + self.assertTrue(result.pending) + self.assertEqual(len(fake.requests), 3) + + def test_a_poll_that_fails_is_mapped_like_any_error(self): + accepted = reply(202, sync_body(status="accepted")) + gone = reply(404, error_body("sync_not_found")) + error, _ = self.failure([meta_reply(), whoami_reply(), accepted, gone]) + self.assertEqual((error.exit_code, error.code), (14, "sync_not_found")) + + def test_a_sync_that_failed_is_mapped_from_its_error(self): + failed = sync_body( + status="failed", + error=error_body("internal_error", "Scoring failed.", True)["error"], + ) + error, _ = self.failure([meta_reply(), whoami_reply(), reply(201, failed)]) + self.assertEqual((error.exit_code, error.code), (12, "internal_error")) + self.assertEqual(error.message, "Scoring failed.") + + no_detail = sync_body(status="failed") + error, _ = self.failure( + [meta_reply(), whoami_reply(), reply(201, no_detail)], + client=self.client(cache_path=self.tmp / "other"), + ) + self.assertEqual(error.code, "sync_failed") + + def test_a_2xx_without_a_json_object_is_exit_12(self): + for answer in ( + make_response(201, text="proxy"), + reply(201, ["not", "an", "object"]), + ): + with self.subTest(answer=answer.text[:12]): + error, _ = self.failure( + [meta_reply(), whoami_reply(), answer], + client=self.client(cache_path=self.tmp / uuid.uuid4().hex), + ) + self.assertEqual( + (error.exit_code, error.code), (12, "invalid_response") + ) + + def test_an_unsafe_link_is_dropped_and_a_missing_one_is_none(self): + for url in ("javascript:alert(1)", "//evil.test/x", None, 5): + with self.subTest(url=url): + result, _ = self.push( + success_replies(assessment={"url": url}), + client=self.client(cache_path=self.tmp / uuid.uuid4().hex), + ) + self.assertIsNone(result.url) + + +class ErrorMappingTests(ClientTestCase): + def test_every_error_in_the_definition_maps_to_its_exit_code(self): + self.assertEqual(set(ERROR_EXIT_CODES), set(ERRORS["errors"])) + for code, entry in ERRORS["errors"].items(): + with self.subTest(code): + self.assertEqual(ERROR_EXIT_CODES[code], entry["cli_exit"]) + self.assertEqual( + exit_code_for(code, entry["http"], entry["retryable"]), + entry["cli_exit"], + ) + + def test_the_exit_codes_in_the_definition_are_the_ones_in_codes_py(self): + known = { + 10: codes.UPLOAD_REJECTED, + 11: codes.UPLOAD_AUTH, + 12: codes.UPLOAD_UNAVAILABLE, + 13: codes.UPLOAD_QUOTA, + 14: codes.UPLOAD_TARGET, + 15: codes.UPLOAD_UNSUPPORTED, + } + self.assertEqual(sorted(ERRORS["exit_codes"]), sorted(known)) + self.assertEqual(sorted(set(ERROR_EXIT_CODES.values())), sorted(known)) + + def test_a_server_error_answer_reaches_the_caller_with_code_message_and_id(self): + for code, entry in ERRORS["errors"].items(): + with self.subTest(code): + body = error_body(code, f"message for {code}", entry["retryable"]) + error, _ = self.failure( + [meta_reply(), whoami_reply(), reply(entry["http"], body)], + client=self.client(cache_path=self.tmp / code), + ) + self.assertEqual(error.code, code) + self.assertEqual(error.message, f"message for {code}") + self.assertEqual(error.request_id, REQUEST_ID) + self.assertEqual(error.exit_code, entry["cli_exit"]) + self.assertEqual(error.status, entry["http"]) + self.assertEqual(error.retryable, entry["retryable"]) + + def test_unknown_codes_fall_back_to_the_status_class_and_retryable(self): + cases = ( + (400, False, 10), + (413, False, 10), + (422, False, 10), + (409, False, 10), + (401, False, 11), + (403, False, 11), + (402, False, 13), + (404, False, 14), + (410, False, 14), + (429, True, 12), + (409, True, 12), + (500, False, 12), + (503, False, 12), + (502, True, 12), + ) + for status, retryable, exit_code in cases: + with self.subTest(status=status, retryable=retryable): + self.assertEqual( + exit_code_for("brand_new_code", status, retryable), exit_code + ) + body = error_body("brand_new_code", "A new thing.", retryable) + error, _ = self.failure( + [meta_reply(), whoami_reply(), reply(status, body)], + client=self.client(cache_path=self.tmp / f"{status}{retryable}"), + ) + self.assertEqual( + (error.code, error.exit_code), ("brand_new_code", exit_code) + ) + self.assertEqual(exit_code_for(None, None, False), codes.UPLOAD_REJECTED) + self.assertEqual(exit_code_for(None, None, True), codes.UPLOAD_UNAVAILABLE) + + def test_an_answer_that_is_not_an_error_object_uses_the_status_class(self): + for status, text, exit_code in ( + (502, "Bad gateway", 12), + (503, "", 12), + (401, "Unauthorized", 11), + (400, "{not json", 10), + (404, '{"error": "plain string"}', 14), + ): + with self.subTest(status=status): + error, _ = self.failure( + [meta_reply(), whoami_reply(), make_response(status, text=text)], + client=self.client(cache_path=self.tmp / str(status) / text[:3]), + ) + self.assertEqual(error.exit_code, exit_code) + self.assertEqual(error.code, f"http_{status}") + self.assertEqual(error.message, f"HTTP status {status}") + self.assertNotIn("Bad gateway", error.message) + + def test_the_request_id_falls_back_to_the_one_the_client_sent(self): + answer = make_response(502, text="bad") + error, fake = self.failure([meta_reply(), whoami_reply(), answer]) + self.assertEqual( + error.request_id, fake.requests[2].kwargs["headers"]["X-Request-ID"] + ) + + def test_a_wrong_region_answer_names_the_expected_host(self): + body = error_body( + "wrong_region", + "This key belongs to the other region.", + details=[ + { + "code": "wrong_region", + "message": "m", + "expected_host": "https://eu.exitcloud.io", + } + ], + ) + error, _ = self.failure([meta_reply(), whoami_reply(), reply(403, body)]) + self.assertEqual(error.exit_code, codes.UPLOAD_AUTH) + self.assertIn("https://eu.exitcloud.io", error.message) + + def test_messages_are_one_line_and_never_echo_the_key(self): + body = error_body("validation_failed", f"Bad\nvalue {KEY} here") + error, _ = self.failure([meta_reply(), whoami_reply(), reply(422, body)]) + self.assertNotIn("\n", error.message) + self.assertNotIn(KEY, error.message) + self.assertIn("[api key]", error.message) + + +class LoggingTests(ClientTestCase): + def test_nothing_secret_reaches_the_logs(self): + secret_location = "secret-location-marker" + document = self.document( + inventory={ + "resources": [ + { + "type_id": 1, + "type_code": "T", + "location": secret_location, + "count": 1, + } + ] + } + ) + runs = ( + success_replies(), + [ + meta_reply(), + whoami_reply(), + reply(422, error_body("validation_failed", "no")), + ], + [meta_reply(), requests.ConnectionError(f"failed with {KEY}")], + ) + with self.assertLogs("core.engine.api_v2", level=logging.DEBUG) as logs: + for index, replies in enumerate(runs): + try: + with fake_http(*replies): + self.client(cache_path=self.tmp / str(index)).push( + document, name="n", exit_strategy=1 + ) + except ApiError: + pass + + text = "\n".join(logs.output) + self.assertTrue(logs.records) + for secret in (KEY, KEY[8:], "Bearer", "Authorization", secret_location): + self.assertNotIn(secret, text) + self.assertIn("request_id=", text) + self.assertRegex(text, r"body_bytes=\d+") + self.assertIn(REQUEST_ID, text) + + def test_the_exception_text_does_not_carry_the_key_or_the_body(self): + with fake_http(requests.ConnectionError(f"boom {KEY}")): + with self.assertRaises(ApiError) as ctx: + self.client().whoami() + self.assertNotIn(KEY, str(ctx.exception)) + self.assertNotIn(KEY, repr(ctx.exception)) + self.assertIsNone(ctx.exception.__cause__) + + +EXAMPLES = DEFINITION / "examples" +TOKEN = re.compile(r"\{\{([^}]*)\}\}") + +# Requests the CLI never sends: the platform's refusal of them is not the +# client's to handle. +NEVER_SENT = { + "09": "no Idempotency-Key", + "10": "an Idempotency-Key that differs from sync.id", + "27": "an account key without a target", + "37": "a document that breaks the schema", + "39": "invalid JSON", + "40": "another media type", +} +# Requests the CLI refuses before sending, from what /meta announces. +REFUSED_LOCALLY = { + "38": (codes.UPLOAD_REJECTED, 2), + "41": (codes.UPLOAD_UNSUPPORTED, 1), +} +# Key checks that happen without a request. +KEY_CHECKED_LOCALLY = {"33", "34"} +# Reads the CLI makes: capabilities, identity, and the poll of a sync. +READS = {"01", "02", "03", "04", "31", "32", "42", "43"} +# Not part of what a push needs. +NOT_USED = {"35", "44", "45", "46", "47", "48", "49", "50"} + + +class Replay: + def __init__(self, now): + self.now = now + self.ids = { + "main": ASSESSMENT_ID, + "first_sync": PLATFORM_SYNC_ID, + } + self.keys = {} + + def identifier(self, name): + if name not in self.ids: + self.ids[name] = str(uuid.uuid5(uuid.NAMESPACE_URL, f"example/{name}")) + return self.ids[name] + + def fill(self, value, keys=None): + if isinstance(value, dict): + return {k: self.fill(v, keys) for k, v in value.items()} + if isinstance(value, list): + return [self.fill(v, keys) for v in value] + if not isinstance(value, str) or "{{" not in value: + return value + whole = TOKEN.fullmatch(value) + if whole: + return self.token(whole.group(1), keys) + return TOKEN.sub(lambda m: str(self.token(m.group(1), keys)), value) + + def token(self, name, keys): + if name.endswith("_id"): + return self.identifier(name[: -len("_id")]) + offset = re.fullmatch(r"now([+-]\d+)?", name) + if offset: + return int(self.now) + int(offset.group(1) or 0) + key_name = name.split("|")[0] + if key_name in (keys or {}): + key = keys[key_name] + return corrupt(key) if name.endswith("|corrupt") else key + raise AssertionError(f"unknown placeholder {{{{{name}}}}}") + + +def load_examples(): + return { + path.name[:2]: json.loads(path.read_text(encoding="utf-8")) + for path in sorted(EXAMPLES.glob("*.json")) + } + + +class ExampleReplayTests(ClientTestCase): + maxDiff = None + + @classmethod + def setUpClass(cls): + cls.examples = load_examples() + + def setUp(self): + super().setUp() + self.now = 1_800_000_000.0 + self.replay = Replay(self.now) + + def keys_for(self, example): + keys = {} + for index, spec in enumerate(example["fixtures"].get("keys", [])): + prefix = "esc_sk_" + keys[spec["name"]] = make_key(prefix, "ABCDEFGH"[index]) + return keys + + def whoami_for(self, example, key_name): + spec = next(k for k in example["fixtures"]["keys"] if k["name"] == key_name) + scopes = spec.get("scopes") or ["syncs:write"] + return reply( + 200, + whoami_body( + kind=spec["kind"], + scopes=scopes, + assessment_id=self.replay.identifier(spec.get("assessment", "main")), + ), + ) + + def response_for(self, example, keys): + expect = self.replay.fill(example["expect"], keys) + headers = dict(expect.get("headers") or {}) + headers.setdefault("X-Request-ID", REQUEST_ID) + for name in expect.get("headers_present", []): + headers.setdefault(name, "present") + subset = expect.get("body_subset") or {} + if subset.get("object") == "error": + body = merged(error_body(subset["error"]["code"]), subset) + body["error"].setdefault("request_id", REQUEST_ID) + else: + body = merged(sync_body(), subset) + return make_response(expect["status"], json_body=body, headers=headers) + + def run_example(self, number): + example = self.examples[number] + keys = self.keys_for(example) + request = self.replay.fill(example["request"], keys) + key_name = TOKEN.search(example["request"]["headers"]["Authorization"]).group(1) + key = keys[key_name] + body = request["body"] + target = body.get("target") or {} + create = target.get("create") or {} + strategy = {name: n for n, name in EXIT_STRATEGIES.items()}[ + create.get("exit_strategy", "repatriation") + ] + document = {k: v for k, v in body.items() if k != "target"} + client = self.client(key=key, cache_path=self.tmp / number) + replies = [ + meta_reply(), + self.whoami_for(example, key_name), + self.response_for(example, keys), + ] + with fake_http(*replies) as fake: + try: + result = client.push( + document, + name=create.get("name", "Example"), + exit_strategy=strategy, + assessment_id=target.get("assessment_id"), + ) + error = None + except ApiError as exc: + result, error = None, exc + return example, request, fake, result, error + + def test_every_example_is_accounted_for(self): + sent = { + number + for number, example in self.examples.items() + if example["request"]["method"] == "POST" + } + accounted = ( + set(NEVER_SENT) + | set(REFUSED_LOCALLY) + | KEY_CHECKED_LOCALLY + | READS + | NOT_USED + | {n for n in sent if n not in NEVER_SENT and n not in REFUSED_LOCALLY} + ) + self.assertEqual(set(self.examples), accounted) + + def test_what_the_cli_never_sends_really_differs_from_what_it_sends(self): + for number in NEVER_SENT: + with self.subTest(number): + example = self.examples[number] + replay = Replay(self.now) + request = replay.fill( + example["request"], {"assessment_key": KEY, "account_key": KEY} + ) + headers, body = request["headers"], request.get("body") + differs = ( + "Idempotency-Key" not in headers + or not isinstance(body, dict) + or headers["Idempotency-Key"] != (body.get("sync") or {}).get("id") + or headers.get("Content-Type") != "application/json" + or bool(schema_errors(body)) + or ( + request["headers"]["Authorization"] == f"Bearer {KEY}" + and "target" not in body + and number == "27" + ) + ) + self.assertTrue(differs, NEVER_SENT[number]) + + def test_the_request_the_cli_sends_is_the_one_in_the_example(self): + for number, example in self.examples.items(): + if ( + example["request"]["method"] != "POST" + or number in NEVER_SENT + or number in REFUSED_LOCALLY + ): + continue + with self.subTest(number=number, name=example["name"]): + _, request, fake, _, _ = self.run_example(number) + post = fake.requests[2] + self.assertEqual( + (post.method, post.url), ("POST", HOST + request["path"]) + ) + self.assertEqual(self.body_of(post), request["body"]) + self.assertEqual(schema_errors(self.body_of(post)), []) + headers = post.kwargs["headers"] + for name in ("Idempotency-Key", "Content-Type"): + self.assertEqual(headers[name], request["headers"][name]) + self.assertEqual( + headers["Idempotency-Key"], request["body"]["sync"]["id"] + ) + self.assertTrue(headers["Authorization"].startswith("Bearer esc_sk_")) + self.assertEqual(headers["Prefer"], "wait=20") + + def test_success_examples_end_in_a_result(self): + for number, example in self.examples.items(): + if example["request"]["method"] != "POST" or example["expect"][ + "status" + ] not in (200, 201, 202): + continue + with self.subTest(number=number, name=example["name"]): + example, _, _, result, error = self.run_example(number) + self.assertIsNone(error) + subset = example["expect"].get("body_subset") or {} + self.assertEqual(result.status, subset.get("status", "completed")) + headers = example["expect"].get("headers") or {} + self.assertEqual( + result.replayed, + example["expect"]["status"] == 200 + and headers.get("Idempotent-Replayed") == "true", + ) + created = (subset.get("assessment") or {}).get("created") + if created is not None: + self.assertEqual(result.assessment_created, created) + self.assertEqual(result.assessment_id, ASSESSMENT_ID) + + def test_error_examples_end_in_the_exit_code_of_the_registry(self): + seen = set() + for number, example in self.examples.items(): + subset = example["expect"].get("body_subset") or {} + if ( + example["request"]["method"] != "POST" + or subset.get("object") != "error" + or number in NEVER_SENT + or number in REFUSED_LOCALLY + ): + continue + with self.subTest(number=number, name=example["name"]): + _, _, _, result, error = self.run_example(number) + code = subset["error"]["code"] + seen.add(code) + self.assertIsNone(result) + self.assertEqual(error.code, code) + self.assertEqual(error.exit_code, ERRORS["errors"][code]["cli_exit"]) + self.assertEqual(error.status, example["expect"]["status"]) + self.assertEqual(error.request_id, REQUEST_ID) + self.assertTrue(error.message) + self.assertLessEqual( + { + "idempotency_key_reused", + "target_mismatch", + "catalogue_mismatch", + "catalogue_sha256_mismatch", + "insufficient_scope", + "assessment_not_found", + "key_binding_violation", + "payload_too_large", + "insufficient_credits", + }, + seen, + ) + + def test_examples_the_cli_refuses_locally_send_no_push(self): + for number, (exit_code, reads) in REFUSED_LOCALLY.items(): + with self.subTest(number=number): + _, _, fake, result, error = self.run_example_refused(number, reads) + self.assertIsNone(result) + self.assertEqual(error.exit_code, exit_code) + self.assertEqual([r.method for r in fake.requests], ["GET"] * reads) + + def run_example_refused(self, number, reads): + example = self.examples[number] + keys = self.keys_for(example) + request = self.replay.fill(example["request"], keys) + key_name = TOKEN.search(example["request"]["headers"]["Authorization"]).group(1) + client = self.client(key=keys[key_name], cache_path=self.tmp / number) + replies = [meta_reply(), self.whoami_for(example, key_name)][:reads] + with fake_http(*replies) as fake: + try: + result = client.push(request["body"], name="n", exit_strategy=1) + error = None + except ApiError as exc: + result, error = None, exc + return example, request, fake, result, error + + def test_key_examples_are_refused_locally_without_a_request(self): + for number in KEY_CHECKED_LOCALLY: + with self.subTest(number=number): + example = self.examples[number] + keys = self.keys_for(example) + request = self.replay.fill(example["request"], keys) + key = request["headers"]["Authorization"].removeprefix("Bearer ") + with fake_http() as fake: + with self.assertRaises(ApiError) as ctx: + check_key(key, HOST) + self.assertEqual(fake.requests, []) + self.assertEqual(ctx.exception.exit_code, codes.UPLOAD_AUTH) + self.assertEqual( + ctx.exception.code, + example["expect"]["body_subset"]["error"]["code"], + ) + + def test_the_read_examples_match_what_the_client_does_with_them(self): + for number in ("31", "32"): + with self.subTest(number=number): + example = self.examples[number] + keys = self.keys_for(example) + client = self.client(key=next(iter(keys.values()))) + answer = self.response_for(example, keys) + with fake_http(answer): + with self.assertRaises(ApiError) as ctx: + client.whoami() + self.assertEqual(ctx.exception.exit_code, codes.UPLOAD_AUTH) + self.assertEqual( + ctx.exception.code, + example["expect"]["body_subset"]["error"]["code"], + ) + + example = self.examples["02"] + keys = self.keys_for(example) + with fake_http(self.whoami_for(example, "assessment_key")) as fake: + who = self.client(key=keys["assessment_key"]).whoami() + self.assertEqual(who["key"]["kind"], "assessment") + self.assertEqual(fake.requests[0].url, HOST + example["request"]["path"]) + + with fake_http(reply(200, meta_body())) as fake: + self.client().meta() + self.assertEqual( + fake.requests[0].url, HOST + self.examples["01"]["request"]["path"] + ) + self.assertEqual(component_errors("Meta", meta_body()), []) + + def test_the_poll_examples_decide_the_end_of_a_202(self): + accepted = reply(202, sync_body(status="accepted"), **{"Retry-After": "1"}) + for number, outcome in (("42", "ok"), ("43", "error")): + with self.subTest(number=number): + example = self.examples[number] + answer = self.response_for(example, self.keys_for(example)) + with fake_http(meta_reply(), whoami_reply(), accepted, answer) as fake: + client = self.client(cache_path=self.tmp / number) + if outcome == "ok": + result = client.push(self.document(), name="n", exit_strategy=1) + self.assertEqual(result.status, "completed") + else: + with self.assertRaises(ApiError) as ctx: + client.push(self.document(), name="n", exit_strategy=1) + self.assertEqual(ctx.exception.exit_code, codes.UPLOAD_TARGET) + self.assertEqual(fake.requests[3].method, "GET") + + +class FixtureTests(unittest.TestCase): + def test_the_fake_answers_are_valid_against_the_definition(self): + self.assertEqual(component_errors("Meta", meta_body()), []) + self.assertEqual(component_errors("Whoami", whoami_body()), []) + self.assertEqual(component_errors("Whoami", whoami_body(kind="account")), []) + self.assertEqual(component_errors("Sync", sync_body()), []) + self.assertEqual( + component_errors("ErrorResponse", error_body("validation_failed")), [] + ) + + def test_a_minimal_document_is_not_the_real_thing(self): + self.assertNotEqual(schema_errors(minimal_document(SYNC_ID)), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_http_fixtures.py b/tests/test_http_fixtures.py index 8dd775d..be1200e 100644 --- a/tests/test_http_fixtures.py +++ b/tests/test_http_fixtures.py @@ -3,7 +3,6 @@ import requests from tests.http_fixtures import fake_http, make_response -from utils.connection import get_jwt_token class FakeHTTPTests(unittest.TestCase): @@ -46,20 +45,23 @@ def test_raise_for_status_follows_the_status_code(self): class FakeHTTPExampleTests(unittest.TestCase): - def test_get_jwt_token_reads_the_token_from_the_response(self): - with fake_http(make_response(json_body={"access_token": "jwt-1"})) as fake: - token = get_jwt_token(host="api.test", key="test-key") + def test_a_request_through_a_session_is_recorded_with_its_headers(self): + with fake_http(make_response(json_body={"object": "meta"})) as fake: + with requests.Session() as session: + response = session.get( + "https://api.test/api/v2/meta", headers={"Accept": "text/plain"} + ) - self.assertEqual(token, "jwt-1") + self.assertEqual(response.json(), {"object": "meta"}) (request,) = fake.requests - self.assertEqual(request.url, "https://api.test/api/v1/auth/token/") - self.assertEqual( - request.kwargs["headers"], {"Authorization": "Bearer test-key"} - ) + self.assertEqual(request.url, "https://api.test/api/v2/meta") + self.assertEqual(request.kwargs["headers"], {"Accept": "text/plain"}) - def test_get_jwt_token_returns_none_when_the_host_is_unreachable(self): + def test_a_connection_error_reply_is_raised_from_a_session(self): with fake_http(requests.ConnectionError("refused")): - self.assertIsNone(get_jwt_token(host="api.test", key="test-key")) + with requests.Session() as session: + with self.assertRaises(requests.ConnectionError): + session.get("https://api.test/") if __name__ == "__main__": diff --git a/tests/test_report_flow.py b/tests/test_report_flow.py index 0bb64bb..f6a3d71 100644 --- a/tests/test_report_flow.py +++ b/tests/test_report_flow.py @@ -2,12 +2,16 @@ import inspect import json import os +import shutil import sqlite3 import tempfile import unittest +from contextlib import nullcontext from pathlib import Path from unittest.mock import patch +import requests + import main from core.engine import generate_report, perform_risk_assessment from core.sync_document import SyncDocumentError @@ -20,14 +24,27 @@ upload, write_upload_record, ) +from tests.api_fixtures import ( + ASSESSMENT_ID, + KEY, + URL, + corrupt, + error_body, + meta_reply, + minimal_document, + reply, + success_replies, + sync_body, + whoami_reply, +) from tests.flow_fixtures import LOCAL_RISKS, SERVER_SCORES, build_flow_run +from tests.http_fixtures import fake_http, make_response from tests.sync_fixtures import SYNC_ID, egress_row from utils import codes from utils.settings import Setting, Settings AWS_RESOURCES = [(1, "eu-central-1", 8), (4, "eu-central-1", 2)] COSTS = [("2026-09-01", 12.5, "USD")] -URL = "https://platform.test/assessments/a-1" CONFIG = { "name": "Flow Test", "cloudServiceProvider": 2, @@ -42,29 +59,15 @@ } CONFIGURED = Settings( host=Setting("https://platform.test", "env CLOUDEXIT_HOST"), - api_key=Setting("k" * 24, "env CLOUDEXIT_API_KEY"), + api_key=Setting(KEY, "env CLOUDEXIT_API_KEY"), ) -SERVER_PAYLOAD = { - "data": { - "assessment_id": "a-1", - "url": URL, - "risk_inventory": [{"id": 8, "impacted_resources": [1]}], - "scoring_data": { - "exit_score": SERVER_SCORES[0], - "human_score": SERVER_SCORES[1], - "technology_score": SERVER_SCORES[2], - "operational_score": SERVER_SCORES[3], - }, - } -} - - -def post_ok(payload=None): - return {"success": True, "payload": payload or SERVER_PAYLOAD, "status": 201} -def post_failed(status=None, logs="failed"): - return {"success": False, "payload": None, "status": status, "logs": logs} +def failed_replies(status, code="service_unavailable", retryable=True): + if status is None: + return [meta_reply(), whoami_reply(), requests.ConnectionError("refused")] + body = error_body(code, "server said no", retryable) + return [meta_reply(), whoami_reply(), reply(status, body)] class FlowTestCase(unittest.TestCase): @@ -72,11 +75,20 @@ def setUp(self): tmp = tempfile.TemporaryDirectory() self.addCleanup(tmp.cleanup) self.tmp = Path(tmp.name) - patcher = patch.dict(os.environ, {"XDG_DATA_HOME": str(self.tmp / "data")}) + patcher = patch.dict( + os.environ, + { + "XDG_DATA_HOME": str(self.tmp / "data"), + "XDG_CACHE_HOME": str(self.tmp / "cache"), + }, + ) patcher.start() self.addCleanup(patcher.stop) self.counter = 0 + def forget_meta(self): + shutil.rmtree(self.tmp / "cache", ignore_errors=True) + def report(self, **kwargs): self.counter += 1 kwargs.setdefault("resources", AWS_RESOURCES) @@ -206,7 +218,7 @@ def test_a_successful_upload_records_only_the_link(self): info = { "status": "uploaded", "sync_id": SYNC_ID, - "assessment_id": "a-1", + "assessment_id": ASSESSMENT_ID, "url": URL, } @@ -229,74 +241,91 @@ def test_banner_text_is_escaped(self): class UploadInterfaceTests(FlowTestCase): - def call(self, post, document=None, settings=CONFIGURED): - with patch("core.upload.post_assessment", return_value=post) as mock_post: + def call(self, replies, document=None, settings=CONFIGURED): + self.forget_meta() + with fake_http(*replies) as fake: result = upload( - document if document is not None else {"sync": {"id": SYNC_ID}}, + document if document is not None else minimal_document(SYNC_ID), settings, - token="jwt", name="Flow", - report_path="/tmp/report", - started_at=1, - meta={"assessment_type": 1}, + meta={"assessment_type": 1, "exit_strategy": 1}, ) - return result, mock_post + return result, fake def test_success_carries_the_link_and_nothing_else(self): - result, mock_post = self.call(post_ok()) + result, fake = self.call(success_replies()) self.assertTrue(result.success) self.assertFalse(result.failed) self.assertEqual( - (result.sync_id, result.assessment_id, result.url), (SYNC_ID, "a-1", URL) + (result.sync_id, result.assessment_id, result.url), + (SYNC_ID, ASSESSMENT_ID, URL), + ) + self.assertEqual( + [(r.method, r.url) for r in fake.requests], + [ + ("GET", "https://platform.test/api/v2/meta"), + ("GET", "https://platform.test/api/v2/whoami"), + ("POST", "https://platform.test/api/v2/syncs"), + ], ) - self.assertEqual(mock_post.call_args.kwargs["host"], "https://platform.test") - self.assertEqual(mock_post.call_args.kwargs["timeout"], 10.0) self.assertFalse(hasattr(result, "risks")) self.assertFalse(hasattr(result, "scores")) + def test_the_result_carries_the_platform_ids_and_warnings(self): + warning = {"code": "tier_downgraded", "message": "Granted\nBasic."} + result, _ = self.call(success_replies(warnings=[warning])) + + self.assertEqual(result.warnings, ("Granted Basic. (tier_downgraded)",)) + self.assertEqual(result.platform_sync_id, sync_body()["id"]) + self.assertEqual(result.request_id, "req-fake-0001") + self.assertEqual(result.message, "uploaded") + def test_an_unsafe_link_is_dropped(self): - payload = {"data": {"assessment_id": "a-1", "url": "javascript:alert(1)"}} - result, _ = self.call(post_ok(payload)) + result, _ = self.call( + success_replies(assessment={"url": "javascript:alert(1)"}) + ) self.assertIsNone(result.url) def test_failures_map_to_the_upload_codes(self): - cases = { - 503: (REASON_UNAVAILABLE, codes.UPLOAD_UNAVAILABLE), - 500: (REASON_UNAVAILABLE, codes.UPLOAD_UNAVAILABLE), - 429: (REASON_UNAVAILABLE, codes.UPLOAD_UNAVAILABLE), - None: (REASON_UNAVAILABLE, codes.UPLOAD_UNAVAILABLE), - 400: (REASON_REJECTED, codes.UPLOAD_REJECTED), - 401: (REASON_REJECTED, codes.UPLOAD_REJECTED), - 422: (REASON_REJECTED, codes.UPLOAD_REJECTED), - } - for status, (reason, exit_code) in cases.items(): - with self.subTest(status=status): - result, _ = self.call(post_failed(status, "server said no")) + cases = ( + (503, "service_unavailable", True, REASON_UNAVAILABLE, 12), + (500, "internal_error", True, REASON_UNAVAILABLE, 12), + (429, "rate_limited", True, REASON_UNAVAILABLE, 12), + (None, None, True, REASON_UNAVAILABLE, 12), + (400, "invalid_json", False, REASON_REJECTED, 10), + (422, "validation_failed", False, REASON_REJECTED, 10), + (401, "invalid_api_key", False, "upload_auth", 11), + (402, "insufficient_credits", False, "upload_quota", 13), + (404, "assessment_not_found", False, "upload_target", 14), + (400, "unsupported_schema_version", False, "upload_unsupported", 15), + ) + for status, code, retryable, reason, exit_code in cases: + with self.subTest(status=status, code=code): + result, _ = self.call(failed_replies(status, code, retryable)) self.assertTrue(result.failed) self.assertEqual((result.reason, result.exit_code), (reason, exit_code)) - self.assertEqual(result.message, "server said no") self.assertEqual(result.sync_id, SYNC_ID) + if code: + self.assertEqual(result.message, "server said no") + self.assertEqual(result.error_code, code) + self.assertEqual(result.request_id, "req-fake-0001") + + def test_no_document_is_a_rejected_upload_without_a_request(self): + with fake_http(): + result = upload(None, CONFIGURED, name="Flow", meta={}) + self.assertEqual((result.reason, result.exit_code), (REASON_REJECTED, 10)) def test_an_unexpected_error_is_a_failed_upload_not_a_crash(self): - with patch("core.upload.post_assessment", side_effect=ValueError("bad json")): - result = upload( - None, - CONFIGURED, - token="jwt", - name="Flow", - report_path="/tmp/report", - started_at=1, - meta={}, - ) + with patch("core.upload.ApiClient.from_settings", side_effect=ValueError("x")): + result = upload(minimal_document(SYNC_ID), CONFIGURED, name="Flow", meta={}) self.assertTrue(result.failed) self.assertEqual(result.reason, REASON_UNAVAILABLE) self.assertIn("ValueError", result.message) - self.assertNotIn("bad json", result.message) def test_report_info_and_the_record_file(self): success = UploadResult( - True, True, sync_id=SYNC_ID, assessment_id="a-1", url=URL + True, True, sync_id=SYNC_ID, assessment_id=ASSESSMENT_ID, url=URL ) failed = failure(REASON_REJECTED, "nope", SYNC_ID) @@ -305,7 +334,7 @@ def test_report_info_and_the_record_file(self): { "status": "uploaded", "sync_id": SYNC_ID, - "assessment_id": "a-1", + "assessment_id": ASSESSMENT_ID, "url": URL, }, ) @@ -316,6 +345,27 @@ def test_report_info_and_the_record_file(self): self.assertIsNone(report_info(None)) self.assertIsNone(report_info(UploadResult(False, False))) + detailed = UploadResult( + True, + True, + sync_id=SYNC_ID, + assessment_id=ASSESSMENT_ID, + url=URL, + platform_sync_id="p-1", + request_id="req-1", + warnings=("Granted Basic.",), + pending=True, + ) + self.assertEqual(report_info(detailed), report_info(success)) + record = json.loads( + write_upload_record(str(self.tmp), detailed).read_text(encoding="utf-8") + ) + self.assertEqual(record["warnings"], ["Granted Basic."]) + self.assertEqual( + (record["platform_sync_id"], record["request_id"], record["pending"]), + ("p-1", "req-1", True), + ) + path = write_upload_record(str(self.tmp), success) record = json.loads(path.read_text()) self.assertEqual(record["url"], URL) @@ -331,18 +381,17 @@ def run_assessment( self, report, *, - mode=("offline", None), settings=None, - post=None, + replies=None, egress_ok=None, assessment_type=2, **kwargs, ): self.steps = [] raw_data = os.path.join(report, "raw_data") + console_patch = patch("main.console.print") patches = [ patch("main.validate_config"), - patch("main.resolve_mode", return_value=mode), patch("main.create_directory", return_value=(report, raw_data)), patch("main.verify_credentials", return_value=(True, "ok")), patch("main.test_permissions", return_value=(True, True, True, "ok")), @@ -358,10 +407,8 @@ def run_assessment( "main.print_step", side_effect=lambda *a, **k: self.steps.append((a, k)), ), - patch("main.console.print"), + console_patch, ] - if post is not None: - patches.append(patch("core.upload.post_assessment", return_value=post)) if egress_ok is not None: patches += [ patch( @@ -378,12 +425,16 @@ def run_assessment( ), ] for item in patches: - item.start() + started = item.start() self.addCleanup(item.stop) + if item is console_patch: + self.printed = started config = copy.deepcopy(CONFIG) config["assessmentType"] = assessment_type - return main.run_assessment(config, "aws", settings=settings, **kwargs) + self.forget_meta() + with fake_http(*replies) if replies is not None else nullcontext(): + return main.run_assessment(config, "aws", settings=settings, **kwargs) def sync(self, report): return json.loads(self.raw(report, "sync.json").read_text()) @@ -420,9 +471,7 @@ def test_an_offline_run_forces_basic_and_never_uploads(self): def test_the_local_results_are_computed_when_connected_too(self): report = self.report() - self.run_assessment( - report, mode=("online", "jwt"), settings=CONFIGURED, post=post_ok() - ) + self.run_assessment(report, settings=CONFIGURED, replies=success_replies()) self.assertEqual( [(row[1], row[2]) for row in self.rows(report, "risk_inventory")], LOCAL_RISKS, @@ -431,9 +480,7 @@ def test_the_local_results_are_computed_when_connected_too(self): def test_a_successful_upload_stores_the_link_and_no_platform_results(self): report = self.report() - self.run_assessment( - report, mode=("online", "jwt"), settings=CONFIGURED, post=post_ok() - ) + self.run_assessment(report, settings=CONFIGURED, replies=success_replies()) self.assert_local_report(report) record = json.loads(self.raw(report, "upload.json").read_text()) @@ -442,7 +489,7 @@ def test_a_successful_upload_stores_the_link_and_no_platform_results(self): { "status": "uploaded", "sync_id": self.sync(report)["sync"]["id"], - "assessment_id": "a-1", + "assessment_id": ASSESSMENT_ID, "url": URL, }, ) @@ -461,9 +508,7 @@ def test_a_successful_upload_stores_the_link_and_no_platform_results(self): def test_connected_runs_keep_the_requested_depth_in_the_document(self): report = self.report() - self.run_assessment( - report, mode=("online", "jwt"), settings=CONFIGURED, post=post_ok() - ) + self.run_assessment(report, settings=CONFIGURED, replies=success_replies()) self.assertEqual(self.sync(report)["assessment_type"], 2) # The local report is always the Basic report @@ -474,19 +519,19 @@ def test_connected_runs_keep_the_requested_depth_in_the_document(self): def test_a_failed_upload_still_writes_the_report_and_exits_with_an_upload_code( self, ): - for status, exit_code, reason in ( - (503, codes.UPLOAD_UNAVAILABLE, REASON_UNAVAILABLE), - (None, codes.UPLOAD_UNAVAILABLE, REASON_UNAVAILABLE), - (422, codes.UPLOAD_REJECTED, REASON_REJECTED), + for status, code, exit_code, reason in ( + (503, "service_unavailable", codes.UPLOAD_UNAVAILABLE, REASON_UNAVAILABLE), + (None, None, codes.UPLOAD_UNAVAILABLE, REASON_UNAVAILABLE), + (422, "validation_failed", codes.UPLOAD_REJECTED, REASON_REJECTED), + (401, "api_key_revoked", codes.UPLOAD_AUTH, "upload_auth"), ): with self.subTest(status=status): report = self.report() with self.assertRaises(SystemExit) as ctx: self.run_assessment( report, - mode=("online", "jwt"), settings=CONFIGURED, - post=post_failed(status, "server responded"), + replies=failed_replies(status, code, status in (503, None)), ) self.assertEqual(ctx.exception.code, exit_code) @@ -501,29 +546,100 @@ def test_a_failed_upload_still_writes_the_report_and_exits_with_an_upload_code( ) self.assertEqual(self.rows(report, "scoring_data"), []) + def test_the_failure_is_printed_with_its_code_and_request_id(self): + report = self.report() + with self.assertRaises(SystemExit): + self.run_assessment( + report, + settings=CONFIGURED, + replies=failed_replies(422, "validation_failed", False), + ) + + (logs,) = [ + kwargs["logs"] + for args, kwargs in self.steps + if args[0] == "Uploading to the platform..." and kwargs.get("logs") + ] + self.assertEqual(logs, "validation_failed: server said no") + self.assertIn("req-fake-0001", str(self.printed.call_args_list)) + + def test_warnings_from_the_platform_are_printed_one_per_line(self): + report = self.report() + warnings = [ + {"code": "unknown_resource_types", "message": "Two types are unknown."}, + {"code": "tier_downgraded", "message": "Granted Basic."}, + ] + self.run_assessment( + report, + settings=CONFIGURED, + replies=success_replies(warnings=warnings), + ) + + printed = [ + kwargs["logs"] + for args, kwargs in self.steps + if args[0] == "Platform warning" + ] + self.assertEqual( + printed, + [ + "Two types are unknown. (unknown_resource_types)", + "Granted Basic. (tier_downgraded)", + ], + ) + + def test_a_run_still_processing_is_recorded_and_exits_zero(self): + report = self.report() + pending = UploadResult( + True, + True, + message="accepted, still processing", + sync_id=SYNC_ID, + assessment_id=ASSESSMENT_ID, + url=URL, + pending=True, + ) + with patch("main.upload", return_value=pending): + self.run_assessment(report, settings=CONFIGURED) + + self.assert_local_report(report) + record = json.loads(self.raw(report, "upload.json").read_text()) + self.assertEqual((record["status"], record["pending"]), ("uploaded", True)) + self.assertIn(URL, self.html(report)) + logs = [kwargs.get("logs") for _, kwargs in self.steps] + self.assertIn("Accepted by the platform, still processing.", logs) + def test_an_unreadable_platform_answer_does_not_cost_the_report(self): report = self.report() - with ( - patch("core.upload.post_assessment", side_effect=ValueError("not json")), - self.assertRaises(SystemExit) as ctx, - ): - self.run_assessment(report, mode=("online", "jwt"), settings=CONFIGURED) + with self.assertRaises(SystemExit) as ctx: + self.run_assessment( + report, + settings=CONFIGURED, + replies=[ + meta_reply(), + whoami_reply(), + make_response(201, text="not json"), + ], + ) self.assertEqual(ctx.exception.code, codes.UPLOAD_UNAVAILABLE) self.assert_local_report(report) - def test_a_failed_sign_in_is_a_failed_upload_not_an_offline_run(self): + def test_a_key_that_cannot_be_right_is_a_failed_upload_not_an_offline_run(self): report = self.report() + settings = Settings( + host=CONFIGURED.host, api_key=Setting(corrupt(KEY), "env CLOUDEXIT_API_KEY") + ) with ( patch("main.upload") as mock_upload, self.assertRaises(SystemExit) as ctx, ): - self.run_assessment(report, mode=("offline", None), settings=CONFIGURED) + self.run_assessment(report, settings=settings, replies=[]) mock_upload.assert_not_called() - self.assertEqual(ctx.exception.code, codes.UPLOAD_UNAVAILABLE) + self.assertEqual(ctx.exception.code, codes.UPLOAD_AUTH) self.assert_local_report(report) - self.assertIn("Not uploaded: upload_unavailable", self.html(report)) + self.assertIn("Not uploaded: upload_auth", self.html(report)) # A configured platform keeps the requested depth: nothing forces Basic self.assertEqual(self.sync(report)["assessment_type"], 2) messages = [args[0] for args, _ in self.steps] @@ -534,7 +650,7 @@ def test_basic_is_forced_only_without_platform_configuration(self): for settings in (None, Settings(), half): with self.subTest(settings=settings): report = self.report() - self.run_assessment(report, mode=("offline", None), settings=settings) + self.run_assessment(report, settings=settings) self.assertEqual(self.sync(report)["assessment_type"], 1) def test_a_dry_run_never_uploads_and_never_forces_basic(self): @@ -542,8 +658,8 @@ def test_a_dry_run_never_uploads_and_never_forces_basic(self): with patch("main.upload") as mock_upload: self.run_assessment( report, - mode=("online", "jwt"), settings=CONFIGURED, + replies=[], dry_run=True, ) @@ -557,9 +673,8 @@ def test_collection_problems_win_over_upload_codes(self): with self.assertRaises(SystemExit) as ctx: self.run_assessment( report, - mode=("online", "jwt"), settings=CONFIGURED, - post=post_failed(503), + replies=failed_replies(503, "service_unavailable"), egress=True, egress_ok=False, ) @@ -576,9 +691,8 @@ def test_a_sync_document_problem_wins_over_an_upload_code(self): ): self.run_assessment( report, - mode=("online", "jwt"), settings=CONFIGURED, - post=post_failed(503), + replies=[], emit_payload=str(self.tmp / "out"), ) @@ -616,9 +730,8 @@ def traced_risk(*args, **kwargs): ): self.run_assessment( report, - mode=("online", "jwt"), settings=CONFIGURED, - post=post_ok(), + replies=success_replies(), egress=True, ) diff --git a/tests/test_sync_document.py b/tests/test_sync_document.py index 7bce32a..94ec57d 100644 --- a/tests/test_sync_document.py +++ b/tests/test_sync_document.py @@ -827,7 +827,6 @@ def run_assessment(self, report, *, egress_ok=None, **kwargs): raw_data = os.path.join(report, "raw_data") patches = [ patch("main.validate_config"), - patch("main.resolve_mode", return_value=("offline", None)), patch("main.create_directory", return_value=(report, raw_data)), patch("main.verify_credentials", return_value=(True, "ok")), patch("main.test_permissions", return_value=(True, True, True, "ok")), diff --git a/tests/test_utils_and_main.py b/tests/test_utils_and_main.py index b89ce14..7e98d84 100644 --- a/tests/test_utils_and_main.py +++ b/tests/test_utils_and_main.py @@ -9,8 +9,7 @@ import main from core.sync_document import SyncOutput from utils import codes -from utils.connection import resolve_mode -from utils.settings import Setting, Settings +from utils.settings import Settings from utils.utils import load_config VALID_CONFIG = { @@ -62,7 +61,6 @@ def _base_patches(): return [ patch("main.console.print"), patch("main.print_step"), - patch("main.resolve_mode", return_value=("offline", None)), patch("main.create_directory", return_value=("/tmp/report", "/tmp/report/raw")), patch("main.verify_credentials", return_value=(True, "ok")), patch( @@ -107,7 +105,6 @@ def test_config_validation_failure_exits_2(self): with self.assertRaises(SystemExit) as ctx: with ( patch("main.validate_config", side_effect=ValueError("bad config")), - patch("main.resolve_mode"), patch("main.create_directory"), patch("main.verify_credentials"), patch("main.print_step"), @@ -120,7 +117,6 @@ def test_directory_creation_failure_exits_2(self): with self.assertRaises(SystemExit) as ctx: with ( patch("main.validate_config"), - patch("main.resolve_mode", return_value=("offline", None)), patch("main.create_directory", side_effect=RuntimeError("disk full")), patch("main.verify_credentials"), patch("main.print_step"), @@ -133,7 +129,6 @@ def test_credential_failure_exits_3(self): with self.assertRaises(SystemExit) as ctx: with ( patch("main.validate_config"), - patch("main.resolve_mode", return_value=("offline", None)), patch("main.create_directory", return_value=("/tmp/r", "/tmp/r/raw")), patch("main.verify_credentials", return_value=(False, "bad creds")), patch("main.print_step"), @@ -146,7 +141,6 @@ def test_permission_failure_exits_4(self): with self.assertRaises(SystemExit) as ctx: with ( patch("main.validate_config"), - patch("main.resolve_mode", return_value=("offline", None)), patch("main.create_directory", return_value=("/tmp/r", "/tmp/r/raw")), patch("main.verify_credentials", return_value=(True, "ok")), patch( @@ -163,7 +157,6 @@ def test_resource_inventory_failure_exits_5(self): with self.assertRaises(SystemExit) as ctx: with ( patch("main.validate_config"), - patch("main.resolve_mode", return_value=("offline", None)), patch("main.create_directory", return_value=("/tmp/r", "/tmp/r/raw")), patch("main.verify_credentials", return_value=(True, "ok")), patch("main.test_permissions", return_value=(True, True, True, "ok")), @@ -181,7 +174,6 @@ def test_cost_inventory_failure_exits_6(self): with self.assertRaises(SystemExit) as ctx: with ( patch("main.validate_config"), - patch("main.resolve_mode", return_value=("offline", None)), patch("main.create_directory", return_value=("/tmp/r", "/tmp/r/raw")), patch("main.verify_credentials", return_value=(True, "ok")), patch("main.test_permissions", return_value=(True, True, True, "ok")), @@ -203,7 +195,6 @@ def test_risk_assessment_failure_exits_7(self): with self.assertRaises(SystemExit) as ctx: with ( patch("main.validate_config"), - patch("main.resolve_mode", return_value=("offline", None)), patch("main.create_directory", return_value=("/tmp/r", "/tmp/r/raw")), patch("main.verify_credentials", return_value=(True, "ok")), patch("main.test_permissions", return_value=(True, True, True, "ok")), @@ -229,7 +220,6 @@ def test_report_generation_failure_exits_8(self): with self.assertRaises(SystemExit) as ctx: with ( patch("main.validate_config"), - patch("main.resolve_mode", return_value=("offline", None)), patch("main.create_directory", return_value=("/tmp/r", "/tmp/r/raw")), patch("main.verify_credentials", return_value=(True, "ok")), patch("main.test_permissions", return_value=(True, True, True, "ok")), @@ -272,7 +262,6 @@ def test_unexpected_error_writes_traceback_file(self): with self.assertRaises(SystemExit) as ctx: with ( patch("main.validate_config"), - patch("main.resolve_mode", return_value=("offline", None)), patch( "main.create_directory", return_value=(tmp_dir, raw_data_path), @@ -296,7 +285,6 @@ def test_unexpected_error_writes_traceback_file(self): def test_full_success_exits_0(self): with ( patch("main.validate_config"), - patch("main.resolve_mode", return_value=("offline", None)), patch("main.create_directory", return_value=("/tmp/r", "/tmp/r/raw")), patch("main.verify_credentials", return_value=(True, "ok")), patch("main.test_permissions", return_value=(True, True, True, "ok")), @@ -333,7 +321,6 @@ def test_dry_run_writes_the_sync_document_generates_report_and_skips_sync(self): with ( patch("main.validate_config"), - patch("main.resolve_mode", return_value=("online", "jwt-token")), patch( "main.create_directory", return_value=(tmp_dir, raw_data_path), @@ -907,50 +894,6 @@ def test_bad_config_file_exits_config_end_to_end(self): mock_run.assert_not_called() -class ResolveModeSettingsTests(unittest.TestCase): - def test_host_and_key_from_settings_go_online(self): - settings = Settings( - host=Setting("https://platform.test", "env CLOUDEXIT_HOST"), - api_key=Setting("k" * 16, "env CLOUDEXIT_API_KEY"), - timeout=Setting(30.0, "flag --timeout"), - ca_bundle=Setting("/etc/ca.pem", "flag --ca-bundle"), - ) - with patch( - "utils.connection.get_jwt_token", return_value="tok123" - ) as mock_token: - mode, token = resolve_mode(settings) - - self.assertEqual(mode, "online") - self.assertEqual(token, "tok123") - mock_token.assert_called_once_with( - host="https://platform.test", - key="k" * 16, - timeout=30.0, - verify="/etc/ca.pem", - ) - - def test_no_host_or_key_stays_offline_without_a_request(self): - for settings in ( - Settings(), - Settings(host=Setting("https://platform.test", "flag --host")), - Settings(api_key=Setting("k" * 16, "env CLOUDEXIT_API_KEY")), - ): - with patch("utils.connection.get_jwt_token") as mock_token: - mode, token = resolve_mode(settings) - - self.assertEqual(mode, "offline") - self.assertIsNone(token) - mock_token.assert_not_called() - - def test_failed_authentication_falls_back_to_offline(self): - settings = Settings( - host=Setting("https://platform.test", "flag --host"), - api_key=Setting("k" * 16, "env CLOUDEXIT_API_KEY"), - ) - with patch("utils.connection.get_jwt_token", return_value=None): - self.assertEqual(resolve_mode(settings), ("offline", None)) - - TFSTATE_CONFIG = { "name": "Tfstate Assessment", "cloudServiceProvider": 2, @@ -1077,7 +1020,6 @@ def test_run_assessment_skips_credential_permission_and_cost_stages(self): with ( patch("main.validate_config"), - patch("main.resolve_mode", return_value=("offline", None)), patch("main.create_directory", return_value=(tmp_dir, raw_data_path)), patch("main.verify_credentials") as mock_creds, patch("main.test_permissions") as mock_perms, @@ -1115,7 +1057,6 @@ def _run_tfstate_stage3(self, inventory_result): with ( patch("main.validate_config"), - patch("main.resolve_mode", return_value=("offline", None)), patch("main.create_directory", return_value=(tmp_dir, raw_data_path)), patch("main.create_resource_inventory", return_value=inventory_result), patch( @@ -1183,7 +1124,6 @@ def test_live_mode_result_without_coverage_keeps_stage3_ok(self): def test_run_assessment_rejects_egress_from_tfstate_config_file(self): with ( patch("main.validate_config"), - patch("main.resolve_mode", return_value=("offline", None)), patch("main.create_directory") as mock_dir, patch("main.print_step"), patch("main.console.print"), diff --git a/tests/test_utils_sync.py b/tests/test_utils_sync.py deleted file mode 100644 index e75357c..0000000 --- a/tests/test_utils_sync.py +++ /dev/null @@ -1,92 +0,0 @@ -import unittest -from unittest.mock import patch - -from core.utils_sync import _build_payload, post_assessment -from tests.http_fixtures import fake_http, make_response -from utils.settings import SettingsError - -META = {"exit_strategy": 1, "cloud_service_provider": 2, "assessment_type": 1} - - -def post(**overrides): - kwargs = dict( - name="Demo", - started_at=123, - report_path="/tmp/report", - meta=META, - token="jwt-token", - host="platform.test", - ) - kwargs.update(overrides) - return post_assessment(**kwargs) - - -class PostAssessmentTests(unittest.TestCase): - def test_posts_to_the_given_host_over_https(self): - with ( - patch("core.utils_sync._build_payload", return_value={"sample": "payload"}), - fake_http(make_response(json_body={"data": {"ok": True}})) as fake, - ): - result = post() - - self.assertTrue(result["success"]) - (request,) = fake.requests - self.assertEqual(request.url, "https://platform.test/api/v1/assessments/") - self.assertEqual(request.kwargs["json"], {"sample": "payload"}) - - def test_timeout_and_ca_bundle_are_passed_to_requests(self): - with ( - patch("core.utils_sync._build_payload", return_value={}), - fake_http(make_response(json_body={})) as fake, - ): - post(timeout=42.5, verify="/etc/ca.pem") - - self.assertEqual(fake.requests[0].kwargs["timeout"], 42.5) - self.assertEqual(fake.requests[0].kwargs["verify"], "/etc/ca.pem") - - def test_returns_clear_error_when_no_host_is_configured(self): - result = post(host=None) - - self.assertFalse(result["success"]) - self.assertEqual(result["payload"], None) - self.assertIn("no upload host configured", result["logs"]) - - def test_plain_http_to_a_remote_host_is_refused(self): - with patch("core.utils_sync._build_payload", return_value={}): - with self.assertRaises(SettingsError): - post(host="http://platform.test") - - -class BuildPayloadLoggingTests(unittest.TestCase): - def test_the_outgoing_payload_is_summarised_not_dumped(self): - rows = { - "resource_inventory": [ - {"resource_type": 10, "location": "eu-central-1", "count": 3} - ], - "cost_inventory": [{"month": "2025-01", "cost": 42.5, "currency": "USD"}], - } - with ( - patch( - "core.utils_sync.load_data", - side_effect=lambda table, db_path=None: rows[table], - ), - self.assertLogs("core.engine.sync", level="DEBUG") as logs, - ): - payload = _build_payload( - report_path="/tmp/report", - name="Secret Assessment Name", - started_at=1000, - exit_strategy=1, - cloud_service_provider=2, - assessment_type=2, - ) - - self.assertEqual(payload["data"]["name"], "Secret Assessment Name") - text = "\n".join(logs.output) - self.assertIn("1 resource row(s), 1 cost month(s)", text) - self.assertNotIn("Secret Assessment Name", text) - self.assertNotIn("eu-central-1", text) - - -if __name__ == "__main__": - unittest.main() diff --git a/utils/connection.py b/utils/connection.py deleted file mode 100644 index dd06f84..0000000 --- a/utils/connection.py +++ /dev/null @@ -1,86 +0,0 @@ -# utils/connection.py -from __future__ import annotations - -import logging -import requests - -from utils.settings import Settings, load_settings, normalize_host - -logger = logging.getLogger("main.utils.connection") - -_AUTH_PATH = "/api/v1/auth/token/" - - -def _build_url(host: str) -> str: - return f"{normalize_host(host)}{_AUTH_PATH}" - - -def get_jwt_token( - host: str | None = None, - key: str | None = None, - *, - timeout: float = 10, - verify: str | bool = True, -) -> str | None: - if not host: - logger.debug("Host empty – skipping ExitCloud authentication.") - return None - if not key: - logger.debug("API key empty – skipping ExitCloud authentication.") - return None - - url = _build_url(host) - headers = {"Authorization": f"Bearer {key}"} - - try: - resp = requests.post(url, headers=headers, timeout=timeout, verify=verify) - resp.raise_for_status() - data = resp.json() - - token = ( - data.get("access_token") - or data.get("token") - or data.get("access") - or data.get("jwt") - ) - if token: - return token - - logger.error( - "Authentication succeeded but token field missing in response: %s", data - ) - except requests.RequestException as exc: - logger.error( - "EscapeCloud authentication request failed: %s", exc, exc_info=True - ) - except ValueError: - logger.error( - "EscapeCloud authentication response was not valid JSON.", exc_info=True - ) - - return None - - -def resolve_mode(settings: Settings | None = None) -> tuple[str, str | None]: - settings = settings or load_settings() - host = settings.host.value - key = settings.api_key.value - - if not host: - logger.debug("Host empty – running in offline mode.") - return "offline", None - if not key: - logger.debug("API key empty – running in offline mode.") - return "offline", None - - token = get_jwt_token( - host=host, - key=key, - timeout=settings.timeout.value, - verify=settings.ca_bundle.value or True, - ) - if token: - return "online", token - - logger.debug("ExitCloud auth failed – falling back to offline mode.") - return "offline", None From b208b187ebe75d9e6ab943ac8840422a3c6dca71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Sat, 24 Oct 2026 06:01:54 +0200 Subject: [PATCH 09/61] ship report assets inside the package --- .github/workflows/pr-checks.yml | 44 ++++ .gitignore | 1 + README.md | 2 +- {assets => core/assets}/css/style.css | 0 .../assets}/fonts/IBMPlexMono-Regular.ttf | Bin .../assets}/fonts/IBMPlexMono-SemiBold.ttf | Bin .../assets}/fonts/IBMPlexSans-Italic.ttf | Bin .../assets}/fonts/IBMPlexSans-Medium.ttf | Bin .../assets}/fonts/IBMPlexSans-Regular.ttf | Bin .../assets}/fonts/IBMPlexSans-SemiBold.ttf | Bin .../fonts/IBMPlexSans-SemiBoldItalic.ttf | Bin {assets => core/assets}/fonts/OFL.txt | 0 {assets => core/assets}/icons/README.md | 0 .../assets}/icons/aws/Analytics/Athena.png | Bin .../assets}/icons/aws/Analytics/Athena.svg | 0 .../icons/aws/Analytics/Clean-Rooms.png | Bin .../icons/aws/Analytics/Clean-Rooms.svg | 0 .../icons/aws/Analytics/CloudSearch.png | Bin .../icons/aws/Analytics/CloudSearch.svg | 0 .../icons/aws/Analytics/Data-Exchange.png | Bin .../icons/aws/Analytics/Data-Exchange.svg | 0 .../icons/aws/Analytics/Data-Firehose.png | Bin .../icons/aws/Analytics/Data-Firehose.svg | 0 .../icons/aws/Analytics/Data-Pipeline.png | Bin .../icons/aws/Analytics/Data-Pipeline.svg | 0 .../assets}/icons/aws/Analytics/DataZone.png | Bin .../assets}/icons/aws/Analytics/DataZone.svg | 0 .../assets}/icons/aws/Analytics/EMR.png | Bin .../assets}/icons/aws/Analytics/EMR.svg | 0 .../icons/aws/Analytics/Entity-Resolution.png | Bin .../icons/aws/Analytics/Entity-Resolution.svg | 0 .../assets}/icons/aws/Analytics/FinSpace.png | Bin .../assets}/icons/aws/Analytics/FinSpace.svg | 0 .../icons/aws/Analytics/Glue-DataBrew.png | Bin .../icons/aws/Analytics/Glue-DataBrew.svg | 0 .../aws/Analytics/Glue-Elastic-Views.png | Bin .../aws/Analytics/Glue-Elastic-Views.svg | 0 .../assets}/icons/aws/Analytics/Glue.png | Bin .../assets}/icons/aws/Analytics/Glue.svg | 0 .../aws/Analytics/Kinesis-Data-Analytics.png | Bin .../aws/Analytics/Kinesis-Data-Analytics.svg | 0 .../aws/Analytics/Kinesis-Data-Streams.png | Bin .../aws/Analytics/Kinesis-Data-Streams.svg | 0 .../icons/aws/Analytics/Kinesis-Firehose.png | Bin .../icons/aws/Analytics/Kinesis-Firehose.svg | 0 .../aws/Analytics/Kinesis-Video-Streams.png | Bin .../aws/Analytics/Kinesis-Video-Streams.svg | 0 .../assets}/icons/aws/Analytics/Kinesis.png | Bin .../assets}/icons/aws/Analytics/Kinesis.svg | 0 .../icons/aws/Analytics/Lake-Formation.png | Bin .../icons/aws/Analytics/Lake-Formation.svg | 0 .../Managed-Service-for-Apache-Flink.png | Bin .../Managed-Service-for-Apache-Flink.svg | 0 .../Managed-Streaming-for-Apache-Kafka.png | Bin .../Managed-Streaming-for-Apache-Kafka.svg | 0 .../aws/Analytics/OpenSearch-Service.png | Bin .../aws/Analytics/OpenSearch-Service.svg | 0 .../icons/aws/Analytics/QuickSight.png | Bin .../icons/aws/Analytics/QuickSight.svg | 0 .../assets}/icons/aws/Analytics/Redshift.png | Bin .../assets}/icons/aws/Analytics/Redshift.svg | 0 .../icons/aws/App-Integration/API-Gateway.png | Bin .../icons/aws/App-Integration/API-Gateway.svg | 0 .../icons/aws/App-Integration/AppFlow.png | Bin .../icons/aws/App-Integration/AppFlow.svg | 0 .../icons/aws/App-Integration/AppSync.png | Bin .../icons/aws/App-Integration/AppSync.svg | 0 .../App-Integration/B2B-Data-Interchange.png | Bin .../App-Integration/B2B-Data-Interchange.svg | 0 .../Console-Mobile-Application.png | Bin .../Console-Mobile-Application.svg | 0 .../icons/aws/App-Integration/EventBridge.png | Bin .../icons/aws/App-Integration/EventBridge.svg | 0 .../aws/App-Integration/Express-Workflows.png | Bin .../aws/App-Integration/Express-Workflows.svg | 0 .../assets}/icons/aws/App-Integration/MQ.png | Bin .../assets}/icons/aws/App-Integration/MQ.svg | 0 .../Managed-Workflows-for-Apache-Airflow.png | Bin .../Managed-Workflows-for-Apache-Airflow.svg | 0 .../Simple-Notification-Service.png | Bin .../Simple-Notification-Service.svg | 0 .../App-Integration/Simple-Queue-Service.png | Bin .../App-Integration/Simple-Queue-Service.svg | 0 .../aws/App-Integration/Step-Functions.png | Bin .../aws/App-Integration/Step-Functions.svg | 0 .../aws/Blockchain/Managed-Blockchain.png | Bin .../aws/Blockchain/Managed-Blockchain.svg | 0 .../Blockchain/Quantum-Ledger-Database.png | Bin .../Blockchain/Quantum-Ledger-Database.svg | 0 .../Alexa-For-Business.png | Bin .../Alexa-For-Business.svg | 0 .../aws/Business-Applications/AppFabric.png | Bin .../aws/Business-Applications/AppFabric.svg | 0 .../aws/Business-Applications/Chime-SDK.png | Bin .../aws/Business-Applications/Chime-SDK.svg | 0 .../Chime-Voice-Connector.png | Bin .../Chime-Voice-Connector.svg | 0 .../icons/aws/Business-Applications/Chime.png | Bin .../icons/aws/Business-Applications/Chime.svg | 0 .../aws/Business-Applications/Connect.png | Bin .../aws/Business-Applications/Connect.svg | 0 .../End-User-Messaging.png | Bin .../End-User-Messaging.svg | 0 .../aws/Business-Applications/Honeycode.png | Bin .../aws/Business-Applications/Honeycode.svg | 0 .../Business-Applications/Pinpoint-APIs.png | Bin .../Business-Applications/Pinpoint-APIs.svg | 0 .../aws/Business-Applications/Pinpoint.png | Bin .../aws/Business-Applications/Pinpoint.svg | 0 .../icons/aws/Business-Applications/Quick.png | Bin .../icons/aws/Business-Applications/Quick.svg | 0 .../Simple-Email-Service.png | Bin .../Simple-Email-Service.svg | 0 .../Business-Applications/Supply-Chain.png | Bin .../Business-Applications/Supply-Chain.svg | 0 .../icons/aws/Business-Applications/Wickr.png | Bin .../icons/aws/Business-Applications/Wickr.svg | 0 .../Business-Applications/WorkDocs-SDK.png | Bin .../Business-Applications/WorkDocs-SDK.svg | 0 .../aws/Business-Applications/WorkDocs.png | Bin .../aws/Business-Applications/WorkDocs.svg | 0 .../aws/Business-Applications/WorkMail.png | Bin .../aws/Business-Applications/WorkMail.svg | 0 .../Application-Cost-Profiler.png | Bin .../Application-Cost-Profiler.svg | 0 .../Billing-Conductor.png | Bin .../Billing-Conductor.svg | 0 .../Cloud-Financial-Management/Budgets.png | Bin .../Cloud-Financial-Management/Budgets.svg | 0 .../Cost-Explorer.png | Bin .../Cost-Explorer.svg | 0 .../Cost-and-Usage-Report.png | Bin .../Cost-and-Usage-Report.svg | 0 .../Reserved-Instance-Reporting.png | Bin .../Reserved-Instance-Reporting.svg | 0 .../Savings-Plans.png | Bin .../Savings-Plans.svg | 0 .../assets}/icons/aws/Compute/App-Runner.png | Bin .../assets}/icons/aws/Compute/App-Runner.svg | 0 .../aws/Compute/Application-Auto-Scaling.png | Bin .../aws/Compute/Application-Auto-Scaling.svg | 0 .../assets}/icons/aws/Compute/Batch.png | Bin .../assets}/icons/aws/Compute/Batch.svg | 0 .../icons/aws/Compute/Bottlerocket.png | Bin .../icons/aws/Compute/Bottlerocket.svg | 0 .../icons/aws/Compute/Compute-Optimizer.png | Bin .../icons/aws/Compute/Compute-Optimizer.svg | 0 .../assets}/icons/aws/Compute/DCV.png | Bin .../assets}/icons/aws/Compute/DCV.svg | 0 .../icons/aws/Compute/EC2-Auto-Scaling.png | Bin .../icons/aws/Compute/EC2-Auto-Scaling.svg | 0 .../icons/aws/Compute/EC2-Image-Builder.png | Bin .../icons/aws/Compute/EC2-Image-Builder.svg | 0 .../assets}/icons/aws/Compute/EC2.png | Bin .../assets}/icons/aws/Compute/EC2.svg | 0 .../icons/aws/Compute/Elastic-Beanstalk.png | Bin .../icons/aws/Compute/Elastic-Beanstalk.svg | 0 .../aws/Compute/Elastic-Fabric-Adapter.png | Bin .../aws/Compute/Elastic-Fabric-Adapter.svg | 0 .../aws/Compute/Elastic-VMware-Service.png | Bin .../aws/Compute/Elastic-VMware-Service.svg | 0 .../assets}/icons/aws/Compute/Fargate.png | Bin .../assets}/icons/aws/Compute/Fargate.svg | 0 .../icons/aws/Compute/Genomics-CLI.png | Bin .../icons/aws/Compute/Genomics-CLI.svg | 0 .../assets}/icons/aws/Compute/Lambda.png | Bin .../assets}/icons/aws/Compute/Lambda.svg | 0 .../aws/Compute/Lightsail-for-Research.png | Bin .../aws/Compute/Lightsail-for-Research.svg | 0 .../assets}/icons/aws/Compute/Lightsail.png | Bin .../assets}/icons/aws/Compute/Lightsail.svg | 0 .../assets}/icons/aws/Compute/Local-Zones.png | Bin .../assets}/icons/aws/Compute/Local-Zones.svg | 0 .../assets}/icons/aws/Compute/NICE-DCV.png | Bin .../assets}/icons/aws/Compute/NICE-DCV.svg | 0 .../icons/aws/Compute/NICE-EnginFrame.png | Bin .../icons/aws/Compute/NICE-EnginFrame.svg | 0 .../icons/aws/Compute/Nitro-Enclaves.png | Bin .../icons/aws/Compute/Nitro-Enclaves.svg | 0 .../icons/aws/Compute/Outposts-family.png | Bin .../icons/aws/Compute/Outposts-family.svg | 0 .../icons/aws/Compute/Outposts-rack.png | Bin .../icons/aws/Compute/Outposts-rack.svg | 0 .../icons/aws/Compute/Outposts-servers.png | Bin .../icons/aws/Compute/Outposts-servers.svg | 0 .../icons/aws/Compute/Parallel-Cluster.png | Bin .../icons/aws/Compute/Parallel-Cluster.svg | 0 .../Compute/Parallel-Computing-Service.png | Bin .../Compute/Parallel-Computing-Service.svg | 0 .../icons/aws/Compute/ParallelCluster.png | Bin .../icons/aws/Compute/ParallelCluster.svg | 0 .../Serverless-Application-Repository.png | Bin .../Serverless-Application-Repository.svg | 0 .../icons/aws/Compute/SimSpace-Weaver.png | Bin .../icons/aws/Compute/SimSpace-Weaver.svg | 0 .../icons/aws/Compute/Thinkbox-Deadline.png | Bin .../icons/aws/Compute/Thinkbox-Deadline.svg | 0 .../icons/aws/Compute/Thinkbox-Frost.png | Bin .../icons/aws/Compute/Thinkbox-Frost.svg | 0 .../icons/aws/Compute/Thinkbox-Krakatoa.png | Bin .../icons/aws/Compute/Thinkbox-Krakatoa.svg | 0 .../icons/aws/Compute/Thinkbox-Sequoia.png | Bin .../icons/aws/Compute/Thinkbox-Sequoia.svg | 0 .../icons/aws/Compute/Thinkbox-Stoke.png | Bin .../icons/aws/Compute/Thinkbox-Stoke.svg | 0 .../icons/aws/Compute/Thinkbox-XMesh.png | Bin .../icons/aws/Compute/Thinkbox-XMesh.svg | 0 .../icons/aws/Compute/VMware-Cloud-on-AWS.png | Bin .../icons/aws/Compute/VMware-Cloud-on-AWS.svg | 0 .../assets}/icons/aws/Compute/Wavelength.png | Bin .../assets}/icons/aws/Compute/Wavelength.svg | 0 .../icons/aws/Containers/ECS-Anywhere.png | Bin .../icons/aws/Containers/ECS-Anywhere.svg | 0 .../icons/aws/Containers/EKS-Anywhere.png | Bin .../icons/aws/Containers/EKS-Anywhere.svg | 0 .../icons/aws/Containers/EKS-Cloud.png | Bin .../icons/aws/Containers/EKS-Cloud.svg | 0 .../icons/aws/Containers/EKS-Distro.png | Bin .../icons/aws/Containers/EKS-Distro.svg | 0 .../Containers/Elastic-Container-Registry.png | Bin .../Containers/Elastic-Container-Registry.svg | 0 .../Containers/Elastic-Container-Service.png | Bin .../Containers/Elastic-Container-Service.svg | 0 .../Containers/Elastic-Kubernetes-Service.png | Bin .../Containers/Elastic-Kubernetes-Service.svg | 0 .../assets}/icons/aws/Containers/Fargate.png | Bin .../assets}/icons/aws/Containers/Fargate.svg | 0 .../Red-Hat-OpenShift-Service-on-AWS.png | Bin .../Red-Hat-OpenShift-Service-on-AWS.svg | 0 .../aws/Customer-Enablement/Activate.png | Bin .../aws/Customer-Enablement/Activate.svg | 0 .../icons/aws/Customer-Enablement/IQ.png | Bin .../icons/aws/Customer-Enablement/IQ.svg | 0 .../Customer-Enablement/Managed-Services.png | Bin .../Customer-Enablement/Managed-Services.svg | 0 .../Professional-Services.png | Bin .../Professional-Services.svg | 0 .../icons/aws/Customer-Enablement/Support.png | Bin .../icons/aws/Customer-Enablement/Support.svg | 0 .../Training-Certification.png | Bin .../Training-Certification.svg | 0 .../Customer-Enablement/rePost-Private.png | Bin .../Customer-Enablement/rePost-Private.svg | 0 .../icons/aws/Customer-Enablement/rePost.png | Bin .../icons/aws/Customer-Enablement/rePost.svg | 0 .../assets}/icons/aws/Database/Aurora.png | Bin .../assets}/icons/aws/Database/Aurora.svg | 0 .../Database/Database-Migration-Service.png | Bin .../Database/Database-Migration-Service.svg | 0 .../assets}/icons/aws/Database/DocumentDB.png | Bin .../assets}/icons/aws/Database/DocumentDB.svg | 0 .../assets}/icons/aws/Database/DynamoDB.png | Bin .../assets}/icons/aws/Database/DynamoDB.svg | 0 .../icons/aws/Database/ElastiCache.png | Bin .../icons/aws/Database/ElastiCache.svg | 0 .../assets}/icons/aws/Database/Keyspaces.png | Bin .../assets}/icons/aws/Database/Keyspaces.svg | 0 .../icons/aws/Database/MemoryDB-for-Redis.png | Bin .../icons/aws/Database/MemoryDB-for-Redis.svg | 0 .../assets}/icons/aws/Database/MemoryDB.png | Bin .../assets}/icons/aws/Database/MemoryDB.svg | 0 .../assets}/icons/aws/Database/Neptune.png | Bin .../assets}/icons/aws/Database/Neptune.svg | 0 .../aws/Database/Oracle-Database-at-AWS.png | Bin .../aws/Database/Oracle-Database-at-AWS.svg | 0 .../icons/aws/Database/RDS-on-VMware.png | Bin .../icons/aws/Database/RDS-on-VMware.svg | 0 .../assets}/icons/aws/Database/RDS.png | Bin .../assets}/icons/aws/Database/RDS.svg | 0 .../assets}/icons/aws/Database/Timestream.png | Bin .../assets}/icons/aws/Database/Timestream.svg | 0 .../Developer-Tools/Application-Composer.png | Bin .../Developer-Tools/Application-Composer.svg | 0 .../aws/Developer-Tools/Cloud-Control-API.png | Bin .../aws/Developer-Tools/Cloud-Control-API.svg | 0 .../Developer-Tools/Cloud-Development-Kit.png | Bin .../Developer-Tools/Cloud-Development-Kit.svg | 0 .../icons/aws/Developer-Tools/Cloud9.png | Bin .../icons/aws/Developer-Tools/Cloud9.svg | 0 .../icons/aws/Developer-Tools/CloudShell.png | Bin .../icons/aws/Developer-Tools/CloudShell.svg | 0 .../aws/Developer-Tools/CodeArtifact.png | Bin .../aws/Developer-Tools/CodeArtifact.svg | 0 .../icons/aws/Developer-Tools/CodeBuild.png | Bin .../icons/aws/Developer-Tools/CodeBuild.svg | 0 .../aws/Developer-Tools/CodeCatalyst.png | Bin .../aws/Developer-Tools/CodeCatalyst.svg | 0 .../icons/aws/Developer-Tools/CodeCommit.png | Bin .../icons/aws/Developer-Tools/CodeCommit.svg | 0 .../icons/aws/Developer-Tools/CodeDeploy.png | Bin .../icons/aws/Developer-Tools/CodeDeploy.svg | 0 .../aws/Developer-Tools/CodePipeline.png | Bin .../aws/Developer-Tools/CodePipeline.svg | 0 .../icons/aws/Developer-Tools/CodeStar.png | Bin .../icons/aws/Developer-Tools/CodeStar.svg | 0 .../Command-Line-Interface.png | Bin .../Command-Line-Interface.svg | 0 .../icons/aws/Developer-Tools/Corretto.png | Bin .../icons/aws/Developer-Tools/Corretto.svg | 0 .../Fault-Injection-Service.png | Bin .../Fault-Injection-Service.svg | 0 .../Infrastructure-Composer.png | Bin .../Infrastructure-Composer.svg | 0 .../aws/Developer-Tools/Tools-and-SDKs.png | Bin .../aws/Developer-Tools/Tools-and-SDKs.svg | 0 .../icons/aws/Developer-Tools/X-Ray.png | Bin .../icons/aws/Developer-Tools/X-Ray.svg | 0 .../aws/End-User-Computing/AppStream.png | Bin .../aws/End-User-Computing/AppStream.svg | 0 .../icons/aws/End-User-Computing/WorkLink.png | Bin .../icons/aws/End-User-Computing/WorkLink.svg | 0 .../End-User-Computing/WorkSpaces-Family.png | Bin .../End-User-Computing/WorkSpaces-Family.svg | 0 .../aws/End-User-Computing/WorkSpaces.png | Bin .../aws/End-User-Computing/WorkSpaces.svg | 0 .../aws/Front-End-Web-Mobile/Amplify.png | Bin .../aws/Front-End-Web-Mobile/Amplify.svg | 0 .../aws/Front-End-Web-Mobile/Device-Farm.png | Bin .../aws/Front-End-Web-Mobile/Device-Farm.svg | 0 .../Front-End-Web-Mobile/Location-Service.png | Bin .../Front-End-Web-Mobile/Location-Service.svg | 0 .../assets}/icons/aws/Games/GameKit.png | Bin .../assets}/icons/aws/Games/GameKit.svg | 0 .../icons/aws/Games/GameLift-Servers.png | Bin .../icons/aws/Games/GameLift-Servers.svg | 0 .../icons/aws/Games/GameLift-Streams.png | Bin .../icons/aws/Games/GameLift-Streams.svg | 0 .../assets}/icons/aws/Games/GameLift.png | Bin .../assets}/icons/aws/Games/GameLift.svg | 0 .../assets}/icons/aws/Games/GameSparks.png | Bin .../assets}/icons/aws/Games/GameSparks.svg | 0 .../assets}/icons/aws/Games/Lumberyard.png | Bin .../assets}/icons/aws/Games/Lumberyard.svg | 0 .../icons/aws/Games/Open-3D-Engine.png | Bin .../icons/aws/Games/Open-3D-Engine.svg | 0 .../aws/General-Icons/Marketplace_Dark.png | Bin .../aws/General-Icons/Marketplace_Dark.svg | 0 .../aws/General-Icons/Marketplace_Light.png | Bin .../aws/General-Icons/Marketplace_Light.svg | 0 .../icons/aws/Internet-of-Things/FreeRTOS.png | Bin .../icons/aws/Internet-of-Things/FreeRTOS.svg | 0 .../aws/Internet-of-Things/IoT-1-Click.png | Bin .../aws/Internet-of-Things/IoT-1-Click.svg | 0 .../aws/Internet-of-Things/IoT-Analytics.png | Bin .../aws/Internet-of-Things/IoT-Analytics.svg | 0 .../aws/Internet-of-Things/IoT-Button.png | Bin .../aws/Internet-of-Things/IoT-Button.svg | 0 .../icons/aws/Internet-of-Things/IoT-Core.png | Bin .../icons/aws/Internet-of-Things/IoT-Core.svg | 0 .../IoT-Device-Defender.png | Bin .../IoT-Device-Defender.svg | 0 .../IoT-Device-Management.png | Bin .../IoT-Device-Management.svg | 0 .../aws/Internet-of-Things/IoT-EduKit.png | Bin .../aws/Internet-of-Things/IoT-EduKit.svg | 0 .../aws/Internet-of-Things/IoT-Events.png | Bin .../aws/Internet-of-Things/IoT-Events.svg | 0 .../Internet-of-Things/IoT-ExpressLink.png | Bin .../Internet-of-Things/IoT-ExpressLink.svg | 0 .../aws/Internet-of-Things/IoT-FleetWise.png | Bin .../aws/Internet-of-Things/IoT-FleetWise.svg | 0 .../aws/Internet-of-Things/IoT-Greengrass.png | Bin .../aws/Internet-of-Things/IoT-Greengrass.svg | 0 .../aws/Internet-of-Things/IoT-RoboRunner.png | Bin .../aws/Internet-of-Things/IoT-RoboRunner.svg | 0 .../aws/Internet-of-Things/IoT-SiteWise.png | Bin .../aws/Internet-of-Things/IoT-SiteWise.svg | 0 .../Internet-of-Things/IoT-Things-Graph.png | Bin .../Internet-of-Things/IoT-Things-Graph.svg | 0 .../aws/Internet-of-Things/IoT-TwinMaker.png | Bin .../aws/Internet-of-Things/IoT-TwinMaker.svg | 0 .../Machine-Learning/Apache-MXNet-on-AWS.png | Bin .../Machine-Learning/Apache-MXNet-on-AWS.svg | 0 .../icons/aws/Machine-Learning/App-Studio.png | Bin .../icons/aws/Machine-Learning/App-Studio.svg | 0 .../aws/Machine-Learning/Augmented-AI-A2I.png | Bin .../aws/Machine-Learning/Augmented-AI-A2I.svg | 0 .../Machine-Learning/Bedrock-AgentCore.png | Bin .../Machine-Learning/Bedrock-AgentCore.svg | 0 .../icons/aws/Machine-Learning/Bedrock.png | Bin .../icons/aws/Machine-Learning/Bedrock.svg | 0 .../icons/aws/Machine-Learning/CodeGuru.png | Bin .../icons/aws/Machine-Learning/CodeGuru.svg | 0 .../aws/Machine-Learning/CodeWhisperer.png | Bin .../aws/Machine-Learning/CodeWhisperer.svg | 0 .../Machine-Learning/Comprehend-Medical.png | Bin .../Machine-Learning/Comprehend-Medical.svg | 0 .../icons/aws/Machine-Learning/Comprehend.png | Bin .../icons/aws/Machine-Learning/Comprehend.svg | 0 .../Machine-Learning/Deep-Learning-AMIs.png | Bin .../Machine-Learning/Deep-Learning-AMIs.svg | 0 .../Deep-Learning-Containers.png | Bin .../Deep-Learning-Containers.svg | 0 .../aws/Machine-Learning/DeepComposer.png | Bin .../aws/Machine-Learning/DeepComposer.svg | 0 .../icons/aws/Machine-Learning/DeepLens.png | Bin .../icons/aws/Machine-Learning/DeepLens.svg | 0 .../icons/aws/Machine-Learning/DeepRacer.png | Bin .../icons/aws/Machine-Learning/DeepRacer.svg | 0 .../aws/Machine-Learning/DevOps-Guru.png | Bin .../aws/Machine-Learning/DevOps-Guru.svg | 0 .../Machine-Learning/Elastic-Inference.png | Bin .../Machine-Learning/Elastic-Inference.svg | 0 .../Machine-Learning/Elemental-Inference.png | Bin .../Machine-Learning/Elemental-Inference.svg | 0 .../icons/aws/Machine-Learning/Forecast.png | Bin .../icons/aws/Machine-Learning/Forecast.svg | 0 .../aws/Machine-Learning/Fraud-Detector.png | Bin .../aws/Machine-Learning/Fraud-Detector.svg | 0 .../aws/Machine-Learning/HealthImaging.png | Bin .../aws/Machine-Learning/HealthImaging.svg | 0 .../icons/aws/Machine-Learning/HealthLake.png | Bin .../icons/aws/Machine-Learning/HealthLake.svg | 0 .../aws/Machine-Learning/HealthOmics.png | Bin .../aws/Machine-Learning/HealthOmics.svg | 0 .../aws/Machine-Learning/HealthScribe.png | Bin .../aws/Machine-Learning/HealthScribe.svg | 0 .../icons/aws/Machine-Learning/Kendra.png | Bin .../icons/aws/Machine-Learning/Kendra.svg | 0 .../icons/aws/Machine-Learning/Lex.png | Bin .../icons/aws/Machine-Learning/Lex.svg | 0 .../Lookout-for-Equipment.png | Bin .../Lookout-for-Equipment.svg | 0 .../Machine-Learning/Lookout-for-Metrics.png | Bin .../Machine-Learning/Lookout-for-Metrics.svg | 0 .../Machine-Learning/Lookout-for-Vision.png | Bin .../Machine-Learning/Lookout-for-Vision.svg | 0 .../icons/aws/Machine-Learning/Monitron.png | Bin .../icons/aws/Machine-Learning/Monitron.svg | 0 .../icons/aws/Machine-Learning/Neuron.png | Bin .../icons/aws/Machine-Learning/Neuron.svg | 0 .../icons/aws/Machine-Learning/Nova.png | Bin .../icons/aws/Machine-Learning/Nova.svg | 0 .../icons/aws/Machine-Learning/Omics.png | Bin .../icons/aws/Machine-Learning/Omics.svg | 0 .../icons/aws/Machine-Learning/Panorama.png | Bin .../icons/aws/Machine-Learning/Panorama.svg | 0 .../aws/Machine-Learning/Personalize.png | Bin .../aws/Machine-Learning/Personalize.svg | 0 .../icons/aws/Machine-Learning/Polly.png | Bin .../icons/aws/Machine-Learning/Polly.svg | 0 .../aws/Machine-Learning/PyTorch-on-AWS.png | Bin .../aws/Machine-Learning/PyTorch-on-AWS.svg | 0 .../assets}/icons/aws/Machine-Learning/Q.png | Bin .../assets}/icons/aws/Machine-Learning/Q.svg | 0 .../aws/Machine-Learning/Rekognition.png | Bin .../aws/Machine-Learning/Rekognition.svg | 0 .../aws/Machine-Learning/SageMaker-AI.png | Bin .../aws/Machine-Learning/SageMaker-AI.svg | 0 .../SageMaker-Ground-Truth.png | Bin .../SageMaker-Ground-Truth.svg | 0 .../Machine-Learning/SageMaker-Studio-Lab.png | Bin .../Machine-Learning/SageMaker-Studio-Lab.svg | 0 .../icons/aws/Machine-Learning/SageMaker.png | Bin .../icons/aws/Machine-Learning/SageMaker.svg | 0 .../Machine-Learning/TensorFlow-on-AWS.png | Bin .../Machine-Learning/TensorFlow-on-AWS.svg | 0 .../icons/aws/Machine-Learning/Textract.png | Bin .../icons/aws/Machine-Learning/Textract.svg | 0 .../icons/aws/Machine-Learning/TorchServe.png | Bin .../icons/aws/Machine-Learning/TorchServe.svg | 0 .../icons/aws/Machine-Learning/Transcribe.png | Bin .../icons/aws/Machine-Learning/Transcribe.svg | 0 .../icons/aws/Machine-Learning/Translate.png | Bin .../icons/aws/Machine-Learning/Translate.svg | 0 .../aws/Management-Governance/AppConfig.png | Bin .../aws/Management-Governance/AppConfig.svg | 0 .../Application-Auto-Scaling.png | Bin .../Application-Auto-Scaling.svg | 0 .../Management-Governance/Auto-Scaling.png | Bin .../Management-Governance/Auto-Scaling.svg | 0 .../Management-Governance/Backint-Agent.png | Bin .../Management-Governance/Backint-Agent.svg | 0 .../aws/Management-Governance/Chatbot.png | Bin .../aws/Management-Governance/Chatbot.svg | 0 .../Management-Governance/CloudFormation.png | Bin .../Management-Governance/CloudFormation.svg | 0 .../aws/Management-Governance/CloudTrail.png | Bin .../aws/Management-Governance/CloudTrail.svg | 0 .../aws/Management-Governance/CloudWatch.png | Bin .../aws/Management-Governance/CloudWatch.svg | 0 .../aws/Management-Governance/Config.png | Bin .../aws/Management-Governance/Config.svg | 0 .../Management-Governance/Control-Tower.png | Bin .../Management-Governance/Control-Tower.svg | 0 .../Management-Governance/DevOps-Agent.png | Bin .../Management-Governance/DevOps-Agent.svg | 0 .../Distro-for-OpenTelemetry.png | Bin .../Distro-for-OpenTelemetry.svg | 0 .../Fault-Injection-Simulator.png | Bin .../Fault-Injection-Simulator.svg | 0 .../Health-Dashboard.png | Bin .../Health-Dashboard.svg | 0 .../Management-Governance/Launch-Wizard.png | Bin .../Management-Governance/Launch-Wizard.svg | 0 .../Management-Governance/License-Manager.png | Bin .../Management-Governance/License-Manager.svg | 0 .../Management-Governance/Managed-Grafana.png | Bin .../Management-Governance/Managed-Grafana.svg | 0 .../Managed-Service-for-Prometheus.png | Bin .../Managed-Service-for-Prometheus.svg | 0 .../Management-Console.png | Bin .../Management-Console.svg | 0 .../aws/Management-Governance/OpsWorks.png | Bin .../aws/Management-Governance/OpsWorks.svg | 0 .../Management-Governance/Organizations.png | Bin .../Management-Governance/Organizations.svg | 0 .../Management-Governance/Partner-Central.png | Bin .../Management-Governance/Partner-Central.svg | 0 .../Personal-Health-Dashboard.png | Bin .../Personal-Health-Dashboard.svg | 0 .../aws/Management-Governance/Proton.png | Bin .../aws/Management-Governance/Proton.svg | 0 .../Management-Governance/Resilience-Hub.png | Bin .../Management-Governance/Resilience-Hub.svg | 0 .../Resource-Explorer.png | Bin .../Resource-Explorer.svg | 0 .../Management-Governance/Service-Catalog.png | Bin .../Management-Governance/Service-Catalog.svg | 0 .../Service-Management-Connector.png | Bin .../Service-Management-Connector.svg | 0 .../Management-Governance/Sustainability.png | Bin .../Management-Governance/Sustainability.svg | 0 .../Management-Governance/Systems-Manager.png | Bin .../Management-Governance/Systems-Manager.svg | 0 .../Telco-Network-Builder.png | Bin .../Telco-Network-Builder.svg | 0 .../Management-Governance/Trusted-Advisor.png | Bin .../Management-Governance/Trusted-Advisor.svg | 0 .../User-Notifications.png | Bin .../User-Notifications.svg | 0 .../Well-Architected-Tool.png | Bin .../Well-Architected-Tool.svg | 0 .../aws/Media-Services/Deadline-Cloud.png | Bin .../aws/Media-Services/Deadline-Cloud.svg | 0 .../aws/Media-Services/Elastic-Transcoder.png | Bin .../aws/Media-Services/Elastic-Transcoder.svg | 0 .../Elemental-Appliances-&-Software.png | Bin .../Elemental-Appliances-&-Software.svg | 0 .../Media-Services/Elemental-Conductor.png | Bin .../Media-Services/Elemental-Conductor.svg | 0 .../aws/Media-Services/Elemental-Delta.png | Bin .../aws/Media-Services/Elemental-Delta.svg | 0 .../aws/Media-Services/Elemental-Link.png | Bin .../aws/Media-Services/Elemental-Link.svg | 0 .../aws/Media-Services/Elemental-Live.png | Bin .../aws/Media-Services/Elemental-Live.svg | 0 .../Media-Services/Elemental-MediaConnect.png | Bin .../Media-Services/Elemental-MediaConnect.svg | 0 .../Media-Services/Elemental-MediaConvert.png | Bin .../Media-Services/Elemental-MediaConvert.svg | 0 .../Media-Services/Elemental-MediaLive.png | Bin .../Media-Services/Elemental-MediaLive.svg | 0 .../Media-Services/Elemental-MediaPackage.png | Bin .../Media-Services/Elemental-MediaPackage.svg | 0 .../Media-Services/Elemental-MediaStore.png | Bin .../Media-Services/Elemental-MediaStore.svg | 0 .../Media-Services/Elemental-MediaTailor.png | Bin .../Media-Services/Elemental-MediaTailor.svg | 0 .../aws/Media-Services/Elemental-Server.png | Bin .../aws/Media-Services/Elemental-Server.svg | 0 .../Interactive-Video-Service.png | Bin .../Interactive-Video-Service.svg | 0 .../Media-Services/Kinesis-Video-Streams.png | Bin .../Media-Services/Kinesis-Video-Streams.svg | 0 .../aws/Media-Services/Nimble-Studio.png | Bin .../aws/Media-Services/Nimble-Studio.svg | 0 .../Application-Discovery-Service.png | Bin .../Application-Discovery-Service.svg | 0 .../Application-Migration-Service.png | Bin .../Application-Migration-Service.svg | 0 .../Data-Transfer-Terminal.png | Bin .../Data-Transfer-Terminal.svg | 0 .../icons/aws/Migration-Transfer/DataSync.png | Bin .../icons/aws/Migration-Transfer/DataSync.svg | 0 .../Mainframe-Modernization.png | Bin .../Mainframe-Modernization.svg | 0 .../Migration-Evaluator.png | Bin .../Migration-Evaluator.svg | 0 .../aws/Migration-Transfer/Migration-Hub.png | Bin .../aws/Migration-Transfer/Migration-Hub.svg | 0 .../Server-Migration-Service.png | Bin .../Server-Migration-Service.svg | 0 .../Migration-Transfer/Transfer-Family.png | Bin .../Migration-Transfer/Transfer-Family.svg | 0 .../aws/Migration-Transfer/Transform.png | Bin .../aws/Migration-Transfer/Transform.svg | 0 .../Networking-Content-Delivery/App-Mesh.png | Bin .../Networking-Content-Delivery/App-Mesh.svg | 0 .../Application-Recovery-Controller.png | Bin .../Application-Recovery-Controller.svg | 0 .../Client-VPN.png | Bin .../Client-VPN.svg | 0 .../Cloud-Directory.png | Bin .../Cloud-Directory.svg | 0 .../Networking-Content-Delivery/Cloud-Map.png | Bin .../Networking-Content-Delivery/Cloud-Map.svg | 0 .../Networking-Content-Delivery/Cloud-WAN.png | Bin .../Networking-Content-Delivery/Cloud-WAN.svg | 0 .../CloudFront.png | Bin .../CloudFront.svg | 0 .../Direct-Connect.png | Bin .../Direct-Connect.svg | 0 .../Elastic-Load-Balancing.png | Bin .../Elastic-Load-Balancing.svg | 0 .../Global-Accelerator.png | Bin .../Global-Accelerator.svg | 0 .../Interconnect.png | Bin .../Interconnect.svg | 0 .../Private-5G.png | Bin .../Private-5G.svg | 0 .../PrivateLink.png | Bin .../PrivateLink.svg | 0 .../RTB-Fabric.png | Bin .../RTB-Fabric.svg | 0 .../Networking-Content-Delivery/Route-53.png | Bin .../Networking-Content-Delivery/Route-53.svg | 0 .../Site-to-Site-VPN.png | Bin .../Site-to-Site-VPN.svg | 0 .../Transit-Gateway.png | Bin .../Transit-Gateway.svg | 0 .../VPC-Lattice.png | Bin .../VPC-Lattice.svg | 0 .../Verified-Access.png | Bin .../Verified-Access.svg | 0 .../Virtual-Private-Cloud.png | Bin .../Virtual-Private-Cloud.svg | 0 .../icons/aws/Quantum-Technologies/Braket.png | Bin .../icons/aws/Quantum-Technologies/Braket.svg | 0 .../assets}/icons/aws/Robotics/RoboMaker.png | Bin .../assets}/icons/aws/Robotics/RoboMaker.svg | 0 .../icons/aws/Satellite/Ground-Station.png | Bin .../icons/aws/Satellite/Ground-Station.svg | 0 .../Security-Identity-Compliance/Artifact.png | Bin .../Security-Identity-Compliance/Artifact.svg | 0 .../Audit-Manager.png | Bin .../Audit-Manager.svg | 0 .../Certificate-Manager.png | Bin .../Certificate-Manager.svg | 0 .../Cloud-Directory.png | Bin .../Cloud-Directory.svg | 0 .../Security-Identity-Compliance/CloudHSM.png | Bin .../Security-Identity-Compliance/CloudHSM.svg | 0 .../Security-Identity-Compliance/Cognito.png | Bin .../Security-Identity-Compliance/Cognito.svg | 0 .../Detective.png | Bin .../Detective.svg | 0 .../Directory-Service.png | Bin .../Directory-Service.svg | 0 .../Firewall-Manager.png | Bin .../Firewall-Manager.svg | 0 .../GuardDuty.png | Bin .../GuardDuty.svg | 0 .../IAM-Identity-Center.png | Bin .../IAM-Identity-Center.svg | 0 .../Identity-and-Access-Management.png | Bin .../Identity-and-Access-Management.svg | 0 .../Inspector.png | Bin .../Inspector.svg | 0 .../Key-Management-Service.png | Bin .../Key-Management-Service.svg | 0 .../Security-Identity-Compliance/Macie.png | Bin .../Security-Identity-Compliance/Macie.svg | 0 .../Network-Firewall.png | Bin .../Network-Firewall.svg | 0 .../Payment-Cryptography.png | Bin .../Payment-Cryptography.svg | 0 .../Private-Certificate-Authority.png | Bin .../Private-Certificate-Authority.svg | 0 .../Resource-Access-Manager.png | Bin .../Resource-Access-Manager.svg | 0 .../Secrets-Manager.png | Bin .../Secrets-Manager.svg | 0 .../Security-Agent.png | Bin .../Security-Agent.svg | 0 .../Security-Hub.png | Bin .../Security-Hub.svg | 0 .../Security-Incident-Response.png | Bin .../Security-Incident-Response.svg | 0 .../Security-Lake.png | Bin .../Security-Lake.svg | 0 .../Security-Identity-Compliance/Shield.png | Bin .../Security-Identity-Compliance/Shield.svg | 0 .../Security-Identity-Compliance/Signer.png | Bin .../Security-Identity-Compliance/Signer.svg | 0 .../Verified-Permissions.png | Bin .../Verified-Permissions.svg | 0 .../aws/Security-Identity-Compliance/WAF.png | Bin .../aws/Security-Identity-Compliance/WAF.svg | 0 .../assets}/icons/aws/Storage/Backup.png | Bin .../assets}/icons/aws/Storage/Backup.svg | 0 .../assets}/icons/aws/Storage/EFS.png | Bin .../assets}/icons/aws/Storage/EFS.svg | 0 .../icons/aws/Storage/Elastic-Block-Store.png | Bin .../icons/aws/Storage/Elastic-Block-Store.svg | 0 .../aws/Storage/Elastic-Disaster-Recovery.png | Bin .../aws/Storage/Elastic-Disaster-Recovery.svg | 0 .../icons/aws/Storage/FSx-for-Lustre.png | Bin .../icons/aws/Storage/FSx-for-Lustre.svg | 0 .../aws/Storage/FSx-for-NetApp-ONTAP.png | Bin .../aws/Storage/FSx-for-NetApp-ONTAP.svg | 0 .../icons/aws/Storage/FSx-for-OpenZFS.png | Bin .../icons/aws/Storage/FSx-for-OpenZFS.svg | 0 .../assets}/icons/aws/Storage/FSx-for-WFS.png | Bin .../assets}/icons/aws/Storage/FSx-for-WFS.svg | 0 .../assets}/icons/aws/Storage/FSx.png | Bin .../assets}/icons/aws/Storage/FSx.svg | 0 .../assets}/icons/aws/Storage/File-Cache.png | Bin .../assets}/icons/aws/Storage/File-Cache.svg | 0 .../icons/aws/Storage/S3-on-Outposts.png | Bin .../icons/aws/Storage/S3-on-Outposts.svg | 0 .../Simple-Storage-Service-Glacier.png | Bin .../Simple-Storage-Service-Glacier.svg | 0 .../aws/Storage/Simple-Storage-Service.png | Bin .../aws/Storage/Simple-Storage-Service.svg | 0 .../icons/aws/Storage/Snowball-Edge.png | Bin .../icons/aws/Storage/Snowball-Edge.svg | 0 .../assets}/icons/aws/Storage/Snowball.png | Bin .../assets}/icons/aws/Storage/Snowball.svg | 0 .../assets}/icons/aws/Storage/Snowcone.png | Bin .../assets}/icons/aws/Storage/Snowcone.svg | 0 .../assets}/icons/aws/Storage/Snowmobile.png | Bin .../assets}/icons/aws/Storage/Snowmobile.svg | 0 .../icons/aws/Storage/Storage-Gateway.png | Bin .../icons/aws/Storage/Storage-Gateway.svg | 0 .../assets}/icons/aws/VR-AR/Sumerian.png | Bin .../assets}/icons/aws/VR-AR/Sumerian.svg | 0 .../00028-icon-service-Batch-AI.png | Bin .../00028-icon-service-Batch-AI.svg | 0 ...Learning-Studio-(Classic)-Web-Services.png | Bin ...Learning-Studio-(Classic)-Web-Services.svg | 0 .../00031-icon-service-Genomics.png | Bin .../00031-icon-service-Genomics.svg | 0 .../00792-icon-service-Computer-Vision.png | Bin .../00792-icon-service-Computer-Vision.svg | 0 .../00793-icon-service-Custom-Vision.png | Bin .../00793-icon-service-Custom-Vision.svg | 0 .../00794-icon-service-Face-APIs.png | Bin .../00794-icon-service-Face-APIs.svg | 0 .../00795-icon-service-Content-Moderators.png | Bin .../00795-icon-service-Content-Moderators.svg | 0 .../00796-icon-service-Personalizers.png | Bin .../00796-icon-service-Personalizers.svg | 0 .../00797-icon-service-Speech-Services.png | Bin .../00797-icon-service-Speech-Services.svg | 0 .../00799-icon-service-QnA-Makers.png | Bin .../00799-icon-service-QnA-Makers.svg | 0 .../00800-icon-service-Translator-Text.png | Bin .../00800-icon-service-Translator-Text.svg | 0 ...01-icon-service-Language-Understanding.png | Bin ...01-icon-service-Language-Understanding.svg | 0 .../00812-icon-service-Immersive-Readers.png | Bin .../00812-icon-service-Immersive-Readers.svg | 0 .../00814-icon-service-Anomaly-Detector.png | Bin .../00814-icon-service-Anomaly-Detector.svg | 0 .../00819-icon-service-Form-Recognizers.png | Bin .../00819-icon-service-Form-Recognizers.svg | 0 ...n-service-Azure-Experimentation-Studio.png | Bin ...n-service-Azure-Experimentation-Studio.svg | 0 ...con-service-Azure-Object-Understanding.png | Bin ...con-service-Azure-Object-Understanding.svg | 0 .../02409-icon-service-Metrics-Advisor.png | Bin .../02409-icon-service-Metrics-Advisor.svg | 0 ...icon-service-Azure-Applied-AI-Services.png | Bin ...icon-service-Azure-Applied-AI-Services.svg | 0 .../02876-icon-service-Language.png | Bin .../02876-icon-service-Language.svg | 0 .../029455661-icon-service-FRD-QA.png | Bin .../029455661-icon-service-FRD-QA.svg | 0 ...n-service-Cognitive-Services-Decisions.png | Bin ...n-service-Cognitive-Services-Decisions.svg | 0 .../03321-icon-service-Serverless-Search.png | Bin .../03321-icon-service-Serverless-Search.svg | 0 .../03337-icon-service-Bonsai.png | Bin .../03337-icon-service-Bonsai.svg | 0 .../03390-icon-service-Content-Safety.png | Bin .../03390-icon-service-Content-Safety.svg | 0 .../03438-icon-service-Azure-OpenAI.png | Bin .../03438-icon-service-Azure-OpenAI.svg | 0 .../03513-icon-service-AI-Studio.png | Bin .../03513-icon-service-AI-Studio.svg | 0 .../035746832-icon-service-AI-Foundry.png | Bin .../035746832-icon-service-AI-Foundry.svg | 0 ...09374-icon-service-Foundry-Application.png | Bin ...09374-icon-service-Foundry-Application.svg | 0 ...036509415-icon-service-Foundry-Project.png | Bin ...036509415-icon-service-Foundry-Project.svg | 0 ...55-icon-service-Planetary-Computer-Pro.png | Bin ...55-icon-service-Planetary-Computer-Pro.svg | 0 ...70497-icon-service-Azure-AI-Foundry-IQ.png | Bin ...70497-icon-service-Azure-AI-Foundry-IQ.svg | 0 ...10-icon-service-Azure-Machine-Learning.png | Bin ...10-icon-service-Azure-Machine-Learning.svg | 0 ...523-icon-service-Foundry-Agent-Service.png | Bin ...523-icon-service-Foundry-Agent-Service.svg | 0 ...534-icon-service-Foundry-Control-Plane.png | Bin ...534-icon-service-Foundry-Control-Plane.svg | 0 .../038470555-icon-service-Foundry-Labs.png | Bin .../038470555-icon-service-Foundry-Labs.svg | 0 .../038470593-icon-service-Foundry-Local.png | Bin .../038470593-icon-service-Foundry-Local.svg | 0 .../038470614-icon-service-Foundry-Models.png | Bin .../038470614-icon-service-Foundry-Models.svg | 0 .../10044-icon-service-Cognitive-Search.png | Bin .../10044-icon-service-Cognitive-Search.svg | 0 .../10162-icon-service-Cognitive-Services.png | Bin .../10162-icon-service-Cognitive-Services.svg | 0 .../10164-icon-service-Genomics-Accounts.png | Bin .../10164-icon-service-Genomics-Accounts.svg | 0 .../10165-icon-service-Bot-Services.png | Bin .../10165-icon-service-Bot-Services.svg | 0 .../10166-icon-service-Machine-Learning.png | Bin .../10166-icon-service-Machine-Learning.svg | 0 ...ice-Machine-Learning-Studio-Workspaces.png | Bin ...ice-Machine-Learning-Studio-Workspaces.svg | 0 ...hine-Learning-Studio-Web-Service-Plans.png | Bin ...hine-Learning-Studio-Web-Service-Plans.svg | 0 ...-icon-service-Log-Analytics-Workspaces.png | Bin ...-icon-service-Log-Analytics-Workspaces.svg | 0 .../00039-icon-service-Event-Hubs.png | Bin .../00039-icon-service-Event-Hubs.svg | 0 ...042-icon-service-Stream-Analytics-Jobs.png | Bin ...042-icon-service-Stream-Analytics-Jobs.svg | 0 .../00562-icon-service-Endpoint-Analytics.png | Bin .../00562-icon-service-Endpoint-Analytics.svg | 0 ...6-icon-service-Azure-Synapse-Analytics.png | Bin ...6-icon-service-Azure-Synapse-Analytics.svg | 0 .../02189-icon-service-Azure-Workbooks.png | Bin .../02189-icon-service-Azure-Workbooks.svg | 0 ...209-icon-service-Private-Link-Services.png | Bin ...209-icon-service-Private-Link-Services.svg | 0 .../03332-icon-service-Power-BI-Embedded.png | Bin .../03332-icon-service-Power-BI-Embedded.svg | 0 .../03335-icon-service-Power-Platform.png | Bin .../03335-icon-service-Power-Platform.svg | 0 .../10126-icon-service-Data-Factories.png | Bin .../10126-icon-service-Data-Factories.svg | 0 ...10142-icon-service-HD-Insight-Clusters.png | Bin ...10142-icon-service-HD-Insight-Clusters.svg | 0 ...10143-icon-service-Data-Lake-Analytics.png | Bin ...10143-icon-service-Data-Lake-Analytics.svg | 0 ...n-service-Azure-Data-Explorer-Clusters.png | Bin ...n-service-Azure-Data-Explorer-Clusters.svg | 0 .../10148-icon-service-Analysis-Services.png | Bin .../10148-icon-service-Analysis-Services.svg | 0 .../10149-icon-service-Event-Hub-Clusters.png | Bin .../10149-icon-service-Event-Hub-Clusters.svg | 0 ...0150-icon-service-Data-Lake-Store-Gen1.png | Bin ...0150-icon-service-Data-Lake-Store-Gen1.svg | 0 .../10787-icon-service-Azure-Databricks.png | Bin .../10787-icon-service-Azure-Databricks.svg | 0 .../00046-icon-service-App-Service-Plans.png | Bin .../00046-icon-service-App-Service-Plans.svg | 0 ...-icon-service-App-Service-Certificates.png | Bin ...-icon-service-App-Service-Certificates.svg | 0 ...00050-icon-service-App-Service-Domains.png | Bin ...00050-icon-service-App-Service-Domains.svg | 0 .../00056-icon-service-CDN-Profiles.png | Bin .../00056-icon-service-CDN-Profiles.svg | 0 .../10035-icon-service-App-Services.png | Bin .../10035-icon-service-App-Services.svg | 0 .../10044-icon-service-Cognitive-Search.png | Bin .../10044-icon-service-Cognitive-Search.svg | 0 .../10045-icon-service-Notification-Hubs.png | Bin .../10045-icon-service-Notification-Hubs.svg | 0 ...-icon-service-App-Service-Environments.png | Bin ...-icon-service-App-Service-Environments.svg | 0 ...038-icon-service-Collaborative-Service.png | Bin ...038-icon-service-Collaborative-Service.svg | 0 .../02354-icon-service-Applens.png | Bin .../02354-icon-service-Applens.svg | 0 ...02573-icon-service-Azure-Hybrid-Center.png | Bin ...02573-icon-service-Azure-Hybrid-Center.svg | 0 .../00965-icon-service-Multi-Tenancy.png | Bin .../00965-icon-service-Multi-Tenancy.svg | 0 ...108-icon-service-Infrastructure-Backup.png | Bin ...108-icon-service-Infrastructure-Backup.svg | 0 .../10109-icon-service-Capacity.png | Bin .../10109-icon-service-Capacity.svg | 0 .../azure stack/10110-icon-service-Offers.png | Bin .../azure stack/10110-icon-service-Offers.svg | 0 .../10111-icon-service-User-Subscriptions.png | Bin .../10111-icon-service-User-Subscriptions.svg | 0 .../azure stack/10113-icon-service-Plans.png | Bin .../azure stack/10113-icon-service-Plans.svg | 0 .../10115-icon-service-Updates.png | Bin .../10115-icon-service-Updates.svg | 0 ...0-icon-service-Blockchain-Applications.png | Bin ...0-icon-service-Blockchain-Applications.svg | 0 ...10364-icon-service-Outbound-Connection.png | Bin ...10364-icon-service-Outbound-Connection.svg | 0 ...-icon-service-Azure-Blockchain-Service.png | Bin ...-icon-service-Azure-Blockchain-Service.svg | 0 ...10367-icon-service-Azure-Token-Service.png | Bin ...10367-icon-service-Azure-Token-Service.svg | 0 .../10374-icon-service-ABS-Member.png | Bin .../10374-icon-service-ABS-Member.svg | 0 .../10375-icon-service-Consortium.png | Bin .../10375-icon-service-Consortium.svg | 0 ...icon-service-Maintenance-Configuration.png | Bin ...icon-service-Maintenance-Configuration.svg | 0 .../compute/00328-icon-service-Host-Pools.png | Bin .../compute/00328-icon-service-Host-Pools.svg | 0 .../00329-icon-service-Application-Group.png | Bin .../00329-icon-service-Application-Group.svg | 0 .../compute/00330-icon-service-Workspaces.png | Bin .../compute/00330-icon-service-Workspaces.svg | 0 ...0398-icon-service-Disk-Encryption-Sets.png | Bin ...0398-icon-service-Disk-Encryption-Sets.svg | 0 .../compute/00400-icon-service-Workspaces.png | Bin .../compute/00400-icon-service-Workspaces.svg | 0 .../02112-icon-service-Automanaged-VM.png | Bin .../02112-icon-service-Automanaged-VM.svg | 0 ...70-icon-service-Managed-Service-Fabric.png | Bin ...70-icon-service-Managed-Service-Fabric.svg | 0 .../02409-icon-service-Metrics-Advisor.png | Bin .../02409-icon-service-Metrics-Advisor.svg | 0 .../02634-icon-service-Image-Templates.png | Bin .../02634-icon-service-Image-Templates.svg | 0 .../02817-icon-service-Restore-Points.png | Bin .../02817-icon-service-Restore-Points.svg | 0 ...con-service-Restore-Points-Collections.png | Bin ...con-service-Restore-Points-Collections.svg | 0 ...4-icon-service-Azure-Compute-Galleries.png | Bin ...4-icon-service-Azure-Compute-Galleries.svg | 0 .../030868929-icon-service-Stage-Maps.png | Bin .../030868929-icon-service-Stage-Maps.svg | 0 ...3049334-icon-service-Scheduled-Actions.png | Bin ...3049334-icon-service-Scheduled-Actions.svg | 0 .../03487-icon-service-Compute-Fleet.png | Bin .../03487-icon-service-Compute-Fleet.svg | 0 .../03543-icon-service-AKS-Automatic.png | Bin .../03543-icon-service-AKS-Automatic.svg | 0 .../03645-icon-service-Azure-Linux.png | Bin .../03645-icon-service-Azure-Linux.svg | 0 .../10021-icon-service-Virtual-Machine.png | Bin .../10021-icon-service-Virtual-Machine.svg | 0 ...10023-icon-service-Kubernetes-Services.png | Bin ...10023-icon-service-Kubernetes-Services.svg | 0 .../10024-icon-service-Mesh-Applications.png | Bin .../10024-icon-service-Mesh-Applications.svg | 0 .../10025-icon-service-Availability-Sets.png | Bin .../10025-icon-service-Availability-Sets.svg | 0 .../10026-icon-service-Disks-Snapshots.png | Bin .../10026-icon-service-Disks-Snapshots.svg | 0 ...10027-icon-service-OS-Images-(Classic).png | Bin ...10027-icon-service-OS-Images-(Classic).svg | 0 ...con-service-Virtual-Machines-(Classic).png | Bin ...con-service-Virtual-Machines-(Classic).svg | 0 .../10029-icon-service-Function-Apps.png | Bin .../10029-icon-service-Function-Apps.svg | 0 ...-icon-service-Cloud-Services-(Classic).png | Bin ...-icon-service-Cloud-Services-(Classic).svg | 0 .../10031-icon-service-Batch-Accounts.png | Bin .../10031-icon-service-Batch-Accounts.svg | 0 .../compute/10032-icon-service-Disks.png | Bin .../compute/10032-icon-service-Disks.svg | 0 .../compute/10033-icon-service-Images.png | Bin .../compute/10033-icon-service-Images.svg | 0 .../10034-icon-service-VM-Scale-Sets.png | Bin .../10034-icon-service-VM-Scale-Sets.svg | 0 .../10035-icon-service-App-Services.png | Bin .../10035-icon-service-App-Services.svg | 0 ...6-icon-service-Service-Fabric-Clusters.png | Bin ...6-icon-service-Service-Fabric-Clusters.svg | 0 .../10037-icon-service-Image-Definitions.png | Bin .../10037-icon-service-Image-Definitions.svg | 0 .../10038-icon-service-Image-Versions.png | Bin .../10038-icon-service-Image-Versions.svg | 0 ...39-icon-service-Shared-Image-Galleries.png | Bin ...39-icon-service-Shared-Image-Galleries.svg | 0 ...10040-icon-service-VM-Images-(Classic).png | Bin ...10040-icon-service-VM-Images-(Classic).svg | 0 .../10041-icon-service-Disks-(Classic).png | Bin .../10041-icon-service-Disks-(Classic).svg | 0 ...ervice-Container-Services-(Deprecated).png | Bin ...ervice-Container-Services-(Deprecated).svg | 0 ...10104-icon-service-Container-Instances.png | Bin ...10104-icon-service-Container-Instances.svg | 0 .../10346-icon-service-Host-Groups.png | Bin .../10346-icon-service-Host-Groups.svg | 0 .../compute/10347-icon-service-Hosts.png | Bin .../compute/10347-icon-service-Hosts.svg | 0 .../10370-icon-service-Azure-Spring-Apps.png | Bin .../10370-icon-service-Azure-Spring-Apps.svg | 0 .../029637767-icon-service-Service-Groups.png | Bin .../029637767-icon-service-Service-Groups.svg | 0 ...487601-icon-service-AKS-Network-Policy.png | Bin ...487601-icon-service-AKS-Network-Policy.svg | 0 ...1-icon-service-Azure-Red-Hat-OpenShift.png | Bin ...1-icon-service-Azure-Red-Hat-OpenShift.svg | 0 .../033545113-icon-service-Kubernetes-Hub.png | Bin .../033545113-icon-service-Kubernetes-Hub.svg | 0 ...1-icon-service-Azure-Container-Storage.png | Bin ...1-icon-service-Azure-Container-Storage.svg | 0 ...10023-icon-service-Kubernetes-Services.png | Bin ...10023-icon-service-Kubernetes-Services.svg | 0 .../10031-icon-service-Batch-Accounts.png | Bin .../10031-icon-service-Batch-Accounts.svg | 0 .../10035-icon-service-App-Services.png | Bin .../10035-icon-service-App-Services.svg | 0 ...6-icon-service-Service-Fabric-Clusters.png | Bin ...6-icon-service-Service-Fabric-Clusters.svg | 0 ...10104-icon-service-Container-Instances.png | Bin ...10104-icon-service-Container-Instances.svg | 0 ...0105-icon-service-Container-Registries.png | Bin ...0105-icon-service-Container-Registries.svg | 0 ...00036-icon-service-SQL-Data-Warehouses.png | Bin ...00036-icon-service-SQL-Data-Warehouses.svg | 0 ...6-icon-service-Azure-Synapse-Analytics.png | Bin ...6-icon-service-Azure-Synapse-Analytics.svg | 0 .../02390-icon-service-Azure-SQL.png | Bin .../02390-icon-service-Azure-SQL.svg | 0 ...92-icon-service-SSIS-Lift-And-Shift-IR.png | Bin ...92-icon-service-SSIS-Lift-And-Shift-IR.svg | 0 ...17-icon-service-Azure-Purview-Accounts.png | Bin ...17-icon-service-Azure-Purview-Accounts.svg | 0 .../02750-icon-service-Azure-SQL-Edge.png | Bin .../02750-icon-service-Azure-SQL-Edge.svg | 0 ...Azure-Database-PostgreSQL-Server-Group.png | Bin ...Azure-Database-PostgreSQL-Server-Group.svg | 0 .../03490-icon-service-Oracle-Database.png | Bin .../03490-icon-service-Oracle-Database.svg | 0 ...con-service-SQL-Database-Fleet-Manager.png | Bin ...con-service-SQL-Database-Fleet-Manager.svg | 0 ...35656437-icon-service-Azure-DocumentDB.png | Bin ...35656437-icon-service-Azure-DocumentDB.svg | 0 ...03675-icon-service-Azure-Managed-Redis.png | Bin ...03675-icon-service-Azure-Managed-Redis.svg | 0 .../10121-icon-service-Azure-Cosmos-DB.png | Bin .../10121-icon-service-Azure-Cosmos-DB.svg | 0 ...on-service-Azure-Database-MySQL-Server.png | Bin ...on-service-Azure-Database-MySQL-Server.svg | 0 ...-service-Azure-Database-MariaDB-Server.png | Bin ...-service-Azure-Database-MariaDB-Server.svg | 0 .../10124-icon-service-Azure-SQL-VM.png | Bin .../10124-icon-service-Azure-SQL-VM.svg | 0 .../10126-icon-service-Data-Factories.png | Bin .../10126-icon-service-Data-Factories.svg | 0 .../10127-icon-service-Virtual-Clusters.png | Bin .../10127-icon-service-Virtual-Clusters.svg | 0 .../10128-icon-service-Elastic-Job-Agents.png | Bin .../10128-icon-service-Elastic-Job-Agents.svg | 0 .../10130-icon-service-SQL-Database.png | Bin .../10130-icon-service-SQL-Database.svg | 0 ...rvice-Azure-Database-PostgreSQL-Server.png | Bin ...rvice-Azure-Database-PostgreSQL-Server.svg | 0 .../10132-icon-service-SQL-Server.png | Bin .../10132-icon-service-SQL-Server.svg | 0 ...vice-Azure-Database-Migration-Services.png | Bin ...vice-Azure-Database-Migration-Services.svg | 0 .../10134-icon-service-SQL-Elastic-Pools.png | Bin .../10134-icon-service-SQL-Elastic-Pools.svg | 0 .../10135-icon-service-Managed-Database.png | Bin .../10135-icon-service-Managed-Database.svg | 0 ...0136-icon-service-SQL-Managed-Instance.png | Bin ...0136-icon-service-SQL-Managed-Instance.svg | 0 ...ice-Azure-SQL-Server-Stretch-Databases.png | Bin ...ice-Azure-SQL-Server-Stretch-Databases.svg | 0 .../10137-icon-service-Cache-Redis.png | Bin .../10137-icon-service-Cache-Redis.svg | 0 .../10139-icon-service-Instance-Pools.png | Bin .../10139-icon-service-Instance-Pools.svg | 0 ...n-service-Azure-Data-Explorer-Clusters.png | Bin ...n-service-Azure-Data-Explorer-Clusters.svg | 0 ...351-icon-service-SQL-Server-Registries.png | Bin ...351-icon-service-SQL-Server-Registries.svg | 0 ...3805167-icon-service-Azure-App-Testing.png | Bin ...3805167-icon-service-Azure-App-Testing.svg | 0 ...0012-icon-service-Application-Insights.png | Bin ...0012-icon-service-Application-Insights.svg | 0 .../00563-icon-service-Change-Analysis.png | Bin .../00563-icon-service-Change-Analysis.svg | 0 .../devops/02373-icon-service-CloudTest.png | Bin .../devops/02373-icon-service-CloudTest.svg | 0 .../02423-icon-service-Load-Testing.png | Bin .../02423-icon-service-Load-Testing.svg | 0 .../02761-icon-service-Lab-Accounts.png | Bin .../02761-icon-service-Lab-Accounts.svg | 0 .../03339-icon-service-DevOps-Starter.png | Bin .../03339-icon-service-DevOps-Starter.svg | 0 ...3393-icon-service-Managed-DevOps-Pools.png | Bin ...3393-icon-service-Managed-DevOps-Pools.svg | 0 .../03455-icon-service-Code-Optimization.png | Bin .../03455-icon-service-Code-Optimization.svg | 0 .../03623-icon-service-Workspace-Gateway.png | Bin .../03623-icon-service-Workspace-Gateway.svg | 0 ...2-icon-service-API-Management-Services.png | Bin ...2-icon-service-API-Management-Services.svg | 0 .../10048-icon-service-API-Connections.png | Bin .../10048-icon-service-API-Connections.svg | 0 .../10261-icon-service-Azure-DevOps.png | Bin .../10261-icon-service-Azure-DevOps.svg | 0 .../10264-icon-service-DevTest-Labs.png | Bin .../10264-icon-service-DevTest-Labs.svg | 0 .../10265-icon-service-Lab-Services.png | Bin .../10265-icon-service-Lab-Services.svg | 0 ...on-service-Cost-Management-and-Billing.png | Bin ...on-service-Cost-Management-and-Billing.svg | 0 .../00456-icon-service-Preview-Features.png | Bin .../00456-icon-service-Preview-Features.svg | 0 .../10001-icon-service-All-Resources.png | Bin .../10001-icon-service-All-Resources.svg | 0 .../10002-icon-service-Subscriptions.png | Bin .../10002-icon-service-Subscriptions.svg | 0 .../10003-icon-service-Reservations.png | Bin .../10003-icon-service-Reservations.svg | 0 .../10004-icon-service-Service-Health.png | Bin .../10004-icon-service-Service-Health.svg | 0 .../10005-icon-service-Information.png | Bin .../10005-icon-service-Information.svg | 0 .../general/10006-icon-service-Recent.png | Bin .../general/10006-icon-service-Recent.svg | 0 .../10007-icon-service-Resource-Groups.png | Bin .../10007-icon-service-Resource-Groups.svg | 0 .../10008-icon-service-Marketplace.png | Bin .../10008-icon-service-Marketplace.svg | 0 .../general/10009-icon-service-Templates.png | Bin .../general/10009-icon-service-Templates.svg | 0 .../10010-icon-service-Quickstart-Center.png | Bin .../10010-icon-service-Quickstart-Center.svg | 0 .../10011-icon-service-Management-Groups.png | Bin .../10011-icon-service-Management-Groups.svg | 0 .../10013-icon-service-Help-and-Support.png | Bin .../10013-icon-service-Help-and-Support.svg | 0 .../azure/general/10014-icon-service-Tag.png | Bin .../azure/general/10014-icon-service-Tag.svg | 0 .../general/10015-icon-service-Dashboard.png | Bin .../general/10015-icon-service-Dashboard.svg | 0 .../10016-icon-service-Free-Services.png | Bin .../10016-icon-service-Free-Services.svg | 0 .../10019-icon-service-Cost-Management.png | Bin .../10019-icon-service-Cost-Management.svg | 0 ...12-icon-service-Marketplace-Management.png | Bin ...12-icon-service-Marketplace-Management.svg | 0 .../10116-icon-service-Region-Management.png | Bin .../10116-icon-service-Region-Management.svg | 0 .../10341-icon-service-Troubleshoot.png | Bin .../10341-icon-service-Troubleshoot.svg | 0 .../10349-icon-service-Resource-Explorer.png | Bin .../10349-icon-service-Resource-Explorer.svg | 0 .../general/10779-icon-service-Biz-Talk.png | Bin .../general/10779-icon-service-Biz-Talk.svg | 0 .../general/10780-icon-service-Blob-Block.png | Bin .../general/10780-icon-service-Blob-Block.svg | 0 .../general/10781-icon-service-Blob-Page.png | Bin .../general/10781-icon-service-Blob-Page.svg | 0 .../general/10782-icon-service-Branch.png | Bin .../general/10782-icon-service-Branch.svg | 0 .../general/10783-icon-service-Browser.png | Bin .../general/10783-icon-service-Browser.svg | 0 .../azure/general/10784-icon-service-Bug.png | Bin .../azure/general/10784-icon-service-Bug.svg | 0 .../general/10785-icon-service-Builds.png | Bin .../general/10785-icon-service-Builds.svg | 0 .../general/10786-icon-service-Cache.png | Bin .../general/10786-icon-service-Cache.svg | 0 .../azure/general/10787-icon-service-Code.png | Bin .../azure/general/10787-icon-service-Code.svg | 0 .../general/10788-icon-service-Commit.png | Bin .../general/10788-icon-service-Commit.svg | 0 .../general/10789-icon-service-Controls.png | Bin .../general/10789-icon-service-Controls.svg | 0 ...10790-icon-service-Controls-Horizontal.png | Bin ...10790-icon-service-Controls-Horizontal.svg | 0 .../10791-icon-service-Cost-Alerts.png | Bin .../10791-icon-service-Cost-Alerts.svg | 0 .../10792-icon-service-Cost-Analysis.png | Bin .../10792-icon-service-Cost-Analysis.svg | 0 .../10793-icon-service-Cost-Budgets.png | Bin .../10793-icon-service-Cost-Budgets.svg | 0 .../general/10794-icon-service-Counter.png | Bin .../general/10794-icon-service-Counter.svg | 0 .../general/10795-icon-service-Cubes.png | Bin .../general/10795-icon-service-Cubes.svg | 0 .../10796-icon-service-Dev-Console.png | Bin .../10796-icon-service-Dev-Console.svg | 0 .../general/10797-icon-service-Download.png | Bin .../general/10797-icon-service-Download.svg | 0 .../general/10798-icon-service-Error.png | Bin .../general/10798-icon-service-Error.svg | 0 .../general/10799-icon-service-Extensions.png | Bin .../general/10799-icon-service-Extensions.svg | 0 .../azure/general/10800-icon-service-File.png | Bin .../azure/general/10800-icon-service-File.svg | 0 .../general/10801-icon-service-Files.png | Bin .../general/10801-icon-service-Files.svg | 0 .../10802-icon-service-Folder-Blank.png | Bin .../10802-icon-service-Folder-Blank.svg | 0 .../10803-icon-service-Folder-Website.png | Bin .../10803-icon-service-Folder-Website.svg | 0 .../azure/general/10804-icon-service-FTP.png | Bin .../azure/general/10804-icon-service-FTP.svg | 0 .../azure/general/10805-icon-service-Gear.png | Bin .../azure/general/10805-icon-service-Gear.svg | 0 .../10807-icon-service-Globe-Error.png | Bin .../10807-icon-service-Globe-Error.svg | 0 .../10808-icon-service-Globe-Success.png | Bin .../10808-icon-service-Globe-Success.svg | 0 .../10809-icon-service-Globe-Warning.png | Bin .../10809-icon-service-Globe-Warning.svg | 0 .../general/10810-icon-service-Guide.png | Bin .../general/10810-icon-service-Guide.svg | 0 .../general/10811-icon-service-Heart.png | Bin .../general/10811-icon-service-Heart.svg | 0 .../general/10812-icon-service-Image.png | Bin .../general/10812-icon-service-Image.svg | 0 .../10813-icon-service-Input-Output.png | Bin .../10813-icon-service-Input-Output.svg | 0 .../10814-icon-service-Journey-Hub.png | Bin .../10814-icon-service-Journey-Hub.svg | 0 .../10815-icon-service-Launch-Portal.png | Bin .../10815-icon-service-Launch-Portal.svg | 0 .../general/10816-icon-service-Learn.png | Bin .../general/10816-icon-service-Learn.svg | 0 .../general/10817-icon-service-Load-Test.png | Bin .../general/10817-icon-service-Load-Test.svg | 0 .../general/10818-icon-service-Location.png | Bin .../general/10818-icon-service-Location.svg | 0 .../10819-icon-service-Log-Streaming.png | Bin .../10819-icon-service-Log-Streaming.svg | 0 .../10820-icon-service-Management-Portal.png | Bin .../10820-icon-service-Management-Portal.svg | 0 .../general/10821-icon-service-Media-File.png | Bin .../general/10821-icon-service-Media-File.svg | 0 .../general/10822-icon-service-Mobile.png | Bin .../general/10822-icon-service-Mobile.svg | 0 .../10823-icon-service-Mobile-Engagement.png | Bin .../10823-icon-service-Mobile-Engagement.svg | 0 .../general/10824-icon-service-Power.png | Bin .../general/10824-icon-service-Power.svg | 0 .../general/10825-icon-service-Powershell.png | Bin .../general/10825-icon-service-Powershell.svg | 0 .../general/10826-icon-service-Power-Up.png | Bin .../general/10826-icon-service-Power-Up.svg | 0 .../10827-icon-service-Feature-Previews.png | Bin .../10827-icon-service-Feature-Previews.svg | 0 .../10828-icon-service-Process-Explorer.png | Bin .../10828-icon-service-Process-Explorer.svg | 0 ...icon-service-Production-Ready-Database.png | Bin ...icon-service-Production-Ready-Database.svg | 0 ...10830-icon-service-Resource-Group-List.png | Bin ...10830-icon-service-Resource-Group-List.svg | 0 .../10831-icon-service-Resource-Linked.png | Bin .../10831-icon-service-Resource-Linked.svg | 0 .../general/10833-icon-service-Scheduler.png | Bin .../general/10833-icon-service-Scheduler.svg | 0 .../general/10834-icon-service-Search.png | Bin .../general/10834-icon-service-Search.svg | 0 .../10835-icon-service-Server-Farm.png | Bin .../10835-icon-service-Server-Farm.svg | 0 .../azure/general/10837-icon-service-SSD.png | Bin .../azure/general/10837-icon-service-SSD.svg | 0 ...10838-icon-service-Storage-Azure-Files.png | Bin ...10838-icon-service-Storage-Azure-Files.svg | 0 .../10839-icon-service-Storage-Container.png | Bin .../10839-icon-service-Storage-Container.svg | 0 .../10840-icon-service-Storage-Queue.png | Bin .../10840-icon-service-Storage-Queue.svg | 0 .../general/10841-icon-service-Table.png | Bin .../general/10841-icon-service-Table.svg | 0 .../azure/general/10842-icon-service-Tags.png | Bin .../azure/general/10842-icon-service-Tags.svg | 0 .../10843-icon-service-TFS-VC-Repository.png | Bin .../10843-icon-service-TFS-VC-Repository.svg | 0 .../general/10844-icon-service-Toolbox.png | Bin .../general/10844-icon-service-Toolbox.svg | 0 .../general/10845-icon-service-Versions.png | Bin .../general/10845-icon-service-Versions.svg | 0 .../10847-icon-service-Website-Power.png | Bin .../10847-icon-service-Website-Power.svg | 0 .../10848-icon-service-Website-Staging.png | Bin .../10848-icon-service-Website-Staging.svg | 0 .../general/10849-icon-service-Web-Slots.png | Bin .../general/10849-icon-service-Web-Slots.svg | 0 .../general/10850-icon-service-Web-Test.png | Bin .../general/10850-icon-service-Web-Test.svg | 0 .../general/10851-icon-service-Workbooks.png | Bin .../general/10851-icon-service-Workbooks.svg | 0 .../general/10852-icon-service-Workflow.png | Bin .../general/10852-icon-service-Workflow.svg | 0 .../general/10853-icon-service-Backlog.png | Bin .../general/10853-icon-service-Backlog.svg | 0 .../general/10854-icon-service-Media.png | Bin .../general/10854-icon-service-Media.svg | 0 .../general/10855-icon-service-Module.png | Bin .../general/10855-icon-service-Module.svg | 0 .../10856-icon-service-Search-Grid.png | Bin .../10856-icon-service-Search-Grid.svg | 0 ...19-icon-service-Workload-Orchestration.png | Bin ...19-icon-service-Workload-Orchestration.svg | 0 ...48-icon-service-Azure-Operator-5G-Core.png | Bin ...48-icon-service-Azure-Operator-5G-Core.svg | 0 ...3324-icon-service-Azure-Operator-Nexus.png | Bin ...3324-icon-service-Azure-Operator-Nexus.svg | 0 ...3-icon-service-Azure-Operator-Insights.png | Bin ...3-icon-service-Azure-Operator-Insights.svg | 0 ...service-Azure-Operator-Service-Manager.png | Bin ...service-Azure-Operator-Service-Manager.svg | 0 ...ervice-Azure-Programmable-Connectivity.png | Bin ...ervice-Azure-Programmable-Connectivity.svg | 0 .../033745116-icon-service-Azure-Local.png | Bin .../033745116-icon-service-Azure-Local.svg | 0 ...-icon-service-Edge-Storage-Accelerator.png | Bin ...-icon-service-Edge-Storage-Accelerator.svg | 0 ...85-icon-service-Azure-Monitor-Pipeline.png | Bin ...85-icon-service-Azure-Monitor-Pipeline.svg | 0 .../03687-icon-service-AI-at-Edge.png | Bin .../03687-icon-service-AI-at-Edge.svg | 0 .../identity/00321-icon-service-Security.png | Bin .../identity/00321-icon-service-Security.svg | 0 ...0919-icon-service-Administrative-Units.png | Bin ...0919-icon-service-Administrative-Units.svg | 0 ...84-icon-service-Verifiable-Credentials.png | Bin ...84-icon-service-Verifiable-Credentials.svg | 0 ...Azure-AD-Privleged-Identity-Management.png | Bin ...Azure-AD-Privleged-Identity-Management.svg | 0 ...ce-Entra-Privleged-Identity-Management.png | Bin ...ce-Entra-Privleged-Identity-Management.svg | 0 .../identity/02386-icon-service-API-Proxy.png | Bin .../identity/02386-icon-service-API-Proxy.svg | 0 .../02679-icon-service-Tenant-Properties.png | Bin .../02679-icon-service-Tenant-Properties.svg | 0 ...680-icon-service-Custom-Azure-AD-Roles.png | Bin ...680-icon-service-Custom-Azure-AD-Roles.svg | 0 ...on-service-Entra-Identity-Custom-Roles.png | Bin ...on-service-Entra-Identity-Custom-Roles.svg | 0 .../02681-icon-service-AAD-Licenses.png | Bin .../02681-icon-service-AAD-Licenses.svg | 0 ...1-icon-service-Entra-Identity-Licenses.png | Bin ...1-icon-service-Entra-Identity-Licenses.svg | 0 .../02854-icon-service-Entra-Connect.png | Bin .../02854-icon-service-Entra-Connect.svg | 0 .../03143-icon-service-Entra-Verified-ID.png | Bin .../03143-icon-service-Entra-Verified-ID.svg | 0 ...icon-service-Verification-As-A-Service.png | Bin ...icon-service-Verification-As-A-Service.svg | 0 ...on-service-Multi-Factor-Authentication.png | Bin ...on-service-Multi-Factor-Authentication.svg | 0 ...con-service-Entra-Global-Secure-Access.png | Bin ...con-service-Entra-Global-Secure-Access.svg | 0 ...03338-icon-service-External-Identities.png | Bin ...03338-icon-service-External-Identities.svg | 0 ...3382-icon-service-Entra-Private-Access.png | Bin ...3382-icon-service-Entra-Private-Access.svg | 0 ...383-icon-service-Entra-Internet-Access.png | Bin ...383-icon-service-Entra-Internet-Access.svg | 0 .../03425-icon-service-external-id.png | Bin .../03425-icon-service-external-id.svg | 0 ...4251-icon-service-external-id-modified.png | Bin ...4251-icon-service-external-id-modified.svg | 0 .../03533-icon-service-Entra-Connect-Sync.png | Bin .../03533-icon-service-Entra-Connect-Sync.svg | 0 ...21-icon-service-Azure-Active-Directory.png | Bin ...21-icon-service-Azure-Active-Directory.svg | 0 ...-icon-service-Azure-AD-Domain-Services.png | Bin ...-icon-service-Azure-AD-Domain-Services.svg | 0 ...222-icon-service-Entra-Domain-Services.png | Bin ...222-icon-service-Entra-Domain-Services.svg | 0 .../identity/10223-icon-service-Groups.png | Bin .../identity/10223-icon-service-Groups.svg | 0 ...ervice-Active-Directory-Connect-Health.png | Bin ...ervice-Active-Directory-Connect-Health.svg | 0 ...0224-icon-service-Entra-Connect-Health.png | Bin ...0224-icon-service-Entra-Connect-Health.svg | 0 ...5-icon-service-Enterprise-Applications.png | Bin ...5-icon-service-Enterprise-Applications.svg | 0 ...-icon-service-Entra-Managed-Identities.png | Bin ...-icon-service-Entra-Managed-Identities.svg | 0 .../10227-icon-service-Managed-Identities.png | Bin .../10227-icon-service-Managed-Identities.svg | 0 .../10228-icon-service-Azure-AD-B2C.png | Bin .../10228-icon-service-Azure-AD-B2C.svg | 0 ...n-service-Azure-Information-Protection.png | Bin ...n-service-Azure-Information-Protection.svg | 0 .../identity/10230-icon-service-Users.png | Bin .../identity/10230-icon-service-Users.svg | 0 ...n-service-Azure-AD-Identity-Protection.png | Bin ...n-service-Azure-AD-Identity-Protection.svg | 0 ...10231-icon-service-Entra-ID-Protection.png | Bin ...10231-icon-service-Entra-ID-Protection.svg | 0 .../10232-icon-service-App-Registrations.png | Bin .../10232-icon-service-App-Registrations.svg | 0 ...Azure-AD-Privilege-Identity-Management.png | Bin ...Azure-AD-Privilege-Identity-Management.svg | 0 ...10235-icon-service-Identity-Governance.png | Bin ...10235-icon-service-Identity-Governance.svg | 0 ...vice-Azure-AD-Roles-and-Administrators.png | Bin ...vice-Azure-AD-Roles-and-Administrators.svg | 0 ...ntra-Identity-Roles-and-Administrators.png | Bin ...ntra-Identity-Roles-and-Administrators.svg | 0 .../10433-icon-service-User-Settings.png | Bin .../10433-icon-service-User-Settings.svg | 0 ...00036-icon-service-SQL-Data-Warehouses.png | Bin ...00036-icon-service-SQL-Data-Warehouses.svg | 0 ...rvice-Integration-Service-Environments.png | Bin ...rvice-Integration-Service-Environments.svg | 0 ...691-icon-service-Azure-Databox-Gateway.png | Bin ...691-icon-service-Azure-Databox-Gateway.svg | 0 .../02072-icon-service-Partner-Topic.png | Bin .../02072-icon-service-Partner-Topic.svg | 0 .../02073-icon-service-System-Topic.png | Bin .../02073-icon-service-System-Topic.svg | 0 ...2265-icon-service-Partner-Registration.png | Bin ...2265-icon-service-Partner-Registration.svg | 0 .../02266-icon-service-Partner-Namespace.png | Bin .../02266-icon-service-Partner-Namespace.svg | 0 .../02631-icon-service-Logic-Apps.png | Bin .../02631-icon-service-Logic-Apps.svg | 0 .../integration/02987-icon-service-pubsub.png | Bin .../integration/02987-icon-service-pubsub.svg | 0 ...30874-icon-service-Logic-Apps-Template.png | Bin ...30874-icon-service-Logic-Apps-Template.svg | 0 .../03335-icon-service-Power-Platform.png | Bin .../03335-icon-service-Power-Platform.svg | 0 ...-icon-service-Integration-Environments.png | Bin ...-icon-service-Integration-Environments.svg | 0 ...icon-service-Business-Process-Tracking.png | Bin ...icon-service-Business-Process-Tracking.svg | 0 ...2-icon-service-API-Management-Services.png | Bin ...2-icon-service-API-Management-Services.svg | 0 .../10048-icon-service-API-Connections.png | Bin .../10048-icon-service-API-Connections.svg | 0 ...con-service-StorSimple-Device-Managers.png | Bin ...con-service-StorSimple-Device-Managers.svg | 0 .../10095-icon-service-Azure-Stack-Edge.png | Bin .../10095-icon-service-Azure-Stack-Edge.svg | 0 .../10126-icon-service-Data-Factories.png | Bin .../10126-icon-service-Data-Factories.svg | 0 ...ice-Azure-SQL-Server-Stretch-Databases.png | Bin ...ice-Azure-SQL-Server-Stretch-Databases.svg | 0 .../10206-icon-service-Event-Grid-Topics.png | Bin .../10206-icon-service-Event-Grid-Topics.svg | 0 .../integration/10209-icon-service-Relays.png | Bin .../integration/10209-icon-service-Relays.svg | 0 .../10212-icon-service-Azure-API-for-FHIR.png | Bin .../10212-icon-service-Azure-API-for-FHIR.svg | 0 ...213-icon-service-Software-as-a-Service.png | Bin ...213-icon-service-Software-as-a-Service.svg | 0 .../10215-icon-service-Event-Grid-Domains.png | Bin .../10215-icon-service-Event-Grid-Domains.svg | 0 .../10216-icon-service-Azure-Data-Catalog.png | Bin .../10216-icon-service-Azure-Data-Catalog.svg | 0 ...0218-icon-service-Integration-Accounts.png | Bin ...0218-icon-service-Integration-Accounts.svg | 0 .../10219-icon-service-App-Configuration.png | Bin .../10219-icon-service-App-Configuration.svg | 0 .../10220-icon-service-SendGrid-Accounts.png | Bin .../10220-icon-service-SendGrid-Accounts.svg | 0 ...-icon-service-Event-Grid-Subscriptions.png | Bin ...-icon-service-Event-Grid-Subscriptions.svg | 0 ...on-service-Logic-Apps-Custom-Connector.png | Bin ...on-service-Logic-Apps-Custom-Connector.svg | 0 .../10836-icon-service-Azure-Service-Bus.png | Bin .../10836-icon-service-Azure-Service-Bus.svg | 0 ...399-icon-service-Device-Security-Apple.png | Bin ...399-icon-service-Device-Security-Apple.svg | 0 ...99-icon-service-Device-Security-Google.png | Bin ...99-icon-service-Device-Security-Google.svg | 0 ...9-icon-service-Device-Security-Windows.png | Bin ...9-icon-service-Device-Security-Windows.svg | 0 ...391-icon-service-Engage-Center-Connect.png | Bin ...391-icon-service-Engage-Center-Connect.svg | 0 .../intune/10329-icon-service-Intune.png | Bin .../intune/10329-icon-service-Intune.svg | 0 .../intune/10330-icon-service-eBooks.png | Bin .../intune/10330-icon-service-eBooks.svg | 0 .../intune/10331-icon-service-Client-Apps.png | Bin .../intune/10331-icon-service-Client-Apps.svg | 0 .../intune/10332-icon-service-Devices.png | Bin .../intune/10332-icon-service-Devices.svg | 0 .../10333-icon-service-Device-Compliance.png | Bin .../10333-icon-service-Device-Compliance.svg | 0 .../10335-icon-service-Software-Updates.png | Bin .../10335-icon-service-Software-Updates.svg | 0 .../10336-icon-service-Security-Baselines.png | Bin .../10336-icon-service-Security-Baselines.svg | 0 .../10337-icon-service-Device-Enrollment.png | Bin .../10337-icon-service-Device-Enrollment.svg | 0 ...0338-icon-service-Device-Configuration.png | Bin ...0338-icon-service-Device-Configuration.svg | 0 .../10339-icon-service-Exchange-Access.png | Bin .../10339-icon-service-Exchange-Access.svg | 0 ...vice-Azure-AD-Roles-and-Administrators.png | Bin ...vice-Azure-AD-Roles-and-Administrators.svg | 0 ...ntra-Identity-Roles-and-Administrators.png | Bin ...ntra-Identity-Roles-and-Administrators.svg | 0 .../10342-icon-service-Tenant-Status.png | Bin .../10342-icon-service-Tenant-Status.svg | 0 ...0343-icon-service-Intune-For-Education.png | Bin ...0343-icon-service-Intune-For-Education.svg | 0 ...344-icon-service-Intune-App-Protection.png | Bin ...344-icon-service-Intune-App-Protection.svg | 0 .../intune/10350-icon-service-Mindaro.png | Bin .../intune/10350-icon-service-Mindaro.svg | 0 ...Learning-Studio-(Classic)-Web-Services.png | Bin ...Learning-Studio-(Classic)-Web-Services.svg | 0 .../iot/00039-icon-service-Event-Hubs.png | Bin .../iot/00039-icon-service-Event-Hubs.svg | 0 ...042-icon-service-Stream-Analytics-Jobs.png | Bin ...042-icon-service-Stream-Analytics-Jobs.svg | 0 ...691-icon-service-Azure-Databox-Gateway.png | Bin ...691-icon-service-Azure-Databox-Gateway.svg | 0 .../iot/01030-icon-service-Digital-Twins.png | Bin .../iot/01030-icon-service-Digital-Twins.svg | 0 .../iot/02359-icon-service-Industrial-IoT.png | Bin .../iot/02359-icon-service-Industrial-IoT.svg | 0 .../iot/02631-icon-service-Logic-Apps.png | Bin .../iot/02631-icon-service-Logic-Apps.svg | 0 ...293-icon-service-Azure-Stack-HCI-Sizer.png | Bin ...293-icon-service-Azure-Stack-HCI-Sizer.svg | 0 .../03466-icon-service-Stack-HCI-Premium.png | Bin .../03466-icon-service-Stack-HCI-Premium.svg | 0 ...3485-icon-service-Azure-IoT-Operations.png | Bin ...3485-icon-service-Azure-IoT-Operations.svg | 0 ...6-icon-service-Hybrid-Connectivity-Hub.png | Bin ...6-icon-service-Hybrid-Connectivity-Hub.svg | 0 .../iot/10029-icon-service-Function-Apps.png | Bin .../iot/10029-icon-service-Function-Apps.svg | 0 .../10045-icon-service-Notification-Hubs.png | Bin .../10045-icon-service-Notification-Hubs.svg | 0 ...on-service-Notification-Hub-Namespaces.png | Bin ...on-service-Notification-Hub-Namespaces.svg | 0 .../iot/10114-icon-service-Azure-Stack.png | Bin .../iot/10114-icon-service-Azure-Stack.svg | 0 .../10121-icon-service-Azure-Cosmos-DB.png | Bin .../10121-icon-service-Azure-Cosmos-DB.svg | 0 .../10149-icon-service-Event-Hub-Clusters.png | Bin .../10149-icon-service-Event-Hub-Clusters.svg | 0 ...ice-Machine-Learning-Studio-Workspaces.png | Bin ...ice-Machine-Learning-Studio-Workspaces.svg | 0 ...hine-Learning-Studio-Web-Service-Plans.png | Bin ...hine-Learning-Studio-Web-Service-Plans.svg | 0 ...vice-Time-Series-Insights-Environments.png | Bin ...vice-Time-Series-Insights-Environments.svg | 0 .../azure/iot/10182-icon-service-IoT-Hub.png | Bin .../azure/iot/10182-icon-service-IoT-Hub.svg | 0 ...-icon-service-IoT-Central-Applications.png | Bin ...-icon-service-IoT-Central-Applications.svg | 0 ...10185-icon-service-Azure-Maps-Accounts.png | Bin ...10185-icon-service-Azure-Maps-Accounts.svg | 0 .../azure/iot/10186-icon-service-IoT-Edge.png | Bin .../azure/iot/10186-icon-service-IoT-Edge.svg | 0 ...ice-Time-Series-Insights-Event-Sources.png | Bin ...ice-Time-Series-Insights-Event-Sources.svg | 0 ...e-Time-Series-Insights-Access-Policies.png | Bin ...e-Time-Series-Insights-Access-Policies.svg | 0 ...198-icon-service-Time-Series-Data-Sets.png | Bin ...198-icon-service-Time-Series-Data-Sets.svg | 0 ...3-icon-service-Windows10-Core-Services.png | Bin ...3-icon-service-Windows10-Core-Services.svg | 0 ...-icon-service-Event-Grid-Subscriptions.png | Bin ...-icon-service-Event-Grid-Subscriptions.svg | 0 ...n-service-Device-Provisioning-Services.png | Bin ...n-service-Device-Provisioning-Services.svg | 0 .../00001-icon-service-Monitor.png | Bin .../00001-icon-service-Monitor.svg | 0 .../00002-icon-service-Alerts.png | Bin .../00002-icon-service-Alerts.svg | 0 .../00003-icon-service-Advisor.png | Bin .../00003-icon-service-Advisor.svg | 0 ...on-service-Cost-Management-and-Billing.png | Bin ...on-service-Cost-Management-and-Billing.svg | 0 .../00006-icon-service-Blueprints.png | Bin .../00006-icon-service-Blueprints.svg | 0 .../00007-icon-service-Activity-Log.png | Bin .../00007-icon-service-Activity-Log.svg | 0 ...0008-icon-service-Diagnostics-Settings.png | Bin ...0008-icon-service-Diagnostics-Settings.svg | 0 ...-icon-service-Log-Analytics-Workspaces.png | Bin ...-icon-service-Log-Analytics-Workspaces.svg | 0 ...icon-service-Scheduler-Job-Collections.png | Bin ...icon-service-Scheduler-Job-Collections.svg | 0 .../00011-icon-service-Compliance.png | Bin .../00011-icon-service-Compliance.svg | 0 ...0012-icon-service-Application-Insights.png | Bin ...0012-icon-service-Application-Insights.svg | 0 .../00014-icon-service-My-Customers.png | Bin .../00014-icon-service-My-Customers.svg | 0 ...-icon-service-Recovery-Services-Vaults.png | Bin ...-icon-service-Recovery-Services-Vaults.svg | 0 .../00020-icon-service-Metrics.png | Bin .../00020-icon-service-Metrics.svg | 0 .../00021-icon-service-Solutions.png | Bin .../00021-icon-service-Solutions.svg | 0 ...00022-icon-service-Automation-Accounts.png | Bin ...00022-icon-service-Automation-Accounts.svg | 0 ...4-icon-service-Operation-Log-(Classic).png | Bin ...4-icon-service-Operation-Log-(Classic).svg | 0 .../00025-icon-service-Service-Providers.png | Bin .../00025-icon-service-Service-Providers.svg | 0 .../00026-icon-service-Education.png | Bin .../00026-icon-service-Education.svg | 0 ...00027-icon-service-Service-Catalog-MAD.png | Bin ...00027-icon-service-Service-Catalog-MAD.svg | 0 .../00408-icon-service-Intune-Trends.png | Bin .../00408-icon-service-Intune-Trends.svg | 0 .../00471-icon-service-Azure-Lighthouse.png | Bin .../00471-icon-service-Azure-Lighthouse.svg | 0 .../00571-icon-service-Universal-Print.png | Bin .../00571-icon-service-Universal-Print.svg | 0 .../00756-icon-service-Azure-Arc.png | Bin .../00756-icon-service-Azure-Arc.svg | 0 .../01710-icon-service-Arc-Machines.png | Bin .../01710-icon-service-Arc-Machines.svg | 0 ...on-service-Service-Group-Relationships.png | Bin ...on-service-Service-Group-Relationships.svg | 0 .../03366-icon-service-Resources-Provider.png | Bin .../03366-icon-service-Resources-Provider.svg | 0 .../10303-icon-service-User-Privacy.png | Bin .../10303-icon-service-User-Privacy.svg | 0 .../10311-icon-service-Managed-Desktop.png | Bin .../10311-icon-service-Managed-Desktop.svg | 0 ...on-service-Managed-Applications-Center.png | Bin ...on-service-Managed-Applications-Center.svg | 0 ...e-Customer-Lockbox-for-Microsoft-Azure.png | Bin ...e-Customer-Lockbox-for-Microsoft-Azure.svg | 0 .../10316-icon-service-Policy.png | Bin .../10316-icon-service-Policy.svg | 0 ...8-icon-service-Resource-Graph-Explorer.png | Bin ...8-icon-service-Resource-Graph-Explorer.svg | 0 .../10450-icon-service-MachinesAzureArc.png | Bin .../10450-icon-service-MachinesAzureArc.svg | 0 .../azure/menu/00787-icon-service-Keys.png | Bin .../azure/menu/00787-icon-service-Keys.svg | 0 ...on-service-Cost-Management-and-Billing.png | Bin ...on-service-Cost-Management-and-Billing.svg | 0 ...-icon-service-Recovery-Services-Vaults.png | Bin ...-icon-service-Recovery-Services-Vaults.svg | 0 ...691-icon-service-Azure-Databox-Gateway.png | Bin ...691-icon-service-Azure-Databox-Gateway.svg | 0 .../migrate/10094-icon-service-Data-Box.png | Bin .../migrate/10094-icon-service-Data-Box.svg | 0 .../10095-icon-service-Azure-Stack-Edge.png | Bin .../10095-icon-service-Azure-Stack-Edge.svg | 0 .../10281-icon-service-Azure-Migrate.png | Bin .../10281-icon-service-Azure-Migrate.svg | 0 ...vice-Azure-Database-Migration-Services.png | Bin ...vice-Azure-Database-Migration-Services.svg | 0 .../00698-icon-service-Remote-Rendering.png | Bin .../00698-icon-service-Remote-Rendering.svg | 0 ...2-icon-service-Spatial-Anchor-Accounts.png | Bin ...2-icon-service-Spatial-Anchor-Accounts.svg | 0 .../03335-icon-service-Power-Platform.png | Bin .../03335-icon-service-Power-Platform.svg | 0 .../10035-icon-service-App-Services.png | Bin .../10035-icon-service-App-Services.svg | 0 .../10045-icon-service-Notification-Hubs.png | Bin .../10045-icon-service-Notification-Hubs.svg | 0 .../monitor/00001-icon-service-Monitor.png | Bin .../monitor/00001-icon-service-Monitor.svg | 0 .../00007-icon-service-Activity-Log.png | Bin .../00007-icon-service-Activity-Log.svg | 0 ...0008-icon-service-Diagnostics-Settings.png | Bin ...0008-icon-service-Diagnostics-Settings.svg | 0 ...-icon-service-Log-Analytics-Workspaces.png | Bin ...-icon-service-Log-Analytics-Workspaces.svg | 0 ...0012-icon-service-Application-Insights.png | Bin ...0012-icon-service-Application-Insights.svg | 0 .../monitor/00020-icon-service-Metrics.png | Bin .../monitor/00020-icon-service-Metrics.svg | 0 ...rvice-Azure-Monitors-for-SAP-Solutions.png | Bin ...rvice-Azure-Monitors-for-SAP-Solutions.svg | 0 .../00563-icon-service-Change-Analysis.png | Bin .../00563-icon-service-Change-Analysis.svg | 0 .../02189-icon-service-Azure-Workbooks.png | Bin .../02189-icon-service-Azure-Workbooks.svg | 0 .../10066-icon-service-Network-Watcher.png | Bin .../10066-icon-service-Network-Watcher.svg | 0 .../monitor/10832-icon-service-Auto-Scale.png | Bin .../monitor/10832-icon-service-Auto-Scale.svg | 0 .../00056-icon-service-CDN-Profiles.png | Bin .../00056-icon-service-CDN-Profiles.svg | 0 ...71-icon-service-Azure-Firewall-Manager.png | Bin ...71-icon-service-Azure-Firewall-Manager.svg | 0 ...272-icon-service-Azure-Firewall-Policy.png | Bin ...272-icon-service-Azure-Firewall-Policy.svg | 0 .../00427-icon-service-Private-Link.png | Bin .../00427-icon-service-Private-Link.svg | 0 .../00701-icon-service-IP-Groups.png | Bin .../00701-icon-service-IP-Groups.svg | 0 .../00860-icon-service-Virtual-WAN-Hub.png | Bin .../00860-icon-service-Virtual-WAN-Hub.svg | 0 ...1105-icon-service-Private-Link-Service.png | Bin ...1105-icon-service-Private-Link-Service.svg | 0 ...rvice-Resource-Management-Private-Link.png | Bin ...rvice-Resource-Management-Private-Link.svg | 0 ...209-icon-service-Private-Link-Services.png | Bin ...209-icon-service-Private-Link-Services.svg | 0 .../02302-icon-service-Load-Balancer-Hub.png | Bin .../02302-icon-service-Load-Balancer-Hub.svg | 0 .../02422-icon-service-Bastions.png | Bin .../02422-icon-service-Bastions.svg | 0 .../02496-icon-service-Virtual-Router.png | Bin .../02496-icon-service-Virtual-Router.svg | 0 .../02509-icon-service-Connected-Cache.png | Bin .../02509-icon-service-Connected-Cache.svg | 0 .../02692-icon-service-Spot-VMSS.png | Bin .../02692-icon-service-Spot-VMSS.svg | 0 .../networking/02695-icon-service-Spot-VM.png | Bin .../networking/02695-icon-service-Spot-VM.svg | 0 .../networking/02742-icon-service-Subnet.png | Bin .../networking/02742-icon-service-Subnet.svg | 0 ...2882-icon-service-DNS-Private-Resolver.png | Bin ...2882-icon-service-DNS-Private-Resolver.svg | 0 ...9029174-icon-service-Load-Balancer-Hub.png | Bin ...9029174-icon-service-Load-Balancer-Hub.svg | 0 .../029273094-icon-service-Edge-Actions.png | Bin .../029273094-icon-service-Edge-Actions.svg | 0 ...n-service-Azure-Communications-Gateway.png | Bin ...n-service-Azure-Communications-Gateway.svg | 0 ...service-Application-Gateway-Containers.png | Bin ...service-Application-Gateway-Containers.svg | 0 ...03368-icon-service-DNS-Security-Policy.png | Bin ...03368-icon-service-DNS-Security-Policy.svg | 0 .../034412066-icon-service-VNet-Appliance.png | Bin .../034412066-icon-service-VNet-Appliance.svg | 0 .../03459-icon-service-DNS-Multistack.png | Bin .../03459-icon-service-DNS-Multistack.svg | 0 .../03460-icon-service-ATM-Multistack.png | Bin .../03460-icon-service-ATM-Multistack.svg | 0 .../03461-icon-service-IP-Address-manager.png | Bin .../03461-icon-service-IP-Address-manager.svg | 0 ...43-icon-service-Network-Foundation-Hub.png | Bin ...43-icon-service-Network-Foundation-Hub.svg | 0 ...3650-icon-service-Network-Security-Hub.png | Bin ...3650-icon-service-Network-Security-Hub.svg | 0 .../03694-icon-service-VPNClientWindows.png | Bin .../03694-icon-service-VPNClientWindows.svg | 0 .../10061-icon-service-Virtual-Networks.png | Bin .../10061-icon-service-Virtual-Networks.svg | 0 .../10062-icon-service-Load-Balancers.png | Bin .../10062-icon-service-Load-Balancers.svg | 0 ...-icon-service-Virtual-Network-Gateways.png | Bin ...-icon-service-Virtual-Network-Gateways.svg | 0 .../10064-icon-service-DNS-Zones.png | Bin .../10064-icon-service-DNS-Zones.svg | 0 ...-icon-service-Traffic-Manager-Profiles.png | Bin ...-icon-service-Traffic-Manager-Profiles.svg | 0 .../10066-icon-service-Network-Watcher.png | Bin .../10066-icon-service-Network-Watcher.svg | 0 ...7-icon-service-Network-Security-Groups.png | Bin ...7-icon-service-Network-Security-Groups.svg | 0 ...-service-Public-IP-Addresses-(Classic).png | Bin ...-service-Public-IP-Addresses-(Classic).svg | 0 ...10069-icon-service-Public-IP-Addresses.png | Bin ...10069-icon-service-Public-IP-Addresses.svg | 0 ...icon-service-On-Premises-Data-Gateways.png | Bin ...icon-service-On-Premises-Data-Gateways.svg | 0 .../10071-icon-service-Route-Filters.png | Bin .../10071-icon-service-Route-Filters.svg | 0 ...072-icon-service-DDoS-Protection-Plans.png | Bin ...072-icon-service-DDoS-Protection-Plans.svg | 0 ...on-service-Front-Door-and-CDN-Profiles.png | Bin ...on-service-Front-Door-and-CDN-Profiles.svg | 0 ...con-service-Virtual-Networks-(Classic).png | Bin ...con-service-Virtual-Networks-(Classic).svg | 0 ...0076-icon-service-Application-Gateways.png | Bin ...0076-icon-service-Application-Gateways.svg | 0 ...77-icon-service-Local-Network-Gateways.png | Bin ...77-icon-service-Local-Network-Gateways.svg | 0 ...079-icon-service-ExpressRoute-Circuits.png | Bin ...079-icon-service-ExpressRoute-Circuits.svg | 0 .../10080-icon-service-Network-Interfaces.png | Bin .../10080-icon-service-Network-Interfaces.svg | 0 .../10081-icon-service-Connections.png | Bin .../10081-icon-service-Connections.svg | 0 .../10082-icon-service-Route-Tables.png | Bin .../10082-icon-service-Route-Tables.svg | 0 .../10084-icon-service-Firewalls.png | Bin .../10084-icon-service-Firewalls.svg | 0 ...icon-service-Service-Endpoint-Policies.png | Bin ...icon-service-Service-Endpoint-Policies.svg | 0 .../networking/10310-icon-service-NAT.png | Bin .../networking/10310-icon-service-NAT.svg | 0 .../10353-icon-service-Virtual-WANs.png | Bin .../10353-icon-service-Virtual-WANs.svg | 0 ...Web-Application-Firewall-Policies(WAF).png | Bin ...Web-Application-Firewall-Policies(WAF).svg | 0 ...con-service-Proximity-Placement-Groups.png | Bin ...con-service-Proximity-Placement-Groups.svg | 0 ...ervice-Reserved-IP-Addresses-(Classic).png | Bin ...ervice-Reserved-IP-Addresses-(Classic).svg | 0 .../10372-icon-service-Public-IP-Prefixes.png | Bin .../10372-icon-service-Public-IP-Prefixes.svg | 0 ...con-service-Connected-Vehicle-Platform.png | Bin ...con-service-Connected-Vehicle-Platform.svg | 0 ...19-icon-service-Toolchain-Orchestrator.png | Bin ...19-icon-service-Toolchain-Orchestrator.svg | 0 .../029636048-icon-service-Landing-Zone.png | Bin .../029636048-icon-service-Landing-Zone.svg | 0 ...on-service-Service-Group-Relationships.png | Bin ...on-service-Service-Group-Relationships.svg | 0 ...193085-icon-service-Azure-Access-Point.png | Bin ...193085-icon-service-Azure-Access-Point.svg | 0 ...40592-icon-service-Azure-Data-Transfer.png | Bin ...40592-icon-service-Azure-Data-Transfer.svg | 0 .../03400-icon-service-Entra-Identity.png | Bin .../03400-icon-service-Entra-Identity.svg | 0 ...34296882-icon-service-Agentic-Web-Apps.png | Bin ...34296882-icon-service-Agentic-Web-Apps.svg | 0 .../034923704-icon-service-Resiliency.png | Bin .../034923704-icon-service-Resiliency.svg | 0 ...556610-icon-service-DDoS-Custom-Policy.png | Bin ...556610-icon-service-DDoS-Custom-Policy.svg | 0 ...1-icon-service-Disconnected-Operations.png | Bin ...1-icon-service-Disconnected-Operations.svg | 0 ...824068-icon-service-Azure-Data-Sharing.png | Bin ...824068-icon-service-Azure-Data-Sharing.svg | 0 .../037838045-icon-service-AI-Gateway.png | Bin .../037838045-icon-service-AI-Gateway.svg | 0 ...327-icon-service-Azure-Virtual-Desktop.png | Bin ...327-icon-service-Azure-Virtual-Desktop.svg | 0 .../other/00412-icon-service-SSH-Keys.png | Bin .../other/00412-icon-service-SSH-Keys.svg | 0 ...con-service-Internet-Analyzer-Profiles.png | Bin ...con-service-Internet-Analyzer-Profiles.svg | 0 .../00559-icon-service-Azure-Cloud-Shell.png | Bin .../00559-icon-service-Azure-Cloud-Shell.svg | 0 .../00712-icon-service-Video-Analyzers.png | Bin .../00712-icon-service-Video-Analyzers.svg | 0 ...00903-icon-service-ExpressRoute-Direct.png | Bin ...00903-icon-service-ExpressRoute-Direct.svg | 0 .../other/00913-icon-service-Cost-Export.png | Bin .../other/00913-icon-service-Cost-Export.svg | 0 ...n-service-Azure-Communication-Services.png | Bin ...n-service-Azure-Communication-Services.svg | 0 .../00970-icon-service-Peering-Service.png | Bin .../00970-icon-service-Peering-Service.svg | 0 ...ure-Network-Function-Manager-Functions.png | Bin ...ure-Network-Function-Manager-Functions.svg | 0 ...-icon-service-Log-Analytics-Query-Pack.png | Bin ...-icon-service-Log-Analytics-Query-Pack.svg | 0 .../01088-icon-service-Arc-Kubernetes.png | Bin .../01088-icon-service-Arc-Kubernetes.svg | 0 ...219-icon-service-Azure-VMware-Solution.png | Bin ...219-icon-service-Azure-VMware-Solution.svg | 0 .../other/01285-icon-service-Peerings.png | Bin .../other/01285-icon-service-Peerings.svg | 0 .../01757-icon-service-Dashboard-Hub.png | Bin .../01757-icon-service-Dashboard-Hub.svg | 0 ...01800-icon-service-Azure-Video-Indexer.png | Bin ...01800-icon-service-Azure-Video-Indexer.svg | 0 .../azure/other/01846-icon-service-AVS-VM.png | Bin .../azure/other/01846-icon-service-AVS-VM.svg | 0 .../01848-icon-service-Arc-PostgreSQL .png | Bin .../01848-icon-service-Arc-PostgreSQL .svg | 0 ...-icon-service-Arc-SQL-Managed-Instance.png | Bin ...-icon-service-Arc-SQL-Managed-Instance.svg | 0 .../01850-icon-service-Arc-SQL-Server.png | Bin .../01850-icon-service-Arc-SQL-Server.svg | 0 ...857-icon-service-Data-Collection-Rules.png | Bin ...857-icon-service-Data-Collection-Rules.svg | 0 .../02200-icon-service-Resource-Mover.png | Bin .../02200-icon-service-Resource-Mover.svg | 0 .../02223-icon-service-Azure-Chaos-Studio.png | Bin .../02223-icon-service-Azure-Chaos-Studio.svg | 0 .../02237-icon-service-Network-Managers.png | Bin .../02237-icon-service-Network-Managers.svg | 0 .../02322-icon-service-Dedicated-HSM.png | Bin .../02322-icon-service-Dedicated-HSM.svg | 0 ...02323-icon-service-Modular-Data-Center.png | Bin ...02323-icon-service-Modular-Data-Center.svg | 0 .../02340-icon-service-Template-Specs.png | Bin .../02340-icon-service-Template-Specs.svg | 0 .../02356-icon-service-Arc-Data-services.png | Bin .../02356-icon-service-Arc-Data-services.svg | 0 ...02360-icon-service-Azure-Backup-Center.png | Bin ...02360-icon-service-Azure-Backup-Center.svg | 0 .../other/02361-icon-service-Backup-Vault.png | Bin .../other/02361-icon-service-Backup-Vault.svg | 0 ...475-icon-service-Device-Update-IoT-Hub.png | Bin ...475-icon-service-Device-Update-IoT-Hub.svg | 0 .../azure/other/02486-icon-service-Fiji.png | Bin .../azure/other/02486-icon-service-Fiji.svg | 0 ...8-icon-service-Azure-Monitor-Dashboard.png | Bin ...8-icon-service-Azure-Monitor-Dashboard.svg | 0 ...-icon-service-SCVMM-Management-Servers.png | Bin ...-icon-service-SCVMM-Management-Servers.svg | 0 ...vice-Cloud-Services-(extended-support).png | Bin ...vice-Cloud-Services-(extended-support).svg | 0 ...icon-service-Azure-Support-Center-Blue.png | Bin ...icon-service-Azure-Support-Center-Blue.svg | 0 .../02515-icon-service-Web-App-+-Database.png | Bin .../02515-icon-service-Web-App-+-Database.svg | 0 ...518-icon-service-Azure-HPC-Workbenches.png | Bin ...518-icon-service-Azure-HPC-Workbenches.svg | 0 .../other/02560-icon-service-Disk-Pool.png | Bin .../other/02560-icon-service-Disk-Pool.svg | 0 ...icon-service-Bare-Metal-Infrastructure.png | Bin ...icon-service-Bare-Metal-Infrastructure.svg | 0 .../02579-icon-service-Private-Endpoints.png | Bin .../02579-icon-service-Private-Endpoints.svg | 0 ...con-service-Open-Supply-Chain-Platform.png | Bin ...con-service-Open-Supply-Chain-Platform.svg | 0 .../azure/other/02625-icon-service-Aquila.png | Bin .../azure/other/02625-icon-service-Aquila.svg | 0 .../02638-icon-service-Reserved-Capacity.png | Bin .../02638-icon-service-Reserved-Capacity.svg | 0 .../02654-icon-service-Custom-IP-Prefix.png | Bin .../02654-icon-service-Custom-IP-Prefix.svg | 0 .../other/02658-icon-service-FHIR-Service.png | Bin .../other/02658-icon-service-FHIR-Service.svg | 0 .../02660-icon-service-MedTech-Service.png | Bin .../02660-icon-service-MedTech-Service.svg | 0 ...vice-Managed-Instance-Apache-Cassandra.png | Bin ...vice-Managed-Instance-Apache-Cassandra.svg | 0 ...2668-icon-service-Confidential-Ledgers.png | Bin ...2668-icon-service-Confidential-Ledgers.svg | 0 .../other/02681-icon-service-AAD-Licenses.png | Bin .../other/02681-icon-service-AAD-Licenses.svg | 0 ...1-icon-service-Entra-Identity-Licenses.png | Bin ...1-icon-service-Entra-Identity-Licenses.svg | 0 .../other/02691-icon-service-Test-Base.png | Bin .../other/02691-icon-service-Test-Base.svg | 0 .../02697-icon-service-Azure-Orbital.png | Bin .../02697-icon-service-Azure-Orbital.svg | 0 ...on-service-Capacity-Reservation-Groups.png | Bin ...on-service-Capacity-Reservation-Groups.svg | 0 ...-service-Windows-Notification-Services.png | Bin ...-service-Windows-Notification-Services.svg | 0 ...service-Azure-Network-Function-Manager.png | Bin ...service-Azure-Network-Function-Manager.svg | 0 ...2793-icon-service-Mission-Landing-Zone.png | Bin ...2793-icon-service-Mission-Landing-Zone.svg | 0 .../02794-icon-service-Mobile-Networks.png | Bin .../02794-icon-service-Mobile-Networks.svg | 0 .../02805-icon-service-VM-App-Definitions.png | Bin .../02805-icon-service-VM-App-Definitions.svg | 0 .../02806-icon-service-VM-App-Versions.png | Bin .../02806-icon-service-VM-App-Versions.svg | 0 ...con-service-Azure-Edge-Hardware-Center.png | Bin ...con-service-Azure-Edge-Hardware-Center.svg | 0 .../02819-icon-service-Resource-Guard.png | Bin .../02819-icon-service-Resource-Guard.svg | 0 .../azure/other/02828-icon-service-Ceres.png | Bin .../azure/other/02828-icon-service-Ceres.svg | 0 .../other/02842-icon-service-Azurite.png | Bin .../other/02842-icon-service-Azurite.svg | 0 ...-icon-service-Update-Management-Center.png | Bin ...-icon-service-Update-Management-Center.svg | 0 ...4-icon-service-Azure-Compute-Galleries.png | Bin ...4-icon-service-Azure-Compute-Galleries.svg | 0 .../02865-icon-service-Community-Images.png | Bin .../02865-icon-service-Community-Images.svg | 0 .../02866-icon-service-VM-Image-Version.png | Bin .../02866-icon-service-VM-Image-Version.svg | 0 .../02880-icon-service-Savings-Plans.png | Bin .../02880-icon-service-Savings-Plans.svg | 0 ...2884-icon-service-Worker-Container-App.png | Bin ...2884-icon-service-Worker-Container-App.svg | 0 ...905-icon-service-Azure-Managed-Grafana.png | Bin ...905-icon-service-Azure-Managed-Grafana.svg | 0 .../02908-icon-service-Targets-Management.png | Bin .../02908-icon-service-Targets-Management.svg | 0 .../02909-icon-service-Storage-Functions.png | Bin .../02909-icon-service-Storage-Functions.svg | 0 .../02909-icon-service-Storage-Tasks.png | Bin .../02909-icon-service-Storage-Tasks.svg | 0 .../other/02922-icon-service-Sonic-Dash.png | Bin .../other/02922-icon-service-Sonic-Dash.svg | 0 .../02931-icon-service-Compliance-Center.png | Bin .../02931-icon-service-Compliance-Center.svg | 0 ...on-service-Network-Security-Perimeters.png | Bin ...on-service-Network-Security-Perimeters.svg | 0 .../02944-icon-service-Azure-Load-Testing.png | Bin .../02944-icon-service-Azure-Load-Testing.svg | 0 ...49-icon-service-Virtual-Visits-Builder.png | Bin ...49-icon-service-Virtual-Visits-Builder.svg | 0 .../other/02951-icon-service-Azure-Quotas.png | Bin .../other/02951-icon-service-Azure-Quotas.svg | 0 ...on-service-Container-Apps-Environments.png | Bin ...on-service-Container-Apps-Environments.svg | 0 ...icon-service-App-Compliance-Automation.png | Bin ...icon-service-App-Compliance-Automation.svg | 0 ...-icon-service-Virtual-Instance-for-SAP.png | Bin ...-icon-service-Virtual-Instance-for-SAP.svg | 0 ...3090-icon-service-Azure-Center-for-SAP.png | Bin ...3090-icon-service-Azure-Center-for-SAP.svg | 0 ...03091-icon-service-Azure-Storage-Mover.png | Bin ...03091-icon-service-Azure-Storage-Mover.svg | 0 ...rvice-Central-Service-Instance-For-SAP.png | Bin ...rvice-Central-Service-Instance-For-SAP.svg | 0 ...-icon-service-Kubernetes-Fleet-Manager.png | Bin ...-icon-service-Kubernetes-Fleet-Manager.svg | 0 ...ervice-Express-Route-Traffic-Collector.png | Bin ...ervice-Express-Route-Traffic-Collector.svg | 0 ...icon-service-Database-Instance-For-SAP.png | Bin ...icon-service-Database-Instance-For-SAP.svg | 0 .../other/03190-icon-service-Elastic-SAN.png | Bin .../other/03190-icon-service-Elastic-SAN.svg | 0 .../03250-icon-service-Microsoft-Dev-Box.png | Bin .../03250-icon-service-Microsoft-Dev-Box.svg | 0 ...-service-Azure-Deployment-Environments.png | Bin ...-service-Azure-Deployment-Environments.svg | 0 ...69111-icon-service-Microsoft-Discovery.png | Bin ...69111-icon-service-Microsoft-Discovery.svg | 0 .../03308-icon-service-Azure-Dev-Tunnels.png | Bin .../03308-icon-service-Azure-Dev-Tunnels.svg | 0 .../033112809-icon-service-promethus.png | Bin .../033112809-icon-service-promethus.svg | 0 ...3314-icon-service-Azure-Sustainability.png | Bin ...3314-icon-service-Azure-Sustainability.svg | 0 ...03361-icon-service-IcM-Troubleshooting.png | Bin ...03361-icon-service-IcM-Troubleshooting.svg | 0 .../other/03365-icon-service-OSConfig.png | Bin .../other/03365-icon-service-OSConfig.svg | 0 .../03394-icon-service-Azure-Enclaves.png | Bin .../03394-icon-service-Azure-Enclaves.svg | 0 .../other/03399-icon-service-AKS-Istio.png | Bin .../other/03399-icon-service-AKS-Istio.svg | 0 ...icon-service-Defender-CM-Local-Manager.png | Bin ...icon-service-Defender-CM-Local-Manager.svg | 0 ...n-service-Defender-External-Management.png | Bin ...n-service-Defender-External-Management.svg | 0 ...-icon-service-Defender-Freezer-Monitor.png | Bin ...-icon-service-Defender-Freezer-Monitor.svg | 0 .../03405-icon-service-Defender-Historian.png | Bin .../03405-icon-service-Defender-Historian.svg | 0 .../other/03406-icon-service-Defender-HMI.png | Bin .../other/03406-icon-service-Defender-HMI.svg | 0 .../03407-icon-service-Defender-Marquee.png | Bin .../03407-icon-service-Defender-Marquee.svg | 0 ...icon-service-Defender-Robot-Controller.png | Bin ...icon-service-Defender-Robot-Controller.svg | 0 .../03409-icon-service-Defender-Sensor.png | Bin .../03409-icon-service-Defender-Sensor.svg | 0 .../03410-icon-service-Defender-Slot.png | Bin .../03410-icon-service-Defender-Slot.svg | 0 ...on-service-Defender-Web-Guiding-System.png | Bin ...on-service-Defender-Web-Guiding-System.svg | 0 ...2-icon-service-Defender-DCS-Controller.png | Bin ...2-icon-service-Defender-DCS-Controller.svg | 0 ...ce-Defender-Distributer-Control-System.png | Bin ...ce-Defender-Distributer-Control-System.svg | 0 ...n-service-Defender-Engineering-Station.png | Bin ...n-service-Defender-Engineering-Station.svg | 0 ...e-Defender-Industrial-Packaging-System.png | Bin ...e-Defender-Industrial-Packaging-System.svg | 0 ...on-service-Defender-Industrial-Printer.png | Bin ...on-service-Defender-Industrial-Printer.svg | 0 ...rvice-Defender-Industrial-Scale-System.png | Bin ...rvice-Defender-Industrial-Scale-System.svg | 0 ...icon-service-Defender-Industrial-Robot.png | Bin ...icon-service-Defender-Industrial-Robot.svg | 0 .../03419-icon-service-Defender-Meter.png | Bin .../03419-icon-service-Defender-Meter.svg | 0 .../other/03420-icon-service-Defender-PLC.png | Bin .../other/03420-icon-service-Defender-PLC.svg | 0 ...icon-service-Defender-Pneumatic-Device.png | Bin ...icon-service-Defender-Pneumatic-Device.svg | 0 ...con-service-Defender-Programable-Board.png | Bin ...con-service-Defender-Programable-Board.svg | 0 .../03423-icon-service-Defender-Relay.png | Bin .../03423-icon-service-Defender-Relay.svg | 0 .../other/03424-icon-service-Defender-RTU.png | Bin .../other/03424-icon-service-Defender-RTU.svg | 0 .../03468-icon-service-HDI-AKS-Cluster.png | Bin .../03468-icon-service-HDI-AKS-Cluster.svg | 0 ...n-service-Azure-Consumption-Commitment.png | Bin ...n-service-Azure-Consumption-Commitment.svg | 0 ...528-icon-service-Monitor-Health-Models.png | Bin ...528-icon-service-Monitor-Health-Models.svg | 0 .../03563-icon-service-WAC-Installer.png | Bin .../03563-icon-service-WAC-Installer.svg | 0 .../azure/other/03633-icon-service-Breeze.png | Bin .../azure/other/03633-icon-service-Breeze.svg | 0 .../other/10018-icon-service-Azure-A.png | Bin .../other/10018-icon-service-Azure-A.svg | 0 ...77-icon-service-Local-Network-Gateways.png | Bin ...77-icon-service-Local-Network-Gateways.svg | 0 .../10117-icon-service-Edge-Management.png | Bin .../10117-icon-service-Edge-Management.svg | 0 .../10139-icon-service-Instance-Pools.png | Bin .../10139-icon-service-Instance-Pools.svg | 0 .../other/10190-icon-service-Azure-Sphere.png | Bin .../other/10190-icon-service-Azure-Sphere.svg | 0 .../10232-icon-service-App-Registrations.png | Bin .../10232-icon-service-App-Registrations.svg | 0 ...on-service-Exchange-On-Premises-Access.png | Bin ...on-service-Exchange-On-Premises-Access.svg | 0 .../azure/other/10355-icon-service-WAC.png | Bin .../azure/other/10355-icon-service-WAC.svg | 0 .../10422-icon-service-AzureAttestation.png | Bin .../10422-icon-service-AzureAttestation.svg | 0 .../azure/other/10778-icon-service-RTOS.png | Bin .../azure/other/10778-icon-service-RTOS.svg | 0 .../other/10846-icon-service-Web-Jobs.png | Bin .../other/10846-icon-service-Web-Jobs.svg | 0 .../00378-icon-service-Detonation.png | Bin .../00378-icon-service-Detonation.svg | 0 ...con-service-Microsoft-Defender-for-IoT.png | Bin ...con-service-Microsoft-Defender-for-IoT.svg | 0 ...Azure-AD-Privleged-Identity-Management.png | Bin ...Azure-AD-Privleged-Identity-Management.svg | 0 ...6-icon-service-Microsoft-Defender-EASM.png | Bin ...6-icon-service-Microsoft-Defender-EASM.svg | 0 ...340-icon-service-Identity-Secure-Score.png | Bin ...340-icon-service-Identity-Secure-Score.svg | 0 ...41-icon-service-Azure-AD-Risky-Signins.png | Bin ...41-icon-service-Azure-AD-Risky-Signins.svg | 0 ...n-service-Entra-Identity-Risky-Signins.png | Bin ...n-service-Entra-Identity-Risky-Signins.svg | 0 ...3342-icon-service-Azure-AD-Risky-Users.png | Bin ...3342-icon-service-Azure-AD-Risky-Users.svg | 0 ...con-service-Entra-Identity-Risky-Users.png | Bin ...con-service-Entra-Identity-Risky-Users.svg | 0 ...ervice-Azure-AD-Authentication-Methods.png | Bin ...ervice-Azure-AD-Authentication-Methods.svg | 0 ...con-service-Multifactor-Authentication.png | Bin ...con-service-Multifactor-Authentication.svg | 0 ...n-service-Azure-Information-Protection.png | Bin ...n-service-Azure-Information-Protection.svg | 0 ...n-service-Azure-AD-Identity-Protection.png | Bin ...n-service-Azure-AD-Identity-Protection.svg | 0 .../10233-icon-service-Conditional-Access.png | Bin .../10233-icon-service-Conditional-Access.svg | 0 ...n-service-Microsoft-Defender-for-Cloud.png | Bin ...n-service-Microsoft-Defender-for-Cloud.svg | 0 ...on-service-Application-Security-Groups.png | Bin ...on-service-Application-Security-Groups.svg | 0 .../10245-icon-service-Key-Vaults.png | Bin .../10245-icon-service-Key-Vaults.svg | 0 .../10248-icon-service-Azure-Sentinel.png | Bin .../10248-icon-service-Azure-Sentinel.svg | 0 .../10433-icon-service-User-Settings.png | Bin .../10433-icon-service-User-Settings.svg | 0 ...2-icon-service-ExtendedSecurityUpdates.png | Bin ...2-icon-service-ExtendedSecurityUpdates.svg | 0 ...-icon-service-Recovery-Services-Vaults.png | Bin ...-icon-service-Recovery-Services-Vaults.svg | 0 ...691-icon-service-Azure-Databox-Gateway.png | Bin ...691-icon-service-Azure-Databox-Gateway.svg | 0 .../00776-icon-service-Azure-HCP-Cache.png | Bin .../00776-icon-service-Azure-HCP-Cache.svg | 0 ...03566-icon-service-Data-Virtualization.png | Bin ...03566-icon-service-Data-Virtualization.svg | 0 .../030486388-icon-service-Storage-Hubs.png | Bin .../030486388-icon-service-Storage-Hubs.svg | 0 .../03502-icon-service-Storage-Actions.png | Bin .../03502-icon-service-Storage-Actions.svg | 0 ...03549-icon-service-Managed-File-Shares.png | Bin ...03549-icon-service-Managed-File-Shares.svg | 0 .../10086-icon-service-Storage-Accounts.png | Bin .../10086-icon-service-Storage-Accounts.svg | 0 ...con-service-Storage-Accounts-(Classic).png | Bin ...con-service-Storage-Accounts-(Classic).svg | 0 ...con-service-StorSimple-Device-Managers.png | Bin ...con-service-StorSimple-Device-Managers.svg | 0 ...90-icon-service-Data-Lake-Storage-Gen1.png | Bin ...90-icon-service-Data-Lake-Storage-Gen1.svg | 0 .../10091-icon-service-Storage-Explorer.png | Bin .../10091-icon-service-Storage-Explorer.svg | 0 ...-icon-service-StorSimple-Data-Managers.png | Bin ...-icon-service-StorSimple-Data-Managers.svg | 0 ...093-icon-service-Storage-Sync-Services.png | Bin ...093-icon-service-Storage-Sync-Services.svg | 0 .../storage/10094-icon-service-Data-Box.png | Bin .../storage/10094-icon-service-Data-Box.svg | 0 .../10095-icon-service-Azure-Stack-Edge.png | Bin .../10095-icon-service-Azure-Stack-Edge.svg | 0 .../10096-icon-service-Azure-NetApp-Files.png | Bin .../10096-icon-service-Azure-NetApp-Files.svg | 0 ...97-icon-service-Data-Share-Invitations.png | Bin ...97-icon-service-Data-Share-Invitations.svg | 0 .../10098-icon-service-Data-Shares.png | Bin .../10098-icon-service-Data-Shares.svg | 0 .../10100-icon-service-Import-Export-Jobs.png | Bin .../10100-icon-service-Import-Export-Jobs.svg | 0 .../10400-icon-service-Azure-Fileshares.png | Bin .../10400-icon-service-Azure-Fileshares.svg | 0 .../00046-icon-service-App-Service-Plans.png | Bin .../00046-icon-service-App-Service-Plans.svg | 0 ...-icon-service-App-Service-Certificates.png | Bin ...-icon-service-App-Service-Certificates.svg | 0 ...00050-icon-service-App-Service-Domains.png | Bin ...00050-icon-service-App-Service-Domains.svg | 0 .../web/01007-icon-service-Static-Apps.png | Bin .../web/01007-icon-service-Static-Apps.svg | 0 .../web/03291-icon-service-API-Center.png | Bin .../web/03291-icon-service-API-Center.svg | 0 .../web/03335-icon-service-Power-Platform.png | Bin .../web/03335-icon-service-Power-Platform.svg | 0 .../web/03397-icon-service-App-Space.png | Bin .../web/03397-icon-service-App-Space.svg | 0 ...03595-icon-service-App-Space-Component.png | Bin ...03595-icon-service-App-Space-Component.svg | 0 .../web/10035-icon-service-App-Services.png | Bin .../web/10035-icon-service-App-Services.svg | 0 ...2-icon-service-API-Management-Services.png | Bin ...2-icon-service-API-Management-Services.svg | 0 .../10044-icon-service-Cognitive-Search.png | Bin .../10044-icon-service-Cognitive-Search.svg | 0 ...-icon-service-App-Service-Environments.png | Bin ...-icon-service-App-Service-Environments.svg | 0 .../10048-icon-service-API-Connections.png | Bin .../10048-icon-service-API-Connections.svg | 0 .../azure/web/10052-icon-service-SignalR.png | Bin .../azure/web/10052-icon-service-SignalR.svg | 0 ...on-service-Notification-Hub-Namespaces.png | Bin ...on-service-Notification-Hub-Namespaces.svg | 0 ...on-service-Front-Door-and-CDN-Profiles.png | Bin ...on-service-Front-Door-and-CDN-Profiles.svg | 0 .../10162-icon-service-Cognitive-Services.png | Bin .../10162-icon-service-Cognitive-Services.svg | 0 ...10309-icon-service-Azure-Media-Service.png | Bin ...10309-icon-service-Azure-Media-Service.svg | 0 .../10370-icon-service-Azure-Spring-Apps.png | Bin .../10370-icon-service-Azure-Spring-Apps.svg | 0 .../assets}/icons/misc/no_image.png | Bin .../assets}/icons/severity/high.png | Bin .../assets}/icons/severity/low.png | Bin .../assets}/icons/severity/medium.png | Bin {assets => core/assets}/img/logo/favicon.ico | Bin {assets => core/assets}/img/logo/favicon.png | Bin {assets => core/assets}/img/logo/logo.png | Bin .../assets}/img/logo/report_logo.png | Bin {assets => core/assets}/template/egress.html | 0 {assets => core/assets}/template/index.html | 0 core/report/context.py | 9 +- core/report/cover.py | 12 +- core/report/fonts.py | 19 +- core/resources.py | 25 ++ core/utils.py | 6 +- core/utils_report.py | 5 +- core/utils_report_egress.py | 15 +- core/utils_report_pdf.py | 5 +- pyproject.toml | 3 + tests/fixtures/smoke/aws.tfstate | 74 ++++++ tests/make_smoke_dataset.py | 106 ++++++++ tests/report_fixtures.py | 8 +- tests/test_copy_assets.py | 3 +- tests/test_packaging.py | 249 ++++++++++++++++++ tests/test_utils_report_egress.py | 4 +- 2234 files changed, 553 insertions(+), 37 deletions(-) rename {assets => core/assets}/css/style.css (100%) rename {assets => core/assets}/fonts/IBMPlexMono-Regular.ttf (100%) rename {assets => core/assets}/fonts/IBMPlexMono-SemiBold.ttf (100%) rename {assets => core/assets}/fonts/IBMPlexSans-Italic.ttf (100%) rename {assets => core/assets}/fonts/IBMPlexSans-Medium.ttf (100%) rename {assets => core/assets}/fonts/IBMPlexSans-Regular.ttf (100%) rename {assets => core/assets}/fonts/IBMPlexSans-SemiBold.ttf (100%) rename {assets => core/assets}/fonts/IBMPlexSans-SemiBoldItalic.ttf (100%) rename {assets => core/assets}/fonts/OFL.txt (100%) rename {assets => core/assets}/icons/README.md (100%) rename {assets => core/assets}/icons/aws/Analytics/Athena.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Athena.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/Clean-Rooms.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Clean-Rooms.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/CloudSearch.png (100%) rename {assets => core/assets}/icons/aws/Analytics/CloudSearch.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/Data-Exchange.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Data-Exchange.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/Data-Firehose.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Data-Firehose.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/Data-Pipeline.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Data-Pipeline.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/DataZone.png (100%) rename {assets => core/assets}/icons/aws/Analytics/DataZone.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/EMR.png (100%) rename {assets => core/assets}/icons/aws/Analytics/EMR.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/Entity-Resolution.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Entity-Resolution.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/FinSpace.png (100%) rename {assets => core/assets}/icons/aws/Analytics/FinSpace.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/Glue-DataBrew.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Glue-DataBrew.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/Glue-Elastic-Views.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Glue-Elastic-Views.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/Glue.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Glue.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/Kinesis-Data-Analytics.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Kinesis-Data-Analytics.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/Kinesis-Data-Streams.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Kinesis-Data-Streams.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/Kinesis-Firehose.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Kinesis-Firehose.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/Kinesis-Video-Streams.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Kinesis-Video-Streams.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/Kinesis.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Kinesis.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/Lake-Formation.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Lake-Formation.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/Managed-Service-for-Apache-Flink.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Managed-Service-for-Apache-Flink.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/Managed-Streaming-for-Apache-Kafka.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Managed-Streaming-for-Apache-Kafka.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/OpenSearch-Service.png (100%) rename {assets => core/assets}/icons/aws/Analytics/OpenSearch-Service.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/QuickSight.png (100%) rename {assets => core/assets}/icons/aws/Analytics/QuickSight.svg (100%) rename {assets => core/assets}/icons/aws/Analytics/Redshift.png (100%) rename {assets => core/assets}/icons/aws/Analytics/Redshift.svg (100%) rename {assets => core/assets}/icons/aws/App-Integration/API-Gateway.png (100%) rename {assets => core/assets}/icons/aws/App-Integration/API-Gateway.svg (100%) rename {assets => core/assets}/icons/aws/App-Integration/AppFlow.png (100%) rename {assets => core/assets}/icons/aws/App-Integration/AppFlow.svg (100%) rename {assets => core/assets}/icons/aws/App-Integration/AppSync.png (100%) rename {assets => core/assets}/icons/aws/App-Integration/AppSync.svg (100%) rename {assets => core/assets}/icons/aws/App-Integration/B2B-Data-Interchange.png (100%) rename {assets => core/assets}/icons/aws/App-Integration/B2B-Data-Interchange.svg (100%) rename {assets => core/assets}/icons/aws/App-Integration/Console-Mobile-Application.png (100%) rename {assets => core/assets}/icons/aws/App-Integration/Console-Mobile-Application.svg (100%) rename {assets => core/assets}/icons/aws/App-Integration/EventBridge.png (100%) rename {assets => core/assets}/icons/aws/App-Integration/EventBridge.svg (100%) rename {assets => core/assets}/icons/aws/App-Integration/Express-Workflows.png (100%) rename {assets => core/assets}/icons/aws/App-Integration/Express-Workflows.svg (100%) rename {assets => core/assets}/icons/aws/App-Integration/MQ.png (100%) rename {assets => core/assets}/icons/aws/App-Integration/MQ.svg (100%) rename {assets => core/assets}/icons/aws/App-Integration/Managed-Workflows-for-Apache-Airflow.png (100%) rename {assets => core/assets}/icons/aws/App-Integration/Managed-Workflows-for-Apache-Airflow.svg (100%) rename {assets => core/assets}/icons/aws/App-Integration/Simple-Notification-Service.png (100%) rename {assets => core/assets}/icons/aws/App-Integration/Simple-Notification-Service.svg (100%) rename {assets => core/assets}/icons/aws/App-Integration/Simple-Queue-Service.png (100%) rename {assets => core/assets}/icons/aws/App-Integration/Simple-Queue-Service.svg (100%) rename {assets => core/assets}/icons/aws/App-Integration/Step-Functions.png (100%) rename {assets => core/assets}/icons/aws/App-Integration/Step-Functions.svg (100%) rename {assets => core/assets}/icons/aws/Blockchain/Managed-Blockchain.png (100%) rename {assets => core/assets}/icons/aws/Blockchain/Managed-Blockchain.svg (100%) rename {assets => core/assets}/icons/aws/Blockchain/Quantum-Ledger-Database.png (100%) rename {assets => core/assets}/icons/aws/Blockchain/Quantum-Ledger-Database.svg (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Alexa-For-Business.png (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Alexa-For-Business.svg (100%) rename {assets => core/assets}/icons/aws/Business-Applications/AppFabric.png (100%) rename {assets => core/assets}/icons/aws/Business-Applications/AppFabric.svg (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Chime-SDK.png (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Chime-SDK.svg (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Chime-Voice-Connector.png (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Chime-Voice-Connector.svg (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Chime.png (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Chime.svg (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Connect.png (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Connect.svg (100%) rename {assets => core/assets}/icons/aws/Business-Applications/End-User-Messaging.png (100%) rename {assets => core/assets}/icons/aws/Business-Applications/End-User-Messaging.svg (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Honeycode.png (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Honeycode.svg (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Pinpoint-APIs.png (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Pinpoint-APIs.svg (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Pinpoint.png (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Pinpoint.svg (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Quick.png (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Quick.svg (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Simple-Email-Service.png (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Simple-Email-Service.svg (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Supply-Chain.png (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Supply-Chain.svg (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Wickr.png (100%) rename {assets => core/assets}/icons/aws/Business-Applications/Wickr.svg (100%) rename {assets => core/assets}/icons/aws/Business-Applications/WorkDocs-SDK.png (100%) rename {assets => core/assets}/icons/aws/Business-Applications/WorkDocs-SDK.svg (100%) rename {assets => core/assets}/icons/aws/Business-Applications/WorkDocs.png (100%) rename {assets => core/assets}/icons/aws/Business-Applications/WorkDocs.svg (100%) rename {assets => core/assets}/icons/aws/Business-Applications/WorkMail.png (100%) rename {assets => core/assets}/icons/aws/Business-Applications/WorkMail.svg (100%) rename {assets => core/assets}/icons/aws/Cloud-Financial-Management/Application-Cost-Profiler.png (100%) rename {assets => core/assets}/icons/aws/Cloud-Financial-Management/Application-Cost-Profiler.svg (100%) rename {assets => core/assets}/icons/aws/Cloud-Financial-Management/Billing-Conductor.png (100%) rename {assets => core/assets}/icons/aws/Cloud-Financial-Management/Billing-Conductor.svg (100%) rename {assets => core/assets}/icons/aws/Cloud-Financial-Management/Budgets.png (100%) rename {assets => core/assets}/icons/aws/Cloud-Financial-Management/Budgets.svg (100%) rename {assets => core/assets}/icons/aws/Cloud-Financial-Management/Cost-Explorer.png (100%) rename {assets => core/assets}/icons/aws/Cloud-Financial-Management/Cost-Explorer.svg (100%) rename {assets => core/assets}/icons/aws/Cloud-Financial-Management/Cost-and-Usage-Report.png (100%) rename {assets => core/assets}/icons/aws/Cloud-Financial-Management/Cost-and-Usage-Report.svg (100%) rename {assets => core/assets}/icons/aws/Cloud-Financial-Management/Reserved-Instance-Reporting.png (100%) rename {assets => core/assets}/icons/aws/Cloud-Financial-Management/Reserved-Instance-Reporting.svg (100%) rename {assets => core/assets}/icons/aws/Cloud-Financial-Management/Savings-Plans.png (100%) rename {assets => core/assets}/icons/aws/Cloud-Financial-Management/Savings-Plans.svg (100%) rename {assets => core/assets}/icons/aws/Compute/App-Runner.png (100%) rename {assets => core/assets}/icons/aws/Compute/App-Runner.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Application-Auto-Scaling.png (100%) rename {assets => core/assets}/icons/aws/Compute/Application-Auto-Scaling.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Batch.png (100%) rename {assets => core/assets}/icons/aws/Compute/Batch.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Bottlerocket.png (100%) rename {assets => core/assets}/icons/aws/Compute/Bottlerocket.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Compute-Optimizer.png (100%) rename {assets => core/assets}/icons/aws/Compute/Compute-Optimizer.svg (100%) rename {assets => core/assets}/icons/aws/Compute/DCV.png (100%) rename {assets => core/assets}/icons/aws/Compute/DCV.svg (100%) rename {assets => core/assets}/icons/aws/Compute/EC2-Auto-Scaling.png (100%) rename {assets => core/assets}/icons/aws/Compute/EC2-Auto-Scaling.svg (100%) rename {assets => core/assets}/icons/aws/Compute/EC2-Image-Builder.png (100%) rename {assets => core/assets}/icons/aws/Compute/EC2-Image-Builder.svg (100%) rename {assets => core/assets}/icons/aws/Compute/EC2.png (100%) rename {assets => core/assets}/icons/aws/Compute/EC2.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Elastic-Beanstalk.png (100%) rename {assets => core/assets}/icons/aws/Compute/Elastic-Beanstalk.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Elastic-Fabric-Adapter.png (100%) rename {assets => core/assets}/icons/aws/Compute/Elastic-Fabric-Adapter.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Elastic-VMware-Service.png (100%) rename {assets => core/assets}/icons/aws/Compute/Elastic-VMware-Service.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Fargate.png (100%) rename {assets => core/assets}/icons/aws/Compute/Fargate.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Genomics-CLI.png (100%) rename {assets => core/assets}/icons/aws/Compute/Genomics-CLI.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Lambda.png (100%) rename {assets => core/assets}/icons/aws/Compute/Lambda.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Lightsail-for-Research.png (100%) rename {assets => core/assets}/icons/aws/Compute/Lightsail-for-Research.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Lightsail.png (100%) rename {assets => core/assets}/icons/aws/Compute/Lightsail.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Local-Zones.png (100%) rename {assets => core/assets}/icons/aws/Compute/Local-Zones.svg (100%) rename {assets => core/assets}/icons/aws/Compute/NICE-DCV.png (100%) rename {assets => core/assets}/icons/aws/Compute/NICE-DCV.svg (100%) rename {assets => core/assets}/icons/aws/Compute/NICE-EnginFrame.png (100%) rename {assets => core/assets}/icons/aws/Compute/NICE-EnginFrame.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Nitro-Enclaves.png (100%) rename {assets => core/assets}/icons/aws/Compute/Nitro-Enclaves.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Outposts-family.png (100%) rename {assets => core/assets}/icons/aws/Compute/Outposts-family.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Outposts-rack.png (100%) rename {assets => core/assets}/icons/aws/Compute/Outposts-rack.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Outposts-servers.png (100%) rename {assets => core/assets}/icons/aws/Compute/Outposts-servers.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Parallel-Cluster.png (100%) rename {assets => core/assets}/icons/aws/Compute/Parallel-Cluster.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Parallel-Computing-Service.png (100%) rename {assets => core/assets}/icons/aws/Compute/Parallel-Computing-Service.svg (100%) rename {assets => core/assets}/icons/aws/Compute/ParallelCluster.png (100%) rename {assets => core/assets}/icons/aws/Compute/ParallelCluster.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Serverless-Application-Repository.png (100%) rename {assets => core/assets}/icons/aws/Compute/Serverless-Application-Repository.svg (100%) rename {assets => core/assets}/icons/aws/Compute/SimSpace-Weaver.png (100%) rename {assets => core/assets}/icons/aws/Compute/SimSpace-Weaver.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Thinkbox-Deadline.png (100%) rename {assets => core/assets}/icons/aws/Compute/Thinkbox-Deadline.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Thinkbox-Frost.png (100%) rename {assets => core/assets}/icons/aws/Compute/Thinkbox-Frost.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Thinkbox-Krakatoa.png (100%) rename {assets => core/assets}/icons/aws/Compute/Thinkbox-Krakatoa.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Thinkbox-Sequoia.png (100%) rename {assets => core/assets}/icons/aws/Compute/Thinkbox-Sequoia.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Thinkbox-Stoke.png (100%) rename {assets => core/assets}/icons/aws/Compute/Thinkbox-Stoke.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Thinkbox-XMesh.png (100%) rename {assets => core/assets}/icons/aws/Compute/Thinkbox-XMesh.svg (100%) rename {assets => core/assets}/icons/aws/Compute/VMware-Cloud-on-AWS.png (100%) rename {assets => core/assets}/icons/aws/Compute/VMware-Cloud-on-AWS.svg (100%) rename {assets => core/assets}/icons/aws/Compute/Wavelength.png (100%) rename {assets => core/assets}/icons/aws/Compute/Wavelength.svg (100%) rename {assets => core/assets}/icons/aws/Containers/ECS-Anywhere.png (100%) rename {assets => core/assets}/icons/aws/Containers/ECS-Anywhere.svg (100%) rename {assets => core/assets}/icons/aws/Containers/EKS-Anywhere.png (100%) rename {assets => core/assets}/icons/aws/Containers/EKS-Anywhere.svg (100%) rename {assets => core/assets}/icons/aws/Containers/EKS-Cloud.png (100%) rename {assets => core/assets}/icons/aws/Containers/EKS-Cloud.svg (100%) rename {assets => core/assets}/icons/aws/Containers/EKS-Distro.png (100%) rename {assets => core/assets}/icons/aws/Containers/EKS-Distro.svg (100%) rename {assets => core/assets}/icons/aws/Containers/Elastic-Container-Registry.png (100%) rename {assets => core/assets}/icons/aws/Containers/Elastic-Container-Registry.svg (100%) rename {assets => core/assets}/icons/aws/Containers/Elastic-Container-Service.png (100%) rename {assets => core/assets}/icons/aws/Containers/Elastic-Container-Service.svg (100%) rename {assets => core/assets}/icons/aws/Containers/Elastic-Kubernetes-Service.png (100%) rename {assets => core/assets}/icons/aws/Containers/Elastic-Kubernetes-Service.svg (100%) rename {assets => core/assets}/icons/aws/Containers/Fargate.png (100%) rename {assets => core/assets}/icons/aws/Containers/Fargate.svg (100%) rename {assets => core/assets}/icons/aws/Containers/Red-Hat-OpenShift-Service-on-AWS.png (100%) rename {assets => core/assets}/icons/aws/Containers/Red-Hat-OpenShift-Service-on-AWS.svg (100%) rename {assets => core/assets}/icons/aws/Customer-Enablement/Activate.png (100%) rename {assets => core/assets}/icons/aws/Customer-Enablement/Activate.svg (100%) rename {assets => core/assets}/icons/aws/Customer-Enablement/IQ.png (100%) rename {assets => core/assets}/icons/aws/Customer-Enablement/IQ.svg (100%) rename {assets => core/assets}/icons/aws/Customer-Enablement/Managed-Services.png (100%) rename {assets => core/assets}/icons/aws/Customer-Enablement/Managed-Services.svg (100%) rename {assets => core/assets}/icons/aws/Customer-Enablement/Professional-Services.png (100%) rename {assets => core/assets}/icons/aws/Customer-Enablement/Professional-Services.svg (100%) rename {assets => core/assets}/icons/aws/Customer-Enablement/Support.png (100%) rename {assets => core/assets}/icons/aws/Customer-Enablement/Support.svg (100%) rename {assets => core/assets}/icons/aws/Customer-Enablement/Training-Certification.png (100%) rename {assets => core/assets}/icons/aws/Customer-Enablement/Training-Certification.svg (100%) rename {assets => core/assets}/icons/aws/Customer-Enablement/rePost-Private.png (100%) rename {assets => core/assets}/icons/aws/Customer-Enablement/rePost-Private.svg (100%) rename {assets => core/assets}/icons/aws/Customer-Enablement/rePost.png (100%) rename {assets => core/assets}/icons/aws/Customer-Enablement/rePost.svg (100%) rename {assets => core/assets}/icons/aws/Database/Aurora.png (100%) rename {assets => core/assets}/icons/aws/Database/Aurora.svg (100%) rename {assets => core/assets}/icons/aws/Database/Database-Migration-Service.png (100%) rename {assets => core/assets}/icons/aws/Database/Database-Migration-Service.svg (100%) rename {assets => core/assets}/icons/aws/Database/DocumentDB.png (100%) rename {assets => core/assets}/icons/aws/Database/DocumentDB.svg (100%) rename {assets => core/assets}/icons/aws/Database/DynamoDB.png (100%) rename {assets => core/assets}/icons/aws/Database/DynamoDB.svg (100%) rename {assets => core/assets}/icons/aws/Database/ElastiCache.png (100%) rename {assets => core/assets}/icons/aws/Database/ElastiCache.svg (100%) rename {assets => core/assets}/icons/aws/Database/Keyspaces.png (100%) rename {assets => core/assets}/icons/aws/Database/Keyspaces.svg (100%) rename {assets => core/assets}/icons/aws/Database/MemoryDB-for-Redis.png (100%) rename {assets => core/assets}/icons/aws/Database/MemoryDB-for-Redis.svg (100%) rename {assets => core/assets}/icons/aws/Database/MemoryDB.png (100%) rename {assets => core/assets}/icons/aws/Database/MemoryDB.svg (100%) rename {assets => core/assets}/icons/aws/Database/Neptune.png (100%) rename {assets => core/assets}/icons/aws/Database/Neptune.svg (100%) rename {assets => core/assets}/icons/aws/Database/Oracle-Database-at-AWS.png (100%) rename {assets => core/assets}/icons/aws/Database/Oracle-Database-at-AWS.svg (100%) rename {assets => core/assets}/icons/aws/Database/RDS-on-VMware.png (100%) rename {assets => core/assets}/icons/aws/Database/RDS-on-VMware.svg (100%) rename {assets => core/assets}/icons/aws/Database/RDS.png (100%) rename {assets => core/assets}/icons/aws/Database/RDS.svg (100%) rename {assets => core/assets}/icons/aws/Database/Timestream.png (100%) rename {assets => core/assets}/icons/aws/Database/Timestream.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Application-Composer.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Application-Composer.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Cloud-Control-API.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Cloud-Control-API.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Cloud-Development-Kit.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Cloud-Development-Kit.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Cloud9.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Cloud9.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/CloudShell.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/CloudShell.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/CodeArtifact.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/CodeArtifact.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/CodeBuild.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/CodeBuild.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/CodeCatalyst.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/CodeCatalyst.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/CodeCommit.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/CodeCommit.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/CodeDeploy.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/CodeDeploy.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/CodePipeline.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/CodePipeline.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/CodeStar.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/CodeStar.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Command-Line-Interface.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Command-Line-Interface.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Corretto.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Corretto.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Fault-Injection-Service.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Fault-Injection-Service.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Infrastructure-Composer.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Infrastructure-Composer.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Tools-and-SDKs.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/Tools-and-SDKs.svg (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/X-Ray.png (100%) rename {assets => core/assets}/icons/aws/Developer-Tools/X-Ray.svg (100%) rename {assets => core/assets}/icons/aws/End-User-Computing/AppStream.png (100%) rename {assets => core/assets}/icons/aws/End-User-Computing/AppStream.svg (100%) rename {assets => core/assets}/icons/aws/End-User-Computing/WorkLink.png (100%) rename {assets => core/assets}/icons/aws/End-User-Computing/WorkLink.svg (100%) rename {assets => core/assets}/icons/aws/End-User-Computing/WorkSpaces-Family.png (100%) rename {assets => core/assets}/icons/aws/End-User-Computing/WorkSpaces-Family.svg (100%) rename {assets => core/assets}/icons/aws/End-User-Computing/WorkSpaces.png (100%) rename {assets => core/assets}/icons/aws/End-User-Computing/WorkSpaces.svg (100%) rename {assets => core/assets}/icons/aws/Front-End-Web-Mobile/Amplify.png (100%) rename {assets => core/assets}/icons/aws/Front-End-Web-Mobile/Amplify.svg (100%) rename {assets => core/assets}/icons/aws/Front-End-Web-Mobile/Device-Farm.png (100%) rename {assets => core/assets}/icons/aws/Front-End-Web-Mobile/Device-Farm.svg (100%) rename {assets => core/assets}/icons/aws/Front-End-Web-Mobile/Location-Service.png (100%) rename {assets => core/assets}/icons/aws/Front-End-Web-Mobile/Location-Service.svg (100%) rename {assets => core/assets}/icons/aws/Games/GameKit.png (100%) rename {assets => core/assets}/icons/aws/Games/GameKit.svg (100%) rename {assets => core/assets}/icons/aws/Games/GameLift-Servers.png (100%) rename {assets => core/assets}/icons/aws/Games/GameLift-Servers.svg (100%) rename {assets => core/assets}/icons/aws/Games/GameLift-Streams.png (100%) rename {assets => core/assets}/icons/aws/Games/GameLift-Streams.svg (100%) rename {assets => core/assets}/icons/aws/Games/GameLift.png (100%) rename {assets => core/assets}/icons/aws/Games/GameLift.svg (100%) rename {assets => core/assets}/icons/aws/Games/GameSparks.png (100%) rename {assets => core/assets}/icons/aws/Games/GameSparks.svg (100%) rename {assets => core/assets}/icons/aws/Games/Lumberyard.png (100%) rename {assets => core/assets}/icons/aws/Games/Lumberyard.svg (100%) rename {assets => core/assets}/icons/aws/Games/Open-3D-Engine.png (100%) rename {assets => core/assets}/icons/aws/Games/Open-3D-Engine.svg (100%) rename {assets => core/assets}/icons/aws/General-Icons/Marketplace_Dark.png (100%) rename {assets => core/assets}/icons/aws/General-Icons/Marketplace_Dark.svg (100%) rename {assets => core/assets}/icons/aws/General-Icons/Marketplace_Light.png (100%) rename {assets => core/assets}/icons/aws/General-Icons/Marketplace_Light.svg (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/FreeRTOS.png (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/FreeRTOS.svg (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-1-Click.png (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-1-Click.svg (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-Analytics.png (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-Analytics.svg (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-Button.png (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-Button.svg (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-Core.png (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-Core.svg (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-Device-Defender.png (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-Device-Defender.svg (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-Device-Management.png (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-Device-Management.svg (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-EduKit.png (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-EduKit.svg (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-Events.png (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-Events.svg (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-ExpressLink.png (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-ExpressLink.svg (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-FleetWise.png (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-FleetWise.svg (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-Greengrass.png (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-Greengrass.svg (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-RoboRunner.png (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-RoboRunner.svg (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-SiteWise.png (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-SiteWise.svg (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-Things-Graph.png (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-Things-Graph.svg (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-TwinMaker.png (100%) rename {assets => core/assets}/icons/aws/Internet-of-Things/IoT-TwinMaker.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Apache-MXNet-on-AWS.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Apache-MXNet-on-AWS.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/App-Studio.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/App-Studio.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Augmented-AI-A2I.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Augmented-AI-A2I.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Bedrock-AgentCore.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Bedrock-AgentCore.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Bedrock.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Bedrock.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/CodeGuru.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/CodeGuru.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/CodeWhisperer.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/CodeWhisperer.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Comprehend-Medical.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Comprehend-Medical.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Comprehend.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Comprehend.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Deep-Learning-AMIs.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Deep-Learning-AMIs.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Deep-Learning-Containers.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Deep-Learning-Containers.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/DeepComposer.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/DeepComposer.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/DeepLens.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/DeepLens.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/DeepRacer.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/DeepRacer.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/DevOps-Guru.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/DevOps-Guru.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Elastic-Inference.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Elastic-Inference.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Elemental-Inference.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Elemental-Inference.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Forecast.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Forecast.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Fraud-Detector.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Fraud-Detector.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/HealthImaging.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/HealthImaging.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/HealthLake.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/HealthLake.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/HealthOmics.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/HealthOmics.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/HealthScribe.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/HealthScribe.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Kendra.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Kendra.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Lex.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Lex.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Lookout-for-Equipment.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Lookout-for-Equipment.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Lookout-for-Metrics.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Lookout-for-Metrics.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Lookout-for-Vision.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Lookout-for-Vision.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Monitron.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Monitron.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Neuron.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Neuron.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Nova.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Nova.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Omics.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Omics.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Panorama.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Panorama.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Personalize.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Personalize.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Polly.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Polly.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/PyTorch-on-AWS.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/PyTorch-on-AWS.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Q.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Q.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Rekognition.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Rekognition.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/SageMaker-AI.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/SageMaker-AI.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/SageMaker-Ground-Truth.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/SageMaker-Ground-Truth.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/SageMaker-Studio-Lab.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/SageMaker-Studio-Lab.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/SageMaker.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/SageMaker.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/TensorFlow-on-AWS.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/TensorFlow-on-AWS.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Textract.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Textract.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/TorchServe.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/TorchServe.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Transcribe.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Transcribe.svg (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Translate.png (100%) rename {assets => core/assets}/icons/aws/Machine-Learning/Translate.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/AppConfig.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/AppConfig.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Application-Auto-Scaling.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Application-Auto-Scaling.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Auto-Scaling.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Auto-Scaling.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Backint-Agent.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Backint-Agent.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Chatbot.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Chatbot.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/CloudFormation.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/CloudFormation.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/CloudTrail.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/CloudTrail.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/CloudWatch.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/CloudWatch.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Config.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Config.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Control-Tower.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Control-Tower.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/DevOps-Agent.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/DevOps-Agent.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Distro-for-OpenTelemetry.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Distro-for-OpenTelemetry.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Fault-Injection-Simulator.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Fault-Injection-Simulator.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Health-Dashboard.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Health-Dashboard.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Launch-Wizard.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Launch-Wizard.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/License-Manager.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/License-Manager.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Managed-Grafana.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Managed-Grafana.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Managed-Service-for-Prometheus.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Managed-Service-for-Prometheus.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Management-Console.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Management-Console.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/OpsWorks.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/OpsWorks.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Organizations.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Organizations.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Partner-Central.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Partner-Central.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Personal-Health-Dashboard.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Personal-Health-Dashboard.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Proton.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Proton.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Resilience-Hub.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Resilience-Hub.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Resource-Explorer.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Resource-Explorer.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Service-Catalog.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Service-Catalog.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Service-Management-Connector.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Service-Management-Connector.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Sustainability.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Sustainability.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Systems-Manager.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Systems-Manager.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Telco-Network-Builder.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Telco-Network-Builder.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Trusted-Advisor.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Trusted-Advisor.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/User-Notifications.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/User-Notifications.svg (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Well-Architected-Tool.png (100%) rename {assets => core/assets}/icons/aws/Management-Governance/Well-Architected-Tool.svg (100%) rename {assets => core/assets}/icons/aws/Media-Services/Deadline-Cloud.png (100%) rename {assets => core/assets}/icons/aws/Media-Services/Deadline-Cloud.svg (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elastic-Transcoder.png (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elastic-Transcoder.svg (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-Appliances-&-Software.png (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-Appliances-&-Software.svg (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-Conductor.png (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-Conductor.svg (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-Delta.png (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-Delta.svg (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-Link.png (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-Link.svg (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-Live.png (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-Live.svg (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-MediaConnect.png (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-MediaConnect.svg (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-MediaConvert.png (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-MediaConvert.svg (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-MediaLive.png (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-MediaLive.svg (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-MediaPackage.png (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-MediaPackage.svg (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-MediaStore.png (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-MediaStore.svg (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-MediaTailor.png (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-MediaTailor.svg (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-Server.png (100%) rename {assets => core/assets}/icons/aws/Media-Services/Elemental-Server.svg (100%) rename {assets => core/assets}/icons/aws/Media-Services/Interactive-Video-Service.png (100%) rename {assets => core/assets}/icons/aws/Media-Services/Interactive-Video-Service.svg (100%) rename {assets => core/assets}/icons/aws/Media-Services/Kinesis-Video-Streams.png (100%) rename {assets => core/assets}/icons/aws/Media-Services/Kinesis-Video-Streams.svg (100%) rename {assets => core/assets}/icons/aws/Media-Services/Nimble-Studio.png (100%) rename {assets => core/assets}/icons/aws/Media-Services/Nimble-Studio.svg (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Application-Discovery-Service.png (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Application-Discovery-Service.svg (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Application-Migration-Service.png (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Application-Migration-Service.svg (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Data-Transfer-Terminal.png (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Data-Transfer-Terminal.svg (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/DataSync.png (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/DataSync.svg (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Mainframe-Modernization.png (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Mainframe-Modernization.svg (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Migration-Evaluator.png (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Migration-Evaluator.svg (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Migration-Hub.png (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Migration-Hub.svg (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Server-Migration-Service.png (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Server-Migration-Service.svg (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Transfer-Family.png (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Transfer-Family.svg (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Transform.png (100%) rename {assets => core/assets}/icons/aws/Migration-Transfer/Transform.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/App-Mesh.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/App-Mesh.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Application-Recovery-Controller.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Application-Recovery-Controller.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Client-VPN.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Client-VPN.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Cloud-Directory.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Cloud-Directory.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Cloud-Map.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Cloud-Map.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Cloud-WAN.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Cloud-WAN.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/CloudFront.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/CloudFront.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Direct-Connect.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Direct-Connect.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Elastic-Load-Balancing.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Elastic-Load-Balancing.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Global-Accelerator.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Global-Accelerator.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Interconnect.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Interconnect.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Private-5G.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Private-5G.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/PrivateLink.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/PrivateLink.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/RTB-Fabric.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/RTB-Fabric.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Route-53.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Route-53.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Site-to-Site-VPN.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Site-to-Site-VPN.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Transit-Gateway.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Transit-Gateway.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/VPC-Lattice.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/VPC-Lattice.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Verified-Access.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Verified-Access.svg (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Virtual-Private-Cloud.png (100%) rename {assets => core/assets}/icons/aws/Networking-Content-Delivery/Virtual-Private-Cloud.svg (100%) rename {assets => core/assets}/icons/aws/Quantum-Technologies/Braket.png (100%) rename {assets => core/assets}/icons/aws/Quantum-Technologies/Braket.svg (100%) rename {assets => core/assets}/icons/aws/Robotics/RoboMaker.png (100%) rename {assets => core/assets}/icons/aws/Robotics/RoboMaker.svg (100%) rename {assets => core/assets}/icons/aws/Satellite/Ground-Station.png (100%) rename {assets => core/assets}/icons/aws/Satellite/Ground-Station.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Artifact.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Artifact.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Audit-Manager.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Audit-Manager.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Certificate-Manager.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Certificate-Manager.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Cloud-Directory.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Cloud-Directory.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/CloudHSM.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/CloudHSM.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Cognito.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Cognito.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Detective.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Detective.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Directory-Service.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Directory-Service.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Firewall-Manager.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Firewall-Manager.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/GuardDuty.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/GuardDuty.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/IAM-Identity-Center.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/IAM-Identity-Center.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Identity-and-Access-Management.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Identity-and-Access-Management.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Inspector.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Inspector.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Key-Management-Service.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Key-Management-Service.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Macie.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Macie.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Network-Firewall.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Network-Firewall.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Payment-Cryptography.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Payment-Cryptography.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Private-Certificate-Authority.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Private-Certificate-Authority.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Resource-Access-Manager.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Resource-Access-Manager.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Secrets-Manager.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Secrets-Manager.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Security-Agent.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Security-Agent.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Security-Hub.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Security-Hub.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Security-Incident-Response.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Security-Incident-Response.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Security-Lake.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Security-Lake.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Shield.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Shield.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Signer.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Signer.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Verified-Permissions.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/Verified-Permissions.svg (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/WAF.png (100%) rename {assets => core/assets}/icons/aws/Security-Identity-Compliance/WAF.svg (100%) rename {assets => core/assets}/icons/aws/Storage/Backup.png (100%) rename {assets => core/assets}/icons/aws/Storage/Backup.svg (100%) rename {assets => core/assets}/icons/aws/Storage/EFS.png (100%) rename {assets => core/assets}/icons/aws/Storage/EFS.svg (100%) rename {assets => core/assets}/icons/aws/Storage/Elastic-Block-Store.png (100%) rename {assets => core/assets}/icons/aws/Storage/Elastic-Block-Store.svg (100%) rename {assets => core/assets}/icons/aws/Storage/Elastic-Disaster-Recovery.png (100%) rename {assets => core/assets}/icons/aws/Storage/Elastic-Disaster-Recovery.svg (100%) rename {assets => core/assets}/icons/aws/Storage/FSx-for-Lustre.png (100%) rename {assets => core/assets}/icons/aws/Storage/FSx-for-Lustre.svg (100%) rename {assets => core/assets}/icons/aws/Storage/FSx-for-NetApp-ONTAP.png (100%) rename {assets => core/assets}/icons/aws/Storage/FSx-for-NetApp-ONTAP.svg (100%) rename {assets => core/assets}/icons/aws/Storage/FSx-for-OpenZFS.png (100%) rename {assets => core/assets}/icons/aws/Storage/FSx-for-OpenZFS.svg (100%) rename {assets => core/assets}/icons/aws/Storage/FSx-for-WFS.png (100%) rename {assets => core/assets}/icons/aws/Storage/FSx-for-WFS.svg (100%) rename {assets => core/assets}/icons/aws/Storage/FSx.png (100%) rename {assets => core/assets}/icons/aws/Storage/FSx.svg (100%) rename {assets => core/assets}/icons/aws/Storage/File-Cache.png (100%) rename {assets => core/assets}/icons/aws/Storage/File-Cache.svg (100%) rename {assets => core/assets}/icons/aws/Storage/S3-on-Outposts.png (100%) rename {assets => core/assets}/icons/aws/Storage/S3-on-Outposts.svg (100%) rename {assets => core/assets}/icons/aws/Storage/Simple-Storage-Service-Glacier.png (100%) rename {assets => core/assets}/icons/aws/Storage/Simple-Storage-Service-Glacier.svg (100%) rename {assets => core/assets}/icons/aws/Storage/Simple-Storage-Service.png (100%) rename {assets => core/assets}/icons/aws/Storage/Simple-Storage-Service.svg (100%) rename {assets => core/assets}/icons/aws/Storage/Snowball-Edge.png (100%) rename {assets => core/assets}/icons/aws/Storage/Snowball-Edge.svg (100%) rename {assets => core/assets}/icons/aws/Storage/Snowball.png (100%) rename {assets => core/assets}/icons/aws/Storage/Snowball.svg (100%) rename {assets => core/assets}/icons/aws/Storage/Snowcone.png (100%) rename {assets => core/assets}/icons/aws/Storage/Snowcone.svg (100%) rename {assets => core/assets}/icons/aws/Storage/Snowmobile.png (100%) rename {assets => core/assets}/icons/aws/Storage/Snowmobile.svg (100%) rename {assets => core/assets}/icons/aws/Storage/Storage-Gateway.png (100%) rename {assets => core/assets}/icons/aws/Storage/Storage-Gateway.svg (100%) rename {assets => core/assets}/icons/aws/VR-AR/Sumerian.png (100%) rename {assets => core/assets}/icons/aws/VR-AR/Sumerian.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00028-icon-service-Batch-AI.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00028-icon-service-Batch-AI.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00031-icon-service-Genomics.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00031-icon-service-Genomics.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00792-icon-service-Computer-Vision.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00792-icon-service-Computer-Vision.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00793-icon-service-Custom-Vision.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00793-icon-service-Custom-Vision.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00794-icon-service-Face-APIs.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00794-icon-service-Face-APIs.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00795-icon-service-Content-Moderators.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00795-icon-service-Content-Moderators.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00796-icon-service-Personalizers.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00796-icon-service-Personalizers.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00797-icon-service-Speech-Services.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00797-icon-service-Speech-Services.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00799-icon-service-QnA-Makers.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00799-icon-service-QnA-Makers.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00800-icon-service-Translator-Text.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00800-icon-service-Translator-Text.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00801-icon-service-Language-Understanding.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00801-icon-service-Language-Understanding.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00812-icon-service-Immersive-Readers.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00812-icon-service-Immersive-Readers.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00814-icon-service-Anomaly-Detector.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00814-icon-service-Anomaly-Detector.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00819-icon-service-Form-Recognizers.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/00819-icon-service-Form-Recognizers.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/01239-icon-service-Azure-Experimentation-Studio.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/01239-icon-service-Azure-Experimentation-Studio.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/01688-icon-service-Azure-Object-Understanding.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/01688-icon-service-Azure-Object-Understanding.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/02409-icon-service-Metrics-Advisor.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/02409-icon-service-Metrics-Advisor.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/02749-icon-service-Azure-Applied-AI-Services.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/02749-icon-service-Azure-Applied-AI-Services.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/02876-icon-service-Language.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/02876-icon-service-Language.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/029455661-icon-service-FRD-QA.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/029455661-icon-service-FRD-QA.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/03173-icon-service-Cognitive-Services-Decisions.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/03173-icon-service-Cognitive-Services-Decisions.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/03321-icon-service-Serverless-Search.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/03321-icon-service-Serverless-Search.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/03337-icon-service-Bonsai.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/03337-icon-service-Bonsai.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/03390-icon-service-Content-Safety.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/03390-icon-service-Content-Safety.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/03438-icon-service-Azure-OpenAI.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/03438-icon-service-Azure-OpenAI.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/03513-icon-service-AI-Studio.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/03513-icon-service-AI-Studio.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/035746832-icon-service-AI-Foundry.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/035746832-icon-service-AI-Foundry.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/036509374-icon-service-Foundry-Application.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/036509374-icon-service-Foundry-Application.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/036509415-icon-service-Foundry-Project.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/036509415-icon-service-Foundry-Project.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/03755-icon-service-Planetary-Computer-Pro.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/03755-icon-service-Planetary-Computer-Pro.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/038470497-icon-service-Azure-AI-Foundry-IQ.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/038470497-icon-service-Azure-AI-Foundry-IQ.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/038470510-icon-service-Azure-Machine-Learning.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/038470510-icon-service-Azure-Machine-Learning.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/038470523-icon-service-Foundry-Agent-Service.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/038470523-icon-service-Foundry-Agent-Service.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/038470534-icon-service-Foundry-Control-Plane.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/038470534-icon-service-Foundry-Control-Plane.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/038470555-icon-service-Foundry-Labs.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/038470555-icon-service-Foundry-Labs.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/038470593-icon-service-Foundry-Local.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/038470593-icon-service-Foundry-Local.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/038470614-icon-service-Foundry-Models.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/038470614-icon-service-Foundry-Models.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/10044-icon-service-Cognitive-Search.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/10044-icon-service-Cognitive-Search.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/10162-icon-service-Cognitive-Services.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/10162-icon-service-Cognitive-Services.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/10164-icon-service-Genomics-Accounts.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/10164-icon-service-Genomics-Accounts.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/10165-icon-service-Bot-Services.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/10165-icon-service-Bot-Services.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/10166-icon-service-Machine-Learning.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/10166-icon-service-Machine-Learning.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/10167-icon-service-Machine-Learning-Studio-Workspaces.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/10167-icon-service-Machine-Learning-Studio-Workspaces.svg (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.png (100%) rename {assets => core/assets}/icons/azure/ai-machine-learning/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.svg (100%) rename {assets => core/assets}/icons/azure/analytics/00009-icon-service-Log-Analytics-Workspaces.png (100%) rename {assets => core/assets}/icons/azure/analytics/00009-icon-service-Log-Analytics-Workspaces.svg (100%) rename {assets => core/assets}/icons/azure/analytics/00039-icon-service-Event-Hubs.png (100%) rename {assets => core/assets}/icons/azure/analytics/00039-icon-service-Event-Hubs.svg (100%) rename {assets => core/assets}/icons/azure/analytics/00042-icon-service-Stream-Analytics-Jobs.png (100%) rename {assets => core/assets}/icons/azure/analytics/00042-icon-service-Stream-Analytics-Jobs.svg (100%) rename {assets => core/assets}/icons/azure/analytics/00562-icon-service-Endpoint-Analytics.png (100%) rename {assets => core/assets}/icons/azure/analytics/00562-icon-service-Endpoint-Analytics.svg (100%) rename {assets => core/assets}/icons/azure/analytics/00606-icon-service-Azure-Synapse-Analytics.png (100%) rename {assets => core/assets}/icons/azure/analytics/00606-icon-service-Azure-Synapse-Analytics.svg (100%) rename {assets => core/assets}/icons/azure/analytics/02189-icon-service-Azure-Workbooks.png (100%) rename {assets => core/assets}/icons/azure/analytics/02189-icon-service-Azure-Workbooks.svg (100%) rename {assets => core/assets}/icons/azure/analytics/02209-icon-service-Private-Link-Services.png (100%) rename {assets => core/assets}/icons/azure/analytics/02209-icon-service-Private-Link-Services.svg (100%) rename {assets => core/assets}/icons/azure/analytics/03332-icon-service-Power-BI-Embedded.png (100%) rename {assets => core/assets}/icons/azure/analytics/03332-icon-service-Power-BI-Embedded.svg (100%) rename {assets => core/assets}/icons/azure/analytics/03335-icon-service-Power-Platform.png (100%) rename {assets => core/assets}/icons/azure/analytics/03335-icon-service-Power-Platform.svg (100%) rename {assets => core/assets}/icons/azure/analytics/10126-icon-service-Data-Factories.png (100%) rename {assets => core/assets}/icons/azure/analytics/10126-icon-service-Data-Factories.svg (100%) rename {assets => core/assets}/icons/azure/analytics/10142-icon-service-HD-Insight-Clusters.png (100%) rename {assets => core/assets}/icons/azure/analytics/10142-icon-service-HD-Insight-Clusters.svg (100%) rename {assets => core/assets}/icons/azure/analytics/10143-icon-service-Data-Lake-Analytics.png (100%) rename {assets => core/assets}/icons/azure/analytics/10143-icon-service-Data-Lake-Analytics.svg (100%) rename {assets => core/assets}/icons/azure/analytics/10145-icon-service-Azure-Data-Explorer-Clusters.png (100%) rename {assets => core/assets}/icons/azure/analytics/10145-icon-service-Azure-Data-Explorer-Clusters.svg (100%) rename {assets => core/assets}/icons/azure/analytics/10148-icon-service-Analysis-Services.png (100%) rename {assets => core/assets}/icons/azure/analytics/10148-icon-service-Analysis-Services.svg (100%) rename {assets => core/assets}/icons/azure/analytics/10149-icon-service-Event-Hub-Clusters.png (100%) rename {assets => core/assets}/icons/azure/analytics/10149-icon-service-Event-Hub-Clusters.svg (100%) rename {assets => core/assets}/icons/azure/analytics/10150-icon-service-Data-Lake-Store-Gen1.png (100%) rename {assets => core/assets}/icons/azure/analytics/10150-icon-service-Data-Lake-Store-Gen1.svg (100%) rename {assets => core/assets}/icons/azure/analytics/10787-icon-service-Azure-Databricks.png (100%) rename {assets => core/assets}/icons/azure/analytics/10787-icon-service-Azure-Databricks.svg (100%) rename {assets => core/assets}/icons/azure/app-services/00046-icon-service-App-Service-Plans.png (100%) rename {assets => core/assets}/icons/azure/app-services/00046-icon-service-App-Service-Plans.svg (100%) rename {assets => core/assets}/icons/azure/app-services/00049-icon-service-App-Service-Certificates.png (100%) rename {assets => core/assets}/icons/azure/app-services/00049-icon-service-App-Service-Certificates.svg (100%) rename {assets => core/assets}/icons/azure/app-services/00050-icon-service-App-Service-Domains.png (100%) rename {assets => core/assets}/icons/azure/app-services/00050-icon-service-App-Service-Domains.svg (100%) rename {assets => core/assets}/icons/azure/app-services/00056-icon-service-CDN-Profiles.png (100%) rename {assets => core/assets}/icons/azure/app-services/00056-icon-service-CDN-Profiles.svg (100%) rename {assets => core/assets}/icons/azure/app-services/10035-icon-service-App-Services.png (100%) rename {assets => core/assets}/icons/azure/app-services/10035-icon-service-App-Services.svg (100%) rename {assets => core/assets}/icons/azure/app-services/10044-icon-service-Cognitive-Search.png (100%) rename {assets => core/assets}/icons/azure/app-services/10044-icon-service-Cognitive-Search.svg (100%) rename {assets => core/assets}/icons/azure/app-services/10045-icon-service-Notification-Hubs.png (100%) rename {assets => core/assets}/icons/azure/app-services/10045-icon-service-Notification-Hubs.svg (100%) rename {assets => core/assets}/icons/azure/app-services/10047-icon-service-App-Service-Environments.png (100%) rename {assets => core/assets}/icons/azure/app-services/10047-icon-service-App-Service-Environments.svg (100%) rename {assets => core/assets}/icons/azure/azure ecosystem/01038-icon-service-Collaborative-Service.png (100%) rename {assets => core/assets}/icons/azure/azure ecosystem/01038-icon-service-Collaborative-Service.svg (100%) rename {assets => core/assets}/icons/azure/azure ecosystem/02354-icon-service-Applens.png (100%) rename {assets => core/assets}/icons/azure/azure ecosystem/02354-icon-service-Applens.svg (100%) rename {assets => core/assets}/icons/azure/azure ecosystem/02573-icon-service-Azure-Hybrid-Center.png (100%) rename {assets => core/assets}/icons/azure/azure ecosystem/02573-icon-service-Azure-Hybrid-Center.svg (100%) rename {assets => core/assets}/icons/azure/azure stack/00965-icon-service-Multi-Tenancy.png (100%) rename {assets => core/assets}/icons/azure/azure stack/00965-icon-service-Multi-Tenancy.svg (100%) rename {assets => core/assets}/icons/azure/azure stack/10108-icon-service-Infrastructure-Backup.png (100%) rename {assets => core/assets}/icons/azure/azure stack/10108-icon-service-Infrastructure-Backup.svg (100%) rename {assets => core/assets}/icons/azure/azure stack/10109-icon-service-Capacity.png (100%) rename {assets => core/assets}/icons/azure/azure stack/10109-icon-service-Capacity.svg (100%) rename {assets => core/assets}/icons/azure/azure stack/10110-icon-service-Offers.png (100%) rename {assets => core/assets}/icons/azure/azure stack/10110-icon-service-Offers.svg (100%) rename {assets => core/assets}/icons/azure/azure stack/10111-icon-service-User-Subscriptions.png (100%) rename {assets => core/assets}/icons/azure/azure stack/10111-icon-service-User-Subscriptions.svg (100%) rename {assets => core/assets}/icons/azure/azure stack/10113-icon-service-Plans.png (100%) rename {assets => core/assets}/icons/azure/azure stack/10113-icon-service-Plans.svg (100%) rename {assets => core/assets}/icons/azure/azure stack/10115-icon-service-Updates.png (100%) rename {assets => core/assets}/icons/azure/azure stack/10115-icon-service-Updates.svg (100%) rename {assets => core/assets}/icons/azure/blockchain/10210-icon-service-Blockchain-Applications.png (100%) rename {assets => core/assets}/icons/azure/blockchain/10210-icon-service-Blockchain-Applications.svg (100%) rename {assets => core/assets}/icons/azure/blockchain/10364-icon-service-Outbound-Connection.png (100%) rename {assets => core/assets}/icons/azure/blockchain/10364-icon-service-Outbound-Connection.svg (100%) rename {assets => core/assets}/icons/azure/blockchain/10366-icon-service-Azure-Blockchain-Service.png (100%) rename {assets => core/assets}/icons/azure/blockchain/10366-icon-service-Azure-Blockchain-Service.svg (100%) rename {assets => core/assets}/icons/azure/blockchain/10367-icon-service-Azure-Token-Service.png (100%) rename {assets => core/assets}/icons/azure/blockchain/10367-icon-service-Azure-Token-Service.svg (100%) rename {assets => core/assets}/icons/azure/blockchain/10374-icon-service-ABS-Member.png (100%) rename {assets => core/assets}/icons/azure/blockchain/10374-icon-service-ABS-Member.svg (100%) rename {assets => core/assets}/icons/azure/blockchain/10375-icon-service-Consortium.png (100%) rename {assets => core/assets}/icons/azure/blockchain/10375-icon-service-Consortium.svg (100%) rename {assets => core/assets}/icons/azure/compute/00195-icon-service-Maintenance-Configuration.png (100%) rename {assets => core/assets}/icons/azure/compute/00195-icon-service-Maintenance-Configuration.svg (100%) rename {assets => core/assets}/icons/azure/compute/00328-icon-service-Host-Pools.png (100%) rename {assets => core/assets}/icons/azure/compute/00328-icon-service-Host-Pools.svg (100%) rename {assets => core/assets}/icons/azure/compute/00329-icon-service-Application-Group.png (100%) rename {assets => core/assets}/icons/azure/compute/00329-icon-service-Application-Group.svg (100%) rename {assets => core/assets}/icons/azure/compute/00330-icon-service-Workspaces.png (100%) rename {assets => core/assets}/icons/azure/compute/00330-icon-service-Workspaces.svg (100%) rename {assets => core/assets}/icons/azure/compute/00398-icon-service-Disk-Encryption-Sets.png (100%) rename {assets => core/assets}/icons/azure/compute/00398-icon-service-Disk-Encryption-Sets.svg (100%) rename {assets => core/assets}/icons/azure/compute/00400-icon-service-Workspaces.png (100%) rename {assets => core/assets}/icons/azure/compute/00400-icon-service-Workspaces.svg (100%) rename {assets => core/assets}/icons/azure/compute/02112-icon-service-Automanaged-VM.png (100%) rename {assets => core/assets}/icons/azure/compute/02112-icon-service-Automanaged-VM.svg (100%) rename {assets => core/assets}/icons/azure/compute/02370-icon-service-Managed-Service-Fabric.png (100%) rename {assets => core/assets}/icons/azure/compute/02370-icon-service-Managed-Service-Fabric.svg (100%) rename {assets => core/assets}/icons/azure/compute/02409-icon-service-Metrics-Advisor.png (100%) rename {assets => core/assets}/icons/azure/compute/02409-icon-service-Metrics-Advisor.svg (100%) rename {assets => core/assets}/icons/azure/compute/02634-icon-service-Image-Templates.png (100%) rename {assets => core/assets}/icons/azure/compute/02634-icon-service-Image-Templates.svg (100%) rename {assets => core/assets}/icons/azure/compute/02817-icon-service-Restore-Points.png (100%) rename {assets => core/assets}/icons/azure/compute/02817-icon-service-Restore-Points.svg (100%) rename {assets => core/assets}/icons/azure/compute/02818-icon-service-Restore-Points-Collections.png (100%) rename {assets => core/assets}/icons/azure/compute/02818-icon-service-Restore-Points-Collections.svg (100%) rename {assets => core/assets}/icons/azure/compute/02864-icon-service-Azure-Compute-Galleries.png (100%) rename {assets => core/assets}/icons/azure/compute/02864-icon-service-Azure-Compute-Galleries.svg (100%) rename {assets => core/assets}/icons/azure/compute/030868929-icon-service-Stage-Maps.png (100%) rename {assets => core/assets}/icons/azure/compute/030868929-icon-service-Stage-Maps.svg (100%) rename {assets => core/assets}/icons/azure/compute/033049334-icon-service-Scheduled-Actions.png (100%) rename {assets => core/assets}/icons/azure/compute/033049334-icon-service-Scheduled-Actions.svg (100%) rename {assets => core/assets}/icons/azure/compute/03487-icon-service-Compute-Fleet.png (100%) rename {assets => core/assets}/icons/azure/compute/03487-icon-service-Compute-Fleet.svg (100%) rename {assets => core/assets}/icons/azure/compute/03543-icon-service-AKS-Automatic.png (100%) rename {assets => core/assets}/icons/azure/compute/03543-icon-service-AKS-Automatic.svg (100%) rename {assets => core/assets}/icons/azure/compute/03645-icon-service-Azure-Linux.png (100%) rename {assets => core/assets}/icons/azure/compute/03645-icon-service-Azure-Linux.svg (100%) rename {assets => core/assets}/icons/azure/compute/10021-icon-service-Virtual-Machine.png (100%) rename {assets => core/assets}/icons/azure/compute/10021-icon-service-Virtual-Machine.svg (100%) rename {assets => core/assets}/icons/azure/compute/10023-icon-service-Kubernetes-Services.png (100%) rename {assets => core/assets}/icons/azure/compute/10023-icon-service-Kubernetes-Services.svg (100%) rename {assets => core/assets}/icons/azure/compute/10024-icon-service-Mesh-Applications.png (100%) rename {assets => core/assets}/icons/azure/compute/10024-icon-service-Mesh-Applications.svg (100%) rename {assets => core/assets}/icons/azure/compute/10025-icon-service-Availability-Sets.png (100%) rename {assets => core/assets}/icons/azure/compute/10025-icon-service-Availability-Sets.svg (100%) rename {assets => core/assets}/icons/azure/compute/10026-icon-service-Disks-Snapshots.png (100%) rename {assets => core/assets}/icons/azure/compute/10026-icon-service-Disks-Snapshots.svg (100%) rename {assets => core/assets}/icons/azure/compute/10027-icon-service-OS-Images-(Classic).png (100%) rename {assets => core/assets}/icons/azure/compute/10027-icon-service-OS-Images-(Classic).svg (100%) rename {assets => core/assets}/icons/azure/compute/10028-icon-service-Virtual-Machines-(Classic).png (100%) rename {assets => core/assets}/icons/azure/compute/10028-icon-service-Virtual-Machines-(Classic).svg (100%) rename {assets => core/assets}/icons/azure/compute/10029-icon-service-Function-Apps.png (100%) rename {assets => core/assets}/icons/azure/compute/10029-icon-service-Function-Apps.svg (100%) rename {assets => core/assets}/icons/azure/compute/10030-icon-service-Cloud-Services-(Classic).png (100%) rename {assets => core/assets}/icons/azure/compute/10030-icon-service-Cloud-Services-(Classic).svg (100%) rename {assets => core/assets}/icons/azure/compute/10031-icon-service-Batch-Accounts.png (100%) rename {assets => core/assets}/icons/azure/compute/10031-icon-service-Batch-Accounts.svg (100%) rename {assets => core/assets}/icons/azure/compute/10032-icon-service-Disks.png (100%) rename {assets => core/assets}/icons/azure/compute/10032-icon-service-Disks.svg (100%) rename {assets => core/assets}/icons/azure/compute/10033-icon-service-Images.png (100%) rename {assets => core/assets}/icons/azure/compute/10033-icon-service-Images.svg (100%) rename {assets => core/assets}/icons/azure/compute/10034-icon-service-VM-Scale-Sets.png (100%) rename {assets => core/assets}/icons/azure/compute/10034-icon-service-VM-Scale-Sets.svg (100%) rename {assets => core/assets}/icons/azure/compute/10035-icon-service-App-Services.png (100%) rename {assets => core/assets}/icons/azure/compute/10035-icon-service-App-Services.svg (100%) rename {assets => core/assets}/icons/azure/compute/10036-icon-service-Service-Fabric-Clusters.png (100%) rename {assets => core/assets}/icons/azure/compute/10036-icon-service-Service-Fabric-Clusters.svg (100%) rename {assets => core/assets}/icons/azure/compute/10037-icon-service-Image-Definitions.png (100%) rename {assets => core/assets}/icons/azure/compute/10037-icon-service-Image-Definitions.svg (100%) rename {assets => core/assets}/icons/azure/compute/10038-icon-service-Image-Versions.png (100%) rename {assets => core/assets}/icons/azure/compute/10038-icon-service-Image-Versions.svg (100%) rename {assets => core/assets}/icons/azure/compute/10039-icon-service-Shared-Image-Galleries.png (100%) rename {assets => core/assets}/icons/azure/compute/10039-icon-service-Shared-Image-Galleries.svg (100%) rename {assets => core/assets}/icons/azure/compute/10040-icon-service-VM-Images-(Classic).png (100%) rename {assets => core/assets}/icons/azure/compute/10040-icon-service-VM-Images-(Classic).svg (100%) rename {assets => core/assets}/icons/azure/compute/10041-icon-service-Disks-(Classic).png (100%) rename {assets => core/assets}/icons/azure/compute/10041-icon-service-Disks-(Classic).svg (100%) rename {assets => core/assets}/icons/azure/compute/10049-icon-service-Container-Services-(Deprecated).png (100%) rename {assets => core/assets}/icons/azure/compute/10049-icon-service-Container-Services-(Deprecated).svg (100%) rename {assets => core/assets}/icons/azure/compute/10104-icon-service-Container-Instances.png (100%) rename {assets => core/assets}/icons/azure/compute/10104-icon-service-Container-Instances.svg (100%) rename {assets => core/assets}/icons/azure/compute/10346-icon-service-Host-Groups.png (100%) rename {assets => core/assets}/icons/azure/compute/10346-icon-service-Host-Groups.svg (100%) rename {assets => core/assets}/icons/azure/compute/10347-icon-service-Hosts.png (100%) rename {assets => core/assets}/icons/azure/compute/10347-icon-service-Hosts.svg (100%) rename {assets => core/assets}/icons/azure/compute/10370-icon-service-Azure-Spring-Apps.png (100%) rename {assets => core/assets}/icons/azure/compute/10370-icon-service-Azure-Spring-Apps.svg (100%) rename {assets => core/assets}/icons/azure/containers/029637767-icon-service-Service-Groups.png (100%) rename {assets => core/assets}/icons/azure/containers/029637767-icon-service-Service-Groups.svg (100%) rename {assets => core/assets}/icons/azure/containers/032487601-icon-service-AKS-Network-Policy.png (100%) rename {assets => core/assets}/icons/azure/containers/032487601-icon-service-AKS-Network-Policy.svg (100%) rename {assets => core/assets}/icons/azure/containers/03331-icon-service-Azure-Red-Hat-OpenShift.png (100%) rename {assets => core/assets}/icons/azure/containers/03331-icon-service-Azure-Red-Hat-OpenShift.svg (100%) rename {assets => core/assets}/icons/azure/containers/033545113-icon-service-Kubernetes-Hub.png (100%) rename {assets => core/assets}/icons/azure/containers/033545113-icon-service-Kubernetes-Hub.svg (100%) rename {assets => core/assets}/icons/azure/containers/03401-icon-service-Azure-Container-Storage.png (100%) rename {assets => core/assets}/icons/azure/containers/03401-icon-service-Azure-Container-Storage.svg (100%) rename {assets => core/assets}/icons/azure/containers/10023-icon-service-Kubernetes-Services.png (100%) rename {assets => core/assets}/icons/azure/containers/10023-icon-service-Kubernetes-Services.svg (100%) rename {assets => core/assets}/icons/azure/containers/10031-icon-service-Batch-Accounts.png (100%) rename {assets => core/assets}/icons/azure/containers/10031-icon-service-Batch-Accounts.svg (100%) rename {assets => core/assets}/icons/azure/containers/10035-icon-service-App-Services.png (100%) rename {assets => core/assets}/icons/azure/containers/10035-icon-service-App-Services.svg (100%) rename {assets => core/assets}/icons/azure/containers/10036-icon-service-Service-Fabric-Clusters.png (100%) rename {assets => core/assets}/icons/azure/containers/10036-icon-service-Service-Fabric-Clusters.svg (100%) rename {assets => core/assets}/icons/azure/containers/10104-icon-service-Container-Instances.png (100%) rename {assets => core/assets}/icons/azure/containers/10104-icon-service-Container-Instances.svg (100%) rename {assets => core/assets}/icons/azure/containers/10105-icon-service-Container-Registries.png (100%) rename {assets => core/assets}/icons/azure/containers/10105-icon-service-Container-Registries.svg (100%) rename {assets => core/assets}/icons/azure/databases/00036-icon-service-SQL-Data-Warehouses.png (100%) rename {assets => core/assets}/icons/azure/databases/00036-icon-service-SQL-Data-Warehouses.svg (100%) rename {assets => core/assets}/icons/azure/databases/00606-icon-service-Azure-Synapse-Analytics.png (100%) rename {assets => core/assets}/icons/azure/databases/00606-icon-service-Azure-Synapse-Analytics.svg (100%) rename {assets => core/assets}/icons/azure/databases/02390-icon-service-Azure-SQL.png (100%) rename {assets => core/assets}/icons/azure/databases/02390-icon-service-Azure-SQL.svg (100%) rename {assets => core/assets}/icons/azure/databases/02392-icon-service-SSIS-Lift-And-Shift-IR.png (100%) rename {assets => core/assets}/icons/azure/databases/02392-icon-service-SSIS-Lift-And-Shift-IR.svg (100%) rename {assets => core/assets}/icons/azure/databases/02517-icon-service-Azure-Purview-Accounts.png (100%) rename {assets => core/assets}/icons/azure/databases/02517-icon-service-Azure-Purview-Accounts.svg (100%) rename {assets => core/assets}/icons/azure/databases/02750-icon-service-Azure-SQL-Edge.png (100%) rename {assets => core/assets}/icons/azure/databases/02750-icon-service-Azure-SQL-Edge.svg (100%) rename {assets => core/assets}/icons/azure/databases/02827-icon-service-Azure-Database-PostgreSQL-Server-Group.png (100%) rename {assets => core/assets}/icons/azure/databases/02827-icon-service-Azure-Database-PostgreSQL-Server-Group.svg (100%) rename {assets => core/assets}/icons/azure/databases/03490-icon-service-Oracle-Database.png (100%) rename {assets => core/assets}/icons/azure/databases/03490-icon-service-Oracle-Database.svg (100%) rename {assets => core/assets}/icons/azure/databases/03565-icon-service-SQL-Database-Fleet-Manager.png (100%) rename {assets => core/assets}/icons/azure/databases/03565-icon-service-SQL-Database-Fleet-Manager.svg (100%) rename {assets => core/assets}/icons/azure/databases/035656437-icon-service-Azure-DocumentDB.png (100%) rename {assets => core/assets}/icons/azure/databases/035656437-icon-service-Azure-DocumentDB.svg (100%) rename {assets => core/assets}/icons/azure/databases/03675-icon-service-Azure-Managed-Redis.png (100%) rename {assets => core/assets}/icons/azure/databases/03675-icon-service-Azure-Managed-Redis.svg (100%) rename {assets => core/assets}/icons/azure/databases/10121-icon-service-Azure-Cosmos-DB.png (100%) rename {assets => core/assets}/icons/azure/databases/10121-icon-service-Azure-Cosmos-DB.svg (100%) rename {assets => core/assets}/icons/azure/databases/10122-icon-service-Azure-Database-MySQL-Server.png (100%) rename {assets => core/assets}/icons/azure/databases/10122-icon-service-Azure-Database-MySQL-Server.svg (100%) rename {assets => core/assets}/icons/azure/databases/10123-icon-service-Azure-Database-MariaDB-Server.png (100%) rename {assets => core/assets}/icons/azure/databases/10123-icon-service-Azure-Database-MariaDB-Server.svg (100%) rename {assets => core/assets}/icons/azure/databases/10124-icon-service-Azure-SQL-VM.png (100%) rename {assets => core/assets}/icons/azure/databases/10124-icon-service-Azure-SQL-VM.svg (100%) rename {assets => core/assets}/icons/azure/databases/10126-icon-service-Data-Factories.png (100%) rename {assets => core/assets}/icons/azure/databases/10126-icon-service-Data-Factories.svg (100%) rename {assets => core/assets}/icons/azure/databases/10127-icon-service-Virtual-Clusters.png (100%) rename {assets => core/assets}/icons/azure/databases/10127-icon-service-Virtual-Clusters.svg (100%) rename {assets => core/assets}/icons/azure/databases/10128-icon-service-Elastic-Job-Agents.png (100%) rename {assets => core/assets}/icons/azure/databases/10128-icon-service-Elastic-Job-Agents.svg (100%) rename {assets => core/assets}/icons/azure/databases/10130-icon-service-SQL-Database.png (100%) rename {assets => core/assets}/icons/azure/databases/10130-icon-service-SQL-Database.svg (100%) rename {assets => core/assets}/icons/azure/databases/10131-icon-service-Azure-Database-PostgreSQL-Server.png (100%) rename {assets => core/assets}/icons/azure/databases/10131-icon-service-Azure-Database-PostgreSQL-Server.svg (100%) rename {assets => core/assets}/icons/azure/databases/10132-icon-service-SQL-Server.png (100%) rename {assets => core/assets}/icons/azure/databases/10132-icon-service-SQL-Server.svg (100%) rename {assets => core/assets}/icons/azure/databases/10133-icon-service-Azure-Database-Migration-Services.png (100%) rename {assets => core/assets}/icons/azure/databases/10133-icon-service-Azure-Database-Migration-Services.svg (100%) rename {assets => core/assets}/icons/azure/databases/10134-icon-service-SQL-Elastic-Pools.png (100%) rename {assets => core/assets}/icons/azure/databases/10134-icon-service-SQL-Elastic-Pools.svg (100%) rename {assets => core/assets}/icons/azure/databases/10135-icon-service-Managed-Database.png (100%) rename {assets => core/assets}/icons/azure/databases/10135-icon-service-Managed-Database.svg (100%) rename {assets => core/assets}/icons/azure/databases/10136-icon-service-SQL-Managed-Instance.png (100%) rename {assets => core/assets}/icons/azure/databases/10136-icon-service-SQL-Managed-Instance.svg (100%) rename {assets => core/assets}/icons/azure/databases/10137-icon-service-Azure-SQL-Server-Stretch-Databases.png (100%) rename {assets => core/assets}/icons/azure/databases/10137-icon-service-Azure-SQL-Server-Stretch-Databases.svg (100%) rename {assets => core/assets}/icons/azure/databases/10137-icon-service-Cache-Redis.png (100%) rename {assets => core/assets}/icons/azure/databases/10137-icon-service-Cache-Redis.svg (100%) rename {assets => core/assets}/icons/azure/databases/10139-icon-service-Instance-Pools.png (100%) rename {assets => core/assets}/icons/azure/databases/10139-icon-service-Instance-Pools.svg (100%) rename {assets => core/assets}/icons/azure/databases/10145-icon-service-Azure-Data-Explorer-Clusters.png (100%) rename {assets => core/assets}/icons/azure/databases/10145-icon-service-Azure-Data-Explorer-Clusters.svg (100%) rename {assets => core/assets}/icons/azure/databases/10351-icon-service-SQL-Server-Registries.png (100%) rename {assets => core/assets}/icons/azure/databases/10351-icon-service-SQL-Server-Registries.svg (100%) rename {assets => core/assets}/icons/azure/developer-tools/033805167-icon-service-Azure-App-Testing.png (100%) rename {assets => core/assets}/icons/azure/developer-tools/033805167-icon-service-Azure-App-Testing.svg (100%) rename {assets => core/assets}/icons/azure/devops/00012-icon-service-Application-Insights.png (100%) rename {assets => core/assets}/icons/azure/devops/00012-icon-service-Application-Insights.svg (100%) rename {assets => core/assets}/icons/azure/devops/00563-icon-service-Change-Analysis.png (100%) rename {assets => core/assets}/icons/azure/devops/00563-icon-service-Change-Analysis.svg (100%) rename {assets => core/assets}/icons/azure/devops/02373-icon-service-CloudTest.png (100%) rename {assets => core/assets}/icons/azure/devops/02373-icon-service-CloudTest.svg (100%) rename {assets => core/assets}/icons/azure/devops/02423-icon-service-Load-Testing.png (100%) rename {assets => core/assets}/icons/azure/devops/02423-icon-service-Load-Testing.svg (100%) rename {assets => core/assets}/icons/azure/devops/02761-icon-service-Lab-Accounts.png (100%) rename {assets => core/assets}/icons/azure/devops/02761-icon-service-Lab-Accounts.svg (100%) rename {assets => core/assets}/icons/azure/devops/03339-icon-service-DevOps-Starter.png (100%) rename {assets => core/assets}/icons/azure/devops/03339-icon-service-DevOps-Starter.svg (100%) rename {assets => core/assets}/icons/azure/devops/03393-icon-service-Managed-DevOps-Pools.png (100%) rename {assets => core/assets}/icons/azure/devops/03393-icon-service-Managed-DevOps-Pools.svg (100%) rename {assets => core/assets}/icons/azure/devops/03455-icon-service-Code-Optimization.png (100%) rename {assets => core/assets}/icons/azure/devops/03455-icon-service-Code-Optimization.svg (100%) rename {assets => core/assets}/icons/azure/devops/03623-icon-service-Workspace-Gateway.png (100%) rename {assets => core/assets}/icons/azure/devops/03623-icon-service-Workspace-Gateway.svg (100%) rename {assets => core/assets}/icons/azure/devops/10042-icon-service-API-Management-Services.png (100%) rename {assets => core/assets}/icons/azure/devops/10042-icon-service-API-Management-Services.svg (100%) rename {assets => core/assets}/icons/azure/devops/10048-icon-service-API-Connections.png (100%) rename {assets => core/assets}/icons/azure/devops/10048-icon-service-API-Connections.svg (100%) rename {assets => core/assets}/icons/azure/devops/10261-icon-service-Azure-DevOps.png (100%) rename {assets => core/assets}/icons/azure/devops/10261-icon-service-Azure-DevOps.svg (100%) rename {assets => core/assets}/icons/azure/devops/10264-icon-service-DevTest-Labs.png (100%) rename {assets => core/assets}/icons/azure/devops/10264-icon-service-DevTest-Labs.svg (100%) rename {assets => core/assets}/icons/azure/devops/10265-icon-service-Lab-Services.png (100%) rename {assets => core/assets}/icons/azure/devops/10265-icon-service-Lab-Services.svg (100%) rename {assets => core/assets}/icons/azure/general/00004-icon-service-Cost-Management-and-Billing.png (100%) rename {assets => core/assets}/icons/azure/general/00004-icon-service-Cost-Management-and-Billing.svg (100%) rename {assets => core/assets}/icons/azure/general/00456-icon-service-Preview-Features.png (100%) rename {assets => core/assets}/icons/azure/general/00456-icon-service-Preview-Features.svg (100%) rename {assets => core/assets}/icons/azure/general/10001-icon-service-All-Resources.png (100%) rename {assets => core/assets}/icons/azure/general/10001-icon-service-All-Resources.svg (100%) rename {assets => core/assets}/icons/azure/general/10002-icon-service-Subscriptions.png (100%) rename {assets => core/assets}/icons/azure/general/10002-icon-service-Subscriptions.svg (100%) rename {assets => core/assets}/icons/azure/general/10003-icon-service-Reservations.png (100%) rename {assets => core/assets}/icons/azure/general/10003-icon-service-Reservations.svg (100%) rename {assets => core/assets}/icons/azure/general/10004-icon-service-Service-Health.png (100%) rename {assets => core/assets}/icons/azure/general/10004-icon-service-Service-Health.svg (100%) rename {assets => core/assets}/icons/azure/general/10005-icon-service-Information.png (100%) rename {assets => core/assets}/icons/azure/general/10005-icon-service-Information.svg (100%) rename {assets => core/assets}/icons/azure/general/10006-icon-service-Recent.png (100%) rename {assets => core/assets}/icons/azure/general/10006-icon-service-Recent.svg (100%) rename {assets => core/assets}/icons/azure/general/10007-icon-service-Resource-Groups.png (100%) rename {assets => core/assets}/icons/azure/general/10007-icon-service-Resource-Groups.svg (100%) rename {assets => core/assets}/icons/azure/general/10008-icon-service-Marketplace.png (100%) rename {assets => core/assets}/icons/azure/general/10008-icon-service-Marketplace.svg (100%) rename {assets => core/assets}/icons/azure/general/10009-icon-service-Templates.png (100%) rename {assets => core/assets}/icons/azure/general/10009-icon-service-Templates.svg (100%) rename {assets => core/assets}/icons/azure/general/10010-icon-service-Quickstart-Center.png (100%) rename {assets => core/assets}/icons/azure/general/10010-icon-service-Quickstart-Center.svg (100%) rename {assets => core/assets}/icons/azure/general/10011-icon-service-Management-Groups.png (100%) rename {assets => core/assets}/icons/azure/general/10011-icon-service-Management-Groups.svg (100%) rename {assets => core/assets}/icons/azure/general/10013-icon-service-Help-and-Support.png (100%) rename {assets => core/assets}/icons/azure/general/10013-icon-service-Help-and-Support.svg (100%) rename {assets => core/assets}/icons/azure/general/10014-icon-service-Tag.png (100%) rename {assets => core/assets}/icons/azure/general/10014-icon-service-Tag.svg (100%) rename {assets => core/assets}/icons/azure/general/10015-icon-service-Dashboard.png (100%) rename {assets => core/assets}/icons/azure/general/10015-icon-service-Dashboard.svg (100%) rename {assets => core/assets}/icons/azure/general/10016-icon-service-Free-Services.png (100%) rename {assets => core/assets}/icons/azure/general/10016-icon-service-Free-Services.svg (100%) rename {assets => core/assets}/icons/azure/general/10019-icon-service-Cost-Management.png (100%) rename {assets => core/assets}/icons/azure/general/10019-icon-service-Cost-Management.svg (100%) rename {assets => core/assets}/icons/azure/general/10112-icon-service-Marketplace-Management.png (100%) rename {assets => core/assets}/icons/azure/general/10112-icon-service-Marketplace-Management.svg (100%) rename {assets => core/assets}/icons/azure/general/10116-icon-service-Region-Management.png (100%) rename {assets => core/assets}/icons/azure/general/10116-icon-service-Region-Management.svg (100%) rename {assets => core/assets}/icons/azure/general/10341-icon-service-Troubleshoot.png (100%) rename {assets => core/assets}/icons/azure/general/10341-icon-service-Troubleshoot.svg (100%) rename {assets => core/assets}/icons/azure/general/10349-icon-service-Resource-Explorer.png (100%) rename {assets => core/assets}/icons/azure/general/10349-icon-service-Resource-Explorer.svg (100%) rename {assets => core/assets}/icons/azure/general/10779-icon-service-Biz-Talk.png (100%) rename {assets => core/assets}/icons/azure/general/10779-icon-service-Biz-Talk.svg (100%) rename {assets => core/assets}/icons/azure/general/10780-icon-service-Blob-Block.png (100%) rename {assets => core/assets}/icons/azure/general/10780-icon-service-Blob-Block.svg (100%) rename {assets => core/assets}/icons/azure/general/10781-icon-service-Blob-Page.png (100%) rename {assets => core/assets}/icons/azure/general/10781-icon-service-Blob-Page.svg (100%) rename {assets => core/assets}/icons/azure/general/10782-icon-service-Branch.png (100%) rename {assets => core/assets}/icons/azure/general/10782-icon-service-Branch.svg (100%) rename {assets => core/assets}/icons/azure/general/10783-icon-service-Browser.png (100%) rename {assets => core/assets}/icons/azure/general/10783-icon-service-Browser.svg (100%) rename {assets => core/assets}/icons/azure/general/10784-icon-service-Bug.png (100%) rename {assets => core/assets}/icons/azure/general/10784-icon-service-Bug.svg (100%) rename {assets => core/assets}/icons/azure/general/10785-icon-service-Builds.png (100%) rename {assets => core/assets}/icons/azure/general/10785-icon-service-Builds.svg (100%) rename {assets => core/assets}/icons/azure/general/10786-icon-service-Cache.png (100%) rename {assets => core/assets}/icons/azure/general/10786-icon-service-Cache.svg (100%) rename {assets => core/assets}/icons/azure/general/10787-icon-service-Code.png (100%) rename {assets => core/assets}/icons/azure/general/10787-icon-service-Code.svg (100%) rename {assets => core/assets}/icons/azure/general/10788-icon-service-Commit.png (100%) rename {assets => core/assets}/icons/azure/general/10788-icon-service-Commit.svg (100%) rename {assets => core/assets}/icons/azure/general/10789-icon-service-Controls.png (100%) rename {assets => core/assets}/icons/azure/general/10789-icon-service-Controls.svg (100%) rename {assets => core/assets}/icons/azure/general/10790-icon-service-Controls-Horizontal.png (100%) rename {assets => core/assets}/icons/azure/general/10790-icon-service-Controls-Horizontal.svg (100%) rename {assets => core/assets}/icons/azure/general/10791-icon-service-Cost-Alerts.png (100%) rename {assets => core/assets}/icons/azure/general/10791-icon-service-Cost-Alerts.svg (100%) rename {assets => core/assets}/icons/azure/general/10792-icon-service-Cost-Analysis.png (100%) rename {assets => core/assets}/icons/azure/general/10792-icon-service-Cost-Analysis.svg (100%) rename {assets => core/assets}/icons/azure/general/10793-icon-service-Cost-Budgets.png (100%) rename {assets => core/assets}/icons/azure/general/10793-icon-service-Cost-Budgets.svg (100%) rename {assets => core/assets}/icons/azure/general/10794-icon-service-Counter.png (100%) rename {assets => core/assets}/icons/azure/general/10794-icon-service-Counter.svg (100%) rename {assets => core/assets}/icons/azure/general/10795-icon-service-Cubes.png (100%) rename {assets => core/assets}/icons/azure/general/10795-icon-service-Cubes.svg (100%) rename {assets => core/assets}/icons/azure/general/10796-icon-service-Dev-Console.png (100%) rename {assets => core/assets}/icons/azure/general/10796-icon-service-Dev-Console.svg (100%) rename {assets => core/assets}/icons/azure/general/10797-icon-service-Download.png (100%) rename {assets => core/assets}/icons/azure/general/10797-icon-service-Download.svg (100%) rename {assets => core/assets}/icons/azure/general/10798-icon-service-Error.png (100%) rename {assets => core/assets}/icons/azure/general/10798-icon-service-Error.svg (100%) rename {assets => core/assets}/icons/azure/general/10799-icon-service-Extensions.png (100%) rename {assets => core/assets}/icons/azure/general/10799-icon-service-Extensions.svg (100%) rename {assets => core/assets}/icons/azure/general/10800-icon-service-File.png (100%) rename {assets => core/assets}/icons/azure/general/10800-icon-service-File.svg (100%) rename {assets => core/assets}/icons/azure/general/10801-icon-service-Files.png (100%) rename {assets => core/assets}/icons/azure/general/10801-icon-service-Files.svg (100%) rename {assets => core/assets}/icons/azure/general/10802-icon-service-Folder-Blank.png (100%) rename {assets => core/assets}/icons/azure/general/10802-icon-service-Folder-Blank.svg (100%) rename {assets => core/assets}/icons/azure/general/10803-icon-service-Folder-Website.png (100%) rename {assets => core/assets}/icons/azure/general/10803-icon-service-Folder-Website.svg (100%) rename {assets => core/assets}/icons/azure/general/10804-icon-service-FTP.png (100%) rename {assets => core/assets}/icons/azure/general/10804-icon-service-FTP.svg (100%) rename {assets => core/assets}/icons/azure/general/10805-icon-service-Gear.png (100%) rename {assets => core/assets}/icons/azure/general/10805-icon-service-Gear.svg (100%) rename {assets => core/assets}/icons/azure/general/10807-icon-service-Globe-Error.png (100%) rename {assets => core/assets}/icons/azure/general/10807-icon-service-Globe-Error.svg (100%) rename {assets => core/assets}/icons/azure/general/10808-icon-service-Globe-Success.png (100%) rename {assets => core/assets}/icons/azure/general/10808-icon-service-Globe-Success.svg (100%) rename {assets => core/assets}/icons/azure/general/10809-icon-service-Globe-Warning.png (100%) rename {assets => core/assets}/icons/azure/general/10809-icon-service-Globe-Warning.svg (100%) rename {assets => core/assets}/icons/azure/general/10810-icon-service-Guide.png (100%) rename {assets => core/assets}/icons/azure/general/10810-icon-service-Guide.svg (100%) rename {assets => core/assets}/icons/azure/general/10811-icon-service-Heart.png (100%) rename {assets => core/assets}/icons/azure/general/10811-icon-service-Heart.svg (100%) rename {assets => core/assets}/icons/azure/general/10812-icon-service-Image.png (100%) rename {assets => core/assets}/icons/azure/general/10812-icon-service-Image.svg (100%) rename {assets => core/assets}/icons/azure/general/10813-icon-service-Input-Output.png (100%) rename {assets => core/assets}/icons/azure/general/10813-icon-service-Input-Output.svg (100%) rename {assets => core/assets}/icons/azure/general/10814-icon-service-Journey-Hub.png (100%) rename {assets => core/assets}/icons/azure/general/10814-icon-service-Journey-Hub.svg (100%) rename {assets => core/assets}/icons/azure/general/10815-icon-service-Launch-Portal.png (100%) rename {assets => core/assets}/icons/azure/general/10815-icon-service-Launch-Portal.svg (100%) rename {assets => core/assets}/icons/azure/general/10816-icon-service-Learn.png (100%) rename {assets => core/assets}/icons/azure/general/10816-icon-service-Learn.svg (100%) rename {assets => core/assets}/icons/azure/general/10817-icon-service-Load-Test.png (100%) rename {assets => core/assets}/icons/azure/general/10817-icon-service-Load-Test.svg (100%) rename {assets => core/assets}/icons/azure/general/10818-icon-service-Location.png (100%) rename {assets => core/assets}/icons/azure/general/10818-icon-service-Location.svg (100%) rename {assets => core/assets}/icons/azure/general/10819-icon-service-Log-Streaming.png (100%) rename {assets => core/assets}/icons/azure/general/10819-icon-service-Log-Streaming.svg (100%) rename {assets => core/assets}/icons/azure/general/10820-icon-service-Management-Portal.png (100%) rename {assets => core/assets}/icons/azure/general/10820-icon-service-Management-Portal.svg (100%) rename {assets => core/assets}/icons/azure/general/10821-icon-service-Media-File.png (100%) rename {assets => core/assets}/icons/azure/general/10821-icon-service-Media-File.svg (100%) rename {assets => core/assets}/icons/azure/general/10822-icon-service-Mobile.png (100%) rename {assets => core/assets}/icons/azure/general/10822-icon-service-Mobile.svg (100%) rename {assets => core/assets}/icons/azure/general/10823-icon-service-Mobile-Engagement.png (100%) rename {assets => core/assets}/icons/azure/general/10823-icon-service-Mobile-Engagement.svg (100%) rename {assets => core/assets}/icons/azure/general/10824-icon-service-Power.png (100%) rename {assets => core/assets}/icons/azure/general/10824-icon-service-Power.svg (100%) rename {assets => core/assets}/icons/azure/general/10825-icon-service-Powershell.png (100%) rename {assets => core/assets}/icons/azure/general/10825-icon-service-Powershell.svg (100%) rename {assets => core/assets}/icons/azure/general/10826-icon-service-Power-Up.png (100%) rename {assets => core/assets}/icons/azure/general/10826-icon-service-Power-Up.svg (100%) rename {assets => core/assets}/icons/azure/general/10827-icon-service-Feature-Previews.png (100%) rename {assets => core/assets}/icons/azure/general/10827-icon-service-Feature-Previews.svg (100%) rename {assets => core/assets}/icons/azure/general/10828-icon-service-Process-Explorer.png (100%) rename {assets => core/assets}/icons/azure/general/10828-icon-service-Process-Explorer.svg (100%) rename {assets => core/assets}/icons/azure/general/10829-icon-service-Production-Ready-Database.png (100%) rename {assets => core/assets}/icons/azure/general/10829-icon-service-Production-Ready-Database.svg (100%) rename {assets => core/assets}/icons/azure/general/10830-icon-service-Resource-Group-List.png (100%) rename {assets => core/assets}/icons/azure/general/10830-icon-service-Resource-Group-List.svg (100%) rename {assets => core/assets}/icons/azure/general/10831-icon-service-Resource-Linked.png (100%) rename {assets => core/assets}/icons/azure/general/10831-icon-service-Resource-Linked.svg (100%) rename {assets => core/assets}/icons/azure/general/10833-icon-service-Scheduler.png (100%) rename {assets => core/assets}/icons/azure/general/10833-icon-service-Scheduler.svg (100%) rename {assets => core/assets}/icons/azure/general/10834-icon-service-Search.png (100%) rename {assets => core/assets}/icons/azure/general/10834-icon-service-Search.svg (100%) rename {assets => core/assets}/icons/azure/general/10835-icon-service-Server-Farm.png (100%) rename {assets => core/assets}/icons/azure/general/10835-icon-service-Server-Farm.svg (100%) rename {assets => core/assets}/icons/azure/general/10837-icon-service-SSD.png (100%) rename {assets => core/assets}/icons/azure/general/10837-icon-service-SSD.svg (100%) rename {assets => core/assets}/icons/azure/general/10838-icon-service-Storage-Azure-Files.png (100%) rename {assets => core/assets}/icons/azure/general/10838-icon-service-Storage-Azure-Files.svg (100%) rename {assets => core/assets}/icons/azure/general/10839-icon-service-Storage-Container.png (100%) rename {assets => core/assets}/icons/azure/general/10839-icon-service-Storage-Container.svg (100%) rename {assets => core/assets}/icons/azure/general/10840-icon-service-Storage-Queue.png (100%) rename {assets => core/assets}/icons/azure/general/10840-icon-service-Storage-Queue.svg (100%) rename {assets => core/assets}/icons/azure/general/10841-icon-service-Table.png (100%) rename {assets => core/assets}/icons/azure/general/10841-icon-service-Table.svg (100%) rename {assets => core/assets}/icons/azure/general/10842-icon-service-Tags.png (100%) rename {assets => core/assets}/icons/azure/general/10842-icon-service-Tags.svg (100%) rename {assets => core/assets}/icons/azure/general/10843-icon-service-TFS-VC-Repository.png (100%) rename {assets => core/assets}/icons/azure/general/10843-icon-service-TFS-VC-Repository.svg (100%) rename {assets => core/assets}/icons/azure/general/10844-icon-service-Toolbox.png (100%) rename {assets => core/assets}/icons/azure/general/10844-icon-service-Toolbox.svg (100%) rename {assets => core/assets}/icons/azure/general/10845-icon-service-Versions.png (100%) rename {assets => core/assets}/icons/azure/general/10845-icon-service-Versions.svg (100%) rename {assets => core/assets}/icons/azure/general/10847-icon-service-Website-Power.png (100%) rename {assets => core/assets}/icons/azure/general/10847-icon-service-Website-Power.svg (100%) rename {assets => core/assets}/icons/azure/general/10848-icon-service-Website-Staging.png (100%) rename {assets => core/assets}/icons/azure/general/10848-icon-service-Website-Staging.svg (100%) rename {assets => core/assets}/icons/azure/general/10849-icon-service-Web-Slots.png (100%) rename {assets => core/assets}/icons/azure/general/10849-icon-service-Web-Slots.svg (100%) rename {assets => core/assets}/icons/azure/general/10850-icon-service-Web-Test.png (100%) rename {assets => core/assets}/icons/azure/general/10850-icon-service-Web-Test.svg (100%) rename {assets => core/assets}/icons/azure/general/10851-icon-service-Workbooks.png (100%) rename {assets => core/assets}/icons/azure/general/10851-icon-service-Workbooks.svg (100%) rename {assets => core/assets}/icons/azure/general/10852-icon-service-Workflow.png (100%) rename {assets => core/assets}/icons/azure/general/10852-icon-service-Workflow.svg (100%) rename {assets => core/assets}/icons/azure/general/10853-icon-service-Backlog.png (100%) rename {assets => core/assets}/icons/azure/general/10853-icon-service-Backlog.svg (100%) rename {assets => core/assets}/icons/azure/general/10854-icon-service-Media.png (100%) rename {assets => core/assets}/icons/azure/general/10854-icon-service-Media.svg (100%) rename {assets => core/assets}/icons/azure/general/10855-icon-service-Module.png (100%) rename {assets => core/assets}/icons/azure/general/10855-icon-service-Module.svg (100%) rename {assets => core/assets}/icons/azure/general/10856-icon-service-Search-Grid.png (100%) rename {assets => core/assets}/icons/azure/general/10856-icon-service-Search-Grid.svg (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/029109619-icon-service-Workload-Orchestration.png (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/029109619-icon-service-Workload-Orchestration.svg (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/03248-icon-service-Azure-Operator-5G-Core.png (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/03248-icon-service-Azure-Operator-5G-Core.svg (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/03324-icon-service-Azure-Operator-Nexus.png (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/03324-icon-service-Azure-Operator-Nexus.svg (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/03333-icon-service-Azure-Operator-Insights.png (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/03333-icon-service-Azure-Operator-Insights.svg (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/03334-icon-service-Azure-Operator-Service-Manager.png (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/03334-icon-service-Azure-Operator-Service-Manager.svg (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/03347-icon-service-Azure-Programmable-Connectivity.png (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/03347-icon-service-Azure-Programmable-Connectivity.svg (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/033745116-icon-service-Azure-Local.png (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/033745116-icon-service-Azure-Local.svg (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/03521-icon-service-Edge-Storage-Accelerator.png (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/03521-icon-service-Edge-Storage-Accelerator.svg (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/03585-icon-service-Azure-Monitor-Pipeline.png (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/03585-icon-service-Azure-Monitor-Pipeline.svg (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/03687-icon-service-AI-at-Edge.png (100%) rename {assets => core/assets}/icons/azure/hybrid-multicloud/03687-icon-service-AI-at-Edge.svg (100%) rename {assets => core/assets}/icons/azure/identity/00321-icon-service-Security.png (100%) rename {assets => core/assets}/icons/azure/identity/00321-icon-service-Security.svg (100%) rename {assets => core/assets}/icons/azure/identity/00919-icon-service-Administrative-Units.png (100%) rename {assets => core/assets}/icons/azure/identity/00919-icon-service-Administrative-Units.svg (100%) rename {assets => core/assets}/icons/azure/identity/01084-icon-service-Verifiable-Credentials.png (100%) rename {assets => core/assets}/icons/azure/identity/01084-icon-service-Verifiable-Credentials.svg (100%) rename {assets => core/assets}/icons/azure/identity/02251-icon-service-Azure-AD-Privleged-Identity-Management.png (100%) rename {assets => core/assets}/icons/azure/identity/02251-icon-service-Azure-AD-Privleged-Identity-Management.svg (100%) rename {assets => core/assets}/icons/azure/identity/02251-icon-service-Entra-Privleged-Identity-Management.png (100%) rename {assets => core/assets}/icons/azure/identity/02251-icon-service-Entra-Privleged-Identity-Management.svg (100%) rename {assets => core/assets}/icons/azure/identity/02386-icon-service-API-Proxy.png (100%) rename {assets => core/assets}/icons/azure/identity/02386-icon-service-API-Proxy.svg (100%) rename {assets => core/assets}/icons/azure/identity/02679-icon-service-Tenant-Properties.png (100%) rename {assets => core/assets}/icons/azure/identity/02679-icon-service-Tenant-Properties.svg (100%) rename {assets => core/assets}/icons/azure/identity/02680-icon-service-Custom-Azure-AD-Roles.png (100%) rename {assets => core/assets}/icons/azure/identity/02680-icon-service-Custom-Azure-AD-Roles.svg (100%) rename {assets => core/assets}/icons/azure/identity/02680-icon-service-Entra-Identity-Custom-Roles.png (100%) rename {assets => core/assets}/icons/azure/identity/02680-icon-service-Entra-Identity-Custom-Roles.svg (100%) rename {assets => core/assets}/icons/azure/identity/02681-icon-service-AAD-Licenses.png (100%) rename {assets => core/assets}/icons/azure/identity/02681-icon-service-AAD-Licenses.svg (100%) rename {assets => core/assets}/icons/azure/identity/02681-icon-service-Entra-Identity-Licenses.png (100%) rename {assets => core/assets}/icons/azure/identity/02681-icon-service-Entra-Identity-Licenses.svg (100%) rename {assets => core/assets}/icons/azure/identity/02854-icon-service-Entra-Connect.png (100%) rename {assets => core/assets}/icons/azure/identity/02854-icon-service-Entra-Connect.svg (100%) rename {assets => core/assets}/icons/azure/identity/03143-icon-service-Entra-Verified-ID.png (100%) rename {assets => core/assets}/icons/azure/identity/03143-icon-service-Entra-Verified-ID.svg (100%) rename {assets => core/assets}/icons/azure/identity/03225-icon-service-Verification-As-A-Service.png (100%) rename {assets => core/assets}/icons/azure/identity/03225-icon-service-Verification-As-A-Service.svg (100%) rename {assets => core/assets}/icons/azure/identity/03256-icon-service-Multi-Factor-Authentication.png (100%) rename {assets => core/assets}/icons/azure/identity/03256-icon-service-Multi-Factor-Authentication.svg (100%) rename {assets => core/assets}/icons/azure/identity/03309-icon-service-Entra-Global-Secure-Access.png (100%) rename {assets => core/assets}/icons/azure/identity/03309-icon-service-Entra-Global-Secure-Access.svg (100%) rename {assets => core/assets}/icons/azure/identity/03338-icon-service-External-Identities.png (100%) rename {assets => core/assets}/icons/azure/identity/03338-icon-service-External-Identities.svg (100%) rename {assets => core/assets}/icons/azure/identity/03382-icon-service-Entra-Private-Access.png (100%) rename {assets => core/assets}/icons/azure/identity/03382-icon-service-Entra-Private-Access.svg (100%) rename {assets => core/assets}/icons/azure/identity/03383-icon-service-Entra-Internet-Access.png (100%) rename {assets => core/assets}/icons/azure/identity/03383-icon-service-Entra-Internet-Access.svg (100%) rename {assets => core/assets}/icons/azure/identity/03425-icon-service-external-id.png (100%) rename {assets => core/assets}/icons/azure/identity/03425-icon-service-external-id.svg (100%) rename {assets => core/assets}/icons/azure/identity/034251-icon-service-external-id-modified.png (100%) rename {assets => core/assets}/icons/azure/identity/034251-icon-service-external-id-modified.svg (100%) rename {assets => core/assets}/icons/azure/identity/03533-icon-service-Entra-Connect-Sync.png (100%) rename {assets => core/assets}/icons/azure/identity/03533-icon-service-Entra-Connect-Sync.svg (100%) rename {assets => core/assets}/icons/azure/identity/10221-icon-service-Azure-Active-Directory.png (100%) rename {assets => core/assets}/icons/azure/identity/10221-icon-service-Azure-Active-Directory.svg (100%) rename {assets => core/assets}/icons/azure/identity/10222-icon-service-Azure-AD-Domain-Services.png (100%) rename {assets => core/assets}/icons/azure/identity/10222-icon-service-Azure-AD-Domain-Services.svg (100%) rename {assets => core/assets}/icons/azure/identity/10222-icon-service-Entra-Domain-Services.png (100%) rename {assets => core/assets}/icons/azure/identity/10222-icon-service-Entra-Domain-Services.svg (100%) rename {assets => core/assets}/icons/azure/identity/10223-icon-service-Groups.png (100%) rename {assets => core/assets}/icons/azure/identity/10223-icon-service-Groups.svg (100%) rename {assets => core/assets}/icons/azure/identity/10224-icon-service-Active-Directory-Connect-Health.png (100%) rename {assets => core/assets}/icons/azure/identity/10224-icon-service-Active-Directory-Connect-Health.svg (100%) rename {assets => core/assets}/icons/azure/identity/10224-icon-service-Entra-Connect-Health.png (100%) rename {assets => core/assets}/icons/azure/identity/10224-icon-service-Entra-Connect-Health.svg (100%) rename {assets => core/assets}/icons/azure/identity/10225-icon-service-Enterprise-Applications.png (100%) rename {assets => core/assets}/icons/azure/identity/10225-icon-service-Enterprise-Applications.svg (100%) rename {assets => core/assets}/icons/azure/identity/10227-icon-service-Entra-Managed-Identities.png (100%) rename {assets => core/assets}/icons/azure/identity/10227-icon-service-Entra-Managed-Identities.svg (100%) rename {assets => core/assets}/icons/azure/identity/10227-icon-service-Managed-Identities.png (100%) rename {assets => core/assets}/icons/azure/identity/10227-icon-service-Managed-Identities.svg (100%) rename {assets => core/assets}/icons/azure/identity/10228-icon-service-Azure-AD-B2C.png (100%) rename {assets => core/assets}/icons/azure/identity/10228-icon-service-Azure-AD-B2C.svg (100%) rename {assets => core/assets}/icons/azure/identity/10229-icon-service-Azure-Information-Protection.png (100%) rename {assets => core/assets}/icons/azure/identity/10229-icon-service-Azure-Information-Protection.svg (100%) rename {assets => core/assets}/icons/azure/identity/10230-icon-service-Users.png (100%) rename {assets => core/assets}/icons/azure/identity/10230-icon-service-Users.svg (100%) rename {assets => core/assets}/icons/azure/identity/10231-icon-service-Azure-AD-Identity-Protection.png (100%) rename {assets => core/assets}/icons/azure/identity/10231-icon-service-Azure-AD-Identity-Protection.svg (100%) rename {assets => core/assets}/icons/azure/identity/10231-icon-service-Entra-ID-Protection.png (100%) rename {assets => core/assets}/icons/azure/identity/10231-icon-service-Entra-ID-Protection.svg (100%) rename {assets => core/assets}/icons/azure/identity/10232-icon-service-App-Registrations.png (100%) rename {assets => core/assets}/icons/azure/identity/10232-icon-service-App-Registrations.svg (100%) rename {assets => core/assets}/icons/azure/identity/10234-icon-service-Azure-AD-Privilege-Identity-Management.png (100%) rename {assets => core/assets}/icons/azure/identity/10234-icon-service-Azure-AD-Privilege-Identity-Management.svg (100%) rename {assets => core/assets}/icons/azure/identity/10235-icon-service-Identity-Governance.png (100%) rename {assets => core/assets}/icons/azure/identity/10235-icon-service-Identity-Governance.svg (100%) rename {assets => core/assets}/icons/azure/identity/10340-icon-service-Azure-AD-Roles-and-Administrators.png (100%) rename {assets => core/assets}/icons/azure/identity/10340-icon-service-Azure-AD-Roles-and-Administrators.svg (100%) rename {assets => core/assets}/icons/azure/identity/10340-icon-service-Entra-Identity-Roles-and-Administrators.png (100%) rename {assets => core/assets}/icons/azure/identity/10340-icon-service-Entra-Identity-Roles-and-Administrators.svg (100%) rename {assets => core/assets}/icons/azure/identity/10433-icon-service-User-Settings.png (100%) rename {assets => core/assets}/icons/azure/identity/10433-icon-service-User-Settings.svg (100%) rename {assets => core/assets}/icons/azure/integration/00036-icon-service-SQL-Data-Warehouses.png (100%) rename {assets => core/assets}/icons/azure/integration/00036-icon-service-SQL-Data-Warehouses.svg (100%) rename {assets => core/assets}/icons/azure/integration/00555-icon-service-Integration-Service-Environments.png (100%) rename {assets => core/assets}/icons/azure/integration/00555-icon-service-Integration-Service-Environments.svg (100%) rename {assets => core/assets}/icons/azure/integration/00691-icon-service-Azure-Databox-Gateway.png (100%) rename {assets => core/assets}/icons/azure/integration/00691-icon-service-Azure-Databox-Gateway.svg (100%) rename {assets => core/assets}/icons/azure/integration/02072-icon-service-Partner-Topic.png (100%) rename {assets => core/assets}/icons/azure/integration/02072-icon-service-Partner-Topic.svg (100%) rename {assets => core/assets}/icons/azure/integration/02073-icon-service-System-Topic.png (100%) rename {assets => core/assets}/icons/azure/integration/02073-icon-service-System-Topic.svg (100%) rename {assets => core/assets}/icons/azure/integration/02265-icon-service-Partner-Registration.png (100%) rename {assets => core/assets}/icons/azure/integration/02265-icon-service-Partner-Registration.svg (100%) rename {assets => core/assets}/icons/azure/integration/02266-icon-service-Partner-Namespace.png (100%) rename {assets => core/assets}/icons/azure/integration/02266-icon-service-Partner-Namespace.svg (100%) rename {assets => core/assets}/icons/azure/integration/02631-icon-service-Logic-Apps.png (100%) rename {assets => core/assets}/icons/azure/integration/02631-icon-service-Logic-Apps.svg (100%) rename {assets => core/assets}/icons/azure/integration/02987-icon-service-pubsub.png (100%) rename {assets => core/assets}/icons/azure/integration/02987-icon-service-pubsub.svg (100%) rename {assets => core/assets}/icons/azure/integration/032130874-icon-service-Logic-Apps-Template.png (100%) rename {assets => core/assets}/icons/azure/integration/032130874-icon-service-Logic-Apps-Template.svg (100%) rename {assets => core/assets}/icons/azure/integration/03335-icon-service-Power-Platform.png (100%) rename {assets => core/assets}/icons/azure/integration/03335-icon-service-Power-Platform.svg (100%) rename {assets => core/assets}/icons/azure/integration/03345-icon-service-Integration-Environments.png (100%) rename {assets => core/assets}/icons/azure/integration/03345-icon-service-Integration-Environments.svg (100%) rename {assets => core/assets}/icons/azure/integration/03637-icon-service-Business-Process-Tracking.png (100%) rename {assets => core/assets}/icons/azure/integration/03637-icon-service-Business-Process-Tracking.svg (100%) rename {assets => core/assets}/icons/azure/integration/10042-icon-service-API-Management-Services.png (100%) rename {assets => core/assets}/icons/azure/integration/10042-icon-service-API-Management-Services.svg (100%) rename {assets => core/assets}/icons/azure/integration/10048-icon-service-API-Connections.png (100%) rename {assets => core/assets}/icons/azure/integration/10048-icon-service-API-Connections.svg (100%) rename {assets => core/assets}/icons/azure/integration/10089-icon-service-StorSimple-Device-Managers.png (100%) rename {assets => core/assets}/icons/azure/integration/10089-icon-service-StorSimple-Device-Managers.svg (100%) rename {assets => core/assets}/icons/azure/integration/10095-icon-service-Azure-Stack-Edge.png (100%) rename {assets => core/assets}/icons/azure/integration/10095-icon-service-Azure-Stack-Edge.svg (100%) rename {assets => core/assets}/icons/azure/integration/10126-icon-service-Data-Factories.png (100%) rename {assets => core/assets}/icons/azure/integration/10126-icon-service-Data-Factories.svg (100%) rename {assets => core/assets}/icons/azure/integration/10137-icon-service-Azure-SQL-Server-Stretch-Databases.png (100%) rename {assets => core/assets}/icons/azure/integration/10137-icon-service-Azure-SQL-Server-Stretch-Databases.svg (100%) rename {assets => core/assets}/icons/azure/integration/10206-icon-service-Event-Grid-Topics.png (100%) rename {assets => core/assets}/icons/azure/integration/10206-icon-service-Event-Grid-Topics.svg (100%) rename {assets => core/assets}/icons/azure/integration/10209-icon-service-Relays.png (100%) rename {assets => core/assets}/icons/azure/integration/10209-icon-service-Relays.svg (100%) rename {assets => core/assets}/icons/azure/integration/10212-icon-service-Azure-API-for-FHIR.png (100%) rename {assets => core/assets}/icons/azure/integration/10212-icon-service-Azure-API-for-FHIR.svg (100%) rename {assets => core/assets}/icons/azure/integration/10213-icon-service-Software-as-a-Service.png (100%) rename {assets => core/assets}/icons/azure/integration/10213-icon-service-Software-as-a-Service.svg (100%) rename {assets => core/assets}/icons/azure/integration/10215-icon-service-Event-Grid-Domains.png (100%) rename {assets => core/assets}/icons/azure/integration/10215-icon-service-Event-Grid-Domains.svg (100%) rename {assets => core/assets}/icons/azure/integration/10216-icon-service-Azure-Data-Catalog.png (100%) rename {assets => core/assets}/icons/azure/integration/10216-icon-service-Azure-Data-Catalog.svg (100%) rename {assets => core/assets}/icons/azure/integration/10218-icon-service-Integration-Accounts.png (100%) rename {assets => core/assets}/icons/azure/integration/10218-icon-service-Integration-Accounts.svg (100%) rename {assets => core/assets}/icons/azure/integration/10219-icon-service-App-Configuration.png (100%) rename {assets => core/assets}/icons/azure/integration/10219-icon-service-App-Configuration.svg (100%) rename {assets => core/assets}/icons/azure/integration/10220-icon-service-SendGrid-Accounts.png (100%) rename {assets => core/assets}/icons/azure/integration/10220-icon-service-SendGrid-Accounts.svg (100%) rename {assets => core/assets}/icons/azure/integration/10221-icon-service-Event-Grid-Subscriptions.png (100%) rename {assets => core/assets}/icons/azure/integration/10221-icon-service-Event-Grid-Subscriptions.svg (100%) rename {assets => core/assets}/icons/azure/integration/10363-icon-service-Logic-Apps-Custom-Connector.png (100%) rename {assets => core/assets}/icons/azure/integration/10363-icon-service-Logic-Apps-Custom-Connector.svg (100%) rename {assets => core/assets}/icons/azure/integration/10836-icon-service-Azure-Service-Bus.png (100%) rename {assets => core/assets}/icons/azure/integration/10836-icon-service-Azure-Service-Bus.svg (100%) rename {assets => core/assets}/icons/azure/intune/00399-icon-service-Device-Security-Apple.png (100%) rename {assets => core/assets}/icons/azure/intune/00399-icon-service-Device-Security-Apple.svg (100%) rename {assets => core/assets}/icons/azure/intune/00399-icon-service-Device-Security-Google.png (100%) rename {assets => core/assets}/icons/azure/intune/00399-icon-service-Device-Security-Google.svg (100%) rename {assets => core/assets}/icons/azure/intune/00399-icon-service-Device-Security-Windows.png (100%) rename {assets => core/assets}/icons/azure/intune/00399-icon-service-Device-Security-Windows.svg (100%) rename {assets => core/assets}/icons/azure/intune/029797391-icon-service-Engage-Center-Connect.png (100%) rename {assets => core/assets}/icons/azure/intune/029797391-icon-service-Engage-Center-Connect.svg (100%) rename {assets => core/assets}/icons/azure/intune/10329-icon-service-Intune.png (100%) rename {assets => core/assets}/icons/azure/intune/10329-icon-service-Intune.svg (100%) rename {assets => core/assets}/icons/azure/intune/10330-icon-service-eBooks.png (100%) rename {assets => core/assets}/icons/azure/intune/10330-icon-service-eBooks.svg (100%) rename {assets => core/assets}/icons/azure/intune/10331-icon-service-Client-Apps.png (100%) rename {assets => core/assets}/icons/azure/intune/10331-icon-service-Client-Apps.svg (100%) rename {assets => core/assets}/icons/azure/intune/10332-icon-service-Devices.png (100%) rename {assets => core/assets}/icons/azure/intune/10332-icon-service-Devices.svg (100%) rename {assets => core/assets}/icons/azure/intune/10333-icon-service-Device-Compliance.png (100%) rename {assets => core/assets}/icons/azure/intune/10333-icon-service-Device-Compliance.svg (100%) rename {assets => core/assets}/icons/azure/intune/10335-icon-service-Software-Updates.png (100%) rename {assets => core/assets}/icons/azure/intune/10335-icon-service-Software-Updates.svg (100%) rename {assets => core/assets}/icons/azure/intune/10336-icon-service-Security-Baselines.png (100%) rename {assets => core/assets}/icons/azure/intune/10336-icon-service-Security-Baselines.svg (100%) rename {assets => core/assets}/icons/azure/intune/10337-icon-service-Device-Enrollment.png (100%) rename {assets => core/assets}/icons/azure/intune/10337-icon-service-Device-Enrollment.svg (100%) rename {assets => core/assets}/icons/azure/intune/10338-icon-service-Device-Configuration.png (100%) rename {assets => core/assets}/icons/azure/intune/10338-icon-service-Device-Configuration.svg (100%) rename {assets => core/assets}/icons/azure/intune/10339-icon-service-Exchange-Access.png (100%) rename {assets => core/assets}/icons/azure/intune/10339-icon-service-Exchange-Access.svg (100%) rename {assets => core/assets}/icons/azure/intune/10340-icon-service-Azure-AD-Roles-and-Administrators.png (100%) rename {assets => core/assets}/icons/azure/intune/10340-icon-service-Azure-AD-Roles-and-Administrators.svg (100%) rename {assets => core/assets}/icons/azure/intune/10340-icon-service-Entra-Identity-Roles-and-Administrators.png (100%) rename {assets => core/assets}/icons/azure/intune/10340-icon-service-Entra-Identity-Roles-and-Administrators.svg (100%) rename {assets => core/assets}/icons/azure/intune/10342-icon-service-Tenant-Status.png (100%) rename {assets => core/assets}/icons/azure/intune/10342-icon-service-Tenant-Status.svg (100%) rename {assets => core/assets}/icons/azure/intune/10343-icon-service-Intune-For-Education.png (100%) rename {assets => core/assets}/icons/azure/intune/10343-icon-service-Intune-For-Education.svg (100%) rename {assets => core/assets}/icons/azure/intune/10344-icon-service-Intune-App-Protection.png (100%) rename {assets => core/assets}/icons/azure/intune/10344-icon-service-Intune-App-Protection.svg (100%) rename {assets => core/assets}/icons/azure/intune/10350-icon-service-Mindaro.png (100%) rename {assets => core/assets}/icons/azure/intune/10350-icon-service-Mindaro.svg (100%) rename {assets => core/assets}/icons/azure/iot/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.png (100%) rename {assets => core/assets}/icons/azure/iot/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.svg (100%) rename {assets => core/assets}/icons/azure/iot/00039-icon-service-Event-Hubs.png (100%) rename {assets => core/assets}/icons/azure/iot/00039-icon-service-Event-Hubs.svg (100%) rename {assets => core/assets}/icons/azure/iot/00042-icon-service-Stream-Analytics-Jobs.png (100%) rename {assets => core/assets}/icons/azure/iot/00042-icon-service-Stream-Analytics-Jobs.svg (100%) rename {assets => core/assets}/icons/azure/iot/00691-icon-service-Azure-Databox-Gateway.png (100%) rename {assets => core/assets}/icons/azure/iot/00691-icon-service-Azure-Databox-Gateway.svg (100%) rename {assets => core/assets}/icons/azure/iot/01030-icon-service-Digital-Twins.png (100%) rename {assets => core/assets}/icons/azure/iot/01030-icon-service-Digital-Twins.svg (100%) rename {assets => core/assets}/icons/azure/iot/02359-icon-service-Industrial-IoT.png (100%) rename {assets => core/assets}/icons/azure/iot/02359-icon-service-Industrial-IoT.svg (100%) rename {assets => core/assets}/icons/azure/iot/02631-icon-service-Logic-Apps.png (100%) rename {assets => core/assets}/icons/azure/iot/02631-icon-service-Logic-Apps.svg (100%) rename {assets => core/assets}/icons/azure/iot/03293-icon-service-Azure-Stack-HCI-Sizer.png (100%) rename {assets => core/assets}/icons/azure/iot/03293-icon-service-Azure-Stack-HCI-Sizer.svg (100%) rename {assets => core/assets}/icons/azure/iot/03466-icon-service-Stack-HCI-Premium.png (100%) rename {assets => core/assets}/icons/azure/iot/03466-icon-service-Stack-HCI-Premium.svg (100%) rename {assets => core/assets}/icons/azure/iot/03485-icon-service-Azure-IoT-Operations.png (100%) rename {assets => core/assets}/icons/azure/iot/03485-icon-service-Azure-IoT-Operations.svg (100%) rename {assets => core/assets}/icons/azure/iot/03656-icon-service-Hybrid-Connectivity-Hub.png (100%) rename {assets => core/assets}/icons/azure/iot/03656-icon-service-Hybrid-Connectivity-Hub.svg (100%) rename {assets => core/assets}/icons/azure/iot/10029-icon-service-Function-Apps.png (100%) rename {assets => core/assets}/icons/azure/iot/10029-icon-service-Function-Apps.svg (100%) rename {assets => core/assets}/icons/azure/iot/10045-icon-service-Notification-Hubs.png (100%) rename {assets => core/assets}/icons/azure/iot/10045-icon-service-Notification-Hubs.svg (100%) rename {assets => core/assets}/icons/azure/iot/10053-icon-service-Notification-Hub-Namespaces.png (100%) rename {assets => core/assets}/icons/azure/iot/10053-icon-service-Notification-Hub-Namespaces.svg (100%) rename {assets => core/assets}/icons/azure/iot/10114-icon-service-Azure-Stack.png (100%) rename {assets => core/assets}/icons/azure/iot/10114-icon-service-Azure-Stack.svg (100%) rename {assets => core/assets}/icons/azure/iot/10121-icon-service-Azure-Cosmos-DB.png (100%) rename {assets => core/assets}/icons/azure/iot/10121-icon-service-Azure-Cosmos-DB.svg (100%) rename {assets => core/assets}/icons/azure/iot/10149-icon-service-Event-Hub-Clusters.png (100%) rename {assets => core/assets}/icons/azure/iot/10149-icon-service-Event-Hub-Clusters.svg (100%) rename {assets => core/assets}/icons/azure/iot/10167-icon-service-Machine-Learning-Studio-Workspaces.png (100%) rename {assets => core/assets}/icons/azure/iot/10167-icon-service-Machine-Learning-Studio-Workspaces.svg (100%) rename {assets => core/assets}/icons/azure/iot/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.png (100%) rename {assets => core/assets}/icons/azure/iot/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.svg (100%) rename {assets => core/assets}/icons/azure/iot/10181-icon-service-Time-Series-Insights-Environments.png (100%) rename {assets => core/assets}/icons/azure/iot/10181-icon-service-Time-Series-Insights-Environments.svg (100%) rename {assets => core/assets}/icons/azure/iot/10182-icon-service-IoT-Hub.png (100%) rename {assets => core/assets}/icons/azure/iot/10182-icon-service-IoT-Hub.svg (100%) rename {assets => core/assets}/icons/azure/iot/10184-icon-service-IoT-Central-Applications.png (100%) rename {assets => core/assets}/icons/azure/iot/10184-icon-service-IoT-Central-Applications.svg (100%) rename {assets => core/assets}/icons/azure/iot/10185-icon-service-Azure-Maps-Accounts.png (100%) rename {assets => core/assets}/icons/azure/iot/10185-icon-service-Azure-Maps-Accounts.svg (100%) rename {assets => core/assets}/icons/azure/iot/10186-icon-service-IoT-Edge.png (100%) rename {assets => core/assets}/icons/azure/iot/10186-icon-service-IoT-Edge.svg (100%) rename {assets => core/assets}/icons/azure/iot/10188-icon-service-Time-Series-Insights-Event-Sources.png (100%) rename {assets => core/assets}/icons/azure/iot/10188-icon-service-Time-Series-Insights-Event-Sources.svg (100%) rename {assets => core/assets}/icons/azure/iot/10192-icon-service-Time-Series-Insights-Access-Policies.png (100%) rename {assets => core/assets}/icons/azure/iot/10192-icon-service-Time-Series-Insights-Access-Policies.svg (100%) rename {assets => core/assets}/icons/azure/iot/10198-icon-service-Time-Series-Data-Sets.png (100%) rename {assets => core/assets}/icons/azure/iot/10198-icon-service-Time-Series-Data-Sets.svg (100%) rename {assets => core/assets}/icons/azure/iot/10203-icon-service-Windows10-Core-Services.png (100%) rename {assets => core/assets}/icons/azure/iot/10203-icon-service-Windows10-Core-Services.svg (100%) rename {assets => core/assets}/icons/azure/iot/10221-icon-service-Event-Grid-Subscriptions.png (100%) rename {assets => core/assets}/icons/azure/iot/10221-icon-service-Event-Grid-Subscriptions.svg (100%) rename {assets => core/assets}/icons/azure/iot/10369-icon-service-Device-Provisioning-Services.png (100%) rename {assets => core/assets}/icons/azure/iot/10369-icon-service-Device-Provisioning-Services.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00001-icon-service-Monitor.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00001-icon-service-Monitor.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00002-icon-service-Alerts.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00002-icon-service-Alerts.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00003-icon-service-Advisor.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00003-icon-service-Advisor.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00004-icon-service-Cost-Management-and-Billing.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00004-icon-service-Cost-Management-and-Billing.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00006-icon-service-Blueprints.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00006-icon-service-Blueprints.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00007-icon-service-Activity-Log.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00007-icon-service-Activity-Log.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00008-icon-service-Diagnostics-Settings.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00008-icon-service-Diagnostics-Settings.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00009-icon-service-Log-Analytics-Workspaces.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00009-icon-service-Log-Analytics-Workspaces.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00010-icon-service-Scheduler-Job-Collections.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00010-icon-service-Scheduler-Job-Collections.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00011-icon-service-Compliance.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00011-icon-service-Compliance.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00012-icon-service-Application-Insights.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00012-icon-service-Application-Insights.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00014-icon-service-My-Customers.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00014-icon-service-My-Customers.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00017-icon-service-Recovery-Services-Vaults.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00017-icon-service-Recovery-Services-Vaults.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00020-icon-service-Metrics.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00020-icon-service-Metrics.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00021-icon-service-Solutions.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00021-icon-service-Solutions.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00022-icon-service-Automation-Accounts.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00022-icon-service-Automation-Accounts.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00024-icon-service-Operation-Log-(Classic).png (100%) rename {assets => core/assets}/icons/azure/management-governance/00024-icon-service-Operation-Log-(Classic).svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00025-icon-service-Service-Providers.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00025-icon-service-Service-Providers.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00026-icon-service-Education.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00026-icon-service-Education.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00027-icon-service-Service-Catalog-MAD.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00027-icon-service-Service-Catalog-MAD.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00408-icon-service-Intune-Trends.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00408-icon-service-Intune-Trends.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00471-icon-service-Azure-Lighthouse.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00471-icon-service-Azure-Lighthouse.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00571-icon-service-Universal-Print.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00571-icon-service-Universal-Print.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/00756-icon-service-Azure-Arc.png (100%) rename {assets => core/assets}/icons/azure/management-governance/00756-icon-service-Azure-Arc.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/01710-icon-service-Arc-Machines.png (100%) rename {assets => core/assets}/icons/azure/management-governance/01710-icon-service-Arc-Machines.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/030777508-icon-service-Service-Group-Relationships.png (100%) rename {assets => core/assets}/icons/azure/management-governance/030777508-icon-service-Service-Group-Relationships.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/03366-icon-service-Resources-Provider.png (100%) rename {assets => core/assets}/icons/azure/management-governance/03366-icon-service-Resources-Provider.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/10303-icon-service-User-Privacy.png (100%) rename {assets => core/assets}/icons/azure/management-governance/10303-icon-service-User-Privacy.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/10311-icon-service-Managed-Desktop.png (100%) rename {assets => core/assets}/icons/azure/management-governance/10311-icon-service-Managed-Desktop.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/10313-icon-service-Managed-Applications-Center.png (100%) rename {assets => core/assets}/icons/azure/management-governance/10313-icon-service-Managed-Applications-Center.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/10314-icon-service-Customer-Lockbox-for-Microsoft-Azure.png (100%) rename {assets => core/assets}/icons/azure/management-governance/10314-icon-service-Customer-Lockbox-for-Microsoft-Azure.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/10316-icon-service-Policy.png (100%) rename {assets => core/assets}/icons/azure/management-governance/10316-icon-service-Policy.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/10318-icon-service-Resource-Graph-Explorer.png (100%) rename {assets => core/assets}/icons/azure/management-governance/10318-icon-service-Resource-Graph-Explorer.svg (100%) rename {assets => core/assets}/icons/azure/management-governance/10450-icon-service-MachinesAzureArc.png (100%) rename {assets => core/assets}/icons/azure/management-governance/10450-icon-service-MachinesAzureArc.svg (100%) rename {assets => core/assets}/icons/azure/menu/00787-icon-service-Keys.png (100%) rename {assets => core/assets}/icons/azure/menu/00787-icon-service-Keys.svg (100%) rename {assets => core/assets}/icons/azure/migrate/00004-icon-service-Cost-Management-and-Billing.png (100%) rename {assets => core/assets}/icons/azure/migrate/00004-icon-service-Cost-Management-and-Billing.svg (100%) rename {assets => core/assets}/icons/azure/migrate/00017-icon-service-Recovery-Services-Vaults.png (100%) rename {assets => core/assets}/icons/azure/migrate/00017-icon-service-Recovery-Services-Vaults.svg (100%) rename {assets => core/assets}/icons/azure/migrate/00691-icon-service-Azure-Databox-Gateway.png (100%) rename {assets => core/assets}/icons/azure/migrate/00691-icon-service-Azure-Databox-Gateway.svg (100%) rename {assets => core/assets}/icons/azure/migrate/10094-icon-service-Data-Box.png (100%) rename {assets => core/assets}/icons/azure/migrate/10094-icon-service-Data-Box.svg (100%) rename {assets => core/assets}/icons/azure/migrate/10095-icon-service-Azure-Stack-Edge.png (100%) rename {assets => core/assets}/icons/azure/migrate/10095-icon-service-Azure-Stack-Edge.svg (100%) rename {assets => core/assets}/icons/azure/migrate/10281-icon-service-Azure-Migrate.png (100%) rename {assets => core/assets}/icons/azure/migrate/10281-icon-service-Azure-Migrate.svg (100%) rename {assets => core/assets}/icons/azure/migration/10133-icon-service-Azure-Database-Migration-Services.png (100%) rename {assets => core/assets}/icons/azure/migration/10133-icon-service-Azure-Database-Migration-Services.svg (100%) rename {assets => core/assets}/icons/azure/mixed-reality/00698-icon-service-Remote-Rendering.png (100%) rename {assets => core/assets}/icons/azure/mixed-reality/00698-icon-service-Remote-Rendering.svg (100%) rename {assets => core/assets}/icons/azure/mixed-reality/10352-icon-service-Spatial-Anchor-Accounts.png (100%) rename {assets => core/assets}/icons/azure/mixed-reality/10352-icon-service-Spatial-Anchor-Accounts.svg (100%) rename {assets => core/assets}/icons/azure/mobile/03335-icon-service-Power-Platform.png (100%) rename {assets => core/assets}/icons/azure/mobile/03335-icon-service-Power-Platform.svg (100%) rename {assets => core/assets}/icons/azure/mobile/10035-icon-service-App-Services.png (100%) rename {assets => core/assets}/icons/azure/mobile/10035-icon-service-App-Services.svg (100%) rename {assets => core/assets}/icons/azure/mobile/10045-icon-service-Notification-Hubs.png (100%) rename {assets => core/assets}/icons/azure/mobile/10045-icon-service-Notification-Hubs.svg (100%) rename {assets => core/assets}/icons/azure/monitor/00001-icon-service-Monitor.png (100%) rename {assets => core/assets}/icons/azure/monitor/00001-icon-service-Monitor.svg (100%) rename {assets => core/assets}/icons/azure/monitor/00007-icon-service-Activity-Log.png (100%) rename {assets => core/assets}/icons/azure/monitor/00007-icon-service-Activity-Log.svg (100%) rename {assets => core/assets}/icons/azure/monitor/00008-icon-service-Diagnostics-Settings.png (100%) rename {assets => core/assets}/icons/azure/monitor/00008-icon-service-Diagnostics-Settings.svg (100%) rename {assets => core/assets}/icons/azure/monitor/00009-icon-service-Log-Analytics-Workspaces.png (100%) rename {assets => core/assets}/icons/azure/monitor/00009-icon-service-Log-Analytics-Workspaces.svg (100%) rename {assets => core/assets}/icons/azure/monitor/00012-icon-service-Application-Insights.png (100%) rename {assets => core/assets}/icons/azure/monitor/00012-icon-service-Application-Insights.svg (100%) rename {assets => core/assets}/icons/azure/monitor/00020-icon-service-Metrics.png (100%) rename {assets => core/assets}/icons/azure/monitor/00020-icon-service-Metrics.svg (100%) rename {assets => core/assets}/icons/azure/monitor/00438-icon-service-Azure-Monitors-for-SAP-Solutions.png (100%) rename {assets => core/assets}/icons/azure/monitor/00438-icon-service-Azure-Monitors-for-SAP-Solutions.svg (100%) rename {assets => core/assets}/icons/azure/monitor/00563-icon-service-Change-Analysis.png (100%) rename {assets => core/assets}/icons/azure/monitor/00563-icon-service-Change-Analysis.svg (100%) rename {assets => core/assets}/icons/azure/monitor/02189-icon-service-Azure-Workbooks.png (100%) rename {assets => core/assets}/icons/azure/monitor/02189-icon-service-Azure-Workbooks.svg (100%) rename {assets => core/assets}/icons/azure/monitor/10066-icon-service-Network-Watcher.png (100%) rename {assets => core/assets}/icons/azure/monitor/10066-icon-service-Network-Watcher.svg (100%) rename {assets => core/assets}/icons/azure/monitor/10832-icon-service-Auto-Scale.png (100%) rename {assets => core/assets}/icons/azure/monitor/10832-icon-service-Auto-Scale.svg (100%) rename {assets => core/assets}/icons/azure/networking/00056-icon-service-CDN-Profiles.png (100%) rename {assets => core/assets}/icons/azure/networking/00056-icon-service-CDN-Profiles.svg (100%) rename {assets => core/assets}/icons/azure/networking/00271-icon-service-Azure-Firewall-Manager.png (100%) rename {assets => core/assets}/icons/azure/networking/00271-icon-service-Azure-Firewall-Manager.svg (100%) rename {assets => core/assets}/icons/azure/networking/00272-icon-service-Azure-Firewall-Policy.png (100%) rename {assets => core/assets}/icons/azure/networking/00272-icon-service-Azure-Firewall-Policy.svg (100%) rename {assets => core/assets}/icons/azure/networking/00427-icon-service-Private-Link.png (100%) rename {assets => core/assets}/icons/azure/networking/00427-icon-service-Private-Link.svg (100%) rename {assets => core/assets}/icons/azure/networking/00701-icon-service-IP-Groups.png (100%) rename {assets => core/assets}/icons/azure/networking/00701-icon-service-IP-Groups.svg (100%) rename {assets => core/assets}/icons/azure/networking/00860-icon-service-Virtual-WAN-Hub.png (100%) rename {assets => core/assets}/icons/azure/networking/00860-icon-service-Virtual-WAN-Hub.svg (100%) rename {assets => core/assets}/icons/azure/networking/01105-icon-service-Private-Link-Service.png (100%) rename {assets => core/assets}/icons/azure/networking/01105-icon-service-Private-Link-Service.svg (100%) rename {assets => core/assets}/icons/azure/networking/02145-icon-service-Resource-Management-Private-Link.png (100%) rename {assets => core/assets}/icons/azure/networking/02145-icon-service-Resource-Management-Private-Link.svg (100%) rename {assets => core/assets}/icons/azure/networking/02209-icon-service-Private-Link-Services.png (100%) rename {assets => core/assets}/icons/azure/networking/02209-icon-service-Private-Link-Services.svg (100%) rename {assets => core/assets}/icons/azure/networking/02302-icon-service-Load-Balancer-Hub.png (100%) rename {assets => core/assets}/icons/azure/networking/02302-icon-service-Load-Balancer-Hub.svg (100%) rename {assets => core/assets}/icons/azure/networking/02422-icon-service-Bastions.png (100%) rename {assets => core/assets}/icons/azure/networking/02422-icon-service-Bastions.svg (100%) rename {assets => core/assets}/icons/azure/networking/02496-icon-service-Virtual-Router.png (100%) rename {assets => core/assets}/icons/azure/networking/02496-icon-service-Virtual-Router.svg (100%) rename {assets => core/assets}/icons/azure/networking/02509-icon-service-Connected-Cache.png (100%) rename {assets => core/assets}/icons/azure/networking/02509-icon-service-Connected-Cache.svg (100%) rename {assets => core/assets}/icons/azure/networking/02692-icon-service-Spot-VMSS.png (100%) rename {assets => core/assets}/icons/azure/networking/02692-icon-service-Spot-VMSS.svg (100%) rename {assets => core/assets}/icons/azure/networking/02695-icon-service-Spot-VM.png (100%) rename {assets => core/assets}/icons/azure/networking/02695-icon-service-Spot-VM.svg (100%) rename {assets => core/assets}/icons/azure/networking/02742-icon-service-Subnet.png (100%) rename {assets => core/assets}/icons/azure/networking/02742-icon-service-Subnet.svg (100%) rename {assets => core/assets}/icons/azure/networking/02882-icon-service-DNS-Private-Resolver.png (100%) rename {assets => core/assets}/icons/azure/networking/02882-icon-service-DNS-Private-Resolver.svg (100%) rename {assets => core/assets}/icons/azure/networking/029029174-icon-service-Load-Balancer-Hub.png (100%) rename {assets => core/assets}/icons/azure/networking/029029174-icon-service-Load-Balancer-Hub.svg (100%) rename {assets => core/assets}/icons/azure/networking/029273094-icon-service-Edge-Actions.png (100%) rename {assets => core/assets}/icons/azure/networking/029273094-icon-service-Edge-Actions.svg (100%) rename {assets => core/assets}/icons/azure/networking/03311-icon-service-Azure-Communications-Gateway.png (100%) rename {assets => core/assets}/icons/azure/networking/03311-icon-service-Azure-Communications-Gateway.svg (100%) rename {assets => core/assets}/icons/azure/networking/03328-icon-service-Application-Gateway-Containers.png (100%) rename {assets => core/assets}/icons/azure/networking/03328-icon-service-Application-Gateway-Containers.svg (100%) rename {assets => core/assets}/icons/azure/networking/03368-icon-service-DNS-Security-Policy.png (100%) rename {assets => core/assets}/icons/azure/networking/03368-icon-service-DNS-Security-Policy.svg (100%) rename {assets => core/assets}/icons/azure/networking/034412066-icon-service-VNet-Appliance.png (100%) rename {assets => core/assets}/icons/azure/networking/034412066-icon-service-VNet-Appliance.svg (100%) rename {assets => core/assets}/icons/azure/networking/03459-icon-service-DNS-Multistack.png (100%) rename {assets => core/assets}/icons/azure/networking/03459-icon-service-DNS-Multistack.svg (100%) rename {assets => core/assets}/icons/azure/networking/03460-icon-service-ATM-Multistack.png (100%) rename {assets => core/assets}/icons/azure/networking/03460-icon-service-ATM-Multistack.svg (100%) rename {assets => core/assets}/icons/azure/networking/03461-icon-service-IP-Address-manager.png (100%) rename {assets => core/assets}/icons/azure/networking/03461-icon-service-IP-Address-manager.svg (100%) rename {assets => core/assets}/icons/azure/networking/03643-icon-service-Network-Foundation-Hub.png (100%) rename {assets => core/assets}/icons/azure/networking/03643-icon-service-Network-Foundation-Hub.svg (100%) rename {assets => core/assets}/icons/azure/networking/03650-icon-service-Network-Security-Hub.png (100%) rename {assets => core/assets}/icons/azure/networking/03650-icon-service-Network-Security-Hub.svg (100%) rename {assets => core/assets}/icons/azure/networking/03694-icon-service-VPNClientWindows.png (100%) rename {assets => core/assets}/icons/azure/networking/03694-icon-service-VPNClientWindows.svg (100%) rename {assets => core/assets}/icons/azure/networking/10061-icon-service-Virtual-Networks.png (100%) rename {assets => core/assets}/icons/azure/networking/10061-icon-service-Virtual-Networks.svg (100%) rename {assets => core/assets}/icons/azure/networking/10062-icon-service-Load-Balancers.png (100%) rename {assets => core/assets}/icons/azure/networking/10062-icon-service-Load-Balancers.svg (100%) rename {assets => core/assets}/icons/azure/networking/10063-icon-service-Virtual-Network-Gateways.png (100%) rename {assets => core/assets}/icons/azure/networking/10063-icon-service-Virtual-Network-Gateways.svg (100%) rename {assets => core/assets}/icons/azure/networking/10064-icon-service-DNS-Zones.png (100%) rename {assets => core/assets}/icons/azure/networking/10064-icon-service-DNS-Zones.svg (100%) rename {assets => core/assets}/icons/azure/networking/10065-icon-service-Traffic-Manager-Profiles.png (100%) rename {assets => core/assets}/icons/azure/networking/10065-icon-service-Traffic-Manager-Profiles.svg (100%) rename {assets => core/assets}/icons/azure/networking/10066-icon-service-Network-Watcher.png (100%) rename {assets => core/assets}/icons/azure/networking/10066-icon-service-Network-Watcher.svg (100%) rename {assets => core/assets}/icons/azure/networking/10067-icon-service-Network-Security-Groups.png (100%) rename {assets => core/assets}/icons/azure/networking/10067-icon-service-Network-Security-Groups.svg (100%) rename {assets => core/assets}/icons/azure/networking/10068-icon-service-Public-IP-Addresses-(Classic).png (100%) rename {assets => core/assets}/icons/azure/networking/10068-icon-service-Public-IP-Addresses-(Classic).svg (100%) rename {assets => core/assets}/icons/azure/networking/10069-icon-service-Public-IP-Addresses.png (100%) rename {assets => core/assets}/icons/azure/networking/10069-icon-service-Public-IP-Addresses.svg (100%) rename {assets => core/assets}/icons/azure/networking/10070-icon-service-On-Premises-Data-Gateways.png (100%) rename {assets => core/assets}/icons/azure/networking/10070-icon-service-On-Premises-Data-Gateways.svg (100%) rename {assets => core/assets}/icons/azure/networking/10071-icon-service-Route-Filters.png (100%) rename {assets => core/assets}/icons/azure/networking/10071-icon-service-Route-Filters.svg (100%) rename {assets => core/assets}/icons/azure/networking/10072-icon-service-DDoS-Protection-Plans.png (100%) rename {assets => core/assets}/icons/azure/networking/10072-icon-service-DDoS-Protection-Plans.svg (100%) rename {assets => core/assets}/icons/azure/networking/10073-icon-service-Front-Door-and-CDN-Profiles.png (100%) rename {assets => core/assets}/icons/azure/networking/10073-icon-service-Front-Door-and-CDN-Profiles.svg (100%) rename {assets => core/assets}/icons/azure/networking/10075-icon-service-Virtual-Networks-(Classic).png (100%) rename {assets => core/assets}/icons/azure/networking/10075-icon-service-Virtual-Networks-(Classic).svg (100%) rename {assets => core/assets}/icons/azure/networking/10076-icon-service-Application-Gateways.png (100%) rename {assets => core/assets}/icons/azure/networking/10076-icon-service-Application-Gateways.svg (100%) rename {assets => core/assets}/icons/azure/networking/10077-icon-service-Local-Network-Gateways.png (100%) rename {assets => core/assets}/icons/azure/networking/10077-icon-service-Local-Network-Gateways.svg (100%) rename {assets => core/assets}/icons/azure/networking/10079-icon-service-ExpressRoute-Circuits.png (100%) rename {assets => core/assets}/icons/azure/networking/10079-icon-service-ExpressRoute-Circuits.svg (100%) rename {assets => core/assets}/icons/azure/networking/10080-icon-service-Network-Interfaces.png (100%) rename {assets => core/assets}/icons/azure/networking/10080-icon-service-Network-Interfaces.svg (100%) rename {assets => core/assets}/icons/azure/networking/10081-icon-service-Connections.png (100%) rename {assets => core/assets}/icons/azure/networking/10081-icon-service-Connections.svg (100%) rename {assets => core/assets}/icons/azure/networking/10082-icon-service-Route-Tables.png (100%) rename {assets => core/assets}/icons/azure/networking/10082-icon-service-Route-Tables.svg (100%) rename {assets => core/assets}/icons/azure/networking/10084-icon-service-Firewalls.png (100%) rename {assets => core/assets}/icons/azure/networking/10084-icon-service-Firewalls.svg (100%) rename {assets => core/assets}/icons/azure/networking/10085-icon-service-Service-Endpoint-Policies.png (100%) rename {assets => core/assets}/icons/azure/networking/10085-icon-service-Service-Endpoint-Policies.svg (100%) rename {assets => core/assets}/icons/azure/networking/10310-icon-service-NAT.png (100%) rename {assets => core/assets}/icons/azure/networking/10310-icon-service-NAT.svg (100%) rename {assets => core/assets}/icons/azure/networking/10353-icon-service-Virtual-WANs.png (100%) rename {assets => core/assets}/icons/azure/networking/10353-icon-service-Virtual-WANs.svg (100%) rename {assets => core/assets}/icons/azure/networking/10362-icon-service-Web-Application-Firewall-Policies(WAF).png (100%) rename {assets => core/assets}/icons/azure/networking/10362-icon-service-Web-Application-Firewall-Policies(WAF).svg (100%) rename {assets => core/assets}/icons/azure/networking/10365-icon-service-Proximity-Placement-Groups.png (100%) rename {assets => core/assets}/icons/azure/networking/10365-icon-service-Proximity-Placement-Groups.svg (100%) rename {assets => core/assets}/icons/azure/networking/10371-icon-service-Reserved-IP-Addresses-(Classic).png (100%) rename {assets => core/assets}/icons/azure/networking/10371-icon-service-Reserved-IP-Addresses-(Classic).svg (100%) rename {assets => core/assets}/icons/azure/networking/10372-icon-service-Public-IP-Prefixes.png (100%) rename {assets => core/assets}/icons/azure/networking/10372-icon-service-Public-IP-Prefixes.svg (100%) rename {assets => core/assets}/icons/azure/new icons/02573-icon-service-Connected-Vehicle-Platform.png (100%) rename {assets => core/assets}/icons/azure/new icons/02573-icon-service-Connected-Vehicle-Platform.svg (100%) rename {assets => core/assets}/icons/azure/new icons/029109619-icon-service-Toolchain-Orchestrator.png (100%) rename {assets => core/assets}/icons/azure/new icons/029109619-icon-service-Toolchain-Orchestrator.svg (100%) rename {assets => core/assets}/icons/azure/new icons/029636048-icon-service-Landing-Zone.png (100%) rename {assets => core/assets}/icons/azure/new icons/029636048-icon-service-Landing-Zone.svg (100%) rename {assets => core/assets}/icons/azure/new icons/030777508 -icon-service-Service-Group-Relationships.png (100%) rename {assets => core/assets}/icons/azure/new icons/030777508 -icon-service-Service-Group-Relationships.svg (100%) rename {assets => core/assets}/icons/azure/new icons/031193085-icon-service-Azure-Access-Point.png (100%) rename {assets => core/assets}/icons/azure/new icons/031193085-icon-service-Azure-Access-Point.svg (100%) rename {assets => core/assets}/icons/azure/new icons/033740592-icon-service-Azure-Data-Transfer.png (100%) rename {assets => core/assets}/icons/azure/new icons/033740592-icon-service-Azure-Data-Transfer.svg (100%) rename {assets => core/assets}/icons/azure/new icons/03400-icon-service-Entra-Identity.png (100%) rename {assets => core/assets}/icons/azure/new icons/03400-icon-service-Entra-Identity.svg (100%) rename {assets => core/assets}/icons/azure/new icons/034296882-icon-service-Agentic-Web-Apps.png (100%) rename {assets => core/assets}/icons/azure/new icons/034296882-icon-service-Agentic-Web-Apps.svg (100%) rename {assets => core/assets}/icons/azure/new icons/034923704-icon-service-Resiliency.png (100%) rename {assets => core/assets}/icons/azure/new icons/034923704-icon-service-Resiliency.svg (100%) rename {assets => core/assets}/icons/azure/new icons/035556610-icon-service-DDoS-Custom-Policy.png (100%) rename {assets => core/assets}/icons/azure/new icons/035556610-icon-service-DDoS-Custom-Policy.svg (100%) rename {assets => core/assets}/icons/azure/new icons/035712231-icon-service-Disconnected-Operations.png (100%) rename {assets => core/assets}/icons/azure/new icons/035712231-icon-service-Disconnected-Operations.svg (100%) rename {assets => core/assets}/icons/azure/new icons/035824068-icon-service-Azure-Data-Sharing.png (100%) rename {assets => core/assets}/icons/azure/new icons/035824068-icon-service-Azure-Data-Sharing.svg (100%) rename {assets => core/assets}/icons/azure/new icons/037838045-icon-service-AI-Gateway.png (100%) rename {assets => core/assets}/icons/azure/new icons/037838045-icon-service-AI-Gateway.svg (100%) rename {assets => core/assets}/icons/azure/other/00327-icon-service-Azure-Virtual-Desktop.png (100%) rename {assets => core/assets}/icons/azure/other/00327-icon-service-Azure-Virtual-Desktop.svg (100%) rename {assets => core/assets}/icons/azure/other/00412-icon-service-SSH-Keys.png (100%) rename {assets => core/assets}/icons/azure/other/00412-icon-service-SSH-Keys.svg (100%) rename {assets => core/assets}/icons/azure/other/00469-icon-service-Internet-Analyzer-Profiles.png (100%) rename {assets => core/assets}/icons/azure/other/00469-icon-service-Internet-Analyzer-Profiles.svg (100%) rename {assets => core/assets}/icons/azure/other/00559-icon-service-Azure-Cloud-Shell.png (100%) rename {assets => core/assets}/icons/azure/other/00559-icon-service-Azure-Cloud-Shell.svg (100%) rename {assets => core/assets}/icons/azure/other/00712-icon-service-Video-Analyzers.png (100%) rename {assets => core/assets}/icons/azure/other/00712-icon-service-Video-Analyzers.svg (100%) rename {assets => core/assets}/icons/azure/other/00903-icon-service-ExpressRoute-Direct.png (100%) rename {assets => core/assets}/icons/azure/other/00903-icon-service-ExpressRoute-Direct.svg (100%) rename {assets => core/assets}/icons/azure/other/00913-icon-service-Cost-Export.png (100%) rename {assets => core/assets}/icons/azure/other/00913-icon-service-Cost-Export.svg (100%) rename {assets => core/assets}/icons/azure/other/00968-icon-service-Azure-Communication-Services.png (100%) rename {assets => core/assets}/icons/azure/other/00968-icon-service-Azure-Communication-Services.svg (100%) rename {assets => core/assets}/icons/azure/other/00970-icon-service-Peering-Service.png (100%) rename {assets => core/assets}/icons/azure/other/00970-icon-service-Peering-Service.svg (100%) rename {assets => core/assets}/icons/azure/other/01001-icon-service-Azure-Network-Function-Manager-Functions.png (100%) rename {assets => core/assets}/icons/azure/other/01001-icon-service-Azure-Network-Function-Manager-Functions.svg (100%) rename {assets => core/assets}/icons/azure/other/01085-icon-service-Log-Analytics-Query-Pack.png (100%) rename {assets => core/assets}/icons/azure/other/01085-icon-service-Log-Analytics-Query-Pack.svg (100%) rename {assets => core/assets}/icons/azure/other/01088-icon-service-Arc-Kubernetes.png (100%) rename {assets => core/assets}/icons/azure/other/01088-icon-service-Arc-Kubernetes.svg (100%) rename {assets => core/assets}/icons/azure/other/01219-icon-service-Azure-VMware-Solution.png (100%) rename {assets => core/assets}/icons/azure/other/01219-icon-service-Azure-VMware-Solution.svg (100%) rename {assets => core/assets}/icons/azure/other/01285-icon-service-Peerings.png (100%) rename {assets => core/assets}/icons/azure/other/01285-icon-service-Peerings.svg (100%) rename {assets => core/assets}/icons/azure/other/01757-icon-service-Dashboard-Hub.png (100%) rename {assets => core/assets}/icons/azure/other/01757-icon-service-Dashboard-Hub.svg (100%) rename {assets => core/assets}/icons/azure/other/01800-icon-service-Azure-Video-Indexer.png (100%) rename {assets => core/assets}/icons/azure/other/01800-icon-service-Azure-Video-Indexer.svg (100%) rename {assets => core/assets}/icons/azure/other/01846-icon-service-AVS-VM.png (100%) rename {assets => core/assets}/icons/azure/other/01846-icon-service-AVS-VM.svg (100%) rename {assets => core/assets}/icons/azure/other/01848-icon-service-Arc-PostgreSQL .png (100%) rename {assets => core/assets}/icons/azure/other/01848-icon-service-Arc-PostgreSQL .svg (100%) rename {assets => core/assets}/icons/azure/other/01849-icon-service-Arc-SQL-Managed-Instance.png (100%) rename {assets => core/assets}/icons/azure/other/01849-icon-service-Arc-SQL-Managed-Instance.svg (100%) rename {assets => core/assets}/icons/azure/other/01850-icon-service-Arc-SQL-Server.png (100%) rename {assets => core/assets}/icons/azure/other/01850-icon-service-Arc-SQL-Server.svg (100%) rename {assets => core/assets}/icons/azure/other/01857-icon-service-Data-Collection-Rules.png (100%) rename {assets => core/assets}/icons/azure/other/01857-icon-service-Data-Collection-Rules.svg (100%) rename {assets => core/assets}/icons/azure/other/02200-icon-service-Resource-Mover.png (100%) rename {assets => core/assets}/icons/azure/other/02200-icon-service-Resource-Mover.svg (100%) rename {assets => core/assets}/icons/azure/other/02223-icon-service-Azure-Chaos-Studio.png (100%) rename {assets => core/assets}/icons/azure/other/02223-icon-service-Azure-Chaos-Studio.svg (100%) rename {assets => core/assets}/icons/azure/other/02237-icon-service-Network-Managers.png (100%) rename {assets => core/assets}/icons/azure/other/02237-icon-service-Network-Managers.svg (100%) rename {assets => core/assets}/icons/azure/other/02322-icon-service-Dedicated-HSM.png (100%) rename {assets => core/assets}/icons/azure/other/02322-icon-service-Dedicated-HSM.svg (100%) rename {assets => core/assets}/icons/azure/other/02323-icon-service-Modular-Data-Center.png (100%) rename {assets => core/assets}/icons/azure/other/02323-icon-service-Modular-Data-Center.svg (100%) rename {assets => core/assets}/icons/azure/other/02340-icon-service-Template-Specs.png (100%) rename {assets => core/assets}/icons/azure/other/02340-icon-service-Template-Specs.svg (100%) rename {assets => core/assets}/icons/azure/other/02356-icon-service-Arc-Data-services.png (100%) rename {assets => core/assets}/icons/azure/other/02356-icon-service-Arc-Data-services.svg (100%) rename {assets => core/assets}/icons/azure/other/02360-icon-service-Azure-Backup-Center.png (100%) rename {assets => core/assets}/icons/azure/other/02360-icon-service-Azure-Backup-Center.svg (100%) rename {assets => core/assets}/icons/azure/other/02361-icon-service-Backup-Vault.png (100%) rename {assets => core/assets}/icons/azure/other/02361-icon-service-Backup-Vault.svg (100%) rename {assets => core/assets}/icons/azure/other/02475-icon-service-Device-Update-IoT-Hub.png (100%) rename {assets => core/assets}/icons/azure/other/02475-icon-service-Device-Update-IoT-Hub.svg (100%) rename {assets => core/assets}/icons/azure/other/02486-icon-service-Fiji.png (100%) rename {assets => core/assets}/icons/azure/other/02486-icon-service-Fiji.svg (100%) rename {assets => core/assets}/icons/azure/other/02488-icon-service-Azure-Monitor-Dashboard.png (100%) rename {assets => core/assets}/icons/azure/other/02488-icon-service-Azure-Monitor-Dashboard.svg (100%) rename {assets => core/assets}/icons/azure/other/02503-icon-service-SCVMM-Management-Servers.png (100%) rename {assets => core/assets}/icons/azure/other/02503-icon-service-SCVMM-Management-Servers.svg (100%) rename {assets => core/assets}/icons/azure/other/02505-icon-service-Cloud-Services-(extended-support).png (100%) rename {assets => core/assets}/icons/azure/other/02505-icon-service-Cloud-Services-(extended-support).svg (100%) rename {assets => core/assets}/icons/azure/other/02507-icon-service-Azure-Support-Center-Blue.png (100%) rename {assets => core/assets}/icons/azure/other/02507-icon-service-Azure-Support-Center-Blue.svg (100%) rename {assets => core/assets}/icons/azure/other/02515-icon-service-Web-App-+-Database.png (100%) rename {assets => core/assets}/icons/azure/other/02515-icon-service-Web-App-+-Database.svg (100%) rename {assets => core/assets}/icons/azure/other/02518-icon-service-Azure-HPC-Workbenches.png (100%) rename {assets => core/assets}/icons/azure/other/02518-icon-service-Azure-HPC-Workbenches.svg (100%) rename {assets => core/assets}/icons/azure/other/02560-icon-service-Disk-Pool.png (100%) rename {assets => core/assets}/icons/azure/other/02560-icon-service-Disk-Pool.svg (100%) rename {assets => core/assets}/icons/azure/other/02561-icon-service-Bare-Metal-Infrastructure.png (100%) rename {assets => core/assets}/icons/azure/other/02561-icon-service-Bare-Metal-Infrastructure.svg (100%) rename {assets => core/assets}/icons/azure/other/02579-icon-service-Private-Endpoints.png (100%) rename {assets => core/assets}/icons/azure/other/02579-icon-service-Private-Endpoints.svg (100%) rename {assets => core/assets}/icons/azure/other/02602-icon-service-Open-Supply-Chain-Platform.png (100%) rename {assets => core/assets}/icons/azure/other/02602-icon-service-Open-Supply-Chain-Platform.svg (100%) rename {assets => core/assets}/icons/azure/other/02625-icon-service-Aquila.png (100%) rename {assets => core/assets}/icons/azure/other/02625-icon-service-Aquila.svg (100%) rename {assets => core/assets}/icons/azure/other/02638-icon-service-Reserved-Capacity.png (100%) rename {assets => core/assets}/icons/azure/other/02638-icon-service-Reserved-Capacity.svg (100%) rename {assets => core/assets}/icons/azure/other/02654-icon-service-Custom-IP-Prefix.png (100%) rename {assets => core/assets}/icons/azure/other/02654-icon-service-Custom-IP-Prefix.svg (100%) rename {assets => core/assets}/icons/azure/other/02658-icon-service-FHIR-Service.png (100%) rename {assets => core/assets}/icons/azure/other/02658-icon-service-FHIR-Service.svg (100%) rename {assets => core/assets}/icons/azure/other/02660-icon-service-MedTech-Service.png (100%) rename {assets => core/assets}/icons/azure/other/02660-icon-service-MedTech-Service.svg (100%) rename {assets => core/assets}/icons/azure/other/02663-icon-service-Managed-Instance-Apache-Cassandra.png (100%) rename {assets => core/assets}/icons/azure/other/02663-icon-service-Managed-Instance-Apache-Cassandra.svg (100%) rename {assets => core/assets}/icons/azure/other/02668-icon-service-Confidential-Ledgers.png (100%) rename {assets => core/assets}/icons/azure/other/02668-icon-service-Confidential-Ledgers.svg (100%) rename {assets => core/assets}/icons/azure/other/02681-icon-service-AAD-Licenses.png (100%) rename {assets => core/assets}/icons/azure/other/02681-icon-service-AAD-Licenses.svg (100%) rename {assets => core/assets}/icons/azure/other/02681-icon-service-Entra-Identity-Licenses.png (100%) rename {assets => core/assets}/icons/azure/other/02681-icon-service-Entra-Identity-Licenses.svg (100%) rename {assets => core/assets}/icons/azure/other/02691-icon-service-Test-Base.png (100%) rename {assets => core/assets}/icons/azure/other/02691-icon-service-Test-Base.svg (100%) rename {assets => core/assets}/icons/azure/other/02697-icon-service-Azure-Orbital.png (100%) rename {assets => core/assets}/icons/azure/other/02697-icon-service-Azure-Orbital.svg (100%) rename {assets => core/assets}/icons/azure/other/02732-icon-service-Capacity-Reservation-Groups.png (100%) rename {assets => core/assets}/icons/azure/other/02732-icon-service-Capacity-Reservation-Groups.svg (100%) rename {assets => core/assets}/icons/azure/other/02733-icon-service-Windows-Notification-Services.png (100%) rename {assets => core/assets}/icons/azure/other/02733-icon-service-Windows-Notification-Services.svg (100%) rename {assets => core/assets}/icons/azure/other/02740-icon-service-Azure-Network-Function-Manager.png (100%) rename {assets => core/assets}/icons/azure/other/02740-icon-service-Azure-Network-Function-Manager.svg (100%) rename {assets => core/assets}/icons/azure/other/02793-icon-service-Mission-Landing-Zone.png (100%) rename {assets => core/assets}/icons/azure/other/02793-icon-service-Mission-Landing-Zone.svg (100%) rename {assets => core/assets}/icons/azure/other/02794-icon-service-Mobile-Networks.png (100%) rename {assets => core/assets}/icons/azure/other/02794-icon-service-Mobile-Networks.svg (100%) rename {assets => core/assets}/icons/azure/other/02805-icon-service-VM-App-Definitions.png (100%) rename {assets => core/assets}/icons/azure/other/02805-icon-service-VM-App-Definitions.svg (100%) rename {assets => core/assets}/icons/azure/other/02806-icon-service-VM-App-Versions.png (100%) rename {assets => core/assets}/icons/azure/other/02806-icon-service-VM-App-Versions.svg (100%) rename {assets => core/assets}/icons/azure/other/02810-icon-service-Azure-Edge-Hardware-Center.png (100%) rename {assets => core/assets}/icons/azure/other/02810-icon-service-Azure-Edge-Hardware-Center.svg (100%) rename {assets => core/assets}/icons/azure/other/02819-icon-service-Resource-Guard.png (100%) rename {assets => core/assets}/icons/azure/other/02819-icon-service-Resource-Guard.svg (100%) rename {assets => core/assets}/icons/azure/other/02828-icon-service-Ceres.png (100%) rename {assets => core/assets}/icons/azure/other/02828-icon-service-Ceres.svg (100%) rename {assets => core/assets}/icons/azure/other/02842-icon-service-Azurite.png (100%) rename {assets => core/assets}/icons/azure/other/02842-icon-service-Azurite.svg (100%) rename {assets => core/assets}/icons/azure/other/02846-icon-service-Update-Management-Center.png (100%) rename {assets => core/assets}/icons/azure/other/02846-icon-service-Update-Management-Center.svg (100%) rename {assets => core/assets}/icons/azure/other/02864-icon-service-Azure-Compute-Galleries.png (100%) rename {assets => core/assets}/icons/azure/other/02864-icon-service-Azure-Compute-Galleries.svg (100%) rename {assets => core/assets}/icons/azure/other/02865-icon-service-Community-Images.png (100%) rename {assets => core/assets}/icons/azure/other/02865-icon-service-Community-Images.svg (100%) rename {assets => core/assets}/icons/azure/other/02866-icon-service-VM-Image-Version.png (100%) rename {assets => core/assets}/icons/azure/other/02866-icon-service-VM-Image-Version.svg (100%) rename {assets => core/assets}/icons/azure/other/02880-icon-service-Savings-Plans.png (100%) rename {assets => core/assets}/icons/azure/other/02880-icon-service-Savings-Plans.svg (100%) rename {assets => core/assets}/icons/azure/other/02884-icon-service-Worker-Container-App.png (100%) rename {assets => core/assets}/icons/azure/other/02884-icon-service-Worker-Container-App.svg (100%) rename {assets => core/assets}/icons/azure/other/02905-icon-service-Azure-Managed-Grafana.png (100%) rename {assets => core/assets}/icons/azure/other/02905-icon-service-Azure-Managed-Grafana.svg (100%) rename {assets => core/assets}/icons/azure/other/02908-icon-service-Targets-Management.png (100%) rename {assets => core/assets}/icons/azure/other/02908-icon-service-Targets-Management.svg (100%) rename {assets => core/assets}/icons/azure/other/02909-icon-service-Storage-Functions.png (100%) rename {assets => core/assets}/icons/azure/other/02909-icon-service-Storage-Functions.svg (100%) rename {assets => core/assets}/icons/azure/other/02909-icon-service-Storage-Tasks.png (100%) rename {assets => core/assets}/icons/azure/other/02909-icon-service-Storage-Tasks.svg (100%) rename {assets => core/assets}/icons/azure/other/02922-icon-service-Sonic-Dash.png (100%) rename {assets => core/assets}/icons/azure/other/02922-icon-service-Sonic-Dash.svg (100%) rename {assets => core/assets}/icons/azure/other/02931-icon-service-Compliance-Center.png (100%) rename {assets => core/assets}/icons/azure/other/02931-icon-service-Compliance-Center.svg (100%) rename {assets => core/assets}/icons/azure/other/02939-icon-service-Network-Security-Perimeters.png (100%) rename {assets => core/assets}/icons/azure/other/02939-icon-service-Network-Security-Perimeters.svg (100%) rename {assets => core/assets}/icons/azure/other/02944-icon-service-Azure-Load-Testing.png (100%) rename {assets => core/assets}/icons/azure/other/02944-icon-service-Azure-Load-Testing.svg (100%) rename {assets => core/assets}/icons/azure/other/02949-icon-service-Virtual-Visits-Builder.png (100%) rename {assets => core/assets}/icons/azure/other/02949-icon-service-Virtual-Visits-Builder.svg (100%) rename {assets => core/assets}/icons/azure/other/02951-icon-service-Azure-Quotas.png (100%) rename {assets => core/assets}/icons/azure/other/02951-icon-service-Azure-Quotas.svg (100%) rename {assets => core/assets}/icons/azure/other/02989-icon-service-Container-Apps-Environments.png (100%) rename {assets => core/assets}/icons/azure/other/02989-icon-service-Container-Apps-Environments.svg (100%) rename {assets => core/assets}/icons/azure/other/03071-icon-service-App-Compliance-Automation.png (100%) rename {assets => core/assets}/icons/azure/other/03071-icon-service-App-Compliance-Automation.svg (100%) rename {assets => core/assets}/icons/azure/other/03089-icon-service-Virtual-Instance-for-SAP.png (100%) rename {assets => core/assets}/icons/azure/other/03089-icon-service-Virtual-Instance-for-SAP.svg (100%) rename {assets => core/assets}/icons/azure/other/03090-icon-service-Azure-Center-for-SAP.png (100%) rename {assets => core/assets}/icons/azure/other/03090-icon-service-Azure-Center-for-SAP.svg (100%) rename {assets => core/assets}/icons/azure/other/03091-icon-service-Azure-Storage-Mover.png (100%) rename {assets => core/assets}/icons/azure/other/03091-icon-service-Azure-Storage-Mover.svg (100%) rename {assets => core/assets}/icons/azure/other/03092-icon-service-Central-Service-Instance-For-SAP.png (100%) rename {assets => core/assets}/icons/azure/other/03092-icon-service-Central-Service-Instance-For-SAP.svg (100%) rename {assets => core/assets}/icons/azure/other/03134-icon-service-Kubernetes-Fleet-Manager.png (100%) rename {assets => core/assets}/icons/azure/other/03134-icon-service-Kubernetes-Fleet-Manager.svg (100%) rename {assets => core/assets}/icons/azure/other/03137-icon-service-Express-Route-Traffic-Collector.png (100%) rename {assets => core/assets}/icons/azure/other/03137-icon-service-Express-Route-Traffic-Collector.svg (100%) rename {assets => core/assets}/icons/azure/other/03175-icon-service-Database-Instance-For-SAP.png (100%) rename {assets => core/assets}/icons/azure/other/03175-icon-service-Database-Instance-For-SAP.svg (100%) rename {assets => core/assets}/icons/azure/other/03190-icon-service-Elastic-SAN.png (100%) rename {assets => core/assets}/icons/azure/other/03190-icon-service-Elastic-SAN.svg (100%) rename {assets => core/assets}/icons/azure/other/03250-icon-service-Microsoft-Dev-Box.png (100%) rename {assets => core/assets}/icons/azure/other/03250-icon-service-Microsoft-Dev-Box.svg (100%) rename {assets => core/assets}/icons/azure/other/03251-icon-service-Azure-Deployment-Environments.png (100%) rename {assets => core/assets}/icons/azure/other/03251-icon-service-Azure-Deployment-Environments.svg (100%) rename {assets => core/assets}/icons/azure/other/032869111-icon-service-Microsoft-Discovery.png (100%) rename {assets => core/assets}/icons/azure/other/032869111-icon-service-Microsoft-Discovery.svg (100%) rename {assets => core/assets}/icons/azure/other/03308-icon-service-Azure-Dev-Tunnels.png (100%) rename {assets => core/assets}/icons/azure/other/03308-icon-service-Azure-Dev-Tunnels.svg (100%) rename {assets => core/assets}/icons/azure/other/033112809-icon-service-promethus.png (100%) rename {assets => core/assets}/icons/azure/other/033112809-icon-service-promethus.svg (100%) rename {assets => core/assets}/icons/azure/other/03314-icon-service-Azure-Sustainability.png (100%) rename {assets => core/assets}/icons/azure/other/03314-icon-service-Azure-Sustainability.svg (100%) rename {assets => core/assets}/icons/azure/other/03361-icon-service-IcM-Troubleshooting.png (100%) rename {assets => core/assets}/icons/azure/other/03361-icon-service-IcM-Troubleshooting.svg (100%) rename {assets => core/assets}/icons/azure/other/03365-icon-service-OSConfig.png (100%) rename {assets => core/assets}/icons/azure/other/03365-icon-service-OSConfig.svg (100%) rename {assets => core/assets}/icons/azure/other/03394-icon-service-Azure-Enclaves.png (100%) rename {assets => core/assets}/icons/azure/other/03394-icon-service-Azure-Enclaves.svg (100%) rename {assets => core/assets}/icons/azure/other/03399-icon-service-AKS-Istio.png (100%) rename {assets => core/assets}/icons/azure/other/03399-icon-service-AKS-Istio.svg (100%) rename {assets => core/assets}/icons/azure/other/03402-icon-service-Defender-CM-Local-Manager.png (100%) rename {assets => core/assets}/icons/azure/other/03402-icon-service-Defender-CM-Local-Manager.svg (100%) rename {assets => core/assets}/icons/azure/other/03403-icon-service-Defender-External-Management.png (100%) rename {assets => core/assets}/icons/azure/other/03403-icon-service-Defender-External-Management.svg (100%) rename {assets => core/assets}/icons/azure/other/03404-icon-service-Defender-Freezer-Monitor.png (100%) rename {assets => core/assets}/icons/azure/other/03404-icon-service-Defender-Freezer-Monitor.svg (100%) rename {assets => core/assets}/icons/azure/other/03405-icon-service-Defender-Historian.png (100%) rename {assets => core/assets}/icons/azure/other/03405-icon-service-Defender-Historian.svg (100%) rename {assets => core/assets}/icons/azure/other/03406-icon-service-Defender-HMI.png (100%) rename {assets => core/assets}/icons/azure/other/03406-icon-service-Defender-HMI.svg (100%) rename {assets => core/assets}/icons/azure/other/03407-icon-service-Defender-Marquee.png (100%) rename {assets => core/assets}/icons/azure/other/03407-icon-service-Defender-Marquee.svg (100%) rename {assets => core/assets}/icons/azure/other/03408-icon-service-Defender-Robot-Controller.png (100%) rename {assets => core/assets}/icons/azure/other/03408-icon-service-Defender-Robot-Controller.svg (100%) rename {assets => core/assets}/icons/azure/other/03409-icon-service-Defender-Sensor.png (100%) rename {assets => core/assets}/icons/azure/other/03409-icon-service-Defender-Sensor.svg (100%) rename {assets => core/assets}/icons/azure/other/03410-icon-service-Defender-Slot.png (100%) rename {assets => core/assets}/icons/azure/other/03410-icon-service-Defender-Slot.svg (100%) rename {assets => core/assets}/icons/azure/other/03411-icon-service-Defender-Web-Guiding-System.png (100%) rename {assets => core/assets}/icons/azure/other/03411-icon-service-Defender-Web-Guiding-System.svg (100%) rename {assets => core/assets}/icons/azure/other/03412-icon-service-Defender-DCS-Controller.png (100%) rename {assets => core/assets}/icons/azure/other/03412-icon-service-Defender-DCS-Controller.svg (100%) rename {assets => core/assets}/icons/azure/other/03413-icon-service-Defender-Distributer-Control-System.png (100%) rename {assets => core/assets}/icons/azure/other/03413-icon-service-Defender-Distributer-Control-System.svg (100%) rename {assets => core/assets}/icons/azure/other/03414-icon-service-Defender-Engineering-Station.png (100%) rename {assets => core/assets}/icons/azure/other/03414-icon-service-Defender-Engineering-Station.svg (100%) rename {assets => core/assets}/icons/azure/other/03415-icon-service-Defender-Industrial-Packaging-System.png (100%) rename {assets => core/assets}/icons/azure/other/03415-icon-service-Defender-Industrial-Packaging-System.svg (100%) rename {assets => core/assets}/icons/azure/other/03416-icon-service-Defender-Industrial-Printer.png (100%) rename {assets => core/assets}/icons/azure/other/03416-icon-service-Defender-Industrial-Printer.svg (100%) rename {assets => core/assets}/icons/azure/other/03417-icon-service-Defender-Industrial-Scale-System.png (100%) rename {assets => core/assets}/icons/azure/other/03417-icon-service-Defender-Industrial-Scale-System.svg (100%) rename {assets => core/assets}/icons/azure/other/03418-icon-service-Defender-Industrial-Robot.png (100%) rename {assets => core/assets}/icons/azure/other/03418-icon-service-Defender-Industrial-Robot.svg (100%) rename {assets => core/assets}/icons/azure/other/03419-icon-service-Defender-Meter.png (100%) rename {assets => core/assets}/icons/azure/other/03419-icon-service-Defender-Meter.svg (100%) rename {assets => core/assets}/icons/azure/other/03420-icon-service-Defender-PLC.png (100%) rename {assets => core/assets}/icons/azure/other/03420-icon-service-Defender-PLC.svg (100%) rename {assets => core/assets}/icons/azure/other/03421-icon-service-Defender-Pneumatic-Device.png (100%) rename {assets => core/assets}/icons/azure/other/03421-icon-service-Defender-Pneumatic-Device.svg (100%) rename {assets => core/assets}/icons/azure/other/03422-icon-service-Defender-Programable-Board.png (100%) rename {assets => core/assets}/icons/azure/other/03422-icon-service-Defender-Programable-Board.svg (100%) rename {assets => core/assets}/icons/azure/other/03423-icon-service-Defender-Relay.png (100%) rename {assets => core/assets}/icons/azure/other/03423-icon-service-Defender-Relay.svg (100%) rename {assets => core/assets}/icons/azure/other/03424-icon-service-Defender-RTU.png (100%) rename {assets => core/assets}/icons/azure/other/03424-icon-service-Defender-RTU.svg (100%) rename {assets => core/assets}/icons/azure/other/03468-icon-service-HDI-AKS-Cluster.png (100%) rename {assets => core/assets}/icons/azure/other/03468-icon-service-HDI-AKS-Cluster.svg (100%) rename {assets => core/assets}/icons/azure/other/03495-icon-service-Azure-Consumption-Commitment.png (100%) rename {assets => core/assets}/icons/azure/other/03495-icon-service-Azure-Consumption-Commitment.svg (100%) rename {assets => core/assets}/icons/azure/other/03528-icon-service-Monitor-Health-Models.png (100%) rename {assets => core/assets}/icons/azure/other/03528-icon-service-Monitor-Health-Models.svg (100%) rename {assets => core/assets}/icons/azure/other/03563-icon-service-WAC-Installer.png (100%) rename {assets => core/assets}/icons/azure/other/03563-icon-service-WAC-Installer.svg (100%) rename {assets => core/assets}/icons/azure/other/03633-icon-service-Breeze.png (100%) rename {assets => core/assets}/icons/azure/other/03633-icon-service-Breeze.svg (100%) rename {assets => core/assets}/icons/azure/other/10018-icon-service-Azure-A.png (100%) rename {assets => core/assets}/icons/azure/other/10018-icon-service-Azure-A.svg (100%) rename {assets => core/assets}/icons/azure/other/10077-icon-service-Local-Network-Gateways.png (100%) rename {assets => core/assets}/icons/azure/other/10077-icon-service-Local-Network-Gateways.svg (100%) rename {assets => core/assets}/icons/azure/other/10117-icon-service-Edge-Management.png (100%) rename {assets => core/assets}/icons/azure/other/10117-icon-service-Edge-Management.svg (100%) rename {assets => core/assets}/icons/azure/other/10139-icon-service-Instance-Pools.png (100%) rename {assets => core/assets}/icons/azure/other/10139-icon-service-Instance-Pools.svg (100%) rename {assets => core/assets}/icons/azure/other/10190-icon-service-Azure-Sphere.png (100%) rename {assets => core/assets}/icons/azure/other/10190-icon-service-Azure-Sphere.svg (100%) rename {assets => core/assets}/icons/azure/other/10232-icon-service-App-Registrations.png (100%) rename {assets => core/assets}/icons/azure/other/10232-icon-service-App-Registrations.svg (100%) rename {assets => core/assets}/icons/azure/other/10334-icon-service-Exchange-On-Premises-Access.png (100%) rename {assets => core/assets}/icons/azure/other/10334-icon-service-Exchange-On-Premises-Access.svg (100%) rename {assets => core/assets}/icons/azure/other/10355-icon-service-WAC.png (100%) rename {assets => core/assets}/icons/azure/other/10355-icon-service-WAC.svg (100%) rename {assets => core/assets}/icons/azure/other/10422-icon-service-AzureAttestation.png (100%) rename {assets => core/assets}/icons/azure/other/10422-icon-service-AzureAttestation.svg (100%) rename {assets => core/assets}/icons/azure/other/10778-icon-service-RTOS.png (100%) rename {assets => core/assets}/icons/azure/other/10778-icon-service-RTOS.svg (100%) rename {assets => core/assets}/icons/azure/other/10846-icon-service-Web-Jobs.png (100%) rename {assets => core/assets}/icons/azure/other/10846-icon-service-Web-Jobs.svg (100%) rename {assets => core/assets}/icons/azure/security/00378-icon-service-Detonation.png (100%) rename {assets => core/assets}/icons/azure/security/00378-icon-service-Detonation.svg (100%) rename {assets => core/assets}/icons/azure/security/02247-icon-service-Microsoft-Defender-for-IoT.png (100%) rename {assets => core/assets}/icons/azure/security/02247-icon-service-Microsoft-Defender-for-IoT.svg (100%) rename {assets => core/assets}/icons/azure/security/02251-icon-service-Azure-AD-Privleged-Identity-Management.png (100%) rename {assets => core/assets}/icons/azure/security/02251-icon-service-Azure-AD-Privleged-Identity-Management.svg (100%) rename {assets => core/assets}/icons/azure/security/03336-icon-service-Microsoft-Defender-EASM.png (100%) rename {assets => core/assets}/icons/azure/security/03336-icon-service-Microsoft-Defender-EASM.svg (100%) rename {assets => core/assets}/icons/azure/security/03340-icon-service-Identity-Secure-Score.png (100%) rename {assets => core/assets}/icons/azure/security/03340-icon-service-Identity-Secure-Score.svg (100%) rename {assets => core/assets}/icons/azure/security/03341-icon-service-Azure-AD-Risky-Signins.png (100%) rename {assets => core/assets}/icons/azure/security/03341-icon-service-Azure-AD-Risky-Signins.svg (100%) rename {assets => core/assets}/icons/azure/security/03341-icon-service-Entra-Identity-Risky-Signins.png (100%) rename {assets => core/assets}/icons/azure/security/03341-icon-service-Entra-Identity-Risky-Signins.svg (100%) rename {assets => core/assets}/icons/azure/security/03342-icon-service-Azure-AD-Risky-Users.png (100%) rename {assets => core/assets}/icons/azure/security/03342-icon-service-Azure-AD-Risky-Users.svg (100%) rename {assets => core/assets}/icons/azure/security/03342-icon-service-Entra-Identity-Risky-Users.png (100%) rename {assets => core/assets}/icons/azure/security/03342-icon-service-Entra-Identity-Risky-Users.svg (100%) rename {assets => core/assets}/icons/azure/security/03343-icon-service-Azure-AD-Authentication-Methods.png (100%) rename {assets => core/assets}/icons/azure/security/03343-icon-service-Azure-AD-Authentication-Methods.svg (100%) rename {assets => core/assets}/icons/azure/security/03344-icon-service-Multifactor-Authentication.png (100%) rename {assets => core/assets}/icons/azure/security/03344-icon-service-Multifactor-Authentication.svg (100%) rename {assets => core/assets}/icons/azure/security/10229-icon-service-Azure-Information-Protection.png (100%) rename {assets => core/assets}/icons/azure/security/10229-icon-service-Azure-Information-Protection.svg (100%) rename {assets => core/assets}/icons/azure/security/10231-icon-service-Azure-AD-Identity-Protection.png (100%) rename {assets => core/assets}/icons/azure/security/10231-icon-service-Azure-AD-Identity-Protection.svg (100%) rename {assets => core/assets}/icons/azure/security/10233-icon-service-Conditional-Access.png (100%) rename {assets => core/assets}/icons/azure/security/10233-icon-service-Conditional-Access.svg (100%) rename {assets => core/assets}/icons/azure/security/10241-icon-service-Microsoft-Defender-for-Cloud.png (100%) rename {assets => core/assets}/icons/azure/security/10241-icon-service-Microsoft-Defender-for-Cloud.svg (100%) rename {assets => core/assets}/icons/azure/security/10244-icon-service-Application-Security-Groups.png (100%) rename {assets => core/assets}/icons/azure/security/10244-icon-service-Application-Security-Groups.svg (100%) rename {assets => core/assets}/icons/azure/security/10245-icon-service-Key-Vaults.png (100%) rename {assets => core/assets}/icons/azure/security/10245-icon-service-Key-Vaults.svg (100%) rename {assets => core/assets}/icons/azure/security/10248-icon-service-Azure-Sentinel.png (100%) rename {assets => core/assets}/icons/azure/security/10248-icon-service-Azure-Sentinel.svg (100%) rename {assets => core/assets}/icons/azure/security/10433-icon-service-User-Settings.png (100%) rename {assets => core/assets}/icons/azure/security/10433-icon-service-User-Settings.svg (100%) rename {assets => core/assets}/icons/azure/security/10572-icon-service-ExtendedSecurityUpdates.png (100%) rename {assets => core/assets}/icons/azure/security/10572-icon-service-ExtendedSecurityUpdates.svg (100%) rename {assets => core/assets}/icons/azure/storage/00017-icon-service-Recovery-Services-Vaults.png (100%) rename {assets => core/assets}/icons/azure/storage/00017-icon-service-Recovery-Services-Vaults.svg (100%) rename {assets => core/assets}/icons/azure/storage/00691-icon-service-Azure-Databox-Gateway.png (100%) rename {assets => core/assets}/icons/azure/storage/00691-icon-service-Azure-Databox-Gateway.svg (100%) rename {assets => core/assets}/icons/azure/storage/00776-icon-service-Azure-HCP-Cache.png (100%) rename {assets => core/assets}/icons/azure/storage/00776-icon-service-Azure-HCP-Cache.svg (100%) rename {assets => core/assets}/icons/azure/storage/029203566-icon-service-Data-Virtualization.png (100%) rename {assets => core/assets}/icons/azure/storage/029203566-icon-service-Data-Virtualization.svg (100%) rename {assets => core/assets}/icons/azure/storage/030486388-icon-service-Storage-Hubs.png (100%) rename {assets => core/assets}/icons/azure/storage/030486388-icon-service-Storage-Hubs.svg (100%) rename {assets => core/assets}/icons/azure/storage/03502-icon-service-Storage-Actions.png (100%) rename {assets => core/assets}/icons/azure/storage/03502-icon-service-Storage-Actions.svg (100%) rename {assets => core/assets}/icons/azure/storage/03549-icon-service-Managed-File-Shares.png (100%) rename {assets => core/assets}/icons/azure/storage/03549-icon-service-Managed-File-Shares.svg (100%) rename {assets => core/assets}/icons/azure/storage/10086-icon-service-Storage-Accounts.png (100%) rename {assets => core/assets}/icons/azure/storage/10086-icon-service-Storage-Accounts.svg (100%) rename {assets => core/assets}/icons/azure/storage/10087-icon-service-Storage-Accounts-(Classic).png (100%) rename {assets => core/assets}/icons/azure/storage/10087-icon-service-Storage-Accounts-(Classic).svg (100%) rename {assets => core/assets}/icons/azure/storage/10089-icon-service-StorSimple-Device-Managers.png (100%) rename {assets => core/assets}/icons/azure/storage/10089-icon-service-StorSimple-Device-Managers.svg (100%) rename {assets => core/assets}/icons/azure/storage/10090-icon-service-Data-Lake-Storage-Gen1.png (100%) rename {assets => core/assets}/icons/azure/storage/10090-icon-service-Data-Lake-Storage-Gen1.svg (100%) rename {assets => core/assets}/icons/azure/storage/10091-icon-service-Storage-Explorer.png (100%) rename {assets => core/assets}/icons/azure/storage/10091-icon-service-Storage-Explorer.svg (100%) rename {assets => core/assets}/icons/azure/storage/10092-icon-service-StorSimple-Data-Managers.png (100%) rename {assets => core/assets}/icons/azure/storage/10092-icon-service-StorSimple-Data-Managers.svg (100%) rename {assets => core/assets}/icons/azure/storage/10093-icon-service-Storage-Sync-Services.png (100%) rename {assets => core/assets}/icons/azure/storage/10093-icon-service-Storage-Sync-Services.svg (100%) rename {assets => core/assets}/icons/azure/storage/10094-icon-service-Data-Box.png (100%) rename {assets => core/assets}/icons/azure/storage/10094-icon-service-Data-Box.svg (100%) rename {assets => core/assets}/icons/azure/storage/10095-icon-service-Azure-Stack-Edge.png (100%) rename {assets => core/assets}/icons/azure/storage/10095-icon-service-Azure-Stack-Edge.svg (100%) rename {assets => core/assets}/icons/azure/storage/10096-icon-service-Azure-NetApp-Files.png (100%) rename {assets => core/assets}/icons/azure/storage/10096-icon-service-Azure-NetApp-Files.svg (100%) rename {assets => core/assets}/icons/azure/storage/10097-icon-service-Data-Share-Invitations.png (100%) rename {assets => core/assets}/icons/azure/storage/10097-icon-service-Data-Share-Invitations.svg (100%) rename {assets => core/assets}/icons/azure/storage/10098-icon-service-Data-Shares.png (100%) rename {assets => core/assets}/icons/azure/storage/10098-icon-service-Data-Shares.svg (100%) rename {assets => core/assets}/icons/azure/storage/10100-icon-service-Import-Export-Jobs.png (100%) rename {assets => core/assets}/icons/azure/storage/10100-icon-service-Import-Export-Jobs.svg (100%) rename {assets => core/assets}/icons/azure/storage/10400-icon-service-Azure-Fileshares.png (100%) rename {assets => core/assets}/icons/azure/storage/10400-icon-service-Azure-Fileshares.svg (100%) rename {assets => core/assets}/icons/azure/web/00046-icon-service-App-Service-Plans.png (100%) rename {assets => core/assets}/icons/azure/web/00046-icon-service-App-Service-Plans.svg (100%) rename {assets => core/assets}/icons/azure/web/00049-icon-service-App-Service-Certificates.png (100%) rename {assets => core/assets}/icons/azure/web/00049-icon-service-App-Service-Certificates.svg (100%) rename {assets => core/assets}/icons/azure/web/00050-icon-service-App-Service-Domains.png (100%) rename {assets => core/assets}/icons/azure/web/00050-icon-service-App-Service-Domains.svg (100%) rename {assets => core/assets}/icons/azure/web/01007-icon-service-Static-Apps.png (100%) rename {assets => core/assets}/icons/azure/web/01007-icon-service-Static-Apps.svg (100%) rename {assets => core/assets}/icons/azure/web/03291-icon-service-API-Center.png (100%) rename {assets => core/assets}/icons/azure/web/03291-icon-service-API-Center.svg (100%) rename {assets => core/assets}/icons/azure/web/03335-icon-service-Power-Platform.png (100%) rename {assets => core/assets}/icons/azure/web/03335-icon-service-Power-Platform.svg (100%) rename {assets => core/assets}/icons/azure/web/03397-icon-service-App-Space.png (100%) rename {assets => core/assets}/icons/azure/web/03397-icon-service-App-Space.svg (100%) rename {assets => core/assets}/icons/azure/web/03595-icon-service-App-Space-Component.png (100%) rename {assets => core/assets}/icons/azure/web/03595-icon-service-App-Space-Component.svg (100%) rename {assets => core/assets}/icons/azure/web/10035-icon-service-App-Services.png (100%) rename {assets => core/assets}/icons/azure/web/10035-icon-service-App-Services.svg (100%) rename {assets => core/assets}/icons/azure/web/10042-icon-service-API-Management-Services.png (100%) rename {assets => core/assets}/icons/azure/web/10042-icon-service-API-Management-Services.svg (100%) rename {assets => core/assets}/icons/azure/web/10044-icon-service-Cognitive-Search.png (100%) rename {assets => core/assets}/icons/azure/web/10044-icon-service-Cognitive-Search.svg (100%) rename {assets => core/assets}/icons/azure/web/10047-icon-service-App-Service-Environments.png (100%) rename {assets => core/assets}/icons/azure/web/10047-icon-service-App-Service-Environments.svg (100%) rename {assets => core/assets}/icons/azure/web/10048-icon-service-API-Connections.png (100%) rename {assets => core/assets}/icons/azure/web/10048-icon-service-API-Connections.svg (100%) rename {assets => core/assets}/icons/azure/web/10052-icon-service-SignalR.png (100%) rename {assets => core/assets}/icons/azure/web/10052-icon-service-SignalR.svg (100%) rename {assets => core/assets}/icons/azure/web/10053-icon-service-Notification-Hub-Namespaces.png (100%) rename {assets => core/assets}/icons/azure/web/10053-icon-service-Notification-Hub-Namespaces.svg (100%) rename {assets => core/assets}/icons/azure/web/10073-icon-service-Front-Door-and-CDN-Profiles.png (100%) rename {assets => core/assets}/icons/azure/web/10073-icon-service-Front-Door-and-CDN-Profiles.svg (100%) rename {assets => core/assets}/icons/azure/web/10162-icon-service-Cognitive-Services.png (100%) rename {assets => core/assets}/icons/azure/web/10162-icon-service-Cognitive-Services.svg (100%) rename {assets => core/assets}/icons/azure/web/10309-icon-service-Azure-Media-Service.png (100%) rename {assets => core/assets}/icons/azure/web/10309-icon-service-Azure-Media-Service.svg (100%) rename {assets => core/assets}/icons/azure/web/10370-icon-service-Azure-Spring-Apps.png (100%) rename {assets => core/assets}/icons/azure/web/10370-icon-service-Azure-Spring-Apps.svg (100%) rename {assets => core/assets}/icons/misc/no_image.png (100%) rename {assets => core/assets}/icons/severity/high.png (100%) rename {assets => core/assets}/icons/severity/low.png (100%) rename {assets => core/assets}/icons/severity/medium.png (100%) rename {assets => core/assets}/img/logo/favicon.ico (100%) rename {assets => core/assets}/img/logo/favicon.png (100%) rename {assets => core/assets}/img/logo/logo.png (100%) rename {assets => core/assets}/img/logo/report_logo.png (100%) rename {assets => core/assets}/template/egress.html (100%) rename {assets => core/assets}/template/index.html (100%) create mode 100644 core/resources.py create mode 100644 tests/fixtures/smoke/aws.tfstate create mode 100644 tests/make_smoke_dataset.py create mode 100644 tests/test_packaging.py diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 79e7ed0..cb61abc 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -70,3 +70,47 @@ jobs: - name: Unit tests run: python -m unittest discover -s tests + + install-smoke: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install the package (not editable) + run: | + python -m venv "$RUNNER_TEMP/venv" + "$RUNNER_TEMP/venv/bin/python" -m pip install --upgrade pip + "$RUNNER_TEMP/venv/bin/python" -m pip install . + + - name: Build the smoke dataset + run: '"$RUNNER_TEMP/venv/bin/python" -m tests.make_smoke_dataset "$RUNNER_TEMP/smoke.db"' + + - name: Run from an unrelated directory + env: + ESC_EXIT_STRATEGY: "1" + ESC_ASSESSMENT_TYPE: "1" + run: | + cloudexit="$RUNNER_TEMP/venv/bin/cloudexit" + workdir="$(mktemp -d)" + cd "$workdir" + + "$cloudexit" --version + "$cloudexit" config path + "$cloudexit" aws \ + --tfstate "$GITHUB_WORKSPACE/tests/fixtures/smoke/aws.tfstate" \ + --catalogue "$RUNNER_TEMP/smoke.db" \ + --non-interactive + + report="$(ls -d reports/*/ | head -n 1)" + test -f "${report}index.html" + test -f "${report}report.pdf" + test -d "${report}assets/css" + test -d "${report}assets/img" + test -n "$(find "${report}assets/icons/aws" -name '*.png' | head -n 1)" + test "$(wc -c < "${report}report.pdf")" -gt 10000 diff --git a/.gitignore b/.gitignore index beed141..b1aa32b 100644 --- a/.gitignore +++ b/.gitignore @@ -106,6 +106,7 @@ config/* # Terraform state and downloaded datasets *.tfstate +!tests/fixtures/smoke/aws.tfstate datasets/ # TypeScript cache diff --git a/README.md b/README.md index 41eef0c..dfeea92 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ With `--egress-names hashed` each egress resource name is replaced by an HMAC-SH ## The report -Every run writes the same three files: `report.pdf`, `index.html` and `raw_data/assessment_result.json`. The PDF is the Basic Executive Summary, set in IBM Plex (SIL Open Font License, in `assets/fonts/`). It opens with a cover and a document information page that record where the data came from: the cloudexit version, the run ID, the data catalogue version and hash, and either a link to the platform's reports or the statement that the data was self-collected. The same facts are in the PDF metadata and in a `provenance` object in the JSON report. The last page says what the full assessment on a platform adds. +Every run writes the same three files: `report.pdf`, `index.html` and `raw_data/assessment_result.json`. The PDF is the Basic Executive Summary, set in IBM Plex (SIL Open Font License, in `core/assets/fonts/`). It opens with a cover and a document information page that record where the data came from: the cloudexit version, the run ID, the data catalogue version and hash, and either a link to the platform's reports or the statement that the data was self-collected. The same facts are in the PDF metadata and in a `provenance` object in the JSON report. The last page says what the full assessment on a platform adds. ## Reports and uploads diff --git a/assets/css/style.css b/core/assets/css/style.css similarity index 100% rename from assets/css/style.css rename to core/assets/css/style.css diff --git a/assets/fonts/IBMPlexMono-Regular.ttf b/core/assets/fonts/IBMPlexMono-Regular.ttf similarity index 100% rename from assets/fonts/IBMPlexMono-Regular.ttf rename to core/assets/fonts/IBMPlexMono-Regular.ttf diff --git a/assets/fonts/IBMPlexMono-SemiBold.ttf b/core/assets/fonts/IBMPlexMono-SemiBold.ttf similarity index 100% rename from assets/fonts/IBMPlexMono-SemiBold.ttf rename to core/assets/fonts/IBMPlexMono-SemiBold.ttf diff --git a/assets/fonts/IBMPlexSans-Italic.ttf b/core/assets/fonts/IBMPlexSans-Italic.ttf similarity index 100% rename from assets/fonts/IBMPlexSans-Italic.ttf rename to core/assets/fonts/IBMPlexSans-Italic.ttf diff --git a/assets/fonts/IBMPlexSans-Medium.ttf b/core/assets/fonts/IBMPlexSans-Medium.ttf similarity index 100% rename from assets/fonts/IBMPlexSans-Medium.ttf rename to core/assets/fonts/IBMPlexSans-Medium.ttf diff --git a/assets/fonts/IBMPlexSans-Regular.ttf b/core/assets/fonts/IBMPlexSans-Regular.ttf similarity index 100% rename from assets/fonts/IBMPlexSans-Regular.ttf rename to core/assets/fonts/IBMPlexSans-Regular.ttf diff --git a/assets/fonts/IBMPlexSans-SemiBold.ttf b/core/assets/fonts/IBMPlexSans-SemiBold.ttf similarity index 100% rename from assets/fonts/IBMPlexSans-SemiBold.ttf rename to core/assets/fonts/IBMPlexSans-SemiBold.ttf diff --git a/assets/fonts/IBMPlexSans-SemiBoldItalic.ttf b/core/assets/fonts/IBMPlexSans-SemiBoldItalic.ttf similarity index 100% rename from assets/fonts/IBMPlexSans-SemiBoldItalic.ttf rename to core/assets/fonts/IBMPlexSans-SemiBoldItalic.ttf diff --git a/assets/fonts/OFL.txt b/core/assets/fonts/OFL.txt similarity index 100% rename from assets/fonts/OFL.txt rename to core/assets/fonts/OFL.txt diff --git a/assets/icons/README.md b/core/assets/icons/README.md similarity index 100% rename from assets/icons/README.md rename to core/assets/icons/README.md diff --git a/assets/icons/aws/Analytics/Athena.png b/core/assets/icons/aws/Analytics/Athena.png similarity index 100% rename from assets/icons/aws/Analytics/Athena.png rename to core/assets/icons/aws/Analytics/Athena.png diff --git a/assets/icons/aws/Analytics/Athena.svg b/core/assets/icons/aws/Analytics/Athena.svg similarity index 100% rename from assets/icons/aws/Analytics/Athena.svg rename to core/assets/icons/aws/Analytics/Athena.svg diff --git a/assets/icons/aws/Analytics/Clean-Rooms.png b/core/assets/icons/aws/Analytics/Clean-Rooms.png similarity index 100% rename from assets/icons/aws/Analytics/Clean-Rooms.png rename to core/assets/icons/aws/Analytics/Clean-Rooms.png diff --git a/assets/icons/aws/Analytics/Clean-Rooms.svg b/core/assets/icons/aws/Analytics/Clean-Rooms.svg similarity index 100% rename from assets/icons/aws/Analytics/Clean-Rooms.svg rename to core/assets/icons/aws/Analytics/Clean-Rooms.svg diff --git a/assets/icons/aws/Analytics/CloudSearch.png b/core/assets/icons/aws/Analytics/CloudSearch.png similarity index 100% rename from assets/icons/aws/Analytics/CloudSearch.png rename to core/assets/icons/aws/Analytics/CloudSearch.png diff --git a/assets/icons/aws/Analytics/CloudSearch.svg b/core/assets/icons/aws/Analytics/CloudSearch.svg similarity index 100% rename from assets/icons/aws/Analytics/CloudSearch.svg rename to core/assets/icons/aws/Analytics/CloudSearch.svg diff --git a/assets/icons/aws/Analytics/Data-Exchange.png b/core/assets/icons/aws/Analytics/Data-Exchange.png similarity index 100% rename from assets/icons/aws/Analytics/Data-Exchange.png rename to core/assets/icons/aws/Analytics/Data-Exchange.png diff --git a/assets/icons/aws/Analytics/Data-Exchange.svg b/core/assets/icons/aws/Analytics/Data-Exchange.svg similarity index 100% rename from assets/icons/aws/Analytics/Data-Exchange.svg rename to core/assets/icons/aws/Analytics/Data-Exchange.svg diff --git a/assets/icons/aws/Analytics/Data-Firehose.png b/core/assets/icons/aws/Analytics/Data-Firehose.png similarity index 100% rename from assets/icons/aws/Analytics/Data-Firehose.png rename to core/assets/icons/aws/Analytics/Data-Firehose.png diff --git a/assets/icons/aws/Analytics/Data-Firehose.svg b/core/assets/icons/aws/Analytics/Data-Firehose.svg similarity index 100% rename from assets/icons/aws/Analytics/Data-Firehose.svg rename to core/assets/icons/aws/Analytics/Data-Firehose.svg diff --git a/assets/icons/aws/Analytics/Data-Pipeline.png b/core/assets/icons/aws/Analytics/Data-Pipeline.png similarity index 100% rename from assets/icons/aws/Analytics/Data-Pipeline.png rename to core/assets/icons/aws/Analytics/Data-Pipeline.png diff --git a/assets/icons/aws/Analytics/Data-Pipeline.svg b/core/assets/icons/aws/Analytics/Data-Pipeline.svg similarity index 100% rename from assets/icons/aws/Analytics/Data-Pipeline.svg rename to core/assets/icons/aws/Analytics/Data-Pipeline.svg diff --git a/assets/icons/aws/Analytics/DataZone.png b/core/assets/icons/aws/Analytics/DataZone.png similarity index 100% rename from assets/icons/aws/Analytics/DataZone.png rename to core/assets/icons/aws/Analytics/DataZone.png diff --git a/assets/icons/aws/Analytics/DataZone.svg b/core/assets/icons/aws/Analytics/DataZone.svg similarity index 100% rename from assets/icons/aws/Analytics/DataZone.svg rename to core/assets/icons/aws/Analytics/DataZone.svg diff --git a/assets/icons/aws/Analytics/EMR.png b/core/assets/icons/aws/Analytics/EMR.png similarity index 100% rename from assets/icons/aws/Analytics/EMR.png rename to core/assets/icons/aws/Analytics/EMR.png diff --git a/assets/icons/aws/Analytics/EMR.svg b/core/assets/icons/aws/Analytics/EMR.svg similarity index 100% rename from assets/icons/aws/Analytics/EMR.svg rename to core/assets/icons/aws/Analytics/EMR.svg diff --git a/assets/icons/aws/Analytics/Entity-Resolution.png b/core/assets/icons/aws/Analytics/Entity-Resolution.png similarity index 100% rename from assets/icons/aws/Analytics/Entity-Resolution.png rename to core/assets/icons/aws/Analytics/Entity-Resolution.png diff --git a/assets/icons/aws/Analytics/Entity-Resolution.svg b/core/assets/icons/aws/Analytics/Entity-Resolution.svg similarity index 100% rename from assets/icons/aws/Analytics/Entity-Resolution.svg rename to core/assets/icons/aws/Analytics/Entity-Resolution.svg diff --git a/assets/icons/aws/Analytics/FinSpace.png b/core/assets/icons/aws/Analytics/FinSpace.png similarity index 100% rename from assets/icons/aws/Analytics/FinSpace.png rename to core/assets/icons/aws/Analytics/FinSpace.png diff --git a/assets/icons/aws/Analytics/FinSpace.svg b/core/assets/icons/aws/Analytics/FinSpace.svg similarity index 100% rename from assets/icons/aws/Analytics/FinSpace.svg rename to core/assets/icons/aws/Analytics/FinSpace.svg diff --git a/assets/icons/aws/Analytics/Glue-DataBrew.png b/core/assets/icons/aws/Analytics/Glue-DataBrew.png similarity index 100% rename from assets/icons/aws/Analytics/Glue-DataBrew.png rename to core/assets/icons/aws/Analytics/Glue-DataBrew.png diff --git a/assets/icons/aws/Analytics/Glue-DataBrew.svg b/core/assets/icons/aws/Analytics/Glue-DataBrew.svg similarity index 100% rename from assets/icons/aws/Analytics/Glue-DataBrew.svg rename to core/assets/icons/aws/Analytics/Glue-DataBrew.svg diff --git a/assets/icons/aws/Analytics/Glue-Elastic-Views.png b/core/assets/icons/aws/Analytics/Glue-Elastic-Views.png similarity index 100% rename from assets/icons/aws/Analytics/Glue-Elastic-Views.png rename to core/assets/icons/aws/Analytics/Glue-Elastic-Views.png diff --git a/assets/icons/aws/Analytics/Glue-Elastic-Views.svg b/core/assets/icons/aws/Analytics/Glue-Elastic-Views.svg similarity index 100% rename from assets/icons/aws/Analytics/Glue-Elastic-Views.svg rename to core/assets/icons/aws/Analytics/Glue-Elastic-Views.svg diff --git a/assets/icons/aws/Analytics/Glue.png b/core/assets/icons/aws/Analytics/Glue.png similarity index 100% rename from assets/icons/aws/Analytics/Glue.png rename to core/assets/icons/aws/Analytics/Glue.png diff --git a/assets/icons/aws/Analytics/Glue.svg b/core/assets/icons/aws/Analytics/Glue.svg similarity index 100% rename from assets/icons/aws/Analytics/Glue.svg rename to core/assets/icons/aws/Analytics/Glue.svg diff --git a/assets/icons/aws/Analytics/Kinesis-Data-Analytics.png b/core/assets/icons/aws/Analytics/Kinesis-Data-Analytics.png similarity index 100% rename from assets/icons/aws/Analytics/Kinesis-Data-Analytics.png rename to core/assets/icons/aws/Analytics/Kinesis-Data-Analytics.png diff --git a/assets/icons/aws/Analytics/Kinesis-Data-Analytics.svg b/core/assets/icons/aws/Analytics/Kinesis-Data-Analytics.svg similarity index 100% rename from assets/icons/aws/Analytics/Kinesis-Data-Analytics.svg rename to core/assets/icons/aws/Analytics/Kinesis-Data-Analytics.svg diff --git a/assets/icons/aws/Analytics/Kinesis-Data-Streams.png b/core/assets/icons/aws/Analytics/Kinesis-Data-Streams.png similarity index 100% rename from assets/icons/aws/Analytics/Kinesis-Data-Streams.png rename to core/assets/icons/aws/Analytics/Kinesis-Data-Streams.png diff --git a/assets/icons/aws/Analytics/Kinesis-Data-Streams.svg b/core/assets/icons/aws/Analytics/Kinesis-Data-Streams.svg similarity index 100% rename from assets/icons/aws/Analytics/Kinesis-Data-Streams.svg rename to core/assets/icons/aws/Analytics/Kinesis-Data-Streams.svg diff --git a/assets/icons/aws/Analytics/Kinesis-Firehose.png b/core/assets/icons/aws/Analytics/Kinesis-Firehose.png similarity index 100% rename from assets/icons/aws/Analytics/Kinesis-Firehose.png rename to core/assets/icons/aws/Analytics/Kinesis-Firehose.png diff --git a/assets/icons/aws/Analytics/Kinesis-Firehose.svg b/core/assets/icons/aws/Analytics/Kinesis-Firehose.svg similarity index 100% rename from assets/icons/aws/Analytics/Kinesis-Firehose.svg rename to core/assets/icons/aws/Analytics/Kinesis-Firehose.svg diff --git a/assets/icons/aws/Analytics/Kinesis-Video-Streams.png b/core/assets/icons/aws/Analytics/Kinesis-Video-Streams.png similarity index 100% rename from assets/icons/aws/Analytics/Kinesis-Video-Streams.png rename to core/assets/icons/aws/Analytics/Kinesis-Video-Streams.png diff --git a/assets/icons/aws/Analytics/Kinesis-Video-Streams.svg b/core/assets/icons/aws/Analytics/Kinesis-Video-Streams.svg similarity index 100% rename from assets/icons/aws/Analytics/Kinesis-Video-Streams.svg rename to core/assets/icons/aws/Analytics/Kinesis-Video-Streams.svg diff --git a/assets/icons/aws/Analytics/Kinesis.png b/core/assets/icons/aws/Analytics/Kinesis.png similarity index 100% rename from assets/icons/aws/Analytics/Kinesis.png rename to core/assets/icons/aws/Analytics/Kinesis.png diff --git a/assets/icons/aws/Analytics/Kinesis.svg b/core/assets/icons/aws/Analytics/Kinesis.svg similarity index 100% rename from assets/icons/aws/Analytics/Kinesis.svg rename to core/assets/icons/aws/Analytics/Kinesis.svg diff --git a/assets/icons/aws/Analytics/Lake-Formation.png b/core/assets/icons/aws/Analytics/Lake-Formation.png similarity index 100% rename from assets/icons/aws/Analytics/Lake-Formation.png rename to core/assets/icons/aws/Analytics/Lake-Formation.png diff --git a/assets/icons/aws/Analytics/Lake-Formation.svg b/core/assets/icons/aws/Analytics/Lake-Formation.svg similarity index 100% rename from assets/icons/aws/Analytics/Lake-Formation.svg rename to core/assets/icons/aws/Analytics/Lake-Formation.svg diff --git a/assets/icons/aws/Analytics/Managed-Service-for-Apache-Flink.png b/core/assets/icons/aws/Analytics/Managed-Service-for-Apache-Flink.png similarity index 100% rename from assets/icons/aws/Analytics/Managed-Service-for-Apache-Flink.png rename to core/assets/icons/aws/Analytics/Managed-Service-for-Apache-Flink.png diff --git a/assets/icons/aws/Analytics/Managed-Service-for-Apache-Flink.svg b/core/assets/icons/aws/Analytics/Managed-Service-for-Apache-Flink.svg similarity index 100% rename from assets/icons/aws/Analytics/Managed-Service-for-Apache-Flink.svg rename to core/assets/icons/aws/Analytics/Managed-Service-for-Apache-Flink.svg diff --git a/assets/icons/aws/Analytics/Managed-Streaming-for-Apache-Kafka.png b/core/assets/icons/aws/Analytics/Managed-Streaming-for-Apache-Kafka.png similarity index 100% rename from assets/icons/aws/Analytics/Managed-Streaming-for-Apache-Kafka.png rename to core/assets/icons/aws/Analytics/Managed-Streaming-for-Apache-Kafka.png diff --git a/assets/icons/aws/Analytics/Managed-Streaming-for-Apache-Kafka.svg b/core/assets/icons/aws/Analytics/Managed-Streaming-for-Apache-Kafka.svg similarity index 100% rename from assets/icons/aws/Analytics/Managed-Streaming-for-Apache-Kafka.svg rename to core/assets/icons/aws/Analytics/Managed-Streaming-for-Apache-Kafka.svg diff --git a/assets/icons/aws/Analytics/OpenSearch-Service.png b/core/assets/icons/aws/Analytics/OpenSearch-Service.png similarity index 100% rename from assets/icons/aws/Analytics/OpenSearch-Service.png rename to core/assets/icons/aws/Analytics/OpenSearch-Service.png diff --git a/assets/icons/aws/Analytics/OpenSearch-Service.svg b/core/assets/icons/aws/Analytics/OpenSearch-Service.svg similarity index 100% rename from assets/icons/aws/Analytics/OpenSearch-Service.svg rename to core/assets/icons/aws/Analytics/OpenSearch-Service.svg diff --git a/assets/icons/aws/Analytics/QuickSight.png b/core/assets/icons/aws/Analytics/QuickSight.png similarity index 100% rename from assets/icons/aws/Analytics/QuickSight.png rename to core/assets/icons/aws/Analytics/QuickSight.png diff --git a/assets/icons/aws/Analytics/QuickSight.svg b/core/assets/icons/aws/Analytics/QuickSight.svg similarity index 100% rename from assets/icons/aws/Analytics/QuickSight.svg rename to core/assets/icons/aws/Analytics/QuickSight.svg diff --git a/assets/icons/aws/Analytics/Redshift.png b/core/assets/icons/aws/Analytics/Redshift.png similarity index 100% rename from assets/icons/aws/Analytics/Redshift.png rename to core/assets/icons/aws/Analytics/Redshift.png diff --git a/assets/icons/aws/Analytics/Redshift.svg b/core/assets/icons/aws/Analytics/Redshift.svg similarity index 100% rename from assets/icons/aws/Analytics/Redshift.svg rename to core/assets/icons/aws/Analytics/Redshift.svg diff --git a/assets/icons/aws/App-Integration/API-Gateway.png b/core/assets/icons/aws/App-Integration/API-Gateway.png similarity index 100% rename from assets/icons/aws/App-Integration/API-Gateway.png rename to core/assets/icons/aws/App-Integration/API-Gateway.png diff --git a/assets/icons/aws/App-Integration/API-Gateway.svg b/core/assets/icons/aws/App-Integration/API-Gateway.svg similarity index 100% rename from assets/icons/aws/App-Integration/API-Gateway.svg rename to core/assets/icons/aws/App-Integration/API-Gateway.svg diff --git a/assets/icons/aws/App-Integration/AppFlow.png b/core/assets/icons/aws/App-Integration/AppFlow.png similarity index 100% rename from assets/icons/aws/App-Integration/AppFlow.png rename to core/assets/icons/aws/App-Integration/AppFlow.png diff --git a/assets/icons/aws/App-Integration/AppFlow.svg b/core/assets/icons/aws/App-Integration/AppFlow.svg similarity index 100% rename from assets/icons/aws/App-Integration/AppFlow.svg rename to core/assets/icons/aws/App-Integration/AppFlow.svg diff --git a/assets/icons/aws/App-Integration/AppSync.png b/core/assets/icons/aws/App-Integration/AppSync.png similarity index 100% rename from assets/icons/aws/App-Integration/AppSync.png rename to core/assets/icons/aws/App-Integration/AppSync.png diff --git a/assets/icons/aws/App-Integration/AppSync.svg b/core/assets/icons/aws/App-Integration/AppSync.svg similarity index 100% rename from assets/icons/aws/App-Integration/AppSync.svg rename to core/assets/icons/aws/App-Integration/AppSync.svg diff --git a/assets/icons/aws/App-Integration/B2B-Data-Interchange.png b/core/assets/icons/aws/App-Integration/B2B-Data-Interchange.png similarity index 100% rename from assets/icons/aws/App-Integration/B2B-Data-Interchange.png rename to core/assets/icons/aws/App-Integration/B2B-Data-Interchange.png diff --git a/assets/icons/aws/App-Integration/B2B-Data-Interchange.svg b/core/assets/icons/aws/App-Integration/B2B-Data-Interchange.svg similarity index 100% rename from assets/icons/aws/App-Integration/B2B-Data-Interchange.svg rename to core/assets/icons/aws/App-Integration/B2B-Data-Interchange.svg diff --git a/assets/icons/aws/App-Integration/Console-Mobile-Application.png b/core/assets/icons/aws/App-Integration/Console-Mobile-Application.png similarity index 100% rename from assets/icons/aws/App-Integration/Console-Mobile-Application.png rename to core/assets/icons/aws/App-Integration/Console-Mobile-Application.png diff --git a/assets/icons/aws/App-Integration/Console-Mobile-Application.svg b/core/assets/icons/aws/App-Integration/Console-Mobile-Application.svg similarity index 100% rename from assets/icons/aws/App-Integration/Console-Mobile-Application.svg rename to core/assets/icons/aws/App-Integration/Console-Mobile-Application.svg diff --git a/assets/icons/aws/App-Integration/EventBridge.png b/core/assets/icons/aws/App-Integration/EventBridge.png similarity index 100% rename from assets/icons/aws/App-Integration/EventBridge.png rename to core/assets/icons/aws/App-Integration/EventBridge.png diff --git a/assets/icons/aws/App-Integration/EventBridge.svg b/core/assets/icons/aws/App-Integration/EventBridge.svg similarity index 100% rename from assets/icons/aws/App-Integration/EventBridge.svg rename to core/assets/icons/aws/App-Integration/EventBridge.svg diff --git a/assets/icons/aws/App-Integration/Express-Workflows.png b/core/assets/icons/aws/App-Integration/Express-Workflows.png similarity index 100% rename from assets/icons/aws/App-Integration/Express-Workflows.png rename to core/assets/icons/aws/App-Integration/Express-Workflows.png diff --git a/assets/icons/aws/App-Integration/Express-Workflows.svg b/core/assets/icons/aws/App-Integration/Express-Workflows.svg similarity index 100% rename from assets/icons/aws/App-Integration/Express-Workflows.svg rename to core/assets/icons/aws/App-Integration/Express-Workflows.svg diff --git a/assets/icons/aws/App-Integration/MQ.png b/core/assets/icons/aws/App-Integration/MQ.png similarity index 100% rename from assets/icons/aws/App-Integration/MQ.png rename to core/assets/icons/aws/App-Integration/MQ.png diff --git a/assets/icons/aws/App-Integration/MQ.svg b/core/assets/icons/aws/App-Integration/MQ.svg similarity index 100% rename from assets/icons/aws/App-Integration/MQ.svg rename to core/assets/icons/aws/App-Integration/MQ.svg diff --git a/assets/icons/aws/App-Integration/Managed-Workflows-for-Apache-Airflow.png b/core/assets/icons/aws/App-Integration/Managed-Workflows-for-Apache-Airflow.png similarity index 100% rename from assets/icons/aws/App-Integration/Managed-Workflows-for-Apache-Airflow.png rename to core/assets/icons/aws/App-Integration/Managed-Workflows-for-Apache-Airflow.png diff --git a/assets/icons/aws/App-Integration/Managed-Workflows-for-Apache-Airflow.svg b/core/assets/icons/aws/App-Integration/Managed-Workflows-for-Apache-Airflow.svg similarity index 100% rename from assets/icons/aws/App-Integration/Managed-Workflows-for-Apache-Airflow.svg rename to core/assets/icons/aws/App-Integration/Managed-Workflows-for-Apache-Airflow.svg diff --git a/assets/icons/aws/App-Integration/Simple-Notification-Service.png b/core/assets/icons/aws/App-Integration/Simple-Notification-Service.png similarity index 100% rename from assets/icons/aws/App-Integration/Simple-Notification-Service.png rename to core/assets/icons/aws/App-Integration/Simple-Notification-Service.png diff --git a/assets/icons/aws/App-Integration/Simple-Notification-Service.svg b/core/assets/icons/aws/App-Integration/Simple-Notification-Service.svg similarity index 100% rename from assets/icons/aws/App-Integration/Simple-Notification-Service.svg rename to core/assets/icons/aws/App-Integration/Simple-Notification-Service.svg diff --git a/assets/icons/aws/App-Integration/Simple-Queue-Service.png b/core/assets/icons/aws/App-Integration/Simple-Queue-Service.png similarity index 100% rename from assets/icons/aws/App-Integration/Simple-Queue-Service.png rename to core/assets/icons/aws/App-Integration/Simple-Queue-Service.png diff --git a/assets/icons/aws/App-Integration/Simple-Queue-Service.svg b/core/assets/icons/aws/App-Integration/Simple-Queue-Service.svg similarity index 100% rename from assets/icons/aws/App-Integration/Simple-Queue-Service.svg rename to core/assets/icons/aws/App-Integration/Simple-Queue-Service.svg diff --git a/assets/icons/aws/App-Integration/Step-Functions.png b/core/assets/icons/aws/App-Integration/Step-Functions.png similarity index 100% rename from assets/icons/aws/App-Integration/Step-Functions.png rename to core/assets/icons/aws/App-Integration/Step-Functions.png diff --git a/assets/icons/aws/App-Integration/Step-Functions.svg b/core/assets/icons/aws/App-Integration/Step-Functions.svg similarity index 100% rename from assets/icons/aws/App-Integration/Step-Functions.svg rename to core/assets/icons/aws/App-Integration/Step-Functions.svg diff --git a/assets/icons/aws/Blockchain/Managed-Blockchain.png b/core/assets/icons/aws/Blockchain/Managed-Blockchain.png similarity index 100% rename from assets/icons/aws/Blockchain/Managed-Blockchain.png rename to core/assets/icons/aws/Blockchain/Managed-Blockchain.png diff --git a/assets/icons/aws/Blockchain/Managed-Blockchain.svg b/core/assets/icons/aws/Blockchain/Managed-Blockchain.svg similarity index 100% rename from assets/icons/aws/Blockchain/Managed-Blockchain.svg rename to core/assets/icons/aws/Blockchain/Managed-Blockchain.svg diff --git a/assets/icons/aws/Blockchain/Quantum-Ledger-Database.png b/core/assets/icons/aws/Blockchain/Quantum-Ledger-Database.png similarity index 100% rename from assets/icons/aws/Blockchain/Quantum-Ledger-Database.png rename to core/assets/icons/aws/Blockchain/Quantum-Ledger-Database.png diff --git a/assets/icons/aws/Blockchain/Quantum-Ledger-Database.svg b/core/assets/icons/aws/Blockchain/Quantum-Ledger-Database.svg similarity index 100% rename from assets/icons/aws/Blockchain/Quantum-Ledger-Database.svg rename to core/assets/icons/aws/Blockchain/Quantum-Ledger-Database.svg diff --git a/assets/icons/aws/Business-Applications/Alexa-For-Business.png b/core/assets/icons/aws/Business-Applications/Alexa-For-Business.png similarity index 100% rename from assets/icons/aws/Business-Applications/Alexa-For-Business.png rename to core/assets/icons/aws/Business-Applications/Alexa-For-Business.png diff --git a/assets/icons/aws/Business-Applications/Alexa-For-Business.svg b/core/assets/icons/aws/Business-Applications/Alexa-For-Business.svg similarity index 100% rename from assets/icons/aws/Business-Applications/Alexa-For-Business.svg rename to core/assets/icons/aws/Business-Applications/Alexa-For-Business.svg diff --git a/assets/icons/aws/Business-Applications/AppFabric.png b/core/assets/icons/aws/Business-Applications/AppFabric.png similarity index 100% rename from assets/icons/aws/Business-Applications/AppFabric.png rename to core/assets/icons/aws/Business-Applications/AppFabric.png diff --git a/assets/icons/aws/Business-Applications/AppFabric.svg b/core/assets/icons/aws/Business-Applications/AppFabric.svg similarity index 100% rename from assets/icons/aws/Business-Applications/AppFabric.svg rename to core/assets/icons/aws/Business-Applications/AppFabric.svg diff --git a/assets/icons/aws/Business-Applications/Chime-SDK.png b/core/assets/icons/aws/Business-Applications/Chime-SDK.png similarity index 100% rename from assets/icons/aws/Business-Applications/Chime-SDK.png rename to core/assets/icons/aws/Business-Applications/Chime-SDK.png diff --git a/assets/icons/aws/Business-Applications/Chime-SDK.svg b/core/assets/icons/aws/Business-Applications/Chime-SDK.svg similarity index 100% rename from assets/icons/aws/Business-Applications/Chime-SDK.svg rename to core/assets/icons/aws/Business-Applications/Chime-SDK.svg diff --git a/assets/icons/aws/Business-Applications/Chime-Voice-Connector.png b/core/assets/icons/aws/Business-Applications/Chime-Voice-Connector.png similarity index 100% rename from assets/icons/aws/Business-Applications/Chime-Voice-Connector.png rename to core/assets/icons/aws/Business-Applications/Chime-Voice-Connector.png diff --git a/assets/icons/aws/Business-Applications/Chime-Voice-Connector.svg b/core/assets/icons/aws/Business-Applications/Chime-Voice-Connector.svg similarity index 100% rename from assets/icons/aws/Business-Applications/Chime-Voice-Connector.svg rename to core/assets/icons/aws/Business-Applications/Chime-Voice-Connector.svg diff --git a/assets/icons/aws/Business-Applications/Chime.png b/core/assets/icons/aws/Business-Applications/Chime.png similarity index 100% rename from assets/icons/aws/Business-Applications/Chime.png rename to core/assets/icons/aws/Business-Applications/Chime.png diff --git a/assets/icons/aws/Business-Applications/Chime.svg b/core/assets/icons/aws/Business-Applications/Chime.svg similarity index 100% rename from assets/icons/aws/Business-Applications/Chime.svg rename to core/assets/icons/aws/Business-Applications/Chime.svg diff --git a/assets/icons/aws/Business-Applications/Connect.png b/core/assets/icons/aws/Business-Applications/Connect.png similarity index 100% rename from assets/icons/aws/Business-Applications/Connect.png rename to core/assets/icons/aws/Business-Applications/Connect.png diff --git a/assets/icons/aws/Business-Applications/Connect.svg b/core/assets/icons/aws/Business-Applications/Connect.svg similarity index 100% rename from assets/icons/aws/Business-Applications/Connect.svg rename to core/assets/icons/aws/Business-Applications/Connect.svg diff --git a/assets/icons/aws/Business-Applications/End-User-Messaging.png b/core/assets/icons/aws/Business-Applications/End-User-Messaging.png similarity index 100% rename from assets/icons/aws/Business-Applications/End-User-Messaging.png rename to core/assets/icons/aws/Business-Applications/End-User-Messaging.png diff --git a/assets/icons/aws/Business-Applications/End-User-Messaging.svg b/core/assets/icons/aws/Business-Applications/End-User-Messaging.svg similarity index 100% rename from assets/icons/aws/Business-Applications/End-User-Messaging.svg rename to core/assets/icons/aws/Business-Applications/End-User-Messaging.svg diff --git a/assets/icons/aws/Business-Applications/Honeycode.png b/core/assets/icons/aws/Business-Applications/Honeycode.png similarity index 100% rename from assets/icons/aws/Business-Applications/Honeycode.png rename to core/assets/icons/aws/Business-Applications/Honeycode.png diff --git a/assets/icons/aws/Business-Applications/Honeycode.svg b/core/assets/icons/aws/Business-Applications/Honeycode.svg similarity index 100% rename from assets/icons/aws/Business-Applications/Honeycode.svg rename to core/assets/icons/aws/Business-Applications/Honeycode.svg diff --git a/assets/icons/aws/Business-Applications/Pinpoint-APIs.png b/core/assets/icons/aws/Business-Applications/Pinpoint-APIs.png similarity index 100% rename from assets/icons/aws/Business-Applications/Pinpoint-APIs.png rename to core/assets/icons/aws/Business-Applications/Pinpoint-APIs.png diff --git a/assets/icons/aws/Business-Applications/Pinpoint-APIs.svg b/core/assets/icons/aws/Business-Applications/Pinpoint-APIs.svg similarity index 100% rename from assets/icons/aws/Business-Applications/Pinpoint-APIs.svg rename to core/assets/icons/aws/Business-Applications/Pinpoint-APIs.svg diff --git a/assets/icons/aws/Business-Applications/Pinpoint.png b/core/assets/icons/aws/Business-Applications/Pinpoint.png similarity index 100% rename from assets/icons/aws/Business-Applications/Pinpoint.png rename to core/assets/icons/aws/Business-Applications/Pinpoint.png diff --git a/assets/icons/aws/Business-Applications/Pinpoint.svg b/core/assets/icons/aws/Business-Applications/Pinpoint.svg similarity index 100% rename from assets/icons/aws/Business-Applications/Pinpoint.svg rename to core/assets/icons/aws/Business-Applications/Pinpoint.svg diff --git a/assets/icons/aws/Business-Applications/Quick.png b/core/assets/icons/aws/Business-Applications/Quick.png similarity index 100% rename from assets/icons/aws/Business-Applications/Quick.png rename to core/assets/icons/aws/Business-Applications/Quick.png diff --git a/assets/icons/aws/Business-Applications/Quick.svg b/core/assets/icons/aws/Business-Applications/Quick.svg similarity index 100% rename from assets/icons/aws/Business-Applications/Quick.svg rename to core/assets/icons/aws/Business-Applications/Quick.svg diff --git a/assets/icons/aws/Business-Applications/Simple-Email-Service.png b/core/assets/icons/aws/Business-Applications/Simple-Email-Service.png similarity index 100% rename from assets/icons/aws/Business-Applications/Simple-Email-Service.png rename to core/assets/icons/aws/Business-Applications/Simple-Email-Service.png diff --git a/assets/icons/aws/Business-Applications/Simple-Email-Service.svg b/core/assets/icons/aws/Business-Applications/Simple-Email-Service.svg similarity index 100% rename from assets/icons/aws/Business-Applications/Simple-Email-Service.svg rename to core/assets/icons/aws/Business-Applications/Simple-Email-Service.svg diff --git a/assets/icons/aws/Business-Applications/Supply-Chain.png b/core/assets/icons/aws/Business-Applications/Supply-Chain.png similarity index 100% rename from assets/icons/aws/Business-Applications/Supply-Chain.png rename to core/assets/icons/aws/Business-Applications/Supply-Chain.png diff --git a/assets/icons/aws/Business-Applications/Supply-Chain.svg b/core/assets/icons/aws/Business-Applications/Supply-Chain.svg similarity index 100% rename from assets/icons/aws/Business-Applications/Supply-Chain.svg rename to core/assets/icons/aws/Business-Applications/Supply-Chain.svg diff --git a/assets/icons/aws/Business-Applications/Wickr.png b/core/assets/icons/aws/Business-Applications/Wickr.png similarity index 100% rename from assets/icons/aws/Business-Applications/Wickr.png rename to core/assets/icons/aws/Business-Applications/Wickr.png diff --git a/assets/icons/aws/Business-Applications/Wickr.svg b/core/assets/icons/aws/Business-Applications/Wickr.svg similarity index 100% rename from assets/icons/aws/Business-Applications/Wickr.svg rename to core/assets/icons/aws/Business-Applications/Wickr.svg diff --git a/assets/icons/aws/Business-Applications/WorkDocs-SDK.png b/core/assets/icons/aws/Business-Applications/WorkDocs-SDK.png similarity index 100% rename from assets/icons/aws/Business-Applications/WorkDocs-SDK.png rename to core/assets/icons/aws/Business-Applications/WorkDocs-SDK.png diff --git a/assets/icons/aws/Business-Applications/WorkDocs-SDK.svg b/core/assets/icons/aws/Business-Applications/WorkDocs-SDK.svg similarity index 100% rename from assets/icons/aws/Business-Applications/WorkDocs-SDK.svg rename to core/assets/icons/aws/Business-Applications/WorkDocs-SDK.svg diff --git a/assets/icons/aws/Business-Applications/WorkDocs.png b/core/assets/icons/aws/Business-Applications/WorkDocs.png similarity index 100% rename from assets/icons/aws/Business-Applications/WorkDocs.png rename to core/assets/icons/aws/Business-Applications/WorkDocs.png diff --git a/assets/icons/aws/Business-Applications/WorkDocs.svg b/core/assets/icons/aws/Business-Applications/WorkDocs.svg similarity index 100% rename from assets/icons/aws/Business-Applications/WorkDocs.svg rename to core/assets/icons/aws/Business-Applications/WorkDocs.svg diff --git a/assets/icons/aws/Business-Applications/WorkMail.png b/core/assets/icons/aws/Business-Applications/WorkMail.png similarity index 100% rename from assets/icons/aws/Business-Applications/WorkMail.png rename to core/assets/icons/aws/Business-Applications/WorkMail.png diff --git a/assets/icons/aws/Business-Applications/WorkMail.svg b/core/assets/icons/aws/Business-Applications/WorkMail.svg similarity index 100% rename from assets/icons/aws/Business-Applications/WorkMail.svg rename to core/assets/icons/aws/Business-Applications/WorkMail.svg diff --git a/assets/icons/aws/Cloud-Financial-Management/Application-Cost-Profiler.png b/core/assets/icons/aws/Cloud-Financial-Management/Application-Cost-Profiler.png similarity index 100% rename from assets/icons/aws/Cloud-Financial-Management/Application-Cost-Profiler.png rename to core/assets/icons/aws/Cloud-Financial-Management/Application-Cost-Profiler.png diff --git a/assets/icons/aws/Cloud-Financial-Management/Application-Cost-Profiler.svg b/core/assets/icons/aws/Cloud-Financial-Management/Application-Cost-Profiler.svg similarity index 100% rename from assets/icons/aws/Cloud-Financial-Management/Application-Cost-Profiler.svg rename to core/assets/icons/aws/Cloud-Financial-Management/Application-Cost-Profiler.svg diff --git a/assets/icons/aws/Cloud-Financial-Management/Billing-Conductor.png b/core/assets/icons/aws/Cloud-Financial-Management/Billing-Conductor.png similarity index 100% rename from assets/icons/aws/Cloud-Financial-Management/Billing-Conductor.png rename to core/assets/icons/aws/Cloud-Financial-Management/Billing-Conductor.png diff --git a/assets/icons/aws/Cloud-Financial-Management/Billing-Conductor.svg b/core/assets/icons/aws/Cloud-Financial-Management/Billing-Conductor.svg similarity index 100% rename from assets/icons/aws/Cloud-Financial-Management/Billing-Conductor.svg rename to core/assets/icons/aws/Cloud-Financial-Management/Billing-Conductor.svg diff --git a/assets/icons/aws/Cloud-Financial-Management/Budgets.png b/core/assets/icons/aws/Cloud-Financial-Management/Budgets.png similarity index 100% rename from assets/icons/aws/Cloud-Financial-Management/Budgets.png rename to core/assets/icons/aws/Cloud-Financial-Management/Budgets.png diff --git a/assets/icons/aws/Cloud-Financial-Management/Budgets.svg b/core/assets/icons/aws/Cloud-Financial-Management/Budgets.svg similarity index 100% rename from assets/icons/aws/Cloud-Financial-Management/Budgets.svg rename to core/assets/icons/aws/Cloud-Financial-Management/Budgets.svg diff --git a/assets/icons/aws/Cloud-Financial-Management/Cost-Explorer.png b/core/assets/icons/aws/Cloud-Financial-Management/Cost-Explorer.png similarity index 100% rename from assets/icons/aws/Cloud-Financial-Management/Cost-Explorer.png rename to core/assets/icons/aws/Cloud-Financial-Management/Cost-Explorer.png diff --git a/assets/icons/aws/Cloud-Financial-Management/Cost-Explorer.svg b/core/assets/icons/aws/Cloud-Financial-Management/Cost-Explorer.svg similarity index 100% rename from assets/icons/aws/Cloud-Financial-Management/Cost-Explorer.svg rename to core/assets/icons/aws/Cloud-Financial-Management/Cost-Explorer.svg diff --git a/assets/icons/aws/Cloud-Financial-Management/Cost-and-Usage-Report.png b/core/assets/icons/aws/Cloud-Financial-Management/Cost-and-Usage-Report.png similarity index 100% rename from assets/icons/aws/Cloud-Financial-Management/Cost-and-Usage-Report.png rename to core/assets/icons/aws/Cloud-Financial-Management/Cost-and-Usage-Report.png diff --git a/assets/icons/aws/Cloud-Financial-Management/Cost-and-Usage-Report.svg b/core/assets/icons/aws/Cloud-Financial-Management/Cost-and-Usage-Report.svg similarity index 100% rename from assets/icons/aws/Cloud-Financial-Management/Cost-and-Usage-Report.svg rename to core/assets/icons/aws/Cloud-Financial-Management/Cost-and-Usage-Report.svg diff --git a/assets/icons/aws/Cloud-Financial-Management/Reserved-Instance-Reporting.png b/core/assets/icons/aws/Cloud-Financial-Management/Reserved-Instance-Reporting.png similarity index 100% rename from assets/icons/aws/Cloud-Financial-Management/Reserved-Instance-Reporting.png rename to core/assets/icons/aws/Cloud-Financial-Management/Reserved-Instance-Reporting.png diff --git a/assets/icons/aws/Cloud-Financial-Management/Reserved-Instance-Reporting.svg b/core/assets/icons/aws/Cloud-Financial-Management/Reserved-Instance-Reporting.svg similarity index 100% rename from assets/icons/aws/Cloud-Financial-Management/Reserved-Instance-Reporting.svg rename to core/assets/icons/aws/Cloud-Financial-Management/Reserved-Instance-Reporting.svg diff --git a/assets/icons/aws/Cloud-Financial-Management/Savings-Plans.png b/core/assets/icons/aws/Cloud-Financial-Management/Savings-Plans.png similarity index 100% rename from assets/icons/aws/Cloud-Financial-Management/Savings-Plans.png rename to core/assets/icons/aws/Cloud-Financial-Management/Savings-Plans.png diff --git a/assets/icons/aws/Cloud-Financial-Management/Savings-Plans.svg b/core/assets/icons/aws/Cloud-Financial-Management/Savings-Plans.svg similarity index 100% rename from assets/icons/aws/Cloud-Financial-Management/Savings-Plans.svg rename to core/assets/icons/aws/Cloud-Financial-Management/Savings-Plans.svg diff --git a/assets/icons/aws/Compute/App-Runner.png b/core/assets/icons/aws/Compute/App-Runner.png similarity index 100% rename from assets/icons/aws/Compute/App-Runner.png rename to core/assets/icons/aws/Compute/App-Runner.png diff --git a/assets/icons/aws/Compute/App-Runner.svg b/core/assets/icons/aws/Compute/App-Runner.svg similarity index 100% rename from assets/icons/aws/Compute/App-Runner.svg rename to core/assets/icons/aws/Compute/App-Runner.svg diff --git a/assets/icons/aws/Compute/Application-Auto-Scaling.png b/core/assets/icons/aws/Compute/Application-Auto-Scaling.png similarity index 100% rename from assets/icons/aws/Compute/Application-Auto-Scaling.png rename to core/assets/icons/aws/Compute/Application-Auto-Scaling.png diff --git a/assets/icons/aws/Compute/Application-Auto-Scaling.svg b/core/assets/icons/aws/Compute/Application-Auto-Scaling.svg similarity index 100% rename from assets/icons/aws/Compute/Application-Auto-Scaling.svg rename to core/assets/icons/aws/Compute/Application-Auto-Scaling.svg diff --git a/assets/icons/aws/Compute/Batch.png b/core/assets/icons/aws/Compute/Batch.png similarity index 100% rename from assets/icons/aws/Compute/Batch.png rename to core/assets/icons/aws/Compute/Batch.png diff --git a/assets/icons/aws/Compute/Batch.svg b/core/assets/icons/aws/Compute/Batch.svg similarity index 100% rename from assets/icons/aws/Compute/Batch.svg rename to core/assets/icons/aws/Compute/Batch.svg diff --git a/assets/icons/aws/Compute/Bottlerocket.png b/core/assets/icons/aws/Compute/Bottlerocket.png similarity index 100% rename from assets/icons/aws/Compute/Bottlerocket.png rename to core/assets/icons/aws/Compute/Bottlerocket.png diff --git a/assets/icons/aws/Compute/Bottlerocket.svg b/core/assets/icons/aws/Compute/Bottlerocket.svg similarity index 100% rename from assets/icons/aws/Compute/Bottlerocket.svg rename to core/assets/icons/aws/Compute/Bottlerocket.svg diff --git a/assets/icons/aws/Compute/Compute-Optimizer.png b/core/assets/icons/aws/Compute/Compute-Optimizer.png similarity index 100% rename from assets/icons/aws/Compute/Compute-Optimizer.png rename to core/assets/icons/aws/Compute/Compute-Optimizer.png diff --git a/assets/icons/aws/Compute/Compute-Optimizer.svg b/core/assets/icons/aws/Compute/Compute-Optimizer.svg similarity index 100% rename from assets/icons/aws/Compute/Compute-Optimizer.svg rename to core/assets/icons/aws/Compute/Compute-Optimizer.svg diff --git a/assets/icons/aws/Compute/DCV.png b/core/assets/icons/aws/Compute/DCV.png similarity index 100% rename from assets/icons/aws/Compute/DCV.png rename to core/assets/icons/aws/Compute/DCV.png diff --git a/assets/icons/aws/Compute/DCV.svg b/core/assets/icons/aws/Compute/DCV.svg similarity index 100% rename from assets/icons/aws/Compute/DCV.svg rename to core/assets/icons/aws/Compute/DCV.svg diff --git a/assets/icons/aws/Compute/EC2-Auto-Scaling.png b/core/assets/icons/aws/Compute/EC2-Auto-Scaling.png similarity index 100% rename from assets/icons/aws/Compute/EC2-Auto-Scaling.png rename to core/assets/icons/aws/Compute/EC2-Auto-Scaling.png diff --git a/assets/icons/aws/Compute/EC2-Auto-Scaling.svg b/core/assets/icons/aws/Compute/EC2-Auto-Scaling.svg similarity index 100% rename from assets/icons/aws/Compute/EC2-Auto-Scaling.svg rename to core/assets/icons/aws/Compute/EC2-Auto-Scaling.svg diff --git a/assets/icons/aws/Compute/EC2-Image-Builder.png b/core/assets/icons/aws/Compute/EC2-Image-Builder.png similarity index 100% rename from assets/icons/aws/Compute/EC2-Image-Builder.png rename to core/assets/icons/aws/Compute/EC2-Image-Builder.png diff --git a/assets/icons/aws/Compute/EC2-Image-Builder.svg b/core/assets/icons/aws/Compute/EC2-Image-Builder.svg similarity index 100% rename from assets/icons/aws/Compute/EC2-Image-Builder.svg rename to core/assets/icons/aws/Compute/EC2-Image-Builder.svg diff --git a/assets/icons/aws/Compute/EC2.png b/core/assets/icons/aws/Compute/EC2.png similarity index 100% rename from assets/icons/aws/Compute/EC2.png rename to core/assets/icons/aws/Compute/EC2.png diff --git a/assets/icons/aws/Compute/EC2.svg b/core/assets/icons/aws/Compute/EC2.svg similarity index 100% rename from assets/icons/aws/Compute/EC2.svg rename to core/assets/icons/aws/Compute/EC2.svg diff --git a/assets/icons/aws/Compute/Elastic-Beanstalk.png b/core/assets/icons/aws/Compute/Elastic-Beanstalk.png similarity index 100% rename from assets/icons/aws/Compute/Elastic-Beanstalk.png rename to core/assets/icons/aws/Compute/Elastic-Beanstalk.png diff --git a/assets/icons/aws/Compute/Elastic-Beanstalk.svg b/core/assets/icons/aws/Compute/Elastic-Beanstalk.svg similarity index 100% rename from assets/icons/aws/Compute/Elastic-Beanstalk.svg rename to core/assets/icons/aws/Compute/Elastic-Beanstalk.svg diff --git a/assets/icons/aws/Compute/Elastic-Fabric-Adapter.png b/core/assets/icons/aws/Compute/Elastic-Fabric-Adapter.png similarity index 100% rename from assets/icons/aws/Compute/Elastic-Fabric-Adapter.png rename to core/assets/icons/aws/Compute/Elastic-Fabric-Adapter.png diff --git a/assets/icons/aws/Compute/Elastic-Fabric-Adapter.svg b/core/assets/icons/aws/Compute/Elastic-Fabric-Adapter.svg similarity index 100% rename from assets/icons/aws/Compute/Elastic-Fabric-Adapter.svg rename to core/assets/icons/aws/Compute/Elastic-Fabric-Adapter.svg diff --git a/assets/icons/aws/Compute/Elastic-VMware-Service.png b/core/assets/icons/aws/Compute/Elastic-VMware-Service.png similarity index 100% rename from assets/icons/aws/Compute/Elastic-VMware-Service.png rename to core/assets/icons/aws/Compute/Elastic-VMware-Service.png diff --git a/assets/icons/aws/Compute/Elastic-VMware-Service.svg b/core/assets/icons/aws/Compute/Elastic-VMware-Service.svg similarity index 100% rename from assets/icons/aws/Compute/Elastic-VMware-Service.svg rename to core/assets/icons/aws/Compute/Elastic-VMware-Service.svg diff --git a/assets/icons/aws/Compute/Fargate.png b/core/assets/icons/aws/Compute/Fargate.png similarity index 100% rename from assets/icons/aws/Compute/Fargate.png rename to core/assets/icons/aws/Compute/Fargate.png diff --git a/assets/icons/aws/Compute/Fargate.svg b/core/assets/icons/aws/Compute/Fargate.svg similarity index 100% rename from assets/icons/aws/Compute/Fargate.svg rename to core/assets/icons/aws/Compute/Fargate.svg diff --git a/assets/icons/aws/Compute/Genomics-CLI.png b/core/assets/icons/aws/Compute/Genomics-CLI.png similarity index 100% rename from assets/icons/aws/Compute/Genomics-CLI.png rename to core/assets/icons/aws/Compute/Genomics-CLI.png diff --git a/assets/icons/aws/Compute/Genomics-CLI.svg b/core/assets/icons/aws/Compute/Genomics-CLI.svg similarity index 100% rename from assets/icons/aws/Compute/Genomics-CLI.svg rename to core/assets/icons/aws/Compute/Genomics-CLI.svg diff --git a/assets/icons/aws/Compute/Lambda.png b/core/assets/icons/aws/Compute/Lambda.png similarity index 100% rename from assets/icons/aws/Compute/Lambda.png rename to core/assets/icons/aws/Compute/Lambda.png diff --git a/assets/icons/aws/Compute/Lambda.svg b/core/assets/icons/aws/Compute/Lambda.svg similarity index 100% rename from assets/icons/aws/Compute/Lambda.svg rename to core/assets/icons/aws/Compute/Lambda.svg diff --git a/assets/icons/aws/Compute/Lightsail-for-Research.png b/core/assets/icons/aws/Compute/Lightsail-for-Research.png similarity index 100% rename from assets/icons/aws/Compute/Lightsail-for-Research.png rename to core/assets/icons/aws/Compute/Lightsail-for-Research.png diff --git a/assets/icons/aws/Compute/Lightsail-for-Research.svg b/core/assets/icons/aws/Compute/Lightsail-for-Research.svg similarity index 100% rename from assets/icons/aws/Compute/Lightsail-for-Research.svg rename to core/assets/icons/aws/Compute/Lightsail-for-Research.svg diff --git a/assets/icons/aws/Compute/Lightsail.png b/core/assets/icons/aws/Compute/Lightsail.png similarity index 100% rename from assets/icons/aws/Compute/Lightsail.png rename to core/assets/icons/aws/Compute/Lightsail.png diff --git a/assets/icons/aws/Compute/Lightsail.svg b/core/assets/icons/aws/Compute/Lightsail.svg similarity index 100% rename from assets/icons/aws/Compute/Lightsail.svg rename to core/assets/icons/aws/Compute/Lightsail.svg diff --git a/assets/icons/aws/Compute/Local-Zones.png b/core/assets/icons/aws/Compute/Local-Zones.png similarity index 100% rename from assets/icons/aws/Compute/Local-Zones.png rename to core/assets/icons/aws/Compute/Local-Zones.png diff --git a/assets/icons/aws/Compute/Local-Zones.svg b/core/assets/icons/aws/Compute/Local-Zones.svg similarity index 100% rename from assets/icons/aws/Compute/Local-Zones.svg rename to core/assets/icons/aws/Compute/Local-Zones.svg diff --git a/assets/icons/aws/Compute/NICE-DCV.png b/core/assets/icons/aws/Compute/NICE-DCV.png similarity index 100% rename from assets/icons/aws/Compute/NICE-DCV.png rename to core/assets/icons/aws/Compute/NICE-DCV.png diff --git a/assets/icons/aws/Compute/NICE-DCV.svg b/core/assets/icons/aws/Compute/NICE-DCV.svg similarity index 100% rename from assets/icons/aws/Compute/NICE-DCV.svg rename to core/assets/icons/aws/Compute/NICE-DCV.svg diff --git a/assets/icons/aws/Compute/NICE-EnginFrame.png b/core/assets/icons/aws/Compute/NICE-EnginFrame.png similarity index 100% rename from assets/icons/aws/Compute/NICE-EnginFrame.png rename to core/assets/icons/aws/Compute/NICE-EnginFrame.png diff --git a/assets/icons/aws/Compute/NICE-EnginFrame.svg b/core/assets/icons/aws/Compute/NICE-EnginFrame.svg similarity index 100% rename from assets/icons/aws/Compute/NICE-EnginFrame.svg rename to core/assets/icons/aws/Compute/NICE-EnginFrame.svg diff --git a/assets/icons/aws/Compute/Nitro-Enclaves.png b/core/assets/icons/aws/Compute/Nitro-Enclaves.png similarity index 100% rename from assets/icons/aws/Compute/Nitro-Enclaves.png rename to core/assets/icons/aws/Compute/Nitro-Enclaves.png diff --git a/assets/icons/aws/Compute/Nitro-Enclaves.svg b/core/assets/icons/aws/Compute/Nitro-Enclaves.svg similarity index 100% rename from assets/icons/aws/Compute/Nitro-Enclaves.svg rename to core/assets/icons/aws/Compute/Nitro-Enclaves.svg diff --git a/assets/icons/aws/Compute/Outposts-family.png b/core/assets/icons/aws/Compute/Outposts-family.png similarity index 100% rename from assets/icons/aws/Compute/Outposts-family.png rename to core/assets/icons/aws/Compute/Outposts-family.png diff --git a/assets/icons/aws/Compute/Outposts-family.svg b/core/assets/icons/aws/Compute/Outposts-family.svg similarity index 100% rename from assets/icons/aws/Compute/Outposts-family.svg rename to core/assets/icons/aws/Compute/Outposts-family.svg diff --git a/assets/icons/aws/Compute/Outposts-rack.png b/core/assets/icons/aws/Compute/Outposts-rack.png similarity index 100% rename from assets/icons/aws/Compute/Outposts-rack.png rename to core/assets/icons/aws/Compute/Outposts-rack.png diff --git a/assets/icons/aws/Compute/Outposts-rack.svg b/core/assets/icons/aws/Compute/Outposts-rack.svg similarity index 100% rename from assets/icons/aws/Compute/Outposts-rack.svg rename to core/assets/icons/aws/Compute/Outposts-rack.svg diff --git a/assets/icons/aws/Compute/Outposts-servers.png b/core/assets/icons/aws/Compute/Outposts-servers.png similarity index 100% rename from assets/icons/aws/Compute/Outposts-servers.png rename to core/assets/icons/aws/Compute/Outposts-servers.png diff --git a/assets/icons/aws/Compute/Outposts-servers.svg b/core/assets/icons/aws/Compute/Outposts-servers.svg similarity index 100% rename from assets/icons/aws/Compute/Outposts-servers.svg rename to core/assets/icons/aws/Compute/Outposts-servers.svg diff --git a/assets/icons/aws/Compute/Parallel-Cluster.png b/core/assets/icons/aws/Compute/Parallel-Cluster.png similarity index 100% rename from assets/icons/aws/Compute/Parallel-Cluster.png rename to core/assets/icons/aws/Compute/Parallel-Cluster.png diff --git a/assets/icons/aws/Compute/Parallel-Cluster.svg b/core/assets/icons/aws/Compute/Parallel-Cluster.svg similarity index 100% rename from assets/icons/aws/Compute/Parallel-Cluster.svg rename to core/assets/icons/aws/Compute/Parallel-Cluster.svg diff --git a/assets/icons/aws/Compute/Parallel-Computing-Service.png b/core/assets/icons/aws/Compute/Parallel-Computing-Service.png similarity index 100% rename from assets/icons/aws/Compute/Parallel-Computing-Service.png rename to core/assets/icons/aws/Compute/Parallel-Computing-Service.png diff --git a/assets/icons/aws/Compute/Parallel-Computing-Service.svg b/core/assets/icons/aws/Compute/Parallel-Computing-Service.svg similarity index 100% rename from assets/icons/aws/Compute/Parallel-Computing-Service.svg rename to core/assets/icons/aws/Compute/Parallel-Computing-Service.svg diff --git a/assets/icons/aws/Compute/ParallelCluster.png b/core/assets/icons/aws/Compute/ParallelCluster.png similarity index 100% rename from assets/icons/aws/Compute/ParallelCluster.png rename to core/assets/icons/aws/Compute/ParallelCluster.png diff --git a/assets/icons/aws/Compute/ParallelCluster.svg b/core/assets/icons/aws/Compute/ParallelCluster.svg similarity index 100% rename from assets/icons/aws/Compute/ParallelCluster.svg rename to core/assets/icons/aws/Compute/ParallelCluster.svg diff --git a/assets/icons/aws/Compute/Serverless-Application-Repository.png b/core/assets/icons/aws/Compute/Serverless-Application-Repository.png similarity index 100% rename from assets/icons/aws/Compute/Serverless-Application-Repository.png rename to core/assets/icons/aws/Compute/Serverless-Application-Repository.png diff --git a/assets/icons/aws/Compute/Serverless-Application-Repository.svg b/core/assets/icons/aws/Compute/Serverless-Application-Repository.svg similarity index 100% rename from assets/icons/aws/Compute/Serverless-Application-Repository.svg rename to core/assets/icons/aws/Compute/Serverless-Application-Repository.svg diff --git a/assets/icons/aws/Compute/SimSpace-Weaver.png b/core/assets/icons/aws/Compute/SimSpace-Weaver.png similarity index 100% rename from assets/icons/aws/Compute/SimSpace-Weaver.png rename to core/assets/icons/aws/Compute/SimSpace-Weaver.png diff --git a/assets/icons/aws/Compute/SimSpace-Weaver.svg b/core/assets/icons/aws/Compute/SimSpace-Weaver.svg similarity index 100% rename from assets/icons/aws/Compute/SimSpace-Weaver.svg rename to core/assets/icons/aws/Compute/SimSpace-Weaver.svg diff --git a/assets/icons/aws/Compute/Thinkbox-Deadline.png b/core/assets/icons/aws/Compute/Thinkbox-Deadline.png similarity index 100% rename from assets/icons/aws/Compute/Thinkbox-Deadline.png rename to core/assets/icons/aws/Compute/Thinkbox-Deadline.png diff --git a/assets/icons/aws/Compute/Thinkbox-Deadline.svg b/core/assets/icons/aws/Compute/Thinkbox-Deadline.svg similarity index 100% rename from assets/icons/aws/Compute/Thinkbox-Deadline.svg rename to core/assets/icons/aws/Compute/Thinkbox-Deadline.svg diff --git a/assets/icons/aws/Compute/Thinkbox-Frost.png b/core/assets/icons/aws/Compute/Thinkbox-Frost.png similarity index 100% rename from assets/icons/aws/Compute/Thinkbox-Frost.png rename to core/assets/icons/aws/Compute/Thinkbox-Frost.png diff --git a/assets/icons/aws/Compute/Thinkbox-Frost.svg b/core/assets/icons/aws/Compute/Thinkbox-Frost.svg similarity index 100% rename from assets/icons/aws/Compute/Thinkbox-Frost.svg rename to core/assets/icons/aws/Compute/Thinkbox-Frost.svg diff --git a/assets/icons/aws/Compute/Thinkbox-Krakatoa.png b/core/assets/icons/aws/Compute/Thinkbox-Krakatoa.png similarity index 100% rename from assets/icons/aws/Compute/Thinkbox-Krakatoa.png rename to core/assets/icons/aws/Compute/Thinkbox-Krakatoa.png diff --git a/assets/icons/aws/Compute/Thinkbox-Krakatoa.svg b/core/assets/icons/aws/Compute/Thinkbox-Krakatoa.svg similarity index 100% rename from assets/icons/aws/Compute/Thinkbox-Krakatoa.svg rename to core/assets/icons/aws/Compute/Thinkbox-Krakatoa.svg diff --git a/assets/icons/aws/Compute/Thinkbox-Sequoia.png b/core/assets/icons/aws/Compute/Thinkbox-Sequoia.png similarity index 100% rename from assets/icons/aws/Compute/Thinkbox-Sequoia.png rename to core/assets/icons/aws/Compute/Thinkbox-Sequoia.png diff --git a/assets/icons/aws/Compute/Thinkbox-Sequoia.svg b/core/assets/icons/aws/Compute/Thinkbox-Sequoia.svg similarity index 100% rename from assets/icons/aws/Compute/Thinkbox-Sequoia.svg rename to core/assets/icons/aws/Compute/Thinkbox-Sequoia.svg diff --git a/assets/icons/aws/Compute/Thinkbox-Stoke.png b/core/assets/icons/aws/Compute/Thinkbox-Stoke.png similarity index 100% rename from assets/icons/aws/Compute/Thinkbox-Stoke.png rename to core/assets/icons/aws/Compute/Thinkbox-Stoke.png diff --git a/assets/icons/aws/Compute/Thinkbox-Stoke.svg b/core/assets/icons/aws/Compute/Thinkbox-Stoke.svg similarity index 100% rename from assets/icons/aws/Compute/Thinkbox-Stoke.svg rename to core/assets/icons/aws/Compute/Thinkbox-Stoke.svg diff --git a/assets/icons/aws/Compute/Thinkbox-XMesh.png b/core/assets/icons/aws/Compute/Thinkbox-XMesh.png similarity index 100% rename from assets/icons/aws/Compute/Thinkbox-XMesh.png rename to core/assets/icons/aws/Compute/Thinkbox-XMesh.png diff --git a/assets/icons/aws/Compute/Thinkbox-XMesh.svg b/core/assets/icons/aws/Compute/Thinkbox-XMesh.svg similarity index 100% rename from assets/icons/aws/Compute/Thinkbox-XMesh.svg rename to core/assets/icons/aws/Compute/Thinkbox-XMesh.svg diff --git a/assets/icons/aws/Compute/VMware-Cloud-on-AWS.png b/core/assets/icons/aws/Compute/VMware-Cloud-on-AWS.png similarity index 100% rename from assets/icons/aws/Compute/VMware-Cloud-on-AWS.png rename to core/assets/icons/aws/Compute/VMware-Cloud-on-AWS.png diff --git a/assets/icons/aws/Compute/VMware-Cloud-on-AWS.svg b/core/assets/icons/aws/Compute/VMware-Cloud-on-AWS.svg similarity index 100% rename from assets/icons/aws/Compute/VMware-Cloud-on-AWS.svg rename to core/assets/icons/aws/Compute/VMware-Cloud-on-AWS.svg diff --git a/assets/icons/aws/Compute/Wavelength.png b/core/assets/icons/aws/Compute/Wavelength.png similarity index 100% rename from assets/icons/aws/Compute/Wavelength.png rename to core/assets/icons/aws/Compute/Wavelength.png diff --git a/assets/icons/aws/Compute/Wavelength.svg b/core/assets/icons/aws/Compute/Wavelength.svg similarity index 100% rename from assets/icons/aws/Compute/Wavelength.svg rename to core/assets/icons/aws/Compute/Wavelength.svg diff --git a/assets/icons/aws/Containers/ECS-Anywhere.png b/core/assets/icons/aws/Containers/ECS-Anywhere.png similarity index 100% rename from assets/icons/aws/Containers/ECS-Anywhere.png rename to core/assets/icons/aws/Containers/ECS-Anywhere.png diff --git a/assets/icons/aws/Containers/ECS-Anywhere.svg b/core/assets/icons/aws/Containers/ECS-Anywhere.svg similarity index 100% rename from assets/icons/aws/Containers/ECS-Anywhere.svg rename to core/assets/icons/aws/Containers/ECS-Anywhere.svg diff --git a/assets/icons/aws/Containers/EKS-Anywhere.png b/core/assets/icons/aws/Containers/EKS-Anywhere.png similarity index 100% rename from assets/icons/aws/Containers/EKS-Anywhere.png rename to core/assets/icons/aws/Containers/EKS-Anywhere.png diff --git a/assets/icons/aws/Containers/EKS-Anywhere.svg b/core/assets/icons/aws/Containers/EKS-Anywhere.svg similarity index 100% rename from assets/icons/aws/Containers/EKS-Anywhere.svg rename to core/assets/icons/aws/Containers/EKS-Anywhere.svg diff --git a/assets/icons/aws/Containers/EKS-Cloud.png b/core/assets/icons/aws/Containers/EKS-Cloud.png similarity index 100% rename from assets/icons/aws/Containers/EKS-Cloud.png rename to core/assets/icons/aws/Containers/EKS-Cloud.png diff --git a/assets/icons/aws/Containers/EKS-Cloud.svg b/core/assets/icons/aws/Containers/EKS-Cloud.svg similarity index 100% rename from assets/icons/aws/Containers/EKS-Cloud.svg rename to core/assets/icons/aws/Containers/EKS-Cloud.svg diff --git a/assets/icons/aws/Containers/EKS-Distro.png b/core/assets/icons/aws/Containers/EKS-Distro.png similarity index 100% rename from assets/icons/aws/Containers/EKS-Distro.png rename to core/assets/icons/aws/Containers/EKS-Distro.png diff --git a/assets/icons/aws/Containers/EKS-Distro.svg b/core/assets/icons/aws/Containers/EKS-Distro.svg similarity index 100% rename from assets/icons/aws/Containers/EKS-Distro.svg rename to core/assets/icons/aws/Containers/EKS-Distro.svg diff --git a/assets/icons/aws/Containers/Elastic-Container-Registry.png b/core/assets/icons/aws/Containers/Elastic-Container-Registry.png similarity index 100% rename from assets/icons/aws/Containers/Elastic-Container-Registry.png rename to core/assets/icons/aws/Containers/Elastic-Container-Registry.png diff --git a/assets/icons/aws/Containers/Elastic-Container-Registry.svg b/core/assets/icons/aws/Containers/Elastic-Container-Registry.svg similarity index 100% rename from assets/icons/aws/Containers/Elastic-Container-Registry.svg rename to core/assets/icons/aws/Containers/Elastic-Container-Registry.svg diff --git a/assets/icons/aws/Containers/Elastic-Container-Service.png b/core/assets/icons/aws/Containers/Elastic-Container-Service.png similarity index 100% rename from assets/icons/aws/Containers/Elastic-Container-Service.png rename to core/assets/icons/aws/Containers/Elastic-Container-Service.png diff --git a/assets/icons/aws/Containers/Elastic-Container-Service.svg b/core/assets/icons/aws/Containers/Elastic-Container-Service.svg similarity index 100% rename from assets/icons/aws/Containers/Elastic-Container-Service.svg rename to core/assets/icons/aws/Containers/Elastic-Container-Service.svg diff --git a/assets/icons/aws/Containers/Elastic-Kubernetes-Service.png b/core/assets/icons/aws/Containers/Elastic-Kubernetes-Service.png similarity index 100% rename from assets/icons/aws/Containers/Elastic-Kubernetes-Service.png rename to core/assets/icons/aws/Containers/Elastic-Kubernetes-Service.png diff --git a/assets/icons/aws/Containers/Elastic-Kubernetes-Service.svg b/core/assets/icons/aws/Containers/Elastic-Kubernetes-Service.svg similarity index 100% rename from assets/icons/aws/Containers/Elastic-Kubernetes-Service.svg rename to core/assets/icons/aws/Containers/Elastic-Kubernetes-Service.svg diff --git a/assets/icons/aws/Containers/Fargate.png b/core/assets/icons/aws/Containers/Fargate.png similarity index 100% rename from assets/icons/aws/Containers/Fargate.png rename to core/assets/icons/aws/Containers/Fargate.png diff --git a/assets/icons/aws/Containers/Fargate.svg b/core/assets/icons/aws/Containers/Fargate.svg similarity index 100% rename from assets/icons/aws/Containers/Fargate.svg rename to core/assets/icons/aws/Containers/Fargate.svg diff --git a/assets/icons/aws/Containers/Red-Hat-OpenShift-Service-on-AWS.png b/core/assets/icons/aws/Containers/Red-Hat-OpenShift-Service-on-AWS.png similarity index 100% rename from assets/icons/aws/Containers/Red-Hat-OpenShift-Service-on-AWS.png rename to core/assets/icons/aws/Containers/Red-Hat-OpenShift-Service-on-AWS.png diff --git a/assets/icons/aws/Containers/Red-Hat-OpenShift-Service-on-AWS.svg b/core/assets/icons/aws/Containers/Red-Hat-OpenShift-Service-on-AWS.svg similarity index 100% rename from assets/icons/aws/Containers/Red-Hat-OpenShift-Service-on-AWS.svg rename to core/assets/icons/aws/Containers/Red-Hat-OpenShift-Service-on-AWS.svg diff --git a/assets/icons/aws/Customer-Enablement/Activate.png b/core/assets/icons/aws/Customer-Enablement/Activate.png similarity index 100% rename from assets/icons/aws/Customer-Enablement/Activate.png rename to core/assets/icons/aws/Customer-Enablement/Activate.png diff --git a/assets/icons/aws/Customer-Enablement/Activate.svg b/core/assets/icons/aws/Customer-Enablement/Activate.svg similarity index 100% rename from assets/icons/aws/Customer-Enablement/Activate.svg rename to core/assets/icons/aws/Customer-Enablement/Activate.svg diff --git a/assets/icons/aws/Customer-Enablement/IQ.png b/core/assets/icons/aws/Customer-Enablement/IQ.png similarity index 100% rename from assets/icons/aws/Customer-Enablement/IQ.png rename to core/assets/icons/aws/Customer-Enablement/IQ.png diff --git a/assets/icons/aws/Customer-Enablement/IQ.svg b/core/assets/icons/aws/Customer-Enablement/IQ.svg similarity index 100% rename from assets/icons/aws/Customer-Enablement/IQ.svg rename to core/assets/icons/aws/Customer-Enablement/IQ.svg diff --git a/assets/icons/aws/Customer-Enablement/Managed-Services.png b/core/assets/icons/aws/Customer-Enablement/Managed-Services.png similarity index 100% rename from assets/icons/aws/Customer-Enablement/Managed-Services.png rename to core/assets/icons/aws/Customer-Enablement/Managed-Services.png diff --git a/assets/icons/aws/Customer-Enablement/Managed-Services.svg b/core/assets/icons/aws/Customer-Enablement/Managed-Services.svg similarity index 100% rename from assets/icons/aws/Customer-Enablement/Managed-Services.svg rename to core/assets/icons/aws/Customer-Enablement/Managed-Services.svg diff --git a/assets/icons/aws/Customer-Enablement/Professional-Services.png b/core/assets/icons/aws/Customer-Enablement/Professional-Services.png similarity index 100% rename from assets/icons/aws/Customer-Enablement/Professional-Services.png rename to core/assets/icons/aws/Customer-Enablement/Professional-Services.png diff --git a/assets/icons/aws/Customer-Enablement/Professional-Services.svg b/core/assets/icons/aws/Customer-Enablement/Professional-Services.svg similarity index 100% rename from assets/icons/aws/Customer-Enablement/Professional-Services.svg rename to core/assets/icons/aws/Customer-Enablement/Professional-Services.svg diff --git a/assets/icons/aws/Customer-Enablement/Support.png b/core/assets/icons/aws/Customer-Enablement/Support.png similarity index 100% rename from assets/icons/aws/Customer-Enablement/Support.png rename to core/assets/icons/aws/Customer-Enablement/Support.png diff --git a/assets/icons/aws/Customer-Enablement/Support.svg b/core/assets/icons/aws/Customer-Enablement/Support.svg similarity index 100% rename from assets/icons/aws/Customer-Enablement/Support.svg rename to core/assets/icons/aws/Customer-Enablement/Support.svg diff --git a/assets/icons/aws/Customer-Enablement/Training-Certification.png b/core/assets/icons/aws/Customer-Enablement/Training-Certification.png similarity index 100% rename from assets/icons/aws/Customer-Enablement/Training-Certification.png rename to core/assets/icons/aws/Customer-Enablement/Training-Certification.png diff --git a/assets/icons/aws/Customer-Enablement/Training-Certification.svg b/core/assets/icons/aws/Customer-Enablement/Training-Certification.svg similarity index 100% rename from assets/icons/aws/Customer-Enablement/Training-Certification.svg rename to core/assets/icons/aws/Customer-Enablement/Training-Certification.svg diff --git a/assets/icons/aws/Customer-Enablement/rePost-Private.png b/core/assets/icons/aws/Customer-Enablement/rePost-Private.png similarity index 100% rename from assets/icons/aws/Customer-Enablement/rePost-Private.png rename to core/assets/icons/aws/Customer-Enablement/rePost-Private.png diff --git a/assets/icons/aws/Customer-Enablement/rePost-Private.svg b/core/assets/icons/aws/Customer-Enablement/rePost-Private.svg similarity index 100% rename from assets/icons/aws/Customer-Enablement/rePost-Private.svg rename to core/assets/icons/aws/Customer-Enablement/rePost-Private.svg diff --git a/assets/icons/aws/Customer-Enablement/rePost.png b/core/assets/icons/aws/Customer-Enablement/rePost.png similarity index 100% rename from assets/icons/aws/Customer-Enablement/rePost.png rename to core/assets/icons/aws/Customer-Enablement/rePost.png diff --git a/assets/icons/aws/Customer-Enablement/rePost.svg b/core/assets/icons/aws/Customer-Enablement/rePost.svg similarity index 100% rename from assets/icons/aws/Customer-Enablement/rePost.svg rename to core/assets/icons/aws/Customer-Enablement/rePost.svg diff --git a/assets/icons/aws/Database/Aurora.png b/core/assets/icons/aws/Database/Aurora.png similarity index 100% rename from assets/icons/aws/Database/Aurora.png rename to core/assets/icons/aws/Database/Aurora.png diff --git a/assets/icons/aws/Database/Aurora.svg b/core/assets/icons/aws/Database/Aurora.svg similarity index 100% rename from assets/icons/aws/Database/Aurora.svg rename to core/assets/icons/aws/Database/Aurora.svg diff --git a/assets/icons/aws/Database/Database-Migration-Service.png b/core/assets/icons/aws/Database/Database-Migration-Service.png similarity index 100% rename from assets/icons/aws/Database/Database-Migration-Service.png rename to core/assets/icons/aws/Database/Database-Migration-Service.png diff --git a/assets/icons/aws/Database/Database-Migration-Service.svg b/core/assets/icons/aws/Database/Database-Migration-Service.svg similarity index 100% rename from assets/icons/aws/Database/Database-Migration-Service.svg rename to core/assets/icons/aws/Database/Database-Migration-Service.svg diff --git a/assets/icons/aws/Database/DocumentDB.png b/core/assets/icons/aws/Database/DocumentDB.png similarity index 100% rename from assets/icons/aws/Database/DocumentDB.png rename to core/assets/icons/aws/Database/DocumentDB.png diff --git a/assets/icons/aws/Database/DocumentDB.svg b/core/assets/icons/aws/Database/DocumentDB.svg similarity index 100% rename from assets/icons/aws/Database/DocumentDB.svg rename to core/assets/icons/aws/Database/DocumentDB.svg diff --git a/assets/icons/aws/Database/DynamoDB.png b/core/assets/icons/aws/Database/DynamoDB.png similarity index 100% rename from assets/icons/aws/Database/DynamoDB.png rename to core/assets/icons/aws/Database/DynamoDB.png diff --git a/assets/icons/aws/Database/DynamoDB.svg b/core/assets/icons/aws/Database/DynamoDB.svg similarity index 100% rename from assets/icons/aws/Database/DynamoDB.svg rename to core/assets/icons/aws/Database/DynamoDB.svg diff --git a/assets/icons/aws/Database/ElastiCache.png b/core/assets/icons/aws/Database/ElastiCache.png similarity index 100% rename from assets/icons/aws/Database/ElastiCache.png rename to core/assets/icons/aws/Database/ElastiCache.png diff --git a/assets/icons/aws/Database/ElastiCache.svg b/core/assets/icons/aws/Database/ElastiCache.svg similarity index 100% rename from assets/icons/aws/Database/ElastiCache.svg rename to core/assets/icons/aws/Database/ElastiCache.svg diff --git a/assets/icons/aws/Database/Keyspaces.png b/core/assets/icons/aws/Database/Keyspaces.png similarity index 100% rename from assets/icons/aws/Database/Keyspaces.png rename to core/assets/icons/aws/Database/Keyspaces.png diff --git a/assets/icons/aws/Database/Keyspaces.svg b/core/assets/icons/aws/Database/Keyspaces.svg similarity index 100% rename from assets/icons/aws/Database/Keyspaces.svg rename to core/assets/icons/aws/Database/Keyspaces.svg diff --git a/assets/icons/aws/Database/MemoryDB-for-Redis.png b/core/assets/icons/aws/Database/MemoryDB-for-Redis.png similarity index 100% rename from assets/icons/aws/Database/MemoryDB-for-Redis.png rename to core/assets/icons/aws/Database/MemoryDB-for-Redis.png diff --git a/assets/icons/aws/Database/MemoryDB-for-Redis.svg b/core/assets/icons/aws/Database/MemoryDB-for-Redis.svg similarity index 100% rename from assets/icons/aws/Database/MemoryDB-for-Redis.svg rename to core/assets/icons/aws/Database/MemoryDB-for-Redis.svg diff --git a/assets/icons/aws/Database/MemoryDB.png b/core/assets/icons/aws/Database/MemoryDB.png similarity index 100% rename from assets/icons/aws/Database/MemoryDB.png rename to core/assets/icons/aws/Database/MemoryDB.png diff --git a/assets/icons/aws/Database/MemoryDB.svg b/core/assets/icons/aws/Database/MemoryDB.svg similarity index 100% rename from assets/icons/aws/Database/MemoryDB.svg rename to core/assets/icons/aws/Database/MemoryDB.svg diff --git a/assets/icons/aws/Database/Neptune.png b/core/assets/icons/aws/Database/Neptune.png similarity index 100% rename from assets/icons/aws/Database/Neptune.png rename to core/assets/icons/aws/Database/Neptune.png diff --git a/assets/icons/aws/Database/Neptune.svg b/core/assets/icons/aws/Database/Neptune.svg similarity index 100% rename from assets/icons/aws/Database/Neptune.svg rename to core/assets/icons/aws/Database/Neptune.svg diff --git a/assets/icons/aws/Database/Oracle-Database-at-AWS.png b/core/assets/icons/aws/Database/Oracle-Database-at-AWS.png similarity index 100% rename from assets/icons/aws/Database/Oracle-Database-at-AWS.png rename to core/assets/icons/aws/Database/Oracle-Database-at-AWS.png diff --git a/assets/icons/aws/Database/Oracle-Database-at-AWS.svg b/core/assets/icons/aws/Database/Oracle-Database-at-AWS.svg similarity index 100% rename from assets/icons/aws/Database/Oracle-Database-at-AWS.svg rename to core/assets/icons/aws/Database/Oracle-Database-at-AWS.svg diff --git a/assets/icons/aws/Database/RDS-on-VMware.png b/core/assets/icons/aws/Database/RDS-on-VMware.png similarity index 100% rename from assets/icons/aws/Database/RDS-on-VMware.png rename to core/assets/icons/aws/Database/RDS-on-VMware.png diff --git a/assets/icons/aws/Database/RDS-on-VMware.svg b/core/assets/icons/aws/Database/RDS-on-VMware.svg similarity index 100% rename from assets/icons/aws/Database/RDS-on-VMware.svg rename to core/assets/icons/aws/Database/RDS-on-VMware.svg diff --git a/assets/icons/aws/Database/RDS.png b/core/assets/icons/aws/Database/RDS.png similarity index 100% rename from assets/icons/aws/Database/RDS.png rename to core/assets/icons/aws/Database/RDS.png diff --git a/assets/icons/aws/Database/RDS.svg b/core/assets/icons/aws/Database/RDS.svg similarity index 100% rename from assets/icons/aws/Database/RDS.svg rename to core/assets/icons/aws/Database/RDS.svg diff --git a/assets/icons/aws/Database/Timestream.png b/core/assets/icons/aws/Database/Timestream.png similarity index 100% rename from assets/icons/aws/Database/Timestream.png rename to core/assets/icons/aws/Database/Timestream.png diff --git a/assets/icons/aws/Database/Timestream.svg b/core/assets/icons/aws/Database/Timestream.svg similarity index 100% rename from assets/icons/aws/Database/Timestream.svg rename to core/assets/icons/aws/Database/Timestream.svg diff --git a/assets/icons/aws/Developer-Tools/Application-Composer.png b/core/assets/icons/aws/Developer-Tools/Application-Composer.png similarity index 100% rename from assets/icons/aws/Developer-Tools/Application-Composer.png rename to core/assets/icons/aws/Developer-Tools/Application-Composer.png diff --git a/assets/icons/aws/Developer-Tools/Application-Composer.svg b/core/assets/icons/aws/Developer-Tools/Application-Composer.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/Application-Composer.svg rename to core/assets/icons/aws/Developer-Tools/Application-Composer.svg diff --git a/assets/icons/aws/Developer-Tools/Cloud-Control-API.png b/core/assets/icons/aws/Developer-Tools/Cloud-Control-API.png similarity index 100% rename from assets/icons/aws/Developer-Tools/Cloud-Control-API.png rename to core/assets/icons/aws/Developer-Tools/Cloud-Control-API.png diff --git a/assets/icons/aws/Developer-Tools/Cloud-Control-API.svg b/core/assets/icons/aws/Developer-Tools/Cloud-Control-API.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/Cloud-Control-API.svg rename to core/assets/icons/aws/Developer-Tools/Cloud-Control-API.svg diff --git a/assets/icons/aws/Developer-Tools/Cloud-Development-Kit.png b/core/assets/icons/aws/Developer-Tools/Cloud-Development-Kit.png similarity index 100% rename from assets/icons/aws/Developer-Tools/Cloud-Development-Kit.png rename to core/assets/icons/aws/Developer-Tools/Cloud-Development-Kit.png diff --git a/assets/icons/aws/Developer-Tools/Cloud-Development-Kit.svg b/core/assets/icons/aws/Developer-Tools/Cloud-Development-Kit.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/Cloud-Development-Kit.svg rename to core/assets/icons/aws/Developer-Tools/Cloud-Development-Kit.svg diff --git a/assets/icons/aws/Developer-Tools/Cloud9.png b/core/assets/icons/aws/Developer-Tools/Cloud9.png similarity index 100% rename from assets/icons/aws/Developer-Tools/Cloud9.png rename to core/assets/icons/aws/Developer-Tools/Cloud9.png diff --git a/assets/icons/aws/Developer-Tools/Cloud9.svg b/core/assets/icons/aws/Developer-Tools/Cloud9.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/Cloud9.svg rename to core/assets/icons/aws/Developer-Tools/Cloud9.svg diff --git a/assets/icons/aws/Developer-Tools/CloudShell.png b/core/assets/icons/aws/Developer-Tools/CloudShell.png similarity index 100% rename from assets/icons/aws/Developer-Tools/CloudShell.png rename to core/assets/icons/aws/Developer-Tools/CloudShell.png diff --git a/assets/icons/aws/Developer-Tools/CloudShell.svg b/core/assets/icons/aws/Developer-Tools/CloudShell.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/CloudShell.svg rename to core/assets/icons/aws/Developer-Tools/CloudShell.svg diff --git a/assets/icons/aws/Developer-Tools/CodeArtifact.png b/core/assets/icons/aws/Developer-Tools/CodeArtifact.png similarity index 100% rename from assets/icons/aws/Developer-Tools/CodeArtifact.png rename to core/assets/icons/aws/Developer-Tools/CodeArtifact.png diff --git a/assets/icons/aws/Developer-Tools/CodeArtifact.svg b/core/assets/icons/aws/Developer-Tools/CodeArtifact.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/CodeArtifact.svg rename to core/assets/icons/aws/Developer-Tools/CodeArtifact.svg diff --git a/assets/icons/aws/Developer-Tools/CodeBuild.png b/core/assets/icons/aws/Developer-Tools/CodeBuild.png similarity index 100% rename from assets/icons/aws/Developer-Tools/CodeBuild.png rename to core/assets/icons/aws/Developer-Tools/CodeBuild.png diff --git a/assets/icons/aws/Developer-Tools/CodeBuild.svg b/core/assets/icons/aws/Developer-Tools/CodeBuild.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/CodeBuild.svg rename to core/assets/icons/aws/Developer-Tools/CodeBuild.svg diff --git a/assets/icons/aws/Developer-Tools/CodeCatalyst.png b/core/assets/icons/aws/Developer-Tools/CodeCatalyst.png similarity index 100% rename from assets/icons/aws/Developer-Tools/CodeCatalyst.png rename to core/assets/icons/aws/Developer-Tools/CodeCatalyst.png diff --git a/assets/icons/aws/Developer-Tools/CodeCatalyst.svg b/core/assets/icons/aws/Developer-Tools/CodeCatalyst.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/CodeCatalyst.svg rename to core/assets/icons/aws/Developer-Tools/CodeCatalyst.svg diff --git a/assets/icons/aws/Developer-Tools/CodeCommit.png b/core/assets/icons/aws/Developer-Tools/CodeCommit.png similarity index 100% rename from assets/icons/aws/Developer-Tools/CodeCommit.png rename to core/assets/icons/aws/Developer-Tools/CodeCommit.png diff --git a/assets/icons/aws/Developer-Tools/CodeCommit.svg b/core/assets/icons/aws/Developer-Tools/CodeCommit.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/CodeCommit.svg rename to core/assets/icons/aws/Developer-Tools/CodeCommit.svg diff --git a/assets/icons/aws/Developer-Tools/CodeDeploy.png b/core/assets/icons/aws/Developer-Tools/CodeDeploy.png similarity index 100% rename from assets/icons/aws/Developer-Tools/CodeDeploy.png rename to core/assets/icons/aws/Developer-Tools/CodeDeploy.png diff --git a/assets/icons/aws/Developer-Tools/CodeDeploy.svg b/core/assets/icons/aws/Developer-Tools/CodeDeploy.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/CodeDeploy.svg rename to core/assets/icons/aws/Developer-Tools/CodeDeploy.svg diff --git a/assets/icons/aws/Developer-Tools/CodePipeline.png b/core/assets/icons/aws/Developer-Tools/CodePipeline.png similarity index 100% rename from assets/icons/aws/Developer-Tools/CodePipeline.png rename to core/assets/icons/aws/Developer-Tools/CodePipeline.png diff --git a/assets/icons/aws/Developer-Tools/CodePipeline.svg b/core/assets/icons/aws/Developer-Tools/CodePipeline.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/CodePipeline.svg rename to core/assets/icons/aws/Developer-Tools/CodePipeline.svg diff --git a/assets/icons/aws/Developer-Tools/CodeStar.png b/core/assets/icons/aws/Developer-Tools/CodeStar.png similarity index 100% rename from assets/icons/aws/Developer-Tools/CodeStar.png rename to core/assets/icons/aws/Developer-Tools/CodeStar.png diff --git a/assets/icons/aws/Developer-Tools/CodeStar.svg b/core/assets/icons/aws/Developer-Tools/CodeStar.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/CodeStar.svg rename to core/assets/icons/aws/Developer-Tools/CodeStar.svg diff --git a/assets/icons/aws/Developer-Tools/Command-Line-Interface.png b/core/assets/icons/aws/Developer-Tools/Command-Line-Interface.png similarity index 100% rename from assets/icons/aws/Developer-Tools/Command-Line-Interface.png rename to core/assets/icons/aws/Developer-Tools/Command-Line-Interface.png diff --git a/assets/icons/aws/Developer-Tools/Command-Line-Interface.svg b/core/assets/icons/aws/Developer-Tools/Command-Line-Interface.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/Command-Line-Interface.svg rename to core/assets/icons/aws/Developer-Tools/Command-Line-Interface.svg diff --git a/assets/icons/aws/Developer-Tools/Corretto.png b/core/assets/icons/aws/Developer-Tools/Corretto.png similarity index 100% rename from assets/icons/aws/Developer-Tools/Corretto.png rename to core/assets/icons/aws/Developer-Tools/Corretto.png diff --git a/assets/icons/aws/Developer-Tools/Corretto.svg b/core/assets/icons/aws/Developer-Tools/Corretto.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/Corretto.svg rename to core/assets/icons/aws/Developer-Tools/Corretto.svg diff --git a/assets/icons/aws/Developer-Tools/Fault-Injection-Service.png b/core/assets/icons/aws/Developer-Tools/Fault-Injection-Service.png similarity index 100% rename from assets/icons/aws/Developer-Tools/Fault-Injection-Service.png rename to core/assets/icons/aws/Developer-Tools/Fault-Injection-Service.png diff --git a/assets/icons/aws/Developer-Tools/Fault-Injection-Service.svg b/core/assets/icons/aws/Developer-Tools/Fault-Injection-Service.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/Fault-Injection-Service.svg rename to core/assets/icons/aws/Developer-Tools/Fault-Injection-Service.svg diff --git a/assets/icons/aws/Developer-Tools/Infrastructure-Composer.png b/core/assets/icons/aws/Developer-Tools/Infrastructure-Composer.png similarity index 100% rename from assets/icons/aws/Developer-Tools/Infrastructure-Composer.png rename to core/assets/icons/aws/Developer-Tools/Infrastructure-Composer.png diff --git a/assets/icons/aws/Developer-Tools/Infrastructure-Composer.svg b/core/assets/icons/aws/Developer-Tools/Infrastructure-Composer.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/Infrastructure-Composer.svg rename to core/assets/icons/aws/Developer-Tools/Infrastructure-Composer.svg diff --git a/assets/icons/aws/Developer-Tools/Tools-and-SDKs.png b/core/assets/icons/aws/Developer-Tools/Tools-and-SDKs.png similarity index 100% rename from assets/icons/aws/Developer-Tools/Tools-and-SDKs.png rename to core/assets/icons/aws/Developer-Tools/Tools-and-SDKs.png diff --git a/assets/icons/aws/Developer-Tools/Tools-and-SDKs.svg b/core/assets/icons/aws/Developer-Tools/Tools-and-SDKs.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/Tools-and-SDKs.svg rename to core/assets/icons/aws/Developer-Tools/Tools-and-SDKs.svg diff --git a/assets/icons/aws/Developer-Tools/X-Ray.png b/core/assets/icons/aws/Developer-Tools/X-Ray.png similarity index 100% rename from assets/icons/aws/Developer-Tools/X-Ray.png rename to core/assets/icons/aws/Developer-Tools/X-Ray.png diff --git a/assets/icons/aws/Developer-Tools/X-Ray.svg b/core/assets/icons/aws/Developer-Tools/X-Ray.svg similarity index 100% rename from assets/icons/aws/Developer-Tools/X-Ray.svg rename to core/assets/icons/aws/Developer-Tools/X-Ray.svg diff --git a/assets/icons/aws/End-User-Computing/AppStream.png b/core/assets/icons/aws/End-User-Computing/AppStream.png similarity index 100% rename from assets/icons/aws/End-User-Computing/AppStream.png rename to core/assets/icons/aws/End-User-Computing/AppStream.png diff --git a/assets/icons/aws/End-User-Computing/AppStream.svg b/core/assets/icons/aws/End-User-Computing/AppStream.svg similarity index 100% rename from assets/icons/aws/End-User-Computing/AppStream.svg rename to core/assets/icons/aws/End-User-Computing/AppStream.svg diff --git a/assets/icons/aws/End-User-Computing/WorkLink.png b/core/assets/icons/aws/End-User-Computing/WorkLink.png similarity index 100% rename from assets/icons/aws/End-User-Computing/WorkLink.png rename to core/assets/icons/aws/End-User-Computing/WorkLink.png diff --git a/assets/icons/aws/End-User-Computing/WorkLink.svg b/core/assets/icons/aws/End-User-Computing/WorkLink.svg similarity index 100% rename from assets/icons/aws/End-User-Computing/WorkLink.svg rename to core/assets/icons/aws/End-User-Computing/WorkLink.svg diff --git a/assets/icons/aws/End-User-Computing/WorkSpaces-Family.png b/core/assets/icons/aws/End-User-Computing/WorkSpaces-Family.png similarity index 100% rename from assets/icons/aws/End-User-Computing/WorkSpaces-Family.png rename to core/assets/icons/aws/End-User-Computing/WorkSpaces-Family.png diff --git a/assets/icons/aws/End-User-Computing/WorkSpaces-Family.svg b/core/assets/icons/aws/End-User-Computing/WorkSpaces-Family.svg similarity index 100% rename from assets/icons/aws/End-User-Computing/WorkSpaces-Family.svg rename to core/assets/icons/aws/End-User-Computing/WorkSpaces-Family.svg diff --git a/assets/icons/aws/End-User-Computing/WorkSpaces.png b/core/assets/icons/aws/End-User-Computing/WorkSpaces.png similarity index 100% rename from assets/icons/aws/End-User-Computing/WorkSpaces.png rename to core/assets/icons/aws/End-User-Computing/WorkSpaces.png diff --git a/assets/icons/aws/End-User-Computing/WorkSpaces.svg b/core/assets/icons/aws/End-User-Computing/WorkSpaces.svg similarity index 100% rename from assets/icons/aws/End-User-Computing/WorkSpaces.svg rename to core/assets/icons/aws/End-User-Computing/WorkSpaces.svg diff --git a/assets/icons/aws/Front-End-Web-Mobile/Amplify.png b/core/assets/icons/aws/Front-End-Web-Mobile/Amplify.png similarity index 100% rename from assets/icons/aws/Front-End-Web-Mobile/Amplify.png rename to core/assets/icons/aws/Front-End-Web-Mobile/Amplify.png diff --git a/assets/icons/aws/Front-End-Web-Mobile/Amplify.svg b/core/assets/icons/aws/Front-End-Web-Mobile/Amplify.svg similarity index 100% rename from assets/icons/aws/Front-End-Web-Mobile/Amplify.svg rename to core/assets/icons/aws/Front-End-Web-Mobile/Amplify.svg diff --git a/assets/icons/aws/Front-End-Web-Mobile/Device-Farm.png b/core/assets/icons/aws/Front-End-Web-Mobile/Device-Farm.png similarity index 100% rename from assets/icons/aws/Front-End-Web-Mobile/Device-Farm.png rename to core/assets/icons/aws/Front-End-Web-Mobile/Device-Farm.png diff --git a/assets/icons/aws/Front-End-Web-Mobile/Device-Farm.svg b/core/assets/icons/aws/Front-End-Web-Mobile/Device-Farm.svg similarity index 100% rename from assets/icons/aws/Front-End-Web-Mobile/Device-Farm.svg rename to core/assets/icons/aws/Front-End-Web-Mobile/Device-Farm.svg diff --git a/assets/icons/aws/Front-End-Web-Mobile/Location-Service.png b/core/assets/icons/aws/Front-End-Web-Mobile/Location-Service.png similarity index 100% rename from assets/icons/aws/Front-End-Web-Mobile/Location-Service.png rename to core/assets/icons/aws/Front-End-Web-Mobile/Location-Service.png diff --git a/assets/icons/aws/Front-End-Web-Mobile/Location-Service.svg b/core/assets/icons/aws/Front-End-Web-Mobile/Location-Service.svg similarity index 100% rename from assets/icons/aws/Front-End-Web-Mobile/Location-Service.svg rename to core/assets/icons/aws/Front-End-Web-Mobile/Location-Service.svg diff --git a/assets/icons/aws/Games/GameKit.png b/core/assets/icons/aws/Games/GameKit.png similarity index 100% rename from assets/icons/aws/Games/GameKit.png rename to core/assets/icons/aws/Games/GameKit.png diff --git a/assets/icons/aws/Games/GameKit.svg b/core/assets/icons/aws/Games/GameKit.svg similarity index 100% rename from assets/icons/aws/Games/GameKit.svg rename to core/assets/icons/aws/Games/GameKit.svg diff --git a/assets/icons/aws/Games/GameLift-Servers.png b/core/assets/icons/aws/Games/GameLift-Servers.png similarity index 100% rename from assets/icons/aws/Games/GameLift-Servers.png rename to core/assets/icons/aws/Games/GameLift-Servers.png diff --git a/assets/icons/aws/Games/GameLift-Servers.svg b/core/assets/icons/aws/Games/GameLift-Servers.svg similarity index 100% rename from assets/icons/aws/Games/GameLift-Servers.svg rename to core/assets/icons/aws/Games/GameLift-Servers.svg diff --git a/assets/icons/aws/Games/GameLift-Streams.png b/core/assets/icons/aws/Games/GameLift-Streams.png similarity index 100% rename from assets/icons/aws/Games/GameLift-Streams.png rename to core/assets/icons/aws/Games/GameLift-Streams.png diff --git a/assets/icons/aws/Games/GameLift-Streams.svg b/core/assets/icons/aws/Games/GameLift-Streams.svg similarity index 100% rename from assets/icons/aws/Games/GameLift-Streams.svg rename to core/assets/icons/aws/Games/GameLift-Streams.svg diff --git a/assets/icons/aws/Games/GameLift.png b/core/assets/icons/aws/Games/GameLift.png similarity index 100% rename from assets/icons/aws/Games/GameLift.png rename to core/assets/icons/aws/Games/GameLift.png diff --git a/assets/icons/aws/Games/GameLift.svg b/core/assets/icons/aws/Games/GameLift.svg similarity index 100% rename from assets/icons/aws/Games/GameLift.svg rename to core/assets/icons/aws/Games/GameLift.svg diff --git a/assets/icons/aws/Games/GameSparks.png b/core/assets/icons/aws/Games/GameSparks.png similarity index 100% rename from assets/icons/aws/Games/GameSparks.png rename to core/assets/icons/aws/Games/GameSparks.png diff --git a/assets/icons/aws/Games/GameSparks.svg b/core/assets/icons/aws/Games/GameSparks.svg similarity index 100% rename from assets/icons/aws/Games/GameSparks.svg rename to core/assets/icons/aws/Games/GameSparks.svg diff --git a/assets/icons/aws/Games/Lumberyard.png b/core/assets/icons/aws/Games/Lumberyard.png similarity index 100% rename from assets/icons/aws/Games/Lumberyard.png rename to core/assets/icons/aws/Games/Lumberyard.png diff --git a/assets/icons/aws/Games/Lumberyard.svg b/core/assets/icons/aws/Games/Lumberyard.svg similarity index 100% rename from assets/icons/aws/Games/Lumberyard.svg rename to core/assets/icons/aws/Games/Lumberyard.svg diff --git a/assets/icons/aws/Games/Open-3D-Engine.png b/core/assets/icons/aws/Games/Open-3D-Engine.png similarity index 100% rename from assets/icons/aws/Games/Open-3D-Engine.png rename to core/assets/icons/aws/Games/Open-3D-Engine.png diff --git a/assets/icons/aws/Games/Open-3D-Engine.svg b/core/assets/icons/aws/Games/Open-3D-Engine.svg similarity index 100% rename from assets/icons/aws/Games/Open-3D-Engine.svg rename to core/assets/icons/aws/Games/Open-3D-Engine.svg diff --git a/assets/icons/aws/General-Icons/Marketplace_Dark.png b/core/assets/icons/aws/General-Icons/Marketplace_Dark.png similarity index 100% rename from assets/icons/aws/General-Icons/Marketplace_Dark.png rename to core/assets/icons/aws/General-Icons/Marketplace_Dark.png diff --git a/assets/icons/aws/General-Icons/Marketplace_Dark.svg b/core/assets/icons/aws/General-Icons/Marketplace_Dark.svg similarity index 100% rename from assets/icons/aws/General-Icons/Marketplace_Dark.svg rename to core/assets/icons/aws/General-Icons/Marketplace_Dark.svg diff --git a/assets/icons/aws/General-Icons/Marketplace_Light.png b/core/assets/icons/aws/General-Icons/Marketplace_Light.png similarity index 100% rename from assets/icons/aws/General-Icons/Marketplace_Light.png rename to core/assets/icons/aws/General-Icons/Marketplace_Light.png diff --git a/assets/icons/aws/General-Icons/Marketplace_Light.svg b/core/assets/icons/aws/General-Icons/Marketplace_Light.svg similarity index 100% rename from assets/icons/aws/General-Icons/Marketplace_Light.svg rename to core/assets/icons/aws/General-Icons/Marketplace_Light.svg diff --git a/assets/icons/aws/Internet-of-Things/FreeRTOS.png b/core/assets/icons/aws/Internet-of-Things/FreeRTOS.png similarity index 100% rename from assets/icons/aws/Internet-of-Things/FreeRTOS.png rename to core/assets/icons/aws/Internet-of-Things/FreeRTOS.png diff --git a/assets/icons/aws/Internet-of-Things/FreeRTOS.svg b/core/assets/icons/aws/Internet-of-Things/FreeRTOS.svg similarity index 100% rename from assets/icons/aws/Internet-of-Things/FreeRTOS.svg rename to core/assets/icons/aws/Internet-of-Things/FreeRTOS.svg diff --git a/assets/icons/aws/Internet-of-Things/IoT-1-Click.png b/core/assets/icons/aws/Internet-of-Things/IoT-1-Click.png similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-1-Click.png rename to core/assets/icons/aws/Internet-of-Things/IoT-1-Click.png diff --git a/assets/icons/aws/Internet-of-Things/IoT-1-Click.svg b/core/assets/icons/aws/Internet-of-Things/IoT-1-Click.svg similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-1-Click.svg rename to core/assets/icons/aws/Internet-of-Things/IoT-1-Click.svg diff --git a/assets/icons/aws/Internet-of-Things/IoT-Analytics.png b/core/assets/icons/aws/Internet-of-Things/IoT-Analytics.png similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-Analytics.png rename to core/assets/icons/aws/Internet-of-Things/IoT-Analytics.png diff --git a/assets/icons/aws/Internet-of-Things/IoT-Analytics.svg b/core/assets/icons/aws/Internet-of-Things/IoT-Analytics.svg similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-Analytics.svg rename to core/assets/icons/aws/Internet-of-Things/IoT-Analytics.svg diff --git a/assets/icons/aws/Internet-of-Things/IoT-Button.png b/core/assets/icons/aws/Internet-of-Things/IoT-Button.png similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-Button.png rename to core/assets/icons/aws/Internet-of-Things/IoT-Button.png diff --git a/assets/icons/aws/Internet-of-Things/IoT-Button.svg b/core/assets/icons/aws/Internet-of-Things/IoT-Button.svg similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-Button.svg rename to core/assets/icons/aws/Internet-of-Things/IoT-Button.svg diff --git a/assets/icons/aws/Internet-of-Things/IoT-Core.png b/core/assets/icons/aws/Internet-of-Things/IoT-Core.png similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-Core.png rename to core/assets/icons/aws/Internet-of-Things/IoT-Core.png diff --git a/assets/icons/aws/Internet-of-Things/IoT-Core.svg b/core/assets/icons/aws/Internet-of-Things/IoT-Core.svg similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-Core.svg rename to core/assets/icons/aws/Internet-of-Things/IoT-Core.svg diff --git a/assets/icons/aws/Internet-of-Things/IoT-Device-Defender.png b/core/assets/icons/aws/Internet-of-Things/IoT-Device-Defender.png similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-Device-Defender.png rename to core/assets/icons/aws/Internet-of-Things/IoT-Device-Defender.png diff --git a/assets/icons/aws/Internet-of-Things/IoT-Device-Defender.svg b/core/assets/icons/aws/Internet-of-Things/IoT-Device-Defender.svg similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-Device-Defender.svg rename to core/assets/icons/aws/Internet-of-Things/IoT-Device-Defender.svg diff --git a/assets/icons/aws/Internet-of-Things/IoT-Device-Management.png b/core/assets/icons/aws/Internet-of-Things/IoT-Device-Management.png similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-Device-Management.png rename to core/assets/icons/aws/Internet-of-Things/IoT-Device-Management.png diff --git a/assets/icons/aws/Internet-of-Things/IoT-Device-Management.svg b/core/assets/icons/aws/Internet-of-Things/IoT-Device-Management.svg similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-Device-Management.svg rename to core/assets/icons/aws/Internet-of-Things/IoT-Device-Management.svg diff --git a/assets/icons/aws/Internet-of-Things/IoT-EduKit.png b/core/assets/icons/aws/Internet-of-Things/IoT-EduKit.png similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-EduKit.png rename to core/assets/icons/aws/Internet-of-Things/IoT-EduKit.png diff --git a/assets/icons/aws/Internet-of-Things/IoT-EduKit.svg b/core/assets/icons/aws/Internet-of-Things/IoT-EduKit.svg similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-EduKit.svg rename to core/assets/icons/aws/Internet-of-Things/IoT-EduKit.svg diff --git a/assets/icons/aws/Internet-of-Things/IoT-Events.png b/core/assets/icons/aws/Internet-of-Things/IoT-Events.png similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-Events.png rename to core/assets/icons/aws/Internet-of-Things/IoT-Events.png diff --git a/assets/icons/aws/Internet-of-Things/IoT-Events.svg b/core/assets/icons/aws/Internet-of-Things/IoT-Events.svg similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-Events.svg rename to core/assets/icons/aws/Internet-of-Things/IoT-Events.svg diff --git a/assets/icons/aws/Internet-of-Things/IoT-ExpressLink.png b/core/assets/icons/aws/Internet-of-Things/IoT-ExpressLink.png similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-ExpressLink.png rename to core/assets/icons/aws/Internet-of-Things/IoT-ExpressLink.png diff --git a/assets/icons/aws/Internet-of-Things/IoT-ExpressLink.svg b/core/assets/icons/aws/Internet-of-Things/IoT-ExpressLink.svg similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-ExpressLink.svg rename to core/assets/icons/aws/Internet-of-Things/IoT-ExpressLink.svg diff --git a/assets/icons/aws/Internet-of-Things/IoT-FleetWise.png b/core/assets/icons/aws/Internet-of-Things/IoT-FleetWise.png similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-FleetWise.png rename to core/assets/icons/aws/Internet-of-Things/IoT-FleetWise.png diff --git a/assets/icons/aws/Internet-of-Things/IoT-FleetWise.svg b/core/assets/icons/aws/Internet-of-Things/IoT-FleetWise.svg similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-FleetWise.svg rename to core/assets/icons/aws/Internet-of-Things/IoT-FleetWise.svg diff --git a/assets/icons/aws/Internet-of-Things/IoT-Greengrass.png b/core/assets/icons/aws/Internet-of-Things/IoT-Greengrass.png similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-Greengrass.png rename to core/assets/icons/aws/Internet-of-Things/IoT-Greengrass.png diff --git a/assets/icons/aws/Internet-of-Things/IoT-Greengrass.svg b/core/assets/icons/aws/Internet-of-Things/IoT-Greengrass.svg similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-Greengrass.svg rename to core/assets/icons/aws/Internet-of-Things/IoT-Greengrass.svg diff --git a/assets/icons/aws/Internet-of-Things/IoT-RoboRunner.png b/core/assets/icons/aws/Internet-of-Things/IoT-RoboRunner.png similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-RoboRunner.png rename to core/assets/icons/aws/Internet-of-Things/IoT-RoboRunner.png diff --git a/assets/icons/aws/Internet-of-Things/IoT-RoboRunner.svg b/core/assets/icons/aws/Internet-of-Things/IoT-RoboRunner.svg similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-RoboRunner.svg rename to core/assets/icons/aws/Internet-of-Things/IoT-RoboRunner.svg diff --git a/assets/icons/aws/Internet-of-Things/IoT-SiteWise.png b/core/assets/icons/aws/Internet-of-Things/IoT-SiteWise.png similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-SiteWise.png rename to core/assets/icons/aws/Internet-of-Things/IoT-SiteWise.png diff --git a/assets/icons/aws/Internet-of-Things/IoT-SiteWise.svg b/core/assets/icons/aws/Internet-of-Things/IoT-SiteWise.svg similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-SiteWise.svg rename to core/assets/icons/aws/Internet-of-Things/IoT-SiteWise.svg diff --git a/assets/icons/aws/Internet-of-Things/IoT-Things-Graph.png b/core/assets/icons/aws/Internet-of-Things/IoT-Things-Graph.png similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-Things-Graph.png rename to core/assets/icons/aws/Internet-of-Things/IoT-Things-Graph.png diff --git a/assets/icons/aws/Internet-of-Things/IoT-Things-Graph.svg b/core/assets/icons/aws/Internet-of-Things/IoT-Things-Graph.svg similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-Things-Graph.svg rename to core/assets/icons/aws/Internet-of-Things/IoT-Things-Graph.svg diff --git a/assets/icons/aws/Internet-of-Things/IoT-TwinMaker.png b/core/assets/icons/aws/Internet-of-Things/IoT-TwinMaker.png similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-TwinMaker.png rename to core/assets/icons/aws/Internet-of-Things/IoT-TwinMaker.png diff --git a/assets/icons/aws/Internet-of-Things/IoT-TwinMaker.svg b/core/assets/icons/aws/Internet-of-Things/IoT-TwinMaker.svg similarity index 100% rename from assets/icons/aws/Internet-of-Things/IoT-TwinMaker.svg rename to core/assets/icons/aws/Internet-of-Things/IoT-TwinMaker.svg diff --git a/assets/icons/aws/Machine-Learning/Apache-MXNet-on-AWS.png b/core/assets/icons/aws/Machine-Learning/Apache-MXNet-on-AWS.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Apache-MXNet-on-AWS.png rename to core/assets/icons/aws/Machine-Learning/Apache-MXNet-on-AWS.png diff --git a/assets/icons/aws/Machine-Learning/Apache-MXNet-on-AWS.svg b/core/assets/icons/aws/Machine-Learning/Apache-MXNet-on-AWS.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Apache-MXNet-on-AWS.svg rename to core/assets/icons/aws/Machine-Learning/Apache-MXNet-on-AWS.svg diff --git a/assets/icons/aws/Machine-Learning/App-Studio.png b/core/assets/icons/aws/Machine-Learning/App-Studio.png similarity index 100% rename from assets/icons/aws/Machine-Learning/App-Studio.png rename to core/assets/icons/aws/Machine-Learning/App-Studio.png diff --git a/assets/icons/aws/Machine-Learning/App-Studio.svg b/core/assets/icons/aws/Machine-Learning/App-Studio.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/App-Studio.svg rename to core/assets/icons/aws/Machine-Learning/App-Studio.svg diff --git a/assets/icons/aws/Machine-Learning/Augmented-AI-A2I.png b/core/assets/icons/aws/Machine-Learning/Augmented-AI-A2I.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Augmented-AI-A2I.png rename to core/assets/icons/aws/Machine-Learning/Augmented-AI-A2I.png diff --git a/assets/icons/aws/Machine-Learning/Augmented-AI-A2I.svg b/core/assets/icons/aws/Machine-Learning/Augmented-AI-A2I.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Augmented-AI-A2I.svg rename to core/assets/icons/aws/Machine-Learning/Augmented-AI-A2I.svg diff --git a/assets/icons/aws/Machine-Learning/Bedrock-AgentCore.png b/core/assets/icons/aws/Machine-Learning/Bedrock-AgentCore.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Bedrock-AgentCore.png rename to core/assets/icons/aws/Machine-Learning/Bedrock-AgentCore.png diff --git a/assets/icons/aws/Machine-Learning/Bedrock-AgentCore.svg b/core/assets/icons/aws/Machine-Learning/Bedrock-AgentCore.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Bedrock-AgentCore.svg rename to core/assets/icons/aws/Machine-Learning/Bedrock-AgentCore.svg diff --git a/assets/icons/aws/Machine-Learning/Bedrock.png b/core/assets/icons/aws/Machine-Learning/Bedrock.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Bedrock.png rename to core/assets/icons/aws/Machine-Learning/Bedrock.png diff --git a/assets/icons/aws/Machine-Learning/Bedrock.svg b/core/assets/icons/aws/Machine-Learning/Bedrock.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Bedrock.svg rename to core/assets/icons/aws/Machine-Learning/Bedrock.svg diff --git a/assets/icons/aws/Machine-Learning/CodeGuru.png b/core/assets/icons/aws/Machine-Learning/CodeGuru.png similarity index 100% rename from assets/icons/aws/Machine-Learning/CodeGuru.png rename to core/assets/icons/aws/Machine-Learning/CodeGuru.png diff --git a/assets/icons/aws/Machine-Learning/CodeGuru.svg b/core/assets/icons/aws/Machine-Learning/CodeGuru.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/CodeGuru.svg rename to core/assets/icons/aws/Machine-Learning/CodeGuru.svg diff --git a/assets/icons/aws/Machine-Learning/CodeWhisperer.png b/core/assets/icons/aws/Machine-Learning/CodeWhisperer.png similarity index 100% rename from assets/icons/aws/Machine-Learning/CodeWhisperer.png rename to core/assets/icons/aws/Machine-Learning/CodeWhisperer.png diff --git a/assets/icons/aws/Machine-Learning/CodeWhisperer.svg b/core/assets/icons/aws/Machine-Learning/CodeWhisperer.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/CodeWhisperer.svg rename to core/assets/icons/aws/Machine-Learning/CodeWhisperer.svg diff --git a/assets/icons/aws/Machine-Learning/Comprehend-Medical.png b/core/assets/icons/aws/Machine-Learning/Comprehend-Medical.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Comprehend-Medical.png rename to core/assets/icons/aws/Machine-Learning/Comprehend-Medical.png diff --git a/assets/icons/aws/Machine-Learning/Comprehend-Medical.svg b/core/assets/icons/aws/Machine-Learning/Comprehend-Medical.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Comprehend-Medical.svg rename to core/assets/icons/aws/Machine-Learning/Comprehend-Medical.svg diff --git a/assets/icons/aws/Machine-Learning/Comprehend.png b/core/assets/icons/aws/Machine-Learning/Comprehend.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Comprehend.png rename to core/assets/icons/aws/Machine-Learning/Comprehend.png diff --git a/assets/icons/aws/Machine-Learning/Comprehend.svg b/core/assets/icons/aws/Machine-Learning/Comprehend.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Comprehend.svg rename to core/assets/icons/aws/Machine-Learning/Comprehend.svg diff --git a/assets/icons/aws/Machine-Learning/Deep-Learning-AMIs.png b/core/assets/icons/aws/Machine-Learning/Deep-Learning-AMIs.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Deep-Learning-AMIs.png rename to core/assets/icons/aws/Machine-Learning/Deep-Learning-AMIs.png diff --git a/assets/icons/aws/Machine-Learning/Deep-Learning-AMIs.svg b/core/assets/icons/aws/Machine-Learning/Deep-Learning-AMIs.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Deep-Learning-AMIs.svg rename to core/assets/icons/aws/Machine-Learning/Deep-Learning-AMIs.svg diff --git a/assets/icons/aws/Machine-Learning/Deep-Learning-Containers.png b/core/assets/icons/aws/Machine-Learning/Deep-Learning-Containers.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Deep-Learning-Containers.png rename to core/assets/icons/aws/Machine-Learning/Deep-Learning-Containers.png diff --git a/assets/icons/aws/Machine-Learning/Deep-Learning-Containers.svg b/core/assets/icons/aws/Machine-Learning/Deep-Learning-Containers.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Deep-Learning-Containers.svg rename to core/assets/icons/aws/Machine-Learning/Deep-Learning-Containers.svg diff --git a/assets/icons/aws/Machine-Learning/DeepComposer.png b/core/assets/icons/aws/Machine-Learning/DeepComposer.png similarity index 100% rename from assets/icons/aws/Machine-Learning/DeepComposer.png rename to core/assets/icons/aws/Machine-Learning/DeepComposer.png diff --git a/assets/icons/aws/Machine-Learning/DeepComposer.svg b/core/assets/icons/aws/Machine-Learning/DeepComposer.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/DeepComposer.svg rename to core/assets/icons/aws/Machine-Learning/DeepComposer.svg diff --git a/assets/icons/aws/Machine-Learning/DeepLens.png b/core/assets/icons/aws/Machine-Learning/DeepLens.png similarity index 100% rename from assets/icons/aws/Machine-Learning/DeepLens.png rename to core/assets/icons/aws/Machine-Learning/DeepLens.png diff --git a/assets/icons/aws/Machine-Learning/DeepLens.svg b/core/assets/icons/aws/Machine-Learning/DeepLens.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/DeepLens.svg rename to core/assets/icons/aws/Machine-Learning/DeepLens.svg diff --git a/assets/icons/aws/Machine-Learning/DeepRacer.png b/core/assets/icons/aws/Machine-Learning/DeepRacer.png similarity index 100% rename from assets/icons/aws/Machine-Learning/DeepRacer.png rename to core/assets/icons/aws/Machine-Learning/DeepRacer.png diff --git a/assets/icons/aws/Machine-Learning/DeepRacer.svg b/core/assets/icons/aws/Machine-Learning/DeepRacer.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/DeepRacer.svg rename to core/assets/icons/aws/Machine-Learning/DeepRacer.svg diff --git a/assets/icons/aws/Machine-Learning/DevOps-Guru.png b/core/assets/icons/aws/Machine-Learning/DevOps-Guru.png similarity index 100% rename from assets/icons/aws/Machine-Learning/DevOps-Guru.png rename to core/assets/icons/aws/Machine-Learning/DevOps-Guru.png diff --git a/assets/icons/aws/Machine-Learning/DevOps-Guru.svg b/core/assets/icons/aws/Machine-Learning/DevOps-Guru.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/DevOps-Guru.svg rename to core/assets/icons/aws/Machine-Learning/DevOps-Guru.svg diff --git a/assets/icons/aws/Machine-Learning/Elastic-Inference.png b/core/assets/icons/aws/Machine-Learning/Elastic-Inference.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Elastic-Inference.png rename to core/assets/icons/aws/Machine-Learning/Elastic-Inference.png diff --git a/assets/icons/aws/Machine-Learning/Elastic-Inference.svg b/core/assets/icons/aws/Machine-Learning/Elastic-Inference.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Elastic-Inference.svg rename to core/assets/icons/aws/Machine-Learning/Elastic-Inference.svg diff --git a/assets/icons/aws/Machine-Learning/Elemental-Inference.png b/core/assets/icons/aws/Machine-Learning/Elemental-Inference.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Elemental-Inference.png rename to core/assets/icons/aws/Machine-Learning/Elemental-Inference.png diff --git a/assets/icons/aws/Machine-Learning/Elemental-Inference.svg b/core/assets/icons/aws/Machine-Learning/Elemental-Inference.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Elemental-Inference.svg rename to core/assets/icons/aws/Machine-Learning/Elemental-Inference.svg diff --git a/assets/icons/aws/Machine-Learning/Forecast.png b/core/assets/icons/aws/Machine-Learning/Forecast.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Forecast.png rename to core/assets/icons/aws/Machine-Learning/Forecast.png diff --git a/assets/icons/aws/Machine-Learning/Forecast.svg b/core/assets/icons/aws/Machine-Learning/Forecast.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Forecast.svg rename to core/assets/icons/aws/Machine-Learning/Forecast.svg diff --git a/assets/icons/aws/Machine-Learning/Fraud-Detector.png b/core/assets/icons/aws/Machine-Learning/Fraud-Detector.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Fraud-Detector.png rename to core/assets/icons/aws/Machine-Learning/Fraud-Detector.png diff --git a/assets/icons/aws/Machine-Learning/Fraud-Detector.svg b/core/assets/icons/aws/Machine-Learning/Fraud-Detector.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Fraud-Detector.svg rename to core/assets/icons/aws/Machine-Learning/Fraud-Detector.svg diff --git a/assets/icons/aws/Machine-Learning/HealthImaging.png b/core/assets/icons/aws/Machine-Learning/HealthImaging.png similarity index 100% rename from assets/icons/aws/Machine-Learning/HealthImaging.png rename to core/assets/icons/aws/Machine-Learning/HealthImaging.png diff --git a/assets/icons/aws/Machine-Learning/HealthImaging.svg b/core/assets/icons/aws/Machine-Learning/HealthImaging.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/HealthImaging.svg rename to core/assets/icons/aws/Machine-Learning/HealthImaging.svg diff --git a/assets/icons/aws/Machine-Learning/HealthLake.png b/core/assets/icons/aws/Machine-Learning/HealthLake.png similarity index 100% rename from assets/icons/aws/Machine-Learning/HealthLake.png rename to core/assets/icons/aws/Machine-Learning/HealthLake.png diff --git a/assets/icons/aws/Machine-Learning/HealthLake.svg b/core/assets/icons/aws/Machine-Learning/HealthLake.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/HealthLake.svg rename to core/assets/icons/aws/Machine-Learning/HealthLake.svg diff --git a/assets/icons/aws/Machine-Learning/HealthOmics.png b/core/assets/icons/aws/Machine-Learning/HealthOmics.png similarity index 100% rename from assets/icons/aws/Machine-Learning/HealthOmics.png rename to core/assets/icons/aws/Machine-Learning/HealthOmics.png diff --git a/assets/icons/aws/Machine-Learning/HealthOmics.svg b/core/assets/icons/aws/Machine-Learning/HealthOmics.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/HealthOmics.svg rename to core/assets/icons/aws/Machine-Learning/HealthOmics.svg diff --git a/assets/icons/aws/Machine-Learning/HealthScribe.png b/core/assets/icons/aws/Machine-Learning/HealthScribe.png similarity index 100% rename from assets/icons/aws/Machine-Learning/HealthScribe.png rename to core/assets/icons/aws/Machine-Learning/HealthScribe.png diff --git a/assets/icons/aws/Machine-Learning/HealthScribe.svg b/core/assets/icons/aws/Machine-Learning/HealthScribe.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/HealthScribe.svg rename to core/assets/icons/aws/Machine-Learning/HealthScribe.svg diff --git a/assets/icons/aws/Machine-Learning/Kendra.png b/core/assets/icons/aws/Machine-Learning/Kendra.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Kendra.png rename to core/assets/icons/aws/Machine-Learning/Kendra.png diff --git a/assets/icons/aws/Machine-Learning/Kendra.svg b/core/assets/icons/aws/Machine-Learning/Kendra.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Kendra.svg rename to core/assets/icons/aws/Machine-Learning/Kendra.svg diff --git a/assets/icons/aws/Machine-Learning/Lex.png b/core/assets/icons/aws/Machine-Learning/Lex.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Lex.png rename to core/assets/icons/aws/Machine-Learning/Lex.png diff --git a/assets/icons/aws/Machine-Learning/Lex.svg b/core/assets/icons/aws/Machine-Learning/Lex.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Lex.svg rename to core/assets/icons/aws/Machine-Learning/Lex.svg diff --git a/assets/icons/aws/Machine-Learning/Lookout-for-Equipment.png b/core/assets/icons/aws/Machine-Learning/Lookout-for-Equipment.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Lookout-for-Equipment.png rename to core/assets/icons/aws/Machine-Learning/Lookout-for-Equipment.png diff --git a/assets/icons/aws/Machine-Learning/Lookout-for-Equipment.svg b/core/assets/icons/aws/Machine-Learning/Lookout-for-Equipment.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Lookout-for-Equipment.svg rename to core/assets/icons/aws/Machine-Learning/Lookout-for-Equipment.svg diff --git a/assets/icons/aws/Machine-Learning/Lookout-for-Metrics.png b/core/assets/icons/aws/Machine-Learning/Lookout-for-Metrics.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Lookout-for-Metrics.png rename to core/assets/icons/aws/Machine-Learning/Lookout-for-Metrics.png diff --git a/assets/icons/aws/Machine-Learning/Lookout-for-Metrics.svg b/core/assets/icons/aws/Machine-Learning/Lookout-for-Metrics.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Lookout-for-Metrics.svg rename to core/assets/icons/aws/Machine-Learning/Lookout-for-Metrics.svg diff --git a/assets/icons/aws/Machine-Learning/Lookout-for-Vision.png b/core/assets/icons/aws/Machine-Learning/Lookout-for-Vision.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Lookout-for-Vision.png rename to core/assets/icons/aws/Machine-Learning/Lookout-for-Vision.png diff --git a/assets/icons/aws/Machine-Learning/Lookout-for-Vision.svg b/core/assets/icons/aws/Machine-Learning/Lookout-for-Vision.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Lookout-for-Vision.svg rename to core/assets/icons/aws/Machine-Learning/Lookout-for-Vision.svg diff --git a/assets/icons/aws/Machine-Learning/Monitron.png b/core/assets/icons/aws/Machine-Learning/Monitron.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Monitron.png rename to core/assets/icons/aws/Machine-Learning/Monitron.png diff --git a/assets/icons/aws/Machine-Learning/Monitron.svg b/core/assets/icons/aws/Machine-Learning/Monitron.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Monitron.svg rename to core/assets/icons/aws/Machine-Learning/Monitron.svg diff --git a/assets/icons/aws/Machine-Learning/Neuron.png b/core/assets/icons/aws/Machine-Learning/Neuron.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Neuron.png rename to core/assets/icons/aws/Machine-Learning/Neuron.png diff --git a/assets/icons/aws/Machine-Learning/Neuron.svg b/core/assets/icons/aws/Machine-Learning/Neuron.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Neuron.svg rename to core/assets/icons/aws/Machine-Learning/Neuron.svg diff --git a/assets/icons/aws/Machine-Learning/Nova.png b/core/assets/icons/aws/Machine-Learning/Nova.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Nova.png rename to core/assets/icons/aws/Machine-Learning/Nova.png diff --git a/assets/icons/aws/Machine-Learning/Nova.svg b/core/assets/icons/aws/Machine-Learning/Nova.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Nova.svg rename to core/assets/icons/aws/Machine-Learning/Nova.svg diff --git a/assets/icons/aws/Machine-Learning/Omics.png b/core/assets/icons/aws/Machine-Learning/Omics.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Omics.png rename to core/assets/icons/aws/Machine-Learning/Omics.png diff --git a/assets/icons/aws/Machine-Learning/Omics.svg b/core/assets/icons/aws/Machine-Learning/Omics.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Omics.svg rename to core/assets/icons/aws/Machine-Learning/Omics.svg diff --git a/assets/icons/aws/Machine-Learning/Panorama.png b/core/assets/icons/aws/Machine-Learning/Panorama.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Panorama.png rename to core/assets/icons/aws/Machine-Learning/Panorama.png diff --git a/assets/icons/aws/Machine-Learning/Panorama.svg b/core/assets/icons/aws/Machine-Learning/Panorama.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Panorama.svg rename to core/assets/icons/aws/Machine-Learning/Panorama.svg diff --git a/assets/icons/aws/Machine-Learning/Personalize.png b/core/assets/icons/aws/Machine-Learning/Personalize.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Personalize.png rename to core/assets/icons/aws/Machine-Learning/Personalize.png diff --git a/assets/icons/aws/Machine-Learning/Personalize.svg b/core/assets/icons/aws/Machine-Learning/Personalize.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Personalize.svg rename to core/assets/icons/aws/Machine-Learning/Personalize.svg diff --git a/assets/icons/aws/Machine-Learning/Polly.png b/core/assets/icons/aws/Machine-Learning/Polly.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Polly.png rename to core/assets/icons/aws/Machine-Learning/Polly.png diff --git a/assets/icons/aws/Machine-Learning/Polly.svg b/core/assets/icons/aws/Machine-Learning/Polly.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Polly.svg rename to core/assets/icons/aws/Machine-Learning/Polly.svg diff --git a/assets/icons/aws/Machine-Learning/PyTorch-on-AWS.png b/core/assets/icons/aws/Machine-Learning/PyTorch-on-AWS.png similarity index 100% rename from assets/icons/aws/Machine-Learning/PyTorch-on-AWS.png rename to core/assets/icons/aws/Machine-Learning/PyTorch-on-AWS.png diff --git a/assets/icons/aws/Machine-Learning/PyTorch-on-AWS.svg b/core/assets/icons/aws/Machine-Learning/PyTorch-on-AWS.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/PyTorch-on-AWS.svg rename to core/assets/icons/aws/Machine-Learning/PyTorch-on-AWS.svg diff --git a/assets/icons/aws/Machine-Learning/Q.png b/core/assets/icons/aws/Machine-Learning/Q.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Q.png rename to core/assets/icons/aws/Machine-Learning/Q.png diff --git a/assets/icons/aws/Machine-Learning/Q.svg b/core/assets/icons/aws/Machine-Learning/Q.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Q.svg rename to core/assets/icons/aws/Machine-Learning/Q.svg diff --git a/assets/icons/aws/Machine-Learning/Rekognition.png b/core/assets/icons/aws/Machine-Learning/Rekognition.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Rekognition.png rename to core/assets/icons/aws/Machine-Learning/Rekognition.png diff --git a/assets/icons/aws/Machine-Learning/Rekognition.svg b/core/assets/icons/aws/Machine-Learning/Rekognition.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Rekognition.svg rename to core/assets/icons/aws/Machine-Learning/Rekognition.svg diff --git a/assets/icons/aws/Machine-Learning/SageMaker-AI.png b/core/assets/icons/aws/Machine-Learning/SageMaker-AI.png similarity index 100% rename from assets/icons/aws/Machine-Learning/SageMaker-AI.png rename to core/assets/icons/aws/Machine-Learning/SageMaker-AI.png diff --git a/assets/icons/aws/Machine-Learning/SageMaker-AI.svg b/core/assets/icons/aws/Machine-Learning/SageMaker-AI.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/SageMaker-AI.svg rename to core/assets/icons/aws/Machine-Learning/SageMaker-AI.svg diff --git a/assets/icons/aws/Machine-Learning/SageMaker-Ground-Truth.png b/core/assets/icons/aws/Machine-Learning/SageMaker-Ground-Truth.png similarity index 100% rename from assets/icons/aws/Machine-Learning/SageMaker-Ground-Truth.png rename to core/assets/icons/aws/Machine-Learning/SageMaker-Ground-Truth.png diff --git a/assets/icons/aws/Machine-Learning/SageMaker-Ground-Truth.svg b/core/assets/icons/aws/Machine-Learning/SageMaker-Ground-Truth.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/SageMaker-Ground-Truth.svg rename to core/assets/icons/aws/Machine-Learning/SageMaker-Ground-Truth.svg diff --git a/assets/icons/aws/Machine-Learning/SageMaker-Studio-Lab.png b/core/assets/icons/aws/Machine-Learning/SageMaker-Studio-Lab.png similarity index 100% rename from assets/icons/aws/Machine-Learning/SageMaker-Studio-Lab.png rename to core/assets/icons/aws/Machine-Learning/SageMaker-Studio-Lab.png diff --git a/assets/icons/aws/Machine-Learning/SageMaker-Studio-Lab.svg b/core/assets/icons/aws/Machine-Learning/SageMaker-Studio-Lab.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/SageMaker-Studio-Lab.svg rename to core/assets/icons/aws/Machine-Learning/SageMaker-Studio-Lab.svg diff --git a/assets/icons/aws/Machine-Learning/SageMaker.png b/core/assets/icons/aws/Machine-Learning/SageMaker.png similarity index 100% rename from assets/icons/aws/Machine-Learning/SageMaker.png rename to core/assets/icons/aws/Machine-Learning/SageMaker.png diff --git a/assets/icons/aws/Machine-Learning/SageMaker.svg b/core/assets/icons/aws/Machine-Learning/SageMaker.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/SageMaker.svg rename to core/assets/icons/aws/Machine-Learning/SageMaker.svg diff --git a/assets/icons/aws/Machine-Learning/TensorFlow-on-AWS.png b/core/assets/icons/aws/Machine-Learning/TensorFlow-on-AWS.png similarity index 100% rename from assets/icons/aws/Machine-Learning/TensorFlow-on-AWS.png rename to core/assets/icons/aws/Machine-Learning/TensorFlow-on-AWS.png diff --git a/assets/icons/aws/Machine-Learning/TensorFlow-on-AWS.svg b/core/assets/icons/aws/Machine-Learning/TensorFlow-on-AWS.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/TensorFlow-on-AWS.svg rename to core/assets/icons/aws/Machine-Learning/TensorFlow-on-AWS.svg diff --git a/assets/icons/aws/Machine-Learning/Textract.png b/core/assets/icons/aws/Machine-Learning/Textract.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Textract.png rename to core/assets/icons/aws/Machine-Learning/Textract.png diff --git a/assets/icons/aws/Machine-Learning/Textract.svg b/core/assets/icons/aws/Machine-Learning/Textract.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Textract.svg rename to core/assets/icons/aws/Machine-Learning/Textract.svg diff --git a/assets/icons/aws/Machine-Learning/TorchServe.png b/core/assets/icons/aws/Machine-Learning/TorchServe.png similarity index 100% rename from assets/icons/aws/Machine-Learning/TorchServe.png rename to core/assets/icons/aws/Machine-Learning/TorchServe.png diff --git a/assets/icons/aws/Machine-Learning/TorchServe.svg b/core/assets/icons/aws/Machine-Learning/TorchServe.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/TorchServe.svg rename to core/assets/icons/aws/Machine-Learning/TorchServe.svg diff --git a/assets/icons/aws/Machine-Learning/Transcribe.png b/core/assets/icons/aws/Machine-Learning/Transcribe.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Transcribe.png rename to core/assets/icons/aws/Machine-Learning/Transcribe.png diff --git a/assets/icons/aws/Machine-Learning/Transcribe.svg b/core/assets/icons/aws/Machine-Learning/Transcribe.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Transcribe.svg rename to core/assets/icons/aws/Machine-Learning/Transcribe.svg diff --git a/assets/icons/aws/Machine-Learning/Translate.png b/core/assets/icons/aws/Machine-Learning/Translate.png similarity index 100% rename from assets/icons/aws/Machine-Learning/Translate.png rename to core/assets/icons/aws/Machine-Learning/Translate.png diff --git a/assets/icons/aws/Machine-Learning/Translate.svg b/core/assets/icons/aws/Machine-Learning/Translate.svg similarity index 100% rename from assets/icons/aws/Machine-Learning/Translate.svg rename to core/assets/icons/aws/Machine-Learning/Translate.svg diff --git a/assets/icons/aws/Management-Governance/AppConfig.png b/core/assets/icons/aws/Management-Governance/AppConfig.png similarity index 100% rename from assets/icons/aws/Management-Governance/AppConfig.png rename to core/assets/icons/aws/Management-Governance/AppConfig.png diff --git a/assets/icons/aws/Management-Governance/AppConfig.svg b/core/assets/icons/aws/Management-Governance/AppConfig.svg similarity index 100% rename from assets/icons/aws/Management-Governance/AppConfig.svg rename to core/assets/icons/aws/Management-Governance/AppConfig.svg diff --git a/assets/icons/aws/Management-Governance/Application-Auto-Scaling.png b/core/assets/icons/aws/Management-Governance/Application-Auto-Scaling.png similarity index 100% rename from assets/icons/aws/Management-Governance/Application-Auto-Scaling.png rename to core/assets/icons/aws/Management-Governance/Application-Auto-Scaling.png diff --git a/assets/icons/aws/Management-Governance/Application-Auto-Scaling.svg b/core/assets/icons/aws/Management-Governance/Application-Auto-Scaling.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Application-Auto-Scaling.svg rename to core/assets/icons/aws/Management-Governance/Application-Auto-Scaling.svg diff --git a/assets/icons/aws/Management-Governance/Auto-Scaling.png b/core/assets/icons/aws/Management-Governance/Auto-Scaling.png similarity index 100% rename from assets/icons/aws/Management-Governance/Auto-Scaling.png rename to core/assets/icons/aws/Management-Governance/Auto-Scaling.png diff --git a/assets/icons/aws/Management-Governance/Auto-Scaling.svg b/core/assets/icons/aws/Management-Governance/Auto-Scaling.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Auto-Scaling.svg rename to core/assets/icons/aws/Management-Governance/Auto-Scaling.svg diff --git a/assets/icons/aws/Management-Governance/Backint-Agent.png b/core/assets/icons/aws/Management-Governance/Backint-Agent.png similarity index 100% rename from assets/icons/aws/Management-Governance/Backint-Agent.png rename to core/assets/icons/aws/Management-Governance/Backint-Agent.png diff --git a/assets/icons/aws/Management-Governance/Backint-Agent.svg b/core/assets/icons/aws/Management-Governance/Backint-Agent.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Backint-Agent.svg rename to core/assets/icons/aws/Management-Governance/Backint-Agent.svg diff --git a/assets/icons/aws/Management-Governance/Chatbot.png b/core/assets/icons/aws/Management-Governance/Chatbot.png similarity index 100% rename from assets/icons/aws/Management-Governance/Chatbot.png rename to core/assets/icons/aws/Management-Governance/Chatbot.png diff --git a/assets/icons/aws/Management-Governance/Chatbot.svg b/core/assets/icons/aws/Management-Governance/Chatbot.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Chatbot.svg rename to core/assets/icons/aws/Management-Governance/Chatbot.svg diff --git a/assets/icons/aws/Management-Governance/CloudFormation.png b/core/assets/icons/aws/Management-Governance/CloudFormation.png similarity index 100% rename from assets/icons/aws/Management-Governance/CloudFormation.png rename to core/assets/icons/aws/Management-Governance/CloudFormation.png diff --git a/assets/icons/aws/Management-Governance/CloudFormation.svg b/core/assets/icons/aws/Management-Governance/CloudFormation.svg similarity index 100% rename from assets/icons/aws/Management-Governance/CloudFormation.svg rename to core/assets/icons/aws/Management-Governance/CloudFormation.svg diff --git a/assets/icons/aws/Management-Governance/CloudTrail.png b/core/assets/icons/aws/Management-Governance/CloudTrail.png similarity index 100% rename from assets/icons/aws/Management-Governance/CloudTrail.png rename to core/assets/icons/aws/Management-Governance/CloudTrail.png diff --git a/assets/icons/aws/Management-Governance/CloudTrail.svg b/core/assets/icons/aws/Management-Governance/CloudTrail.svg similarity index 100% rename from assets/icons/aws/Management-Governance/CloudTrail.svg rename to core/assets/icons/aws/Management-Governance/CloudTrail.svg diff --git a/assets/icons/aws/Management-Governance/CloudWatch.png b/core/assets/icons/aws/Management-Governance/CloudWatch.png similarity index 100% rename from assets/icons/aws/Management-Governance/CloudWatch.png rename to core/assets/icons/aws/Management-Governance/CloudWatch.png diff --git a/assets/icons/aws/Management-Governance/CloudWatch.svg b/core/assets/icons/aws/Management-Governance/CloudWatch.svg similarity index 100% rename from assets/icons/aws/Management-Governance/CloudWatch.svg rename to core/assets/icons/aws/Management-Governance/CloudWatch.svg diff --git a/assets/icons/aws/Management-Governance/Config.png b/core/assets/icons/aws/Management-Governance/Config.png similarity index 100% rename from assets/icons/aws/Management-Governance/Config.png rename to core/assets/icons/aws/Management-Governance/Config.png diff --git a/assets/icons/aws/Management-Governance/Config.svg b/core/assets/icons/aws/Management-Governance/Config.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Config.svg rename to core/assets/icons/aws/Management-Governance/Config.svg diff --git a/assets/icons/aws/Management-Governance/Control-Tower.png b/core/assets/icons/aws/Management-Governance/Control-Tower.png similarity index 100% rename from assets/icons/aws/Management-Governance/Control-Tower.png rename to core/assets/icons/aws/Management-Governance/Control-Tower.png diff --git a/assets/icons/aws/Management-Governance/Control-Tower.svg b/core/assets/icons/aws/Management-Governance/Control-Tower.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Control-Tower.svg rename to core/assets/icons/aws/Management-Governance/Control-Tower.svg diff --git a/assets/icons/aws/Management-Governance/DevOps-Agent.png b/core/assets/icons/aws/Management-Governance/DevOps-Agent.png similarity index 100% rename from assets/icons/aws/Management-Governance/DevOps-Agent.png rename to core/assets/icons/aws/Management-Governance/DevOps-Agent.png diff --git a/assets/icons/aws/Management-Governance/DevOps-Agent.svg b/core/assets/icons/aws/Management-Governance/DevOps-Agent.svg similarity index 100% rename from assets/icons/aws/Management-Governance/DevOps-Agent.svg rename to core/assets/icons/aws/Management-Governance/DevOps-Agent.svg diff --git a/assets/icons/aws/Management-Governance/Distro-for-OpenTelemetry.png b/core/assets/icons/aws/Management-Governance/Distro-for-OpenTelemetry.png similarity index 100% rename from assets/icons/aws/Management-Governance/Distro-for-OpenTelemetry.png rename to core/assets/icons/aws/Management-Governance/Distro-for-OpenTelemetry.png diff --git a/assets/icons/aws/Management-Governance/Distro-for-OpenTelemetry.svg b/core/assets/icons/aws/Management-Governance/Distro-for-OpenTelemetry.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Distro-for-OpenTelemetry.svg rename to core/assets/icons/aws/Management-Governance/Distro-for-OpenTelemetry.svg diff --git a/assets/icons/aws/Management-Governance/Fault-Injection-Simulator.png b/core/assets/icons/aws/Management-Governance/Fault-Injection-Simulator.png similarity index 100% rename from assets/icons/aws/Management-Governance/Fault-Injection-Simulator.png rename to core/assets/icons/aws/Management-Governance/Fault-Injection-Simulator.png diff --git a/assets/icons/aws/Management-Governance/Fault-Injection-Simulator.svg b/core/assets/icons/aws/Management-Governance/Fault-Injection-Simulator.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Fault-Injection-Simulator.svg rename to core/assets/icons/aws/Management-Governance/Fault-Injection-Simulator.svg diff --git a/assets/icons/aws/Management-Governance/Health-Dashboard.png b/core/assets/icons/aws/Management-Governance/Health-Dashboard.png similarity index 100% rename from assets/icons/aws/Management-Governance/Health-Dashboard.png rename to core/assets/icons/aws/Management-Governance/Health-Dashboard.png diff --git a/assets/icons/aws/Management-Governance/Health-Dashboard.svg b/core/assets/icons/aws/Management-Governance/Health-Dashboard.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Health-Dashboard.svg rename to core/assets/icons/aws/Management-Governance/Health-Dashboard.svg diff --git a/assets/icons/aws/Management-Governance/Launch-Wizard.png b/core/assets/icons/aws/Management-Governance/Launch-Wizard.png similarity index 100% rename from assets/icons/aws/Management-Governance/Launch-Wizard.png rename to core/assets/icons/aws/Management-Governance/Launch-Wizard.png diff --git a/assets/icons/aws/Management-Governance/Launch-Wizard.svg b/core/assets/icons/aws/Management-Governance/Launch-Wizard.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Launch-Wizard.svg rename to core/assets/icons/aws/Management-Governance/Launch-Wizard.svg diff --git a/assets/icons/aws/Management-Governance/License-Manager.png b/core/assets/icons/aws/Management-Governance/License-Manager.png similarity index 100% rename from assets/icons/aws/Management-Governance/License-Manager.png rename to core/assets/icons/aws/Management-Governance/License-Manager.png diff --git a/assets/icons/aws/Management-Governance/License-Manager.svg b/core/assets/icons/aws/Management-Governance/License-Manager.svg similarity index 100% rename from assets/icons/aws/Management-Governance/License-Manager.svg rename to core/assets/icons/aws/Management-Governance/License-Manager.svg diff --git a/assets/icons/aws/Management-Governance/Managed-Grafana.png b/core/assets/icons/aws/Management-Governance/Managed-Grafana.png similarity index 100% rename from assets/icons/aws/Management-Governance/Managed-Grafana.png rename to core/assets/icons/aws/Management-Governance/Managed-Grafana.png diff --git a/assets/icons/aws/Management-Governance/Managed-Grafana.svg b/core/assets/icons/aws/Management-Governance/Managed-Grafana.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Managed-Grafana.svg rename to core/assets/icons/aws/Management-Governance/Managed-Grafana.svg diff --git a/assets/icons/aws/Management-Governance/Managed-Service-for-Prometheus.png b/core/assets/icons/aws/Management-Governance/Managed-Service-for-Prometheus.png similarity index 100% rename from assets/icons/aws/Management-Governance/Managed-Service-for-Prometheus.png rename to core/assets/icons/aws/Management-Governance/Managed-Service-for-Prometheus.png diff --git a/assets/icons/aws/Management-Governance/Managed-Service-for-Prometheus.svg b/core/assets/icons/aws/Management-Governance/Managed-Service-for-Prometheus.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Managed-Service-for-Prometheus.svg rename to core/assets/icons/aws/Management-Governance/Managed-Service-for-Prometheus.svg diff --git a/assets/icons/aws/Management-Governance/Management-Console.png b/core/assets/icons/aws/Management-Governance/Management-Console.png similarity index 100% rename from assets/icons/aws/Management-Governance/Management-Console.png rename to core/assets/icons/aws/Management-Governance/Management-Console.png diff --git a/assets/icons/aws/Management-Governance/Management-Console.svg b/core/assets/icons/aws/Management-Governance/Management-Console.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Management-Console.svg rename to core/assets/icons/aws/Management-Governance/Management-Console.svg diff --git a/assets/icons/aws/Management-Governance/OpsWorks.png b/core/assets/icons/aws/Management-Governance/OpsWorks.png similarity index 100% rename from assets/icons/aws/Management-Governance/OpsWorks.png rename to core/assets/icons/aws/Management-Governance/OpsWorks.png diff --git a/assets/icons/aws/Management-Governance/OpsWorks.svg b/core/assets/icons/aws/Management-Governance/OpsWorks.svg similarity index 100% rename from assets/icons/aws/Management-Governance/OpsWorks.svg rename to core/assets/icons/aws/Management-Governance/OpsWorks.svg diff --git a/assets/icons/aws/Management-Governance/Organizations.png b/core/assets/icons/aws/Management-Governance/Organizations.png similarity index 100% rename from assets/icons/aws/Management-Governance/Organizations.png rename to core/assets/icons/aws/Management-Governance/Organizations.png diff --git a/assets/icons/aws/Management-Governance/Organizations.svg b/core/assets/icons/aws/Management-Governance/Organizations.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Organizations.svg rename to core/assets/icons/aws/Management-Governance/Organizations.svg diff --git a/assets/icons/aws/Management-Governance/Partner-Central.png b/core/assets/icons/aws/Management-Governance/Partner-Central.png similarity index 100% rename from assets/icons/aws/Management-Governance/Partner-Central.png rename to core/assets/icons/aws/Management-Governance/Partner-Central.png diff --git a/assets/icons/aws/Management-Governance/Partner-Central.svg b/core/assets/icons/aws/Management-Governance/Partner-Central.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Partner-Central.svg rename to core/assets/icons/aws/Management-Governance/Partner-Central.svg diff --git a/assets/icons/aws/Management-Governance/Personal-Health-Dashboard.png b/core/assets/icons/aws/Management-Governance/Personal-Health-Dashboard.png similarity index 100% rename from assets/icons/aws/Management-Governance/Personal-Health-Dashboard.png rename to core/assets/icons/aws/Management-Governance/Personal-Health-Dashboard.png diff --git a/assets/icons/aws/Management-Governance/Personal-Health-Dashboard.svg b/core/assets/icons/aws/Management-Governance/Personal-Health-Dashboard.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Personal-Health-Dashboard.svg rename to core/assets/icons/aws/Management-Governance/Personal-Health-Dashboard.svg diff --git a/assets/icons/aws/Management-Governance/Proton.png b/core/assets/icons/aws/Management-Governance/Proton.png similarity index 100% rename from assets/icons/aws/Management-Governance/Proton.png rename to core/assets/icons/aws/Management-Governance/Proton.png diff --git a/assets/icons/aws/Management-Governance/Proton.svg b/core/assets/icons/aws/Management-Governance/Proton.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Proton.svg rename to core/assets/icons/aws/Management-Governance/Proton.svg diff --git a/assets/icons/aws/Management-Governance/Resilience-Hub.png b/core/assets/icons/aws/Management-Governance/Resilience-Hub.png similarity index 100% rename from assets/icons/aws/Management-Governance/Resilience-Hub.png rename to core/assets/icons/aws/Management-Governance/Resilience-Hub.png diff --git a/assets/icons/aws/Management-Governance/Resilience-Hub.svg b/core/assets/icons/aws/Management-Governance/Resilience-Hub.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Resilience-Hub.svg rename to core/assets/icons/aws/Management-Governance/Resilience-Hub.svg diff --git a/assets/icons/aws/Management-Governance/Resource-Explorer.png b/core/assets/icons/aws/Management-Governance/Resource-Explorer.png similarity index 100% rename from assets/icons/aws/Management-Governance/Resource-Explorer.png rename to core/assets/icons/aws/Management-Governance/Resource-Explorer.png diff --git a/assets/icons/aws/Management-Governance/Resource-Explorer.svg b/core/assets/icons/aws/Management-Governance/Resource-Explorer.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Resource-Explorer.svg rename to core/assets/icons/aws/Management-Governance/Resource-Explorer.svg diff --git a/assets/icons/aws/Management-Governance/Service-Catalog.png b/core/assets/icons/aws/Management-Governance/Service-Catalog.png similarity index 100% rename from assets/icons/aws/Management-Governance/Service-Catalog.png rename to core/assets/icons/aws/Management-Governance/Service-Catalog.png diff --git a/assets/icons/aws/Management-Governance/Service-Catalog.svg b/core/assets/icons/aws/Management-Governance/Service-Catalog.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Service-Catalog.svg rename to core/assets/icons/aws/Management-Governance/Service-Catalog.svg diff --git a/assets/icons/aws/Management-Governance/Service-Management-Connector.png b/core/assets/icons/aws/Management-Governance/Service-Management-Connector.png similarity index 100% rename from assets/icons/aws/Management-Governance/Service-Management-Connector.png rename to core/assets/icons/aws/Management-Governance/Service-Management-Connector.png diff --git a/assets/icons/aws/Management-Governance/Service-Management-Connector.svg b/core/assets/icons/aws/Management-Governance/Service-Management-Connector.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Service-Management-Connector.svg rename to core/assets/icons/aws/Management-Governance/Service-Management-Connector.svg diff --git a/assets/icons/aws/Management-Governance/Sustainability.png b/core/assets/icons/aws/Management-Governance/Sustainability.png similarity index 100% rename from assets/icons/aws/Management-Governance/Sustainability.png rename to core/assets/icons/aws/Management-Governance/Sustainability.png diff --git a/assets/icons/aws/Management-Governance/Sustainability.svg b/core/assets/icons/aws/Management-Governance/Sustainability.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Sustainability.svg rename to core/assets/icons/aws/Management-Governance/Sustainability.svg diff --git a/assets/icons/aws/Management-Governance/Systems-Manager.png b/core/assets/icons/aws/Management-Governance/Systems-Manager.png similarity index 100% rename from assets/icons/aws/Management-Governance/Systems-Manager.png rename to core/assets/icons/aws/Management-Governance/Systems-Manager.png diff --git a/assets/icons/aws/Management-Governance/Systems-Manager.svg b/core/assets/icons/aws/Management-Governance/Systems-Manager.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Systems-Manager.svg rename to core/assets/icons/aws/Management-Governance/Systems-Manager.svg diff --git a/assets/icons/aws/Management-Governance/Telco-Network-Builder.png b/core/assets/icons/aws/Management-Governance/Telco-Network-Builder.png similarity index 100% rename from assets/icons/aws/Management-Governance/Telco-Network-Builder.png rename to core/assets/icons/aws/Management-Governance/Telco-Network-Builder.png diff --git a/assets/icons/aws/Management-Governance/Telco-Network-Builder.svg b/core/assets/icons/aws/Management-Governance/Telco-Network-Builder.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Telco-Network-Builder.svg rename to core/assets/icons/aws/Management-Governance/Telco-Network-Builder.svg diff --git a/assets/icons/aws/Management-Governance/Trusted-Advisor.png b/core/assets/icons/aws/Management-Governance/Trusted-Advisor.png similarity index 100% rename from assets/icons/aws/Management-Governance/Trusted-Advisor.png rename to core/assets/icons/aws/Management-Governance/Trusted-Advisor.png diff --git a/assets/icons/aws/Management-Governance/Trusted-Advisor.svg b/core/assets/icons/aws/Management-Governance/Trusted-Advisor.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Trusted-Advisor.svg rename to core/assets/icons/aws/Management-Governance/Trusted-Advisor.svg diff --git a/assets/icons/aws/Management-Governance/User-Notifications.png b/core/assets/icons/aws/Management-Governance/User-Notifications.png similarity index 100% rename from assets/icons/aws/Management-Governance/User-Notifications.png rename to core/assets/icons/aws/Management-Governance/User-Notifications.png diff --git a/assets/icons/aws/Management-Governance/User-Notifications.svg b/core/assets/icons/aws/Management-Governance/User-Notifications.svg similarity index 100% rename from assets/icons/aws/Management-Governance/User-Notifications.svg rename to core/assets/icons/aws/Management-Governance/User-Notifications.svg diff --git a/assets/icons/aws/Management-Governance/Well-Architected-Tool.png b/core/assets/icons/aws/Management-Governance/Well-Architected-Tool.png similarity index 100% rename from assets/icons/aws/Management-Governance/Well-Architected-Tool.png rename to core/assets/icons/aws/Management-Governance/Well-Architected-Tool.png diff --git a/assets/icons/aws/Management-Governance/Well-Architected-Tool.svg b/core/assets/icons/aws/Management-Governance/Well-Architected-Tool.svg similarity index 100% rename from assets/icons/aws/Management-Governance/Well-Architected-Tool.svg rename to core/assets/icons/aws/Management-Governance/Well-Architected-Tool.svg diff --git a/assets/icons/aws/Media-Services/Deadline-Cloud.png b/core/assets/icons/aws/Media-Services/Deadline-Cloud.png similarity index 100% rename from assets/icons/aws/Media-Services/Deadline-Cloud.png rename to core/assets/icons/aws/Media-Services/Deadline-Cloud.png diff --git a/assets/icons/aws/Media-Services/Deadline-Cloud.svg b/core/assets/icons/aws/Media-Services/Deadline-Cloud.svg similarity index 100% rename from assets/icons/aws/Media-Services/Deadline-Cloud.svg rename to core/assets/icons/aws/Media-Services/Deadline-Cloud.svg diff --git a/assets/icons/aws/Media-Services/Elastic-Transcoder.png b/core/assets/icons/aws/Media-Services/Elastic-Transcoder.png similarity index 100% rename from assets/icons/aws/Media-Services/Elastic-Transcoder.png rename to core/assets/icons/aws/Media-Services/Elastic-Transcoder.png diff --git a/assets/icons/aws/Media-Services/Elastic-Transcoder.svg b/core/assets/icons/aws/Media-Services/Elastic-Transcoder.svg similarity index 100% rename from assets/icons/aws/Media-Services/Elastic-Transcoder.svg rename to core/assets/icons/aws/Media-Services/Elastic-Transcoder.svg diff --git a/assets/icons/aws/Media-Services/Elemental-Appliances-&-Software.png b/core/assets/icons/aws/Media-Services/Elemental-Appliances-&-Software.png similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-Appliances-&-Software.png rename to core/assets/icons/aws/Media-Services/Elemental-Appliances-&-Software.png diff --git a/assets/icons/aws/Media-Services/Elemental-Appliances-&-Software.svg b/core/assets/icons/aws/Media-Services/Elemental-Appliances-&-Software.svg similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-Appliances-&-Software.svg rename to core/assets/icons/aws/Media-Services/Elemental-Appliances-&-Software.svg diff --git a/assets/icons/aws/Media-Services/Elemental-Conductor.png b/core/assets/icons/aws/Media-Services/Elemental-Conductor.png similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-Conductor.png rename to core/assets/icons/aws/Media-Services/Elemental-Conductor.png diff --git a/assets/icons/aws/Media-Services/Elemental-Conductor.svg b/core/assets/icons/aws/Media-Services/Elemental-Conductor.svg similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-Conductor.svg rename to core/assets/icons/aws/Media-Services/Elemental-Conductor.svg diff --git a/assets/icons/aws/Media-Services/Elemental-Delta.png b/core/assets/icons/aws/Media-Services/Elemental-Delta.png similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-Delta.png rename to core/assets/icons/aws/Media-Services/Elemental-Delta.png diff --git a/assets/icons/aws/Media-Services/Elemental-Delta.svg b/core/assets/icons/aws/Media-Services/Elemental-Delta.svg similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-Delta.svg rename to core/assets/icons/aws/Media-Services/Elemental-Delta.svg diff --git a/assets/icons/aws/Media-Services/Elemental-Link.png b/core/assets/icons/aws/Media-Services/Elemental-Link.png similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-Link.png rename to core/assets/icons/aws/Media-Services/Elemental-Link.png diff --git a/assets/icons/aws/Media-Services/Elemental-Link.svg b/core/assets/icons/aws/Media-Services/Elemental-Link.svg similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-Link.svg rename to core/assets/icons/aws/Media-Services/Elemental-Link.svg diff --git a/assets/icons/aws/Media-Services/Elemental-Live.png b/core/assets/icons/aws/Media-Services/Elemental-Live.png similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-Live.png rename to core/assets/icons/aws/Media-Services/Elemental-Live.png diff --git a/assets/icons/aws/Media-Services/Elemental-Live.svg b/core/assets/icons/aws/Media-Services/Elemental-Live.svg similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-Live.svg rename to core/assets/icons/aws/Media-Services/Elemental-Live.svg diff --git a/assets/icons/aws/Media-Services/Elemental-MediaConnect.png b/core/assets/icons/aws/Media-Services/Elemental-MediaConnect.png similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-MediaConnect.png rename to core/assets/icons/aws/Media-Services/Elemental-MediaConnect.png diff --git a/assets/icons/aws/Media-Services/Elemental-MediaConnect.svg b/core/assets/icons/aws/Media-Services/Elemental-MediaConnect.svg similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-MediaConnect.svg rename to core/assets/icons/aws/Media-Services/Elemental-MediaConnect.svg diff --git a/assets/icons/aws/Media-Services/Elemental-MediaConvert.png b/core/assets/icons/aws/Media-Services/Elemental-MediaConvert.png similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-MediaConvert.png rename to core/assets/icons/aws/Media-Services/Elemental-MediaConvert.png diff --git a/assets/icons/aws/Media-Services/Elemental-MediaConvert.svg b/core/assets/icons/aws/Media-Services/Elemental-MediaConvert.svg similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-MediaConvert.svg rename to core/assets/icons/aws/Media-Services/Elemental-MediaConvert.svg diff --git a/assets/icons/aws/Media-Services/Elemental-MediaLive.png b/core/assets/icons/aws/Media-Services/Elemental-MediaLive.png similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-MediaLive.png rename to core/assets/icons/aws/Media-Services/Elemental-MediaLive.png diff --git a/assets/icons/aws/Media-Services/Elemental-MediaLive.svg b/core/assets/icons/aws/Media-Services/Elemental-MediaLive.svg similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-MediaLive.svg rename to core/assets/icons/aws/Media-Services/Elemental-MediaLive.svg diff --git a/assets/icons/aws/Media-Services/Elemental-MediaPackage.png b/core/assets/icons/aws/Media-Services/Elemental-MediaPackage.png similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-MediaPackage.png rename to core/assets/icons/aws/Media-Services/Elemental-MediaPackage.png diff --git a/assets/icons/aws/Media-Services/Elemental-MediaPackage.svg b/core/assets/icons/aws/Media-Services/Elemental-MediaPackage.svg similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-MediaPackage.svg rename to core/assets/icons/aws/Media-Services/Elemental-MediaPackage.svg diff --git a/assets/icons/aws/Media-Services/Elemental-MediaStore.png b/core/assets/icons/aws/Media-Services/Elemental-MediaStore.png similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-MediaStore.png rename to core/assets/icons/aws/Media-Services/Elemental-MediaStore.png diff --git a/assets/icons/aws/Media-Services/Elemental-MediaStore.svg b/core/assets/icons/aws/Media-Services/Elemental-MediaStore.svg similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-MediaStore.svg rename to core/assets/icons/aws/Media-Services/Elemental-MediaStore.svg diff --git a/assets/icons/aws/Media-Services/Elemental-MediaTailor.png b/core/assets/icons/aws/Media-Services/Elemental-MediaTailor.png similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-MediaTailor.png rename to core/assets/icons/aws/Media-Services/Elemental-MediaTailor.png diff --git a/assets/icons/aws/Media-Services/Elemental-MediaTailor.svg b/core/assets/icons/aws/Media-Services/Elemental-MediaTailor.svg similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-MediaTailor.svg rename to core/assets/icons/aws/Media-Services/Elemental-MediaTailor.svg diff --git a/assets/icons/aws/Media-Services/Elemental-Server.png b/core/assets/icons/aws/Media-Services/Elemental-Server.png similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-Server.png rename to core/assets/icons/aws/Media-Services/Elemental-Server.png diff --git a/assets/icons/aws/Media-Services/Elemental-Server.svg b/core/assets/icons/aws/Media-Services/Elemental-Server.svg similarity index 100% rename from assets/icons/aws/Media-Services/Elemental-Server.svg rename to core/assets/icons/aws/Media-Services/Elemental-Server.svg diff --git a/assets/icons/aws/Media-Services/Interactive-Video-Service.png b/core/assets/icons/aws/Media-Services/Interactive-Video-Service.png similarity index 100% rename from assets/icons/aws/Media-Services/Interactive-Video-Service.png rename to core/assets/icons/aws/Media-Services/Interactive-Video-Service.png diff --git a/assets/icons/aws/Media-Services/Interactive-Video-Service.svg b/core/assets/icons/aws/Media-Services/Interactive-Video-Service.svg similarity index 100% rename from assets/icons/aws/Media-Services/Interactive-Video-Service.svg rename to core/assets/icons/aws/Media-Services/Interactive-Video-Service.svg diff --git a/assets/icons/aws/Media-Services/Kinesis-Video-Streams.png b/core/assets/icons/aws/Media-Services/Kinesis-Video-Streams.png similarity index 100% rename from assets/icons/aws/Media-Services/Kinesis-Video-Streams.png rename to core/assets/icons/aws/Media-Services/Kinesis-Video-Streams.png diff --git a/assets/icons/aws/Media-Services/Kinesis-Video-Streams.svg b/core/assets/icons/aws/Media-Services/Kinesis-Video-Streams.svg similarity index 100% rename from assets/icons/aws/Media-Services/Kinesis-Video-Streams.svg rename to core/assets/icons/aws/Media-Services/Kinesis-Video-Streams.svg diff --git a/assets/icons/aws/Media-Services/Nimble-Studio.png b/core/assets/icons/aws/Media-Services/Nimble-Studio.png similarity index 100% rename from assets/icons/aws/Media-Services/Nimble-Studio.png rename to core/assets/icons/aws/Media-Services/Nimble-Studio.png diff --git a/assets/icons/aws/Media-Services/Nimble-Studio.svg b/core/assets/icons/aws/Media-Services/Nimble-Studio.svg similarity index 100% rename from assets/icons/aws/Media-Services/Nimble-Studio.svg rename to core/assets/icons/aws/Media-Services/Nimble-Studio.svg diff --git a/assets/icons/aws/Migration-Transfer/Application-Discovery-Service.png b/core/assets/icons/aws/Migration-Transfer/Application-Discovery-Service.png similarity index 100% rename from assets/icons/aws/Migration-Transfer/Application-Discovery-Service.png rename to core/assets/icons/aws/Migration-Transfer/Application-Discovery-Service.png diff --git a/assets/icons/aws/Migration-Transfer/Application-Discovery-Service.svg b/core/assets/icons/aws/Migration-Transfer/Application-Discovery-Service.svg similarity index 100% rename from assets/icons/aws/Migration-Transfer/Application-Discovery-Service.svg rename to core/assets/icons/aws/Migration-Transfer/Application-Discovery-Service.svg diff --git a/assets/icons/aws/Migration-Transfer/Application-Migration-Service.png b/core/assets/icons/aws/Migration-Transfer/Application-Migration-Service.png similarity index 100% rename from assets/icons/aws/Migration-Transfer/Application-Migration-Service.png rename to core/assets/icons/aws/Migration-Transfer/Application-Migration-Service.png diff --git a/assets/icons/aws/Migration-Transfer/Application-Migration-Service.svg b/core/assets/icons/aws/Migration-Transfer/Application-Migration-Service.svg similarity index 100% rename from assets/icons/aws/Migration-Transfer/Application-Migration-Service.svg rename to core/assets/icons/aws/Migration-Transfer/Application-Migration-Service.svg diff --git a/assets/icons/aws/Migration-Transfer/Data-Transfer-Terminal.png b/core/assets/icons/aws/Migration-Transfer/Data-Transfer-Terminal.png similarity index 100% rename from assets/icons/aws/Migration-Transfer/Data-Transfer-Terminal.png rename to core/assets/icons/aws/Migration-Transfer/Data-Transfer-Terminal.png diff --git a/assets/icons/aws/Migration-Transfer/Data-Transfer-Terminal.svg b/core/assets/icons/aws/Migration-Transfer/Data-Transfer-Terminal.svg similarity index 100% rename from assets/icons/aws/Migration-Transfer/Data-Transfer-Terminal.svg rename to core/assets/icons/aws/Migration-Transfer/Data-Transfer-Terminal.svg diff --git a/assets/icons/aws/Migration-Transfer/DataSync.png b/core/assets/icons/aws/Migration-Transfer/DataSync.png similarity index 100% rename from assets/icons/aws/Migration-Transfer/DataSync.png rename to core/assets/icons/aws/Migration-Transfer/DataSync.png diff --git a/assets/icons/aws/Migration-Transfer/DataSync.svg b/core/assets/icons/aws/Migration-Transfer/DataSync.svg similarity index 100% rename from assets/icons/aws/Migration-Transfer/DataSync.svg rename to core/assets/icons/aws/Migration-Transfer/DataSync.svg diff --git a/assets/icons/aws/Migration-Transfer/Mainframe-Modernization.png b/core/assets/icons/aws/Migration-Transfer/Mainframe-Modernization.png similarity index 100% rename from assets/icons/aws/Migration-Transfer/Mainframe-Modernization.png rename to core/assets/icons/aws/Migration-Transfer/Mainframe-Modernization.png diff --git a/assets/icons/aws/Migration-Transfer/Mainframe-Modernization.svg b/core/assets/icons/aws/Migration-Transfer/Mainframe-Modernization.svg similarity index 100% rename from assets/icons/aws/Migration-Transfer/Mainframe-Modernization.svg rename to core/assets/icons/aws/Migration-Transfer/Mainframe-Modernization.svg diff --git a/assets/icons/aws/Migration-Transfer/Migration-Evaluator.png b/core/assets/icons/aws/Migration-Transfer/Migration-Evaluator.png similarity index 100% rename from assets/icons/aws/Migration-Transfer/Migration-Evaluator.png rename to core/assets/icons/aws/Migration-Transfer/Migration-Evaluator.png diff --git a/assets/icons/aws/Migration-Transfer/Migration-Evaluator.svg b/core/assets/icons/aws/Migration-Transfer/Migration-Evaluator.svg similarity index 100% rename from assets/icons/aws/Migration-Transfer/Migration-Evaluator.svg rename to core/assets/icons/aws/Migration-Transfer/Migration-Evaluator.svg diff --git a/assets/icons/aws/Migration-Transfer/Migration-Hub.png b/core/assets/icons/aws/Migration-Transfer/Migration-Hub.png similarity index 100% rename from assets/icons/aws/Migration-Transfer/Migration-Hub.png rename to core/assets/icons/aws/Migration-Transfer/Migration-Hub.png diff --git a/assets/icons/aws/Migration-Transfer/Migration-Hub.svg b/core/assets/icons/aws/Migration-Transfer/Migration-Hub.svg similarity index 100% rename from assets/icons/aws/Migration-Transfer/Migration-Hub.svg rename to core/assets/icons/aws/Migration-Transfer/Migration-Hub.svg diff --git a/assets/icons/aws/Migration-Transfer/Server-Migration-Service.png b/core/assets/icons/aws/Migration-Transfer/Server-Migration-Service.png similarity index 100% rename from assets/icons/aws/Migration-Transfer/Server-Migration-Service.png rename to core/assets/icons/aws/Migration-Transfer/Server-Migration-Service.png diff --git a/assets/icons/aws/Migration-Transfer/Server-Migration-Service.svg b/core/assets/icons/aws/Migration-Transfer/Server-Migration-Service.svg similarity index 100% rename from assets/icons/aws/Migration-Transfer/Server-Migration-Service.svg rename to core/assets/icons/aws/Migration-Transfer/Server-Migration-Service.svg diff --git a/assets/icons/aws/Migration-Transfer/Transfer-Family.png b/core/assets/icons/aws/Migration-Transfer/Transfer-Family.png similarity index 100% rename from assets/icons/aws/Migration-Transfer/Transfer-Family.png rename to core/assets/icons/aws/Migration-Transfer/Transfer-Family.png diff --git a/assets/icons/aws/Migration-Transfer/Transfer-Family.svg b/core/assets/icons/aws/Migration-Transfer/Transfer-Family.svg similarity index 100% rename from assets/icons/aws/Migration-Transfer/Transfer-Family.svg rename to core/assets/icons/aws/Migration-Transfer/Transfer-Family.svg diff --git a/assets/icons/aws/Migration-Transfer/Transform.png b/core/assets/icons/aws/Migration-Transfer/Transform.png similarity index 100% rename from assets/icons/aws/Migration-Transfer/Transform.png rename to core/assets/icons/aws/Migration-Transfer/Transform.png diff --git a/assets/icons/aws/Migration-Transfer/Transform.svg b/core/assets/icons/aws/Migration-Transfer/Transform.svg similarity index 100% rename from assets/icons/aws/Migration-Transfer/Transform.svg rename to core/assets/icons/aws/Migration-Transfer/Transform.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/App-Mesh.png b/core/assets/icons/aws/Networking-Content-Delivery/App-Mesh.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/App-Mesh.png rename to core/assets/icons/aws/Networking-Content-Delivery/App-Mesh.png diff --git a/assets/icons/aws/Networking-Content-Delivery/App-Mesh.svg b/core/assets/icons/aws/Networking-Content-Delivery/App-Mesh.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/App-Mesh.svg rename to core/assets/icons/aws/Networking-Content-Delivery/App-Mesh.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/Application-Recovery-Controller.png b/core/assets/icons/aws/Networking-Content-Delivery/Application-Recovery-Controller.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Application-Recovery-Controller.png rename to core/assets/icons/aws/Networking-Content-Delivery/Application-Recovery-Controller.png diff --git a/assets/icons/aws/Networking-Content-Delivery/Application-Recovery-Controller.svg b/core/assets/icons/aws/Networking-Content-Delivery/Application-Recovery-Controller.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Application-Recovery-Controller.svg rename to core/assets/icons/aws/Networking-Content-Delivery/Application-Recovery-Controller.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/Client-VPN.png b/core/assets/icons/aws/Networking-Content-Delivery/Client-VPN.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Client-VPN.png rename to core/assets/icons/aws/Networking-Content-Delivery/Client-VPN.png diff --git a/assets/icons/aws/Networking-Content-Delivery/Client-VPN.svg b/core/assets/icons/aws/Networking-Content-Delivery/Client-VPN.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Client-VPN.svg rename to core/assets/icons/aws/Networking-Content-Delivery/Client-VPN.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/Cloud-Directory.png b/core/assets/icons/aws/Networking-Content-Delivery/Cloud-Directory.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Cloud-Directory.png rename to core/assets/icons/aws/Networking-Content-Delivery/Cloud-Directory.png diff --git a/assets/icons/aws/Networking-Content-Delivery/Cloud-Directory.svg b/core/assets/icons/aws/Networking-Content-Delivery/Cloud-Directory.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Cloud-Directory.svg rename to core/assets/icons/aws/Networking-Content-Delivery/Cloud-Directory.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/Cloud-Map.png b/core/assets/icons/aws/Networking-Content-Delivery/Cloud-Map.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Cloud-Map.png rename to core/assets/icons/aws/Networking-Content-Delivery/Cloud-Map.png diff --git a/assets/icons/aws/Networking-Content-Delivery/Cloud-Map.svg b/core/assets/icons/aws/Networking-Content-Delivery/Cloud-Map.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Cloud-Map.svg rename to core/assets/icons/aws/Networking-Content-Delivery/Cloud-Map.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/Cloud-WAN.png b/core/assets/icons/aws/Networking-Content-Delivery/Cloud-WAN.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Cloud-WAN.png rename to core/assets/icons/aws/Networking-Content-Delivery/Cloud-WAN.png diff --git a/assets/icons/aws/Networking-Content-Delivery/Cloud-WAN.svg b/core/assets/icons/aws/Networking-Content-Delivery/Cloud-WAN.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Cloud-WAN.svg rename to core/assets/icons/aws/Networking-Content-Delivery/Cloud-WAN.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/CloudFront.png b/core/assets/icons/aws/Networking-Content-Delivery/CloudFront.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/CloudFront.png rename to core/assets/icons/aws/Networking-Content-Delivery/CloudFront.png diff --git a/assets/icons/aws/Networking-Content-Delivery/CloudFront.svg b/core/assets/icons/aws/Networking-Content-Delivery/CloudFront.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/CloudFront.svg rename to core/assets/icons/aws/Networking-Content-Delivery/CloudFront.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/Direct-Connect.png b/core/assets/icons/aws/Networking-Content-Delivery/Direct-Connect.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Direct-Connect.png rename to core/assets/icons/aws/Networking-Content-Delivery/Direct-Connect.png diff --git a/assets/icons/aws/Networking-Content-Delivery/Direct-Connect.svg b/core/assets/icons/aws/Networking-Content-Delivery/Direct-Connect.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Direct-Connect.svg rename to core/assets/icons/aws/Networking-Content-Delivery/Direct-Connect.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/Elastic-Load-Balancing.png b/core/assets/icons/aws/Networking-Content-Delivery/Elastic-Load-Balancing.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Elastic-Load-Balancing.png rename to core/assets/icons/aws/Networking-Content-Delivery/Elastic-Load-Balancing.png diff --git a/assets/icons/aws/Networking-Content-Delivery/Elastic-Load-Balancing.svg b/core/assets/icons/aws/Networking-Content-Delivery/Elastic-Load-Balancing.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Elastic-Load-Balancing.svg rename to core/assets/icons/aws/Networking-Content-Delivery/Elastic-Load-Balancing.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/Global-Accelerator.png b/core/assets/icons/aws/Networking-Content-Delivery/Global-Accelerator.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Global-Accelerator.png rename to core/assets/icons/aws/Networking-Content-Delivery/Global-Accelerator.png diff --git a/assets/icons/aws/Networking-Content-Delivery/Global-Accelerator.svg b/core/assets/icons/aws/Networking-Content-Delivery/Global-Accelerator.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Global-Accelerator.svg rename to core/assets/icons/aws/Networking-Content-Delivery/Global-Accelerator.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/Interconnect.png b/core/assets/icons/aws/Networking-Content-Delivery/Interconnect.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Interconnect.png rename to core/assets/icons/aws/Networking-Content-Delivery/Interconnect.png diff --git a/assets/icons/aws/Networking-Content-Delivery/Interconnect.svg b/core/assets/icons/aws/Networking-Content-Delivery/Interconnect.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Interconnect.svg rename to core/assets/icons/aws/Networking-Content-Delivery/Interconnect.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/Private-5G.png b/core/assets/icons/aws/Networking-Content-Delivery/Private-5G.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Private-5G.png rename to core/assets/icons/aws/Networking-Content-Delivery/Private-5G.png diff --git a/assets/icons/aws/Networking-Content-Delivery/Private-5G.svg b/core/assets/icons/aws/Networking-Content-Delivery/Private-5G.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Private-5G.svg rename to core/assets/icons/aws/Networking-Content-Delivery/Private-5G.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/PrivateLink.png b/core/assets/icons/aws/Networking-Content-Delivery/PrivateLink.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/PrivateLink.png rename to core/assets/icons/aws/Networking-Content-Delivery/PrivateLink.png diff --git a/assets/icons/aws/Networking-Content-Delivery/PrivateLink.svg b/core/assets/icons/aws/Networking-Content-Delivery/PrivateLink.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/PrivateLink.svg rename to core/assets/icons/aws/Networking-Content-Delivery/PrivateLink.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/RTB-Fabric.png b/core/assets/icons/aws/Networking-Content-Delivery/RTB-Fabric.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/RTB-Fabric.png rename to core/assets/icons/aws/Networking-Content-Delivery/RTB-Fabric.png diff --git a/assets/icons/aws/Networking-Content-Delivery/RTB-Fabric.svg b/core/assets/icons/aws/Networking-Content-Delivery/RTB-Fabric.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/RTB-Fabric.svg rename to core/assets/icons/aws/Networking-Content-Delivery/RTB-Fabric.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/Route-53.png b/core/assets/icons/aws/Networking-Content-Delivery/Route-53.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Route-53.png rename to core/assets/icons/aws/Networking-Content-Delivery/Route-53.png diff --git a/assets/icons/aws/Networking-Content-Delivery/Route-53.svg b/core/assets/icons/aws/Networking-Content-Delivery/Route-53.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Route-53.svg rename to core/assets/icons/aws/Networking-Content-Delivery/Route-53.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/Site-to-Site-VPN.png b/core/assets/icons/aws/Networking-Content-Delivery/Site-to-Site-VPN.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Site-to-Site-VPN.png rename to core/assets/icons/aws/Networking-Content-Delivery/Site-to-Site-VPN.png diff --git a/assets/icons/aws/Networking-Content-Delivery/Site-to-Site-VPN.svg b/core/assets/icons/aws/Networking-Content-Delivery/Site-to-Site-VPN.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Site-to-Site-VPN.svg rename to core/assets/icons/aws/Networking-Content-Delivery/Site-to-Site-VPN.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/Transit-Gateway.png b/core/assets/icons/aws/Networking-Content-Delivery/Transit-Gateway.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Transit-Gateway.png rename to core/assets/icons/aws/Networking-Content-Delivery/Transit-Gateway.png diff --git a/assets/icons/aws/Networking-Content-Delivery/Transit-Gateway.svg b/core/assets/icons/aws/Networking-Content-Delivery/Transit-Gateway.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Transit-Gateway.svg rename to core/assets/icons/aws/Networking-Content-Delivery/Transit-Gateway.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/VPC-Lattice.png b/core/assets/icons/aws/Networking-Content-Delivery/VPC-Lattice.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/VPC-Lattice.png rename to core/assets/icons/aws/Networking-Content-Delivery/VPC-Lattice.png diff --git a/assets/icons/aws/Networking-Content-Delivery/VPC-Lattice.svg b/core/assets/icons/aws/Networking-Content-Delivery/VPC-Lattice.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/VPC-Lattice.svg rename to core/assets/icons/aws/Networking-Content-Delivery/VPC-Lattice.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/Verified-Access.png b/core/assets/icons/aws/Networking-Content-Delivery/Verified-Access.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Verified-Access.png rename to core/assets/icons/aws/Networking-Content-Delivery/Verified-Access.png diff --git a/assets/icons/aws/Networking-Content-Delivery/Verified-Access.svg b/core/assets/icons/aws/Networking-Content-Delivery/Verified-Access.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Verified-Access.svg rename to core/assets/icons/aws/Networking-Content-Delivery/Verified-Access.svg diff --git a/assets/icons/aws/Networking-Content-Delivery/Virtual-Private-Cloud.png b/core/assets/icons/aws/Networking-Content-Delivery/Virtual-Private-Cloud.png similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Virtual-Private-Cloud.png rename to core/assets/icons/aws/Networking-Content-Delivery/Virtual-Private-Cloud.png diff --git a/assets/icons/aws/Networking-Content-Delivery/Virtual-Private-Cloud.svg b/core/assets/icons/aws/Networking-Content-Delivery/Virtual-Private-Cloud.svg similarity index 100% rename from assets/icons/aws/Networking-Content-Delivery/Virtual-Private-Cloud.svg rename to core/assets/icons/aws/Networking-Content-Delivery/Virtual-Private-Cloud.svg diff --git a/assets/icons/aws/Quantum-Technologies/Braket.png b/core/assets/icons/aws/Quantum-Technologies/Braket.png similarity index 100% rename from assets/icons/aws/Quantum-Technologies/Braket.png rename to core/assets/icons/aws/Quantum-Technologies/Braket.png diff --git a/assets/icons/aws/Quantum-Technologies/Braket.svg b/core/assets/icons/aws/Quantum-Technologies/Braket.svg similarity index 100% rename from assets/icons/aws/Quantum-Technologies/Braket.svg rename to core/assets/icons/aws/Quantum-Technologies/Braket.svg diff --git a/assets/icons/aws/Robotics/RoboMaker.png b/core/assets/icons/aws/Robotics/RoboMaker.png similarity index 100% rename from assets/icons/aws/Robotics/RoboMaker.png rename to core/assets/icons/aws/Robotics/RoboMaker.png diff --git a/assets/icons/aws/Robotics/RoboMaker.svg b/core/assets/icons/aws/Robotics/RoboMaker.svg similarity index 100% rename from assets/icons/aws/Robotics/RoboMaker.svg rename to core/assets/icons/aws/Robotics/RoboMaker.svg diff --git a/assets/icons/aws/Satellite/Ground-Station.png b/core/assets/icons/aws/Satellite/Ground-Station.png similarity index 100% rename from assets/icons/aws/Satellite/Ground-Station.png rename to core/assets/icons/aws/Satellite/Ground-Station.png diff --git a/assets/icons/aws/Satellite/Ground-Station.svg b/core/assets/icons/aws/Satellite/Ground-Station.svg similarity index 100% rename from assets/icons/aws/Satellite/Ground-Station.svg rename to core/assets/icons/aws/Satellite/Ground-Station.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Artifact.png b/core/assets/icons/aws/Security-Identity-Compliance/Artifact.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Artifact.png rename to core/assets/icons/aws/Security-Identity-Compliance/Artifact.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Artifact.svg b/core/assets/icons/aws/Security-Identity-Compliance/Artifact.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Artifact.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Artifact.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Audit-Manager.png b/core/assets/icons/aws/Security-Identity-Compliance/Audit-Manager.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Audit-Manager.png rename to core/assets/icons/aws/Security-Identity-Compliance/Audit-Manager.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Audit-Manager.svg b/core/assets/icons/aws/Security-Identity-Compliance/Audit-Manager.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Audit-Manager.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Audit-Manager.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Certificate-Manager.png b/core/assets/icons/aws/Security-Identity-Compliance/Certificate-Manager.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Certificate-Manager.png rename to core/assets/icons/aws/Security-Identity-Compliance/Certificate-Manager.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Certificate-Manager.svg b/core/assets/icons/aws/Security-Identity-Compliance/Certificate-Manager.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Certificate-Manager.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Certificate-Manager.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Cloud-Directory.png b/core/assets/icons/aws/Security-Identity-Compliance/Cloud-Directory.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Cloud-Directory.png rename to core/assets/icons/aws/Security-Identity-Compliance/Cloud-Directory.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Cloud-Directory.svg b/core/assets/icons/aws/Security-Identity-Compliance/Cloud-Directory.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Cloud-Directory.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Cloud-Directory.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/CloudHSM.png b/core/assets/icons/aws/Security-Identity-Compliance/CloudHSM.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/CloudHSM.png rename to core/assets/icons/aws/Security-Identity-Compliance/CloudHSM.png diff --git a/assets/icons/aws/Security-Identity-Compliance/CloudHSM.svg b/core/assets/icons/aws/Security-Identity-Compliance/CloudHSM.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/CloudHSM.svg rename to core/assets/icons/aws/Security-Identity-Compliance/CloudHSM.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Cognito.png b/core/assets/icons/aws/Security-Identity-Compliance/Cognito.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Cognito.png rename to core/assets/icons/aws/Security-Identity-Compliance/Cognito.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Cognito.svg b/core/assets/icons/aws/Security-Identity-Compliance/Cognito.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Cognito.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Cognito.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Detective.png b/core/assets/icons/aws/Security-Identity-Compliance/Detective.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Detective.png rename to core/assets/icons/aws/Security-Identity-Compliance/Detective.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Detective.svg b/core/assets/icons/aws/Security-Identity-Compliance/Detective.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Detective.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Detective.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Directory-Service.png b/core/assets/icons/aws/Security-Identity-Compliance/Directory-Service.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Directory-Service.png rename to core/assets/icons/aws/Security-Identity-Compliance/Directory-Service.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Directory-Service.svg b/core/assets/icons/aws/Security-Identity-Compliance/Directory-Service.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Directory-Service.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Directory-Service.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Firewall-Manager.png b/core/assets/icons/aws/Security-Identity-Compliance/Firewall-Manager.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Firewall-Manager.png rename to core/assets/icons/aws/Security-Identity-Compliance/Firewall-Manager.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Firewall-Manager.svg b/core/assets/icons/aws/Security-Identity-Compliance/Firewall-Manager.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Firewall-Manager.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Firewall-Manager.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/GuardDuty.png b/core/assets/icons/aws/Security-Identity-Compliance/GuardDuty.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/GuardDuty.png rename to core/assets/icons/aws/Security-Identity-Compliance/GuardDuty.png diff --git a/assets/icons/aws/Security-Identity-Compliance/GuardDuty.svg b/core/assets/icons/aws/Security-Identity-Compliance/GuardDuty.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/GuardDuty.svg rename to core/assets/icons/aws/Security-Identity-Compliance/GuardDuty.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/IAM-Identity-Center.png b/core/assets/icons/aws/Security-Identity-Compliance/IAM-Identity-Center.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/IAM-Identity-Center.png rename to core/assets/icons/aws/Security-Identity-Compliance/IAM-Identity-Center.png diff --git a/assets/icons/aws/Security-Identity-Compliance/IAM-Identity-Center.svg b/core/assets/icons/aws/Security-Identity-Compliance/IAM-Identity-Center.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/IAM-Identity-Center.svg rename to core/assets/icons/aws/Security-Identity-Compliance/IAM-Identity-Center.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Identity-and-Access-Management.png b/core/assets/icons/aws/Security-Identity-Compliance/Identity-and-Access-Management.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Identity-and-Access-Management.png rename to core/assets/icons/aws/Security-Identity-Compliance/Identity-and-Access-Management.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Identity-and-Access-Management.svg b/core/assets/icons/aws/Security-Identity-Compliance/Identity-and-Access-Management.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Identity-and-Access-Management.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Identity-and-Access-Management.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Inspector.png b/core/assets/icons/aws/Security-Identity-Compliance/Inspector.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Inspector.png rename to core/assets/icons/aws/Security-Identity-Compliance/Inspector.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Inspector.svg b/core/assets/icons/aws/Security-Identity-Compliance/Inspector.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Inspector.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Inspector.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Key-Management-Service.png b/core/assets/icons/aws/Security-Identity-Compliance/Key-Management-Service.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Key-Management-Service.png rename to core/assets/icons/aws/Security-Identity-Compliance/Key-Management-Service.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Key-Management-Service.svg b/core/assets/icons/aws/Security-Identity-Compliance/Key-Management-Service.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Key-Management-Service.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Key-Management-Service.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Macie.png b/core/assets/icons/aws/Security-Identity-Compliance/Macie.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Macie.png rename to core/assets/icons/aws/Security-Identity-Compliance/Macie.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Macie.svg b/core/assets/icons/aws/Security-Identity-Compliance/Macie.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Macie.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Macie.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Network-Firewall.png b/core/assets/icons/aws/Security-Identity-Compliance/Network-Firewall.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Network-Firewall.png rename to core/assets/icons/aws/Security-Identity-Compliance/Network-Firewall.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Network-Firewall.svg b/core/assets/icons/aws/Security-Identity-Compliance/Network-Firewall.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Network-Firewall.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Network-Firewall.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Payment-Cryptography.png b/core/assets/icons/aws/Security-Identity-Compliance/Payment-Cryptography.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Payment-Cryptography.png rename to core/assets/icons/aws/Security-Identity-Compliance/Payment-Cryptography.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Payment-Cryptography.svg b/core/assets/icons/aws/Security-Identity-Compliance/Payment-Cryptography.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Payment-Cryptography.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Payment-Cryptography.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Private-Certificate-Authority.png b/core/assets/icons/aws/Security-Identity-Compliance/Private-Certificate-Authority.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Private-Certificate-Authority.png rename to core/assets/icons/aws/Security-Identity-Compliance/Private-Certificate-Authority.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Private-Certificate-Authority.svg b/core/assets/icons/aws/Security-Identity-Compliance/Private-Certificate-Authority.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Private-Certificate-Authority.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Private-Certificate-Authority.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Resource-Access-Manager.png b/core/assets/icons/aws/Security-Identity-Compliance/Resource-Access-Manager.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Resource-Access-Manager.png rename to core/assets/icons/aws/Security-Identity-Compliance/Resource-Access-Manager.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Resource-Access-Manager.svg b/core/assets/icons/aws/Security-Identity-Compliance/Resource-Access-Manager.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Resource-Access-Manager.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Resource-Access-Manager.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Secrets-Manager.png b/core/assets/icons/aws/Security-Identity-Compliance/Secrets-Manager.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Secrets-Manager.png rename to core/assets/icons/aws/Security-Identity-Compliance/Secrets-Manager.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Secrets-Manager.svg b/core/assets/icons/aws/Security-Identity-Compliance/Secrets-Manager.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Secrets-Manager.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Secrets-Manager.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Security-Agent.png b/core/assets/icons/aws/Security-Identity-Compliance/Security-Agent.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Security-Agent.png rename to core/assets/icons/aws/Security-Identity-Compliance/Security-Agent.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Security-Agent.svg b/core/assets/icons/aws/Security-Identity-Compliance/Security-Agent.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Security-Agent.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Security-Agent.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Security-Hub.png b/core/assets/icons/aws/Security-Identity-Compliance/Security-Hub.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Security-Hub.png rename to core/assets/icons/aws/Security-Identity-Compliance/Security-Hub.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Security-Hub.svg b/core/assets/icons/aws/Security-Identity-Compliance/Security-Hub.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Security-Hub.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Security-Hub.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Security-Incident-Response.png b/core/assets/icons/aws/Security-Identity-Compliance/Security-Incident-Response.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Security-Incident-Response.png rename to core/assets/icons/aws/Security-Identity-Compliance/Security-Incident-Response.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Security-Incident-Response.svg b/core/assets/icons/aws/Security-Identity-Compliance/Security-Incident-Response.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Security-Incident-Response.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Security-Incident-Response.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Security-Lake.png b/core/assets/icons/aws/Security-Identity-Compliance/Security-Lake.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Security-Lake.png rename to core/assets/icons/aws/Security-Identity-Compliance/Security-Lake.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Security-Lake.svg b/core/assets/icons/aws/Security-Identity-Compliance/Security-Lake.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Security-Lake.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Security-Lake.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Shield.png b/core/assets/icons/aws/Security-Identity-Compliance/Shield.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Shield.png rename to core/assets/icons/aws/Security-Identity-Compliance/Shield.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Shield.svg b/core/assets/icons/aws/Security-Identity-Compliance/Shield.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Shield.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Shield.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Signer.png b/core/assets/icons/aws/Security-Identity-Compliance/Signer.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Signer.png rename to core/assets/icons/aws/Security-Identity-Compliance/Signer.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Signer.svg b/core/assets/icons/aws/Security-Identity-Compliance/Signer.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Signer.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Signer.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/Verified-Permissions.png b/core/assets/icons/aws/Security-Identity-Compliance/Verified-Permissions.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Verified-Permissions.png rename to core/assets/icons/aws/Security-Identity-Compliance/Verified-Permissions.png diff --git a/assets/icons/aws/Security-Identity-Compliance/Verified-Permissions.svg b/core/assets/icons/aws/Security-Identity-Compliance/Verified-Permissions.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/Verified-Permissions.svg rename to core/assets/icons/aws/Security-Identity-Compliance/Verified-Permissions.svg diff --git a/assets/icons/aws/Security-Identity-Compliance/WAF.png b/core/assets/icons/aws/Security-Identity-Compliance/WAF.png similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/WAF.png rename to core/assets/icons/aws/Security-Identity-Compliance/WAF.png diff --git a/assets/icons/aws/Security-Identity-Compliance/WAF.svg b/core/assets/icons/aws/Security-Identity-Compliance/WAF.svg similarity index 100% rename from assets/icons/aws/Security-Identity-Compliance/WAF.svg rename to core/assets/icons/aws/Security-Identity-Compliance/WAF.svg diff --git a/assets/icons/aws/Storage/Backup.png b/core/assets/icons/aws/Storage/Backup.png similarity index 100% rename from assets/icons/aws/Storage/Backup.png rename to core/assets/icons/aws/Storage/Backup.png diff --git a/assets/icons/aws/Storage/Backup.svg b/core/assets/icons/aws/Storage/Backup.svg similarity index 100% rename from assets/icons/aws/Storage/Backup.svg rename to core/assets/icons/aws/Storage/Backup.svg diff --git a/assets/icons/aws/Storage/EFS.png b/core/assets/icons/aws/Storage/EFS.png similarity index 100% rename from assets/icons/aws/Storage/EFS.png rename to core/assets/icons/aws/Storage/EFS.png diff --git a/assets/icons/aws/Storage/EFS.svg b/core/assets/icons/aws/Storage/EFS.svg similarity index 100% rename from assets/icons/aws/Storage/EFS.svg rename to core/assets/icons/aws/Storage/EFS.svg diff --git a/assets/icons/aws/Storage/Elastic-Block-Store.png b/core/assets/icons/aws/Storage/Elastic-Block-Store.png similarity index 100% rename from assets/icons/aws/Storage/Elastic-Block-Store.png rename to core/assets/icons/aws/Storage/Elastic-Block-Store.png diff --git a/assets/icons/aws/Storage/Elastic-Block-Store.svg b/core/assets/icons/aws/Storage/Elastic-Block-Store.svg similarity index 100% rename from assets/icons/aws/Storage/Elastic-Block-Store.svg rename to core/assets/icons/aws/Storage/Elastic-Block-Store.svg diff --git a/assets/icons/aws/Storage/Elastic-Disaster-Recovery.png b/core/assets/icons/aws/Storage/Elastic-Disaster-Recovery.png similarity index 100% rename from assets/icons/aws/Storage/Elastic-Disaster-Recovery.png rename to core/assets/icons/aws/Storage/Elastic-Disaster-Recovery.png diff --git a/assets/icons/aws/Storage/Elastic-Disaster-Recovery.svg b/core/assets/icons/aws/Storage/Elastic-Disaster-Recovery.svg similarity index 100% rename from assets/icons/aws/Storage/Elastic-Disaster-Recovery.svg rename to core/assets/icons/aws/Storage/Elastic-Disaster-Recovery.svg diff --git a/assets/icons/aws/Storage/FSx-for-Lustre.png b/core/assets/icons/aws/Storage/FSx-for-Lustre.png similarity index 100% rename from assets/icons/aws/Storage/FSx-for-Lustre.png rename to core/assets/icons/aws/Storage/FSx-for-Lustre.png diff --git a/assets/icons/aws/Storage/FSx-for-Lustre.svg b/core/assets/icons/aws/Storage/FSx-for-Lustre.svg similarity index 100% rename from assets/icons/aws/Storage/FSx-for-Lustre.svg rename to core/assets/icons/aws/Storage/FSx-for-Lustre.svg diff --git a/assets/icons/aws/Storage/FSx-for-NetApp-ONTAP.png b/core/assets/icons/aws/Storage/FSx-for-NetApp-ONTAP.png similarity index 100% rename from assets/icons/aws/Storage/FSx-for-NetApp-ONTAP.png rename to core/assets/icons/aws/Storage/FSx-for-NetApp-ONTAP.png diff --git a/assets/icons/aws/Storage/FSx-for-NetApp-ONTAP.svg b/core/assets/icons/aws/Storage/FSx-for-NetApp-ONTAP.svg similarity index 100% rename from assets/icons/aws/Storage/FSx-for-NetApp-ONTAP.svg rename to core/assets/icons/aws/Storage/FSx-for-NetApp-ONTAP.svg diff --git a/assets/icons/aws/Storage/FSx-for-OpenZFS.png b/core/assets/icons/aws/Storage/FSx-for-OpenZFS.png similarity index 100% rename from assets/icons/aws/Storage/FSx-for-OpenZFS.png rename to core/assets/icons/aws/Storage/FSx-for-OpenZFS.png diff --git a/assets/icons/aws/Storage/FSx-for-OpenZFS.svg b/core/assets/icons/aws/Storage/FSx-for-OpenZFS.svg similarity index 100% rename from assets/icons/aws/Storage/FSx-for-OpenZFS.svg rename to core/assets/icons/aws/Storage/FSx-for-OpenZFS.svg diff --git a/assets/icons/aws/Storage/FSx-for-WFS.png b/core/assets/icons/aws/Storage/FSx-for-WFS.png similarity index 100% rename from assets/icons/aws/Storage/FSx-for-WFS.png rename to core/assets/icons/aws/Storage/FSx-for-WFS.png diff --git a/assets/icons/aws/Storage/FSx-for-WFS.svg b/core/assets/icons/aws/Storage/FSx-for-WFS.svg similarity index 100% rename from assets/icons/aws/Storage/FSx-for-WFS.svg rename to core/assets/icons/aws/Storage/FSx-for-WFS.svg diff --git a/assets/icons/aws/Storage/FSx.png b/core/assets/icons/aws/Storage/FSx.png similarity index 100% rename from assets/icons/aws/Storage/FSx.png rename to core/assets/icons/aws/Storage/FSx.png diff --git a/assets/icons/aws/Storage/FSx.svg b/core/assets/icons/aws/Storage/FSx.svg similarity index 100% rename from assets/icons/aws/Storage/FSx.svg rename to core/assets/icons/aws/Storage/FSx.svg diff --git a/assets/icons/aws/Storage/File-Cache.png b/core/assets/icons/aws/Storage/File-Cache.png similarity index 100% rename from assets/icons/aws/Storage/File-Cache.png rename to core/assets/icons/aws/Storage/File-Cache.png diff --git a/assets/icons/aws/Storage/File-Cache.svg b/core/assets/icons/aws/Storage/File-Cache.svg similarity index 100% rename from assets/icons/aws/Storage/File-Cache.svg rename to core/assets/icons/aws/Storage/File-Cache.svg diff --git a/assets/icons/aws/Storage/S3-on-Outposts.png b/core/assets/icons/aws/Storage/S3-on-Outposts.png similarity index 100% rename from assets/icons/aws/Storage/S3-on-Outposts.png rename to core/assets/icons/aws/Storage/S3-on-Outposts.png diff --git a/assets/icons/aws/Storage/S3-on-Outposts.svg b/core/assets/icons/aws/Storage/S3-on-Outposts.svg similarity index 100% rename from assets/icons/aws/Storage/S3-on-Outposts.svg rename to core/assets/icons/aws/Storage/S3-on-Outposts.svg diff --git a/assets/icons/aws/Storage/Simple-Storage-Service-Glacier.png b/core/assets/icons/aws/Storage/Simple-Storage-Service-Glacier.png similarity index 100% rename from assets/icons/aws/Storage/Simple-Storage-Service-Glacier.png rename to core/assets/icons/aws/Storage/Simple-Storage-Service-Glacier.png diff --git a/assets/icons/aws/Storage/Simple-Storage-Service-Glacier.svg b/core/assets/icons/aws/Storage/Simple-Storage-Service-Glacier.svg similarity index 100% rename from assets/icons/aws/Storage/Simple-Storage-Service-Glacier.svg rename to core/assets/icons/aws/Storage/Simple-Storage-Service-Glacier.svg diff --git a/assets/icons/aws/Storage/Simple-Storage-Service.png b/core/assets/icons/aws/Storage/Simple-Storage-Service.png similarity index 100% rename from assets/icons/aws/Storage/Simple-Storage-Service.png rename to core/assets/icons/aws/Storage/Simple-Storage-Service.png diff --git a/assets/icons/aws/Storage/Simple-Storage-Service.svg b/core/assets/icons/aws/Storage/Simple-Storage-Service.svg similarity index 100% rename from assets/icons/aws/Storage/Simple-Storage-Service.svg rename to core/assets/icons/aws/Storage/Simple-Storage-Service.svg diff --git a/assets/icons/aws/Storage/Snowball-Edge.png b/core/assets/icons/aws/Storage/Snowball-Edge.png similarity index 100% rename from assets/icons/aws/Storage/Snowball-Edge.png rename to core/assets/icons/aws/Storage/Snowball-Edge.png diff --git a/assets/icons/aws/Storage/Snowball-Edge.svg b/core/assets/icons/aws/Storage/Snowball-Edge.svg similarity index 100% rename from assets/icons/aws/Storage/Snowball-Edge.svg rename to core/assets/icons/aws/Storage/Snowball-Edge.svg diff --git a/assets/icons/aws/Storage/Snowball.png b/core/assets/icons/aws/Storage/Snowball.png similarity index 100% rename from assets/icons/aws/Storage/Snowball.png rename to core/assets/icons/aws/Storage/Snowball.png diff --git a/assets/icons/aws/Storage/Snowball.svg b/core/assets/icons/aws/Storage/Snowball.svg similarity index 100% rename from assets/icons/aws/Storage/Snowball.svg rename to core/assets/icons/aws/Storage/Snowball.svg diff --git a/assets/icons/aws/Storage/Snowcone.png b/core/assets/icons/aws/Storage/Snowcone.png similarity index 100% rename from assets/icons/aws/Storage/Snowcone.png rename to core/assets/icons/aws/Storage/Snowcone.png diff --git a/assets/icons/aws/Storage/Snowcone.svg b/core/assets/icons/aws/Storage/Snowcone.svg similarity index 100% rename from assets/icons/aws/Storage/Snowcone.svg rename to core/assets/icons/aws/Storage/Snowcone.svg diff --git a/assets/icons/aws/Storage/Snowmobile.png b/core/assets/icons/aws/Storage/Snowmobile.png similarity index 100% rename from assets/icons/aws/Storage/Snowmobile.png rename to core/assets/icons/aws/Storage/Snowmobile.png diff --git a/assets/icons/aws/Storage/Snowmobile.svg b/core/assets/icons/aws/Storage/Snowmobile.svg similarity index 100% rename from assets/icons/aws/Storage/Snowmobile.svg rename to core/assets/icons/aws/Storage/Snowmobile.svg diff --git a/assets/icons/aws/Storage/Storage-Gateway.png b/core/assets/icons/aws/Storage/Storage-Gateway.png similarity index 100% rename from assets/icons/aws/Storage/Storage-Gateway.png rename to core/assets/icons/aws/Storage/Storage-Gateway.png diff --git a/assets/icons/aws/Storage/Storage-Gateway.svg b/core/assets/icons/aws/Storage/Storage-Gateway.svg similarity index 100% rename from assets/icons/aws/Storage/Storage-Gateway.svg rename to core/assets/icons/aws/Storage/Storage-Gateway.svg diff --git a/assets/icons/aws/VR-AR/Sumerian.png b/core/assets/icons/aws/VR-AR/Sumerian.png similarity index 100% rename from assets/icons/aws/VR-AR/Sumerian.png rename to core/assets/icons/aws/VR-AR/Sumerian.png diff --git a/assets/icons/aws/VR-AR/Sumerian.svg b/core/assets/icons/aws/VR-AR/Sumerian.svg similarity index 100% rename from assets/icons/aws/VR-AR/Sumerian.svg rename to core/assets/icons/aws/VR-AR/Sumerian.svg diff --git a/assets/icons/azure/ai-machine-learning/00028-icon-service-Batch-AI.png b/core/assets/icons/azure/ai-machine-learning/00028-icon-service-Batch-AI.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/00028-icon-service-Batch-AI.png rename to core/assets/icons/azure/ai-machine-learning/00028-icon-service-Batch-AI.png diff --git a/assets/icons/azure/ai-machine-learning/00028-icon-service-Batch-AI.svg b/core/assets/icons/azure/ai-machine-learning/00028-icon-service-Batch-AI.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/00028-icon-service-Batch-AI.svg rename to core/assets/icons/azure/ai-machine-learning/00028-icon-service-Batch-AI.svg diff --git a/assets/icons/azure/ai-machine-learning/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.png b/core/assets/icons/azure/ai-machine-learning/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.png rename to core/assets/icons/azure/ai-machine-learning/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.png diff --git a/assets/icons/azure/ai-machine-learning/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.svg b/core/assets/icons/azure/ai-machine-learning/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.svg rename to core/assets/icons/azure/ai-machine-learning/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.svg diff --git a/assets/icons/azure/ai-machine-learning/00031-icon-service-Genomics.png b/core/assets/icons/azure/ai-machine-learning/00031-icon-service-Genomics.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/00031-icon-service-Genomics.png rename to core/assets/icons/azure/ai-machine-learning/00031-icon-service-Genomics.png diff --git a/assets/icons/azure/ai-machine-learning/00031-icon-service-Genomics.svg b/core/assets/icons/azure/ai-machine-learning/00031-icon-service-Genomics.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/00031-icon-service-Genomics.svg rename to core/assets/icons/azure/ai-machine-learning/00031-icon-service-Genomics.svg diff --git a/assets/icons/azure/ai-machine-learning/00792-icon-service-Computer-Vision.png b/core/assets/icons/azure/ai-machine-learning/00792-icon-service-Computer-Vision.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/00792-icon-service-Computer-Vision.png rename to core/assets/icons/azure/ai-machine-learning/00792-icon-service-Computer-Vision.png diff --git a/assets/icons/azure/ai-machine-learning/00792-icon-service-Computer-Vision.svg b/core/assets/icons/azure/ai-machine-learning/00792-icon-service-Computer-Vision.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/00792-icon-service-Computer-Vision.svg rename to core/assets/icons/azure/ai-machine-learning/00792-icon-service-Computer-Vision.svg diff --git a/assets/icons/azure/ai-machine-learning/00793-icon-service-Custom-Vision.png b/core/assets/icons/azure/ai-machine-learning/00793-icon-service-Custom-Vision.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/00793-icon-service-Custom-Vision.png rename to core/assets/icons/azure/ai-machine-learning/00793-icon-service-Custom-Vision.png diff --git a/assets/icons/azure/ai-machine-learning/00793-icon-service-Custom-Vision.svg b/core/assets/icons/azure/ai-machine-learning/00793-icon-service-Custom-Vision.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/00793-icon-service-Custom-Vision.svg rename to core/assets/icons/azure/ai-machine-learning/00793-icon-service-Custom-Vision.svg diff --git a/assets/icons/azure/ai-machine-learning/00794-icon-service-Face-APIs.png b/core/assets/icons/azure/ai-machine-learning/00794-icon-service-Face-APIs.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/00794-icon-service-Face-APIs.png rename to core/assets/icons/azure/ai-machine-learning/00794-icon-service-Face-APIs.png diff --git a/assets/icons/azure/ai-machine-learning/00794-icon-service-Face-APIs.svg b/core/assets/icons/azure/ai-machine-learning/00794-icon-service-Face-APIs.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/00794-icon-service-Face-APIs.svg rename to core/assets/icons/azure/ai-machine-learning/00794-icon-service-Face-APIs.svg diff --git a/assets/icons/azure/ai-machine-learning/00795-icon-service-Content-Moderators.png b/core/assets/icons/azure/ai-machine-learning/00795-icon-service-Content-Moderators.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/00795-icon-service-Content-Moderators.png rename to core/assets/icons/azure/ai-machine-learning/00795-icon-service-Content-Moderators.png diff --git a/assets/icons/azure/ai-machine-learning/00795-icon-service-Content-Moderators.svg b/core/assets/icons/azure/ai-machine-learning/00795-icon-service-Content-Moderators.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/00795-icon-service-Content-Moderators.svg rename to core/assets/icons/azure/ai-machine-learning/00795-icon-service-Content-Moderators.svg diff --git a/assets/icons/azure/ai-machine-learning/00796-icon-service-Personalizers.png b/core/assets/icons/azure/ai-machine-learning/00796-icon-service-Personalizers.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/00796-icon-service-Personalizers.png rename to core/assets/icons/azure/ai-machine-learning/00796-icon-service-Personalizers.png diff --git a/assets/icons/azure/ai-machine-learning/00796-icon-service-Personalizers.svg b/core/assets/icons/azure/ai-machine-learning/00796-icon-service-Personalizers.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/00796-icon-service-Personalizers.svg rename to core/assets/icons/azure/ai-machine-learning/00796-icon-service-Personalizers.svg diff --git a/assets/icons/azure/ai-machine-learning/00797-icon-service-Speech-Services.png b/core/assets/icons/azure/ai-machine-learning/00797-icon-service-Speech-Services.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/00797-icon-service-Speech-Services.png rename to core/assets/icons/azure/ai-machine-learning/00797-icon-service-Speech-Services.png diff --git a/assets/icons/azure/ai-machine-learning/00797-icon-service-Speech-Services.svg b/core/assets/icons/azure/ai-machine-learning/00797-icon-service-Speech-Services.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/00797-icon-service-Speech-Services.svg rename to core/assets/icons/azure/ai-machine-learning/00797-icon-service-Speech-Services.svg diff --git a/assets/icons/azure/ai-machine-learning/00799-icon-service-QnA-Makers.png b/core/assets/icons/azure/ai-machine-learning/00799-icon-service-QnA-Makers.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/00799-icon-service-QnA-Makers.png rename to core/assets/icons/azure/ai-machine-learning/00799-icon-service-QnA-Makers.png diff --git a/assets/icons/azure/ai-machine-learning/00799-icon-service-QnA-Makers.svg b/core/assets/icons/azure/ai-machine-learning/00799-icon-service-QnA-Makers.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/00799-icon-service-QnA-Makers.svg rename to core/assets/icons/azure/ai-machine-learning/00799-icon-service-QnA-Makers.svg diff --git a/assets/icons/azure/ai-machine-learning/00800-icon-service-Translator-Text.png b/core/assets/icons/azure/ai-machine-learning/00800-icon-service-Translator-Text.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/00800-icon-service-Translator-Text.png rename to core/assets/icons/azure/ai-machine-learning/00800-icon-service-Translator-Text.png diff --git a/assets/icons/azure/ai-machine-learning/00800-icon-service-Translator-Text.svg b/core/assets/icons/azure/ai-machine-learning/00800-icon-service-Translator-Text.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/00800-icon-service-Translator-Text.svg rename to core/assets/icons/azure/ai-machine-learning/00800-icon-service-Translator-Text.svg diff --git a/assets/icons/azure/ai-machine-learning/00801-icon-service-Language-Understanding.png b/core/assets/icons/azure/ai-machine-learning/00801-icon-service-Language-Understanding.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/00801-icon-service-Language-Understanding.png rename to core/assets/icons/azure/ai-machine-learning/00801-icon-service-Language-Understanding.png diff --git a/assets/icons/azure/ai-machine-learning/00801-icon-service-Language-Understanding.svg b/core/assets/icons/azure/ai-machine-learning/00801-icon-service-Language-Understanding.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/00801-icon-service-Language-Understanding.svg rename to core/assets/icons/azure/ai-machine-learning/00801-icon-service-Language-Understanding.svg diff --git a/assets/icons/azure/ai-machine-learning/00812-icon-service-Immersive-Readers.png b/core/assets/icons/azure/ai-machine-learning/00812-icon-service-Immersive-Readers.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/00812-icon-service-Immersive-Readers.png rename to core/assets/icons/azure/ai-machine-learning/00812-icon-service-Immersive-Readers.png diff --git a/assets/icons/azure/ai-machine-learning/00812-icon-service-Immersive-Readers.svg b/core/assets/icons/azure/ai-machine-learning/00812-icon-service-Immersive-Readers.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/00812-icon-service-Immersive-Readers.svg rename to core/assets/icons/azure/ai-machine-learning/00812-icon-service-Immersive-Readers.svg diff --git a/assets/icons/azure/ai-machine-learning/00814-icon-service-Anomaly-Detector.png b/core/assets/icons/azure/ai-machine-learning/00814-icon-service-Anomaly-Detector.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/00814-icon-service-Anomaly-Detector.png rename to core/assets/icons/azure/ai-machine-learning/00814-icon-service-Anomaly-Detector.png diff --git a/assets/icons/azure/ai-machine-learning/00814-icon-service-Anomaly-Detector.svg b/core/assets/icons/azure/ai-machine-learning/00814-icon-service-Anomaly-Detector.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/00814-icon-service-Anomaly-Detector.svg rename to core/assets/icons/azure/ai-machine-learning/00814-icon-service-Anomaly-Detector.svg diff --git a/assets/icons/azure/ai-machine-learning/00819-icon-service-Form-Recognizers.png b/core/assets/icons/azure/ai-machine-learning/00819-icon-service-Form-Recognizers.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/00819-icon-service-Form-Recognizers.png rename to core/assets/icons/azure/ai-machine-learning/00819-icon-service-Form-Recognizers.png diff --git a/assets/icons/azure/ai-machine-learning/00819-icon-service-Form-Recognizers.svg b/core/assets/icons/azure/ai-machine-learning/00819-icon-service-Form-Recognizers.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/00819-icon-service-Form-Recognizers.svg rename to core/assets/icons/azure/ai-machine-learning/00819-icon-service-Form-Recognizers.svg diff --git a/assets/icons/azure/ai-machine-learning/01239-icon-service-Azure-Experimentation-Studio.png b/core/assets/icons/azure/ai-machine-learning/01239-icon-service-Azure-Experimentation-Studio.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/01239-icon-service-Azure-Experimentation-Studio.png rename to core/assets/icons/azure/ai-machine-learning/01239-icon-service-Azure-Experimentation-Studio.png diff --git a/assets/icons/azure/ai-machine-learning/01239-icon-service-Azure-Experimentation-Studio.svg b/core/assets/icons/azure/ai-machine-learning/01239-icon-service-Azure-Experimentation-Studio.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/01239-icon-service-Azure-Experimentation-Studio.svg rename to core/assets/icons/azure/ai-machine-learning/01239-icon-service-Azure-Experimentation-Studio.svg diff --git a/assets/icons/azure/ai-machine-learning/01688-icon-service-Azure-Object-Understanding.png b/core/assets/icons/azure/ai-machine-learning/01688-icon-service-Azure-Object-Understanding.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/01688-icon-service-Azure-Object-Understanding.png rename to core/assets/icons/azure/ai-machine-learning/01688-icon-service-Azure-Object-Understanding.png diff --git a/assets/icons/azure/ai-machine-learning/01688-icon-service-Azure-Object-Understanding.svg b/core/assets/icons/azure/ai-machine-learning/01688-icon-service-Azure-Object-Understanding.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/01688-icon-service-Azure-Object-Understanding.svg rename to core/assets/icons/azure/ai-machine-learning/01688-icon-service-Azure-Object-Understanding.svg diff --git a/assets/icons/azure/ai-machine-learning/02409-icon-service-Metrics-Advisor.png b/core/assets/icons/azure/ai-machine-learning/02409-icon-service-Metrics-Advisor.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/02409-icon-service-Metrics-Advisor.png rename to core/assets/icons/azure/ai-machine-learning/02409-icon-service-Metrics-Advisor.png diff --git a/assets/icons/azure/ai-machine-learning/02409-icon-service-Metrics-Advisor.svg b/core/assets/icons/azure/ai-machine-learning/02409-icon-service-Metrics-Advisor.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/02409-icon-service-Metrics-Advisor.svg rename to core/assets/icons/azure/ai-machine-learning/02409-icon-service-Metrics-Advisor.svg diff --git a/assets/icons/azure/ai-machine-learning/02749-icon-service-Azure-Applied-AI-Services.png b/core/assets/icons/azure/ai-machine-learning/02749-icon-service-Azure-Applied-AI-Services.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/02749-icon-service-Azure-Applied-AI-Services.png rename to core/assets/icons/azure/ai-machine-learning/02749-icon-service-Azure-Applied-AI-Services.png diff --git a/assets/icons/azure/ai-machine-learning/02749-icon-service-Azure-Applied-AI-Services.svg b/core/assets/icons/azure/ai-machine-learning/02749-icon-service-Azure-Applied-AI-Services.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/02749-icon-service-Azure-Applied-AI-Services.svg rename to core/assets/icons/azure/ai-machine-learning/02749-icon-service-Azure-Applied-AI-Services.svg diff --git a/assets/icons/azure/ai-machine-learning/02876-icon-service-Language.png b/core/assets/icons/azure/ai-machine-learning/02876-icon-service-Language.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/02876-icon-service-Language.png rename to core/assets/icons/azure/ai-machine-learning/02876-icon-service-Language.png diff --git a/assets/icons/azure/ai-machine-learning/02876-icon-service-Language.svg b/core/assets/icons/azure/ai-machine-learning/02876-icon-service-Language.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/02876-icon-service-Language.svg rename to core/assets/icons/azure/ai-machine-learning/02876-icon-service-Language.svg diff --git a/assets/icons/azure/ai-machine-learning/029455661-icon-service-FRD-QA.png b/core/assets/icons/azure/ai-machine-learning/029455661-icon-service-FRD-QA.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/029455661-icon-service-FRD-QA.png rename to core/assets/icons/azure/ai-machine-learning/029455661-icon-service-FRD-QA.png diff --git a/assets/icons/azure/ai-machine-learning/029455661-icon-service-FRD-QA.svg b/core/assets/icons/azure/ai-machine-learning/029455661-icon-service-FRD-QA.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/029455661-icon-service-FRD-QA.svg rename to core/assets/icons/azure/ai-machine-learning/029455661-icon-service-FRD-QA.svg diff --git a/assets/icons/azure/ai-machine-learning/03173-icon-service-Cognitive-Services-Decisions.png b/core/assets/icons/azure/ai-machine-learning/03173-icon-service-Cognitive-Services-Decisions.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/03173-icon-service-Cognitive-Services-Decisions.png rename to core/assets/icons/azure/ai-machine-learning/03173-icon-service-Cognitive-Services-Decisions.png diff --git a/assets/icons/azure/ai-machine-learning/03173-icon-service-Cognitive-Services-Decisions.svg b/core/assets/icons/azure/ai-machine-learning/03173-icon-service-Cognitive-Services-Decisions.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/03173-icon-service-Cognitive-Services-Decisions.svg rename to core/assets/icons/azure/ai-machine-learning/03173-icon-service-Cognitive-Services-Decisions.svg diff --git a/assets/icons/azure/ai-machine-learning/03321-icon-service-Serverless-Search.png b/core/assets/icons/azure/ai-machine-learning/03321-icon-service-Serverless-Search.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/03321-icon-service-Serverless-Search.png rename to core/assets/icons/azure/ai-machine-learning/03321-icon-service-Serverless-Search.png diff --git a/assets/icons/azure/ai-machine-learning/03321-icon-service-Serverless-Search.svg b/core/assets/icons/azure/ai-machine-learning/03321-icon-service-Serverless-Search.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/03321-icon-service-Serverless-Search.svg rename to core/assets/icons/azure/ai-machine-learning/03321-icon-service-Serverless-Search.svg diff --git a/assets/icons/azure/ai-machine-learning/03337-icon-service-Bonsai.png b/core/assets/icons/azure/ai-machine-learning/03337-icon-service-Bonsai.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/03337-icon-service-Bonsai.png rename to core/assets/icons/azure/ai-machine-learning/03337-icon-service-Bonsai.png diff --git a/assets/icons/azure/ai-machine-learning/03337-icon-service-Bonsai.svg b/core/assets/icons/azure/ai-machine-learning/03337-icon-service-Bonsai.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/03337-icon-service-Bonsai.svg rename to core/assets/icons/azure/ai-machine-learning/03337-icon-service-Bonsai.svg diff --git a/assets/icons/azure/ai-machine-learning/03390-icon-service-Content-Safety.png b/core/assets/icons/azure/ai-machine-learning/03390-icon-service-Content-Safety.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/03390-icon-service-Content-Safety.png rename to core/assets/icons/azure/ai-machine-learning/03390-icon-service-Content-Safety.png diff --git a/assets/icons/azure/ai-machine-learning/03390-icon-service-Content-Safety.svg b/core/assets/icons/azure/ai-machine-learning/03390-icon-service-Content-Safety.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/03390-icon-service-Content-Safety.svg rename to core/assets/icons/azure/ai-machine-learning/03390-icon-service-Content-Safety.svg diff --git a/assets/icons/azure/ai-machine-learning/03438-icon-service-Azure-OpenAI.png b/core/assets/icons/azure/ai-machine-learning/03438-icon-service-Azure-OpenAI.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/03438-icon-service-Azure-OpenAI.png rename to core/assets/icons/azure/ai-machine-learning/03438-icon-service-Azure-OpenAI.png diff --git a/assets/icons/azure/ai-machine-learning/03438-icon-service-Azure-OpenAI.svg b/core/assets/icons/azure/ai-machine-learning/03438-icon-service-Azure-OpenAI.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/03438-icon-service-Azure-OpenAI.svg rename to core/assets/icons/azure/ai-machine-learning/03438-icon-service-Azure-OpenAI.svg diff --git a/assets/icons/azure/ai-machine-learning/03513-icon-service-AI-Studio.png b/core/assets/icons/azure/ai-machine-learning/03513-icon-service-AI-Studio.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/03513-icon-service-AI-Studio.png rename to core/assets/icons/azure/ai-machine-learning/03513-icon-service-AI-Studio.png diff --git a/assets/icons/azure/ai-machine-learning/03513-icon-service-AI-Studio.svg b/core/assets/icons/azure/ai-machine-learning/03513-icon-service-AI-Studio.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/03513-icon-service-AI-Studio.svg rename to core/assets/icons/azure/ai-machine-learning/03513-icon-service-AI-Studio.svg diff --git a/assets/icons/azure/ai-machine-learning/035746832-icon-service-AI-Foundry.png b/core/assets/icons/azure/ai-machine-learning/035746832-icon-service-AI-Foundry.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/035746832-icon-service-AI-Foundry.png rename to core/assets/icons/azure/ai-machine-learning/035746832-icon-service-AI-Foundry.png diff --git a/assets/icons/azure/ai-machine-learning/035746832-icon-service-AI-Foundry.svg b/core/assets/icons/azure/ai-machine-learning/035746832-icon-service-AI-Foundry.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/035746832-icon-service-AI-Foundry.svg rename to core/assets/icons/azure/ai-machine-learning/035746832-icon-service-AI-Foundry.svg diff --git a/assets/icons/azure/ai-machine-learning/036509374-icon-service-Foundry-Application.png b/core/assets/icons/azure/ai-machine-learning/036509374-icon-service-Foundry-Application.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/036509374-icon-service-Foundry-Application.png rename to core/assets/icons/azure/ai-machine-learning/036509374-icon-service-Foundry-Application.png diff --git a/assets/icons/azure/ai-machine-learning/036509374-icon-service-Foundry-Application.svg b/core/assets/icons/azure/ai-machine-learning/036509374-icon-service-Foundry-Application.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/036509374-icon-service-Foundry-Application.svg rename to core/assets/icons/azure/ai-machine-learning/036509374-icon-service-Foundry-Application.svg diff --git a/assets/icons/azure/ai-machine-learning/036509415-icon-service-Foundry-Project.png b/core/assets/icons/azure/ai-machine-learning/036509415-icon-service-Foundry-Project.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/036509415-icon-service-Foundry-Project.png rename to core/assets/icons/azure/ai-machine-learning/036509415-icon-service-Foundry-Project.png diff --git a/assets/icons/azure/ai-machine-learning/036509415-icon-service-Foundry-Project.svg b/core/assets/icons/azure/ai-machine-learning/036509415-icon-service-Foundry-Project.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/036509415-icon-service-Foundry-Project.svg rename to core/assets/icons/azure/ai-machine-learning/036509415-icon-service-Foundry-Project.svg diff --git a/assets/icons/azure/ai-machine-learning/03755-icon-service-Planetary-Computer-Pro.png b/core/assets/icons/azure/ai-machine-learning/03755-icon-service-Planetary-Computer-Pro.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/03755-icon-service-Planetary-Computer-Pro.png rename to core/assets/icons/azure/ai-machine-learning/03755-icon-service-Planetary-Computer-Pro.png diff --git a/assets/icons/azure/ai-machine-learning/03755-icon-service-Planetary-Computer-Pro.svg b/core/assets/icons/azure/ai-machine-learning/03755-icon-service-Planetary-Computer-Pro.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/03755-icon-service-Planetary-Computer-Pro.svg rename to core/assets/icons/azure/ai-machine-learning/03755-icon-service-Planetary-Computer-Pro.svg diff --git a/assets/icons/azure/ai-machine-learning/038470497-icon-service-Azure-AI-Foundry-IQ.png b/core/assets/icons/azure/ai-machine-learning/038470497-icon-service-Azure-AI-Foundry-IQ.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/038470497-icon-service-Azure-AI-Foundry-IQ.png rename to core/assets/icons/azure/ai-machine-learning/038470497-icon-service-Azure-AI-Foundry-IQ.png diff --git a/assets/icons/azure/ai-machine-learning/038470497-icon-service-Azure-AI-Foundry-IQ.svg b/core/assets/icons/azure/ai-machine-learning/038470497-icon-service-Azure-AI-Foundry-IQ.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/038470497-icon-service-Azure-AI-Foundry-IQ.svg rename to core/assets/icons/azure/ai-machine-learning/038470497-icon-service-Azure-AI-Foundry-IQ.svg diff --git a/assets/icons/azure/ai-machine-learning/038470510-icon-service-Azure-Machine-Learning.png b/core/assets/icons/azure/ai-machine-learning/038470510-icon-service-Azure-Machine-Learning.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/038470510-icon-service-Azure-Machine-Learning.png rename to core/assets/icons/azure/ai-machine-learning/038470510-icon-service-Azure-Machine-Learning.png diff --git a/assets/icons/azure/ai-machine-learning/038470510-icon-service-Azure-Machine-Learning.svg b/core/assets/icons/azure/ai-machine-learning/038470510-icon-service-Azure-Machine-Learning.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/038470510-icon-service-Azure-Machine-Learning.svg rename to core/assets/icons/azure/ai-machine-learning/038470510-icon-service-Azure-Machine-Learning.svg diff --git a/assets/icons/azure/ai-machine-learning/038470523-icon-service-Foundry-Agent-Service.png b/core/assets/icons/azure/ai-machine-learning/038470523-icon-service-Foundry-Agent-Service.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/038470523-icon-service-Foundry-Agent-Service.png rename to core/assets/icons/azure/ai-machine-learning/038470523-icon-service-Foundry-Agent-Service.png diff --git a/assets/icons/azure/ai-machine-learning/038470523-icon-service-Foundry-Agent-Service.svg b/core/assets/icons/azure/ai-machine-learning/038470523-icon-service-Foundry-Agent-Service.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/038470523-icon-service-Foundry-Agent-Service.svg rename to core/assets/icons/azure/ai-machine-learning/038470523-icon-service-Foundry-Agent-Service.svg diff --git a/assets/icons/azure/ai-machine-learning/038470534-icon-service-Foundry-Control-Plane.png b/core/assets/icons/azure/ai-machine-learning/038470534-icon-service-Foundry-Control-Plane.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/038470534-icon-service-Foundry-Control-Plane.png rename to core/assets/icons/azure/ai-machine-learning/038470534-icon-service-Foundry-Control-Plane.png diff --git a/assets/icons/azure/ai-machine-learning/038470534-icon-service-Foundry-Control-Plane.svg b/core/assets/icons/azure/ai-machine-learning/038470534-icon-service-Foundry-Control-Plane.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/038470534-icon-service-Foundry-Control-Plane.svg rename to core/assets/icons/azure/ai-machine-learning/038470534-icon-service-Foundry-Control-Plane.svg diff --git a/assets/icons/azure/ai-machine-learning/038470555-icon-service-Foundry-Labs.png b/core/assets/icons/azure/ai-machine-learning/038470555-icon-service-Foundry-Labs.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/038470555-icon-service-Foundry-Labs.png rename to core/assets/icons/azure/ai-machine-learning/038470555-icon-service-Foundry-Labs.png diff --git a/assets/icons/azure/ai-machine-learning/038470555-icon-service-Foundry-Labs.svg b/core/assets/icons/azure/ai-machine-learning/038470555-icon-service-Foundry-Labs.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/038470555-icon-service-Foundry-Labs.svg rename to core/assets/icons/azure/ai-machine-learning/038470555-icon-service-Foundry-Labs.svg diff --git a/assets/icons/azure/ai-machine-learning/038470593-icon-service-Foundry-Local.png b/core/assets/icons/azure/ai-machine-learning/038470593-icon-service-Foundry-Local.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/038470593-icon-service-Foundry-Local.png rename to core/assets/icons/azure/ai-machine-learning/038470593-icon-service-Foundry-Local.png diff --git a/assets/icons/azure/ai-machine-learning/038470593-icon-service-Foundry-Local.svg b/core/assets/icons/azure/ai-machine-learning/038470593-icon-service-Foundry-Local.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/038470593-icon-service-Foundry-Local.svg rename to core/assets/icons/azure/ai-machine-learning/038470593-icon-service-Foundry-Local.svg diff --git a/assets/icons/azure/ai-machine-learning/038470614-icon-service-Foundry-Models.png b/core/assets/icons/azure/ai-machine-learning/038470614-icon-service-Foundry-Models.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/038470614-icon-service-Foundry-Models.png rename to core/assets/icons/azure/ai-machine-learning/038470614-icon-service-Foundry-Models.png diff --git a/assets/icons/azure/ai-machine-learning/038470614-icon-service-Foundry-Models.svg b/core/assets/icons/azure/ai-machine-learning/038470614-icon-service-Foundry-Models.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/038470614-icon-service-Foundry-Models.svg rename to core/assets/icons/azure/ai-machine-learning/038470614-icon-service-Foundry-Models.svg diff --git a/assets/icons/azure/ai-machine-learning/10044-icon-service-Cognitive-Search.png b/core/assets/icons/azure/ai-machine-learning/10044-icon-service-Cognitive-Search.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/10044-icon-service-Cognitive-Search.png rename to core/assets/icons/azure/ai-machine-learning/10044-icon-service-Cognitive-Search.png diff --git a/assets/icons/azure/ai-machine-learning/10044-icon-service-Cognitive-Search.svg b/core/assets/icons/azure/ai-machine-learning/10044-icon-service-Cognitive-Search.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/10044-icon-service-Cognitive-Search.svg rename to core/assets/icons/azure/ai-machine-learning/10044-icon-service-Cognitive-Search.svg diff --git a/assets/icons/azure/ai-machine-learning/10162-icon-service-Cognitive-Services.png b/core/assets/icons/azure/ai-machine-learning/10162-icon-service-Cognitive-Services.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/10162-icon-service-Cognitive-Services.png rename to core/assets/icons/azure/ai-machine-learning/10162-icon-service-Cognitive-Services.png diff --git a/assets/icons/azure/ai-machine-learning/10162-icon-service-Cognitive-Services.svg b/core/assets/icons/azure/ai-machine-learning/10162-icon-service-Cognitive-Services.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/10162-icon-service-Cognitive-Services.svg rename to core/assets/icons/azure/ai-machine-learning/10162-icon-service-Cognitive-Services.svg diff --git a/assets/icons/azure/ai-machine-learning/10164-icon-service-Genomics-Accounts.png b/core/assets/icons/azure/ai-machine-learning/10164-icon-service-Genomics-Accounts.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/10164-icon-service-Genomics-Accounts.png rename to core/assets/icons/azure/ai-machine-learning/10164-icon-service-Genomics-Accounts.png diff --git a/assets/icons/azure/ai-machine-learning/10164-icon-service-Genomics-Accounts.svg b/core/assets/icons/azure/ai-machine-learning/10164-icon-service-Genomics-Accounts.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/10164-icon-service-Genomics-Accounts.svg rename to core/assets/icons/azure/ai-machine-learning/10164-icon-service-Genomics-Accounts.svg diff --git a/assets/icons/azure/ai-machine-learning/10165-icon-service-Bot-Services.png b/core/assets/icons/azure/ai-machine-learning/10165-icon-service-Bot-Services.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/10165-icon-service-Bot-Services.png rename to core/assets/icons/azure/ai-machine-learning/10165-icon-service-Bot-Services.png diff --git a/assets/icons/azure/ai-machine-learning/10165-icon-service-Bot-Services.svg b/core/assets/icons/azure/ai-machine-learning/10165-icon-service-Bot-Services.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/10165-icon-service-Bot-Services.svg rename to core/assets/icons/azure/ai-machine-learning/10165-icon-service-Bot-Services.svg diff --git a/assets/icons/azure/ai-machine-learning/10166-icon-service-Machine-Learning.png b/core/assets/icons/azure/ai-machine-learning/10166-icon-service-Machine-Learning.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/10166-icon-service-Machine-Learning.png rename to core/assets/icons/azure/ai-machine-learning/10166-icon-service-Machine-Learning.png diff --git a/assets/icons/azure/ai-machine-learning/10166-icon-service-Machine-Learning.svg b/core/assets/icons/azure/ai-machine-learning/10166-icon-service-Machine-Learning.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/10166-icon-service-Machine-Learning.svg rename to core/assets/icons/azure/ai-machine-learning/10166-icon-service-Machine-Learning.svg diff --git a/assets/icons/azure/ai-machine-learning/10167-icon-service-Machine-Learning-Studio-Workspaces.png b/core/assets/icons/azure/ai-machine-learning/10167-icon-service-Machine-Learning-Studio-Workspaces.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/10167-icon-service-Machine-Learning-Studio-Workspaces.png rename to core/assets/icons/azure/ai-machine-learning/10167-icon-service-Machine-Learning-Studio-Workspaces.png diff --git a/assets/icons/azure/ai-machine-learning/10167-icon-service-Machine-Learning-Studio-Workspaces.svg b/core/assets/icons/azure/ai-machine-learning/10167-icon-service-Machine-Learning-Studio-Workspaces.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/10167-icon-service-Machine-Learning-Studio-Workspaces.svg rename to core/assets/icons/azure/ai-machine-learning/10167-icon-service-Machine-Learning-Studio-Workspaces.svg diff --git a/assets/icons/azure/ai-machine-learning/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.png b/core/assets/icons/azure/ai-machine-learning/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.png similarity index 100% rename from assets/icons/azure/ai-machine-learning/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.png rename to core/assets/icons/azure/ai-machine-learning/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.png diff --git a/assets/icons/azure/ai-machine-learning/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.svg b/core/assets/icons/azure/ai-machine-learning/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.svg similarity index 100% rename from assets/icons/azure/ai-machine-learning/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.svg rename to core/assets/icons/azure/ai-machine-learning/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.svg diff --git a/assets/icons/azure/analytics/00009-icon-service-Log-Analytics-Workspaces.png b/core/assets/icons/azure/analytics/00009-icon-service-Log-Analytics-Workspaces.png similarity index 100% rename from assets/icons/azure/analytics/00009-icon-service-Log-Analytics-Workspaces.png rename to core/assets/icons/azure/analytics/00009-icon-service-Log-Analytics-Workspaces.png diff --git a/assets/icons/azure/analytics/00009-icon-service-Log-Analytics-Workspaces.svg b/core/assets/icons/azure/analytics/00009-icon-service-Log-Analytics-Workspaces.svg similarity index 100% rename from assets/icons/azure/analytics/00009-icon-service-Log-Analytics-Workspaces.svg rename to core/assets/icons/azure/analytics/00009-icon-service-Log-Analytics-Workspaces.svg diff --git a/assets/icons/azure/analytics/00039-icon-service-Event-Hubs.png b/core/assets/icons/azure/analytics/00039-icon-service-Event-Hubs.png similarity index 100% rename from assets/icons/azure/analytics/00039-icon-service-Event-Hubs.png rename to core/assets/icons/azure/analytics/00039-icon-service-Event-Hubs.png diff --git a/assets/icons/azure/analytics/00039-icon-service-Event-Hubs.svg b/core/assets/icons/azure/analytics/00039-icon-service-Event-Hubs.svg similarity index 100% rename from assets/icons/azure/analytics/00039-icon-service-Event-Hubs.svg rename to core/assets/icons/azure/analytics/00039-icon-service-Event-Hubs.svg diff --git a/assets/icons/azure/analytics/00042-icon-service-Stream-Analytics-Jobs.png b/core/assets/icons/azure/analytics/00042-icon-service-Stream-Analytics-Jobs.png similarity index 100% rename from assets/icons/azure/analytics/00042-icon-service-Stream-Analytics-Jobs.png rename to core/assets/icons/azure/analytics/00042-icon-service-Stream-Analytics-Jobs.png diff --git a/assets/icons/azure/analytics/00042-icon-service-Stream-Analytics-Jobs.svg b/core/assets/icons/azure/analytics/00042-icon-service-Stream-Analytics-Jobs.svg similarity index 100% rename from assets/icons/azure/analytics/00042-icon-service-Stream-Analytics-Jobs.svg rename to core/assets/icons/azure/analytics/00042-icon-service-Stream-Analytics-Jobs.svg diff --git a/assets/icons/azure/analytics/00562-icon-service-Endpoint-Analytics.png b/core/assets/icons/azure/analytics/00562-icon-service-Endpoint-Analytics.png similarity index 100% rename from assets/icons/azure/analytics/00562-icon-service-Endpoint-Analytics.png rename to core/assets/icons/azure/analytics/00562-icon-service-Endpoint-Analytics.png diff --git a/assets/icons/azure/analytics/00562-icon-service-Endpoint-Analytics.svg b/core/assets/icons/azure/analytics/00562-icon-service-Endpoint-Analytics.svg similarity index 100% rename from assets/icons/azure/analytics/00562-icon-service-Endpoint-Analytics.svg rename to core/assets/icons/azure/analytics/00562-icon-service-Endpoint-Analytics.svg diff --git a/assets/icons/azure/analytics/00606-icon-service-Azure-Synapse-Analytics.png b/core/assets/icons/azure/analytics/00606-icon-service-Azure-Synapse-Analytics.png similarity index 100% rename from assets/icons/azure/analytics/00606-icon-service-Azure-Synapse-Analytics.png rename to core/assets/icons/azure/analytics/00606-icon-service-Azure-Synapse-Analytics.png diff --git a/assets/icons/azure/analytics/00606-icon-service-Azure-Synapse-Analytics.svg b/core/assets/icons/azure/analytics/00606-icon-service-Azure-Synapse-Analytics.svg similarity index 100% rename from assets/icons/azure/analytics/00606-icon-service-Azure-Synapse-Analytics.svg rename to core/assets/icons/azure/analytics/00606-icon-service-Azure-Synapse-Analytics.svg diff --git a/assets/icons/azure/analytics/02189-icon-service-Azure-Workbooks.png b/core/assets/icons/azure/analytics/02189-icon-service-Azure-Workbooks.png similarity index 100% rename from assets/icons/azure/analytics/02189-icon-service-Azure-Workbooks.png rename to core/assets/icons/azure/analytics/02189-icon-service-Azure-Workbooks.png diff --git a/assets/icons/azure/analytics/02189-icon-service-Azure-Workbooks.svg b/core/assets/icons/azure/analytics/02189-icon-service-Azure-Workbooks.svg similarity index 100% rename from assets/icons/azure/analytics/02189-icon-service-Azure-Workbooks.svg rename to core/assets/icons/azure/analytics/02189-icon-service-Azure-Workbooks.svg diff --git a/assets/icons/azure/analytics/02209-icon-service-Private-Link-Services.png b/core/assets/icons/azure/analytics/02209-icon-service-Private-Link-Services.png similarity index 100% rename from assets/icons/azure/analytics/02209-icon-service-Private-Link-Services.png rename to core/assets/icons/azure/analytics/02209-icon-service-Private-Link-Services.png diff --git a/assets/icons/azure/analytics/02209-icon-service-Private-Link-Services.svg b/core/assets/icons/azure/analytics/02209-icon-service-Private-Link-Services.svg similarity index 100% rename from assets/icons/azure/analytics/02209-icon-service-Private-Link-Services.svg rename to core/assets/icons/azure/analytics/02209-icon-service-Private-Link-Services.svg diff --git a/assets/icons/azure/analytics/03332-icon-service-Power-BI-Embedded.png b/core/assets/icons/azure/analytics/03332-icon-service-Power-BI-Embedded.png similarity index 100% rename from assets/icons/azure/analytics/03332-icon-service-Power-BI-Embedded.png rename to core/assets/icons/azure/analytics/03332-icon-service-Power-BI-Embedded.png diff --git a/assets/icons/azure/analytics/03332-icon-service-Power-BI-Embedded.svg b/core/assets/icons/azure/analytics/03332-icon-service-Power-BI-Embedded.svg similarity index 100% rename from assets/icons/azure/analytics/03332-icon-service-Power-BI-Embedded.svg rename to core/assets/icons/azure/analytics/03332-icon-service-Power-BI-Embedded.svg diff --git a/assets/icons/azure/analytics/03335-icon-service-Power-Platform.png b/core/assets/icons/azure/analytics/03335-icon-service-Power-Platform.png similarity index 100% rename from assets/icons/azure/analytics/03335-icon-service-Power-Platform.png rename to core/assets/icons/azure/analytics/03335-icon-service-Power-Platform.png diff --git a/assets/icons/azure/analytics/03335-icon-service-Power-Platform.svg b/core/assets/icons/azure/analytics/03335-icon-service-Power-Platform.svg similarity index 100% rename from assets/icons/azure/analytics/03335-icon-service-Power-Platform.svg rename to core/assets/icons/azure/analytics/03335-icon-service-Power-Platform.svg diff --git a/assets/icons/azure/analytics/10126-icon-service-Data-Factories.png b/core/assets/icons/azure/analytics/10126-icon-service-Data-Factories.png similarity index 100% rename from assets/icons/azure/analytics/10126-icon-service-Data-Factories.png rename to core/assets/icons/azure/analytics/10126-icon-service-Data-Factories.png diff --git a/assets/icons/azure/analytics/10126-icon-service-Data-Factories.svg b/core/assets/icons/azure/analytics/10126-icon-service-Data-Factories.svg similarity index 100% rename from assets/icons/azure/analytics/10126-icon-service-Data-Factories.svg rename to core/assets/icons/azure/analytics/10126-icon-service-Data-Factories.svg diff --git a/assets/icons/azure/analytics/10142-icon-service-HD-Insight-Clusters.png b/core/assets/icons/azure/analytics/10142-icon-service-HD-Insight-Clusters.png similarity index 100% rename from assets/icons/azure/analytics/10142-icon-service-HD-Insight-Clusters.png rename to core/assets/icons/azure/analytics/10142-icon-service-HD-Insight-Clusters.png diff --git a/assets/icons/azure/analytics/10142-icon-service-HD-Insight-Clusters.svg b/core/assets/icons/azure/analytics/10142-icon-service-HD-Insight-Clusters.svg similarity index 100% rename from assets/icons/azure/analytics/10142-icon-service-HD-Insight-Clusters.svg rename to core/assets/icons/azure/analytics/10142-icon-service-HD-Insight-Clusters.svg diff --git a/assets/icons/azure/analytics/10143-icon-service-Data-Lake-Analytics.png b/core/assets/icons/azure/analytics/10143-icon-service-Data-Lake-Analytics.png similarity index 100% rename from assets/icons/azure/analytics/10143-icon-service-Data-Lake-Analytics.png rename to core/assets/icons/azure/analytics/10143-icon-service-Data-Lake-Analytics.png diff --git a/assets/icons/azure/analytics/10143-icon-service-Data-Lake-Analytics.svg b/core/assets/icons/azure/analytics/10143-icon-service-Data-Lake-Analytics.svg similarity index 100% rename from assets/icons/azure/analytics/10143-icon-service-Data-Lake-Analytics.svg rename to core/assets/icons/azure/analytics/10143-icon-service-Data-Lake-Analytics.svg diff --git a/assets/icons/azure/analytics/10145-icon-service-Azure-Data-Explorer-Clusters.png b/core/assets/icons/azure/analytics/10145-icon-service-Azure-Data-Explorer-Clusters.png similarity index 100% rename from assets/icons/azure/analytics/10145-icon-service-Azure-Data-Explorer-Clusters.png rename to core/assets/icons/azure/analytics/10145-icon-service-Azure-Data-Explorer-Clusters.png diff --git a/assets/icons/azure/analytics/10145-icon-service-Azure-Data-Explorer-Clusters.svg b/core/assets/icons/azure/analytics/10145-icon-service-Azure-Data-Explorer-Clusters.svg similarity index 100% rename from assets/icons/azure/analytics/10145-icon-service-Azure-Data-Explorer-Clusters.svg rename to core/assets/icons/azure/analytics/10145-icon-service-Azure-Data-Explorer-Clusters.svg diff --git a/assets/icons/azure/analytics/10148-icon-service-Analysis-Services.png b/core/assets/icons/azure/analytics/10148-icon-service-Analysis-Services.png similarity index 100% rename from assets/icons/azure/analytics/10148-icon-service-Analysis-Services.png rename to core/assets/icons/azure/analytics/10148-icon-service-Analysis-Services.png diff --git a/assets/icons/azure/analytics/10148-icon-service-Analysis-Services.svg b/core/assets/icons/azure/analytics/10148-icon-service-Analysis-Services.svg similarity index 100% rename from assets/icons/azure/analytics/10148-icon-service-Analysis-Services.svg rename to core/assets/icons/azure/analytics/10148-icon-service-Analysis-Services.svg diff --git a/assets/icons/azure/analytics/10149-icon-service-Event-Hub-Clusters.png b/core/assets/icons/azure/analytics/10149-icon-service-Event-Hub-Clusters.png similarity index 100% rename from assets/icons/azure/analytics/10149-icon-service-Event-Hub-Clusters.png rename to core/assets/icons/azure/analytics/10149-icon-service-Event-Hub-Clusters.png diff --git a/assets/icons/azure/analytics/10149-icon-service-Event-Hub-Clusters.svg b/core/assets/icons/azure/analytics/10149-icon-service-Event-Hub-Clusters.svg similarity index 100% rename from assets/icons/azure/analytics/10149-icon-service-Event-Hub-Clusters.svg rename to core/assets/icons/azure/analytics/10149-icon-service-Event-Hub-Clusters.svg diff --git a/assets/icons/azure/analytics/10150-icon-service-Data-Lake-Store-Gen1.png b/core/assets/icons/azure/analytics/10150-icon-service-Data-Lake-Store-Gen1.png similarity index 100% rename from assets/icons/azure/analytics/10150-icon-service-Data-Lake-Store-Gen1.png rename to core/assets/icons/azure/analytics/10150-icon-service-Data-Lake-Store-Gen1.png diff --git a/assets/icons/azure/analytics/10150-icon-service-Data-Lake-Store-Gen1.svg b/core/assets/icons/azure/analytics/10150-icon-service-Data-Lake-Store-Gen1.svg similarity index 100% rename from assets/icons/azure/analytics/10150-icon-service-Data-Lake-Store-Gen1.svg rename to core/assets/icons/azure/analytics/10150-icon-service-Data-Lake-Store-Gen1.svg diff --git a/assets/icons/azure/analytics/10787-icon-service-Azure-Databricks.png b/core/assets/icons/azure/analytics/10787-icon-service-Azure-Databricks.png similarity index 100% rename from assets/icons/azure/analytics/10787-icon-service-Azure-Databricks.png rename to core/assets/icons/azure/analytics/10787-icon-service-Azure-Databricks.png diff --git a/assets/icons/azure/analytics/10787-icon-service-Azure-Databricks.svg b/core/assets/icons/azure/analytics/10787-icon-service-Azure-Databricks.svg similarity index 100% rename from assets/icons/azure/analytics/10787-icon-service-Azure-Databricks.svg rename to core/assets/icons/azure/analytics/10787-icon-service-Azure-Databricks.svg diff --git a/assets/icons/azure/app-services/00046-icon-service-App-Service-Plans.png b/core/assets/icons/azure/app-services/00046-icon-service-App-Service-Plans.png similarity index 100% rename from assets/icons/azure/app-services/00046-icon-service-App-Service-Plans.png rename to core/assets/icons/azure/app-services/00046-icon-service-App-Service-Plans.png diff --git a/assets/icons/azure/app-services/00046-icon-service-App-Service-Plans.svg b/core/assets/icons/azure/app-services/00046-icon-service-App-Service-Plans.svg similarity index 100% rename from assets/icons/azure/app-services/00046-icon-service-App-Service-Plans.svg rename to core/assets/icons/azure/app-services/00046-icon-service-App-Service-Plans.svg diff --git a/assets/icons/azure/app-services/00049-icon-service-App-Service-Certificates.png b/core/assets/icons/azure/app-services/00049-icon-service-App-Service-Certificates.png similarity index 100% rename from assets/icons/azure/app-services/00049-icon-service-App-Service-Certificates.png rename to core/assets/icons/azure/app-services/00049-icon-service-App-Service-Certificates.png diff --git a/assets/icons/azure/app-services/00049-icon-service-App-Service-Certificates.svg b/core/assets/icons/azure/app-services/00049-icon-service-App-Service-Certificates.svg similarity index 100% rename from assets/icons/azure/app-services/00049-icon-service-App-Service-Certificates.svg rename to core/assets/icons/azure/app-services/00049-icon-service-App-Service-Certificates.svg diff --git a/assets/icons/azure/app-services/00050-icon-service-App-Service-Domains.png b/core/assets/icons/azure/app-services/00050-icon-service-App-Service-Domains.png similarity index 100% rename from assets/icons/azure/app-services/00050-icon-service-App-Service-Domains.png rename to core/assets/icons/azure/app-services/00050-icon-service-App-Service-Domains.png diff --git a/assets/icons/azure/app-services/00050-icon-service-App-Service-Domains.svg b/core/assets/icons/azure/app-services/00050-icon-service-App-Service-Domains.svg similarity index 100% rename from assets/icons/azure/app-services/00050-icon-service-App-Service-Domains.svg rename to core/assets/icons/azure/app-services/00050-icon-service-App-Service-Domains.svg diff --git a/assets/icons/azure/app-services/00056-icon-service-CDN-Profiles.png b/core/assets/icons/azure/app-services/00056-icon-service-CDN-Profiles.png similarity index 100% rename from assets/icons/azure/app-services/00056-icon-service-CDN-Profiles.png rename to core/assets/icons/azure/app-services/00056-icon-service-CDN-Profiles.png diff --git a/assets/icons/azure/app-services/00056-icon-service-CDN-Profiles.svg b/core/assets/icons/azure/app-services/00056-icon-service-CDN-Profiles.svg similarity index 100% rename from assets/icons/azure/app-services/00056-icon-service-CDN-Profiles.svg rename to core/assets/icons/azure/app-services/00056-icon-service-CDN-Profiles.svg diff --git a/assets/icons/azure/app-services/10035-icon-service-App-Services.png b/core/assets/icons/azure/app-services/10035-icon-service-App-Services.png similarity index 100% rename from assets/icons/azure/app-services/10035-icon-service-App-Services.png rename to core/assets/icons/azure/app-services/10035-icon-service-App-Services.png diff --git a/assets/icons/azure/app-services/10035-icon-service-App-Services.svg b/core/assets/icons/azure/app-services/10035-icon-service-App-Services.svg similarity index 100% rename from assets/icons/azure/app-services/10035-icon-service-App-Services.svg rename to core/assets/icons/azure/app-services/10035-icon-service-App-Services.svg diff --git a/assets/icons/azure/app-services/10044-icon-service-Cognitive-Search.png b/core/assets/icons/azure/app-services/10044-icon-service-Cognitive-Search.png similarity index 100% rename from assets/icons/azure/app-services/10044-icon-service-Cognitive-Search.png rename to core/assets/icons/azure/app-services/10044-icon-service-Cognitive-Search.png diff --git a/assets/icons/azure/app-services/10044-icon-service-Cognitive-Search.svg b/core/assets/icons/azure/app-services/10044-icon-service-Cognitive-Search.svg similarity index 100% rename from assets/icons/azure/app-services/10044-icon-service-Cognitive-Search.svg rename to core/assets/icons/azure/app-services/10044-icon-service-Cognitive-Search.svg diff --git a/assets/icons/azure/app-services/10045-icon-service-Notification-Hubs.png b/core/assets/icons/azure/app-services/10045-icon-service-Notification-Hubs.png similarity index 100% rename from assets/icons/azure/app-services/10045-icon-service-Notification-Hubs.png rename to core/assets/icons/azure/app-services/10045-icon-service-Notification-Hubs.png diff --git a/assets/icons/azure/app-services/10045-icon-service-Notification-Hubs.svg b/core/assets/icons/azure/app-services/10045-icon-service-Notification-Hubs.svg similarity index 100% rename from assets/icons/azure/app-services/10045-icon-service-Notification-Hubs.svg rename to core/assets/icons/azure/app-services/10045-icon-service-Notification-Hubs.svg diff --git a/assets/icons/azure/app-services/10047-icon-service-App-Service-Environments.png b/core/assets/icons/azure/app-services/10047-icon-service-App-Service-Environments.png similarity index 100% rename from assets/icons/azure/app-services/10047-icon-service-App-Service-Environments.png rename to core/assets/icons/azure/app-services/10047-icon-service-App-Service-Environments.png diff --git a/assets/icons/azure/app-services/10047-icon-service-App-Service-Environments.svg b/core/assets/icons/azure/app-services/10047-icon-service-App-Service-Environments.svg similarity index 100% rename from assets/icons/azure/app-services/10047-icon-service-App-Service-Environments.svg rename to core/assets/icons/azure/app-services/10047-icon-service-App-Service-Environments.svg diff --git a/assets/icons/azure/azure ecosystem/01038-icon-service-Collaborative-Service.png b/core/assets/icons/azure/azure ecosystem/01038-icon-service-Collaborative-Service.png similarity index 100% rename from assets/icons/azure/azure ecosystem/01038-icon-service-Collaborative-Service.png rename to core/assets/icons/azure/azure ecosystem/01038-icon-service-Collaborative-Service.png diff --git a/assets/icons/azure/azure ecosystem/01038-icon-service-Collaborative-Service.svg b/core/assets/icons/azure/azure ecosystem/01038-icon-service-Collaborative-Service.svg similarity index 100% rename from assets/icons/azure/azure ecosystem/01038-icon-service-Collaborative-Service.svg rename to core/assets/icons/azure/azure ecosystem/01038-icon-service-Collaborative-Service.svg diff --git a/assets/icons/azure/azure ecosystem/02354-icon-service-Applens.png b/core/assets/icons/azure/azure ecosystem/02354-icon-service-Applens.png similarity index 100% rename from assets/icons/azure/azure ecosystem/02354-icon-service-Applens.png rename to core/assets/icons/azure/azure ecosystem/02354-icon-service-Applens.png diff --git a/assets/icons/azure/azure ecosystem/02354-icon-service-Applens.svg b/core/assets/icons/azure/azure ecosystem/02354-icon-service-Applens.svg similarity index 100% rename from assets/icons/azure/azure ecosystem/02354-icon-service-Applens.svg rename to core/assets/icons/azure/azure ecosystem/02354-icon-service-Applens.svg diff --git a/assets/icons/azure/azure ecosystem/02573-icon-service-Azure-Hybrid-Center.png b/core/assets/icons/azure/azure ecosystem/02573-icon-service-Azure-Hybrid-Center.png similarity index 100% rename from assets/icons/azure/azure ecosystem/02573-icon-service-Azure-Hybrid-Center.png rename to core/assets/icons/azure/azure ecosystem/02573-icon-service-Azure-Hybrid-Center.png diff --git a/assets/icons/azure/azure ecosystem/02573-icon-service-Azure-Hybrid-Center.svg b/core/assets/icons/azure/azure ecosystem/02573-icon-service-Azure-Hybrid-Center.svg similarity index 100% rename from assets/icons/azure/azure ecosystem/02573-icon-service-Azure-Hybrid-Center.svg rename to core/assets/icons/azure/azure ecosystem/02573-icon-service-Azure-Hybrid-Center.svg diff --git a/assets/icons/azure/azure stack/00965-icon-service-Multi-Tenancy.png b/core/assets/icons/azure/azure stack/00965-icon-service-Multi-Tenancy.png similarity index 100% rename from assets/icons/azure/azure stack/00965-icon-service-Multi-Tenancy.png rename to core/assets/icons/azure/azure stack/00965-icon-service-Multi-Tenancy.png diff --git a/assets/icons/azure/azure stack/00965-icon-service-Multi-Tenancy.svg b/core/assets/icons/azure/azure stack/00965-icon-service-Multi-Tenancy.svg similarity index 100% rename from assets/icons/azure/azure stack/00965-icon-service-Multi-Tenancy.svg rename to core/assets/icons/azure/azure stack/00965-icon-service-Multi-Tenancy.svg diff --git a/assets/icons/azure/azure stack/10108-icon-service-Infrastructure-Backup.png b/core/assets/icons/azure/azure stack/10108-icon-service-Infrastructure-Backup.png similarity index 100% rename from assets/icons/azure/azure stack/10108-icon-service-Infrastructure-Backup.png rename to core/assets/icons/azure/azure stack/10108-icon-service-Infrastructure-Backup.png diff --git a/assets/icons/azure/azure stack/10108-icon-service-Infrastructure-Backup.svg b/core/assets/icons/azure/azure stack/10108-icon-service-Infrastructure-Backup.svg similarity index 100% rename from assets/icons/azure/azure stack/10108-icon-service-Infrastructure-Backup.svg rename to core/assets/icons/azure/azure stack/10108-icon-service-Infrastructure-Backup.svg diff --git a/assets/icons/azure/azure stack/10109-icon-service-Capacity.png b/core/assets/icons/azure/azure stack/10109-icon-service-Capacity.png similarity index 100% rename from assets/icons/azure/azure stack/10109-icon-service-Capacity.png rename to core/assets/icons/azure/azure stack/10109-icon-service-Capacity.png diff --git a/assets/icons/azure/azure stack/10109-icon-service-Capacity.svg b/core/assets/icons/azure/azure stack/10109-icon-service-Capacity.svg similarity index 100% rename from assets/icons/azure/azure stack/10109-icon-service-Capacity.svg rename to core/assets/icons/azure/azure stack/10109-icon-service-Capacity.svg diff --git a/assets/icons/azure/azure stack/10110-icon-service-Offers.png b/core/assets/icons/azure/azure stack/10110-icon-service-Offers.png similarity index 100% rename from assets/icons/azure/azure stack/10110-icon-service-Offers.png rename to core/assets/icons/azure/azure stack/10110-icon-service-Offers.png diff --git a/assets/icons/azure/azure stack/10110-icon-service-Offers.svg b/core/assets/icons/azure/azure stack/10110-icon-service-Offers.svg similarity index 100% rename from assets/icons/azure/azure stack/10110-icon-service-Offers.svg rename to core/assets/icons/azure/azure stack/10110-icon-service-Offers.svg diff --git a/assets/icons/azure/azure stack/10111-icon-service-User-Subscriptions.png b/core/assets/icons/azure/azure stack/10111-icon-service-User-Subscriptions.png similarity index 100% rename from assets/icons/azure/azure stack/10111-icon-service-User-Subscriptions.png rename to core/assets/icons/azure/azure stack/10111-icon-service-User-Subscriptions.png diff --git a/assets/icons/azure/azure stack/10111-icon-service-User-Subscriptions.svg b/core/assets/icons/azure/azure stack/10111-icon-service-User-Subscriptions.svg similarity index 100% rename from assets/icons/azure/azure stack/10111-icon-service-User-Subscriptions.svg rename to core/assets/icons/azure/azure stack/10111-icon-service-User-Subscriptions.svg diff --git a/assets/icons/azure/azure stack/10113-icon-service-Plans.png b/core/assets/icons/azure/azure stack/10113-icon-service-Plans.png similarity index 100% rename from assets/icons/azure/azure stack/10113-icon-service-Plans.png rename to core/assets/icons/azure/azure stack/10113-icon-service-Plans.png diff --git a/assets/icons/azure/azure stack/10113-icon-service-Plans.svg b/core/assets/icons/azure/azure stack/10113-icon-service-Plans.svg similarity index 100% rename from assets/icons/azure/azure stack/10113-icon-service-Plans.svg rename to core/assets/icons/azure/azure stack/10113-icon-service-Plans.svg diff --git a/assets/icons/azure/azure stack/10115-icon-service-Updates.png b/core/assets/icons/azure/azure stack/10115-icon-service-Updates.png similarity index 100% rename from assets/icons/azure/azure stack/10115-icon-service-Updates.png rename to core/assets/icons/azure/azure stack/10115-icon-service-Updates.png diff --git a/assets/icons/azure/azure stack/10115-icon-service-Updates.svg b/core/assets/icons/azure/azure stack/10115-icon-service-Updates.svg similarity index 100% rename from assets/icons/azure/azure stack/10115-icon-service-Updates.svg rename to core/assets/icons/azure/azure stack/10115-icon-service-Updates.svg diff --git a/assets/icons/azure/blockchain/10210-icon-service-Blockchain-Applications.png b/core/assets/icons/azure/blockchain/10210-icon-service-Blockchain-Applications.png similarity index 100% rename from assets/icons/azure/blockchain/10210-icon-service-Blockchain-Applications.png rename to core/assets/icons/azure/blockchain/10210-icon-service-Blockchain-Applications.png diff --git a/assets/icons/azure/blockchain/10210-icon-service-Blockchain-Applications.svg b/core/assets/icons/azure/blockchain/10210-icon-service-Blockchain-Applications.svg similarity index 100% rename from assets/icons/azure/blockchain/10210-icon-service-Blockchain-Applications.svg rename to core/assets/icons/azure/blockchain/10210-icon-service-Blockchain-Applications.svg diff --git a/assets/icons/azure/blockchain/10364-icon-service-Outbound-Connection.png b/core/assets/icons/azure/blockchain/10364-icon-service-Outbound-Connection.png similarity index 100% rename from assets/icons/azure/blockchain/10364-icon-service-Outbound-Connection.png rename to core/assets/icons/azure/blockchain/10364-icon-service-Outbound-Connection.png diff --git a/assets/icons/azure/blockchain/10364-icon-service-Outbound-Connection.svg b/core/assets/icons/azure/blockchain/10364-icon-service-Outbound-Connection.svg similarity index 100% rename from assets/icons/azure/blockchain/10364-icon-service-Outbound-Connection.svg rename to core/assets/icons/azure/blockchain/10364-icon-service-Outbound-Connection.svg diff --git a/assets/icons/azure/blockchain/10366-icon-service-Azure-Blockchain-Service.png b/core/assets/icons/azure/blockchain/10366-icon-service-Azure-Blockchain-Service.png similarity index 100% rename from assets/icons/azure/blockchain/10366-icon-service-Azure-Blockchain-Service.png rename to core/assets/icons/azure/blockchain/10366-icon-service-Azure-Blockchain-Service.png diff --git a/assets/icons/azure/blockchain/10366-icon-service-Azure-Blockchain-Service.svg b/core/assets/icons/azure/blockchain/10366-icon-service-Azure-Blockchain-Service.svg similarity index 100% rename from assets/icons/azure/blockchain/10366-icon-service-Azure-Blockchain-Service.svg rename to core/assets/icons/azure/blockchain/10366-icon-service-Azure-Blockchain-Service.svg diff --git a/assets/icons/azure/blockchain/10367-icon-service-Azure-Token-Service.png b/core/assets/icons/azure/blockchain/10367-icon-service-Azure-Token-Service.png similarity index 100% rename from assets/icons/azure/blockchain/10367-icon-service-Azure-Token-Service.png rename to core/assets/icons/azure/blockchain/10367-icon-service-Azure-Token-Service.png diff --git a/assets/icons/azure/blockchain/10367-icon-service-Azure-Token-Service.svg b/core/assets/icons/azure/blockchain/10367-icon-service-Azure-Token-Service.svg similarity index 100% rename from assets/icons/azure/blockchain/10367-icon-service-Azure-Token-Service.svg rename to core/assets/icons/azure/blockchain/10367-icon-service-Azure-Token-Service.svg diff --git a/assets/icons/azure/blockchain/10374-icon-service-ABS-Member.png b/core/assets/icons/azure/blockchain/10374-icon-service-ABS-Member.png similarity index 100% rename from assets/icons/azure/blockchain/10374-icon-service-ABS-Member.png rename to core/assets/icons/azure/blockchain/10374-icon-service-ABS-Member.png diff --git a/assets/icons/azure/blockchain/10374-icon-service-ABS-Member.svg b/core/assets/icons/azure/blockchain/10374-icon-service-ABS-Member.svg similarity index 100% rename from assets/icons/azure/blockchain/10374-icon-service-ABS-Member.svg rename to core/assets/icons/azure/blockchain/10374-icon-service-ABS-Member.svg diff --git a/assets/icons/azure/blockchain/10375-icon-service-Consortium.png b/core/assets/icons/azure/blockchain/10375-icon-service-Consortium.png similarity index 100% rename from assets/icons/azure/blockchain/10375-icon-service-Consortium.png rename to core/assets/icons/azure/blockchain/10375-icon-service-Consortium.png diff --git a/assets/icons/azure/blockchain/10375-icon-service-Consortium.svg b/core/assets/icons/azure/blockchain/10375-icon-service-Consortium.svg similarity index 100% rename from assets/icons/azure/blockchain/10375-icon-service-Consortium.svg rename to core/assets/icons/azure/blockchain/10375-icon-service-Consortium.svg diff --git a/assets/icons/azure/compute/00195-icon-service-Maintenance-Configuration.png b/core/assets/icons/azure/compute/00195-icon-service-Maintenance-Configuration.png similarity index 100% rename from assets/icons/azure/compute/00195-icon-service-Maintenance-Configuration.png rename to core/assets/icons/azure/compute/00195-icon-service-Maintenance-Configuration.png diff --git a/assets/icons/azure/compute/00195-icon-service-Maintenance-Configuration.svg b/core/assets/icons/azure/compute/00195-icon-service-Maintenance-Configuration.svg similarity index 100% rename from assets/icons/azure/compute/00195-icon-service-Maintenance-Configuration.svg rename to core/assets/icons/azure/compute/00195-icon-service-Maintenance-Configuration.svg diff --git a/assets/icons/azure/compute/00328-icon-service-Host-Pools.png b/core/assets/icons/azure/compute/00328-icon-service-Host-Pools.png similarity index 100% rename from assets/icons/azure/compute/00328-icon-service-Host-Pools.png rename to core/assets/icons/azure/compute/00328-icon-service-Host-Pools.png diff --git a/assets/icons/azure/compute/00328-icon-service-Host-Pools.svg b/core/assets/icons/azure/compute/00328-icon-service-Host-Pools.svg similarity index 100% rename from assets/icons/azure/compute/00328-icon-service-Host-Pools.svg rename to core/assets/icons/azure/compute/00328-icon-service-Host-Pools.svg diff --git a/assets/icons/azure/compute/00329-icon-service-Application-Group.png b/core/assets/icons/azure/compute/00329-icon-service-Application-Group.png similarity index 100% rename from assets/icons/azure/compute/00329-icon-service-Application-Group.png rename to core/assets/icons/azure/compute/00329-icon-service-Application-Group.png diff --git a/assets/icons/azure/compute/00329-icon-service-Application-Group.svg b/core/assets/icons/azure/compute/00329-icon-service-Application-Group.svg similarity index 100% rename from assets/icons/azure/compute/00329-icon-service-Application-Group.svg rename to core/assets/icons/azure/compute/00329-icon-service-Application-Group.svg diff --git a/assets/icons/azure/compute/00330-icon-service-Workspaces.png b/core/assets/icons/azure/compute/00330-icon-service-Workspaces.png similarity index 100% rename from assets/icons/azure/compute/00330-icon-service-Workspaces.png rename to core/assets/icons/azure/compute/00330-icon-service-Workspaces.png diff --git a/assets/icons/azure/compute/00330-icon-service-Workspaces.svg b/core/assets/icons/azure/compute/00330-icon-service-Workspaces.svg similarity index 100% rename from assets/icons/azure/compute/00330-icon-service-Workspaces.svg rename to core/assets/icons/azure/compute/00330-icon-service-Workspaces.svg diff --git a/assets/icons/azure/compute/00398-icon-service-Disk-Encryption-Sets.png b/core/assets/icons/azure/compute/00398-icon-service-Disk-Encryption-Sets.png similarity index 100% rename from assets/icons/azure/compute/00398-icon-service-Disk-Encryption-Sets.png rename to core/assets/icons/azure/compute/00398-icon-service-Disk-Encryption-Sets.png diff --git a/assets/icons/azure/compute/00398-icon-service-Disk-Encryption-Sets.svg b/core/assets/icons/azure/compute/00398-icon-service-Disk-Encryption-Sets.svg similarity index 100% rename from assets/icons/azure/compute/00398-icon-service-Disk-Encryption-Sets.svg rename to core/assets/icons/azure/compute/00398-icon-service-Disk-Encryption-Sets.svg diff --git a/assets/icons/azure/compute/00400-icon-service-Workspaces.png b/core/assets/icons/azure/compute/00400-icon-service-Workspaces.png similarity index 100% rename from assets/icons/azure/compute/00400-icon-service-Workspaces.png rename to core/assets/icons/azure/compute/00400-icon-service-Workspaces.png diff --git a/assets/icons/azure/compute/00400-icon-service-Workspaces.svg b/core/assets/icons/azure/compute/00400-icon-service-Workspaces.svg similarity index 100% rename from assets/icons/azure/compute/00400-icon-service-Workspaces.svg rename to core/assets/icons/azure/compute/00400-icon-service-Workspaces.svg diff --git a/assets/icons/azure/compute/02112-icon-service-Automanaged-VM.png b/core/assets/icons/azure/compute/02112-icon-service-Automanaged-VM.png similarity index 100% rename from assets/icons/azure/compute/02112-icon-service-Automanaged-VM.png rename to core/assets/icons/azure/compute/02112-icon-service-Automanaged-VM.png diff --git a/assets/icons/azure/compute/02112-icon-service-Automanaged-VM.svg b/core/assets/icons/azure/compute/02112-icon-service-Automanaged-VM.svg similarity index 100% rename from assets/icons/azure/compute/02112-icon-service-Automanaged-VM.svg rename to core/assets/icons/azure/compute/02112-icon-service-Automanaged-VM.svg diff --git a/assets/icons/azure/compute/02370-icon-service-Managed-Service-Fabric.png b/core/assets/icons/azure/compute/02370-icon-service-Managed-Service-Fabric.png similarity index 100% rename from assets/icons/azure/compute/02370-icon-service-Managed-Service-Fabric.png rename to core/assets/icons/azure/compute/02370-icon-service-Managed-Service-Fabric.png diff --git a/assets/icons/azure/compute/02370-icon-service-Managed-Service-Fabric.svg b/core/assets/icons/azure/compute/02370-icon-service-Managed-Service-Fabric.svg similarity index 100% rename from assets/icons/azure/compute/02370-icon-service-Managed-Service-Fabric.svg rename to core/assets/icons/azure/compute/02370-icon-service-Managed-Service-Fabric.svg diff --git a/assets/icons/azure/compute/02409-icon-service-Metrics-Advisor.png b/core/assets/icons/azure/compute/02409-icon-service-Metrics-Advisor.png similarity index 100% rename from assets/icons/azure/compute/02409-icon-service-Metrics-Advisor.png rename to core/assets/icons/azure/compute/02409-icon-service-Metrics-Advisor.png diff --git a/assets/icons/azure/compute/02409-icon-service-Metrics-Advisor.svg b/core/assets/icons/azure/compute/02409-icon-service-Metrics-Advisor.svg similarity index 100% rename from assets/icons/azure/compute/02409-icon-service-Metrics-Advisor.svg rename to core/assets/icons/azure/compute/02409-icon-service-Metrics-Advisor.svg diff --git a/assets/icons/azure/compute/02634-icon-service-Image-Templates.png b/core/assets/icons/azure/compute/02634-icon-service-Image-Templates.png similarity index 100% rename from assets/icons/azure/compute/02634-icon-service-Image-Templates.png rename to core/assets/icons/azure/compute/02634-icon-service-Image-Templates.png diff --git a/assets/icons/azure/compute/02634-icon-service-Image-Templates.svg b/core/assets/icons/azure/compute/02634-icon-service-Image-Templates.svg similarity index 100% rename from assets/icons/azure/compute/02634-icon-service-Image-Templates.svg rename to core/assets/icons/azure/compute/02634-icon-service-Image-Templates.svg diff --git a/assets/icons/azure/compute/02817-icon-service-Restore-Points.png b/core/assets/icons/azure/compute/02817-icon-service-Restore-Points.png similarity index 100% rename from assets/icons/azure/compute/02817-icon-service-Restore-Points.png rename to core/assets/icons/azure/compute/02817-icon-service-Restore-Points.png diff --git a/assets/icons/azure/compute/02817-icon-service-Restore-Points.svg b/core/assets/icons/azure/compute/02817-icon-service-Restore-Points.svg similarity index 100% rename from assets/icons/azure/compute/02817-icon-service-Restore-Points.svg rename to core/assets/icons/azure/compute/02817-icon-service-Restore-Points.svg diff --git a/assets/icons/azure/compute/02818-icon-service-Restore-Points-Collections.png b/core/assets/icons/azure/compute/02818-icon-service-Restore-Points-Collections.png similarity index 100% rename from assets/icons/azure/compute/02818-icon-service-Restore-Points-Collections.png rename to core/assets/icons/azure/compute/02818-icon-service-Restore-Points-Collections.png diff --git a/assets/icons/azure/compute/02818-icon-service-Restore-Points-Collections.svg b/core/assets/icons/azure/compute/02818-icon-service-Restore-Points-Collections.svg similarity index 100% rename from assets/icons/azure/compute/02818-icon-service-Restore-Points-Collections.svg rename to core/assets/icons/azure/compute/02818-icon-service-Restore-Points-Collections.svg diff --git a/assets/icons/azure/compute/02864-icon-service-Azure-Compute-Galleries.png b/core/assets/icons/azure/compute/02864-icon-service-Azure-Compute-Galleries.png similarity index 100% rename from assets/icons/azure/compute/02864-icon-service-Azure-Compute-Galleries.png rename to core/assets/icons/azure/compute/02864-icon-service-Azure-Compute-Galleries.png diff --git a/assets/icons/azure/compute/02864-icon-service-Azure-Compute-Galleries.svg b/core/assets/icons/azure/compute/02864-icon-service-Azure-Compute-Galleries.svg similarity index 100% rename from assets/icons/azure/compute/02864-icon-service-Azure-Compute-Galleries.svg rename to core/assets/icons/azure/compute/02864-icon-service-Azure-Compute-Galleries.svg diff --git a/assets/icons/azure/compute/030868929-icon-service-Stage-Maps.png b/core/assets/icons/azure/compute/030868929-icon-service-Stage-Maps.png similarity index 100% rename from assets/icons/azure/compute/030868929-icon-service-Stage-Maps.png rename to core/assets/icons/azure/compute/030868929-icon-service-Stage-Maps.png diff --git a/assets/icons/azure/compute/030868929-icon-service-Stage-Maps.svg b/core/assets/icons/azure/compute/030868929-icon-service-Stage-Maps.svg similarity index 100% rename from assets/icons/azure/compute/030868929-icon-service-Stage-Maps.svg rename to core/assets/icons/azure/compute/030868929-icon-service-Stage-Maps.svg diff --git a/assets/icons/azure/compute/033049334-icon-service-Scheduled-Actions.png b/core/assets/icons/azure/compute/033049334-icon-service-Scheduled-Actions.png similarity index 100% rename from assets/icons/azure/compute/033049334-icon-service-Scheduled-Actions.png rename to core/assets/icons/azure/compute/033049334-icon-service-Scheduled-Actions.png diff --git a/assets/icons/azure/compute/033049334-icon-service-Scheduled-Actions.svg b/core/assets/icons/azure/compute/033049334-icon-service-Scheduled-Actions.svg similarity index 100% rename from assets/icons/azure/compute/033049334-icon-service-Scheduled-Actions.svg rename to core/assets/icons/azure/compute/033049334-icon-service-Scheduled-Actions.svg diff --git a/assets/icons/azure/compute/03487-icon-service-Compute-Fleet.png b/core/assets/icons/azure/compute/03487-icon-service-Compute-Fleet.png similarity index 100% rename from assets/icons/azure/compute/03487-icon-service-Compute-Fleet.png rename to core/assets/icons/azure/compute/03487-icon-service-Compute-Fleet.png diff --git a/assets/icons/azure/compute/03487-icon-service-Compute-Fleet.svg b/core/assets/icons/azure/compute/03487-icon-service-Compute-Fleet.svg similarity index 100% rename from assets/icons/azure/compute/03487-icon-service-Compute-Fleet.svg rename to core/assets/icons/azure/compute/03487-icon-service-Compute-Fleet.svg diff --git a/assets/icons/azure/compute/03543-icon-service-AKS-Automatic.png b/core/assets/icons/azure/compute/03543-icon-service-AKS-Automatic.png similarity index 100% rename from assets/icons/azure/compute/03543-icon-service-AKS-Automatic.png rename to core/assets/icons/azure/compute/03543-icon-service-AKS-Automatic.png diff --git a/assets/icons/azure/compute/03543-icon-service-AKS-Automatic.svg b/core/assets/icons/azure/compute/03543-icon-service-AKS-Automatic.svg similarity index 100% rename from assets/icons/azure/compute/03543-icon-service-AKS-Automatic.svg rename to core/assets/icons/azure/compute/03543-icon-service-AKS-Automatic.svg diff --git a/assets/icons/azure/compute/03645-icon-service-Azure-Linux.png b/core/assets/icons/azure/compute/03645-icon-service-Azure-Linux.png similarity index 100% rename from assets/icons/azure/compute/03645-icon-service-Azure-Linux.png rename to core/assets/icons/azure/compute/03645-icon-service-Azure-Linux.png diff --git a/assets/icons/azure/compute/03645-icon-service-Azure-Linux.svg b/core/assets/icons/azure/compute/03645-icon-service-Azure-Linux.svg similarity index 100% rename from assets/icons/azure/compute/03645-icon-service-Azure-Linux.svg rename to core/assets/icons/azure/compute/03645-icon-service-Azure-Linux.svg diff --git a/assets/icons/azure/compute/10021-icon-service-Virtual-Machine.png b/core/assets/icons/azure/compute/10021-icon-service-Virtual-Machine.png similarity index 100% rename from assets/icons/azure/compute/10021-icon-service-Virtual-Machine.png rename to core/assets/icons/azure/compute/10021-icon-service-Virtual-Machine.png diff --git a/assets/icons/azure/compute/10021-icon-service-Virtual-Machine.svg b/core/assets/icons/azure/compute/10021-icon-service-Virtual-Machine.svg similarity index 100% rename from assets/icons/azure/compute/10021-icon-service-Virtual-Machine.svg rename to core/assets/icons/azure/compute/10021-icon-service-Virtual-Machine.svg diff --git a/assets/icons/azure/compute/10023-icon-service-Kubernetes-Services.png b/core/assets/icons/azure/compute/10023-icon-service-Kubernetes-Services.png similarity index 100% rename from assets/icons/azure/compute/10023-icon-service-Kubernetes-Services.png rename to core/assets/icons/azure/compute/10023-icon-service-Kubernetes-Services.png diff --git a/assets/icons/azure/compute/10023-icon-service-Kubernetes-Services.svg b/core/assets/icons/azure/compute/10023-icon-service-Kubernetes-Services.svg similarity index 100% rename from assets/icons/azure/compute/10023-icon-service-Kubernetes-Services.svg rename to core/assets/icons/azure/compute/10023-icon-service-Kubernetes-Services.svg diff --git a/assets/icons/azure/compute/10024-icon-service-Mesh-Applications.png b/core/assets/icons/azure/compute/10024-icon-service-Mesh-Applications.png similarity index 100% rename from assets/icons/azure/compute/10024-icon-service-Mesh-Applications.png rename to core/assets/icons/azure/compute/10024-icon-service-Mesh-Applications.png diff --git a/assets/icons/azure/compute/10024-icon-service-Mesh-Applications.svg b/core/assets/icons/azure/compute/10024-icon-service-Mesh-Applications.svg similarity index 100% rename from assets/icons/azure/compute/10024-icon-service-Mesh-Applications.svg rename to core/assets/icons/azure/compute/10024-icon-service-Mesh-Applications.svg diff --git a/assets/icons/azure/compute/10025-icon-service-Availability-Sets.png b/core/assets/icons/azure/compute/10025-icon-service-Availability-Sets.png similarity index 100% rename from assets/icons/azure/compute/10025-icon-service-Availability-Sets.png rename to core/assets/icons/azure/compute/10025-icon-service-Availability-Sets.png diff --git a/assets/icons/azure/compute/10025-icon-service-Availability-Sets.svg b/core/assets/icons/azure/compute/10025-icon-service-Availability-Sets.svg similarity index 100% rename from assets/icons/azure/compute/10025-icon-service-Availability-Sets.svg rename to core/assets/icons/azure/compute/10025-icon-service-Availability-Sets.svg diff --git a/assets/icons/azure/compute/10026-icon-service-Disks-Snapshots.png b/core/assets/icons/azure/compute/10026-icon-service-Disks-Snapshots.png similarity index 100% rename from assets/icons/azure/compute/10026-icon-service-Disks-Snapshots.png rename to core/assets/icons/azure/compute/10026-icon-service-Disks-Snapshots.png diff --git a/assets/icons/azure/compute/10026-icon-service-Disks-Snapshots.svg b/core/assets/icons/azure/compute/10026-icon-service-Disks-Snapshots.svg similarity index 100% rename from assets/icons/azure/compute/10026-icon-service-Disks-Snapshots.svg rename to core/assets/icons/azure/compute/10026-icon-service-Disks-Snapshots.svg diff --git a/assets/icons/azure/compute/10027-icon-service-OS-Images-(Classic).png b/core/assets/icons/azure/compute/10027-icon-service-OS-Images-(Classic).png similarity index 100% rename from assets/icons/azure/compute/10027-icon-service-OS-Images-(Classic).png rename to core/assets/icons/azure/compute/10027-icon-service-OS-Images-(Classic).png diff --git a/assets/icons/azure/compute/10027-icon-service-OS-Images-(Classic).svg b/core/assets/icons/azure/compute/10027-icon-service-OS-Images-(Classic).svg similarity index 100% rename from assets/icons/azure/compute/10027-icon-service-OS-Images-(Classic).svg rename to core/assets/icons/azure/compute/10027-icon-service-OS-Images-(Classic).svg diff --git a/assets/icons/azure/compute/10028-icon-service-Virtual-Machines-(Classic).png b/core/assets/icons/azure/compute/10028-icon-service-Virtual-Machines-(Classic).png similarity index 100% rename from assets/icons/azure/compute/10028-icon-service-Virtual-Machines-(Classic).png rename to core/assets/icons/azure/compute/10028-icon-service-Virtual-Machines-(Classic).png diff --git a/assets/icons/azure/compute/10028-icon-service-Virtual-Machines-(Classic).svg b/core/assets/icons/azure/compute/10028-icon-service-Virtual-Machines-(Classic).svg similarity index 100% rename from assets/icons/azure/compute/10028-icon-service-Virtual-Machines-(Classic).svg rename to core/assets/icons/azure/compute/10028-icon-service-Virtual-Machines-(Classic).svg diff --git a/assets/icons/azure/compute/10029-icon-service-Function-Apps.png b/core/assets/icons/azure/compute/10029-icon-service-Function-Apps.png similarity index 100% rename from assets/icons/azure/compute/10029-icon-service-Function-Apps.png rename to core/assets/icons/azure/compute/10029-icon-service-Function-Apps.png diff --git a/assets/icons/azure/compute/10029-icon-service-Function-Apps.svg b/core/assets/icons/azure/compute/10029-icon-service-Function-Apps.svg similarity index 100% rename from assets/icons/azure/compute/10029-icon-service-Function-Apps.svg rename to core/assets/icons/azure/compute/10029-icon-service-Function-Apps.svg diff --git a/assets/icons/azure/compute/10030-icon-service-Cloud-Services-(Classic).png b/core/assets/icons/azure/compute/10030-icon-service-Cloud-Services-(Classic).png similarity index 100% rename from assets/icons/azure/compute/10030-icon-service-Cloud-Services-(Classic).png rename to core/assets/icons/azure/compute/10030-icon-service-Cloud-Services-(Classic).png diff --git a/assets/icons/azure/compute/10030-icon-service-Cloud-Services-(Classic).svg b/core/assets/icons/azure/compute/10030-icon-service-Cloud-Services-(Classic).svg similarity index 100% rename from assets/icons/azure/compute/10030-icon-service-Cloud-Services-(Classic).svg rename to core/assets/icons/azure/compute/10030-icon-service-Cloud-Services-(Classic).svg diff --git a/assets/icons/azure/compute/10031-icon-service-Batch-Accounts.png b/core/assets/icons/azure/compute/10031-icon-service-Batch-Accounts.png similarity index 100% rename from assets/icons/azure/compute/10031-icon-service-Batch-Accounts.png rename to core/assets/icons/azure/compute/10031-icon-service-Batch-Accounts.png diff --git a/assets/icons/azure/compute/10031-icon-service-Batch-Accounts.svg b/core/assets/icons/azure/compute/10031-icon-service-Batch-Accounts.svg similarity index 100% rename from assets/icons/azure/compute/10031-icon-service-Batch-Accounts.svg rename to core/assets/icons/azure/compute/10031-icon-service-Batch-Accounts.svg diff --git a/assets/icons/azure/compute/10032-icon-service-Disks.png b/core/assets/icons/azure/compute/10032-icon-service-Disks.png similarity index 100% rename from assets/icons/azure/compute/10032-icon-service-Disks.png rename to core/assets/icons/azure/compute/10032-icon-service-Disks.png diff --git a/assets/icons/azure/compute/10032-icon-service-Disks.svg b/core/assets/icons/azure/compute/10032-icon-service-Disks.svg similarity index 100% rename from assets/icons/azure/compute/10032-icon-service-Disks.svg rename to core/assets/icons/azure/compute/10032-icon-service-Disks.svg diff --git a/assets/icons/azure/compute/10033-icon-service-Images.png b/core/assets/icons/azure/compute/10033-icon-service-Images.png similarity index 100% rename from assets/icons/azure/compute/10033-icon-service-Images.png rename to core/assets/icons/azure/compute/10033-icon-service-Images.png diff --git a/assets/icons/azure/compute/10033-icon-service-Images.svg b/core/assets/icons/azure/compute/10033-icon-service-Images.svg similarity index 100% rename from assets/icons/azure/compute/10033-icon-service-Images.svg rename to core/assets/icons/azure/compute/10033-icon-service-Images.svg diff --git a/assets/icons/azure/compute/10034-icon-service-VM-Scale-Sets.png b/core/assets/icons/azure/compute/10034-icon-service-VM-Scale-Sets.png similarity index 100% rename from assets/icons/azure/compute/10034-icon-service-VM-Scale-Sets.png rename to core/assets/icons/azure/compute/10034-icon-service-VM-Scale-Sets.png diff --git a/assets/icons/azure/compute/10034-icon-service-VM-Scale-Sets.svg b/core/assets/icons/azure/compute/10034-icon-service-VM-Scale-Sets.svg similarity index 100% rename from assets/icons/azure/compute/10034-icon-service-VM-Scale-Sets.svg rename to core/assets/icons/azure/compute/10034-icon-service-VM-Scale-Sets.svg diff --git a/assets/icons/azure/compute/10035-icon-service-App-Services.png b/core/assets/icons/azure/compute/10035-icon-service-App-Services.png similarity index 100% rename from assets/icons/azure/compute/10035-icon-service-App-Services.png rename to core/assets/icons/azure/compute/10035-icon-service-App-Services.png diff --git a/assets/icons/azure/compute/10035-icon-service-App-Services.svg b/core/assets/icons/azure/compute/10035-icon-service-App-Services.svg similarity index 100% rename from assets/icons/azure/compute/10035-icon-service-App-Services.svg rename to core/assets/icons/azure/compute/10035-icon-service-App-Services.svg diff --git a/assets/icons/azure/compute/10036-icon-service-Service-Fabric-Clusters.png b/core/assets/icons/azure/compute/10036-icon-service-Service-Fabric-Clusters.png similarity index 100% rename from assets/icons/azure/compute/10036-icon-service-Service-Fabric-Clusters.png rename to core/assets/icons/azure/compute/10036-icon-service-Service-Fabric-Clusters.png diff --git a/assets/icons/azure/compute/10036-icon-service-Service-Fabric-Clusters.svg b/core/assets/icons/azure/compute/10036-icon-service-Service-Fabric-Clusters.svg similarity index 100% rename from assets/icons/azure/compute/10036-icon-service-Service-Fabric-Clusters.svg rename to core/assets/icons/azure/compute/10036-icon-service-Service-Fabric-Clusters.svg diff --git a/assets/icons/azure/compute/10037-icon-service-Image-Definitions.png b/core/assets/icons/azure/compute/10037-icon-service-Image-Definitions.png similarity index 100% rename from assets/icons/azure/compute/10037-icon-service-Image-Definitions.png rename to core/assets/icons/azure/compute/10037-icon-service-Image-Definitions.png diff --git a/assets/icons/azure/compute/10037-icon-service-Image-Definitions.svg b/core/assets/icons/azure/compute/10037-icon-service-Image-Definitions.svg similarity index 100% rename from assets/icons/azure/compute/10037-icon-service-Image-Definitions.svg rename to core/assets/icons/azure/compute/10037-icon-service-Image-Definitions.svg diff --git a/assets/icons/azure/compute/10038-icon-service-Image-Versions.png b/core/assets/icons/azure/compute/10038-icon-service-Image-Versions.png similarity index 100% rename from assets/icons/azure/compute/10038-icon-service-Image-Versions.png rename to core/assets/icons/azure/compute/10038-icon-service-Image-Versions.png diff --git a/assets/icons/azure/compute/10038-icon-service-Image-Versions.svg b/core/assets/icons/azure/compute/10038-icon-service-Image-Versions.svg similarity index 100% rename from assets/icons/azure/compute/10038-icon-service-Image-Versions.svg rename to core/assets/icons/azure/compute/10038-icon-service-Image-Versions.svg diff --git a/assets/icons/azure/compute/10039-icon-service-Shared-Image-Galleries.png b/core/assets/icons/azure/compute/10039-icon-service-Shared-Image-Galleries.png similarity index 100% rename from assets/icons/azure/compute/10039-icon-service-Shared-Image-Galleries.png rename to core/assets/icons/azure/compute/10039-icon-service-Shared-Image-Galleries.png diff --git a/assets/icons/azure/compute/10039-icon-service-Shared-Image-Galleries.svg b/core/assets/icons/azure/compute/10039-icon-service-Shared-Image-Galleries.svg similarity index 100% rename from assets/icons/azure/compute/10039-icon-service-Shared-Image-Galleries.svg rename to core/assets/icons/azure/compute/10039-icon-service-Shared-Image-Galleries.svg diff --git a/assets/icons/azure/compute/10040-icon-service-VM-Images-(Classic).png b/core/assets/icons/azure/compute/10040-icon-service-VM-Images-(Classic).png similarity index 100% rename from assets/icons/azure/compute/10040-icon-service-VM-Images-(Classic).png rename to core/assets/icons/azure/compute/10040-icon-service-VM-Images-(Classic).png diff --git a/assets/icons/azure/compute/10040-icon-service-VM-Images-(Classic).svg b/core/assets/icons/azure/compute/10040-icon-service-VM-Images-(Classic).svg similarity index 100% rename from assets/icons/azure/compute/10040-icon-service-VM-Images-(Classic).svg rename to core/assets/icons/azure/compute/10040-icon-service-VM-Images-(Classic).svg diff --git a/assets/icons/azure/compute/10041-icon-service-Disks-(Classic).png b/core/assets/icons/azure/compute/10041-icon-service-Disks-(Classic).png similarity index 100% rename from assets/icons/azure/compute/10041-icon-service-Disks-(Classic).png rename to core/assets/icons/azure/compute/10041-icon-service-Disks-(Classic).png diff --git a/assets/icons/azure/compute/10041-icon-service-Disks-(Classic).svg b/core/assets/icons/azure/compute/10041-icon-service-Disks-(Classic).svg similarity index 100% rename from assets/icons/azure/compute/10041-icon-service-Disks-(Classic).svg rename to core/assets/icons/azure/compute/10041-icon-service-Disks-(Classic).svg diff --git a/assets/icons/azure/compute/10049-icon-service-Container-Services-(Deprecated).png b/core/assets/icons/azure/compute/10049-icon-service-Container-Services-(Deprecated).png similarity index 100% rename from assets/icons/azure/compute/10049-icon-service-Container-Services-(Deprecated).png rename to core/assets/icons/azure/compute/10049-icon-service-Container-Services-(Deprecated).png diff --git a/assets/icons/azure/compute/10049-icon-service-Container-Services-(Deprecated).svg b/core/assets/icons/azure/compute/10049-icon-service-Container-Services-(Deprecated).svg similarity index 100% rename from assets/icons/azure/compute/10049-icon-service-Container-Services-(Deprecated).svg rename to core/assets/icons/azure/compute/10049-icon-service-Container-Services-(Deprecated).svg diff --git a/assets/icons/azure/compute/10104-icon-service-Container-Instances.png b/core/assets/icons/azure/compute/10104-icon-service-Container-Instances.png similarity index 100% rename from assets/icons/azure/compute/10104-icon-service-Container-Instances.png rename to core/assets/icons/azure/compute/10104-icon-service-Container-Instances.png diff --git a/assets/icons/azure/compute/10104-icon-service-Container-Instances.svg b/core/assets/icons/azure/compute/10104-icon-service-Container-Instances.svg similarity index 100% rename from assets/icons/azure/compute/10104-icon-service-Container-Instances.svg rename to core/assets/icons/azure/compute/10104-icon-service-Container-Instances.svg diff --git a/assets/icons/azure/compute/10346-icon-service-Host-Groups.png b/core/assets/icons/azure/compute/10346-icon-service-Host-Groups.png similarity index 100% rename from assets/icons/azure/compute/10346-icon-service-Host-Groups.png rename to core/assets/icons/azure/compute/10346-icon-service-Host-Groups.png diff --git a/assets/icons/azure/compute/10346-icon-service-Host-Groups.svg b/core/assets/icons/azure/compute/10346-icon-service-Host-Groups.svg similarity index 100% rename from assets/icons/azure/compute/10346-icon-service-Host-Groups.svg rename to core/assets/icons/azure/compute/10346-icon-service-Host-Groups.svg diff --git a/assets/icons/azure/compute/10347-icon-service-Hosts.png b/core/assets/icons/azure/compute/10347-icon-service-Hosts.png similarity index 100% rename from assets/icons/azure/compute/10347-icon-service-Hosts.png rename to core/assets/icons/azure/compute/10347-icon-service-Hosts.png diff --git a/assets/icons/azure/compute/10347-icon-service-Hosts.svg b/core/assets/icons/azure/compute/10347-icon-service-Hosts.svg similarity index 100% rename from assets/icons/azure/compute/10347-icon-service-Hosts.svg rename to core/assets/icons/azure/compute/10347-icon-service-Hosts.svg diff --git a/assets/icons/azure/compute/10370-icon-service-Azure-Spring-Apps.png b/core/assets/icons/azure/compute/10370-icon-service-Azure-Spring-Apps.png similarity index 100% rename from assets/icons/azure/compute/10370-icon-service-Azure-Spring-Apps.png rename to core/assets/icons/azure/compute/10370-icon-service-Azure-Spring-Apps.png diff --git a/assets/icons/azure/compute/10370-icon-service-Azure-Spring-Apps.svg b/core/assets/icons/azure/compute/10370-icon-service-Azure-Spring-Apps.svg similarity index 100% rename from assets/icons/azure/compute/10370-icon-service-Azure-Spring-Apps.svg rename to core/assets/icons/azure/compute/10370-icon-service-Azure-Spring-Apps.svg diff --git a/assets/icons/azure/containers/029637767-icon-service-Service-Groups.png b/core/assets/icons/azure/containers/029637767-icon-service-Service-Groups.png similarity index 100% rename from assets/icons/azure/containers/029637767-icon-service-Service-Groups.png rename to core/assets/icons/azure/containers/029637767-icon-service-Service-Groups.png diff --git a/assets/icons/azure/containers/029637767-icon-service-Service-Groups.svg b/core/assets/icons/azure/containers/029637767-icon-service-Service-Groups.svg similarity index 100% rename from assets/icons/azure/containers/029637767-icon-service-Service-Groups.svg rename to core/assets/icons/azure/containers/029637767-icon-service-Service-Groups.svg diff --git a/assets/icons/azure/containers/032487601-icon-service-AKS-Network-Policy.png b/core/assets/icons/azure/containers/032487601-icon-service-AKS-Network-Policy.png similarity index 100% rename from assets/icons/azure/containers/032487601-icon-service-AKS-Network-Policy.png rename to core/assets/icons/azure/containers/032487601-icon-service-AKS-Network-Policy.png diff --git a/assets/icons/azure/containers/032487601-icon-service-AKS-Network-Policy.svg b/core/assets/icons/azure/containers/032487601-icon-service-AKS-Network-Policy.svg similarity index 100% rename from assets/icons/azure/containers/032487601-icon-service-AKS-Network-Policy.svg rename to core/assets/icons/azure/containers/032487601-icon-service-AKS-Network-Policy.svg diff --git a/assets/icons/azure/containers/03331-icon-service-Azure-Red-Hat-OpenShift.png b/core/assets/icons/azure/containers/03331-icon-service-Azure-Red-Hat-OpenShift.png similarity index 100% rename from assets/icons/azure/containers/03331-icon-service-Azure-Red-Hat-OpenShift.png rename to core/assets/icons/azure/containers/03331-icon-service-Azure-Red-Hat-OpenShift.png diff --git a/assets/icons/azure/containers/03331-icon-service-Azure-Red-Hat-OpenShift.svg b/core/assets/icons/azure/containers/03331-icon-service-Azure-Red-Hat-OpenShift.svg similarity index 100% rename from assets/icons/azure/containers/03331-icon-service-Azure-Red-Hat-OpenShift.svg rename to core/assets/icons/azure/containers/03331-icon-service-Azure-Red-Hat-OpenShift.svg diff --git a/assets/icons/azure/containers/033545113-icon-service-Kubernetes-Hub.png b/core/assets/icons/azure/containers/033545113-icon-service-Kubernetes-Hub.png similarity index 100% rename from assets/icons/azure/containers/033545113-icon-service-Kubernetes-Hub.png rename to core/assets/icons/azure/containers/033545113-icon-service-Kubernetes-Hub.png diff --git a/assets/icons/azure/containers/033545113-icon-service-Kubernetes-Hub.svg b/core/assets/icons/azure/containers/033545113-icon-service-Kubernetes-Hub.svg similarity index 100% rename from assets/icons/azure/containers/033545113-icon-service-Kubernetes-Hub.svg rename to core/assets/icons/azure/containers/033545113-icon-service-Kubernetes-Hub.svg diff --git a/assets/icons/azure/containers/03401-icon-service-Azure-Container-Storage.png b/core/assets/icons/azure/containers/03401-icon-service-Azure-Container-Storage.png similarity index 100% rename from assets/icons/azure/containers/03401-icon-service-Azure-Container-Storage.png rename to core/assets/icons/azure/containers/03401-icon-service-Azure-Container-Storage.png diff --git a/assets/icons/azure/containers/03401-icon-service-Azure-Container-Storage.svg b/core/assets/icons/azure/containers/03401-icon-service-Azure-Container-Storage.svg similarity index 100% rename from assets/icons/azure/containers/03401-icon-service-Azure-Container-Storage.svg rename to core/assets/icons/azure/containers/03401-icon-service-Azure-Container-Storage.svg diff --git a/assets/icons/azure/containers/10023-icon-service-Kubernetes-Services.png b/core/assets/icons/azure/containers/10023-icon-service-Kubernetes-Services.png similarity index 100% rename from assets/icons/azure/containers/10023-icon-service-Kubernetes-Services.png rename to core/assets/icons/azure/containers/10023-icon-service-Kubernetes-Services.png diff --git a/assets/icons/azure/containers/10023-icon-service-Kubernetes-Services.svg b/core/assets/icons/azure/containers/10023-icon-service-Kubernetes-Services.svg similarity index 100% rename from assets/icons/azure/containers/10023-icon-service-Kubernetes-Services.svg rename to core/assets/icons/azure/containers/10023-icon-service-Kubernetes-Services.svg diff --git a/assets/icons/azure/containers/10031-icon-service-Batch-Accounts.png b/core/assets/icons/azure/containers/10031-icon-service-Batch-Accounts.png similarity index 100% rename from assets/icons/azure/containers/10031-icon-service-Batch-Accounts.png rename to core/assets/icons/azure/containers/10031-icon-service-Batch-Accounts.png diff --git a/assets/icons/azure/containers/10031-icon-service-Batch-Accounts.svg b/core/assets/icons/azure/containers/10031-icon-service-Batch-Accounts.svg similarity index 100% rename from assets/icons/azure/containers/10031-icon-service-Batch-Accounts.svg rename to core/assets/icons/azure/containers/10031-icon-service-Batch-Accounts.svg diff --git a/assets/icons/azure/containers/10035-icon-service-App-Services.png b/core/assets/icons/azure/containers/10035-icon-service-App-Services.png similarity index 100% rename from assets/icons/azure/containers/10035-icon-service-App-Services.png rename to core/assets/icons/azure/containers/10035-icon-service-App-Services.png diff --git a/assets/icons/azure/containers/10035-icon-service-App-Services.svg b/core/assets/icons/azure/containers/10035-icon-service-App-Services.svg similarity index 100% rename from assets/icons/azure/containers/10035-icon-service-App-Services.svg rename to core/assets/icons/azure/containers/10035-icon-service-App-Services.svg diff --git a/assets/icons/azure/containers/10036-icon-service-Service-Fabric-Clusters.png b/core/assets/icons/azure/containers/10036-icon-service-Service-Fabric-Clusters.png similarity index 100% rename from assets/icons/azure/containers/10036-icon-service-Service-Fabric-Clusters.png rename to core/assets/icons/azure/containers/10036-icon-service-Service-Fabric-Clusters.png diff --git a/assets/icons/azure/containers/10036-icon-service-Service-Fabric-Clusters.svg b/core/assets/icons/azure/containers/10036-icon-service-Service-Fabric-Clusters.svg similarity index 100% rename from assets/icons/azure/containers/10036-icon-service-Service-Fabric-Clusters.svg rename to core/assets/icons/azure/containers/10036-icon-service-Service-Fabric-Clusters.svg diff --git a/assets/icons/azure/containers/10104-icon-service-Container-Instances.png b/core/assets/icons/azure/containers/10104-icon-service-Container-Instances.png similarity index 100% rename from assets/icons/azure/containers/10104-icon-service-Container-Instances.png rename to core/assets/icons/azure/containers/10104-icon-service-Container-Instances.png diff --git a/assets/icons/azure/containers/10104-icon-service-Container-Instances.svg b/core/assets/icons/azure/containers/10104-icon-service-Container-Instances.svg similarity index 100% rename from assets/icons/azure/containers/10104-icon-service-Container-Instances.svg rename to core/assets/icons/azure/containers/10104-icon-service-Container-Instances.svg diff --git a/assets/icons/azure/containers/10105-icon-service-Container-Registries.png b/core/assets/icons/azure/containers/10105-icon-service-Container-Registries.png similarity index 100% rename from assets/icons/azure/containers/10105-icon-service-Container-Registries.png rename to core/assets/icons/azure/containers/10105-icon-service-Container-Registries.png diff --git a/assets/icons/azure/containers/10105-icon-service-Container-Registries.svg b/core/assets/icons/azure/containers/10105-icon-service-Container-Registries.svg similarity index 100% rename from assets/icons/azure/containers/10105-icon-service-Container-Registries.svg rename to core/assets/icons/azure/containers/10105-icon-service-Container-Registries.svg diff --git a/assets/icons/azure/databases/00036-icon-service-SQL-Data-Warehouses.png b/core/assets/icons/azure/databases/00036-icon-service-SQL-Data-Warehouses.png similarity index 100% rename from assets/icons/azure/databases/00036-icon-service-SQL-Data-Warehouses.png rename to core/assets/icons/azure/databases/00036-icon-service-SQL-Data-Warehouses.png diff --git a/assets/icons/azure/databases/00036-icon-service-SQL-Data-Warehouses.svg b/core/assets/icons/azure/databases/00036-icon-service-SQL-Data-Warehouses.svg similarity index 100% rename from assets/icons/azure/databases/00036-icon-service-SQL-Data-Warehouses.svg rename to core/assets/icons/azure/databases/00036-icon-service-SQL-Data-Warehouses.svg diff --git a/assets/icons/azure/databases/00606-icon-service-Azure-Synapse-Analytics.png b/core/assets/icons/azure/databases/00606-icon-service-Azure-Synapse-Analytics.png similarity index 100% rename from assets/icons/azure/databases/00606-icon-service-Azure-Synapse-Analytics.png rename to core/assets/icons/azure/databases/00606-icon-service-Azure-Synapse-Analytics.png diff --git a/assets/icons/azure/databases/00606-icon-service-Azure-Synapse-Analytics.svg b/core/assets/icons/azure/databases/00606-icon-service-Azure-Synapse-Analytics.svg similarity index 100% rename from assets/icons/azure/databases/00606-icon-service-Azure-Synapse-Analytics.svg rename to core/assets/icons/azure/databases/00606-icon-service-Azure-Synapse-Analytics.svg diff --git a/assets/icons/azure/databases/02390-icon-service-Azure-SQL.png b/core/assets/icons/azure/databases/02390-icon-service-Azure-SQL.png similarity index 100% rename from assets/icons/azure/databases/02390-icon-service-Azure-SQL.png rename to core/assets/icons/azure/databases/02390-icon-service-Azure-SQL.png diff --git a/assets/icons/azure/databases/02390-icon-service-Azure-SQL.svg b/core/assets/icons/azure/databases/02390-icon-service-Azure-SQL.svg similarity index 100% rename from assets/icons/azure/databases/02390-icon-service-Azure-SQL.svg rename to core/assets/icons/azure/databases/02390-icon-service-Azure-SQL.svg diff --git a/assets/icons/azure/databases/02392-icon-service-SSIS-Lift-And-Shift-IR.png b/core/assets/icons/azure/databases/02392-icon-service-SSIS-Lift-And-Shift-IR.png similarity index 100% rename from assets/icons/azure/databases/02392-icon-service-SSIS-Lift-And-Shift-IR.png rename to core/assets/icons/azure/databases/02392-icon-service-SSIS-Lift-And-Shift-IR.png diff --git a/assets/icons/azure/databases/02392-icon-service-SSIS-Lift-And-Shift-IR.svg b/core/assets/icons/azure/databases/02392-icon-service-SSIS-Lift-And-Shift-IR.svg similarity index 100% rename from assets/icons/azure/databases/02392-icon-service-SSIS-Lift-And-Shift-IR.svg rename to core/assets/icons/azure/databases/02392-icon-service-SSIS-Lift-And-Shift-IR.svg diff --git a/assets/icons/azure/databases/02517-icon-service-Azure-Purview-Accounts.png b/core/assets/icons/azure/databases/02517-icon-service-Azure-Purview-Accounts.png similarity index 100% rename from assets/icons/azure/databases/02517-icon-service-Azure-Purview-Accounts.png rename to core/assets/icons/azure/databases/02517-icon-service-Azure-Purview-Accounts.png diff --git a/assets/icons/azure/databases/02517-icon-service-Azure-Purview-Accounts.svg b/core/assets/icons/azure/databases/02517-icon-service-Azure-Purview-Accounts.svg similarity index 100% rename from assets/icons/azure/databases/02517-icon-service-Azure-Purview-Accounts.svg rename to core/assets/icons/azure/databases/02517-icon-service-Azure-Purview-Accounts.svg diff --git a/assets/icons/azure/databases/02750-icon-service-Azure-SQL-Edge.png b/core/assets/icons/azure/databases/02750-icon-service-Azure-SQL-Edge.png similarity index 100% rename from assets/icons/azure/databases/02750-icon-service-Azure-SQL-Edge.png rename to core/assets/icons/azure/databases/02750-icon-service-Azure-SQL-Edge.png diff --git a/assets/icons/azure/databases/02750-icon-service-Azure-SQL-Edge.svg b/core/assets/icons/azure/databases/02750-icon-service-Azure-SQL-Edge.svg similarity index 100% rename from assets/icons/azure/databases/02750-icon-service-Azure-SQL-Edge.svg rename to core/assets/icons/azure/databases/02750-icon-service-Azure-SQL-Edge.svg diff --git a/assets/icons/azure/databases/02827-icon-service-Azure-Database-PostgreSQL-Server-Group.png b/core/assets/icons/azure/databases/02827-icon-service-Azure-Database-PostgreSQL-Server-Group.png similarity index 100% rename from assets/icons/azure/databases/02827-icon-service-Azure-Database-PostgreSQL-Server-Group.png rename to core/assets/icons/azure/databases/02827-icon-service-Azure-Database-PostgreSQL-Server-Group.png diff --git a/assets/icons/azure/databases/02827-icon-service-Azure-Database-PostgreSQL-Server-Group.svg b/core/assets/icons/azure/databases/02827-icon-service-Azure-Database-PostgreSQL-Server-Group.svg similarity index 100% rename from assets/icons/azure/databases/02827-icon-service-Azure-Database-PostgreSQL-Server-Group.svg rename to core/assets/icons/azure/databases/02827-icon-service-Azure-Database-PostgreSQL-Server-Group.svg diff --git a/assets/icons/azure/databases/03490-icon-service-Oracle-Database.png b/core/assets/icons/azure/databases/03490-icon-service-Oracle-Database.png similarity index 100% rename from assets/icons/azure/databases/03490-icon-service-Oracle-Database.png rename to core/assets/icons/azure/databases/03490-icon-service-Oracle-Database.png diff --git a/assets/icons/azure/databases/03490-icon-service-Oracle-Database.svg b/core/assets/icons/azure/databases/03490-icon-service-Oracle-Database.svg similarity index 100% rename from assets/icons/azure/databases/03490-icon-service-Oracle-Database.svg rename to core/assets/icons/azure/databases/03490-icon-service-Oracle-Database.svg diff --git a/assets/icons/azure/databases/03565-icon-service-SQL-Database-Fleet-Manager.png b/core/assets/icons/azure/databases/03565-icon-service-SQL-Database-Fleet-Manager.png similarity index 100% rename from assets/icons/azure/databases/03565-icon-service-SQL-Database-Fleet-Manager.png rename to core/assets/icons/azure/databases/03565-icon-service-SQL-Database-Fleet-Manager.png diff --git a/assets/icons/azure/databases/03565-icon-service-SQL-Database-Fleet-Manager.svg b/core/assets/icons/azure/databases/03565-icon-service-SQL-Database-Fleet-Manager.svg similarity index 100% rename from assets/icons/azure/databases/03565-icon-service-SQL-Database-Fleet-Manager.svg rename to core/assets/icons/azure/databases/03565-icon-service-SQL-Database-Fleet-Manager.svg diff --git a/assets/icons/azure/databases/035656437-icon-service-Azure-DocumentDB.png b/core/assets/icons/azure/databases/035656437-icon-service-Azure-DocumentDB.png similarity index 100% rename from assets/icons/azure/databases/035656437-icon-service-Azure-DocumentDB.png rename to core/assets/icons/azure/databases/035656437-icon-service-Azure-DocumentDB.png diff --git a/assets/icons/azure/databases/035656437-icon-service-Azure-DocumentDB.svg b/core/assets/icons/azure/databases/035656437-icon-service-Azure-DocumentDB.svg similarity index 100% rename from assets/icons/azure/databases/035656437-icon-service-Azure-DocumentDB.svg rename to core/assets/icons/azure/databases/035656437-icon-service-Azure-DocumentDB.svg diff --git a/assets/icons/azure/databases/03675-icon-service-Azure-Managed-Redis.png b/core/assets/icons/azure/databases/03675-icon-service-Azure-Managed-Redis.png similarity index 100% rename from assets/icons/azure/databases/03675-icon-service-Azure-Managed-Redis.png rename to core/assets/icons/azure/databases/03675-icon-service-Azure-Managed-Redis.png diff --git a/assets/icons/azure/databases/03675-icon-service-Azure-Managed-Redis.svg b/core/assets/icons/azure/databases/03675-icon-service-Azure-Managed-Redis.svg similarity index 100% rename from assets/icons/azure/databases/03675-icon-service-Azure-Managed-Redis.svg rename to core/assets/icons/azure/databases/03675-icon-service-Azure-Managed-Redis.svg diff --git a/assets/icons/azure/databases/10121-icon-service-Azure-Cosmos-DB.png b/core/assets/icons/azure/databases/10121-icon-service-Azure-Cosmos-DB.png similarity index 100% rename from assets/icons/azure/databases/10121-icon-service-Azure-Cosmos-DB.png rename to core/assets/icons/azure/databases/10121-icon-service-Azure-Cosmos-DB.png diff --git a/assets/icons/azure/databases/10121-icon-service-Azure-Cosmos-DB.svg b/core/assets/icons/azure/databases/10121-icon-service-Azure-Cosmos-DB.svg similarity index 100% rename from assets/icons/azure/databases/10121-icon-service-Azure-Cosmos-DB.svg rename to core/assets/icons/azure/databases/10121-icon-service-Azure-Cosmos-DB.svg diff --git a/assets/icons/azure/databases/10122-icon-service-Azure-Database-MySQL-Server.png b/core/assets/icons/azure/databases/10122-icon-service-Azure-Database-MySQL-Server.png similarity index 100% rename from assets/icons/azure/databases/10122-icon-service-Azure-Database-MySQL-Server.png rename to core/assets/icons/azure/databases/10122-icon-service-Azure-Database-MySQL-Server.png diff --git a/assets/icons/azure/databases/10122-icon-service-Azure-Database-MySQL-Server.svg b/core/assets/icons/azure/databases/10122-icon-service-Azure-Database-MySQL-Server.svg similarity index 100% rename from assets/icons/azure/databases/10122-icon-service-Azure-Database-MySQL-Server.svg rename to core/assets/icons/azure/databases/10122-icon-service-Azure-Database-MySQL-Server.svg diff --git a/assets/icons/azure/databases/10123-icon-service-Azure-Database-MariaDB-Server.png b/core/assets/icons/azure/databases/10123-icon-service-Azure-Database-MariaDB-Server.png similarity index 100% rename from assets/icons/azure/databases/10123-icon-service-Azure-Database-MariaDB-Server.png rename to core/assets/icons/azure/databases/10123-icon-service-Azure-Database-MariaDB-Server.png diff --git a/assets/icons/azure/databases/10123-icon-service-Azure-Database-MariaDB-Server.svg b/core/assets/icons/azure/databases/10123-icon-service-Azure-Database-MariaDB-Server.svg similarity index 100% rename from assets/icons/azure/databases/10123-icon-service-Azure-Database-MariaDB-Server.svg rename to core/assets/icons/azure/databases/10123-icon-service-Azure-Database-MariaDB-Server.svg diff --git a/assets/icons/azure/databases/10124-icon-service-Azure-SQL-VM.png b/core/assets/icons/azure/databases/10124-icon-service-Azure-SQL-VM.png similarity index 100% rename from assets/icons/azure/databases/10124-icon-service-Azure-SQL-VM.png rename to core/assets/icons/azure/databases/10124-icon-service-Azure-SQL-VM.png diff --git a/assets/icons/azure/databases/10124-icon-service-Azure-SQL-VM.svg b/core/assets/icons/azure/databases/10124-icon-service-Azure-SQL-VM.svg similarity index 100% rename from assets/icons/azure/databases/10124-icon-service-Azure-SQL-VM.svg rename to core/assets/icons/azure/databases/10124-icon-service-Azure-SQL-VM.svg diff --git a/assets/icons/azure/databases/10126-icon-service-Data-Factories.png b/core/assets/icons/azure/databases/10126-icon-service-Data-Factories.png similarity index 100% rename from assets/icons/azure/databases/10126-icon-service-Data-Factories.png rename to core/assets/icons/azure/databases/10126-icon-service-Data-Factories.png diff --git a/assets/icons/azure/databases/10126-icon-service-Data-Factories.svg b/core/assets/icons/azure/databases/10126-icon-service-Data-Factories.svg similarity index 100% rename from assets/icons/azure/databases/10126-icon-service-Data-Factories.svg rename to core/assets/icons/azure/databases/10126-icon-service-Data-Factories.svg diff --git a/assets/icons/azure/databases/10127-icon-service-Virtual-Clusters.png b/core/assets/icons/azure/databases/10127-icon-service-Virtual-Clusters.png similarity index 100% rename from assets/icons/azure/databases/10127-icon-service-Virtual-Clusters.png rename to core/assets/icons/azure/databases/10127-icon-service-Virtual-Clusters.png diff --git a/assets/icons/azure/databases/10127-icon-service-Virtual-Clusters.svg b/core/assets/icons/azure/databases/10127-icon-service-Virtual-Clusters.svg similarity index 100% rename from assets/icons/azure/databases/10127-icon-service-Virtual-Clusters.svg rename to core/assets/icons/azure/databases/10127-icon-service-Virtual-Clusters.svg diff --git a/assets/icons/azure/databases/10128-icon-service-Elastic-Job-Agents.png b/core/assets/icons/azure/databases/10128-icon-service-Elastic-Job-Agents.png similarity index 100% rename from assets/icons/azure/databases/10128-icon-service-Elastic-Job-Agents.png rename to core/assets/icons/azure/databases/10128-icon-service-Elastic-Job-Agents.png diff --git a/assets/icons/azure/databases/10128-icon-service-Elastic-Job-Agents.svg b/core/assets/icons/azure/databases/10128-icon-service-Elastic-Job-Agents.svg similarity index 100% rename from assets/icons/azure/databases/10128-icon-service-Elastic-Job-Agents.svg rename to core/assets/icons/azure/databases/10128-icon-service-Elastic-Job-Agents.svg diff --git a/assets/icons/azure/databases/10130-icon-service-SQL-Database.png b/core/assets/icons/azure/databases/10130-icon-service-SQL-Database.png similarity index 100% rename from assets/icons/azure/databases/10130-icon-service-SQL-Database.png rename to core/assets/icons/azure/databases/10130-icon-service-SQL-Database.png diff --git a/assets/icons/azure/databases/10130-icon-service-SQL-Database.svg b/core/assets/icons/azure/databases/10130-icon-service-SQL-Database.svg similarity index 100% rename from assets/icons/azure/databases/10130-icon-service-SQL-Database.svg rename to core/assets/icons/azure/databases/10130-icon-service-SQL-Database.svg diff --git a/assets/icons/azure/databases/10131-icon-service-Azure-Database-PostgreSQL-Server.png b/core/assets/icons/azure/databases/10131-icon-service-Azure-Database-PostgreSQL-Server.png similarity index 100% rename from assets/icons/azure/databases/10131-icon-service-Azure-Database-PostgreSQL-Server.png rename to core/assets/icons/azure/databases/10131-icon-service-Azure-Database-PostgreSQL-Server.png diff --git a/assets/icons/azure/databases/10131-icon-service-Azure-Database-PostgreSQL-Server.svg b/core/assets/icons/azure/databases/10131-icon-service-Azure-Database-PostgreSQL-Server.svg similarity index 100% rename from assets/icons/azure/databases/10131-icon-service-Azure-Database-PostgreSQL-Server.svg rename to core/assets/icons/azure/databases/10131-icon-service-Azure-Database-PostgreSQL-Server.svg diff --git a/assets/icons/azure/databases/10132-icon-service-SQL-Server.png b/core/assets/icons/azure/databases/10132-icon-service-SQL-Server.png similarity index 100% rename from assets/icons/azure/databases/10132-icon-service-SQL-Server.png rename to core/assets/icons/azure/databases/10132-icon-service-SQL-Server.png diff --git a/assets/icons/azure/databases/10132-icon-service-SQL-Server.svg b/core/assets/icons/azure/databases/10132-icon-service-SQL-Server.svg similarity index 100% rename from assets/icons/azure/databases/10132-icon-service-SQL-Server.svg rename to core/assets/icons/azure/databases/10132-icon-service-SQL-Server.svg diff --git a/assets/icons/azure/databases/10133-icon-service-Azure-Database-Migration-Services.png b/core/assets/icons/azure/databases/10133-icon-service-Azure-Database-Migration-Services.png similarity index 100% rename from assets/icons/azure/databases/10133-icon-service-Azure-Database-Migration-Services.png rename to core/assets/icons/azure/databases/10133-icon-service-Azure-Database-Migration-Services.png diff --git a/assets/icons/azure/databases/10133-icon-service-Azure-Database-Migration-Services.svg b/core/assets/icons/azure/databases/10133-icon-service-Azure-Database-Migration-Services.svg similarity index 100% rename from assets/icons/azure/databases/10133-icon-service-Azure-Database-Migration-Services.svg rename to core/assets/icons/azure/databases/10133-icon-service-Azure-Database-Migration-Services.svg diff --git a/assets/icons/azure/databases/10134-icon-service-SQL-Elastic-Pools.png b/core/assets/icons/azure/databases/10134-icon-service-SQL-Elastic-Pools.png similarity index 100% rename from assets/icons/azure/databases/10134-icon-service-SQL-Elastic-Pools.png rename to core/assets/icons/azure/databases/10134-icon-service-SQL-Elastic-Pools.png diff --git a/assets/icons/azure/databases/10134-icon-service-SQL-Elastic-Pools.svg b/core/assets/icons/azure/databases/10134-icon-service-SQL-Elastic-Pools.svg similarity index 100% rename from assets/icons/azure/databases/10134-icon-service-SQL-Elastic-Pools.svg rename to core/assets/icons/azure/databases/10134-icon-service-SQL-Elastic-Pools.svg diff --git a/assets/icons/azure/databases/10135-icon-service-Managed-Database.png b/core/assets/icons/azure/databases/10135-icon-service-Managed-Database.png similarity index 100% rename from assets/icons/azure/databases/10135-icon-service-Managed-Database.png rename to core/assets/icons/azure/databases/10135-icon-service-Managed-Database.png diff --git a/assets/icons/azure/databases/10135-icon-service-Managed-Database.svg b/core/assets/icons/azure/databases/10135-icon-service-Managed-Database.svg similarity index 100% rename from assets/icons/azure/databases/10135-icon-service-Managed-Database.svg rename to core/assets/icons/azure/databases/10135-icon-service-Managed-Database.svg diff --git a/assets/icons/azure/databases/10136-icon-service-SQL-Managed-Instance.png b/core/assets/icons/azure/databases/10136-icon-service-SQL-Managed-Instance.png similarity index 100% rename from assets/icons/azure/databases/10136-icon-service-SQL-Managed-Instance.png rename to core/assets/icons/azure/databases/10136-icon-service-SQL-Managed-Instance.png diff --git a/assets/icons/azure/databases/10136-icon-service-SQL-Managed-Instance.svg b/core/assets/icons/azure/databases/10136-icon-service-SQL-Managed-Instance.svg similarity index 100% rename from assets/icons/azure/databases/10136-icon-service-SQL-Managed-Instance.svg rename to core/assets/icons/azure/databases/10136-icon-service-SQL-Managed-Instance.svg diff --git a/assets/icons/azure/databases/10137-icon-service-Azure-SQL-Server-Stretch-Databases.png b/core/assets/icons/azure/databases/10137-icon-service-Azure-SQL-Server-Stretch-Databases.png similarity index 100% rename from assets/icons/azure/databases/10137-icon-service-Azure-SQL-Server-Stretch-Databases.png rename to core/assets/icons/azure/databases/10137-icon-service-Azure-SQL-Server-Stretch-Databases.png diff --git a/assets/icons/azure/databases/10137-icon-service-Azure-SQL-Server-Stretch-Databases.svg b/core/assets/icons/azure/databases/10137-icon-service-Azure-SQL-Server-Stretch-Databases.svg similarity index 100% rename from assets/icons/azure/databases/10137-icon-service-Azure-SQL-Server-Stretch-Databases.svg rename to core/assets/icons/azure/databases/10137-icon-service-Azure-SQL-Server-Stretch-Databases.svg diff --git a/assets/icons/azure/databases/10137-icon-service-Cache-Redis.png b/core/assets/icons/azure/databases/10137-icon-service-Cache-Redis.png similarity index 100% rename from assets/icons/azure/databases/10137-icon-service-Cache-Redis.png rename to core/assets/icons/azure/databases/10137-icon-service-Cache-Redis.png diff --git a/assets/icons/azure/databases/10137-icon-service-Cache-Redis.svg b/core/assets/icons/azure/databases/10137-icon-service-Cache-Redis.svg similarity index 100% rename from assets/icons/azure/databases/10137-icon-service-Cache-Redis.svg rename to core/assets/icons/azure/databases/10137-icon-service-Cache-Redis.svg diff --git a/assets/icons/azure/databases/10139-icon-service-Instance-Pools.png b/core/assets/icons/azure/databases/10139-icon-service-Instance-Pools.png similarity index 100% rename from assets/icons/azure/databases/10139-icon-service-Instance-Pools.png rename to core/assets/icons/azure/databases/10139-icon-service-Instance-Pools.png diff --git a/assets/icons/azure/databases/10139-icon-service-Instance-Pools.svg b/core/assets/icons/azure/databases/10139-icon-service-Instance-Pools.svg similarity index 100% rename from assets/icons/azure/databases/10139-icon-service-Instance-Pools.svg rename to core/assets/icons/azure/databases/10139-icon-service-Instance-Pools.svg diff --git a/assets/icons/azure/databases/10145-icon-service-Azure-Data-Explorer-Clusters.png b/core/assets/icons/azure/databases/10145-icon-service-Azure-Data-Explorer-Clusters.png similarity index 100% rename from assets/icons/azure/databases/10145-icon-service-Azure-Data-Explorer-Clusters.png rename to core/assets/icons/azure/databases/10145-icon-service-Azure-Data-Explorer-Clusters.png diff --git a/assets/icons/azure/databases/10145-icon-service-Azure-Data-Explorer-Clusters.svg b/core/assets/icons/azure/databases/10145-icon-service-Azure-Data-Explorer-Clusters.svg similarity index 100% rename from assets/icons/azure/databases/10145-icon-service-Azure-Data-Explorer-Clusters.svg rename to core/assets/icons/azure/databases/10145-icon-service-Azure-Data-Explorer-Clusters.svg diff --git a/assets/icons/azure/databases/10351-icon-service-SQL-Server-Registries.png b/core/assets/icons/azure/databases/10351-icon-service-SQL-Server-Registries.png similarity index 100% rename from assets/icons/azure/databases/10351-icon-service-SQL-Server-Registries.png rename to core/assets/icons/azure/databases/10351-icon-service-SQL-Server-Registries.png diff --git a/assets/icons/azure/databases/10351-icon-service-SQL-Server-Registries.svg b/core/assets/icons/azure/databases/10351-icon-service-SQL-Server-Registries.svg similarity index 100% rename from assets/icons/azure/databases/10351-icon-service-SQL-Server-Registries.svg rename to core/assets/icons/azure/databases/10351-icon-service-SQL-Server-Registries.svg diff --git a/assets/icons/azure/developer-tools/033805167-icon-service-Azure-App-Testing.png b/core/assets/icons/azure/developer-tools/033805167-icon-service-Azure-App-Testing.png similarity index 100% rename from assets/icons/azure/developer-tools/033805167-icon-service-Azure-App-Testing.png rename to core/assets/icons/azure/developer-tools/033805167-icon-service-Azure-App-Testing.png diff --git a/assets/icons/azure/developer-tools/033805167-icon-service-Azure-App-Testing.svg b/core/assets/icons/azure/developer-tools/033805167-icon-service-Azure-App-Testing.svg similarity index 100% rename from assets/icons/azure/developer-tools/033805167-icon-service-Azure-App-Testing.svg rename to core/assets/icons/azure/developer-tools/033805167-icon-service-Azure-App-Testing.svg diff --git a/assets/icons/azure/devops/00012-icon-service-Application-Insights.png b/core/assets/icons/azure/devops/00012-icon-service-Application-Insights.png similarity index 100% rename from assets/icons/azure/devops/00012-icon-service-Application-Insights.png rename to core/assets/icons/azure/devops/00012-icon-service-Application-Insights.png diff --git a/assets/icons/azure/devops/00012-icon-service-Application-Insights.svg b/core/assets/icons/azure/devops/00012-icon-service-Application-Insights.svg similarity index 100% rename from assets/icons/azure/devops/00012-icon-service-Application-Insights.svg rename to core/assets/icons/azure/devops/00012-icon-service-Application-Insights.svg diff --git a/assets/icons/azure/devops/00563-icon-service-Change-Analysis.png b/core/assets/icons/azure/devops/00563-icon-service-Change-Analysis.png similarity index 100% rename from assets/icons/azure/devops/00563-icon-service-Change-Analysis.png rename to core/assets/icons/azure/devops/00563-icon-service-Change-Analysis.png diff --git a/assets/icons/azure/devops/00563-icon-service-Change-Analysis.svg b/core/assets/icons/azure/devops/00563-icon-service-Change-Analysis.svg similarity index 100% rename from assets/icons/azure/devops/00563-icon-service-Change-Analysis.svg rename to core/assets/icons/azure/devops/00563-icon-service-Change-Analysis.svg diff --git a/assets/icons/azure/devops/02373-icon-service-CloudTest.png b/core/assets/icons/azure/devops/02373-icon-service-CloudTest.png similarity index 100% rename from assets/icons/azure/devops/02373-icon-service-CloudTest.png rename to core/assets/icons/azure/devops/02373-icon-service-CloudTest.png diff --git a/assets/icons/azure/devops/02373-icon-service-CloudTest.svg b/core/assets/icons/azure/devops/02373-icon-service-CloudTest.svg similarity index 100% rename from assets/icons/azure/devops/02373-icon-service-CloudTest.svg rename to core/assets/icons/azure/devops/02373-icon-service-CloudTest.svg diff --git a/assets/icons/azure/devops/02423-icon-service-Load-Testing.png b/core/assets/icons/azure/devops/02423-icon-service-Load-Testing.png similarity index 100% rename from assets/icons/azure/devops/02423-icon-service-Load-Testing.png rename to core/assets/icons/azure/devops/02423-icon-service-Load-Testing.png diff --git a/assets/icons/azure/devops/02423-icon-service-Load-Testing.svg b/core/assets/icons/azure/devops/02423-icon-service-Load-Testing.svg similarity index 100% rename from assets/icons/azure/devops/02423-icon-service-Load-Testing.svg rename to core/assets/icons/azure/devops/02423-icon-service-Load-Testing.svg diff --git a/assets/icons/azure/devops/02761-icon-service-Lab-Accounts.png b/core/assets/icons/azure/devops/02761-icon-service-Lab-Accounts.png similarity index 100% rename from assets/icons/azure/devops/02761-icon-service-Lab-Accounts.png rename to core/assets/icons/azure/devops/02761-icon-service-Lab-Accounts.png diff --git a/assets/icons/azure/devops/02761-icon-service-Lab-Accounts.svg b/core/assets/icons/azure/devops/02761-icon-service-Lab-Accounts.svg similarity index 100% rename from assets/icons/azure/devops/02761-icon-service-Lab-Accounts.svg rename to core/assets/icons/azure/devops/02761-icon-service-Lab-Accounts.svg diff --git a/assets/icons/azure/devops/03339-icon-service-DevOps-Starter.png b/core/assets/icons/azure/devops/03339-icon-service-DevOps-Starter.png similarity index 100% rename from assets/icons/azure/devops/03339-icon-service-DevOps-Starter.png rename to core/assets/icons/azure/devops/03339-icon-service-DevOps-Starter.png diff --git a/assets/icons/azure/devops/03339-icon-service-DevOps-Starter.svg b/core/assets/icons/azure/devops/03339-icon-service-DevOps-Starter.svg similarity index 100% rename from assets/icons/azure/devops/03339-icon-service-DevOps-Starter.svg rename to core/assets/icons/azure/devops/03339-icon-service-DevOps-Starter.svg diff --git a/assets/icons/azure/devops/03393-icon-service-Managed-DevOps-Pools.png b/core/assets/icons/azure/devops/03393-icon-service-Managed-DevOps-Pools.png similarity index 100% rename from assets/icons/azure/devops/03393-icon-service-Managed-DevOps-Pools.png rename to core/assets/icons/azure/devops/03393-icon-service-Managed-DevOps-Pools.png diff --git a/assets/icons/azure/devops/03393-icon-service-Managed-DevOps-Pools.svg b/core/assets/icons/azure/devops/03393-icon-service-Managed-DevOps-Pools.svg similarity index 100% rename from assets/icons/azure/devops/03393-icon-service-Managed-DevOps-Pools.svg rename to core/assets/icons/azure/devops/03393-icon-service-Managed-DevOps-Pools.svg diff --git a/assets/icons/azure/devops/03455-icon-service-Code-Optimization.png b/core/assets/icons/azure/devops/03455-icon-service-Code-Optimization.png similarity index 100% rename from assets/icons/azure/devops/03455-icon-service-Code-Optimization.png rename to core/assets/icons/azure/devops/03455-icon-service-Code-Optimization.png diff --git a/assets/icons/azure/devops/03455-icon-service-Code-Optimization.svg b/core/assets/icons/azure/devops/03455-icon-service-Code-Optimization.svg similarity index 100% rename from assets/icons/azure/devops/03455-icon-service-Code-Optimization.svg rename to core/assets/icons/azure/devops/03455-icon-service-Code-Optimization.svg diff --git a/assets/icons/azure/devops/03623-icon-service-Workspace-Gateway.png b/core/assets/icons/azure/devops/03623-icon-service-Workspace-Gateway.png similarity index 100% rename from assets/icons/azure/devops/03623-icon-service-Workspace-Gateway.png rename to core/assets/icons/azure/devops/03623-icon-service-Workspace-Gateway.png diff --git a/assets/icons/azure/devops/03623-icon-service-Workspace-Gateway.svg b/core/assets/icons/azure/devops/03623-icon-service-Workspace-Gateway.svg similarity index 100% rename from assets/icons/azure/devops/03623-icon-service-Workspace-Gateway.svg rename to core/assets/icons/azure/devops/03623-icon-service-Workspace-Gateway.svg diff --git a/assets/icons/azure/devops/10042-icon-service-API-Management-Services.png b/core/assets/icons/azure/devops/10042-icon-service-API-Management-Services.png similarity index 100% rename from assets/icons/azure/devops/10042-icon-service-API-Management-Services.png rename to core/assets/icons/azure/devops/10042-icon-service-API-Management-Services.png diff --git a/assets/icons/azure/devops/10042-icon-service-API-Management-Services.svg b/core/assets/icons/azure/devops/10042-icon-service-API-Management-Services.svg similarity index 100% rename from assets/icons/azure/devops/10042-icon-service-API-Management-Services.svg rename to core/assets/icons/azure/devops/10042-icon-service-API-Management-Services.svg diff --git a/assets/icons/azure/devops/10048-icon-service-API-Connections.png b/core/assets/icons/azure/devops/10048-icon-service-API-Connections.png similarity index 100% rename from assets/icons/azure/devops/10048-icon-service-API-Connections.png rename to core/assets/icons/azure/devops/10048-icon-service-API-Connections.png diff --git a/assets/icons/azure/devops/10048-icon-service-API-Connections.svg b/core/assets/icons/azure/devops/10048-icon-service-API-Connections.svg similarity index 100% rename from assets/icons/azure/devops/10048-icon-service-API-Connections.svg rename to core/assets/icons/azure/devops/10048-icon-service-API-Connections.svg diff --git a/assets/icons/azure/devops/10261-icon-service-Azure-DevOps.png b/core/assets/icons/azure/devops/10261-icon-service-Azure-DevOps.png similarity index 100% rename from assets/icons/azure/devops/10261-icon-service-Azure-DevOps.png rename to core/assets/icons/azure/devops/10261-icon-service-Azure-DevOps.png diff --git a/assets/icons/azure/devops/10261-icon-service-Azure-DevOps.svg b/core/assets/icons/azure/devops/10261-icon-service-Azure-DevOps.svg similarity index 100% rename from assets/icons/azure/devops/10261-icon-service-Azure-DevOps.svg rename to core/assets/icons/azure/devops/10261-icon-service-Azure-DevOps.svg diff --git a/assets/icons/azure/devops/10264-icon-service-DevTest-Labs.png b/core/assets/icons/azure/devops/10264-icon-service-DevTest-Labs.png similarity index 100% rename from assets/icons/azure/devops/10264-icon-service-DevTest-Labs.png rename to core/assets/icons/azure/devops/10264-icon-service-DevTest-Labs.png diff --git a/assets/icons/azure/devops/10264-icon-service-DevTest-Labs.svg b/core/assets/icons/azure/devops/10264-icon-service-DevTest-Labs.svg similarity index 100% rename from assets/icons/azure/devops/10264-icon-service-DevTest-Labs.svg rename to core/assets/icons/azure/devops/10264-icon-service-DevTest-Labs.svg diff --git a/assets/icons/azure/devops/10265-icon-service-Lab-Services.png b/core/assets/icons/azure/devops/10265-icon-service-Lab-Services.png similarity index 100% rename from assets/icons/azure/devops/10265-icon-service-Lab-Services.png rename to core/assets/icons/azure/devops/10265-icon-service-Lab-Services.png diff --git a/assets/icons/azure/devops/10265-icon-service-Lab-Services.svg b/core/assets/icons/azure/devops/10265-icon-service-Lab-Services.svg similarity index 100% rename from assets/icons/azure/devops/10265-icon-service-Lab-Services.svg rename to core/assets/icons/azure/devops/10265-icon-service-Lab-Services.svg diff --git a/assets/icons/azure/general/00004-icon-service-Cost-Management-and-Billing.png b/core/assets/icons/azure/general/00004-icon-service-Cost-Management-and-Billing.png similarity index 100% rename from assets/icons/azure/general/00004-icon-service-Cost-Management-and-Billing.png rename to core/assets/icons/azure/general/00004-icon-service-Cost-Management-and-Billing.png diff --git a/assets/icons/azure/general/00004-icon-service-Cost-Management-and-Billing.svg b/core/assets/icons/azure/general/00004-icon-service-Cost-Management-and-Billing.svg similarity index 100% rename from assets/icons/azure/general/00004-icon-service-Cost-Management-and-Billing.svg rename to core/assets/icons/azure/general/00004-icon-service-Cost-Management-and-Billing.svg diff --git a/assets/icons/azure/general/00456-icon-service-Preview-Features.png b/core/assets/icons/azure/general/00456-icon-service-Preview-Features.png similarity index 100% rename from assets/icons/azure/general/00456-icon-service-Preview-Features.png rename to core/assets/icons/azure/general/00456-icon-service-Preview-Features.png diff --git a/assets/icons/azure/general/00456-icon-service-Preview-Features.svg b/core/assets/icons/azure/general/00456-icon-service-Preview-Features.svg similarity index 100% rename from assets/icons/azure/general/00456-icon-service-Preview-Features.svg rename to core/assets/icons/azure/general/00456-icon-service-Preview-Features.svg diff --git a/assets/icons/azure/general/10001-icon-service-All-Resources.png b/core/assets/icons/azure/general/10001-icon-service-All-Resources.png similarity index 100% rename from assets/icons/azure/general/10001-icon-service-All-Resources.png rename to core/assets/icons/azure/general/10001-icon-service-All-Resources.png diff --git a/assets/icons/azure/general/10001-icon-service-All-Resources.svg b/core/assets/icons/azure/general/10001-icon-service-All-Resources.svg similarity index 100% rename from assets/icons/azure/general/10001-icon-service-All-Resources.svg rename to core/assets/icons/azure/general/10001-icon-service-All-Resources.svg diff --git a/assets/icons/azure/general/10002-icon-service-Subscriptions.png b/core/assets/icons/azure/general/10002-icon-service-Subscriptions.png similarity index 100% rename from assets/icons/azure/general/10002-icon-service-Subscriptions.png rename to core/assets/icons/azure/general/10002-icon-service-Subscriptions.png diff --git a/assets/icons/azure/general/10002-icon-service-Subscriptions.svg b/core/assets/icons/azure/general/10002-icon-service-Subscriptions.svg similarity index 100% rename from assets/icons/azure/general/10002-icon-service-Subscriptions.svg rename to core/assets/icons/azure/general/10002-icon-service-Subscriptions.svg diff --git a/assets/icons/azure/general/10003-icon-service-Reservations.png b/core/assets/icons/azure/general/10003-icon-service-Reservations.png similarity index 100% rename from assets/icons/azure/general/10003-icon-service-Reservations.png rename to core/assets/icons/azure/general/10003-icon-service-Reservations.png diff --git a/assets/icons/azure/general/10003-icon-service-Reservations.svg b/core/assets/icons/azure/general/10003-icon-service-Reservations.svg similarity index 100% rename from assets/icons/azure/general/10003-icon-service-Reservations.svg rename to core/assets/icons/azure/general/10003-icon-service-Reservations.svg diff --git a/assets/icons/azure/general/10004-icon-service-Service-Health.png b/core/assets/icons/azure/general/10004-icon-service-Service-Health.png similarity index 100% rename from assets/icons/azure/general/10004-icon-service-Service-Health.png rename to core/assets/icons/azure/general/10004-icon-service-Service-Health.png diff --git a/assets/icons/azure/general/10004-icon-service-Service-Health.svg b/core/assets/icons/azure/general/10004-icon-service-Service-Health.svg similarity index 100% rename from assets/icons/azure/general/10004-icon-service-Service-Health.svg rename to core/assets/icons/azure/general/10004-icon-service-Service-Health.svg diff --git a/assets/icons/azure/general/10005-icon-service-Information.png b/core/assets/icons/azure/general/10005-icon-service-Information.png similarity index 100% rename from assets/icons/azure/general/10005-icon-service-Information.png rename to core/assets/icons/azure/general/10005-icon-service-Information.png diff --git a/assets/icons/azure/general/10005-icon-service-Information.svg b/core/assets/icons/azure/general/10005-icon-service-Information.svg similarity index 100% rename from assets/icons/azure/general/10005-icon-service-Information.svg rename to core/assets/icons/azure/general/10005-icon-service-Information.svg diff --git a/assets/icons/azure/general/10006-icon-service-Recent.png b/core/assets/icons/azure/general/10006-icon-service-Recent.png similarity index 100% rename from assets/icons/azure/general/10006-icon-service-Recent.png rename to core/assets/icons/azure/general/10006-icon-service-Recent.png diff --git a/assets/icons/azure/general/10006-icon-service-Recent.svg b/core/assets/icons/azure/general/10006-icon-service-Recent.svg similarity index 100% rename from assets/icons/azure/general/10006-icon-service-Recent.svg rename to core/assets/icons/azure/general/10006-icon-service-Recent.svg diff --git a/assets/icons/azure/general/10007-icon-service-Resource-Groups.png b/core/assets/icons/azure/general/10007-icon-service-Resource-Groups.png similarity index 100% rename from assets/icons/azure/general/10007-icon-service-Resource-Groups.png rename to core/assets/icons/azure/general/10007-icon-service-Resource-Groups.png diff --git a/assets/icons/azure/general/10007-icon-service-Resource-Groups.svg b/core/assets/icons/azure/general/10007-icon-service-Resource-Groups.svg similarity index 100% rename from assets/icons/azure/general/10007-icon-service-Resource-Groups.svg rename to core/assets/icons/azure/general/10007-icon-service-Resource-Groups.svg diff --git a/assets/icons/azure/general/10008-icon-service-Marketplace.png b/core/assets/icons/azure/general/10008-icon-service-Marketplace.png similarity index 100% rename from assets/icons/azure/general/10008-icon-service-Marketplace.png rename to core/assets/icons/azure/general/10008-icon-service-Marketplace.png diff --git a/assets/icons/azure/general/10008-icon-service-Marketplace.svg b/core/assets/icons/azure/general/10008-icon-service-Marketplace.svg similarity index 100% rename from assets/icons/azure/general/10008-icon-service-Marketplace.svg rename to core/assets/icons/azure/general/10008-icon-service-Marketplace.svg diff --git a/assets/icons/azure/general/10009-icon-service-Templates.png b/core/assets/icons/azure/general/10009-icon-service-Templates.png similarity index 100% rename from assets/icons/azure/general/10009-icon-service-Templates.png rename to core/assets/icons/azure/general/10009-icon-service-Templates.png diff --git a/assets/icons/azure/general/10009-icon-service-Templates.svg b/core/assets/icons/azure/general/10009-icon-service-Templates.svg similarity index 100% rename from assets/icons/azure/general/10009-icon-service-Templates.svg rename to core/assets/icons/azure/general/10009-icon-service-Templates.svg diff --git a/assets/icons/azure/general/10010-icon-service-Quickstart-Center.png b/core/assets/icons/azure/general/10010-icon-service-Quickstart-Center.png similarity index 100% rename from assets/icons/azure/general/10010-icon-service-Quickstart-Center.png rename to core/assets/icons/azure/general/10010-icon-service-Quickstart-Center.png diff --git a/assets/icons/azure/general/10010-icon-service-Quickstart-Center.svg b/core/assets/icons/azure/general/10010-icon-service-Quickstart-Center.svg similarity index 100% rename from assets/icons/azure/general/10010-icon-service-Quickstart-Center.svg rename to core/assets/icons/azure/general/10010-icon-service-Quickstart-Center.svg diff --git a/assets/icons/azure/general/10011-icon-service-Management-Groups.png b/core/assets/icons/azure/general/10011-icon-service-Management-Groups.png similarity index 100% rename from assets/icons/azure/general/10011-icon-service-Management-Groups.png rename to core/assets/icons/azure/general/10011-icon-service-Management-Groups.png diff --git a/assets/icons/azure/general/10011-icon-service-Management-Groups.svg b/core/assets/icons/azure/general/10011-icon-service-Management-Groups.svg similarity index 100% rename from assets/icons/azure/general/10011-icon-service-Management-Groups.svg rename to core/assets/icons/azure/general/10011-icon-service-Management-Groups.svg diff --git a/assets/icons/azure/general/10013-icon-service-Help-and-Support.png b/core/assets/icons/azure/general/10013-icon-service-Help-and-Support.png similarity index 100% rename from assets/icons/azure/general/10013-icon-service-Help-and-Support.png rename to core/assets/icons/azure/general/10013-icon-service-Help-and-Support.png diff --git a/assets/icons/azure/general/10013-icon-service-Help-and-Support.svg b/core/assets/icons/azure/general/10013-icon-service-Help-and-Support.svg similarity index 100% rename from assets/icons/azure/general/10013-icon-service-Help-and-Support.svg rename to core/assets/icons/azure/general/10013-icon-service-Help-and-Support.svg diff --git a/assets/icons/azure/general/10014-icon-service-Tag.png b/core/assets/icons/azure/general/10014-icon-service-Tag.png similarity index 100% rename from assets/icons/azure/general/10014-icon-service-Tag.png rename to core/assets/icons/azure/general/10014-icon-service-Tag.png diff --git a/assets/icons/azure/general/10014-icon-service-Tag.svg b/core/assets/icons/azure/general/10014-icon-service-Tag.svg similarity index 100% rename from assets/icons/azure/general/10014-icon-service-Tag.svg rename to core/assets/icons/azure/general/10014-icon-service-Tag.svg diff --git a/assets/icons/azure/general/10015-icon-service-Dashboard.png b/core/assets/icons/azure/general/10015-icon-service-Dashboard.png similarity index 100% rename from assets/icons/azure/general/10015-icon-service-Dashboard.png rename to core/assets/icons/azure/general/10015-icon-service-Dashboard.png diff --git a/assets/icons/azure/general/10015-icon-service-Dashboard.svg b/core/assets/icons/azure/general/10015-icon-service-Dashboard.svg similarity index 100% rename from assets/icons/azure/general/10015-icon-service-Dashboard.svg rename to core/assets/icons/azure/general/10015-icon-service-Dashboard.svg diff --git a/assets/icons/azure/general/10016-icon-service-Free-Services.png b/core/assets/icons/azure/general/10016-icon-service-Free-Services.png similarity index 100% rename from assets/icons/azure/general/10016-icon-service-Free-Services.png rename to core/assets/icons/azure/general/10016-icon-service-Free-Services.png diff --git a/assets/icons/azure/general/10016-icon-service-Free-Services.svg b/core/assets/icons/azure/general/10016-icon-service-Free-Services.svg similarity index 100% rename from assets/icons/azure/general/10016-icon-service-Free-Services.svg rename to core/assets/icons/azure/general/10016-icon-service-Free-Services.svg diff --git a/assets/icons/azure/general/10019-icon-service-Cost-Management.png b/core/assets/icons/azure/general/10019-icon-service-Cost-Management.png similarity index 100% rename from assets/icons/azure/general/10019-icon-service-Cost-Management.png rename to core/assets/icons/azure/general/10019-icon-service-Cost-Management.png diff --git a/assets/icons/azure/general/10019-icon-service-Cost-Management.svg b/core/assets/icons/azure/general/10019-icon-service-Cost-Management.svg similarity index 100% rename from assets/icons/azure/general/10019-icon-service-Cost-Management.svg rename to core/assets/icons/azure/general/10019-icon-service-Cost-Management.svg diff --git a/assets/icons/azure/general/10112-icon-service-Marketplace-Management.png b/core/assets/icons/azure/general/10112-icon-service-Marketplace-Management.png similarity index 100% rename from assets/icons/azure/general/10112-icon-service-Marketplace-Management.png rename to core/assets/icons/azure/general/10112-icon-service-Marketplace-Management.png diff --git a/assets/icons/azure/general/10112-icon-service-Marketplace-Management.svg b/core/assets/icons/azure/general/10112-icon-service-Marketplace-Management.svg similarity index 100% rename from assets/icons/azure/general/10112-icon-service-Marketplace-Management.svg rename to core/assets/icons/azure/general/10112-icon-service-Marketplace-Management.svg diff --git a/assets/icons/azure/general/10116-icon-service-Region-Management.png b/core/assets/icons/azure/general/10116-icon-service-Region-Management.png similarity index 100% rename from assets/icons/azure/general/10116-icon-service-Region-Management.png rename to core/assets/icons/azure/general/10116-icon-service-Region-Management.png diff --git a/assets/icons/azure/general/10116-icon-service-Region-Management.svg b/core/assets/icons/azure/general/10116-icon-service-Region-Management.svg similarity index 100% rename from assets/icons/azure/general/10116-icon-service-Region-Management.svg rename to core/assets/icons/azure/general/10116-icon-service-Region-Management.svg diff --git a/assets/icons/azure/general/10341-icon-service-Troubleshoot.png b/core/assets/icons/azure/general/10341-icon-service-Troubleshoot.png similarity index 100% rename from assets/icons/azure/general/10341-icon-service-Troubleshoot.png rename to core/assets/icons/azure/general/10341-icon-service-Troubleshoot.png diff --git a/assets/icons/azure/general/10341-icon-service-Troubleshoot.svg b/core/assets/icons/azure/general/10341-icon-service-Troubleshoot.svg similarity index 100% rename from assets/icons/azure/general/10341-icon-service-Troubleshoot.svg rename to core/assets/icons/azure/general/10341-icon-service-Troubleshoot.svg diff --git a/assets/icons/azure/general/10349-icon-service-Resource-Explorer.png b/core/assets/icons/azure/general/10349-icon-service-Resource-Explorer.png similarity index 100% rename from assets/icons/azure/general/10349-icon-service-Resource-Explorer.png rename to core/assets/icons/azure/general/10349-icon-service-Resource-Explorer.png diff --git a/assets/icons/azure/general/10349-icon-service-Resource-Explorer.svg b/core/assets/icons/azure/general/10349-icon-service-Resource-Explorer.svg similarity index 100% rename from assets/icons/azure/general/10349-icon-service-Resource-Explorer.svg rename to core/assets/icons/azure/general/10349-icon-service-Resource-Explorer.svg diff --git a/assets/icons/azure/general/10779-icon-service-Biz-Talk.png b/core/assets/icons/azure/general/10779-icon-service-Biz-Talk.png similarity index 100% rename from assets/icons/azure/general/10779-icon-service-Biz-Talk.png rename to core/assets/icons/azure/general/10779-icon-service-Biz-Talk.png diff --git a/assets/icons/azure/general/10779-icon-service-Biz-Talk.svg b/core/assets/icons/azure/general/10779-icon-service-Biz-Talk.svg similarity index 100% rename from assets/icons/azure/general/10779-icon-service-Biz-Talk.svg rename to core/assets/icons/azure/general/10779-icon-service-Biz-Talk.svg diff --git a/assets/icons/azure/general/10780-icon-service-Blob-Block.png b/core/assets/icons/azure/general/10780-icon-service-Blob-Block.png similarity index 100% rename from assets/icons/azure/general/10780-icon-service-Blob-Block.png rename to core/assets/icons/azure/general/10780-icon-service-Blob-Block.png diff --git a/assets/icons/azure/general/10780-icon-service-Blob-Block.svg b/core/assets/icons/azure/general/10780-icon-service-Blob-Block.svg similarity index 100% rename from assets/icons/azure/general/10780-icon-service-Blob-Block.svg rename to core/assets/icons/azure/general/10780-icon-service-Blob-Block.svg diff --git a/assets/icons/azure/general/10781-icon-service-Blob-Page.png b/core/assets/icons/azure/general/10781-icon-service-Blob-Page.png similarity index 100% rename from assets/icons/azure/general/10781-icon-service-Blob-Page.png rename to core/assets/icons/azure/general/10781-icon-service-Blob-Page.png diff --git a/assets/icons/azure/general/10781-icon-service-Blob-Page.svg b/core/assets/icons/azure/general/10781-icon-service-Blob-Page.svg similarity index 100% rename from assets/icons/azure/general/10781-icon-service-Blob-Page.svg rename to core/assets/icons/azure/general/10781-icon-service-Blob-Page.svg diff --git a/assets/icons/azure/general/10782-icon-service-Branch.png b/core/assets/icons/azure/general/10782-icon-service-Branch.png similarity index 100% rename from assets/icons/azure/general/10782-icon-service-Branch.png rename to core/assets/icons/azure/general/10782-icon-service-Branch.png diff --git a/assets/icons/azure/general/10782-icon-service-Branch.svg b/core/assets/icons/azure/general/10782-icon-service-Branch.svg similarity index 100% rename from assets/icons/azure/general/10782-icon-service-Branch.svg rename to core/assets/icons/azure/general/10782-icon-service-Branch.svg diff --git a/assets/icons/azure/general/10783-icon-service-Browser.png b/core/assets/icons/azure/general/10783-icon-service-Browser.png similarity index 100% rename from assets/icons/azure/general/10783-icon-service-Browser.png rename to core/assets/icons/azure/general/10783-icon-service-Browser.png diff --git a/assets/icons/azure/general/10783-icon-service-Browser.svg b/core/assets/icons/azure/general/10783-icon-service-Browser.svg similarity index 100% rename from assets/icons/azure/general/10783-icon-service-Browser.svg rename to core/assets/icons/azure/general/10783-icon-service-Browser.svg diff --git a/assets/icons/azure/general/10784-icon-service-Bug.png b/core/assets/icons/azure/general/10784-icon-service-Bug.png similarity index 100% rename from assets/icons/azure/general/10784-icon-service-Bug.png rename to core/assets/icons/azure/general/10784-icon-service-Bug.png diff --git a/assets/icons/azure/general/10784-icon-service-Bug.svg b/core/assets/icons/azure/general/10784-icon-service-Bug.svg similarity index 100% rename from assets/icons/azure/general/10784-icon-service-Bug.svg rename to core/assets/icons/azure/general/10784-icon-service-Bug.svg diff --git a/assets/icons/azure/general/10785-icon-service-Builds.png b/core/assets/icons/azure/general/10785-icon-service-Builds.png similarity index 100% rename from assets/icons/azure/general/10785-icon-service-Builds.png rename to core/assets/icons/azure/general/10785-icon-service-Builds.png diff --git a/assets/icons/azure/general/10785-icon-service-Builds.svg b/core/assets/icons/azure/general/10785-icon-service-Builds.svg similarity index 100% rename from assets/icons/azure/general/10785-icon-service-Builds.svg rename to core/assets/icons/azure/general/10785-icon-service-Builds.svg diff --git a/assets/icons/azure/general/10786-icon-service-Cache.png b/core/assets/icons/azure/general/10786-icon-service-Cache.png similarity index 100% rename from assets/icons/azure/general/10786-icon-service-Cache.png rename to core/assets/icons/azure/general/10786-icon-service-Cache.png diff --git a/assets/icons/azure/general/10786-icon-service-Cache.svg b/core/assets/icons/azure/general/10786-icon-service-Cache.svg similarity index 100% rename from assets/icons/azure/general/10786-icon-service-Cache.svg rename to core/assets/icons/azure/general/10786-icon-service-Cache.svg diff --git a/assets/icons/azure/general/10787-icon-service-Code.png b/core/assets/icons/azure/general/10787-icon-service-Code.png similarity index 100% rename from assets/icons/azure/general/10787-icon-service-Code.png rename to core/assets/icons/azure/general/10787-icon-service-Code.png diff --git a/assets/icons/azure/general/10787-icon-service-Code.svg b/core/assets/icons/azure/general/10787-icon-service-Code.svg similarity index 100% rename from assets/icons/azure/general/10787-icon-service-Code.svg rename to core/assets/icons/azure/general/10787-icon-service-Code.svg diff --git a/assets/icons/azure/general/10788-icon-service-Commit.png b/core/assets/icons/azure/general/10788-icon-service-Commit.png similarity index 100% rename from assets/icons/azure/general/10788-icon-service-Commit.png rename to core/assets/icons/azure/general/10788-icon-service-Commit.png diff --git a/assets/icons/azure/general/10788-icon-service-Commit.svg b/core/assets/icons/azure/general/10788-icon-service-Commit.svg similarity index 100% rename from assets/icons/azure/general/10788-icon-service-Commit.svg rename to core/assets/icons/azure/general/10788-icon-service-Commit.svg diff --git a/assets/icons/azure/general/10789-icon-service-Controls.png b/core/assets/icons/azure/general/10789-icon-service-Controls.png similarity index 100% rename from assets/icons/azure/general/10789-icon-service-Controls.png rename to core/assets/icons/azure/general/10789-icon-service-Controls.png diff --git a/assets/icons/azure/general/10789-icon-service-Controls.svg b/core/assets/icons/azure/general/10789-icon-service-Controls.svg similarity index 100% rename from assets/icons/azure/general/10789-icon-service-Controls.svg rename to core/assets/icons/azure/general/10789-icon-service-Controls.svg diff --git a/assets/icons/azure/general/10790-icon-service-Controls-Horizontal.png b/core/assets/icons/azure/general/10790-icon-service-Controls-Horizontal.png similarity index 100% rename from assets/icons/azure/general/10790-icon-service-Controls-Horizontal.png rename to core/assets/icons/azure/general/10790-icon-service-Controls-Horizontal.png diff --git a/assets/icons/azure/general/10790-icon-service-Controls-Horizontal.svg b/core/assets/icons/azure/general/10790-icon-service-Controls-Horizontal.svg similarity index 100% rename from assets/icons/azure/general/10790-icon-service-Controls-Horizontal.svg rename to core/assets/icons/azure/general/10790-icon-service-Controls-Horizontal.svg diff --git a/assets/icons/azure/general/10791-icon-service-Cost-Alerts.png b/core/assets/icons/azure/general/10791-icon-service-Cost-Alerts.png similarity index 100% rename from assets/icons/azure/general/10791-icon-service-Cost-Alerts.png rename to core/assets/icons/azure/general/10791-icon-service-Cost-Alerts.png diff --git a/assets/icons/azure/general/10791-icon-service-Cost-Alerts.svg b/core/assets/icons/azure/general/10791-icon-service-Cost-Alerts.svg similarity index 100% rename from assets/icons/azure/general/10791-icon-service-Cost-Alerts.svg rename to core/assets/icons/azure/general/10791-icon-service-Cost-Alerts.svg diff --git a/assets/icons/azure/general/10792-icon-service-Cost-Analysis.png b/core/assets/icons/azure/general/10792-icon-service-Cost-Analysis.png similarity index 100% rename from assets/icons/azure/general/10792-icon-service-Cost-Analysis.png rename to core/assets/icons/azure/general/10792-icon-service-Cost-Analysis.png diff --git a/assets/icons/azure/general/10792-icon-service-Cost-Analysis.svg b/core/assets/icons/azure/general/10792-icon-service-Cost-Analysis.svg similarity index 100% rename from assets/icons/azure/general/10792-icon-service-Cost-Analysis.svg rename to core/assets/icons/azure/general/10792-icon-service-Cost-Analysis.svg diff --git a/assets/icons/azure/general/10793-icon-service-Cost-Budgets.png b/core/assets/icons/azure/general/10793-icon-service-Cost-Budgets.png similarity index 100% rename from assets/icons/azure/general/10793-icon-service-Cost-Budgets.png rename to core/assets/icons/azure/general/10793-icon-service-Cost-Budgets.png diff --git a/assets/icons/azure/general/10793-icon-service-Cost-Budgets.svg b/core/assets/icons/azure/general/10793-icon-service-Cost-Budgets.svg similarity index 100% rename from assets/icons/azure/general/10793-icon-service-Cost-Budgets.svg rename to core/assets/icons/azure/general/10793-icon-service-Cost-Budgets.svg diff --git a/assets/icons/azure/general/10794-icon-service-Counter.png b/core/assets/icons/azure/general/10794-icon-service-Counter.png similarity index 100% rename from assets/icons/azure/general/10794-icon-service-Counter.png rename to core/assets/icons/azure/general/10794-icon-service-Counter.png diff --git a/assets/icons/azure/general/10794-icon-service-Counter.svg b/core/assets/icons/azure/general/10794-icon-service-Counter.svg similarity index 100% rename from assets/icons/azure/general/10794-icon-service-Counter.svg rename to core/assets/icons/azure/general/10794-icon-service-Counter.svg diff --git a/assets/icons/azure/general/10795-icon-service-Cubes.png b/core/assets/icons/azure/general/10795-icon-service-Cubes.png similarity index 100% rename from assets/icons/azure/general/10795-icon-service-Cubes.png rename to core/assets/icons/azure/general/10795-icon-service-Cubes.png diff --git a/assets/icons/azure/general/10795-icon-service-Cubes.svg b/core/assets/icons/azure/general/10795-icon-service-Cubes.svg similarity index 100% rename from assets/icons/azure/general/10795-icon-service-Cubes.svg rename to core/assets/icons/azure/general/10795-icon-service-Cubes.svg diff --git a/assets/icons/azure/general/10796-icon-service-Dev-Console.png b/core/assets/icons/azure/general/10796-icon-service-Dev-Console.png similarity index 100% rename from assets/icons/azure/general/10796-icon-service-Dev-Console.png rename to core/assets/icons/azure/general/10796-icon-service-Dev-Console.png diff --git a/assets/icons/azure/general/10796-icon-service-Dev-Console.svg b/core/assets/icons/azure/general/10796-icon-service-Dev-Console.svg similarity index 100% rename from assets/icons/azure/general/10796-icon-service-Dev-Console.svg rename to core/assets/icons/azure/general/10796-icon-service-Dev-Console.svg diff --git a/assets/icons/azure/general/10797-icon-service-Download.png b/core/assets/icons/azure/general/10797-icon-service-Download.png similarity index 100% rename from assets/icons/azure/general/10797-icon-service-Download.png rename to core/assets/icons/azure/general/10797-icon-service-Download.png diff --git a/assets/icons/azure/general/10797-icon-service-Download.svg b/core/assets/icons/azure/general/10797-icon-service-Download.svg similarity index 100% rename from assets/icons/azure/general/10797-icon-service-Download.svg rename to core/assets/icons/azure/general/10797-icon-service-Download.svg diff --git a/assets/icons/azure/general/10798-icon-service-Error.png b/core/assets/icons/azure/general/10798-icon-service-Error.png similarity index 100% rename from assets/icons/azure/general/10798-icon-service-Error.png rename to core/assets/icons/azure/general/10798-icon-service-Error.png diff --git a/assets/icons/azure/general/10798-icon-service-Error.svg b/core/assets/icons/azure/general/10798-icon-service-Error.svg similarity index 100% rename from assets/icons/azure/general/10798-icon-service-Error.svg rename to core/assets/icons/azure/general/10798-icon-service-Error.svg diff --git a/assets/icons/azure/general/10799-icon-service-Extensions.png b/core/assets/icons/azure/general/10799-icon-service-Extensions.png similarity index 100% rename from assets/icons/azure/general/10799-icon-service-Extensions.png rename to core/assets/icons/azure/general/10799-icon-service-Extensions.png diff --git a/assets/icons/azure/general/10799-icon-service-Extensions.svg b/core/assets/icons/azure/general/10799-icon-service-Extensions.svg similarity index 100% rename from assets/icons/azure/general/10799-icon-service-Extensions.svg rename to core/assets/icons/azure/general/10799-icon-service-Extensions.svg diff --git a/assets/icons/azure/general/10800-icon-service-File.png b/core/assets/icons/azure/general/10800-icon-service-File.png similarity index 100% rename from assets/icons/azure/general/10800-icon-service-File.png rename to core/assets/icons/azure/general/10800-icon-service-File.png diff --git a/assets/icons/azure/general/10800-icon-service-File.svg b/core/assets/icons/azure/general/10800-icon-service-File.svg similarity index 100% rename from assets/icons/azure/general/10800-icon-service-File.svg rename to core/assets/icons/azure/general/10800-icon-service-File.svg diff --git a/assets/icons/azure/general/10801-icon-service-Files.png b/core/assets/icons/azure/general/10801-icon-service-Files.png similarity index 100% rename from assets/icons/azure/general/10801-icon-service-Files.png rename to core/assets/icons/azure/general/10801-icon-service-Files.png diff --git a/assets/icons/azure/general/10801-icon-service-Files.svg b/core/assets/icons/azure/general/10801-icon-service-Files.svg similarity index 100% rename from assets/icons/azure/general/10801-icon-service-Files.svg rename to core/assets/icons/azure/general/10801-icon-service-Files.svg diff --git a/assets/icons/azure/general/10802-icon-service-Folder-Blank.png b/core/assets/icons/azure/general/10802-icon-service-Folder-Blank.png similarity index 100% rename from assets/icons/azure/general/10802-icon-service-Folder-Blank.png rename to core/assets/icons/azure/general/10802-icon-service-Folder-Blank.png diff --git a/assets/icons/azure/general/10802-icon-service-Folder-Blank.svg b/core/assets/icons/azure/general/10802-icon-service-Folder-Blank.svg similarity index 100% rename from assets/icons/azure/general/10802-icon-service-Folder-Blank.svg rename to core/assets/icons/azure/general/10802-icon-service-Folder-Blank.svg diff --git a/assets/icons/azure/general/10803-icon-service-Folder-Website.png b/core/assets/icons/azure/general/10803-icon-service-Folder-Website.png similarity index 100% rename from assets/icons/azure/general/10803-icon-service-Folder-Website.png rename to core/assets/icons/azure/general/10803-icon-service-Folder-Website.png diff --git a/assets/icons/azure/general/10803-icon-service-Folder-Website.svg b/core/assets/icons/azure/general/10803-icon-service-Folder-Website.svg similarity index 100% rename from assets/icons/azure/general/10803-icon-service-Folder-Website.svg rename to core/assets/icons/azure/general/10803-icon-service-Folder-Website.svg diff --git a/assets/icons/azure/general/10804-icon-service-FTP.png b/core/assets/icons/azure/general/10804-icon-service-FTP.png similarity index 100% rename from assets/icons/azure/general/10804-icon-service-FTP.png rename to core/assets/icons/azure/general/10804-icon-service-FTP.png diff --git a/assets/icons/azure/general/10804-icon-service-FTP.svg b/core/assets/icons/azure/general/10804-icon-service-FTP.svg similarity index 100% rename from assets/icons/azure/general/10804-icon-service-FTP.svg rename to core/assets/icons/azure/general/10804-icon-service-FTP.svg diff --git a/assets/icons/azure/general/10805-icon-service-Gear.png b/core/assets/icons/azure/general/10805-icon-service-Gear.png similarity index 100% rename from assets/icons/azure/general/10805-icon-service-Gear.png rename to core/assets/icons/azure/general/10805-icon-service-Gear.png diff --git a/assets/icons/azure/general/10805-icon-service-Gear.svg b/core/assets/icons/azure/general/10805-icon-service-Gear.svg similarity index 100% rename from assets/icons/azure/general/10805-icon-service-Gear.svg rename to core/assets/icons/azure/general/10805-icon-service-Gear.svg diff --git a/assets/icons/azure/general/10807-icon-service-Globe-Error.png b/core/assets/icons/azure/general/10807-icon-service-Globe-Error.png similarity index 100% rename from assets/icons/azure/general/10807-icon-service-Globe-Error.png rename to core/assets/icons/azure/general/10807-icon-service-Globe-Error.png diff --git a/assets/icons/azure/general/10807-icon-service-Globe-Error.svg b/core/assets/icons/azure/general/10807-icon-service-Globe-Error.svg similarity index 100% rename from assets/icons/azure/general/10807-icon-service-Globe-Error.svg rename to core/assets/icons/azure/general/10807-icon-service-Globe-Error.svg diff --git a/assets/icons/azure/general/10808-icon-service-Globe-Success.png b/core/assets/icons/azure/general/10808-icon-service-Globe-Success.png similarity index 100% rename from assets/icons/azure/general/10808-icon-service-Globe-Success.png rename to core/assets/icons/azure/general/10808-icon-service-Globe-Success.png diff --git a/assets/icons/azure/general/10808-icon-service-Globe-Success.svg b/core/assets/icons/azure/general/10808-icon-service-Globe-Success.svg similarity index 100% rename from assets/icons/azure/general/10808-icon-service-Globe-Success.svg rename to core/assets/icons/azure/general/10808-icon-service-Globe-Success.svg diff --git a/assets/icons/azure/general/10809-icon-service-Globe-Warning.png b/core/assets/icons/azure/general/10809-icon-service-Globe-Warning.png similarity index 100% rename from assets/icons/azure/general/10809-icon-service-Globe-Warning.png rename to core/assets/icons/azure/general/10809-icon-service-Globe-Warning.png diff --git a/assets/icons/azure/general/10809-icon-service-Globe-Warning.svg b/core/assets/icons/azure/general/10809-icon-service-Globe-Warning.svg similarity index 100% rename from assets/icons/azure/general/10809-icon-service-Globe-Warning.svg rename to core/assets/icons/azure/general/10809-icon-service-Globe-Warning.svg diff --git a/assets/icons/azure/general/10810-icon-service-Guide.png b/core/assets/icons/azure/general/10810-icon-service-Guide.png similarity index 100% rename from assets/icons/azure/general/10810-icon-service-Guide.png rename to core/assets/icons/azure/general/10810-icon-service-Guide.png diff --git a/assets/icons/azure/general/10810-icon-service-Guide.svg b/core/assets/icons/azure/general/10810-icon-service-Guide.svg similarity index 100% rename from assets/icons/azure/general/10810-icon-service-Guide.svg rename to core/assets/icons/azure/general/10810-icon-service-Guide.svg diff --git a/assets/icons/azure/general/10811-icon-service-Heart.png b/core/assets/icons/azure/general/10811-icon-service-Heart.png similarity index 100% rename from assets/icons/azure/general/10811-icon-service-Heart.png rename to core/assets/icons/azure/general/10811-icon-service-Heart.png diff --git a/assets/icons/azure/general/10811-icon-service-Heart.svg b/core/assets/icons/azure/general/10811-icon-service-Heart.svg similarity index 100% rename from assets/icons/azure/general/10811-icon-service-Heart.svg rename to core/assets/icons/azure/general/10811-icon-service-Heart.svg diff --git a/assets/icons/azure/general/10812-icon-service-Image.png b/core/assets/icons/azure/general/10812-icon-service-Image.png similarity index 100% rename from assets/icons/azure/general/10812-icon-service-Image.png rename to core/assets/icons/azure/general/10812-icon-service-Image.png diff --git a/assets/icons/azure/general/10812-icon-service-Image.svg b/core/assets/icons/azure/general/10812-icon-service-Image.svg similarity index 100% rename from assets/icons/azure/general/10812-icon-service-Image.svg rename to core/assets/icons/azure/general/10812-icon-service-Image.svg diff --git a/assets/icons/azure/general/10813-icon-service-Input-Output.png b/core/assets/icons/azure/general/10813-icon-service-Input-Output.png similarity index 100% rename from assets/icons/azure/general/10813-icon-service-Input-Output.png rename to core/assets/icons/azure/general/10813-icon-service-Input-Output.png diff --git a/assets/icons/azure/general/10813-icon-service-Input-Output.svg b/core/assets/icons/azure/general/10813-icon-service-Input-Output.svg similarity index 100% rename from assets/icons/azure/general/10813-icon-service-Input-Output.svg rename to core/assets/icons/azure/general/10813-icon-service-Input-Output.svg diff --git a/assets/icons/azure/general/10814-icon-service-Journey-Hub.png b/core/assets/icons/azure/general/10814-icon-service-Journey-Hub.png similarity index 100% rename from assets/icons/azure/general/10814-icon-service-Journey-Hub.png rename to core/assets/icons/azure/general/10814-icon-service-Journey-Hub.png diff --git a/assets/icons/azure/general/10814-icon-service-Journey-Hub.svg b/core/assets/icons/azure/general/10814-icon-service-Journey-Hub.svg similarity index 100% rename from assets/icons/azure/general/10814-icon-service-Journey-Hub.svg rename to core/assets/icons/azure/general/10814-icon-service-Journey-Hub.svg diff --git a/assets/icons/azure/general/10815-icon-service-Launch-Portal.png b/core/assets/icons/azure/general/10815-icon-service-Launch-Portal.png similarity index 100% rename from assets/icons/azure/general/10815-icon-service-Launch-Portal.png rename to core/assets/icons/azure/general/10815-icon-service-Launch-Portal.png diff --git a/assets/icons/azure/general/10815-icon-service-Launch-Portal.svg b/core/assets/icons/azure/general/10815-icon-service-Launch-Portal.svg similarity index 100% rename from assets/icons/azure/general/10815-icon-service-Launch-Portal.svg rename to core/assets/icons/azure/general/10815-icon-service-Launch-Portal.svg diff --git a/assets/icons/azure/general/10816-icon-service-Learn.png b/core/assets/icons/azure/general/10816-icon-service-Learn.png similarity index 100% rename from assets/icons/azure/general/10816-icon-service-Learn.png rename to core/assets/icons/azure/general/10816-icon-service-Learn.png diff --git a/assets/icons/azure/general/10816-icon-service-Learn.svg b/core/assets/icons/azure/general/10816-icon-service-Learn.svg similarity index 100% rename from assets/icons/azure/general/10816-icon-service-Learn.svg rename to core/assets/icons/azure/general/10816-icon-service-Learn.svg diff --git a/assets/icons/azure/general/10817-icon-service-Load-Test.png b/core/assets/icons/azure/general/10817-icon-service-Load-Test.png similarity index 100% rename from assets/icons/azure/general/10817-icon-service-Load-Test.png rename to core/assets/icons/azure/general/10817-icon-service-Load-Test.png diff --git a/assets/icons/azure/general/10817-icon-service-Load-Test.svg b/core/assets/icons/azure/general/10817-icon-service-Load-Test.svg similarity index 100% rename from assets/icons/azure/general/10817-icon-service-Load-Test.svg rename to core/assets/icons/azure/general/10817-icon-service-Load-Test.svg diff --git a/assets/icons/azure/general/10818-icon-service-Location.png b/core/assets/icons/azure/general/10818-icon-service-Location.png similarity index 100% rename from assets/icons/azure/general/10818-icon-service-Location.png rename to core/assets/icons/azure/general/10818-icon-service-Location.png diff --git a/assets/icons/azure/general/10818-icon-service-Location.svg b/core/assets/icons/azure/general/10818-icon-service-Location.svg similarity index 100% rename from assets/icons/azure/general/10818-icon-service-Location.svg rename to core/assets/icons/azure/general/10818-icon-service-Location.svg diff --git a/assets/icons/azure/general/10819-icon-service-Log-Streaming.png b/core/assets/icons/azure/general/10819-icon-service-Log-Streaming.png similarity index 100% rename from assets/icons/azure/general/10819-icon-service-Log-Streaming.png rename to core/assets/icons/azure/general/10819-icon-service-Log-Streaming.png diff --git a/assets/icons/azure/general/10819-icon-service-Log-Streaming.svg b/core/assets/icons/azure/general/10819-icon-service-Log-Streaming.svg similarity index 100% rename from assets/icons/azure/general/10819-icon-service-Log-Streaming.svg rename to core/assets/icons/azure/general/10819-icon-service-Log-Streaming.svg diff --git a/assets/icons/azure/general/10820-icon-service-Management-Portal.png b/core/assets/icons/azure/general/10820-icon-service-Management-Portal.png similarity index 100% rename from assets/icons/azure/general/10820-icon-service-Management-Portal.png rename to core/assets/icons/azure/general/10820-icon-service-Management-Portal.png diff --git a/assets/icons/azure/general/10820-icon-service-Management-Portal.svg b/core/assets/icons/azure/general/10820-icon-service-Management-Portal.svg similarity index 100% rename from assets/icons/azure/general/10820-icon-service-Management-Portal.svg rename to core/assets/icons/azure/general/10820-icon-service-Management-Portal.svg diff --git a/assets/icons/azure/general/10821-icon-service-Media-File.png b/core/assets/icons/azure/general/10821-icon-service-Media-File.png similarity index 100% rename from assets/icons/azure/general/10821-icon-service-Media-File.png rename to core/assets/icons/azure/general/10821-icon-service-Media-File.png diff --git a/assets/icons/azure/general/10821-icon-service-Media-File.svg b/core/assets/icons/azure/general/10821-icon-service-Media-File.svg similarity index 100% rename from assets/icons/azure/general/10821-icon-service-Media-File.svg rename to core/assets/icons/azure/general/10821-icon-service-Media-File.svg diff --git a/assets/icons/azure/general/10822-icon-service-Mobile.png b/core/assets/icons/azure/general/10822-icon-service-Mobile.png similarity index 100% rename from assets/icons/azure/general/10822-icon-service-Mobile.png rename to core/assets/icons/azure/general/10822-icon-service-Mobile.png diff --git a/assets/icons/azure/general/10822-icon-service-Mobile.svg b/core/assets/icons/azure/general/10822-icon-service-Mobile.svg similarity index 100% rename from assets/icons/azure/general/10822-icon-service-Mobile.svg rename to core/assets/icons/azure/general/10822-icon-service-Mobile.svg diff --git a/assets/icons/azure/general/10823-icon-service-Mobile-Engagement.png b/core/assets/icons/azure/general/10823-icon-service-Mobile-Engagement.png similarity index 100% rename from assets/icons/azure/general/10823-icon-service-Mobile-Engagement.png rename to core/assets/icons/azure/general/10823-icon-service-Mobile-Engagement.png diff --git a/assets/icons/azure/general/10823-icon-service-Mobile-Engagement.svg b/core/assets/icons/azure/general/10823-icon-service-Mobile-Engagement.svg similarity index 100% rename from assets/icons/azure/general/10823-icon-service-Mobile-Engagement.svg rename to core/assets/icons/azure/general/10823-icon-service-Mobile-Engagement.svg diff --git a/assets/icons/azure/general/10824-icon-service-Power.png b/core/assets/icons/azure/general/10824-icon-service-Power.png similarity index 100% rename from assets/icons/azure/general/10824-icon-service-Power.png rename to core/assets/icons/azure/general/10824-icon-service-Power.png diff --git a/assets/icons/azure/general/10824-icon-service-Power.svg b/core/assets/icons/azure/general/10824-icon-service-Power.svg similarity index 100% rename from assets/icons/azure/general/10824-icon-service-Power.svg rename to core/assets/icons/azure/general/10824-icon-service-Power.svg diff --git a/assets/icons/azure/general/10825-icon-service-Powershell.png b/core/assets/icons/azure/general/10825-icon-service-Powershell.png similarity index 100% rename from assets/icons/azure/general/10825-icon-service-Powershell.png rename to core/assets/icons/azure/general/10825-icon-service-Powershell.png diff --git a/assets/icons/azure/general/10825-icon-service-Powershell.svg b/core/assets/icons/azure/general/10825-icon-service-Powershell.svg similarity index 100% rename from assets/icons/azure/general/10825-icon-service-Powershell.svg rename to core/assets/icons/azure/general/10825-icon-service-Powershell.svg diff --git a/assets/icons/azure/general/10826-icon-service-Power-Up.png b/core/assets/icons/azure/general/10826-icon-service-Power-Up.png similarity index 100% rename from assets/icons/azure/general/10826-icon-service-Power-Up.png rename to core/assets/icons/azure/general/10826-icon-service-Power-Up.png diff --git a/assets/icons/azure/general/10826-icon-service-Power-Up.svg b/core/assets/icons/azure/general/10826-icon-service-Power-Up.svg similarity index 100% rename from assets/icons/azure/general/10826-icon-service-Power-Up.svg rename to core/assets/icons/azure/general/10826-icon-service-Power-Up.svg diff --git a/assets/icons/azure/general/10827-icon-service-Feature-Previews.png b/core/assets/icons/azure/general/10827-icon-service-Feature-Previews.png similarity index 100% rename from assets/icons/azure/general/10827-icon-service-Feature-Previews.png rename to core/assets/icons/azure/general/10827-icon-service-Feature-Previews.png diff --git a/assets/icons/azure/general/10827-icon-service-Feature-Previews.svg b/core/assets/icons/azure/general/10827-icon-service-Feature-Previews.svg similarity index 100% rename from assets/icons/azure/general/10827-icon-service-Feature-Previews.svg rename to core/assets/icons/azure/general/10827-icon-service-Feature-Previews.svg diff --git a/assets/icons/azure/general/10828-icon-service-Process-Explorer.png b/core/assets/icons/azure/general/10828-icon-service-Process-Explorer.png similarity index 100% rename from assets/icons/azure/general/10828-icon-service-Process-Explorer.png rename to core/assets/icons/azure/general/10828-icon-service-Process-Explorer.png diff --git a/assets/icons/azure/general/10828-icon-service-Process-Explorer.svg b/core/assets/icons/azure/general/10828-icon-service-Process-Explorer.svg similarity index 100% rename from assets/icons/azure/general/10828-icon-service-Process-Explorer.svg rename to core/assets/icons/azure/general/10828-icon-service-Process-Explorer.svg diff --git a/assets/icons/azure/general/10829-icon-service-Production-Ready-Database.png b/core/assets/icons/azure/general/10829-icon-service-Production-Ready-Database.png similarity index 100% rename from assets/icons/azure/general/10829-icon-service-Production-Ready-Database.png rename to core/assets/icons/azure/general/10829-icon-service-Production-Ready-Database.png diff --git a/assets/icons/azure/general/10829-icon-service-Production-Ready-Database.svg b/core/assets/icons/azure/general/10829-icon-service-Production-Ready-Database.svg similarity index 100% rename from assets/icons/azure/general/10829-icon-service-Production-Ready-Database.svg rename to core/assets/icons/azure/general/10829-icon-service-Production-Ready-Database.svg diff --git a/assets/icons/azure/general/10830-icon-service-Resource-Group-List.png b/core/assets/icons/azure/general/10830-icon-service-Resource-Group-List.png similarity index 100% rename from assets/icons/azure/general/10830-icon-service-Resource-Group-List.png rename to core/assets/icons/azure/general/10830-icon-service-Resource-Group-List.png diff --git a/assets/icons/azure/general/10830-icon-service-Resource-Group-List.svg b/core/assets/icons/azure/general/10830-icon-service-Resource-Group-List.svg similarity index 100% rename from assets/icons/azure/general/10830-icon-service-Resource-Group-List.svg rename to core/assets/icons/azure/general/10830-icon-service-Resource-Group-List.svg diff --git a/assets/icons/azure/general/10831-icon-service-Resource-Linked.png b/core/assets/icons/azure/general/10831-icon-service-Resource-Linked.png similarity index 100% rename from assets/icons/azure/general/10831-icon-service-Resource-Linked.png rename to core/assets/icons/azure/general/10831-icon-service-Resource-Linked.png diff --git a/assets/icons/azure/general/10831-icon-service-Resource-Linked.svg b/core/assets/icons/azure/general/10831-icon-service-Resource-Linked.svg similarity index 100% rename from assets/icons/azure/general/10831-icon-service-Resource-Linked.svg rename to core/assets/icons/azure/general/10831-icon-service-Resource-Linked.svg diff --git a/assets/icons/azure/general/10833-icon-service-Scheduler.png b/core/assets/icons/azure/general/10833-icon-service-Scheduler.png similarity index 100% rename from assets/icons/azure/general/10833-icon-service-Scheduler.png rename to core/assets/icons/azure/general/10833-icon-service-Scheduler.png diff --git a/assets/icons/azure/general/10833-icon-service-Scheduler.svg b/core/assets/icons/azure/general/10833-icon-service-Scheduler.svg similarity index 100% rename from assets/icons/azure/general/10833-icon-service-Scheduler.svg rename to core/assets/icons/azure/general/10833-icon-service-Scheduler.svg diff --git a/assets/icons/azure/general/10834-icon-service-Search.png b/core/assets/icons/azure/general/10834-icon-service-Search.png similarity index 100% rename from assets/icons/azure/general/10834-icon-service-Search.png rename to core/assets/icons/azure/general/10834-icon-service-Search.png diff --git a/assets/icons/azure/general/10834-icon-service-Search.svg b/core/assets/icons/azure/general/10834-icon-service-Search.svg similarity index 100% rename from assets/icons/azure/general/10834-icon-service-Search.svg rename to core/assets/icons/azure/general/10834-icon-service-Search.svg diff --git a/assets/icons/azure/general/10835-icon-service-Server-Farm.png b/core/assets/icons/azure/general/10835-icon-service-Server-Farm.png similarity index 100% rename from assets/icons/azure/general/10835-icon-service-Server-Farm.png rename to core/assets/icons/azure/general/10835-icon-service-Server-Farm.png diff --git a/assets/icons/azure/general/10835-icon-service-Server-Farm.svg b/core/assets/icons/azure/general/10835-icon-service-Server-Farm.svg similarity index 100% rename from assets/icons/azure/general/10835-icon-service-Server-Farm.svg rename to core/assets/icons/azure/general/10835-icon-service-Server-Farm.svg diff --git a/assets/icons/azure/general/10837-icon-service-SSD.png b/core/assets/icons/azure/general/10837-icon-service-SSD.png similarity index 100% rename from assets/icons/azure/general/10837-icon-service-SSD.png rename to core/assets/icons/azure/general/10837-icon-service-SSD.png diff --git a/assets/icons/azure/general/10837-icon-service-SSD.svg b/core/assets/icons/azure/general/10837-icon-service-SSD.svg similarity index 100% rename from assets/icons/azure/general/10837-icon-service-SSD.svg rename to core/assets/icons/azure/general/10837-icon-service-SSD.svg diff --git a/assets/icons/azure/general/10838-icon-service-Storage-Azure-Files.png b/core/assets/icons/azure/general/10838-icon-service-Storage-Azure-Files.png similarity index 100% rename from assets/icons/azure/general/10838-icon-service-Storage-Azure-Files.png rename to core/assets/icons/azure/general/10838-icon-service-Storage-Azure-Files.png diff --git a/assets/icons/azure/general/10838-icon-service-Storage-Azure-Files.svg b/core/assets/icons/azure/general/10838-icon-service-Storage-Azure-Files.svg similarity index 100% rename from assets/icons/azure/general/10838-icon-service-Storage-Azure-Files.svg rename to core/assets/icons/azure/general/10838-icon-service-Storage-Azure-Files.svg diff --git a/assets/icons/azure/general/10839-icon-service-Storage-Container.png b/core/assets/icons/azure/general/10839-icon-service-Storage-Container.png similarity index 100% rename from assets/icons/azure/general/10839-icon-service-Storage-Container.png rename to core/assets/icons/azure/general/10839-icon-service-Storage-Container.png diff --git a/assets/icons/azure/general/10839-icon-service-Storage-Container.svg b/core/assets/icons/azure/general/10839-icon-service-Storage-Container.svg similarity index 100% rename from assets/icons/azure/general/10839-icon-service-Storage-Container.svg rename to core/assets/icons/azure/general/10839-icon-service-Storage-Container.svg diff --git a/assets/icons/azure/general/10840-icon-service-Storage-Queue.png b/core/assets/icons/azure/general/10840-icon-service-Storage-Queue.png similarity index 100% rename from assets/icons/azure/general/10840-icon-service-Storage-Queue.png rename to core/assets/icons/azure/general/10840-icon-service-Storage-Queue.png diff --git a/assets/icons/azure/general/10840-icon-service-Storage-Queue.svg b/core/assets/icons/azure/general/10840-icon-service-Storage-Queue.svg similarity index 100% rename from assets/icons/azure/general/10840-icon-service-Storage-Queue.svg rename to core/assets/icons/azure/general/10840-icon-service-Storage-Queue.svg diff --git a/assets/icons/azure/general/10841-icon-service-Table.png b/core/assets/icons/azure/general/10841-icon-service-Table.png similarity index 100% rename from assets/icons/azure/general/10841-icon-service-Table.png rename to core/assets/icons/azure/general/10841-icon-service-Table.png diff --git a/assets/icons/azure/general/10841-icon-service-Table.svg b/core/assets/icons/azure/general/10841-icon-service-Table.svg similarity index 100% rename from assets/icons/azure/general/10841-icon-service-Table.svg rename to core/assets/icons/azure/general/10841-icon-service-Table.svg diff --git a/assets/icons/azure/general/10842-icon-service-Tags.png b/core/assets/icons/azure/general/10842-icon-service-Tags.png similarity index 100% rename from assets/icons/azure/general/10842-icon-service-Tags.png rename to core/assets/icons/azure/general/10842-icon-service-Tags.png diff --git a/assets/icons/azure/general/10842-icon-service-Tags.svg b/core/assets/icons/azure/general/10842-icon-service-Tags.svg similarity index 100% rename from assets/icons/azure/general/10842-icon-service-Tags.svg rename to core/assets/icons/azure/general/10842-icon-service-Tags.svg diff --git a/assets/icons/azure/general/10843-icon-service-TFS-VC-Repository.png b/core/assets/icons/azure/general/10843-icon-service-TFS-VC-Repository.png similarity index 100% rename from assets/icons/azure/general/10843-icon-service-TFS-VC-Repository.png rename to core/assets/icons/azure/general/10843-icon-service-TFS-VC-Repository.png diff --git a/assets/icons/azure/general/10843-icon-service-TFS-VC-Repository.svg b/core/assets/icons/azure/general/10843-icon-service-TFS-VC-Repository.svg similarity index 100% rename from assets/icons/azure/general/10843-icon-service-TFS-VC-Repository.svg rename to core/assets/icons/azure/general/10843-icon-service-TFS-VC-Repository.svg diff --git a/assets/icons/azure/general/10844-icon-service-Toolbox.png b/core/assets/icons/azure/general/10844-icon-service-Toolbox.png similarity index 100% rename from assets/icons/azure/general/10844-icon-service-Toolbox.png rename to core/assets/icons/azure/general/10844-icon-service-Toolbox.png diff --git a/assets/icons/azure/general/10844-icon-service-Toolbox.svg b/core/assets/icons/azure/general/10844-icon-service-Toolbox.svg similarity index 100% rename from assets/icons/azure/general/10844-icon-service-Toolbox.svg rename to core/assets/icons/azure/general/10844-icon-service-Toolbox.svg diff --git a/assets/icons/azure/general/10845-icon-service-Versions.png b/core/assets/icons/azure/general/10845-icon-service-Versions.png similarity index 100% rename from assets/icons/azure/general/10845-icon-service-Versions.png rename to core/assets/icons/azure/general/10845-icon-service-Versions.png diff --git a/assets/icons/azure/general/10845-icon-service-Versions.svg b/core/assets/icons/azure/general/10845-icon-service-Versions.svg similarity index 100% rename from assets/icons/azure/general/10845-icon-service-Versions.svg rename to core/assets/icons/azure/general/10845-icon-service-Versions.svg diff --git a/assets/icons/azure/general/10847-icon-service-Website-Power.png b/core/assets/icons/azure/general/10847-icon-service-Website-Power.png similarity index 100% rename from assets/icons/azure/general/10847-icon-service-Website-Power.png rename to core/assets/icons/azure/general/10847-icon-service-Website-Power.png diff --git a/assets/icons/azure/general/10847-icon-service-Website-Power.svg b/core/assets/icons/azure/general/10847-icon-service-Website-Power.svg similarity index 100% rename from assets/icons/azure/general/10847-icon-service-Website-Power.svg rename to core/assets/icons/azure/general/10847-icon-service-Website-Power.svg diff --git a/assets/icons/azure/general/10848-icon-service-Website-Staging.png b/core/assets/icons/azure/general/10848-icon-service-Website-Staging.png similarity index 100% rename from assets/icons/azure/general/10848-icon-service-Website-Staging.png rename to core/assets/icons/azure/general/10848-icon-service-Website-Staging.png diff --git a/assets/icons/azure/general/10848-icon-service-Website-Staging.svg b/core/assets/icons/azure/general/10848-icon-service-Website-Staging.svg similarity index 100% rename from assets/icons/azure/general/10848-icon-service-Website-Staging.svg rename to core/assets/icons/azure/general/10848-icon-service-Website-Staging.svg diff --git a/assets/icons/azure/general/10849-icon-service-Web-Slots.png b/core/assets/icons/azure/general/10849-icon-service-Web-Slots.png similarity index 100% rename from assets/icons/azure/general/10849-icon-service-Web-Slots.png rename to core/assets/icons/azure/general/10849-icon-service-Web-Slots.png diff --git a/assets/icons/azure/general/10849-icon-service-Web-Slots.svg b/core/assets/icons/azure/general/10849-icon-service-Web-Slots.svg similarity index 100% rename from assets/icons/azure/general/10849-icon-service-Web-Slots.svg rename to core/assets/icons/azure/general/10849-icon-service-Web-Slots.svg diff --git a/assets/icons/azure/general/10850-icon-service-Web-Test.png b/core/assets/icons/azure/general/10850-icon-service-Web-Test.png similarity index 100% rename from assets/icons/azure/general/10850-icon-service-Web-Test.png rename to core/assets/icons/azure/general/10850-icon-service-Web-Test.png diff --git a/assets/icons/azure/general/10850-icon-service-Web-Test.svg b/core/assets/icons/azure/general/10850-icon-service-Web-Test.svg similarity index 100% rename from assets/icons/azure/general/10850-icon-service-Web-Test.svg rename to core/assets/icons/azure/general/10850-icon-service-Web-Test.svg diff --git a/assets/icons/azure/general/10851-icon-service-Workbooks.png b/core/assets/icons/azure/general/10851-icon-service-Workbooks.png similarity index 100% rename from assets/icons/azure/general/10851-icon-service-Workbooks.png rename to core/assets/icons/azure/general/10851-icon-service-Workbooks.png diff --git a/assets/icons/azure/general/10851-icon-service-Workbooks.svg b/core/assets/icons/azure/general/10851-icon-service-Workbooks.svg similarity index 100% rename from assets/icons/azure/general/10851-icon-service-Workbooks.svg rename to core/assets/icons/azure/general/10851-icon-service-Workbooks.svg diff --git a/assets/icons/azure/general/10852-icon-service-Workflow.png b/core/assets/icons/azure/general/10852-icon-service-Workflow.png similarity index 100% rename from assets/icons/azure/general/10852-icon-service-Workflow.png rename to core/assets/icons/azure/general/10852-icon-service-Workflow.png diff --git a/assets/icons/azure/general/10852-icon-service-Workflow.svg b/core/assets/icons/azure/general/10852-icon-service-Workflow.svg similarity index 100% rename from assets/icons/azure/general/10852-icon-service-Workflow.svg rename to core/assets/icons/azure/general/10852-icon-service-Workflow.svg diff --git a/assets/icons/azure/general/10853-icon-service-Backlog.png b/core/assets/icons/azure/general/10853-icon-service-Backlog.png similarity index 100% rename from assets/icons/azure/general/10853-icon-service-Backlog.png rename to core/assets/icons/azure/general/10853-icon-service-Backlog.png diff --git a/assets/icons/azure/general/10853-icon-service-Backlog.svg b/core/assets/icons/azure/general/10853-icon-service-Backlog.svg similarity index 100% rename from assets/icons/azure/general/10853-icon-service-Backlog.svg rename to core/assets/icons/azure/general/10853-icon-service-Backlog.svg diff --git a/assets/icons/azure/general/10854-icon-service-Media.png b/core/assets/icons/azure/general/10854-icon-service-Media.png similarity index 100% rename from assets/icons/azure/general/10854-icon-service-Media.png rename to core/assets/icons/azure/general/10854-icon-service-Media.png diff --git a/assets/icons/azure/general/10854-icon-service-Media.svg b/core/assets/icons/azure/general/10854-icon-service-Media.svg similarity index 100% rename from assets/icons/azure/general/10854-icon-service-Media.svg rename to core/assets/icons/azure/general/10854-icon-service-Media.svg diff --git a/assets/icons/azure/general/10855-icon-service-Module.png b/core/assets/icons/azure/general/10855-icon-service-Module.png similarity index 100% rename from assets/icons/azure/general/10855-icon-service-Module.png rename to core/assets/icons/azure/general/10855-icon-service-Module.png diff --git a/assets/icons/azure/general/10855-icon-service-Module.svg b/core/assets/icons/azure/general/10855-icon-service-Module.svg similarity index 100% rename from assets/icons/azure/general/10855-icon-service-Module.svg rename to core/assets/icons/azure/general/10855-icon-service-Module.svg diff --git a/assets/icons/azure/general/10856-icon-service-Search-Grid.png b/core/assets/icons/azure/general/10856-icon-service-Search-Grid.png similarity index 100% rename from assets/icons/azure/general/10856-icon-service-Search-Grid.png rename to core/assets/icons/azure/general/10856-icon-service-Search-Grid.png diff --git a/assets/icons/azure/general/10856-icon-service-Search-Grid.svg b/core/assets/icons/azure/general/10856-icon-service-Search-Grid.svg similarity index 100% rename from assets/icons/azure/general/10856-icon-service-Search-Grid.svg rename to core/assets/icons/azure/general/10856-icon-service-Search-Grid.svg diff --git a/assets/icons/azure/hybrid-multicloud/029109619-icon-service-Workload-Orchestration.png b/core/assets/icons/azure/hybrid-multicloud/029109619-icon-service-Workload-Orchestration.png similarity index 100% rename from assets/icons/azure/hybrid-multicloud/029109619-icon-service-Workload-Orchestration.png rename to core/assets/icons/azure/hybrid-multicloud/029109619-icon-service-Workload-Orchestration.png diff --git a/assets/icons/azure/hybrid-multicloud/029109619-icon-service-Workload-Orchestration.svg b/core/assets/icons/azure/hybrid-multicloud/029109619-icon-service-Workload-Orchestration.svg similarity index 100% rename from assets/icons/azure/hybrid-multicloud/029109619-icon-service-Workload-Orchestration.svg rename to core/assets/icons/azure/hybrid-multicloud/029109619-icon-service-Workload-Orchestration.svg diff --git a/assets/icons/azure/hybrid-multicloud/03248-icon-service-Azure-Operator-5G-Core.png b/core/assets/icons/azure/hybrid-multicloud/03248-icon-service-Azure-Operator-5G-Core.png similarity index 100% rename from assets/icons/azure/hybrid-multicloud/03248-icon-service-Azure-Operator-5G-Core.png rename to core/assets/icons/azure/hybrid-multicloud/03248-icon-service-Azure-Operator-5G-Core.png diff --git a/assets/icons/azure/hybrid-multicloud/03248-icon-service-Azure-Operator-5G-Core.svg b/core/assets/icons/azure/hybrid-multicloud/03248-icon-service-Azure-Operator-5G-Core.svg similarity index 100% rename from assets/icons/azure/hybrid-multicloud/03248-icon-service-Azure-Operator-5G-Core.svg rename to core/assets/icons/azure/hybrid-multicloud/03248-icon-service-Azure-Operator-5G-Core.svg diff --git a/assets/icons/azure/hybrid-multicloud/03324-icon-service-Azure-Operator-Nexus.png b/core/assets/icons/azure/hybrid-multicloud/03324-icon-service-Azure-Operator-Nexus.png similarity index 100% rename from assets/icons/azure/hybrid-multicloud/03324-icon-service-Azure-Operator-Nexus.png rename to core/assets/icons/azure/hybrid-multicloud/03324-icon-service-Azure-Operator-Nexus.png diff --git a/assets/icons/azure/hybrid-multicloud/03324-icon-service-Azure-Operator-Nexus.svg b/core/assets/icons/azure/hybrid-multicloud/03324-icon-service-Azure-Operator-Nexus.svg similarity index 100% rename from assets/icons/azure/hybrid-multicloud/03324-icon-service-Azure-Operator-Nexus.svg rename to core/assets/icons/azure/hybrid-multicloud/03324-icon-service-Azure-Operator-Nexus.svg diff --git a/assets/icons/azure/hybrid-multicloud/03333-icon-service-Azure-Operator-Insights.png b/core/assets/icons/azure/hybrid-multicloud/03333-icon-service-Azure-Operator-Insights.png similarity index 100% rename from assets/icons/azure/hybrid-multicloud/03333-icon-service-Azure-Operator-Insights.png rename to core/assets/icons/azure/hybrid-multicloud/03333-icon-service-Azure-Operator-Insights.png diff --git a/assets/icons/azure/hybrid-multicloud/03333-icon-service-Azure-Operator-Insights.svg b/core/assets/icons/azure/hybrid-multicloud/03333-icon-service-Azure-Operator-Insights.svg similarity index 100% rename from assets/icons/azure/hybrid-multicloud/03333-icon-service-Azure-Operator-Insights.svg rename to core/assets/icons/azure/hybrid-multicloud/03333-icon-service-Azure-Operator-Insights.svg diff --git a/assets/icons/azure/hybrid-multicloud/03334-icon-service-Azure-Operator-Service-Manager.png b/core/assets/icons/azure/hybrid-multicloud/03334-icon-service-Azure-Operator-Service-Manager.png similarity index 100% rename from assets/icons/azure/hybrid-multicloud/03334-icon-service-Azure-Operator-Service-Manager.png rename to core/assets/icons/azure/hybrid-multicloud/03334-icon-service-Azure-Operator-Service-Manager.png diff --git a/assets/icons/azure/hybrid-multicloud/03334-icon-service-Azure-Operator-Service-Manager.svg b/core/assets/icons/azure/hybrid-multicloud/03334-icon-service-Azure-Operator-Service-Manager.svg similarity index 100% rename from assets/icons/azure/hybrid-multicloud/03334-icon-service-Azure-Operator-Service-Manager.svg rename to core/assets/icons/azure/hybrid-multicloud/03334-icon-service-Azure-Operator-Service-Manager.svg diff --git a/assets/icons/azure/hybrid-multicloud/03347-icon-service-Azure-Programmable-Connectivity.png b/core/assets/icons/azure/hybrid-multicloud/03347-icon-service-Azure-Programmable-Connectivity.png similarity index 100% rename from assets/icons/azure/hybrid-multicloud/03347-icon-service-Azure-Programmable-Connectivity.png rename to core/assets/icons/azure/hybrid-multicloud/03347-icon-service-Azure-Programmable-Connectivity.png diff --git a/assets/icons/azure/hybrid-multicloud/03347-icon-service-Azure-Programmable-Connectivity.svg b/core/assets/icons/azure/hybrid-multicloud/03347-icon-service-Azure-Programmable-Connectivity.svg similarity index 100% rename from assets/icons/azure/hybrid-multicloud/03347-icon-service-Azure-Programmable-Connectivity.svg rename to core/assets/icons/azure/hybrid-multicloud/03347-icon-service-Azure-Programmable-Connectivity.svg diff --git a/assets/icons/azure/hybrid-multicloud/033745116-icon-service-Azure-Local.png b/core/assets/icons/azure/hybrid-multicloud/033745116-icon-service-Azure-Local.png similarity index 100% rename from assets/icons/azure/hybrid-multicloud/033745116-icon-service-Azure-Local.png rename to core/assets/icons/azure/hybrid-multicloud/033745116-icon-service-Azure-Local.png diff --git a/assets/icons/azure/hybrid-multicloud/033745116-icon-service-Azure-Local.svg b/core/assets/icons/azure/hybrid-multicloud/033745116-icon-service-Azure-Local.svg similarity index 100% rename from assets/icons/azure/hybrid-multicloud/033745116-icon-service-Azure-Local.svg rename to core/assets/icons/azure/hybrid-multicloud/033745116-icon-service-Azure-Local.svg diff --git a/assets/icons/azure/hybrid-multicloud/03521-icon-service-Edge-Storage-Accelerator.png b/core/assets/icons/azure/hybrid-multicloud/03521-icon-service-Edge-Storage-Accelerator.png similarity index 100% rename from assets/icons/azure/hybrid-multicloud/03521-icon-service-Edge-Storage-Accelerator.png rename to core/assets/icons/azure/hybrid-multicloud/03521-icon-service-Edge-Storage-Accelerator.png diff --git a/assets/icons/azure/hybrid-multicloud/03521-icon-service-Edge-Storage-Accelerator.svg b/core/assets/icons/azure/hybrid-multicloud/03521-icon-service-Edge-Storage-Accelerator.svg similarity index 100% rename from assets/icons/azure/hybrid-multicloud/03521-icon-service-Edge-Storage-Accelerator.svg rename to core/assets/icons/azure/hybrid-multicloud/03521-icon-service-Edge-Storage-Accelerator.svg diff --git a/assets/icons/azure/hybrid-multicloud/03585-icon-service-Azure-Monitor-Pipeline.png b/core/assets/icons/azure/hybrid-multicloud/03585-icon-service-Azure-Monitor-Pipeline.png similarity index 100% rename from assets/icons/azure/hybrid-multicloud/03585-icon-service-Azure-Monitor-Pipeline.png rename to core/assets/icons/azure/hybrid-multicloud/03585-icon-service-Azure-Monitor-Pipeline.png diff --git a/assets/icons/azure/hybrid-multicloud/03585-icon-service-Azure-Monitor-Pipeline.svg b/core/assets/icons/azure/hybrid-multicloud/03585-icon-service-Azure-Monitor-Pipeline.svg similarity index 100% rename from assets/icons/azure/hybrid-multicloud/03585-icon-service-Azure-Monitor-Pipeline.svg rename to core/assets/icons/azure/hybrid-multicloud/03585-icon-service-Azure-Monitor-Pipeline.svg diff --git a/assets/icons/azure/hybrid-multicloud/03687-icon-service-AI-at-Edge.png b/core/assets/icons/azure/hybrid-multicloud/03687-icon-service-AI-at-Edge.png similarity index 100% rename from assets/icons/azure/hybrid-multicloud/03687-icon-service-AI-at-Edge.png rename to core/assets/icons/azure/hybrid-multicloud/03687-icon-service-AI-at-Edge.png diff --git a/assets/icons/azure/hybrid-multicloud/03687-icon-service-AI-at-Edge.svg b/core/assets/icons/azure/hybrid-multicloud/03687-icon-service-AI-at-Edge.svg similarity index 100% rename from assets/icons/azure/hybrid-multicloud/03687-icon-service-AI-at-Edge.svg rename to core/assets/icons/azure/hybrid-multicloud/03687-icon-service-AI-at-Edge.svg diff --git a/assets/icons/azure/identity/00321-icon-service-Security.png b/core/assets/icons/azure/identity/00321-icon-service-Security.png similarity index 100% rename from assets/icons/azure/identity/00321-icon-service-Security.png rename to core/assets/icons/azure/identity/00321-icon-service-Security.png diff --git a/assets/icons/azure/identity/00321-icon-service-Security.svg b/core/assets/icons/azure/identity/00321-icon-service-Security.svg similarity index 100% rename from assets/icons/azure/identity/00321-icon-service-Security.svg rename to core/assets/icons/azure/identity/00321-icon-service-Security.svg diff --git a/assets/icons/azure/identity/00919-icon-service-Administrative-Units.png b/core/assets/icons/azure/identity/00919-icon-service-Administrative-Units.png similarity index 100% rename from assets/icons/azure/identity/00919-icon-service-Administrative-Units.png rename to core/assets/icons/azure/identity/00919-icon-service-Administrative-Units.png diff --git a/assets/icons/azure/identity/00919-icon-service-Administrative-Units.svg b/core/assets/icons/azure/identity/00919-icon-service-Administrative-Units.svg similarity index 100% rename from assets/icons/azure/identity/00919-icon-service-Administrative-Units.svg rename to core/assets/icons/azure/identity/00919-icon-service-Administrative-Units.svg diff --git a/assets/icons/azure/identity/01084-icon-service-Verifiable-Credentials.png b/core/assets/icons/azure/identity/01084-icon-service-Verifiable-Credentials.png similarity index 100% rename from assets/icons/azure/identity/01084-icon-service-Verifiable-Credentials.png rename to core/assets/icons/azure/identity/01084-icon-service-Verifiable-Credentials.png diff --git a/assets/icons/azure/identity/01084-icon-service-Verifiable-Credentials.svg b/core/assets/icons/azure/identity/01084-icon-service-Verifiable-Credentials.svg similarity index 100% rename from assets/icons/azure/identity/01084-icon-service-Verifiable-Credentials.svg rename to core/assets/icons/azure/identity/01084-icon-service-Verifiable-Credentials.svg diff --git a/assets/icons/azure/identity/02251-icon-service-Azure-AD-Privleged-Identity-Management.png b/core/assets/icons/azure/identity/02251-icon-service-Azure-AD-Privleged-Identity-Management.png similarity index 100% rename from assets/icons/azure/identity/02251-icon-service-Azure-AD-Privleged-Identity-Management.png rename to core/assets/icons/azure/identity/02251-icon-service-Azure-AD-Privleged-Identity-Management.png diff --git a/assets/icons/azure/identity/02251-icon-service-Azure-AD-Privleged-Identity-Management.svg b/core/assets/icons/azure/identity/02251-icon-service-Azure-AD-Privleged-Identity-Management.svg similarity index 100% rename from assets/icons/azure/identity/02251-icon-service-Azure-AD-Privleged-Identity-Management.svg rename to core/assets/icons/azure/identity/02251-icon-service-Azure-AD-Privleged-Identity-Management.svg diff --git a/assets/icons/azure/identity/02251-icon-service-Entra-Privleged-Identity-Management.png b/core/assets/icons/azure/identity/02251-icon-service-Entra-Privleged-Identity-Management.png similarity index 100% rename from assets/icons/azure/identity/02251-icon-service-Entra-Privleged-Identity-Management.png rename to core/assets/icons/azure/identity/02251-icon-service-Entra-Privleged-Identity-Management.png diff --git a/assets/icons/azure/identity/02251-icon-service-Entra-Privleged-Identity-Management.svg b/core/assets/icons/azure/identity/02251-icon-service-Entra-Privleged-Identity-Management.svg similarity index 100% rename from assets/icons/azure/identity/02251-icon-service-Entra-Privleged-Identity-Management.svg rename to core/assets/icons/azure/identity/02251-icon-service-Entra-Privleged-Identity-Management.svg diff --git a/assets/icons/azure/identity/02386-icon-service-API-Proxy.png b/core/assets/icons/azure/identity/02386-icon-service-API-Proxy.png similarity index 100% rename from assets/icons/azure/identity/02386-icon-service-API-Proxy.png rename to core/assets/icons/azure/identity/02386-icon-service-API-Proxy.png diff --git a/assets/icons/azure/identity/02386-icon-service-API-Proxy.svg b/core/assets/icons/azure/identity/02386-icon-service-API-Proxy.svg similarity index 100% rename from assets/icons/azure/identity/02386-icon-service-API-Proxy.svg rename to core/assets/icons/azure/identity/02386-icon-service-API-Proxy.svg diff --git a/assets/icons/azure/identity/02679-icon-service-Tenant-Properties.png b/core/assets/icons/azure/identity/02679-icon-service-Tenant-Properties.png similarity index 100% rename from assets/icons/azure/identity/02679-icon-service-Tenant-Properties.png rename to core/assets/icons/azure/identity/02679-icon-service-Tenant-Properties.png diff --git a/assets/icons/azure/identity/02679-icon-service-Tenant-Properties.svg b/core/assets/icons/azure/identity/02679-icon-service-Tenant-Properties.svg similarity index 100% rename from assets/icons/azure/identity/02679-icon-service-Tenant-Properties.svg rename to core/assets/icons/azure/identity/02679-icon-service-Tenant-Properties.svg diff --git a/assets/icons/azure/identity/02680-icon-service-Custom-Azure-AD-Roles.png b/core/assets/icons/azure/identity/02680-icon-service-Custom-Azure-AD-Roles.png similarity index 100% rename from assets/icons/azure/identity/02680-icon-service-Custom-Azure-AD-Roles.png rename to core/assets/icons/azure/identity/02680-icon-service-Custom-Azure-AD-Roles.png diff --git a/assets/icons/azure/identity/02680-icon-service-Custom-Azure-AD-Roles.svg b/core/assets/icons/azure/identity/02680-icon-service-Custom-Azure-AD-Roles.svg similarity index 100% rename from assets/icons/azure/identity/02680-icon-service-Custom-Azure-AD-Roles.svg rename to core/assets/icons/azure/identity/02680-icon-service-Custom-Azure-AD-Roles.svg diff --git a/assets/icons/azure/identity/02680-icon-service-Entra-Identity-Custom-Roles.png b/core/assets/icons/azure/identity/02680-icon-service-Entra-Identity-Custom-Roles.png similarity index 100% rename from assets/icons/azure/identity/02680-icon-service-Entra-Identity-Custom-Roles.png rename to core/assets/icons/azure/identity/02680-icon-service-Entra-Identity-Custom-Roles.png diff --git a/assets/icons/azure/identity/02680-icon-service-Entra-Identity-Custom-Roles.svg b/core/assets/icons/azure/identity/02680-icon-service-Entra-Identity-Custom-Roles.svg similarity index 100% rename from assets/icons/azure/identity/02680-icon-service-Entra-Identity-Custom-Roles.svg rename to core/assets/icons/azure/identity/02680-icon-service-Entra-Identity-Custom-Roles.svg diff --git a/assets/icons/azure/identity/02681-icon-service-AAD-Licenses.png b/core/assets/icons/azure/identity/02681-icon-service-AAD-Licenses.png similarity index 100% rename from assets/icons/azure/identity/02681-icon-service-AAD-Licenses.png rename to core/assets/icons/azure/identity/02681-icon-service-AAD-Licenses.png diff --git a/assets/icons/azure/identity/02681-icon-service-AAD-Licenses.svg b/core/assets/icons/azure/identity/02681-icon-service-AAD-Licenses.svg similarity index 100% rename from assets/icons/azure/identity/02681-icon-service-AAD-Licenses.svg rename to core/assets/icons/azure/identity/02681-icon-service-AAD-Licenses.svg diff --git a/assets/icons/azure/identity/02681-icon-service-Entra-Identity-Licenses.png b/core/assets/icons/azure/identity/02681-icon-service-Entra-Identity-Licenses.png similarity index 100% rename from assets/icons/azure/identity/02681-icon-service-Entra-Identity-Licenses.png rename to core/assets/icons/azure/identity/02681-icon-service-Entra-Identity-Licenses.png diff --git a/assets/icons/azure/identity/02681-icon-service-Entra-Identity-Licenses.svg b/core/assets/icons/azure/identity/02681-icon-service-Entra-Identity-Licenses.svg similarity index 100% rename from assets/icons/azure/identity/02681-icon-service-Entra-Identity-Licenses.svg rename to core/assets/icons/azure/identity/02681-icon-service-Entra-Identity-Licenses.svg diff --git a/assets/icons/azure/identity/02854-icon-service-Entra-Connect.png b/core/assets/icons/azure/identity/02854-icon-service-Entra-Connect.png similarity index 100% rename from assets/icons/azure/identity/02854-icon-service-Entra-Connect.png rename to core/assets/icons/azure/identity/02854-icon-service-Entra-Connect.png diff --git a/assets/icons/azure/identity/02854-icon-service-Entra-Connect.svg b/core/assets/icons/azure/identity/02854-icon-service-Entra-Connect.svg similarity index 100% rename from assets/icons/azure/identity/02854-icon-service-Entra-Connect.svg rename to core/assets/icons/azure/identity/02854-icon-service-Entra-Connect.svg diff --git a/assets/icons/azure/identity/03143-icon-service-Entra-Verified-ID.png b/core/assets/icons/azure/identity/03143-icon-service-Entra-Verified-ID.png similarity index 100% rename from assets/icons/azure/identity/03143-icon-service-Entra-Verified-ID.png rename to core/assets/icons/azure/identity/03143-icon-service-Entra-Verified-ID.png diff --git a/assets/icons/azure/identity/03143-icon-service-Entra-Verified-ID.svg b/core/assets/icons/azure/identity/03143-icon-service-Entra-Verified-ID.svg similarity index 100% rename from assets/icons/azure/identity/03143-icon-service-Entra-Verified-ID.svg rename to core/assets/icons/azure/identity/03143-icon-service-Entra-Verified-ID.svg diff --git a/assets/icons/azure/identity/03225-icon-service-Verification-As-A-Service.png b/core/assets/icons/azure/identity/03225-icon-service-Verification-As-A-Service.png similarity index 100% rename from assets/icons/azure/identity/03225-icon-service-Verification-As-A-Service.png rename to core/assets/icons/azure/identity/03225-icon-service-Verification-As-A-Service.png diff --git a/assets/icons/azure/identity/03225-icon-service-Verification-As-A-Service.svg b/core/assets/icons/azure/identity/03225-icon-service-Verification-As-A-Service.svg similarity index 100% rename from assets/icons/azure/identity/03225-icon-service-Verification-As-A-Service.svg rename to core/assets/icons/azure/identity/03225-icon-service-Verification-As-A-Service.svg diff --git a/assets/icons/azure/identity/03256-icon-service-Multi-Factor-Authentication.png b/core/assets/icons/azure/identity/03256-icon-service-Multi-Factor-Authentication.png similarity index 100% rename from assets/icons/azure/identity/03256-icon-service-Multi-Factor-Authentication.png rename to core/assets/icons/azure/identity/03256-icon-service-Multi-Factor-Authentication.png diff --git a/assets/icons/azure/identity/03256-icon-service-Multi-Factor-Authentication.svg b/core/assets/icons/azure/identity/03256-icon-service-Multi-Factor-Authentication.svg similarity index 100% rename from assets/icons/azure/identity/03256-icon-service-Multi-Factor-Authentication.svg rename to core/assets/icons/azure/identity/03256-icon-service-Multi-Factor-Authentication.svg diff --git a/assets/icons/azure/identity/03309-icon-service-Entra-Global-Secure-Access.png b/core/assets/icons/azure/identity/03309-icon-service-Entra-Global-Secure-Access.png similarity index 100% rename from assets/icons/azure/identity/03309-icon-service-Entra-Global-Secure-Access.png rename to core/assets/icons/azure/identity/03309-icon-service-Entra-Global-Secure-Access.png diff --git a/assets/icons/azure/identity/03309-icon-service-Entra-Global-Secure-Access.svg b/core/assets/icons/azure/identity/03309-icon-service-Entra-Global-Secure-Access.svg similarity index 100% rename from assets/icons/azure/identity/03309-icon-service-Entra-Global-Secure-Access.svg rename to core/assets/icons/azure/identity/03309-icon-service-Entra-Global-Secure-Access.svg diff --git a/assets/icons/azure/identity/03338-icon-service-External-Identities.png b/core/assets/icons/azure/identity/03338-icon-service-External-Identities.png similarity index 100% rename from assets/icons/azure/identity/03338-icon-service-External-Identities.png rename to core/assets/icons/azure/identity/03338-icon-service-External-Identities.png diff --git a/assets/icons/azure/identity/03338-icon-service-External-Identities.svg b/core/assets/icons/azure/identity/03338-icon-service-External-Identities.svg similarity index 100% rename from assets/icons/azure/identity/03338-icon-service-External-Identities.svg rename to core/assets/icons/azure/identity/03338-icon-service-External-Identities.svg diff --git a/assets/icons/azure/identity/03382-icon-service-Entra-Private-Access.png b/core/assets/icons/azure/identity/03382-icon-service-Entra-Private-Access.png similarity index 100% rename from assets/icons/azure/identity/03382-icon-service-Entra-Private-Access.png rename to core/assets/icons/azure/identity/03382-icon-service-Entra-Private-Access.png diff --git a/assets/icons/azure/identity/03382-icon-service-Entra-Private-Access.svg b/core/assets/icons/azure/identity/03382-icon-service-Entra-Private-Access.svg similarity index 100% rename from assets/icons/azure/identity/03382-icon-service-Entra-Private-Access.svg rename to core/assets/icons/azure/identity/03382-icon-service-Entra-Private-Access.svg diff --git a/assets/icons/azure/identity/03383-icon-service-Entra-Internet-Access.png b/core/assets/icons/azure/identity/03383-icon-service-Entra-Internet-Access.png similarity index 100% rename from assets/icons/azure/identity/03383-icon-service-Entra-Internet-Access.png rename to core/assets/icons/azure/identity/03383-icon-service-Entra-Internet-Access.png diff --git a/assets/icons/azure/identity/03383-icon-service-Entra-Internet-Access.svg b/core/assets/icons/azure/identity/03383-icon-service-Entra-Internet-Access.svg similarity index 100% rename from assets/icons/azure/identity/03383-icon-service-Entra-Internet-Access.svg rename to core/assets/icons/azure/identity/03383-icon-service-Entra-Internet-Access.svg diff --git a/assets/icons/azure/identity/03425-icon-service-external-id.png b/core/assets/icons/azure/identity/03425-icon-service-external-id.png similarity index 100% rename from assets/icons/azure/identity/03425-icon-service-external-id.png rename to core/assets/icons/azure/identity/03425-icon-service-external-id.png diff --git a/assets/icons/azure/identity/03425-icon-service-external-id.svg b/core/assets/icons/azure/identity/03425-icon-service-external-id.svg similarity index 100% rename from assets/icons/azure/identity/03425-icon-service-external-id.svg rename to core/assets/icons/azure/identity/03425-icon-service-external-id.svg diff --git a/assets/icons/azure/identity/034251-icon-service-external-id-modified.png b/core/assets/icons/azure/identity/034251-icon-service-external-id-modified.png similarity index 100% rename from assets/icons/azure/identity/034251-icon-service-external-id-modified.png rename to core/assets/icons/azure/identity/034251-icon-service-external-id-modified.png diff --git a/assets/icons/azure/identity/034251-icon-service-external-id-modified.svg b/core/assets/icons/azure/identity/034251-icon-service-external-id-modified.svg similarity index 100% rename from assets/icons/azure/identity/034251-icon-service-external-id-modified.svg rename to core/assets/icons/azure/identity/034251-icon-service-external-id-modified.svg diff --git a/assets/icons/azure/identity/03533-icon-service-Entra-Connect-Sync.png b/core/assets/icons/azure/identity/03533-icon-service-Entra-Connect-Sync.png similarity index 100% rename from assets/icons/azure/identity/03533-icon-service-Entra-Connect-Sync.png rename to core/assets/icons/azure/identity/03533-icon-service-Entra-Connect-Sync.png diff --git a/assets/icons/azure/identity/03533-icon-service-Entra-Connect-Sync.svg b/core/assets/icons/azure/identity/03533-icon-service-Entra-Connect-Sync.svg similarity index 100% rename from assets/icons/azure/identity/03533-icon-service-Entra-Connect-Sync.svg rename to core/assets/icons/azure/identity/03533-icon-service-Entra-Connect-Sync.svg diff --git a/assets/icons/azure/identity/10221-icon-service-Azure-Active-Directory.png b/core/assets/icons/azure/identity/10221-icon-service-Azure-Active-Directory.png similarity index 100% rename from assets/icons/azure/identity/10221-icon-service-Azure-Active-Directory.png rename to core/assets/icons/azure/identity/10221-icon-service-Azure-Active-Directory.png diff --git a/assets/icons/azure/identity/10221-icon-service-Azure-Active-Directory.svg b/core/assets/icons/azure/identity/10221-icon-service-Azure-Active-Directory.svg similarity index 100% rename from assets/icons/azure/identity/10221-icon-service-Azure-Active-Directory.svg rename to core/assets/icons/azure/identity/10221-icon-service-Azure-Active-Directory.svg diff --git a/assets/icons/azure/identity/10222-icon-service-Azure-AD-Domain-Services.png b/core/assets/icons/azure/identity/10222-icon-service-Azure-AD-Domain-Services.png similarity index 100% rename from assets/icons/azure/identity/10222-icon-service-Azure-AD-Domain-Services.png rename to core/assets/icons/azure/identity/10222-icon-service-Azure-AD-Domain-Services.png diff --git a/assets/icons/azure/identity/10222-icon-service-Azure-AD-Domain-Services.svg b/core/assets/icons/azure/identity/10222-icon-service-Azure-AD-Domain-Services.svg similarity index 100% rename from assets/icons/azure/identity/10222-icon-service-Azure-AD-Domain-Services.svg rename to core/assets/icons/azure/identity/10222-icon-service-Azure-AD-Domain-Services.svg diff --git a/assets/icons/azure/identity/10222-icon-service-Entra-Domain-Services.png b/core/assets/icons/azure/identity/10222-icon-service-Entra-Domain-Services.png similarity index 100% rename from assets/icons/azure/identity/10222-icon-service-Entra-Domain-Services.png rename to core/assets/icons/azure/identity/10222-icon-service-Entra-Domain-Services.png diff --git a/assets/icons/azure/identity/10222-icon-service-Entra-Domain-Services.svg b/core/assets/icons/azure/identity/10222-icon-service-Entra-Domain-Services.svg similarity index 100% rename from assets/icons/azure/identity/10222-icon-service-Entra-Domain-Services.svg rename to core/assets/icons/azure/identity/10222-icon-service-Entra-Domain-Services.svg diff --git a/assets/icons/azure/identity/10223-icon-service-Groups.png b/core/assets/icons/azure/identity/10223-icon-service-Groups.png similarity index 100% rename from assets/icons/azure/identity/10223-icon-service-Groups.png rename to core/assets/icons/azure/identity/10223-icon-service-Groups.png diff --git a/assets/icons/azure/identity/10223-icon-service-Groups.svg b/core/assets/icons/azure/identity/10223-icon-service-Groups.svg similarity index 100% rename from assets/icons/azure/identity/10223-icon-service-Groups.svg rename to core/assets/icons/azure/identity/10223-icon-service-Groups.svg diff --git a/assets/icons/azure/identity/10224-icon-service-Active-Directory-Connect-Health.png b/core/assets/icons/azure/identity/10224-icon-service-Active-Directory-Connect-Health.png similarity index 100% rename from assets/icons/azure/identity/10224-icon-service-Active-Directory-Connect-Health.png rename to core/assets/icons/azure/identity/10224-icon-service-Active-Directory-Connect-Health.png diff --git a/assets/icons/azure/identity/10224-icon-service-Active-Directory-Connect-Health.svg b/core/assets/icons/azure/identity/10224-icon-service-Active-Directory-Connect-Health.svg similarity index 100% rename from assets/icons/azure/identity/10224-icon-service-Active-Directory-Connect-Health.svg rename to core/assets/icons/azure/identity/10224-icon-service-Active-Directory-Connect-Health.svg diff --git a/assets/icons/azure/identity/10224-icon-service-Entra-Connect-Health.png b/core/assets/icons/azure/identity/10224-icon-service-Entra-Connect-Health.png similarity index 100% rename from assets/icons/azure/identity/10224-icon-service-Entra-Connect-Health.png rename to core/assets/icons/azure/identity/10224-icon-service-Entra-Connect-Health.png diff --git a/assets/icons/azure/identity/10224-icon-service-Entra-Connect-Health.svg b/core/assets/icons/azure/identity/10224-icon-service-Entra-Connect-Health.svg similarity index 100% rename from assets/icons/azure/identity/10224-icon-service-Entra-Connect-Health.svg rename to core/assets/icons/azure/identity/10224-icon-service-Entra-Connect-Health.svg diff --git a/assets/icons/azure/identity/10225-icon-service-Enterprise-Applications.png b/core/assets/icons/azure/identity/10225-icon-service-Enterprise-Applications.png similarity index 100% rename from assets/icons/azure/identity/10225-icon-service-Enterprise-Applications.png rename to core/assets/icons/azure/identity/10225-icon-service-Enterprise-Applications.png diff --git a/assets/icons/azure/identity/10225-icon-service-Enterprise-Applications.svg b/core/assets/icons/azure/identity/10225-icon-service-Enterprise-Applications.svg similarity index 100% rename from assets/icons/azure/identity/10225-icon-service-Enterprise-Applications.svg rename to core/assets/icons/azure/identity/10225-icon-service-Enterprise-Applications.svg diff --git a/assets/icons/azure/identity/10227-icon-service-Entra-Managed-Identities.png b/core/assets/icons/azure/identity/10227-icon-service-Entra-Managed-Identities.png similarity index 100% rename from assets/icons/azure/identity/10227-icon-service-Entra-Managed-Identities.png rename to core/assets/icons/azure/identity/10227-icon-service-Entra-Managed-Identities.png diff --git a/assets/icons/azure/identity/10227-icon-service-Entra-Managed-Identities.svg b/core/assets/icons/azure/identity/10227-icon-service-Entra-Managed-Identities.svg similarity index 100% rename from assets/icons/azure/identity/10227-icon-service-Entra-Managed-Identities.svg rename to core/assets/icons/azure/identity/10227-icon-service-Entra-Managed-Identities.svg diff --git a/assets/icons/azure/identity/10227-icon-service-Managed-Identities.png b/core/assets/icons/azure/identity/10227-icon-service-Managed-Identities.png similarity index 100% rename from assets/icons/azure/identity/10227-icon-service-Managed-Identities.png rename to core/assets/icons/azure/identity/10227-icon-service-Managed-Identities.png diff --git a/assets/icons/azure/identity/10227-icon-service-Managed-Identities.svg b/core/assets/icons/azure/identity/10227-icon-service-Managed-Identities.svg similarity index 100% rename from assets/icons/azure/identity/10227-icon-service-Managed-Identities.svg rename to core/assets/icons/azure/identity/10227-icon-service-Managed-Identities.svg diff --git a/assets/icons/azure/identity/10228-icon-service-Azure-AD-B2C.png b/core/assets/icons/azure/identity/10228-icon-service-Azure-AD-B2C.png similarity index 100% rename from assets/icons/azure/identity/10228-icon-service-Azure-AD-B2C.png rename to core/assets/icons/azure/identity/10228-icon-service-Azure-AD-B2C.png diff --git a/assets/icons/azure/identity/10228-icon-service-Azure-AD-B2C.svg b/core/assets/icons/azure/identity/10228-icon-service-Azure-AD-B2C.svg similarity index 100% rename from assets/icons/azure/identity/10228-icon-service-Azure-AD-B2C.svg rename to core/assets/icons/azure/identity/10228-icon-service-Azure-AD-B2C.svg diff --git a/assets/icons/azure/identity/10229-icon-service-Azure-Information-Protection.png b/core/assets/icons/azure/identity/10229-icon-service-Azure-Information-Protection.png similarity index 100% rename from assets/icons/azure/identity/10229-icon-service-Azure-Information-Protection.png rename to core/assets/icons/azure/identity/10229-icon-service-Azure-Information-Protection.png diff --git a/assets/icons/azure/identity/10229-icon-service-Azure-Information-Protection.svg b/core/assets/icons/azure/identity/10229-icon-service-Azure-Information-Protection.svg similarity index 100% rename from assets/icons/azure/identity/10229-icon-service-Azure-Information-Protection.svg rename to core/assets/icons/azure/identity/10229-icon-service-Azure-Information-Protection.svg diff --git a/assets/icons/azure/identity/10230-icon-service-Users.png b/core/assets/icons/azure/identity/10230-icon-service-Users.png similarity index 100% rename from assets/icons/azure/identity/10230-icon-service-Users.png rename to core/assets/icons/azure/identity/10230-icon-service-Users.png diff --git a/assets/icons/azure/identity/10230-icon-service-Users.svg b/core/assets/icons/azure/identity/10230-icon-service-Users.svg similarity index 100% rename from assets/icons/azure/identity/10230-icon-service-Users.svg rename to core/assets/icons/azure/identity/10230-icon-service-Users.svg diff --git a/assets/icons/azure/identity/10231-icon-service-Azure-AD-Identity-Protection.png b/core/assets/icons/azure/identity/10231-icon-service-Azure-AD-Identity-Protection.png similarity index 100% rename from assets/icons/azure/identity/10231-icon-service-Azure-AD-Identity-Protection.png rename to core/assets/icons/azure/identity/10231-icon-service-Azure-AD-Identity-Protection.png diff --git a/assets/icons/azure/identity/10231-icon-service-Azure-AD-Identity-Protection.svg b/core/assets/icons/azure/identity/10231-icon-service-Azure-AD-Identity-Protection.svg similarity index 100% rename from assets/icons/azure/identity/10231-icon-service-Azure-AD-Identity-Protection.svg rename to core/assets/icons/azure/identity/10231-icon-service-Azure-AD-Identity-Protection.svg diff --git a/assets/icons/azure/identity/10231-icon-service-Entra-ID-Protection.png b/core/assets/icons/azure/identity/10231-icon-service-Entra-ID-Protection.png similarity index 100% rename from assets/icons/azure/identity/10231-icon-service-Entra-ID-Protection.png rename to core/assets/icons/azure/identity/10231-icon-service-Entra-ID-Protection.png diff --git a/assets/icons/azure/identity/10231-icon-service-Entra-ID-Protection.svg b/core/assets/icons/azure/identity/10231-icon-service-Entra-ID-Protection.svg similarity index 100% rename from assets/icons/azure/identity/10231-icon-service-Entra-ID-Protection.svg rename to core/assets/icons/azure/identity/10231-icon-service-Entra-ID-Protection.svg diff --git a/assets/icons/azure/identity/10232-icon-service-App-Registrations.png b/core/assets/icons/azure/identity/10232-icon-service-App-Registrations.png similarity index 100% rename from assets/icons/azure/identity/10232-icon-service-App-Registrations.png rename to core/assets/icons/azure/identity/10232-icon-service-App-Registrations.png diff --git a/assets/icons/azure/identity/10232-icon-service-App-Registrations.svg b/core/assets/icons/azure/identity/10232-icon-service-App-Registrations.svg similarity index 100% rename from assets/icons/azure/identity/10232-icon-service-App-Registrations.svg rename to core/assets/icons/azure/identity/10232-icon-service-App-Registrations.svg diff --git a/assets/icons/azure/identity/10234-icon-service-Azure-AD-Privilege-Identity-Management.png b/core/assets/icons/azure/identity/10234-icon-service-Azure-AD-Privilege-Identity-Management.png similarity index 100% rename from assets/icons/azure/identity/10234-icon-service-Azure-AD-Privilege-Identity-Management.png rename to core/assets/icons/azure/identity/10234-icon-service-Azure-AD-Privilege-Identity-Management.png diff --git a/assets/icons/azure/identity/10234-icon-service-Azure-AD-Privilege-Identity-Management.svg b/core/assets/icons/azure/identity/10234-icon-service-Azure-AD-Privilege-Identity-Management.svg similarity index 100% rename from assets/icons/azure/identity/10234-icon-service-Azure-AD-Privilege-Identity-Management.svg rename to core/assets/icons/azure/identity/10234-icon-service-Azure-AD-Privilege-Identity-Management.svg diff --git a/assets/icons/azure/identity/10235-icon-service-Identity-Governance.png b/core/assets/icons/azure/identity/10235-icon-service-Identity-Governance.png similarity index 100% rename from assets/icons/azure/identity/10235-icon-service-Identity-Governance.png rename to core/assets/icons/azure/identity/10235-icon-service-Identity-Governance.png diff --git a/assets/icons/azure/identity/10235-icon-service-Identity-Governance.svg b/core/assets/icons/azure/identity/10235-icon-service-Identity-Governance.svg similarity index 100% rename from assets/icons/azure/identity/10235-icon-service-Identity-Governance.svg rename to core/assets/icons/azure/identity/10235-icon-service-Identity-Governance.svg diff --git a/assets/icons/azure/identity/10340-icon-service-Azure-AD-Roles-and-Administrators.png b/core/assets/icons/azure/identity/10340-icon-service-Azure-AD-Roles-and-Administrators.png similarity index 100% rename from assets/icons/azure/identity/10340-icon-service-Azure-AD-Roles-and-Administrators.png rename to core/assets/icons/azure/identity/10340-icon-service-Azure-AD-Roles-and-Administrators.png diff --git a/assets/icons/azure/identity/10340-icon-service-Azure-AD-Roles-and-Administrators.svg b/core/assets/icons/azure/identity/10340-icon-service-Azure-AD-Roles-and-Administrators.svg similarity index 100% rename from assets/icons/azure/identity/10340-icon-service-Azure-AD-Roles-and-Administrators.svg rename to core/assets/icons/azure/identity/10340-icon-service-Azure-AD-Roles-and-Administrators.svg diff --git a/assets/icons/azure/identity/10340-icon-service-Entra-Identity-Roles-and-Administrators.png b/core/assets/icons/azure/identity/10340-icon-service-Entra-Identity-Roles-and-Administrators.png similarity index 100% rename from assets/icons/azure/identity/10340-icon-service-Entra-Identity-Roles-and-Administrators.png rename to core/assets/icons/azure/identity/10340-icon-service-Entra-Identity-Roles-and-Administrators.png diff --git a/assets/icons/azure/identity/10340-icon-service-Entra-Identity-Roles-and-Administrators.svg b/core/assets/icons/azure/identity/10340-icon-service-Entra-Identity-Roles-and-Administrators.svg similarity index 100% rename from assets/icons/azure/identity/10340-icon-service-Entra-Identity-Roles-and-Administrators.svg rename to core/assets/icons/azure/identity/10340-icon-service-Entra-Identity-Roles-and-Administrators.svg diff --git a/assets/icons/azure/identity/10433-icon-service-User-Settings.png b/core/assets/icons/azure/identity/10433-icon-service-User-Settings.png similarity index 100% rename from assets/icons/azure/identity/10433-icon-service-User-Settings.png rename to core/assets/icons/azure/identity/10433-icon-service-User-Settings.png diff --git a/assets/icons/azure/identity/10433-icon-service-User-Settings.svg b/core/assets/icons/azure/identity/10433-icon-service-User-Settings.svg similarity index 100% rename from assets/icons/azure/identity/10433-icon-service-User-Settings.svg rename to core/assets/icons/azure/identity/10433-icon-service-User-Settings.svg diff --git a/assets/icons/azure/integration/00036-icon-service-SQL-Data-Warehouses.png b/core/assets/icons/azure/integration/00036-icon-service-SQL-Data-Warehouses.png similarity index 100% rename from assets/icons/azure/integration/00036-icon-service-SQL-Data-Warehouses.png rename to core/assets/icons/azure/integration/00036-icon-service-SQL-Data-Warehouses.png diff --git a/assets/icons/azure/integration/00036-icon-service-SQL-Data-Warehouses.svg b/core/assets/icons/azure/integration/00036-icon-service-SQL-Data-Warehouses.svg similarity index 100% rename from assets/icons/azure/integration/00036-icon-service-SQL-Data-Warehouses.svg rename to core/assets/icons/azure/integration/00036-icon-service-SQL-Data-Warehouses.svg diff --git a/assets/icons/azure/integration/00555-icon-service-Integration-Service-Environments.png b/core/assets/icons/azure/integration/00555-icon-service-Integration-Service-Environments.png similarity index 100% rename from assets/icons/azure/integration/00555-icon-service-Integration-Service-Environments.png rename to core/assets/icons/azure/integration/00555-icon-service-Integration-Service-Environments.png diff --git a/assets/icons/azure/integration/00555-icon-service-Integration-Service-Environments.svg b/core/assets/icons/azure/integration/00555-icon-service-Integration-Service-Environments.svg similarity index 100% rename from assets/icons/azure/integration/00555-icon-service-Integration-Service-Environments.svg rename to core/assets/icons/azure/integration/00555-icon-service-Integration-Service-Environments.svg diff --git a/assets/icons/azure/integration/00691-icon-service-Azure-Databox-Gateway.png b/core/assets/icons/azure/integration/00691-icon-service-Azure-Databox-Gateway.png similarity index 100% rename from assets/icons/azure/integration/00691-icon-service-Azure-Databox-Gateway.png rename to core/assets/icons/azure/integration/00691-icon-service-Azure-Databox-Gateway.png diff --git a/assets/icons/azure/integration/00691-icon-service-Azure-Databox-Gateway.svg b/core/assets/icons/azure/integration/00691-icon-service-Azure-Databox-Gateway.svg similarity index 100% rename from assets/icons/azure/integration/00691-icon-service-Azure-Databox-Gateway.svg rename to core/assets/icons/azure/integration/00691-icon-service-Azure-Databox-Gateway.svg diff --git a/assets/icons/azure/integration/02072-icon-service-Partner-Topic.png b/core/assets/icons/azure/integration/02072-icon-service-Partner-Topic.png similarity index 100% rename from assets/icons/azure/integration/02072-icon-service-Partner-Topic.png rename to core/assets/icons/azure/integration/02072-icon-service-Partner-Topic.png diff --git a/assets/icons/azure/integration/02072-icon-service-Partner-Topic.svg b/core/assets/icons/azure/integration/02072-icon-service-Partner-Topic.svg similarity index 100% rename from assets/icons/azure/integration/02072-icon-service-Partner-Topic.svg rename to core/assets/icons/azure/integration/02072-icon-service-Partner-Topic.svg diff --git a/assets/icons/azure/integration/02073-icon-service-System-Topic.png b/core/assets/icons/azure/integration/02073-icon-service-System-Topic.png similarity index 100% rename from assets/icons/azure/integration/02073-icon-service-System-Topic.png rename to core/assets/icons/azure/integration/02073-icon-service-System-Topic.png diff --git a/assets/icons/azure/integration/02073-icon-service-System-Topic.svg b/core/assets/icons/azure/integration/02073-icon-service-System-Topic.svg similarity index 100% rename from assets/icons/azure/integration/02073-icon-service-System-Topic.svg rename to core/assets/icons/azure/integration/02073-icon-service-System-Topic.svg diff --git a/assets/icons/azure/integration/02265-icon-service-Partner-Registration.png b/core/assets/icons/azure/integration/02265-icon-service-Partner-Registration.png similarity index 100% rename from assets/icons/azure/integration/02265-icon-service-Partner-Registration.png rename to core/assets/icons/azure/integration/02265-icon-service-Partner-Registration.png diff --git a/assets/icons/azure/integration/02265-icon-service-Partner-Registration.svg b/core/assets/icons/azure/integration/02265-icon-service-Partner-Registration.svg similarity index 100% rename from assets/icons/azure/integration/02265-icon-service-Partner-Registration.svg rename to core/assets/icons/azure/integration/02265-icon-service-Partner-Registration.svg diff --git a/assets/icons/azure/integration/02266-icon-service-Partner-Namespace.png b/core/assets/icons/azure/integration/02266-icon-service-Partner-Namespace.png similarity index 100% rename from assets/icons/azure/integration/02266-icon-service-Partner-Namespace.png rename to core/assets/icons/azure/integration/02266-icon-service-Partner-Namespace.png diff --git a/assets/icons/azure/integration/02266-icon-service-Partner-Namespace.svg b/core/assets/icons/azure/integration/02266-icon-service-Partner-Namespace.svg similarity index 100% rename from assets/icons/azure/integration/02266-icon-service-Partner-Namespace.svg rename to core/assets/icons/azure/integration/02266-icon-service-Partner-Namespace.svg diff --git a/assets/icons/azure/integration/02631-icon-service-Logic-Apps.png b/core/assets/icons/azure/integration/02631-icon-service-Logic-Apps.png similarity index 100% rename from assets/icons/azure/integration/02631-icon-service-Logic-Apps.png rename to core/assets/icons/azure/integration/02631-icon-service-Logic-Apps.png diff --git a/assets/icons/azure/integration/02631-icon-service-Logic-Apps.svg b/core/assets/icons/azure/integration/02631-icon-service-Logic-Apps.svg similarity index 100% rename from assets/icons/azure/integration/02631-icon-service-Logic-Apps.svg rename to core/assets/icons/azure/integration/02631-icon-service-Logic-Apps.svg diff --git a/assets/icons/azure/integration/02987-icon-service-pubsub.png b/core/assets/icons/azure/integration/02987-icon-service-pubsub.png similarity index 100% rename from assets/icons/azure/integration/02987-icon-service-pubsub.png rename to core/assets/icons/azure/integration/02987-icon-service-pubsub.png diff --git a/assets/icons/azure/integration/02987-icon-service-pubsub.svg b/core/assets/icons/azure/integration/02987-icon-service-pubsub.svg similarity index 100% rename from assets/icons/azure/integration/02987-icon-service-pubsub.svg rename to core/assets/icons/azure/integration/02987-icon-service-pubsub.svg diff --git a/assets/icons/azure/integration/032130874-icon-service-Logic-Apps-Template.png b/core/assets/icons/azure/integration/032130874-icon-service-Logic-Apps-Template.png similarity index 100% rename from assets/icons/azure/integration/032130874-icon-service-Logic-Apps-Template.png rename to core/assets/icons/azure/integration/032130874-icon-service-Logic-Apps-Template.png diff --git a/assets/icons/azure/integration/032130874-icon-service-Logic-Apps-Template.svg b/core/assets/icons/azure/integration/032130874-icon-service-Logic-Apps-Template.svg similarity index 100% rename from assets/icons/azure/integration/032130874-icon-service-Logic-Apps-Template.svg rename to core/assets/icons/azure/integration/032130874-icon-service-Logic-Apps-Template.svg diff --git a/assets/icons/azure/integration/03335-icon-service-Power-Platform.png b/core/assets/icons/azure/integration/03335-icon-service-Power-Platform.png similarity index 100% rename from assets/icons/azure/integration/03335-icon-service-Power-Platform.png rename to core/assets/icons/azure/integration/03335-icon-service-Power-Platform.png diff --git a/assets/icons/azure/integration/03335-icon-service-Power-Platform.svg b/core/assets/icons/azure/integration/03335-icon-service-Power-Platform.svg similarity index 100% rename from assets/icons/azure/integration/03335-icon-service-Power-Platform.svg rename to core/assets/icons/azure/integration/03335-icon-service-Power-Platform.svg diff --git a/assets/icons/azure/integration/03345-icon-service-Integration-Environments.png b/core/assets/icons/azure/integration/03345-icon-service-Integration-Environments.png similarity index 100% rename from assets/icons/azure/integration/03345-icon-service-Integration-Environments.png rename to core/assets/icons/azure/integration/03345-icon-service-Integration-Environments.png diff --git a/assets/icons/azure/integration/03345-icon-service-Integration-Environments.svg b/core/assets/icons/azure/integration/03345-icon-service-Integration-Environments.svg similarity index 100% rename from assets/icons/azure/integration/03345-icon-service-Integration-Environments.svg rename to core/assets/icons/azure/integration/03345-icon-service-Integration-Environments.svg diff --git a/assets/icons/azure/integration/03637-icon-service-Business-Process-Tracking.png b/core/assets/icons/azure/integration/03637-icon-service-Business-Process-Tracking.png similarity index 100% rename from assets/icons/azure/integration/03637-icon-service-Business-Process-Tracking.png rename to core/assets/icons/azure/integration/03637-icon-service-Business-Process-Tracking.png diff --git a/assets/icons/azure/integration/03637-icon-service-Business-Process-Tracking.svg b/core/assets/icons/azure/integration/03637-icon-service-Business-Process-Tracking.svg similarity index 100% rename from assets/icons/azure/integration/03637-icon-service-Business-Process-Tracking.svg rename to core/assets/icons/azure/integration/03637-icon-service-Business-Process-Tracking.svg diff --git a/assets/icons/azure/integration/10042-icon-service-API-Management-Services.png b/core/assets/icons/azure/integration/10042-icon-service-API-Management-Services.png similarity index 100% rename from assets/icons/azure/integration/10042-icon-service-API-Management-Services.png rename to core/assets/icons/azure/integration/10042-icon-service-API-Management-Services.png diff --git a/assets/icons/azure/integration/10042-icon-service-API-Management-Services.svg b/core/assets/icons/azure/integration/10042-icon-service-API-Management-Services.svg similarity index 100% rename from assets/icons/azure/integration/10042-icon-service-API-Management-Services.svg rename to core/assets/icons/azure/integration/10042-icon-service-API-Management-Services.svg diff --git a/assets/icons/azure/integration/10048-icon-service-API-Connections.png b/core/assets/icons/azure/integration/10048-icon-service-API-Connections.png similarity index 100% rename from assets/icons/azure/integration/10048-icon-service-API-Connections.png rename to core/assets/icons/azure/integration/10048-icon-service-API-Connections.png diff --git a/assets/icons/azure/integration/10048-icon-service-API-Connections.svg b/core/assets/icons/azure/integration/10048-icon-service-API-Connections.svg similarity index 100% rename from assets/icons/azure/integration/10048-icon-service-API-Connections.svg rename to core/assets/icons/azure/integration/10048-icon-service-API-Connections.svg diff --git a/assets/icons/azure/integration/10089-icon-service-StorSimple-Device-Managers.png b/core/assets/icons/azure/integration/10089-icon-service-StorSimple-Device-Managers.png similarity index 100% rename from assets/icons/azure/integration/10089-icon-service-StorSimple-Device-Managers.png rename to core/assets/icons/azure/integration/10089-icon-service-StorSimple-Device-Managers.png diff --git a/assets/icons/azure/integration/10089-icon-service-StorSimple-Device-Managers.svg b/core/assets/icons/azure/integration/10089-icon-service-StorSimple-Device-Managers.svg similarity index 100% rename from assets/icons/azure/integration/10089-icon-service-StorSimple-Device-Managers.svg rename to core/assets/icons/azure/integration/10089-icon-service-StorSimple-Device-Managers.svg diff --git a/assets/icons/azure/integration/10095-icon-service-Azure-Stack-Edge.png b/core/assets/icons/azure/integration/10095-icon-service-Azure-Stack-Edge.png similarity index 100% rename from assets/icons/azure/integration/10095-icon-service-Azure-Stack-Edge.png rename to core/assets/icons/azure/integration/10095-icon-service-Azure-Stack-Edge.png diff --git a/assets/icons/azure/integration/10095-icon-service-Azure-Stack-Edge.svg b/core/assets/icons/azure/integration/10095-icon-service-Azure-Stack-Edge.svg similarity index 100% rename from assets/icons/azure/integration/10095-icon-service-Azure-Stack-Edge.svg rename to core/assets/icons/azure/integration/10095-icon-service-Azure-Stack-Edge.svg diff --git a/assets/icons/azure/integration/10126-icon-service-Data-Factories.png b/core/assets/icons/azure/integration/10126-icon-service-Data-Factories.png similarity index 100% rename from assets/icons/azure/integration/10126-icon-service-Data-Factories.png rename to core/assets/icons/azure/integration/10126-icon-service-Data-Factories.png diff --git a/assets/icons/azure/integration/10126-icon-service-Data-Factories.svg b/core/assets/icons/azure/integration/10126-icon-service-Data-Factories.svg similarity index 100% rename from assets/icons/azure/integration/10126-icon-service-Data-Factories.svg rename to core/assets/icons/azure/integration/10126-icon-service-Data-Factories.svg diff --git a/assets/icons/azure/integration/10137-icon-service-Azure-SQL-Server-Stretch-Databases.png b/core/assets/icons/azure/integration/10137-icon-service-Azure-SQL-Server-Stretch-Databases.png similarity index 100% rename from assets/icons/azure/integration/10137-icon-service-Azure-SQL-Server-Stretch-Databases.png rename to core/assets/icons/azure/integration/10137-icon-service-Azure-SQL-Server-Stretch-Databases.png diff --git a/assets/icons/azure/integration/10137-icon-service-Azure-SQL-Server-Stretch-Databases.svg b/core/assets/icons/azure/integration/10137-icon-service-Azure-SQL-Server-Stretch-Databases.svg similarity index 100% rename from assets/icons/azure/integration/10137-icon-service-Azure-SQL-Server-Stretch-Databases.svg rename to core/assets/icons/azure/integration/10137-icon-service-Azure-SQL-Server-Stretch-Databases.svg diff --git a/assets/icons/azure/integration/10206-icon-service-Event-Grid-Topics.png b/core/assets/icons/azure/integration/10206-icon-service-Event-Grid-Topics.png similarity index 100% rename from assets/icons/azure/integration/10206-icon-service-Event-Grid-Topics.png rename to core/assets/icons/azure/integration/10206-icon-service-Event-Grid-Topics.png diff --git a/assets/icons/azure/integration/10206-icon-service-Event-Grid-Topics.svg b/core/assets/icons/azure/integration/10206-icon-service-Event-Grid-Topics.svg similarity index 100% rename from assets/icons/azure/integration/10206-icon-service-Event-Grid-Topics.svg rename to core/assets/icons/azure/integration/10206-icon-service-Event-Grid-Topics.svg diff --git a/assets/icons/azure/integration/10209-icon-service-Relays.png b/core/assets/icons/azure/integration/10209-icon-service-Relays.png similarity index 100% rename from assets/icons/azure/integration/10209-icon-service-Relays.png rename to core/assets/icons/azure/integration/10209-icon-service-Relays.png diff --git a/assets/icons/azure/integration/10209-icon-service-Relays.svg b/core/assets/icons/azure/integration/10209-icon-service-Relays.svg similarity index 100% rename from assets/icons/azure/integration/10209-icon-service-Relays.svg rename to core/assets/icons/azure/integration/10209-icon-service-Relays.svg diff --git a/assets/icons/azure/integration/10212-icon-service-Azure-API-for-FHIR.png b/core/assets/icons/azure/integration/10212-icon-service-Azure-API-for-FHIR.png similarity index 100% rename from assets/icons/azure/integration/10212-icon-service-Azure-API-for-FHIR.png rename to core/assets/icons/azure/integration/10212-icon-service-Azure-API-for-FHIR.png diff --git a/assets/icons/azure/integration/10212-icon-service-Azure-API-for-FHIR.svg b/core/assets/icons/azure/integration/10212-icon-service-Azure-API-for-FHIR.svg similarity index 100% rename from assets/icons/azure/integration/10212-icon-service-Azure-API-for-FHIR.svg rename to core/assets/icons/azure/integration/10212-icon-service-Azure-API-for-FHIR.svg diff --git a/assets/icons/azure/integration/10213-icon-service-Software-as-a-Service.png b/core/assets/icons/azure/integration/10213-icon-service-Software-as-a-Service.png similarity index 100% rename from assets/icons/azure/integration/10213-icon-service-Software-as-a-Service.png rename to core/assets/icons/azure/integration/10213-icon-service-Software-as-a-Service.png diff --git a/assets/icons/azure/integration/10213-icon-service-Software-as-a-Service.svg b/core/assets/icons/azure/integration/10213-icon-service-Software-as-a-Service.svg similarity index 100% rename from assets/icons/azure/integration/10213-icon-service-Software-as-a-Service.svg rename to core/assets/icons/azure/integration/10213-icon-service-Software-as-a-Service.svg diff --git a/assets/icons/azure/integration/10215-icon-service-Event-Grid-Domains.png b/core/assets/icons/azure/integration/10215-icon-service-Event-Grid-Domains.png similarity index 100% rename from assets/icons/azure/integration/10215-icon-service-Event-Grid-Domains.png rename to core/assets/icons/azure/integration/10215-icon-service-Event-Grid-Domains.png diff --git a/assets/icons/azure/integration/10215-icon-service-Event-Grid-Domains.svg b/core/assets/icons/azure/integration/10215-icon-service-Event-Grid-Domains.svg similarity index 100% rename from assets/icons/azure/integration/10215-icon-service-Event-Grid-Domains.svg rename to core/assets/icons/azure/integration/10215-icon-service-Event-Grid-Domains.svg diff --git a/assets/icons/azure/integration/10216-icon-service-Azure-Data-Catalog.png b/core/assets/icons/azure/integration/10216-icon-service-Azure-Data-Catalog.png similarity index 100% rename from assets/icons/azure/integration/10216-icon-service-Azure-Data-Catalog.png rename to core/assets/icons/azure/integration/10216-icon-service-Azure-Data-Catalog.png diff --git a/assets/icons/azure/integration/10216-icon-service-Azure-Data-Catalog.svg b/core/assets/icons/azure/integration/10216-icon-service-Azure-Data-Catalog.svg similarity index 100% rename from assets/icons/azure/integration/10216-icon-service-Azure-Data-Catalog.svg rename to core/assets/icons/azure/integration/10216-icon-service-Azure-Data-Catalog.svg diff --git a/assets/icons/azure/integration/10218-icon-service-Integration-Accounts.png b/core/assets/icons/azure/integration/10218-icon-service-Integration-Accounts.png similarity index 100% rename from assets/icons/azure/integration/10218-icon-service-Integration-Accounts.png rename to core/assets/icons/azure/integration/10218-icon-service-Integration-Accounts.png diff --git a/assets/icons/azure/integration/10218-icon-service-Integration-Accounts.svg b/core/assets/icons/azure/integration/10218-icon-service-Integration-Accounts.svg similarity index 100% rename from assets/icons/azure/integration/10218-icon-service-Integration-Accounts.svg rename to core/assets/icons/azure/integration/10218-icon-service-Integration-Accounts.svg diff --git a/assets/icons/azure/integration/10219-icon-service-App-Configuration.png b/core/assets/icons/azure/integration/10219-icon-service-App-Configuration.png similarity index 100% rename from assets/icons/azure/integration/10219-icon-service-App-Configuration.png rename to core/assets/icons/azure/integration/10219-icon-service-App-Configuration.png diff --git a/assets/icons/azure/integration/10219-icon-service-App-Configuration.svg b/core/assets/icons/azure/integration/10219-icon-service-App-Configuration.svg similarity index 100% rename from assets/icons/azure/integration/10219-icon-service-App-Configuration.svg rename to core/assets/icons/azure/integration/10219-icon-service-App-Configuration.svg diff --git a/assets/icons/azure/integration/10220-icon-service-SendGrid-Accounts.png b/core/assets/icons/azure/integration/10220-icon-service-SendGrid-Accounts.png similarity index 100% rename from assets/icons/azure/integration/10220-icon-service-SendGrid-Accounts.png rename to core/assets/icons/azure/integration/10220-icon-service-SendGrid-Accounts.png diff --git a/assets/icons/azure/integration/10220-icon-service-SendGrid-Accounts.svg b/core/assets/icons/azure/integration/10220-icon-service-SendGrid-Accounts.svg similarity index 100% rename from assets/icons/azure/integration/10220-icon-service-SendGrid-Accounts.svg rename to core/assets/icons/azure/integration/10220-icon-service-SendGrid-Accounts.svg diff --git a/assets/icons/azure/integration/10221-icon-service-Event-Grid-Subscriptions.png b/core/assets/icons/azure/integration/10221-icon-service-Event-Grid-Subscriptions.png similarity index 100% rename from assets/icons/azure/integration/10221-icon-service-Event-Grid-Subscriptions.png rename to core/assets/icons/azure/integration/10221-icon-service-Event-Grid-Subscriptions.png diff --git a/assets/icons/azure/integration/10221-icon-service-Event-Grid-Subscriptions.svg b/core/assets/icons/azure/integration/10221-icon-service-Event-Grid-Subscriptions.svg similarity index 100% rename from assets/icons/azure/integration/10221-icon-service-Event-Grid-Subscriptions.svg rename to core/assets/icons/azure/integration/10221-icon-service-Event-Grid-Subscriptions.svg diff --git a/assets/icons/azure/integration/10363-icon-service-Logic-Apps-Custom-Connector.png b/core/assets/icons/azure/integration/10363-icon-service-Logic-Apps-Custom-Connector.png similarity index 100% rename from assets/icons/azure/integration/10363-icon-service-Logic-Apps-Custom-Connector.png rename to core/assets/icons/azure/integration/10363-icon-service-Logic-Apps-Custom-Connector.png diff --git a/assets/icons/azure/integration/10363-icon-service-Logic-Apps-Custom-Connector.svg b/core/assets/icons/azure/integration/10363-icon-service-Logic-Apps-Custom-Connector.svg similarity index 100% rename from assets/icons/azure/integration/10363-icon-service-Logic-Apps-Custom-Connector.svg rename to core/assets/icons/azure/integration/10363-icon-service-Logic-Apps-Custom-Connector.svg diff --git a/assets/icons/azure/integration/10836-icon-service-Azure-Service-Bus.png b/core/assets/icons/azure/integration/10836-icon-service-Azure-Service-Bus.png similarity index 100% rename from assets/icons/azure/integration/10836-icon-service-Azure-Service-Bus.png rename to core/assets/icons/azure/integration/10836-icon-service-Azure-Service-Bus.png diff --git a/assets/icons/azure/integration/10836-icon-service-Azure-Service-Bus.svg b/core/assets/icons/azure/integration/10836-icon-service-Azure-Service-Bus.svg similarity index 100% rename from assets/icons/azure/integration/10836-icon-service-Azure-Service-Bus.svg rename to core/assets/icons/azure/integration/10836-icon-service-Azure-Service-Bus.svg diff --git a/assets/icons/azure/intune/00399-icon-service-Device-Security-Apple.png b/core/assets/icons/azure/intune/00399-icon-service-Device-Security-Apple.png similarity index 100% rename from assets/icons/azure/intune/00399-icon-service-Device-Security-Apple.png rename to core/assets/icons/azure/intune/00399-icon-service-Device-Security-Apple.png diff --git a/assets/icons/azure/intune/00399-icon-service-Device-Security-Apple.svg b/core/assets/icons/azure/intune/00399-icon-service-Device-Security-Apple.svg similarity index 100% rename from assets/icons/azure/intune/00399-icon-service-Device-Security-Apple.svg rename to core/assets/icons/azure/intune/00399-icon-service-Device-Security-Apple.svg diff --git a/assets/icons/azure/intune/00399-icon-service-Device-Security-Google.png b/core/assets/icons/azure/intune/00399-icon-service-Device-Security-Google.png similarity index 100% rename from assets/icons/azure/intune/00399-icon-service-Device-Security-Google.png rename to core/assets/icons/azure/intune/00399-icon-service-Device-Security-Google.png diff --git a/assets/icons/azure/intune/00399-icon-service-Device-Security-Google.svg b/core/assets/icons/azure/intune/00399-icon-service-Device-Security-Google.svg similarity index 100% rename from assets/icons/azure/intune/00399-icon-service-Device-Security-Google.svg rename to core/assets/icons/azure/intune/00399-icon-service-Device-Security-Google.svg diff --git a/assets/icons/azure/intune/00399-icon-service-Device-Security-Windows.png b/core/assets/icons/azure/intune/00399-icon-service-Device-Security-Windows.png similarity index 100% rename from assets/icons/azure/intune/00399-icon-service-Device-Security-Windows.png rename to core/assets/icons/azure/intune/00399-icon-service-Device-Security-Windows.png diff --git a/assets/icons/azure/intune/00399-icon-service-Device-Security-Windows.svg b/core/assets/icons/azure/intune/00399-icon-service-Device-Security-Windows.svg similarity index 100% rename from assets/icons/azure/intune/00399-icon-service-Device-Security-Windows.svg rename to core/assets/icons/azure/intune/00399-icon-service-Device-Security-Windows.svg diff --git a/assets/icons/azure/intune/029797391-icon-service-Engage-Center-Connect.png b/core/assets/icons/azure/intune/029797391-icon-service-Engage-Center-Connect.png similarity index 100% rename from assets/icons/azure/intune/029797391-icon-service-Engage-Center-Connect.png rename to core/assets/icons/azure/intune/029797391-icon-service-Engage-Center-Connect.png diff --git a/assets/icons/azure/intune/029797391-icon-service-Engage-Center-Connect.svg b/core/assets/icons/azure/intune/029797391-icon-service-Engage-Center-Connect.svg similarity index 100% rename from assets/icons/azure/intune/029797391-icon-service-Engage-Center-Connect.svg rename to core/assets/icons/azure/intune/029797391-icon-service-Engage-Center-Connect.svg diff --git a/assets/icons/azure/intune/10329-icon-service-Intune.png b/core/assets/icons/azure/intune/10329-icon-service-Intune.png similarity index 100% rename from assets/icons/azure/intune/10329-icon-service-Intune.png rename to core/assets/icons/azure/intune/10329-icon-service-Intune.png diff --git a/assets/icons/azure/intune/10329-icon-service-Intune.svg b/core/assets/icons/azure/intune/10329-icon-service-Intune.svg similarity index 100% rename from assets/icons/azure/intune/10329-icon-service-Intune.svg rename to core/assets/icons/azure/intune/10329-icon-service-Intune.svg diff --git a/assets/icons/azure/intune/10330-icon-service-eBooks.png b/core/assets/icons/azure/intune/10330-icon-service-eBooks.png similarity index 100% rename from assets/icons/azure/intune/10330-icon-service-eBooks.png rename to core/assets/icons/azure/intune/10330-icon-service-eBooks.png diff --git a/assets/icons/azure/intune/10330-icon-service-eBooks.svg b/core/assets/icons/azure/intune/10330-icon-service-eBooks.svg similarity index 100% rename from assets/icons/azure/intune/10330-icon-service-eBooks.svg rename to core/assets/icons/azure/intune/10330-icon-service-eBooks.svg diff --git a/assets/icons/azure/intune/10331-icon-service-Client-Apps.png b/core/assets/icons/azure/intune/10331-icon-service-Client-Apps.png similarity index 100% rename from assets/icons/azure/intune/10331-icon-service-Client-Apps.png rename to core/assets/icons/azure/intune/10331-icon-service-Client-Apps.png diff --git a/assets/icons/azure/intune/10331-icon-service-Client-Apps.svg b/core/assets/icons/azure/intune/10331-icon-service-Client-Apps.svg similarity index 100% rename from assets/icons/azure/intune/10331-icon-service-Client-Apps.svg rename to core/assets/icons/azure/intune/10331-icon-service-Client-Apps.svg diff --git a/assets/icons/azure/intune/10332-icon-service-Devices.png b/core/assets/icons/azure/intune/10332-icon-service-Devices.png similarity index 100% rename from assets/icons/azure/intune/10332-icon-service-Devices.png rename to core/assets/icons/azure/intune/10332-icon-service-Devices.png diff --git a/assets/icons/azure/intune/10332-icon-service-Devices.svg b/core/assets/icons/azure/intune/10332-icon-service-Devices.svg similarity index 100% rename from assets/icons/azure/intune/10332-icon-service-Devices.svg rename to core/assets/icons/azure/intune/10332-icon-service-Devices.svg diff --git a/assets/icons/azure/intune/10333-icon-service-Device-Compliance.png b/core/assets/icons/azure/intune/10333-icon-service-Device-Compliance.png similarity index 100% rename from assets/icons/azure/intune/10333-icon-service-Device-Compliance.png rename to core/assets/icons/azure/intune/10333-icon-service-Device-Compliance.png diff --git a/assets/icons/azure/intune/10333-icon-service-Device-Compliance.svg b/core/assets/icons/azure/intune/10333-icon-service-Device-Compliance.svg similarity index 100% rename from assets/icons/azure/intune/10333-icon-service-Device-Compliance.svg rename to core/assets/icons/azure/intune/10333-icon-service-Device-Compliance.svg diff --git a/assets/icons/azure/intune/10335-icon-service-Software-Updates.png b/core/assets/icons/azure/intune/10335-icon-service-Software-Updates.png similarity index 100% rename from assets/icons/azure/intune/10335-icon-service-Software-Updates.png rename to core/assets/icons/azure/intune/10335-icon-service-Software-Updates.png diff --git a/assets/icons/azure/intune/10335-icon-service-Software-Updates.svg b/core/assets/icons/azure/intune/10335-icon-service-Software-Updates.svg similarity index 100% rename from assets/icons/azure/intune/10335-icon-service-Software-Updates.svg rename to core/assets/icons/azure/intune/10335-icon-service-Software-Updates.svg diff --git a/assets/icons/azure/intune/10336-icon-service-Security-Baselines.png b/core/assets/icons/azure/intune/10336-icon-service-Security-Baselines.png similarity index 100% rename from assets/icons/azure/intune/10336-icon-service-Security-Baselines.png rename to core/assets/icons/azure/intune/10336-icon-service-Security-Baselines.png diff --git a/assets/icons/azure/intune/10336-icon-service-Security-Baselines.svg b/core/assets/icons/azure/intune/10336-icon-service-Security-Baselines.svg similarity index 100% rename from assets/icons/azure/intune/10336-icon-service-Security-Baselines.svg rename to core/assets/icons/azure/intune/10336-icon-service-Security-Baselines.svg diff --git a/assets/icons/azure/intune/10337-icon-service-Device-Enrollment.png b/core/assets/icons/azure/intune/10337-icon-service-Device-Enrollment.png similarity index 100% rename from assets/icons/azure/intune/10337-icon-service-Device-Enrollment.png rename to core/assets/icons/azure/intune/10337-icon-service-Device-Enrollment.png diff --git a/assets/icons/azure/intune/10337-icon-service-Device-Enrollment.svg b/core/assets/icons/azure/intune/10337-icon-service-Device-Enrollment.svg similarity index 100% rename from assets/icons/azure/intune/10337-icon-service-Device-Enrollment.svg rename to core/assets/icons/azure/intune/10337-icon-service-Device-Enrollment.svg diff --git a/assets/icons/azure/intune/10338-icon-service-Device-Configuration.png b/core/assets/icons/azure/intune/10338-icon-service-Device-Configuration.png similarity index 100% rename from assets/icons/azure/intune/10338-icon-service-Device-Configuration.png rename to core/assets/icons/azure/intune/10338-icon-service-Device-Configuration.png diff --git a/assets/icons/azure/intune/10338-icon-service-Device-Configuration.svg b/core/assets/icons/azure/intune/10338-icon-service-Device-Configuration.svg similarity index 100% rename from assets/icons/azure/intune/10338-icon-service-Device-Configuration.svg rename to core/assets/icons/azure/intune/10338-icon-service-Device-Configuration.svg diff --git a/assets/icons/azure/intune/10339-icon-service-Exchange-Access.png b/core/assets/icons/azure/intune/10339-icon-service-Exchange-Access.png similarity index 100% rename from assets/icons/azure/intune/10339-icon-service-Exchange-Access.png rename to core/assets/icons/azure/intune/10339-icon-service-Exchange-Access.png diff --git a/assets/icons/azure/intune/10339-icon-service-Exchange-Access.svg b/core/assets/icons/azure/intune/10339-icon-service-Exchange-Access.svg similarity index 100% rename from assets/icons/azure/intune/10339-icon-service-Exchange-Access.svg rename to core/assets/icons/azure/intune/10339-icon-service-Exchange-Access.svg diff --git a/assets/icons/azure/intune/10340-icon-service-Azure-AD-Roles-and-Administrators.png b/core/assets/icons/azure/intune/10340-icon-service-Azure-AD-Roles-and-Administrators.png similarity index 100% rename from assets/icons/azure/intune/10340-icon-service-Azure-AD-Roles-and-Administrators.png rename to core/assets/icons/azure/intune/10340-icon-service-Azure-AD-Roles-and-Administrators.png diff --git a/assets/icons/azure/intune/10340-icon-service-Azure-AD-Roles-and-Administrators.svg b/core/assets/icons/azure/intune/10340-icon-service-Azure-AD-Roles-and-Administrators.svg similarity index 100% rename from assets/icons/azure/intune/10340-icon-service-Azure-AD-Roles-and-Administrators.svg rename to core/assets/icons/azure/intune/10340-icon-service-Azure-AD-Roles-and-Administrators.svg diff --git a/assets/icons/azure/intune/10340-icon-service-Entra-Identity-Roles-and-Administrators.png b/core/assets/icons/azure/intune/10340-icon-service-Entra-Identity-Roles-and-Administrators.png similarity index 100% rename from assets/icons/azure/intune/10340-icon-service-Entra-Identity-Roles-and-Administrators.png rename to core/assets/icons/azure/intune/10340-icon-service-Entra-Identity-Roles-and-Administrators.png diff --git a/assets/icons/azure/intune/10340-icon-service-Entra-Identity-Roles-and-Administrators.svg b/core/assets/icons/azure/intune/10340-icon-service-Entra-Identity-Roles-and-Administrators.svg similarity index 100% rename from assets/icons/azure/intune/10340-icon-service-Entra-Identity-Roles-and-Administrators.svg rename to core/assets/icons/azure/intune/10340-icon-service-Entra-Identity-Roles-and-Administrators.svg diff --git a/assets/icons/azure/intune/10342-icon-service-Tenant-Status.png b/core/assets/icons/azure/intune/10342-icon-service-Tenant-Status.png similarity index 100% rename from assets/icons/azure/intune/10342-icon-service-Tenant-Status.png rename to core/assets/icons/azure/intune/10342-icon-service-Tenant-Status.png diff --git a/assets/icons/azure/intune/10342-icon-service-Tenant-Status.svg b/core/assets/icons/azure/intune/10342-icon-service-Tenant-Status.svg similarity index 100% rename from assets/icons/azure/intune/10342-icon-service-Tenant-Status.svg rename to core/assets/icons/azure/intune/10342-icon-service-Tenant-Status.svg diff --git a/assets/icons/azure/intune/10343-icon-service-Intune-For-Education.png b/core/assets/icons/azure/intune/10343-icon-service-Intune-For-Education.png similarity index 100% rename from assets/icons/azure/intune/10343-icon-service-Intune-For-Education.png rename to core/assets/icons/azure/intune/10343-icon-service-Intune-For-Education.png diff --git a/assets/icons/azure/intune/10343-icon-service-Intune-For-Education.svg b/core/assets/icons/azure/intune/10343-icon-service-Intune-For-Education.svg similarity index 100% rename from assets/icons/azure/intune/10343-icon-service-Intune-For-Education.svg rename to core/assets/icons/azure/intune/10343-icon-service-Intune-For-Education.svg diff --git a/assets/icons/azure/intune/10344-icon-service-Intune-App-Protection.png b/core/assets/icons/azure/intune/10344-icon-service-Intune-App-Protection.png similarity index 100% rename from assets/icons/azure/intune/10344-icon-service-Intune-App-Protection.png rename to core/assets/icons/azure/intune/10344-icon-service-Intune-App-Protection.png diff --git a/assets/icons/azure/intune/10344-icon-service-Intune-App-Protection.svg b/core/assets/icons/azure/intune/10344-icon-service-Intune-App-Protection.svg similarity index 100% rename from assets/icons/azure/intune/10344-icon-service-Intune-App-Protection.svg rename to core/assets/icons/azure/intune/10344-icon-service-Intune-App-Protection.svg diff --git a/assets/icons/azure/intune/10350-icon-service-Mindaro.png b/core/assets/icons/azure/intune/10350-icon-service-Mindaro.png similarity index 100% rename from assets/icons/azure/intune/10350-icon-service-Mindaro.png rename to core/assets/icons/azure/intune/10350-icon-service-Mindaro.png diff --git a/assets/icons/azure/intune/10350-icon-service-Mindaro.svg b/core/assets/icons/azure/intune/10350-icon-service-Mindaro.svg similarity index 100% rename from assets/icons/azure/intune/10350-icon-service-Mindaro.svg rename to core/assets/icons/azure/intune/10350-icon-service-Mindaro.svg diff --git a/assets/icons/azure/iot/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.png b/core/assets/icons/azure/iot/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.png similarity index 100% rename from assets/icons/azure/iot/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.png rename to core/assets/icons/azure/iot/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.png diff --git a/assets/icons/azure/iot/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.svg b/core/assets/icons/azure/iot/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.svg similarity index 100% rename from assets/icons/azure/iot/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.svg rename to core/assets/icons/azure/iot/00030-icon-service-Machine-Learning-Studio-(Classic)-Web-Services.svg diff --git a/assets/icons/azure/iot/00039-icon-service-Event-Hubs.png b/core/assets/icons/azure/iot/00039-icon-service-Event-Hubs.png similarity index 100% rename from assets/icons/azure/iot/00039-icon-service-Event-Hubs.png rename to core/assets/icons/azure/iot/00039-icon-service-Event-Hubs.png diff --git a/assets/icons/azure/iot/00039-icon-service-Event-Hubs.svg b/core/assets/icons/azure/iot/00039-icon-service-Event-Hubs.svg similarity index 100% rename from assets/icons/azure/iot/00039-icon-service-Event-Hubs.svg rename to core/assets/icons/azure/iot/00039-icon-service-Event-Hubs.svg diff --git a/assets/icons/azure/iot/00042-icon-service-Stream-Analytics-Jobs.png b/core/assets/icons/azure/iot/00042-icon-service-Stream-Analytics-Jobs.png similarity index 100% rename from assets/icons/azure/iot/00042-icon-service-Stream-Analytics-Jobs.png rename to core/assets/icons/azure/iot/00042-icon-service-Stream-Analytics-Jobs.png diff --git a/assets/icons/azure/iot/00042-icon-service-Stream-Analytics-Jobs.svg b/core/assets/icons/azure/iot/00042-icon-service-Stream-Analytics-Jobs.svg similarity index 100% rename from assets/icons/azure/iot/00042-icon-service-Stream-Analytics-Jobs.svg rename to core/assets/icons/azure/iot/00042-icon-service-Stream-Analytics-Jobs.svg diff --git a/assets/icons/azure/iot/00691-icon-service-Azure-Databox-Gateway.png b/core/assets/icons/azure/iot/00691-icon-service-Azure-Databox-Gateway.png similarity index 100% rename from assets/icons/azure/iot/00691-icon-service-Azure-Databox-Gateway.png rename to core/assets/icons/azure/iot/00691-icon-service-Azure-Databox-Gateway.png diff --git a/assets/icons/azure/iot/00691-icon-service-Azure-Databox-Gateway.svg b/core/assets/icons/azure/iot/00691-icon-service-Azure-Databox-Gateway.svg similarity index 100% rename from assets/icons/azure/iot/00691-icon-service-Azure-Databox-Gateway.svg rename to core/assets/icons/azure/iot/00691-icon-service-Azure-Databox-Gateway.svg diff --git a/assets/icons/azure/iot/01030-icon-service-Digital-Twins.png b/core/assets/icons/azure/iot/01030-icon-service-Digital-Twins.png similarity index 100% rename from assets/icons/azure/iot/01030-icon-service-Digital-Twins.png rename to core/assets/icons/azure/iot/01030-icon-service-Digital-Twins.png diff --git a/assets/icons/azure/iot/01030-icon-service-Digital-Twins.svg b/core/assets/icons/azure/iot/01030-icon-service-Digital-Twins.svg similarity index 100% rename from assets/icons/azure/iot/01030-icon-service-Digital-Twins.svg rename to core/assets/icons/azure/iot/01030-icon-service-Digital-Twins.svg diff --git a/assets/icons/azure/iot/02359-icon-service-Industrial-IoT.png b/core/assets/icons/azure/iot/02359-icon-service-Industrial-IoT.png similarity index 100% rename from assets/icons/azure/iot/02359-icon-service-Industrial-IoT.png rename to core/assets/icons/azure/iot/02359-icon-service-Industrial-IoT.png diff --git a/assets/icons/azure/iot/02359-icon-service-Industrial-IoT.svg b/core/assets/icons/azure/iot/02359-icon-service-Industrial-IoT.svg similarity index 100% rename from assets/icons/azure/iot/02359-icon-service-Industrial-IoT.svg rename to core/assets/icons/azure/iot/02359-icon-service-Industrial-IoT.svg diff --git a/assets/icons/azure/iot/02631-icon-service-Logic-Apps.png b/core/assets/icons/azure/iot/02631-icon-service-Logic-Apps.png similarity index 100% rename from assets/icons/azure/iot/02631-icon-service-Logic-Apps.png rename to core/assets/icons/azure/iot/02631-icon-service-Logic-Apps.png diff --git a/assets/icons/azure/iot/02631-icon-service-Logic-Apps.svg b/core/assets/icons/azure/iot/02631-icon-service-Logic-Apps.svg similarity index 100% rename from assets/icons/azure/iot/02631-icon-service-Logic-Apps.svg rename to core/assets/icons/azure/iot/02631-icon-service-Logic-Apps.svg diff --git a/assets/icons/azure/iot/03293-icon-service-Azure-Stack-HCI-Sizer.png b/core/assets/icons/azure/iot/03293-icon-service-Azure-Stack-HCI-Sizer.png similarity index 100% rename from assets/icons/azure/iot/03293-icon-service-Azure-Stack-HCI-Sizer.png rename to core/assets/icons/azure/iot/03293-icon-service-Azure-Stack-HCI-Sizer.png diff --git a/assets/icons/azure/iot/03293-icon-service-Azure-Stack-HCI-Sizer.svg b/core/assets/icons/azure/iot/03293-icon-service-Azure-Stack-HCI-Sizer.svg similarity index 100% rename from assets/icons/azure/iot/03293-icon-service-Azure-Stack-HCI-Sizer.svg rename to core/assets/icons/azure/iot/03293-icon-service-Azure-Stack-HCI-Sizer.svg diff --git a/assets/icons/azure/iot/03466-icon-service-Stack-HCI-Premium.png b/core/assets/icons/azure/iot/03466-icon-service-Stack-HCI-Premium.png similarity index 100% rename from assets/icons/azure/iot/03466-icon-service-Stack-HCI-Premium.png rename to core/assets/icons/azure/iot/03466-icon-service-Stack-HCI-Premium.png diff --git a/assets/icons/azure/iot/03466-icon-service-Stack-HCI-Premium.svg b/core/assets/icons/azure/iot/03466-icon-service-Stack-HCI-Premium.svg similarity index 100% rename from assets/icons/azure/iot/03466-icon-service-Stack-HCI-Premium.svg rename to core/assets/icons/azure/iot/03466-icon-service-Stack-HCI-Premium.svg diff --git a/assets/icons/azure/iot/03485-icon-service-Azure-IoT-Operations.png b/core/assets/icons/azure/iot/03485-icon-service-Azure-IoT-Operations.png similarity index 100% rename from assets/icons/azure/iot/03485-icon-service-Azure-IoT-Operations.png rename to core/assets/icons/azure/iot/03485-icon-service-Azure-IoT-Operations.png diff --git a/assets/icons/azure/iot/03485-icon-service-Azure-IoT-Operations.svg b/core/assets/icons/azure/iot/03485-icon-service-Azure-IoT-Operations.svg similarity index 100% rename from assets/icons/azure/iot/03485-icon-service-Azure-IoT-Operations.svg rename to core/assets/icons/azure/iot/03485-icon-service-Azure-IoT-Operations.svg diff --git a/assets/icons/azure/iot/03656-icon-service-Hybrid-Connectivity-Hub.png b/core/assets/icons/azure/iot/03656-icon-service-Hybrid-Connectivity-Hub.png similarity index 100% rename from assets/icons/azure/iot/03656-icon-service-Hybrid-Connectivity-Hub.png rename to core/assets/icons/azure/iot/03656-icon-service-Hybrid-Connectivity-Hub.png diff --git a/assets/icons/azure/iot/03656-icon-service-Hybrid-Connectivity-Hub.svg b/core/assets/icons/azure/iot/03656-icon-service-Hybrid-Connectivity-Hub.svg similarity index 100% rename from assets/icons/azure/iot/03656-icon-service-Hybrid-Connectivity-Hub.svg rename to core/assets/icons/azure/iot/03656-icon-service-Hybrid-Connectivity-Hub.svg diff --git a/assets/icons/azure/iot/10029-icon-service-Function-Apps.png b/core/assets/icons/azure/iot/10029-icon-service-Function-Apps.png similarity index 100% rename from assets/icons/azure/iot/10029-icon-service-Function-Apps.png rename to core/assets/icons/azure/iot/10029-icon-service-Function-Apps.png diff --git a/assets/icons/azure/iot/10029-icon-service-Function-Apps.svg b/core/assets/icons/azure/iot/10029-icon-service-Function-Apps.svg similarity index 100% rename from assets/icons/azure/iot/10029-icon-service-Function-Apps.svg rename to core/assets/icons/azure/iot/10029-icon-service-Function-Apps.svg diff --git a/assets/icons/azure/iot/10045-icon-service-Notification-Hubs.png b/core/assets/icons/azure/iot/10045-icon-service-Notification-Hubs.png similarity index 100% rename from assets/icons/azure/iot/10045-icon-service-Notification-Hubs.png rename to core/assets/icons/azure/iot/10045-icon-service-Notification-Hubs.png diff --git a/assets/icons/azure/iot/10045-icon-service-Notification-Hubs.svg b/core/assets/icons/azure/iot/10045-icon-service-Notification-Hubs.svg similarity index 100% rename from assets/icons/azure/iot/10045-icon-service-Notification-Hubs.svg rename to core/assets/icons/azure/iot/10045-icon-service-Notification-Hubs.svg diff --git a/assets/icons/azure/iot/10053-icon-service-Notification-Hub-Namespaces.png b/core/assets/icons/azure/iot/10053-icon-service-Notification-Hub-Namespaces.png similarity index 100% rename from assets/icons/azure/iot/10053-icon-service-Notification-Hub-Namespaces.png rename to core/assets/icons/azure/iot/10053-icon-service-Notification-Hub-Namespaces.png diff --git a/assets/icons/azure/iot/10053-icon-service-Notification-Hub-Namespaces.svg b/core/assets/icons/azure/iot/10053-icon-service-Notification-Hub-Namespaces.svg similarity index 100% rename from assets/icons/azure/iot/10053-icon-service-Notification-Hub-Namespaces.svg rename to core/assets/icons/azure/iot/10053-icon-service-Notification-Hub-Namespaces.svg diff --git a/assets/icons/azure/iot/10114-icon-service-Azure-Stack.png b/core/assets/icons/azure/iot/10114-icon-service-Azure-Stack.png similarity index 100% rename from assets/icons/azure/iot/10114-icon-service-Azure-Stack.png rename to core/assets/icons/azure/iot/10114-icon-service-Azure-Stack.png diff --git a/assets/icons/azure/iot/10114-icon-service-Azure-Stack.svg b/core/assets/icons/azure/iot/10114-icon-service-Azure-Stack.svg similarity index 100% rename from assets/icons/azure/iot/10114-icon-service-Azure-Stack.svg rename to core/assets/icons/azure/iot/10114-icon-service-Azure-Stack.svg diff --git a/assets/icons/azure/iot/10121-icon-service-Azure-Cosmos-DB.png b/core/assets/icons/azure/iot/10121-icon-service-Azure-Cosmos-DB.png similarity index 100% rename from assets/icons/azure/iot/10121-icon-service-Azure-Cosmos-DB.png rename to core/assets/icons/azure/iot/10121-icon-service-Azure-Cosmos-DB.png diff --git a/assets/icons/azure/iot/10121-icon-service-Azure-Cosmos-DB.svg b/core/assets/icons/azure/iot/10121-icon-service-Azure-Cosmos-DB.svg similarity index 100% rename from assets/icons/azure/iot/10121-icon-service-Azure-Cosmos-DB.svg rename to core/assets/icons/azure/iot/10121-icon-service-Azure-Cosmos-DB.svg diff --git a/assets/icons/azure/iot/10149-icon-service-Event-Hub-Clusters.png b/core/assets/icons/azure/iot/10149-icon-service-Event-Hub-Clusters.png similarity index 100% rename from assets/icons/azure/iot/10149-icon-service-Event-Hub-Clusters.png rename to core/assets/icons/azure/iot/10149-icon-service-Event-Hub-Clusters.png diff --git a/assets/icons/azure/iot/10149-icon-service-Event-Hub-Clusters.svg b/core/assets/icons/azure/iot/10149-icon-service-Event-Hub-Clusters.svg similarity index 100% rename from assets/icons/azure/iot/10149-icon-service-Event-Hub-Clusters.svg rename to core/assets/icons/azure/iot/10149-icon-service-Event-Hub-Clusters.svg diff --git a/assets/icons/azure/iot/10167-icon-service-Machine-Learning-Studio-Workspaces.png b/core/assets/icons/azure/iot/10167-icon-service-Machine-Learning-Studio-Workspaces.png similarity index 100% rename from assets/icons/azure/iot/10167-icon-service-Machine-Learning-Studio-Workspaces.png rename to core/assets/icons/azure/iot/10167-icon-service-Machine-Learning-Studio-Workspaces.png diff --git a/assets/icons/azure/iot/10167-icon-service-Machine-Learning-Studio-Workspaces.svg b/core/assets/icons/azure/iot/10167-icon-service-Machine-Learning-Studio-Workspaces.svg similarity index 100% rename from assets/icons/azure/iot/10167-icon-service-Machine-Learning-Studio-Workspaces.svg rename to core/assets/icons/azure/iot/10167-icon-service-Machine-Learning-Studio-Workspaces.svg diff --git a/assets/icons/azure/iot/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.png b/core/assets/icons/azure/iot/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.png similarity index 100% rename from assets/icons/azure/iot/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.png rename to core/assets/icons/azure/iot/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.png diff --git a/assets/icons/azure/iot/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.svg b/core/assets/icons/azure/iot/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.svg similarity index 100% rename from assets/icons/azure/iot/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.svg rename to core/assets/icons/azure/iot/10168-icon-service-Machine-Learning-Studio-Web-Service-Plans.svg diff --git a/assets/icons/azure/iot/10181-icon-service-Time-Series-Insights-Environments.png b/core/assets/icons/azure/iot/10181-icon-service-Time-Series-Insights-Environments.png similarity index 100% rename from assets/icons/azure/iot/10181-icon-service-Time-Series-Insights-Environments.png rename to core/assets/icons/azure/iot/10181-icon-service-Time-Series-Insights-Environments.png diff --git a/assets/icons/azure/iot/10181-icon-service-Time-Series-Insights-Environments.svg b/core/assets/icons/azure/iot/10181-icon-service-Time-Series-Insights-Environments.svg similarity index 100% rename from assets/icons/azure/iot/10181-icon-service-Time-Series-Insights-Environments.svg rename to core/assets/icons/azure/iot/10181-icon-service-Time-Series-Insights-Environments.svg diff --git a/assets/icons/azure/iot/10182-icon-service-IoT-Hub.png b/core/assets/icons/azure/iot/10182-icon-service-IoT-Hub.png similarity index 100% rename from assets/icons/azure/iot/10182-icon-service-IoT-Hub.png rename to core/assets/icons/azure/iot/10182-icon-service-IoT-Hub.png diff --git a/assets/icons/azure/iot/10182-icon-service-IoT-Hub.svg b/core/assets/icons/azure/iot/10182-icon-service-IoT-Hub.svg similarity index 100% rename from assets/icons/azure/iot/10182-icon-service-IoT-Hub.svg rename to core/assets/icons/azure/iot/10182-icon-service-IoT-Hub.svg diff --git a/assets/icons/azure/iot/10184-icon-service-IoT-Central-Applications.png b/core/assets/icons/azure/iot/10184-icon-service-IoT-Central-Applications.png similarity index 100% rename from assets/icons/azure/iot/10184-icon-service-IoT-Central-Applications.png rename to core/assets/icons/azure/iot/10184-icon-service-IoT-Central-Applications.png diff --git a/assets/icons/azure/iot/10184-icon-service-IoT-Central-Applications.svg b/core/assets/icons/azure/iot/10184-icon-service-IoT-Central-Applications.svg similarity index 100% rename from assets/icons/azure/iot/10184-icon-service-IoT-Central-Applications.svg rename to core/assets/icons/azure/iot/10184-icon-service-IoT-Central-Applications.svg diff --git a/assets/icons/azure/iot/10185-icon-service-Azure-Maps-Accounts.png b/core/assets/icons/azure/iot/10185-icon-service-Azure-Maps-Accounts.png similarity index 100% rename from assets/icons/azure/iot/10185-icon-service-Azure-Maps-Accounts.png rename to core/assets/icons/azure/iot/10185-icon-service-Azure-Maps-Accounts.png diff --git a/assets/icons/azure/iot/10185-icon-service-Azure-Maps-Accounts.svg b/core/assets/icons/azure/iot/10185-icon-service-Azure-Maps-Accounts.svg similarity index 100% rename from assets/icons/azure/iot/10185-icon-service-Azure-Maps-Accounts.svg rename to core/assets/icons/azure/iot/10185-icon-service-Azure-Maps-Accounts.svg diff --git a/assets/icons/azure/iot/10186-icon-service-IoT-Edge.png b/core/assets/icons/azure/iot/10186-icon-service-IoT-Edge.png similarity index 100% rename from assets/icons/azure/iot/10186-icon-service-IoT-Edge.png rename to core/assets/icons/azure/iot/10186-icon-service-IoT-Edge.png diff --git a/assets/icons/azure/iot/10186-icon-service-IoT-Edge.svg b/core/assets/icons/azure/iot/10186-icon-service-IoT-Edge.svg similarity index 100% rename from assets/icons/azure/iot/10186-icon-service-IoT-Edge.svg rename to core/assets/icons/azure/iot/10186-icon-service-IoT-Edge.svg diff --git a/assets/icons/azure/iot/10188-icon-service-Time-Series-Insights-Event-Sources.png b/core/assets/icons/azure/iot/10188-icon-service-Time-Series-Insights-Event-Sources.png similarity index 100% rename from assets/icons/azure/iot/10188-icon-service-Time-Series-Insights-Event-Sources.png rename to core/assets/icons/azure/iot/10188-icon-service-Time-Series-Insights-Event-Sources.png diff --git a/assets/icons/azure/iot/10188-icon-service-Time-Series-Insights-Event-Sources.svg b/core/assets/icons/azure/iot/10188-icon-service-Time-Series-Insights-Event-Sources.svg similarity index 100% rename from assets/icons/azure/iot/10188-icon-service-Time-Series-Insights-Event-Sources.svg rename to core/assets/icons/azure/iot/10188-icon-service-Time-Series-Insights-Event-Sources.svg diff --git a/assets/icons/azure/iot/10192-icon-service-Time-Series-Insights-Access-Policies.png b/core/assets/icons/azure/iot/10192-icon-service-Time-Series-Insights-Access-Policies.png similarity index 100% rename from assets/icons/azure/iot/10192-icon-service-Time-Series-Insights-Access-Policies.png rename to core/assets/icons/azure/iot/10192-icon-service-Time-Series-Insights-Access-Policies.png diff --git a/assets/icons/azure/iot/10192-icon-service-Time-Series-Insights-Access-Policies.svg b/core/assets/icons/azure/iot/10192-icon-service-Time-Series-Insights-Access-Policies.svg similarity index 100% rename from assets/icons/azure/iot/10192-icon-service-Time-Series-Insights-Access-Policies.svg rename to core/assets/icons/azure/iot/10192-icon-service-Time-Series-Insights-Access-Policies.svg diff --git a/assets/icons/azure/iot/10198-icon-service-Time-Series-Data-Sets.png b/core/assets/icons/azure/iot/10198-icon-service-Time-Series-Data-Sets.png similarity index 100% rename from assets/icons/azure/iot/10198-icon-service-Time-Series-Data-Sets.png rename to core/assets/icons/azure/iot/10198-icon-service-Time-Series-Data-Sets.png diff --git a/assets/icons/azure/iot/10198-icon-service-Time-Series-Data-Sets.svg b/core/assets/icons/azure/iot/10198-icon-service-Time-Series-Data-Sets.svg similarity index 100% rename from assets/icons/azure/iot/10198-icon-service-Time-Series-Data-Sets.svg rename to core/assets/icons/azure/iot/10198-icon-service-Time-Series-Data-Sets.svg diff --git a/assets/icons/azure/iot/10203-icon-service-Windows10-Core-Services.png b/core/assets/icons/azure/iot/10203-icon-service-Windows10-Core-Services.png similarity index 100% rename from assets/icons/azure/iot/10203-icon-service-Windows10-Core-Services.png rename to core/assets/icons/azure/iot/10203-icon-service-Windows10-Core-Services.png diff --git a/assets/icons/azure/iot/10203-icon-service-Windows10-Core-Services.svg b/core/assets/icons/azure/iot/10203-icon-service-Windows10-Core-Services.svg similarity index 100% rename from assets/icons/azure/iot/10203-icon-service-Windows10-Core-Services.svg rename to core/assets/icons/azure/iot/10203-icon-service-Windows10-Core-Services.svg diff --git a/assets/icons/azure/iot/10221-icon-service-Event-Grid-Subscriptions.png b/core/assets/icons/azure/iot/10221-icon-service-Event-Grid-Subscriptions.png similarity index 100% rename from assets/icons/azure/iot/10221-icon-service-Event-Grid-Subscriptions.png rename to core/assets/icons/azure/iot/10221-icon-service-Event-Grid-Subscriptions.png diff --git a/assets/icons/azure/iot/10221-icon-service-Event-Grid-Subscriptions.svg b/core/assets/icons/azure/iot/10221-icon-service-Event-Grid-Subscriptions.svg similarity index 100% rename from assets/icons/azure/iot/10221-icon-service-Event-Grid-Subscriptions.svg rename to core/assets/icons/azure/iot/10221-icon-service-Event-Grid-Subscriptions.svg diff --git a/assets/icons/azure/iot/10369-icon-service-Device-Provisioning-Services.png b/core/assets/icons/azure/iot/10369-icon-service-Device-Provisioning-Services.png similarity index 100% rename from assets/icons/azure/iot/10369-icon-service-Device-Provisioning-Services.png rename to core/assets/icons/azure/iot/10369-icon-service-Device-Provisioning-Services.png diff --git a/assets/icons/azure/iot/10369-icon-service-Device-Provisioning-Services.svg b/core/assets/icons/azure/iot/10369-icon-service-Device-Provisioning-Services.svg similarity index 100% rename from assets/icons/azure/iot/10369-icon-service-Device-Provisioning-Services.svg rename to core/assets/icons/azure/iot/10369-icon-service-Device-Provisioning-Services.svg diff --git a/assets/icons/azure/management-governance/00001-icon-service-Monitor.png b/core/assets/icons/azure/management-governance/00001-icon-service-Monitor.png similarity index 100% rename from assets/icons/azure/management-governance/00001-icon-service-Monitor.png rename to core/assets/icons/azure/management-governance/00001-icon-service-Monitor.png diff --git a/assets/icons/azure/management-governance/00001-icon-service-Monitor.svg b/core/assets/icons/azure/management-governance/00001-icon-service-Monitor.svg similarity index 100% rename from assets/icons/azure/management-governance/00001-icon-service-Monitor.svg rename to core/assets/icons/azure/management-governance/00001-icon-service-Monitor.svg diff --git a/assets/icons/azure/management-governance/00002-icon-service-Alerts.png b/core/assets/icons/azure/management-governance/00002-icon-service-Alerts.png similarity index 100% rename from assets/icons/azure/management-governance/00002-icon-service-Alerts.png rename to core/assets/icons/azure/management-governance/00002-icon-service-Alerts.png diff --git a/assets/icons/azure/management-governance/00002-icon-service-Alerts.svg b/core/assets/icons/azure/management-governance/00002-icon-service-Alerts.svg similarity index 100% rename from assets/icons/azure/management-governance/00002-icon-service-Alerts.svg rename to core/assets/icons/azure/management-governance/00002-icon-service-Alerts.svg diff --git a/assets/icons/azure/management-governance/00003-icon-service-Advisor.png b/core/assets/icons/azure/management-governance/00003-icon-service-Advisor.png similarity index 100% rename from assets/icons/azure/management-governance/00003-icon-service-Advisor.png rename to core/assets/icons/azure/management-governance/00003-icon-service-Advisor.png diff --git a/assets/icons/azure/management-governance/00003-icon-service-Advisor.svg b/core/assets/icons/azure/management-governance/00003-icon-service-Advisor.svg similarity index 100% rename from assets/icons/azure/management-governance/00003-icon-service-Advisor.svg rename to core/assets/icons/azure/management-governance/00003-icon-service-Advisor.svg diff --git a/assets/icons/azure/management-governance/00004-icon-service-Cost-Management-and-Billing.png b/core/assets/icons/azure/management-governance/00004-icon-service-Cost-Management-and-Billing.png similarity index 100% rename from assets/icons/azure/management-governance/00004-icon-service-Cost-Management-and-Billing.png rename to core/assets/icons/azure/management-governance/00004-icon-service-Cost-Management-and-Billing.png diff --git a/assets/icons/azure/management-governance/00004-icon-service-Cost-Management-and-Billing.svg b/core/assets/icons/azure/management-governance/00004-icon-service-Cost-Management-and-Billing.svg similarity index 100% rename from assets/icons/azure/management-governance/00004-icon-service-Cost-Management-and-Billing.svg rename to core/assets/icons/azure/management-governance/00004-icon-service-Cost-Management-and-Billing.svg diff --git a/assets/icons/azure/management-governance/00006-icon-service-Blueprints.png b/core/assets/icons/azure/management-governance/00006-icon-service-Blueprints.png similarity index 100% rename from assets/icons/azure/management-governance/00006-icon-service-Blueprints.png rename to core/assets/icons/azure/management-governance/00006-icon-service-Blueprints.png diff --git a/assets/icons/azure/management-governance/00006-icon-service-Blueprints.svg b/core/assets/icons/azure/management-governance/00006-icon-service-Blueprints.svg similarity index 100% rename from assets/icons/azure/management-governance/00006-icon-service-Blueprints.svg rename to core/assets/icons/azure/management-governance/00006-icon-service-Blueprints.svg diff --git a/assets/icons/azure/management-governance/00007-icon-service-Activity-Log.png b/core/assets/icons/azure/management-governance/00007-icon-service-Activity-Log.png similarity index 100% rename from assets/icons/azure/management-governance/00007-icon-service-Activity-Log.png rename to core/assets/icons/azure/management-governance/00007-icon-service-Activity-Log.png diff --git a/assets/icons/azure/management-governance/00007-icon-service-Activity-Log.svg b/core/assets/icons/azure/management-governance/00007-icon-service-Activity-Log.svg similarity index 100% rename from assets/icons/azure/management-governance/00007-icon-service-Activity-Log.svg rename to core/assets/icons/azure/management-governance/00007-icon-service-Activity-Log.svg diff --git a/assets/icons/azure/management-governance/00008-icon-service-Diagnostics-Settings.png b/core/assets/icons/azure/management-governance/00008-icon-service-Diagnostics-Settings.png similarity index 100% rename from assets/icons/azure/management-governance/00008-icon-service-Diagnostics-Settings.png rename to core/assets/icons/azure/management-governance/00008-icon-service-Diagnostics-Settings.png diff --git a/assets/icons/azure/management-governance/00008-icon-service-Diagnostics-Settings.svg b/core/assets/icons/azure/management-governance/00008-icon-service-Diagnostics-Settings.svg similarity index 100% rename from assets/icons/azure/management-governance/00008-icon-service-Diagnostics-Settings.svg rename to core/assets/icons/azure/management-governance/00008-icon-service-Diagnostics-Settings.svg diff --git a/assets/icons/azure/management-governance/00009-icon-service-Log-Analytics-Workspaces.png b/core/assets/icons/azure/management-governance/00009-icon-service-Log-Analytics-Workspaces.png similarity index 100% rename from assets/icons/azure/management-governance/00009-icon-service-Log-Analytics-Workspaces.png rename to core/assets/icons/azure/management-governance/00009-icon-service-Log-Analytics-Workspaces.png diff --git a/assets/icons/azure/management-governance/00009-icon-service-Log-Analytics-Workspaces.svg b/core/assets/icons/azure/management-governance/00009-icon-service-Log-Analytics-Workspaces.svg similarity index 100% rename from assets/icons/azure/management-governance/00009-icon-service-Log-Analytics-Workspaces.svg rename to core/assets/icons/azure/management-governance/00009-icon-service-Log-Analytics-Workspaces.svg diff --git a/assets/icons/azure/management-governance/00010-icon-service-Scheduler-Job-Collections.png b/core/assets/icons/azure/management-governance/00010-icon-service-Scheduler-Job-Collections.png similarity index 100% rename from assets/icons/azure/management-governance/00010-icon-service-Scheduler-Job-Collections.png rename to core/assets/icons/azure/management-governance/00010-icon-service-Scheduler-Job-Collections.png diff --git a/assets/icons/azure/management-governance/00010-icon-service-Scheduler-Job-Collections.svg b/core/assets/icons/azure/management-governance/00010-icon-service-Scheduler-Job-Collections.svg similarity index 100% rename from assets/icons/azure/management-governance/00010-icon-service-Scheduler-Job-Collections.svg rename to core/assets/icons/azure/management-governance/00010-icon-service-Scheduler-Job-Collections.svg diff --git a/assets/icons/azure/management-governance/00011-icon-service-Compliance.png b/core/assets/icons/azure/management-governance/00011-icon-service-Compliance.png similarity index 100% rename from assets/icons/azure/management-governance/00011-icon-service-Compliance.png rename to core/assets/icons/azure/management-governance/00011-icon-service-Compliance.png diff --git a/assets/icons/azure/management-governance/00011-icon-service-Compliance.svg b/core/assets/icons/azure/management-governance/00011-icon-service-Compliance.svg similarity index 100% rename from assets/icons/azure/management-governance/00011-icon-service-Compliance.svg rename to core/assets/icons/azure/management-governance/00011-icon-service-Compliance.svg diff --git a/assets/icons/azure/management-governance/00012-icon-service-Application-Insights.png b/core/assets/icons/azure/management-governance/00012-icon-service-Application-Insights.png similarity index 100% rename from assets/icons/azure/management-governance/00012-icon-service-Application-Insights.png rename to core/assets/icons/azure/management-governance/00012-icon-service-Application-Insights.png diff --git a/assets/icons/azure/management-governance/00012-icon-service-Application-Insights.svg b/core/assets/icons/azure/management-governance/00012-icon-service-Application-Insights.svg similarity index 100% rename from assets/icons/azure/management-governance/00012-icon-service-Application-Insights.svg rename to core/assets/icons/azure/management-governance/00012-icon-service-Application-Insights.svg diff --git a/assets/icons/azure/management-governance/00014-icon-service-My-Customers.png b/core/assets/icons/azure/management-governance/00014-icon-service-My-Customers.png similarity index 100% rename from assets/icons/azure/management-governance/00014-icon-service-My-Customers.png rename to core/assets/icons/azure/management-governance/00014-icon-service-My-Customers.png diff --git a/assets/icons/azure/management-governance/00014-icon-service-My-Customers.svg b/core/assets/icons/azure/management-governance/00014-icon-service-My-Customers.svg similarity index 100% rename from assets/icons/azure/management-governance/00014-icon-service-My-Customers.svg rename to core/assets/icons/azure/management-governance/00014-icon-service-My-Customers.svg diff --git a/assets/icons/azure/management-governance/00017-icon-service-Recovery-Services-Vaults.png b/core/assets/icons/azure/management-governance/00017-icon-service-Recovery-Services-Vaults.png similarity index 100% rename from assets/icons/azure/management-governance/00017-icon-service-Recovery-Services-Vaults.png rename to core/assets/icons/azure/management-governance/00017-icon-service-Recovery-Services-Vaults.png diff --git a/assets/icons/azure/management-governance/00017-icon-service-Recovery-Services-Vaults.svg b/core/assets/icons/azure/management-governance/00017-icon-service-Recovery-Services-Vaults.svg similarity index 100% rename from assets/icons/azure/management-governance/00017-icon-service-Recovery-Services-Vaults.svg rename to core/assets/icons/azure/management-governance/00017-icon-service-Recovery-Services-Vaults.svg diff --git a/assets/icons/azure/management-governance/00020-icon-service-Metrics.png b/core/assets/icons/azure/management-governance/00020-icon-service-Metrics.png similarity index 100% rename from assets/icons/azure/management-governance/00020-icon-service-Metrics.png rename to core/assets/icons/azure/management-governance/00020-icon-service-Metrics.png diff --git a/assets/icons/azure/management-governance/00020-icon-service-Metrics.svg b/core/assets/icons/azure/management-governance/00020-icon-service-Metrics.svg similarity index 100% rename from assets/icons/azure/management-governance/00020-icon-service-Metrics.svg rename to core/assets/icons/azure/management-governance/00020-icon-service-Metrics.svg diff --git a/assets/icons/azure/management-governance/00021-icon-service-Solutions.png b/core/assets/icons/azure/management-governance/00021-icon-service-Solutions.png similarity index 100% rename from assets/icons/azure/management-governance/00021-icon-service-Solutions.png rename to core/assets/icons/azure/management-governance/00021-icon-service-Solutions.png diff --git a/assets/icons/azure/management-governance/00021-icon-service-Solutions.svg b/core/assets/icons/azure/management-governance/00021-icon-service-Solutions.svg similarity index 100% rename from assets/icons/azure/management-governance/00021-icon-service-Solutions.svg rename to core/assets/icons/azure/management-governance/00021-icon-service-Solutions.svg diff --git a/assets/icons/azure/management-governance/00022-icon-service-Automation-Accounts.png b/core/assets/icons/azure/management-governance/00022-icon-service-Automation-Accounts.png similarity index 100% rename from assets/icons/azure/management-governance/00022-icon-service-Automation-Accounts.png rename to core/assets/icons/azure/management-governance/00022-icon-service-Automation-Accounts.png diff --git a/assets/icons/azure/management-governance/00022-icon-service-Automation-Accounts.svg b/core/assets/icons/azure/management-governance/00022-icon-service-Automation-Accounts.svg similarity index 100% rename from assets/icons/azure/management-governance/00022-icon-service-Automation-Accounts.svg rename to core/assets/icons/azure/management-governance/00022-icon-service-Automation-Accounts.svg diff --git a/assets/icons/azure/management-governance/00024-icon-service-Operation-Log-(Classic).png b/core/assets/icons/azure/management-governance/00024-icon-service-Operation-Log-(Classic).png similarity index 100% rename from assets/icons/azure/management-governance/00024-icon-service-Operation-Log-(Classic).png rename to core/assets/icons/azure/management-governance/00024-icon-service-Operation-Log-(Classic).png diff --git a/assets/icons/azure/management-governance/00024-icon-service-Operation-Log-(Classic).svg b/core/assets/icons/azure/management-governance/00024-icon-service-Operation-Log-(Classic).svg similarity index 100% rename from assets/icons/azure/management-governance/00024-icon-service-Operation-Log-(Classic).svg rename to core/assets/icons/azure/management-governance/00024-icon-service-Operation-Log-(Classic).svg diff --git a/assets/icons/azure/management-governance/00025-icon-service-Service-Providers.png b/core/assets/icons/azure/management-governance/00025-icon-service-Service-Providers.png similarity index 100% rename from assets/icons/azure/management-governance/00025-icon-service-Service-Providers.png rename to core/assets/icons/azure/management-governance/00025-icon-service-Service-Providers.png diff --git a/assets/icons/azure/management-governance/00025-icon-service-Service-Providers.svg b/core/assets/icons/azure/management-governance/00025-icon-service-Service-Providers.svg similarity index 100% rename from assets/icons/azure/management-governance/00025-icon-service-Service-Providers.svg rename to core/assets/icons/azure/management-governance/00025-icon-service-Service-Providers.svg diff --git a/assets/icons/azure/management-governance/00026-icon-service-Education.png b/core/assets/icons/azure/management-governance/00026-icon-service-Education.png similarity index 100% rename from assets/icons/azure/management-governance/00026-icon-service-Education.png rename to core/assets/icons/azure/management-governance/00026-icon-service-Education.png diff --git a/assets/icons/azure/management-governance/00026-icon-service-Education.svg b/core/assets/icons/azure/management-governance/00026-icon-service-Education.svg similarity index 100% rename from assets/icons/azure/management-governance/00026-icon-service-Education.svg rename to core/assets/icons/azure/management-governance/00026-icon-service-Education.svg diff --git a/assets/icons/azure/management-governance/00027-icon-service-Service-Catalog-MAD.png b/core/assets/icons/azure/management-governance/00027-icon-service-Service-Catalog-MAD.png similarity index 100% rename from assets/icons/azure/management-governance/00027-icon-service-Service-Catalog-MAD.png rename to core/assets/icons/azure/management-governance/00027-icon-service-Service-Catalog-MAD.png diff --git a/assets/icons/azure/management-governance/00027-icon-service-Service-Catalog-MAD.svg b/core/assets/icons/azure/management-governance/00027-icon-service-Service-Catalog-MAD.svg similarity index 100% rename from assets/icons/azure/management-governance/00027-icon-service-Service-Catalog-MAD.svg rename to core/assets/icons/azure/management-governance/00027-icon-service-Service-Catalog-MAD.svg diff --git a/assets/icons/azure/management-governance/00408-icon-service-Intune-Trends.png b/core/assets/icons/azure/management-governance/00408-icon-service-Intune-Trends.png similarity index 100% rename from assets/icons/azure/management-governance/00408-icon-service-Intune-Trends.png rename to core/assets/icons/azure/management-governance/00408-icon-service-Intune-Trends.png diff --git a/assets/icons/azure/management-governance/00408-icon-service-Intune-Trends.svg b/core/assets/icons/azure/management-governance/00408-icon-service-Intune-Trends.svg similarity index 100% rename from assets/icons/azure/management-governance/00408-icon-service-Intune-Trends.svg rename to core/assets/icons/azure/management-governance/00408-icon-service-Intune-Trends.svg diff --git a/assets/icons/azure/management-governance/00471-icon-service-Azure-Lighthouse.png b/core/assets/icons/azure/management-governance/00471-icon-service-Azure-Lighthouse.png similarity index 100% rename from assets/icons/azure/management-governance/00471-icon-service-Azure-Lighthouse.png rename to core/assets/icons/azure/management-governance/00471-icon-service-Azure-Lighthouse.png diff --git a/assets/icons/azure/management-governance/00471-icon-service-Azure-Lighthouse.svg b/core/assets/icons/azure/management-governance/00471-icon-service-Azure-Lighthouse.svg similarity index 100% rename from assets/icons/azure/management-governance/00471-icon-service-Azure-Lighthouse.svg rename to core/assets/icons/azure/management-governance/00471-icon-service-Azure-Lighthouse.svg diff --git a/assets/icons/azure/management-governance/00571-icon-service-Universal-Print.png b/core/assets/icons/azure/management-governance/00571-icon-service-Universal-Print.png similarity index 100% rename from assets/icons/azure/management-governance/00571-icon-service-Universal-Print.png rename to core/assets/icons/azure/management-governance/00571-icon-service-Universal-Print.png diff --git a/assets/icons/azure/management-governance/00571-icon-service-Universal-Print.svg b/core/assets/icons/azure/management-governance/00571-icon-service-Universal-Print.svg similarity index 100% rename from assets/icons/azure/management-governance/00571-icon-service-Universal-Print.svg rename to core/assets/icons/azure/management-governance/00571-icon-service-Universal-Print.svg diff --git a/assets/icons/azure/management-governance/00756-icon-service-Azure-Arc.png b/core/assets/icons/azure/management-governance/00756-icon-service-Azure-Arc.png similarity index 100% rename from assets/icons/azure/management-governance/00756-icon-service-Azure-Arc.png rename to core/assets/icons/azure/management-governance/00756-icon-service-Azure-Arc.png diff --git a/assets/icons/azure/management-governance/00756-icon-service-Azure-Arc.svg b/core/assets/icons/azure/management-governance/00756-icon-service-Azure-Arc.svg similarity index 100% rename from assets/icons/azure/management-governance/00756-icon-service-Azure-Arc.svg rename to core/assets/icons/azure/management-governance/00756-icon-service-Azure-Arc.svg diff --git a/assets/icons/azure/management-governance/01710-icon-service-Arc-Machines.png b/core/assets/icons/azure/management-governance/01710-icon-service-Arc-Machines.png similarity index 100% rename from assets/icons/azure/management-governance/01710-icon-service-Arc-Machines.png rename to core/assets/icons/azure/management-governance/01710-icon-service-Arc-Machines.png diff --git a/assets/icons/azure/management-governance/01710-icon-service-Arc-Machines.svg b/core/assets/icons/azure/management-governance/01710-icon-service-Arc-Machines.svg similarity index 100% rename from assets/icons/azure/management-governance/01710-icon-service-Arc-Machines.svg rename to core/assets/icons/azure/management-governance/01710-icon-service-Arc-Machines.svg diff --git a/assets/icons/azure/management-governance/030777508-icon-service-Service-Group-Relationships.png b/core/assets/icons/azure/management-governance/030777508-icon-service-Service-Group-Relationships.png similarity index 100% rename from assets/icons/azure/management-governance/030777508-icon-service-Service-Group-Relationships.png rename to core/assets/icons/azure/management-governance/030777508-icon-service-Service-Group-Relationships.png diff --git a/assets/icons/azure/management-governance/030777508-icon-service-Service-Group-Relationships.svg b/core/assets/icons/azure/management-governance/030777508-icon-service-Service-Group-Relationships.svg similarity index 100% rename from assets/icons/azure/management-governance/030777508-icon-service-Service-Group-Relationships.svg rename to core/assets/icons/azure/management-governance/030777508-icon-service-Service-Group-Relationships.svg diff --git a/assets/icons/azure/management-governance/03366-icon-service-Resources-Provider.png b/core/assets/icons/azure/management-governance/03366-icon-service-Resources-Provider.png similarity index 100% rename from assets/icons/azure/management-governance/03366-icon-service-Resources-Provider.png rename to core/assets/icons/azure/management-governance/03366-icon-service-Resources-Provider.png diff --git a/assets/icons/azure/management-governance/03366-icon-service-Resources-Provider.svg b/core/assets/icons/azure/management-governance/03366-icon-service-Resources-Provider.svg similarity index 100% rename from assets/icons/azure/management-governance/03366-icon-service-Resources-Provider.svg rename to core/assets/icons/azure/management-governance/03366-icon-service-Resources-Provider.svg diff --git a/assets/icons/azure/management-governance/10303-icon-service-User-Privacy.png b/core/assets/icons/azure/management-governance/10303-icon-service-User-Privacy.png similarity index 100% rename from assets/icons/azure/management-governance/10303-icon-service-User-Privacy.png rename to core/assets/icons/azure/management-governance/10303-icon-service-User-Privacy.png diff --git a/assets/icons/azure/management-governance/10303-icon-service-User-Privacy.svg b/core/assets/icons/azure/management-governance/10303-icon-service-User-Privacy.svg similarity index 100% rename from assets/icons/azure/management-governance/10303-icon-service-User-Privacy.svg rename to core/assets/icons/azure/management-governance/10303-icon-service-User-Privacy.svg diff --git a/assets/icons/azure/management-governance/10311-icon-service-Managed-Desktop.png b/core/assets/icons/azure/management-governance/10311-icon-service-Managed-Desktop.png similarity index 100% rename from assets/icons/azure/management-governance/10311-icon-service-Managed-Desktop.png rename to core/assets/icons/azure/management-governance/10311-icon-service-Managed-Desktop.png diff --git a/assets/icons/azure/management-governance/10311-icon-service-Managed-Desktop.svg b/core/assets/icons/azure/management-governance/10311-icon-service-Managed-Desktop.svg similarity index 100% rename from assets/icons/azure/management-governance/10311-icon-service-Managed-Desktop.svg rename to core/assets/icons/azure/management-governance/10311-icon-service-Managed-Desktop.svg diff --git a/assets/icons/azure/management-governance/10313-icon-service-Managed-Applications-Center.png b/core/assets/icons/azure/management-governance/10313-icon-service-Managed-Applications-Center.png similarity index 100% rename from assets/icons/azure/management-governance/10313-icon-service-Managed-Applications-Center.png rename to core/assets/icons/azure/management-governance/10313-icon-service-Managed-Applications-Center.png diff --git a/assets/icons/azure/management-governance/10313-icon-service-Managed-Applications-Center.svg b/core/assets/icons/azure/management-governance/10313-icon-service-Managed-Applications-Center.svg similarity index 100% rename from assets/icons/azure/management-governance/10313-icon-service-Managed-Applications-Center.svg rename to core/assets/icons/azure/management-governance/10313-icon-service-Managed-Applications-Center.svg diff --git a/assets/icons/azure/management-governance/10314-icon-service-Customer-Lockbox-for-Microsoft-Azure.png b/core/assets/icons/azure/management-governance/10314-icon-service-Customer-Lockbox-for-Microsoft-Azure.png similarity index 100% rename from assets/icons/azure/management-governance/10314-icon-service-Customer-Lockbox-for-Microsoft-Azure.png rename to core/assets/icons/azure/management-governance/10314-icon-service-Customer-Lockbox-for-Microsoft-Azure.png diff --git a/assets/icons/azure/management-governance/10314-icon-service-Customer-Lockbox-for-Microsoft-Azure.svg b/core/assets/icons/azure/management-governance/10314-icon-service-Customer-Lockbox-for-Microsoft-Azure.svg similarity index 100% rename from assets/icons/azure/management-governance/10314-icon-service-Customer-Lockbox-for-Microsoft-Azure.svg rename to core/assets/icons/azure/management-governance/10314-icon-service-Customer-Lockbox-for-Microsoft-Azure.svg diff --git a/assets/icons/azure/management-governance/10316-icon-service-Policy.png b/core/assets/icons/azure/management-governance/10316-icon-service-Policy.png similarity index 100% rename from assets/icons/azure/management-governance/10316-icon-service-Policy.png rename to core/assets/icons/azure/management-governance/10316-icon-service-Policy.png diff --git a/assets/icons/azure/management-governance/10316-icon-service-Policy.svg b/core/assets/icons/azure/management-governance/10316-icon-service-Policy.svg similarity index 100% rename from assets/icons/azure/management-governance/10316-icon-service-Policy.svg rename to core/assets/icons/azure/management-governance/10316-icon-service-Policy.svg diff --git a/assets/icons/azure/management-governance/10318-icon-service-Resource-Graph-Explorer.png b/core/assets/icons/azure/management-governance/10318-icon-service-Resource-Graph-Explorer.png similarity index 100% rename from assets/icons/azure/management-governance/10318-icon-service-Resource-Graph-Explorer.png rename to core/assets/icons/azure/management-governance/10318-icon-service-Resource-Graph-Explorer.png diff --git a/assets/icons/azure/management-governance/10318-icon-service-Resource-Graph-Explorer.svg b/core/assets/icons/azure/management-governance/10318-icon-service-Resource-Graph-Explorer.svg similarity index 100% rename from assets/icons/azure/management-governance/10318-icon-service-Resource-Graph-Explorer.svg rename to core/assets/icons/azure/management-governance/10318-icon-service-Resource-Graph-Explorer.svg diff --git a/assets/icons/azure/management-governance/10450-icon-service-MachinesAzureArc.png b/core/assets/icons/azure/management-governance/10450-icon-service-MachinesAzureArc.png similarity index 100% rename from assets/icons/azure/management-governance/10450-icon-service-MachinesAzureArc.png rename to core/assets/icons/azure/management-governance/10450-icon-service-MachinesAzureArc.png diff --git a/assets/icons/azure/management-governance/10450-icon-service-MachinesAzureArc.svg b/core/assets/icons/azure/management-governance/10450-icon-service-MachinesAzureArc.svg similarity index 100% rename from assets/icons/azure/management-governance/10450-icon-service-MachinesAzureArc.svg rename to core/assets/icons/azure/management-governance/10450-icon-service-MachinesAzureArc.svg diff --git a/assets/icons/azure/menu/00787-icon-service-Keys.png b/core/assets/icons/azure/menu/00787-icon-service-Keys.png similarity index 100% rename from assets/icons/azure/menu/00787-icon-service-Keys.png rename to core/assets/icons/azure/menu/00787-icon-service-Keys.png diff --git a/assets/icons/azure/menu/00787-icon-service-Keys.svg b/core/assets/icons/azure/menu/00787-icon-service-Keys.svg similarity index 100% rename from assets/icons/azure/menu/00787-icon-service-Keys.svg rename to core/assets/icons/azure/menu/00787-icon-service-Keys.svg diff --git a/assets/icons/azure/migrate/00004-icon-service-Cost-Management-and-Billing.png b/core/assets/icons/azure/migrate/00004-icon-service-Cost-Management-and-Billing.png similarity index 100% rename from assets/icons/azure/migrate/00004-icon-service-Cost-Management-and-Billing.png rename to core/assets/icons/azure/migrate/00004-icon-service-Cost-Management-and-Billing.png diff --git a/assets/icons/azure/migrate/00004-icon-service-Cost-Management-and-Billing.svg b/core/assets/icons/azure/migrate/00004-icon-service-Cost-Management-and-Billing.svg similarity index 100% rename from assets/icons/azure/migrate/00004-icon-service-Cost-Management-and-Billing.svg rename to core/assets/icons/azure/migrate/00004-icon-service-Cost-Management-and-Billing.svg diff --git a/assets/icons/azure/migrate/00017-icon-service-Recovery-Services-Vaults.png b/core/assets/icons/azure/migrate/00017-icon-service-Recovery-Services-Vaults.png similarity index 100% rename from assets/icons/azure/migrate/00017-icon-service-Recovery-Services-Vaults.png rename to core/assets/icons/azure/migrate/00017-icon-service-Recovery-Services-Vaults.png diff --git a/assets/icons/azure/migrate/00017-icon-service-Recovery-Services-Vaults.svg b/core/assets/icons/azure/migrate/00017-icon-service-Recovery-Services-Vaults.svg similarity index 100% rename from assets/icons/azure/migrate/00017-icon-service-Recovery-Services-Vaults.svg rename to core/assets/icons/azure/migrate/00017-icon-service-Recovery-Services-Vaults.svg diff --git a/assets/icons/azure/migrate/00691-icon-service-Azure-Databox-Gateway.png b/core/assets/icons/azure/migrate/00691-icon-service-Azure-Databox-Gateway.png similarity index 100% rename from assets/icons/azure/migrate/00691-icon-service-Azure-Databox-Gateway.png rename to core/assets/icons/azure/migrate/00691-icon-service-Azure-Databox-Gateway.png diff --git a/assets/icons/azure/migrate/00691-icon-service-Azure-Databox-Gateway.svg b/core/assets/icons/azure/migrate/00691-icon-service-Azure-Databox-Gateway.svg similarity index 100% rename from assets/icons/azure/migrate/00691-icon-service-Azure-Databox-Gateway.svg rename to core/assets/icons/azure/migrate/00691-icon-service-Azure-Databox-Gateway.svg diff --git a/assets/icons/azure/migrate/10094-icon-service-Data-Box.png b/core/assets/icons/azure/migrate/10094-icon-service-Data-Box.png similarity index 100% rename from assets/icons/azure/migrate/10094-icon-service-Data-Box.png rename to core/assets/icons/azure/migrate/10094-icon-service-Data-Box.png diff --git a/assets/icons/azure/migrate/10094-icon-service-Data-Box.svg b/core/assets/icons/azure/migrate/10094-icon-service-Data-Box.svg similarity index 100% rename from assets/icons/azure/migrate/10094-icon-service-Data-Box.svg rename to core/assets/icons/azure/migrate/10094-icon-service-Data-Box.svg diff --git a/assets/icons/azure/migrate/10095-icon-service-Azure-Stack-Edge.png b/core/assets/icons/azure/migrate/10095-icon-service-Azure-Stack-Edge.png similarity index 100% rename from assets/icons/azure/migrate/10095-icon-service-Azure-Stack-Edge.png rename to core/assets/icons/azure/migrate/10095-icon-service-Azure-Stack-Edge.png diff --git a/assets/icons/azure/migrate/10095-icon-service-Azure-Stack-Edge.svg b/core/assets/icons/azure/migrate/10095-icon-service-Azure-Stack-Edge.svg similarity index 100% rename from assets/icons/azure/migrate/10095-icon-service-Azure-Stack-Edge.svg rename to core/assets/icons/azure/migrate/10095-icon-service-Azure-Stack-Edge.svg diff --git a/assets/icons/azure/migrate/10281-icon-service-Azure-Migrate.png b/core/assets/icons/azure/migrate/10281-icon-service-Azure-Migrate.png similarity index 100% rename from assets/icons/azure/migrate/10281-icon-service-Azure-Migrate.png rename to core/assets/icons/azure/migrate/10281-icon-service-Azure-Migrate.png diff --git a/assets/icons/azure/migrate/10281-icon-service-Azure-Migrate.svg b/core/assets/icons/azure/migrate/10281-icon-service-Azure-Migrate.svg similarity index 100% rename from assets/icons/azure/migrate/10281-icon-service-Azure-Migrate.svg rename to core/assets/icons/azure/migrate/10281-icon-service-Azure-Migrate.svg diff --git a/assets/icons/azure/migration/10133-icon-service-Azure-Database-Migration-Services.png b/core/assets/icons/azure/migration/10133-icon-service-Azure-Database-Migration-Services.png similarity index 100% rename from assets/icons/azure/migration/10133-icon-service-Azure-Database-Migration-Services.png rename to core/assets/icons/azure/migration/10133-icon-service-Azure-Database-Migration-Services.png diff --git a/assets/icons/azure/migration/10133-icon-service-Azure-Database-Migration-Services.svg b/core/assets/icons/azure/migration/10133-icon-service-Azure-Database-Migration-Services.svg similarity index 100% rename from assets/icons/azure/migration/10133-icon-service-Azure-Database-Migration-Services.svg rename to core/assets/icons/azure/migration/10133-icon-service-Azure-Database-Migration-Services.svg diff --git a/assets/icons/azure/mixed-reality/00698-icon-service-Remote-Rendering.png b/core/assets/icons/azure/mixed-reality/00698-icon-service-Remote-Rendering.png similarity index 100% rename from assets/icons/azure/mixed-reality/00698-icon-service-Remote-Rendering.png rename to core/assets/icons/azure/mixed-reality/00698-icon-service-Remote-Rendering.png diff --git a/assets/icons/azure/mixed-reality/00698-icon-service-Remote-Rendering.svg b/core/assets/icons/azure/mixed-reality/00698-icon-service-Remote-Rendering.svg similarity index 100% rename from assets/icons/azure/mixed-reality/00698-icon-service-Remote-Rendering.svg rename to core/assets/icons/azure/mixed-reality/00698-icon-service-Remote-Rendering.svg diff --git a/assets/icons/azure/mixed-reality/10352-icon-service-Spatial-Anchor-Accounts.png b/core/assets/icons/azure/mixed-reality/10352-icon-service-Spatial-Anchor-Accounts.png similarity index 100% rename from assets/icons/azure/mixed-reality/10352-icon-service-Spatial-Anchor-Accounts.png rename to core/assets/icons/azure/mixed-reality/10352-icon-service-Spatial-Anchor-Accounts.png diff --git a/assets/icons/azure/mixed-reality/10352-icon-service-Spatial-Anchor-Accounts.svg b/core/assets/icons/azure/mixed-reality/10352-icon-service-Spatial-Anchor-Accounts.svg similarity index 100% rename from assets/icons/azure/mixed-reality/10352-icon-service-Spatial-Anchor-Accounts.svg rename to core/assets/icons/azure/mixed-reality/10352-icon-service-Spatial-Anchor-Accounts.svg diff --git a/assets/icons/azure/mobile/03335-icon-service-Power-Platform.png b/core/assets/icons/azure/mobile/03335-icon-service-Power-Platform.png similarity index 100% rename from assets/icons/azure/mobile/03335-icon-service-Power-Platform.png rename to core/assets/icons/azure/mobile/03335-icon-service-Power-Platform.png diff --git a/assets/icons/azure/mobile/03335-icon-service-Power-Platform.svg b/core/assets/icons/azure/mobile/03335-icon-service-Power-Platform.svg similarity index 100% rename from assets/icons/azure/mobile/03335-icon-service-Power-Platform.svg rename to core/assets/icons/azure/mobile/03335-icon-service-Power-Platform.svg diff --git a/assets/icons/azure/mobile/10035-icon-service-App-Services.png b/core/assets/icons/azure/mobile/10035-icon-service-App-Services.png similarity index 100% rename from assets/icons/azure/mobile/10035-icon-service-App-Services.png rename to core/assets/icons/azure/mobile/10035-icon-service-App-Services.png diff --git a/assets/icons/azure/mobile/10035-icon-service-App-Services.svg b/core/assets/icons/azure/mobile/10035-icon-service-App-Services.svg similarity index 100% rename from assets/icons/azure/mobile/10035-icon-service-App-Services.svg rename to core/assets/icons/azure/mobile/10035-icon-service-App-Services.svg diff --git a/assets/icons/azure/mobile/10045-icon-service-Notification-Hubs.png b/core/assets/icons/azure/mobile/10045-icon-service-Notification-Hubs.png similarity index 100% rename from assets/icons/azure/mobile/10045-icon-service-Notification-Hubs.png rename to core/assets/icons/azure/mobile/10045-icon-service-Notification-Hubs.png diff --git a/assets/icons/azure/mobile/10045-icon-service-Notification-Hubs.svg b/core/assets/icons/azure/mobile/10045-icon-service-Notification-Hubs.svg similarity index 100% rename from assets/icons/azure/mobile/10045-icon-service-Notification-Hubs.svg rename to core/assets/icons/azure/mobile/10045-icon-service-Notification-Hubs.svg diff --git a/assets/icons/azure/monitor/00001-icon-service-Monitor.png b/core/assets/icons/azure/monitor/00001-icon-service-Monitor.png similarity index 100% rename from assets/icons/azure/monitor/00001-icon-service-Monitor.png rename to core/assets/icons/azure/monitor/00001-icon-service-Monitor.png diff --git a/assets/icons/azure/monitor/00001-icon-service-Monitor.svg b/core/assets/icons/azure/monitor/00001-icon-service-Monitor.svg similarity index 100% rename from assets/icons/azure/monitor/00001-icon-service-Monitor.svg rename to core/assets/icons/azure/monitor/00001-icon-service-Monitor.svg diff --git a/assets/icons/azure/monitor/00007-icon-service-Activity-Log.png b/core/assets/icons/azure/monitor/00007-icon-service-Activity-Log.png similarity index 100% rename from assets/icons/azure/monitor/00007-icon-service-Activity-Log.png rename to core/assets/icons/azure/monitor/00007-icon-service-Activity-Log.png diff --git a/assets/icons/azure/monitor/00007-icon-service-Activity-Log.svg b/core/assets/icons/azure/monitor/00007-icon-service-Activity-Log.svg similarity index 100% rename from assets/icons/azure/monitor/00007-icon-service-Activity-Log.svg rename to core/assets/icons/azure/monitor/00007-icon-service-Activity-Log.svg diff --git a/assets/icons/azure/monitor/00008-icon-service-Diagnostics-Settings.png b/core/assets/icons/azure/monitor/00008-icon-service-Diagnostics-Settings.png similarity index 100% rename from assets/icons/azure/monitor/00008-icon-service-Diagnostics-Settings.png rename to core/assets/icons/azure/monitor/00008-icon-service-Diagnostics-Settings.png diff --git a/assets/icons/azure/monitor/00008-icon-service-Diagnostics-Settings.svg b/core/assets/icons/azure/monitor/00008-icon-service-Diagnostics-Settings.svg similarity index 100% rename from assets/icons/azure/monitor/00008-icon-service-Diagnostics-Settings.svg rename to core/assets/icons/azure/monitor/00008-icon-service-Diagnostics-Settings.svg diff --git a/assets/icons/azure/monitor/00009-icon-service-Log-Analytics-Workspaces.png b/core/assets/icons/azure/monitor/00009-icon-service-Log-Analytics-Workspaces.png similarity index 100% rename from assets/icons/azure/monitor/00009-icon-service-Log-Analytics-Workspaces.png rename to core/assets/icons/azure/monitor/00009-icon-service-Log-Analytics-Workspaces.png diff --git a/assets/icons/azure/monitor/00009-icon-service-Log-Analytics-Workspaces.svg b/core/assets/icons/azure/monitor/00009-icon-service-Log-Analytics-Workspaces.svg similarity index 100% rename from assets/icons/azure/monitor/00009-icon-service-Log-Analytics-Workspaces.svg rename to core/assets/icons/azure/monitor/00009-icon-service-Log-Analytics-Workspaces.svg diff --git a/assets/icons/azure/monitor/00012-icon-service-Application-Insights.png b/core/assets/icons/azure/monitor/00012-icon-service-Application-Insights.png similarity index 100% rename from assets/icons/azure/monitor/00012-icon-service-Application-Insights.png rename to core/assets/icons/azure/monitor/00012-icon-service-Application-Insights.png diff --git a/assets/icons/azure/monitor/00012-icon-service-Application-Insights.svg b/core/assets/icons/azure/monitor/00012-icon-service-Application-Insights.svg similarity index 100% rename from assets/icons/azure/monitor/00012-icon-service-Application-Insights.svg rename to core/assets/icons/azure/monitor/00012-icon-service-Application-Insights.svg diff --git a/assets/icons/azure/monitor/00020-icon-service-Metrics.png b/core/assets/icons/azure/monitor/00020-icon-service-Metrics.png similarity index 100% rename from assets/icons/azure/monitor/00020-icon-service-Metrics.png rename to core/assets/icons/azure/monitor/00020-icon-service-Metrics.png diff --git a/assets/icons/azure/monitor/00020-icon-service-Metrics.svg b/core/assets/icons/azure/monitor/00020-icon-service-Metrics.svg similarity index 100% rename from assets/icons/azure/monitor/00020-icon-service-Metrics.svg rename to core/assets/icons/azure/monitor/00020-icon-service-Metrics.svg diff --git a/assets/icons/azure/monitor/00438-icon-service-Azure-Monitors-for-SAP-Solutions.png b/core/assets/icons/azure/monitor/00438-icon-service-Azure-Monitors-for-SAP-Solutions.png similarity index 100% rename from assets/icons/azure/monitor/00438-icon-service-Azure-Monitors-for-SAP-Solutions.png rename to core/assets/icons/azure/monitor/00438-icon-service-Azure-Monitors-for-SAP-Solutions.png diff --git a/assets/icons/azure/monitor/00438-icon-service-Azure-Monitors-for-SAP-Solutions.svg b/core/assets/icons/azure/monitor/00438-icon-service-Azure-Monitors-for-SAP-Solutions.svg similarity index 100% rename from assets/icons/azure/monitor/00438-icon-service-Azure-Monitors-for-SAP-Solutions.svg rename to core/assets/icons/azure/monitor/00438-icon-service-Azure-Monitors-for-SAP-Solutions.svg diff --git a/assets/icons/azure/monitor/00563-icon-service-Change-Analysis.png b/core/assets/icons/azure/monitor/00563-icon-service-Change-Analysis.png similarity index 100% rename from assets/icons/azure/monitor/00563-icon-service-Change-Analysis.png rename to core/assets/icons/azure/monitor/00563-icon-service-Change-Analysis.png diff --git a/assets/icons/azure/monitor/00563-icon-service-Change-Analysis.svg b/core/assets/icons/azure/monitor/00563-icon-service-Change-Analysis.svg similarity index 100% rename from assets/icons/azure/monitor/00563-icon-service-Change-Analysis.svg rename to core/assets/icons/azure/monitor/00563-icon-service-Change-Analysis.svg diff --git a/assets/icons/azure/monitor/02189-icon-service-Azure-Workbooks.png b/core/assets/icons/azure/monitor/02189-icon-service-Azure-Workbooks.png similarity index 100% rename from assets/icons/azure/monitor/02189-icon-service-Azure-Workbooks.png rename to core/assets/icons/azure/monitor/02189-icon-service-Azure-Workbooks.png diff --git a/assets/icons/azure/monitor/02189-icon-service-Azure-Workbooks.svg b/core/assets/icons/azure/monitor/02189-icon-service-Azure-Workbooks.svg similarity index 100% rename from assets/icons/azure/monitor/02189-icon-service-Azure-Workbooks.svg rename to core/assets/icons/azure/monitor/02189-icon-service-Azure-Workbooks.svg diff --git a/assets/icons/azure/monitor/10066-icon-service-Network-Watcher.png b/core/assets/icons/azure/monitor/10066-icon-service-Network-Watcher.png similarity index 100% rename from assets/icons/azure/monitor/10066-icon-service-Network-Watcher.png rename to core/assets/icons/azure/monitor/10066-icon-service-Network-Watcher.png diff --git a/assets/icons/azure/monitor/10066-icon-service-Network-Watcher.svg b/core/assets/icons/azure/monitor/10066-icon-service-Network-Watcher.svg similarity index 100% rename from assets/icons/azure/monitor/10066-icon-service-Network-Watcher.svg rename to core/assets/icons/azure/monitor/10066-icon-service-Network-Watcher.svg diff --git a/assets/icons/azure/monitor/10832-icon-service-Auto-Scale.png b/core/assets/icons/azure/monitor/10832-icon-service-Auto-Scale.png similarity index 100% rename from assets/icons/azure/monitor/10832-icon-service-Auto-Scale.png rename to core/assets/icons/azure/monitor/10832-icon-service-Auto-Scale.png diff --git a/assets/icons/azure/monitor/10832-icon-service-Auto-Scale.svg b/core/assets/icons/azure/monitor/10832-icon-service-Auto-Scale.svg similarity index 100% rename from assets/icons/azure/monitor/10832-icon-service-Auto-Scale.svg rename to core/assets/icons/azure/monitor/10832-icon-service-Auto-Scale.svg diff --git a/assets/icons/azure/networking/00056-icon-service-CDN-Profiles.png b/core/assets/icons/azure/networking/00056-icon-service-CDN-Profiles.png similarity index 100% rename from assets/icons/azure/networking/00056-icon-service-CDN-Profiles.png rename to core/assets/icons/azure/networking/00056-icon-service-CDN-Profiles.png diff --git a/assets/icons/azure/networking/00056-icon-service-CDN-Profiles.svg b/core/assets/icons/azure/networking/00056-icon-service-CDN-Profiles.svg similarity index 100% rename from assets/icons/azure/networking/00056-icon-service-CDN-Profiles.svg rename to core/assets/icons/azure/networking/00056-icon-service-CDN-Profiles.svg diff --git a/assets/icons/azure/networking/00271-icon-service-Azure-Firewall-Manager.png b/core/assets/icons/azure/networking/00271-icon-service-Azure-Firewall-Manager.png similarity index 100% rename from assets/icons/azure/networking/00271-icon-service-Azure-Firewall-Manager.png rename to core/assets/icons/azure/networking/00271-icon-service-Azure-Firewall-Manager.png diff --git a/assets/icons/azure/networking/00271-icon-service-Azure-Firewall-Manager.svg b/core/assets/icons/azure/networking/00271-icon-service-Azure-Firewall-Manager.svg similarity index 100% rename from assets/icons/azure/networking/00271-icon-service-Azure-Firewall-Manager.svg rename to core/assets/icons/azure/networking/00271-icon-service-Azure-Firewall-Manager.svg diff --git a/assets/icons/azure/networking/00272-icon-service-Azure-Firewall-Policy.png b/core/assets/icons/azure/networking/00272-icon-service-Azure-Firewall-Policy.png similarity index 100% rename from assets/icons/azure/networking/00272-icon-service-Azure-Firewall-Policy.png rename to core/assets/icons/azure/networking/00272-icon-service-Azure-Firewall-Policy.png diff --git a/assets/icons/azure/networking/00272-icon-service-Azure-Firewall-Policy.svg b/core/assets/icons/azure/networking/00272-icon-service-Azure-Firewall-Policy.svg similarity index 100% rename from assets/icons/azure/networking/00272-icon-service-Azure-Firewall-Policy.svg rename to core/assets/icons/azure/networking/00272-icon-service-Azure-Firewall-Policy.svg diff --git a/assets/icons/azure/networking/00427-icon-service-Private-Link.png b/core/assets/icons/azure/networking/00427-icon-service-Private-Link.png similarity index 100% rename from assets/icons/azure/networking/00427-icon-service-Private-Link.png rename to core/assets/icons/azure/networking/00427-icon-service-Private-Link.png diff --git a/assets/icons/azure/networking/00427-icon-service-Private-Link.svg b/core/assets/icons/azure/networking/00427-icon-service-Private-Link.svg similarity index 100% rename from assets/icons/azure/networking/00427-icon-service-Private-Link.svg rename to core/assets/icons/azure/networking/00427-icon-service-Private-Link.svg diff --git a/assets/icons/azure/networking/00701-icon-service-IP-Groups.png b/core/assets/icons/azure/networking/00701-icon-service-IP-Groups.png similarity index 100% rename from assets/icons/azure/networking/00701-icon-service-IP-Groups.png rename to core/assets/icons/azure/networking/00701-icon-service-IP-Groups.png diff --git a/assets/icons/azure/networking/00701-icon-service-IP-Groups.svg b/core/assets/icons/azure/networking/00701-icon-service-IP-Groups.svg similarity index 100% rename from assets/icons/azure/networking/00701-icon-service-IP-Groups.svg rename to core/assets/icons/azure/networking/00701-icon-service-IP-Groups.svg diff --git a/assets/icons/azure/networking/00860-icon-service-Virtual-WAN-Hub.png b/core/assets/icons/azure/networking/00860-icon-service-Virtual-WAN-Hub.png similarity index 100% rename from assets/icons/azure/networking/00860-icon-service-Virtual-WAN-Hub.png rename to core/assets/icons/azure/networking/00860-icon-service-Virtual-WAN-Hub.png diff --git a/assets/icons/azure/networking/00860-icon-service-Virtual-WAN-Hub.svg b/core/assets/icons/azure/networking/00860-icon-service-Virtual-WAN-Hub.svg similarity index 100% rename from assets/icons/azure/networking/00860-icon-service-Virtual-WAN-Hub.svg rename to core/assets/icons/azure/networking/00860-icon-service-Virtual-WAN-Hub.svg diff --git a/assets/icons/azure/networking/01105-icon-service-Private-Link-Service.png b/core/assets/icons/azure/networking/01105-icon-service-Private-Link-Service.png similarity index 100% rename from assets/icons/azure/networking/01105-icon-service-Private-Link-Service.png rename to core/assets/icons/azure/networking/01105-icon-service-Private-Link-Service.png diff --git a/assets/icons/azure/networking/01105-icon-service-Private-Link-Service.svg b/core/assets/icons/azure/networking/01105-icon-service-Private-Link-Service.svg similarity index 100% rename from assets/icons/azure/networking/01105-icon-service-Private-Link-Service.svg rename to core/assets/icons/azure/networking/01105-icon-service-Private-Link-Service.svg diff --git a/assets/icons/azure/networking/02145-icon-service-Resource-Management-Private-Link.png b/core/assets/icons/azure/networking/02145-icon-service-Resource-Management-Private-Link.png similarity index 100% rename from assets/icons/azure/networking/02145-icon-service-Resource-Management-Private-Link.png rename to core/assets/icons/azure/networking/02145-icon-service-Resource-Management-Private-Link.png diff --git a/assets/icons/azure/networking/02145-icon-service-Resource-Management-Private-Link.svg b/core/assets/icons/azure/networking/02145-icon-service-Resource-Management-Private-Link.svg similarity index 100% rename from assets/icons/azure/networking/02145-icon-service-Resource-Management-Private-Link.svg rename to core/assets/icons/azure/networking/02145-icon-service-Resource-Management-Private-Link.svg diff --git a/assets/icons/azure/networking/02209-icon-service-Private-Link-Services.png b/core/assets/icons/azure/networking/02209-icon-service-Private-Link-Services.png similarity index 100% rename from assets/icons/azure/networking/02209-icon-service-Private-Link-Services.png rename to core/assets/icons/azure/networking/02209-icon-service-Private-Link-Services.png diff --git a/assets/icons/azure/networking/02209-icon-service-Private-Link-Services.svg b/core/assets/icons/azure/networking/02209-icon-service-Private-Link-Services.svg similarity index 100% rename from assets/icons/azure/networking/02209-icon-service-Private-Link-Services.svg rename to core/assets/icons/azure/networking/02209-icon-service-Private-Link-Services.svg diff --git a/assets/icons/azure/networking/02302-icon-service-Load-Balancer-Hub.png b/core/assets/icons/azure/networking/02302-icon-service-Load-Balancer-Hub.png similarity index 100% rename from assets/icons/azure/networking/02302-icon-service-Load-Balancer-Hub.png rename to core/assets/icons/azure/networking/02302-icon-service-Load-Balancer-Hub.png diff --git a/assets/icons/azure/networking/02302-icon-service-Load-Balancer-Hub.svg b/core/assets/icons/azure/networking/02302-icon-service-Load-Balancer-Hub.svg similarity index 100% rename from assets/icons/azure/networking/02302-icon-service-Load-Balancer-Hub.svg rename to core/assets/icons/azure/networking/02302-icon-service-Load-Balancer-Hub.svg diff --git a/assets/icons/azure/networking/02422-icon-service-Bastions.png b/core/assets/icons/azure/networking/02422-icon-service-Bastions.png similarity index 100% rename from assets/icons/azure/networking/02422-icon-service-Bastions.png rename to core/assets/icons/azure/networking/02422-icon-service-Bastions.png diff --git a/assets/icons/azure/networking/02422-icon-service-Bastions.svg b/core/assets/icons/azure/networking/02422-icon-service-Bastions.svg similarity index 100% rename from assets/icons/azure/networking/02422-icon-service-Bastions.svg rename to core/assets/icons/azure/networking/02422-icon-service-Bastions.svg diff --git a/assets/icons/azure/networking/02496-icon-service-Virtual-Router.png b/core/assets/icons/azure/networking/02496-icon-service-Virtual-Router.png similarity index 100% rename from assets/icons/azure/networking/02496-icon-service-Virtual-Router.png rename to core/assets/icons/azure/networking/02496-icon-service-Virtual-Router.png diff --git a/assets/icons/azure/networking/02496-icon-service-Virtual-Router.svg b/core/assets/icons/azure/networking/02496-icon-service-Virtual-Router.svg similarity index 100% rename from assets/icons/azure/networking/02496-icon-service-Virtual-Router.svg rename to core/assets/icons/azure/networking/02496-icon-service-Virtual-Router.svg diff --git a/assets/icons/azure/networking/02509-icon-service-Connected-Cache.png b/core/assets/icons/azure/networking/02509-icon-service-Connected-Cache.png similarity index 100% rename from assets/icons/azure/networking/02509-icon-service-Connected-Cache.png rename to core/assets/icons/azure/networking/02509-icon-service-Connected-Cache.png diff --git a/assets/icons/azure/networking/02509-icon-service-Connected-Cache.svg b/core/assets/icons/azure/networking/02509-icon-service-Connected-Cache.svg similarity index 100% rename from assets/icons/azure/networking/02509-icon-service-Connected-Cache.svg rename to core/assets/icons/azure/networking/02509-icon-service-Connected-Cache.svg diff --git a/assets/icons/azure/networking/02692-icon-service-Spot-VMSS.png b/core/assets/icons/azure/networking/02692-icon-service-Spot-VMSS.png similarity index 100% rename from assets/icons/azure/networking/02692-icon-service-Spot-VMSS.png rename to core/assets/icons/azure/networking/02692-icon-service-Spot-VMSS.png diff --git a/assets/icons/azure/networking/02692-icon-service-Spot-VMSS.svg b/core/assets/icons/azure/networking/02692-icon-service-Spot-VMSS.svg similarity index 100% rename from assets/icons/azure/networking/02692-icon-service-Spot-VMSS.svg rename to core/assets/icons/azure/networking/02692-icon-service-Spot-VMSS.svg diff --git a/assets/icons/azure/networking/02695-icon-service-Spot-VM.png b/core/assets/icons/azure/networking/02695-icon-service-Spot-VM.png similarity index 100% rename from assets/icons/azure/networking/02695-icon-service-Spot-VM.png rename to core/assets/icons/azure/networking/02695-icon-service-Spot-VM.png diff --git a/assets/icons/azure/networking/02695-icon-service-Spot-VM.svg b/core/assets/icons/azure/networking/02695-icon-service-Spot-VM.svg similarity index 100% rename from assets/icons/azure/networking/02695-icon-service-Spot-VM.svg rename to core/assets/icons/azure/networking/02695-icon-service-Spot-VM.svg diff --git a/assets/icons/azure/networking/02742-icon-service-Subnet.png b/core/assets/icons/azure/networking/02742-icon-service-Subnet.png similarity index 100% rename from assets/icons/azure/networking/02742-icon-service-Subnet.png rename to core/assets/icons/azure/networking/02742-icon-service-Subnet.png diff --git a/assets/icons/azure/networking/02742-icon-service-Subnet.svg b/core/assets/icons/azure/networking/02742-icon-service-Subnet.svg similarity index 100% rename from assets/icons/azure/networking/02742-icon-service-Subnet.svg rename to core/assets/icons/azure/networking/02742-icon-service-Subnet.svg diff --git a/assets/icons/azure/networking/02882-icon-service-DNS-Private-Resolver.png b/core/assets/icons/azure/networking/02882-icon-service-DNS-Private-Resolver.png similarity index 100% rename from assets/icons/azure/networking/02882-icon-service-DNS-Private-Resolver.png rename to core/assets/icons/azure/networking/02882-icon-service-DNS-Private-Resolver.png diff --git a/assets/icons/azure/networking/02882-icon-service-DNS-Private-Resolver.svg b/core/assets/icons/azure/networking/02882-icon-service-DNS-Private-Resolver.svg similarity index 100% rename from assets/icons/azure/networking/02882-icon-service-DNS-Private-Resolver.svg rename to core/assets/icons/azure/networking/02882-icon-service-DNS-Private-Resolver.svg diff --git a/assets/icons/azure/networking/029029174-icon-service-Load-Balancer-Hub.png b/core/assets/icons/azure/networking/029029174-icon-service-Load-Balancer-Hub.png similarity index 100% rename from assets/icons/azure/networking/029029174-icon-service-Load-Balancer-Hub.png rename to core/assets/icons/azure/networking/029029174-icon-service-Load-Balancer-Hub.png diff --git a/assets/icons/azure/networking/029029174-icon-service-Load-Balancer-Hub.svg b/core/assets/icons/azure/networking/029029174-icon-service-Load-Balancer-Hub.svg similarity index 100% rename from assets/icons/azure/networking/029029174-icon-service-Load-Balancer-Hub.svg rename to core/assets/icons/azure/networking/029029174-icon-service-Load-Balancer-Hub.svg diff --git a/assets/icons/azure/networking/029273094-icon-service-Edge-Actions.png b/core/assets/icons/azure/networking/029273094-icon-service-Edge-Actions.png similarity index 100% rename from assets/icons/azure/networking/029273094-icon-service-Edge-Actions.png rename to core/assets/icons/azure/networking/029273094-icon-service-Edge-Actions.png diff --git a/assets/icons/azure/networking/029273094-icon-service-Edge-Actions.svg b/core/assets/icons/azure/networking/029273094-icon-service-Edge-Actions.svg similarity index 100% rename from assets/icons/azure/networking/029273094-icon-service-Edge-Actions.svg rename to core/assets/icons/azure/networking/029273094-icon-service-Edge-Actions.svg diff --git a/assets/icons/azure/networking/03311-icon-service-Azure-Communications-Gateway.png b/core/assets/icons/azure/networking/03311-icon-service-Azure-Communications-Gateway.png similarity index 100% rename from assets/icons/azure/networking/03311-icon-service-Azure-Communications-Gateway.png rename to core/assets/icons/azure/networking/03311-icon-service-Azure-Communications-Gateway.png diff --git a/assets/icons/azure/networking/03311-icon-service-Azure-Communications-Gateway.svg b/core/assets/icons/azure/networking/03311-icon-service-Azure-Communications-Gateway.svg similarity index 100% rename from assets/icons/azure/networking/03311-icon-service-Azure-Communications-Gateway.svg rename to core/assets/icons/azure/networking/03311-icon-service-Azure-Communications-Gateway.svg diff --git a/assets/icons/azure/networking/03328-icon-service-Application-Gateway-Containers.png b/core/assets/icons/azure/networking/03328-icon-service-Application-Gateway-Containers.png similarity index 100% rename from assets/icons/azure/networking/03328-icon-service-Application-Gateway-Containers.png rename to core/assets/icons/azure/networking/03328-icon-service-Application-Gateway-Containers.png diff --git a/assets/icons/azure/networking/03328-icon-service-Application-Gateway-Containers.svg b/core/assets/icons/azure/networking/03328-icon-service-Application-Gateway-Containers.svg similarity index 100% rename from assets/icons/azure/networking/03328-icon-service-Application-Gateway-Containers.svg rename to core/assets/icons/azure/networking/03328-icon-service-Application-Gateway-Containers.svg diff --git a/assets/icons/azure/networking/03368-icon-service-DNS-Security-Policy.png b/core/assets/icons/azure/networking/03368-icon-service-DNS-Security-Policy.png similarity index 100% rename from assets/icons/azure/networking/03368-icon-service-DNS-Security-Policy.png rename to core/assets/icons/azure/networking/03368-icon-service-DNS-Security-Policy.png diff --git a/assets/icons/azure/networking/03368-icon-service-DNS-Security-Policy.svg b/core/assets/icons/azure/networking/03368-icon-service-DNS-Security-Policy.svg similarity index 100% rename from assets/icons/azure/networking/03368-icon-service-DNS-Security-Policy.svg rename to core/assets/icons/azure/networking/03368-icon-service-DNS-Security-Policy.svg diff --git a/assets/icons/azure/networking/034412066-icon-service-VNet-Appliance.png b/core/assets/icons/azure/networking/034412066-icon-service-VNet-Appliance.png similarity index 100% rename from assets/icons/azure/networking/034412066-icon-service-VNet-Appliance.png rename to core/assets/icons/azure/networking/034412066-icon-service-VNet-Appliance.png diff --git a/assets/icons/azure/networking/034412066-icon-service-VNet-Appliance.svg b/core/assets/icons/azure/networking/034412066-icon-service-VNet-Appliance.svg similarity index 100% rename from assets/icons/azure/networking/034412066-icon-service-VNet-Appliance.svg rename to core/assets/icons/azure/networking/034412066-icon-service-VNet-Appliance.svg diff --git a/assets/icons/azure/networking/03459-icon-service-DNS-Multistack.png b/core/assets/icons/azure/networking/03459-icon-service-DNS-Multistack.png similarity index 100% rename from assets/icons/azure/networking/03459-icon-service-DNS-Multistack.png rename to core/assets/icons/azure/networking/03459-icon-service-DNS-Multistack.png diff --git a/assets/icons/azure/networking/03459-icon-service-DNS-Multistack.svg b/core/assets/icons/azure/networking/03459-icon-service-DNS-Multistack.svg similarity index 100% rename from assets/icons/azure/networking/03459-icon-service-DNS-Multistack.svg rename to core/assets/icons/azure/networking/03459-icon-service-DNS-Multistack.svg diff --git a/assets/icons/azure/networking/03460-icon-service-ATM-Multistack.png b/core/assets/icons/azure/networking/03460-icon-service-ATM-Multistack.png similarity index 100% rename from assets/icons/azure/networking/03460-icon-service-ATM-Multistack.png rename to core/assets/icons/azure/networking/03460-icon-service-ATM-Multistack.png diff --git a/assets/icons/azure/networking/03460-icon-service-ATM-Multistack.svg b/core/assets/icons/azure/networking/03460-icon-service-ATM-Multistack.svg similarity index 100% rename from assets/icons/azure/networking/03460-icon-service-ATM-Multistack.svg rename to core/assets/icons/azure/networking/03460-icon-service-ATM-Multistack.svg diff --git a/assets/icons/azure/networking/03461-icon-service-IP-Address-manager.png b/core/assets/icons/azure/networking/03461-icon-service-IP-Address-manager.png similarity index 100% rename from assets/icons/azure/networking/03461-icon-service-IP-Address-manager.png rename to core/assets/icons/azure/networking/03461-icon-service-IP-Address-manager.png diff --git a/assets/icons/azure/networking/03461-icon-service-IP-Address-manager.svg b/core/assets/icons/azure/networking/03461-icon-service-IP-Address-manager.svg similarity index 100% rename from assets/icons/azure/networking/03461-icon-service-IP-Address-manager.svg rename to core/assets/icons/azure/networking/03461-icon-service-IP-Address-manager.svg diff --git a/assets/icons/azure/networking/03643-icon-service-Network-Foundation-Hub.png b/core/assets/icons/azure/networking/03643-icon-service-Network-Foundation-Hub.png similarity index 100% rename from assets/icons/azure/networking/03643-icon-service-Network-Foundation-Hub.png rename to core/assets/icons/azure/networking/03643-icon-service-Network-Foundation-Hub.png diff --git a/assets/icons/azure/networking/03643-icon-service-Network-Foundation-Hub.svg b/core/assets/icons/azure/networking/03643-icon-service-Network-Foundation-Hub.svg similarity index 100% rename from assets/icons/azure/networking/03643-icon-service-Network-Foundation-Hub.svg rename to core/assets/icons/azure/networking/03643-icon-service-Network-Foundation-Hub.svg diff --git a/assets/icons/azure/networking/03650-icon-service-Network-Security-Hub.png b/core/assets/icons/azure/networking/03650-icon-service-Network-Security-Hub.png similarity index 100% rename from assets/icons/azure/networking/03650-icon-service-Network-Security-Hub.png rename to core/assets/icons/azure/networking/03650-icon-service-Network-Security-Hub.png diff --git a/assets/icons/azure/networking/03650-icon-service-Network-Security-Hub.svg b/core/assets/icons/azure/networking/03650-icon-service-Network-Security-Hub.svg similarity index 100% rename from assets/icons/azure/networking/03650-icon-service-Network-Security-Hub.svg rename to core/assets/icons/azure/networking/03650-icon-service-Network-Security-Hub.svg diff --git a/assets/icons/azure/networking/03694-icon-service-VPNClientWindows.png b/core/assets/icons/azure/networking/03694-icon-service-VPNClientWindows.png similarity index 100% rename from assets/icons/azure/networking/03694-icon-service-VPNClientWindows.png rename to core/assets/icons/azure/networking/03694-icon-service-VPNClientWindows.png diff --git a/assets/icons/azure/networking/03694-icon-service-VPNClientWindows.svg b/core/assets/icons/azure/networking/03694-icon-service-VPNClientWindows.svg similarity index 100% rename from assets/icons/azure/networking/03694-icon-service-VPNClientWindows.svg rename to core/assets/icons/azure/networking/03694-icon-service-VPNClientWindows.svg diff --git a/assets/icons/azure/networking/10061-icon-service-Virtual-Networks.png b/core/assets/icons/azure/networking/10061-icon-service-Virtual-Networks.png similarity index 100% rename from assets/icons/azure/networking/10061-icon-service-Virtual-Networks.png rename to core/assets/icons/azure/networking/10061-icon-service-Virtual-Networks.png diff --git a/assets/icons/azure/networking/10061-icon-service-Virtual-Networks.svg b/core/assets/icons/azure/networking/10061-icon-service-Virtual-Networks.svg similarity index 100% rename from assets/icons/azure/networking/10061-icon-service-Virtual-Networks.svg rename to core/assets/icons/azure/networking/10061-icon-service-Virtual-Networks.svg diff --git a/assets/icons/azure/networking/10062-icon-service-Load-Balancers.png b/core/assets/icons/azure/networking/10062-icon-service-Load-Balancers.png similarity index 100% rename from assets/icons/azure/networking/10062-icon-service-Load-Balancers.png rename to core/assets/icons/azure/networking/10062-icon-service-Load-Balancers.png diff --git a/assets/icons/azure/networking/10062-icon-service-Load-Balancers.svg b/core/assets/icons/azure/networking/10062-icon-service-Load-Balancers.svg similarity index 100% rename from assets/icons/azure/networking/10062-icon-service-Load-Balancers.svg rename to core/assets/icons/azure/networking/10062-icon-service-Load-Balancers.svg diff --git a/assets/icons/azure/networking/10063-icon-service-Virtual-Network-Gateways.png b/core/assets/icons/azure/networking/10063-icon-service-Virtual-Network-Gateways.png similarity index 100% rename from assets/icons/azure/networking/10063-icon-service-Virtual-Network-Gateways.png rename to core/assets/icons/azure/networking/10063-icon-service-Virtual-Network-Gateways.png diff --git a/assets/icons/azure/networking/10063-icon-service-Virtual-Network-Gateways.svg b/core/assets/icons/azure/networking/10063-icon-service-Virtual-Network-Gateways.svg similarity index 100% rename from assets/icons/azure/networking/10063-icon-service-Virtual-Network-Gateways.svg rename to core/assets/icons/azure/networking/10063-icon-service-Virtual-Network-Gateways.svg diff --git a/assets/icons/azure/networking/10064-icon-service-DNS-Zones.png b/core/assets/icons/azure/networking/10064-icon-service-DNS-Zones.png similarity index 100% rename from assets/icons/azure/networking/10064-icon-service-DNS-Zones.png rename to core/assets/icons/azure/networking/10064-icon-service-DNS-Zones.png diff --git a/assets/icons/azure/networking/10064-icon-service-DNS-Zones.svg b/core/assets/icons/azure/networking/10064-icon-service-DNS-Zones.svg similarity index 100% rename from assets/icons/azure/networking/10064-icon-service-DNS-Zones.svg rename to core/assets/icons/azure/networking/10064-icon-service-DNS-Zones.svg diff --git a/assets/icons/azure/networking/10065-icon-service-Traffic-Manager-Profiles.png b/core/assets/icons/azure/networking/10065-icon-service-Traffic-Manager-Profiles.png similarity index 100% rename from assets/icons/azure/networking/10065-icon-service-Traffic-Manager-Profiles.png rename to core/assets/icons/azure/networking/10065-icon-service-Traffic-Manager-Profiles.png diff --git a/assets/icons/azure/networking/10065-icon-service-Traffic-Manager-Profiles.svg b/core/assets/icons/azure/networking/10065-icon-service-Traffic-Manager-Profiles.svg similarity index 100% rename from assets/icons/azure/networking/10065-icon-service-Traffic-Manager-Profiles.svg rename to core/assets/icons/azure/networking/10065-icon-service-Traffic-Manager-Profiles.svg diff --git a/assets/icons/azure/networking/10066-icon-service-Network-Watcher.png b/core/assets/icons/azure/networking/10066-icon-service-Network-Watcher.png similarity index 100% rename from assets/icons/azure/networking/10066-icon-service-Network-Watcher.png rename to core/assets/icons/azure/networking/10066-icon-service-Network-Watcher.png diff --git a/assets/icons/azure/networking/10066-icon-service-Network-Watcher.svg b/core/assets/icons/azure/networking/10066-icon-service-Network-Watcher.svg similarity index 100% rename from assets/icons/azure/networking/10066-icon-service-Network-Watcher.svg rename to core/assets/icons/azure/networking/10066-icon-service-Network-Watcher.svg diff --git a/assets/icons/azure/networking/10067-icon-service-Network-Security-Groups.png b/core/assets/icons/azure/networking/10067-icon-service-Network-Security-Groups.png similarity index 100% rename from assets/icons/azure/networking/10067-icon-service-Network-Security-Groups.png rename to core/assets/icons/azure/networking/10067-icon-service-Network-Security-Groups.png diff --git a/assets/icons/azure/networking/10067-icon-service-Network-Security-Groups.svg b/core/assets/icons/azure/networking/10067-icon-service-Network-Security-Groups.svg similarity index 100% rename from assets/icons/azure/networking/10067-icon-service-Network-Security-Groups.svg rename to core/assets/icons/azure/networking/10067-icon-service-Network-Security-Groups.svg diff --git a/assets/icons/azure/networking/10068-icon-service-Public-IP-Addresses-(Classic).png b/core/assets/icons/azure/networking/10068-icon-service-Public-IP-Addresses-(Classic).png similarity index 100% rename from assets/icons/azure/networking/10068-icon-service-Public-IP-Addresses-(Classic).png rename to core/assets/icons/azure/networking/10068-icon-service-Public-IP-Addresses-(Classic).png diff --git a/assets/icons/azure/networking/10068-icon-service-Public-IP-Addresses-(Classic).svg b/core/assets/icons/azure/networking/10068-icon-service-Public-IP-Addresses-(Classic).svg similarity index 100% rename from assets/icons/azure/networking/10068-icon-service-Public-IP-Addresses-(Classic).svg rename to core/assets/icons/azure/networking/10068-icon-service-Public-IP-Addresses-(Classic).svg diff --git a/assets/icons/azure/networking/10069-icon-service-Public-IP-Addresses.png b/core/assets/icons/azure/networking/10069-icon-service-Public-IP-Addresses.png similarity index 100% rename from assets/icons/azure/networking/10069-icon-service-Public-IP-Addresses.png rename to core/assets/icons/azure/networking/10069-icon-service-Public-IP-Addresses.png diff --git a/assets/icons/azure/networking/10069-icon-service-Public-IP-Addresses.svg b/core/assets/icons/azure/networking/10069-icon-service-Public-IP-Addresses.svg similarity index 100% rename from assets/icons/azure/networking/10069-icon-service-Public-IP-Addresses.svg rename to core/assets/icons/azure/networking/10069-icon-service-Public-IP-Addresses.svg diff --git a/assets/icons/azure/networking/10070-icon-service-On-Premises-Data-Gateways.png b/core/assets/icons/azure/networking/10070-icon-service-On-Premises-Data-Gateways.png similarity index 100% rename from assets/icons/azure/networking/10070-icon-service-On-Premises-Data-Gateways.png rename to core/assets/icons/azure/networking/10070-icon-service-On-Premises-Data-Gateways.png diff --git a/assets/icons/azure/networking/10070-icon-service-On-Premises-Data-Gateways.svg b/core/assets/icons/azure/networking/10070-icon-service-On-Premises-Data-Gateways.svg similarity index 100% rename from assets/icons/azure/networking/10070-icon-service-On-Premises-Data-Gateways.svg rename to core/assets/icons/azure/networking/10070-icon-service-On-Premises-Data-Gateways.svg diff --git a/assets/icons/azure/networking/10071-icon-service-Route-Filters.png b/core/assets/icons/azure/networking/10071-icon-service-Route-Filters.png similarity index 100% rename from assets/icons/azure/networking/10071-icon-service-Route-Filters.png rename to core/assets/icons/azure/networking/10071-icon-service-Route-Filters.png diff --git a/assets/icons/azure/networking/10071-icon-service-Route-Filters.svg b/core/assets/icons/azure/networking/10071-icon-service-Route-Filters.svg similarity index 100% rename from assets/icons/azure/networking/10071-icon-service-Route-Filters.svg rename to core/assets/icons/azure/networking/10071-icon-service-Route-Filters.svg diff --git a/assets/icons/azure/networking/10072-icon-service-DDoS-Protection-Plans.png b/core/assets/icons/azure/networking/10072-icon-service-DDoS-Protection-Plans.png similarity index 100% rename from assets/icons/azure/networking/10072-icon-service-DDoS-Protection-Plans.png rename to core/assets/icons/azure/networking/10072-icon-service-DDoS-Protection-Plans.png diff --git a/assets/icons/azure/networking/10072-icon-service-DDoS-Protection-Plans.svg b/core/assets/icons/azure/networking/10072-icon-service-DDoS-Protection-Plans.svg similarity index 100% rename from assets/icons/azure/networking/10072-icon-service-DDoS-Protection-Plans.svg rename to core/assets/icons/azure/networking/10072-icon-service-DDoS-Protection-Plans.svg diff --git a/assets/icons/azure/networking/10073-icon-service-Front-Door-and-CDN-Profiles.png b/core/assets/icons/azure/networking/10073-icon-service-Front-Door-and-CDN-Profiles.png similarity index 100% rename from assets/icons/azure/networking/10073-icon-service-Front-Door-and-CDN-Profiles.png rename to core/assets/icons/azure/networking/10073-icon-service-Front-Door-and-CDN-Profiles.png diff --git a/assets/icons/azure/networking/10073-icon-service-Front-Door-and-CDN-Profiles.svg b/core/assets/icons/azure/networking/10073-icon-service-Front-Door-and-CDN-Profiles.svg similarity index 100% rename from assets/icons/azure/networking/10073-icon-service-Front-Door-and-CDN-Profiles.svg rename to core/assets/icons/azure/networking/10073-icon-service-Front-Door-and-CDN-Profiles.svg diff --git a/assets/icons/azure/networking/10075-icon-service-Virtual-Networks-(Classic).png b/core/assets/icons/azure/networking/10075-icon-service-Virtual-Networks-(Classic).png similarity index 100% rename from assets/icons/azure/networking/10075-icon-service-Virtual-Networks-(Classic).png rename to core/assets/icons/azure/networking/10075-icon-service-Virtual-Networks-(Classic).png diff --git a/assets/icons/azure/networking/10075-icon-service-Virtual-Networks-(Classic).svg b/core/assets/icons/azure/networking/10075-icon-service-Virtual-Networks-(Classic).svg similarity index 100% rename from assets/icons/azure/networking/10075-icon-service-Virtual-Networks-(Classic).svg rename to core/assets/icons/azure/networking/10075-icon-service-Virtual-Networks-(Classic).svg diff --git a/assets/icons/azure/networking/10076-icon-service-Application-Gateways.png b/core/assets/icons/azure/networking/10076-icon-service-Application-Gateways.png similarity index 100% rename from assets/icons/azure/networking/10076-icon-service-Application-Gateways.png rename to core/assets/icons/azure/networking/10076-icon-service-Application-Gateways.png diff --git a/assets/icons/azure/networking/10076-icon-service-Application-Gateways.svg b/core/assets/icons/azure/networking/10076-icon-service-Application-Gateways.svg similarity index 100% rename from assets/icons/azure/networking/10076-icon-service-Application-Gateways.svg rename to core/assets/icons/azure/networking/10076-icon-service-Application-Gateways.svg diff --git a/assets/icons/azure/networking/10077-icon-service-Local-Network-Gateways.png b/core/assets/icons/azure/networking/10077-icon-service-Local-Network-Gateways.png similarity index 100% rename from assets/icons/azure/networking/10077-icon-service-Local-Network-Gateways.png rename to core/assets/icons/azure/networking/10077-icon-service-Local-Network-Gateways.png diff --git a/assets/icons/azure/networking/10077-icon-service-Local-Network-Gateways.svg b/core/assets/icons/azure/networking/10077-icon-service-Local-Network-Gateways.svg similarity index 100% rename from assets/icons/azure/networking/10077-icon-service-Local-Network-Gateways.svg rename to core/assets/icons/azure/networking/10077-icon-service-Local-Network-Gateways.svg diff --git a/assets/icons/azure/networking/10079-icon-service-ExpressRoute-Circuits.png b/core/assets/icons/azure/networking/10079-icon-service-ExpressRoute-Circuits.png similarity index 100% rename from assets/icons/azure/networking/10079-icon-service-ExpressRoute-Circuits.png rename to core/assets/icons/azure/networking/10079-icon-service-ExpressRoute-Circuits.png diff --git a/assets/icons/azure/networking/10079-icon-service-ExpressRoute-Circuits.svg b/core/assets/icons/azure/networking/10079-icon-service-ExpressRoute-Circuits.svg similarity index 100% rename from assets/icons/azure/networking/10079-icon-service-ExpressRoute-Circuits.svg rename to core/assets/icons/azure/networking/10079-icon-service-ExpressRoute-Circuits.svg diff --git a/assets/icons/azure/networking/10080-icon-service-Network-Interfaces.png b/core/assets/icons/azure/networking/10080-icon-service-Network-Interfaces.png similarity index 100% rename from assets/icons/azure/networking/10080-icon-service-Network-Interfaces.png rename to core/assets/icons/azure/networking/10080-icon-service-Network-Interfaces.png diff --git a/assets/icons/azure/networking/10080-icon-service-Network-Interfaces.svg b/core/assets/icons/azure/networking/10080-icon-service-Network-Interfaces.svg similarity index 100% rename from assets/icons/azure/networking/10080-icon-service-Network-Interfaces.svg rename to core/assets/icons/azure/networking/10080-icon-service-Network-Interfaces.svg diff --git a/assets/icons/azure/networking/10081-icon-service-Connections.png b/core/assets/icons/azure/networking/10081-icon-service-Connections.png similarity index 100% rename from assets/icons/azure/networking/10081-icon-service-Connections.png rename to core/assets/icons/azure/networking/10081-icon-service-Connections.png diff --git a/assets/icons/azure/networking/10081-icon-service-Connections.svg b/core/assets/icons/azure/networking/10081-icon-service-Connections.svg similarity index 100% rename from assets/icons/azure/networking/10081-icon-service-Connections.svg rename to core/assets/icons/azure/networking/10081-icon-service-Connections.svg diff --git a/assets/icons/azure/networking/10082-icon-service-Route-Tables.png b/core/assets/icons/azure/networking/10082-icon-service-Route-Tables.png similarity index 100% rename from assets/icons/azure/networking/10082-icon-service-Route-Tables.png rename to core/assets/icons/azure/networking/10082-icon-service-Route-Tables.png diff --git a/assets/icons/azure/networking/10082-icon-service-Route-Tables.svg b/core/assets/icons/azure/networking/10082-icon-service-Route-Tables.svg similarity index 100% rename from assets/icons/azure/networking/10082-icon-service-Route-Tables.svg rename to core/assets/icons/azure/networking/10082-icon-service-Route-Tables.svg diff --git a/assets/icons/azure/networking/10084-icon-service-Firewalls.png b/core/assets/icons/azure/networking/10084-icon-service-Firewalls.png similarity index 100% rename from assets/icons/azure/networking/10084-icon-service-Firewalls.png rename to core/assets/icons/azure/networking/10084-icon-service-Firewalls.png diff --git a/assets/icons/azure/networking/10084-icon-service-Firewalls.svg b/core/assets/icons/azure/networking/10084-icon-service-Firewalls.svg similarity index 100% rename from assets/icons/azure/networking/10084-icon-service-Firewalls.svg rename to core/assets/icons/azure/networking/10084-icon-service-Firewalls.svg diff --git a/assets/icons/azure/networking/10085-icon-service-Service-Endpoint-Policies.png b/core/assets/icons/azure/networking/10085-icon-service-Service-Endpoint-Policies.png similarity index 100% rename from assets/icons/azure/networking/10085-icon-service-Service-Endpoint-Policies.png rename to core/assets/icons/azure/networking/10085-icon-service-Service-Endpoint-Policies.png diff --git a/assets/icons/azure/networking/10085-icon-service-Service-Endpoint-Policies.svg b/core/assets/icons/azure/networking/10085-icon-service-Service-Endpoint-Policies.svg similarity index 100% rename from assets/icons/azure/networking/10085-icon-service-Service-Endpoint-Policies.svg rename to core/assets/icons/azure/networking/10085-icon-service-Service-Endpoint-Policies.svg diff --git a/assets/icons/azure/networking/10310-icon-service-NAT.png b/core/assets/icons/azure/networking/10310-icon-service-NAT.png similarity index 100% rename from assets/icons/azure/networking/10310-icon-service-NAT.png rename to core/assets/icons/azure/networking/10310-icon-service-NAT.png diff --git a/assets/icons/azure/networking/10310-icon-service-NAT.svg b/core/assets/icons/azure/networking/10310-icon-service-NAT.svg similarity index 100% rename from assets/icons/azure/networking/10310-icon-service-NAT.svg rename to core/assets/icons/azure/networking/10310-icon-service-NAT.svg diff --git a/assets/icons/azure/networking/10353-icon-service-Virtual-WANs.png b/core/assets/icons/azure/networking/10353-icon-service-Virtual-WANs.png similarity index 100% rename from assets/icons/azure/networking/10353-icon-service-Virtual-WANs.png rename to core/assets/icons/azure/networking/10353-icon-service-Virtual-WANs.png diff --git a/assets/icons/azure/networking/10353-icon-service-Virtual-WANs.svg b/core/assets/icons/azure/networking/10353-icon-service-Virtual-WANs.svg similarity index 100% rename from assets/icons/azure/networking/10353-icon-service-Virtual-WANs.svg rename to core/assets/icons/azure/networking/10353-icon-service-Virtual-WANs.svg diff --git a/assets/icons/azure/networking/10362-icon-service-Web-Application-Firewall-Policies(WAF).png b/core/assets/icons/azure/networking/10362-icon-service-Web-Application-Firewall-Policies(WAF).png similarity index 100% rename from assets/icons/azure/networking/10362-icon-service-Web-Application-Firewall-Policies(WAF).png rename to core/assets/icons/azure/networking/10362-icon-service-Web-Application-Firewall-Policies(WAF).png diff --git a/assets/icons/azure/networking/10362-icon-service-Web-Application-Firewall-Policies(WAF).svg b/core/assets/icons/azure/networking/10362-icon-service-Web-Application-Firewall-Policies(WAF).svg similarity index 100% rename from assets/icons/azure/networking/10362-icon-service-Web-Application-Firewall-Policies(WAF).svg rename to core/assets/icons/azure/networking/10362-icon-service-Web-Application-Firewall-Policies(WAF).svg diff --git a/assets/icons/azure/networking/10365-icon-service-Proximity-Placement-Groups.png b/core/assets/icons/azure/networking/10365-icon-service-Proximity-Placement-Groups.png similarity index 100% rename from assets/icons/azure/networking/10365-icon-service-Proximity-Placement-Groups.png rename to core/assets/icons/azure/networking/10365-icon-service-Proximity-Placement-Groups.png diff --git a/assets/icons/azure/networking/10365-icon-service-Proximity-Placement-Groups.svg b/core/assets/icons/azure/networking/10365-icon-service-Proximity-Placement-Groups.svg similarity index 100% rename from assets/icons/azure/networking/10365-icon-service-Proximity-Placement-Groups.svg rename to core/assets/icons/azure/networking/10365-icon-service-Proximity-Placement-Groups.svg diff --git a/assets/icons/azure/networking/10371-icon-service-Reserved-IP-Addresses-(Classic).png b/core/assets/icons/azure/networking/10371-icon-service-Reserved-IP-Addresses-(Classic).png similarity index 100% rename from assets/icons/azure/networking/10371-icon-service-Reserved-IP-Addresses-(Classic).png rename to core/assets/icons/azure/networking/10371-icon-service-Reserved-IP-Addresses-(Classic).png diff --git a/assets/icons/azure/networking/10371-icon-service-Reserved-IP-Addresses-(Classic).svg b/core/assets/icons/azure/networking/10371-icon-service-Reserved-IP-Addresses-(Classic).svg similarity index 100% rename from assets/icons/azure/networking/10371-icon-service-Reserved-IP-Addresses-(Classic).svg rename to core/assets/icons/azure/networking/10371-icon-service-Reserved-IP-Addresses-(Classic).svg diff --git a/assets/icons/azure/networking/10372-icon-service-Public-IP-Prefixes.png b/core/assets/icons/azure/networking/10372-icon-service-Public-IP-Prefixes.png similarity index 100% rename from assets/icons/azure/networking/10372-icon-service-Public-IP-Prefixes.png rename to core/assets/icons/azure/networking/10372-icon-service-Public-IP-Prefixes.png diff --git a/assets/icons/azure/networking/10372-icon-service-Public-IP-Prefixes.svg b/core/assets/icons/azure/networking/10372-icon-service-Public-IP-Prefixes.svg similarity index 100% rename from assets/icons/azure/networking/10372-icon-service-Public-IP-Prefixes.svg rename to core/assets/icons/azure/networking/10372-icon-service-Public-IP-Prefixes.svg diff --git a/assets/icons/azure/new icons/02573-icon-service-Connected-Vehicle-Platform.png b/core/assets/icons/azure/new icons/02573-icon-service-Connected-Vehicle-Platform.png similarity index 100% rename from assets/icons/azure/new icons/02573-icon-service-Connected-Vehicle-Platform.png rename to core/assets/icons/azure/new icons/02573-icon-service-Connected-Vehicle-Platform.png diff --git a/assets/icons/azure/new icons/02573-icon-service-Connected-Vehicle-Platform.svg b/core/assets/icons/azure/new icons/02573-icon-service-Connected-Vehicle-Platform.svg similarity index 100% rename from assets/icons/azure/new icons/02573-icon-service-Connected-Vehicle-Platform.svg rename to core/assets/icons/azure/new icons/02573-icon-service-Connected-Vehicle-Platform.svg diff --git a/assets/icons/azure/new icons/029109619-icon-service-Toolchain-Orchestrator.png b/core/assets/icons/azure/new icons/029109619-icon-service-Toolchain-Orchestrator.png similarity index 100% rename from assets/icons/azure/new icons/029109619-icon-service-Toolchain-Orchestrator.png rename to core/assets/icons/azure/new icons/029109619-icon-service-Toolchain-Orchestrator.png diff --git a/assets/icons/azure/new icons/029109619-icon-service-Toolchain-Orchestrator.svg b/core/assets/icons/azure/new icons/029109619-icon-service-Toolchain-Orchestrator.svg similarity index 100% rename from assets/icons/azure/new icons/029109619-icon-service-Toolchain-Orchestrator.svg rename to core/assets/icons/azure/new icons/029109619-icon-service-Toolchain-Orchestrator.svg diff --git a/assets/icons/azure/new icons/029636048-icon-service-Landing-Zone.png b/core/assets/icons/azure/new icons/029636048-icon-service-Landing-Zone.png similarity index 100% rename from assets/icons/azure/new icons/029636048-icon-service-Landing-Zone.png rename to core/assets/icons/azure/new icons/029636048-icon-service-Landing-Zone.png diff --git a/assets/icons/azure/new icons/029636048-icon-service-Landing-Zone.svg b/core/assets/icons/azure/new icons/029636048-icon-service-Landing-Zone.svg similarity index 100% rename from assets/icons/azure/new icons/029636048-icon-service-Landing-Zone.svg rename to core/assets/icons/azure/new icons/029636048-icon-service-Landing-Zone.svg diff --git a/assets/icons/azure/new icons/030777508 -icon-service-Service-Group-Relationships.png b/core/assets/icons/azure/new icons/030777508 -icon-service-Service-Group-Relationships.png similarity index 100% rename from assets/icons/azure/new icons/030777508 -icon-service-Service-Group-Relationships.png rename to core/assets/icons/azure/new icons/030777508 -icon-service-Service-Group-Relationships.png diff --git a/assets/icons/azure/new icons/030777508 -icon-service-Service-Group-Relationships.svg b/core/assets/icons/azure/new icons/030777508 -icon-service-Service-Group-Relationships.svg similarity index 100% rename from assets/icons/azure/new icons/030777508 -icon-service-Service-Group-Relationships.svg rename to core/assets/icons/azure/new icons/030777508 -icon-service-Service-Group-Relationships.svg diff --git a/assets/icons/azure/new icons/031193085-icon-service-Azure-Access-Point.png b/core/assets/icons/azure/new icons/031193085-icon-service-Azure-Access-Point.png similarity index 100% rename from assets/icons/azure/new icons/031193085-icon-service-Azure-Access-Point.png rename to core/assets/icons/azure/new icons/031193085-icon-service-Azure-Access-Point.png diff --git a/assets/icons/azure/new icons/031193085-icon-service-Azure-Access-Point.svg b/core/assets/icons/azure/new icons/031193085-icon-service-Azure-Access-Point.svg similarity index 100% rename from assets/icons/azure/new icons/031193085-icon-service-Azure-Access-Point.svg rename to core/assets/icons/azure/new icons/031193085-icon-service-Azure-Access-Point.svg diff --git a/assets/icons/azure/new icons/033740592-icon-service-Azure-Data-Transfer.png b/core/assets/icons/azure/new icons/033740592-icon-service-Azure-Data-Transfer.png similarity index 100% rename from assets/icons/azure/new icons/033740592-icon-service-Azure-Data-Transfer.png rename to core/assets/icons/azure/new icons/033740592-icon-service-Azure-Data-Transfer.png diff --git a/assets/icons/azure/new icons/033740592-icon-service-Azure-Data-Transfer.svg b/core/assets/icons/azure/new icons/033740592-icon-service-Azure-Data-Transfer.svg similarity index 100% rename from assets/icons/azure/new icons/033740592-icon-service-Azure-Data-Transfer.svg rename to core/assets/icons/azure/new icons/033740592-icon-service-Azure-Data-Transfer.svg diff --git a/assets/icons/azure/new icons/03400-icon-service-Entra-Identity.png b/core/assets/icons/azure/new icons/03400-icon-service-Entra-Identity.png similarity index 100% rename from assets/icons/azure/new icons/03400-icon-service-Entra-Identity.png rename to core/assets/icons/azure/new icons/03400-icon-service-Entra-Identity.png diff --git a/assets/icons/azure/new icons/03400-icon-service-Entra-Identity.svg b/core/assets/icons/azure/new icons/03400-icon-service-Entra-Identity.svg similarity index 100% rename from assets/icons/azure/new icons/03400-icon-service-Entra-Identity.svg rename to core/assets/icons/azure/new icons/03400-icon-service-Entra-Identity.svg diff --git a/assets/icons/azure/new icons/034296882-icon-service-Agentic-Web-Apps.png b/core/assets/icons/azure/new icons/034296882-icon-service-Agentic-Web-Apps.png similarity index 100% rename from assets/icons/azure/new icons/034296882-icon-service-Agentic-Web-Apps.png rename to core/assets/icons/azure/new icons/034296882-icon-service-Agentic-Web-Apps.png diff --git a/assets/icons/azure/new icons/034296882-icon-service-Agentic-Web-Apps.svg b/core/assets/icons/azure/new icons/034296882-icon-service-Agentic-Web-Apps.svg similarity index 100% rename from assets/icons/azure/new icons/034296882-icon-service-Agentic-Web-Apps.svg rename to core/assets/icons/azure/new icons/034296882-icon-service-Agentic-Web-Apps.svg diff --git a/assets/icons/azure/new icons/034923704-icon-service-Resiliency.png b/core/assets/icons/azure/new icons/034923704-icon-service-Resiliency.png similarity index 100% rename from assets/icons/azure/new icons/034923704-icon-service-Resiliency.png rename to core/assets/icons/azure/new icons/034923704-icon-service-Resiliency.png diff --git a/assets/icons/azure/new icons/034923704-icon-service-Resiliency.svg b/core/assets/icons/azure/new icons/034923704-icon-service-Resiliency.svg similarity index 100% rename from assets/icons/azure/new icons/034923704-icon-service-Resiliency.svg rename to core/assets/icons/azure/new icons/034923704-icon-service-Resiliency.svg diff --git a/assets/icons/azure/new icons/035556610-icon-service-DDoS-Custom-Policy.png b/core/assets/icons/azure/new icons/035556610-icon-service-DDoS-Custom-Policy.png similarity index 100% rename from assets/icons/azure/new icons/035556610-icon-service-DDoS-Custom-Policy.png rename to core/assets/icons/azure/new icons/035556610-icon-service-DDoS-Custom-Policy.png diff --git a/assets/icons/azure/new icons/035556610-icon-service-DDoS-Custom-Policy.svg b/core/assets/icons/azure/new icons/035556610-icon-service-DDoS-Custom-Policy.svg similarity index 100% rename from assets/icons/azure/new icons/035556610-icon-service-DDoS-Custom-Policy.svg rename to core/assets/icons/azure/new icons/035556610-icon-service-DDoS-Custom-Policy.svg diff --git a/assets/icons/azure/new icons/035712231-icon-service-Disconnected-Operations.png b/core/assets/icons/azure/new icons/035712231-icon-service-Disconnected-Operations.png similarity index 100% rename from assets/icons/azure/new icons/035712231-icon-service-Disconnected-Operations.png rename to core/assets/icons/azure/new icons/035712231-icon-service-Disconnected-Operations.png diff --git a/assets/icons/azure/new icons/035712231-icon-service-Disconnected-Operations.svg b/core/assets/icons/azure/new icons/035712231-icon-service-Disconnected-Operations.svg similarity index 100% rename from assets/icons/azure/new icons/035712231-icon-service-Disconnected-Operations.svg rename to core/assets/icons/azure/new icons/035712231-icon-service-Disconnected-Operations.svg diff --git a/assets/icons/azure/new icons/035824068-icon-service-Azure-Data-Sharing.png b/core/assets/icons/azure/new icons/035824068-icon-service-Azure-Data-Sharing.png similarity index 100% rename from assets/icons/azure/new icons/035824068-icon-service-Azure-Data-Sharing.png rename to core/assets/icons/azure/new icons/035824068-icon-service-Azure-Data-Sharing.png diff --git a/assets/icons/azure/new icons/035824068-icon-service-Azure-Data-Sharing.svg b/core/assets/icons/azure/new icons/035824068-icon-service-Azure-Data-Sharing.svg similarity index 100% rename from assets/icons/azure/new icons/035824068-icon-service-Azure-Data-Sharing.svg rename to core/assets/icons/azure/new icons/035824068-icon-service-Azure-Data-Sharing.svg diff --git a/assets/icons/azure/new icons/037838045-icon-service-AI-Gateway.png b/core/assets/icons/azure/new icons/037838045-icon-service-AI-Gateway.png similarity index 100% rename from assets/icons/azure/new icons/037838045-icon-service-AI-Gateway.png rename to core/assets/icons/azure/new icons/037838045-icon-service-AI-Gateway.png diff --git a/assets/icons/azure/new icons/037838045-icon-service-AI-Gateway.svg b/core/assets/icons/azure/new icons/037838045-icon-service-AI-Gateway.svg similarity index 100% rename from assets/icons/azure/new icons/037838045-icon-service-AI-Gateway.svg rename to core/assets/icons/azure/new icons/037838045-icon-service-AI-Gateway.svg diff --git a/assets/icons/azure/other/00327-icon-service-Azure-Virtual-Desktop.png b/core/assets/icons/azure/other/00327-icon-service-Azure-Virtual-Desktop.png similarity index 100% rename from assets/icons/azure/other/00327-icon-service-Azure-Virtual-Desktop.png rename to core/assets/icons/azure/other/00327-icon-service-Azure-Virtual-Desktop.png diff --git a/assets/icons/azure/other/00327-icon-service-Azure-Virtual-Desktop.svg b/core/assets/icons/azure/other/00327-icon-service-Azure-Virtual-Desktop.svg similarity index 100% rename from assets/icons/azure/other/00327-icon-service-Azure-Virtual-Desktop.svg rename to core/assets/icons/azure/other/00327-icon-service-Azure-Virtual-Desktop.svg diff --git a/assets/icons/azure/other/00412-icon-service-SSH-Keys.png b/core/assets/icons/azure/other/00412-icon-service-SSH-Keys.png similarity index 100% rename from assets/icons/azure/other/00412-icon-service-SSH-Keys.png rename to core/assets/icons/azure/other/00412-icon-service-SSH-Keys.png diff --git a/assets/icons/azure/other/00412-icon-service-SSH-Keys.svg b/core/assets/icons/azure/other/00412-icon-service-SSH-Keys.svg similarity index 100% rename from assets/icons/azure/other/00412-icon-service-SSH-Keys.svg rename to core/assets/icons/azure/other/00412-icon-service-SSH-Keys.svg diff --git a/assets/icons/azure/other/00469-icon-service-Internet-Analyzer-Profiles.png b/core/assets/icons/azure/other/00469-icon-service-Internet-Analyzer-Profiles.png similarity index 100% rename from assets/icons/azure/other/00469-icon-service-Internet-Analyzer-Profiles.png rename to core/assets/icons/azure/other/00469-icon-service-Internet-Analyzer-Profiles.png diff --git a/assets/icons/azure/other/00469-icon-service-Internet-Analyzer-Profiles.svg b/core/assets/icons/azure/other/00469-icon-service-Internet-Analyzer-Profiles.svg similarity index 100% rename from assets/icons/azure/other/00469-icon-service-Internet-Analyzer-Profiles.svg rename to core/assets/icons/azure/other/00469-icon-service-Internet-Analyzer-Profiles.svg diff --git a/assets/icons/azure/other/00559-icon-service-Azure-Cloud-Shell.png b/core/assets/icons/azure/other/00559-icon-service-Azure-Cloud-Shell.png similarity index 100% rename from assets/icons/azure/other/00559-icon-service-Azure-Cloud-Shell.png rename to core/assets/icons/azure/other/00559-icon-service-Azure-Cloud-Shell.png diff --git a/assets/icons/azure/other/00559-icon-service-Azure-Cloud-Shell.svg b/core/assets/icons/azure/other/00559-icon-service-Azure-Cloud-Shell.svg similarity index 100% rename from assets/icons/azure/other/00559-icon-service-Azure-Cloud-Shell.svg rename to core/assets/icons/azure/other/00559-icon-service-Azure-Cloud-Shell.svg diff --git a/assets/icons/azure/other/00712-icon-service-Video-Analyzers.png b/core/assets/icons/azure/other/00712-icon-service-Video-Analyzers.png similarity index 100% rename from assets/icons/azure/other/00712-icon-service-Video-Analyzers.png rename to core/assets/icons/azure/other/00712-icon-service-Video-Analyzers.png diff --git a/assets/icons/azure/other/00712-icon-service-Video-Analyzers.svg b/core/assets/icons/azure/other/00712-icon-service-Video-Analyzers.svg similarity index 100% rename from assets/icons/azure/other/00712-icon-service-Video-Analyzers.svg rename to core/assets/icons/azure/other/00712-icon-service-Video-Analyzers.svg diff --git a/assets/icons/azure/other/00903-icon-service-ExpressRoute-Direct.png b/core/assets/icons/azure/other/00903-icon-service-ExpressRoute-Direct.png similarity index 100% rename from assets/icons/azure/other/00903-icon-service-ExpressRoute-Direct.png rename to core/assets/icons/azure/other/00903-icon-service-ExpressRoute-Direct.png diff --git a/assets/icons/azure/other/00903-icon-service-ExpressRoute-Direct.svg b/core/assets/icons/azure/other/00903-icon-service-ExpressRoute-Direct.svg similarity index 100% rename from assets/icons/azure/other/00903-icon-service-ExpressRoute-Direct.svg rename to core/assets/icons/azure/other/00903-icon-service-ExpressRoute-Direct.svg diff --git a/assets/icons/azure/other/00913-icon-service-Cost-Export.png b/core/assets/icons/azure/other/00913-icon-service-Cost-Export.png similarity index 100% rename from assets/icons/azure/other/00913-icon-service-Cost-Export.png rename to core/assets/icons/azure/other/00913-icon-service-Cost-Export.png diff --git a/assets/icons/azure/other/00913-icon-service-Cost-Export.svg b/core/assets/icons/azure/other/00913-icon-service-Cost-Export.svg similarity index 100% rename from assets/icons/azure/other/00913-icon-service-Cost-Export.svg rename to core/assets/icons/azure/other/00913-icon-service-Cost-Export.svg diff --git a/assets/icons/azure/other/00968-icon-service-Azure-Communication-Services.png b/core/assets/icons/azure/other/00968-icon-service-Azure-Communication-Services.png similarity index 100% rename from assets/icons/azure/other/00968-icon-service-Azure-Communication-Services.png rename to core/assets/icons/azure/other/00968-icon-service-Azure-Communication-Services.png diff --git a/assets/icons/azure/other/00968-icon-service-Azure-Communication-Services.svg b/core/assets/icons/azure/other/00968-icon-service-Azure-Communication-Services.svg similarity index 100% rename from assets/icons/azure/other/00968-icon-service-Azure-Communication-Services.svg rename to core/assets/icons/azure/other/00968-icon-service-Azure-Communication-Services.svg diff --git a/assets/icons/azure/other/00970-icon-service-Peering-Service.png b/core/assets/icons/azure/other/00970-icon-service-Peering-Service.png similarity index 100% rename from assets/icons/azure/other/00970-icon-service-Peering-Service.png rename to core/assets/icons/azure/other/00970-icon-service-Peering-Service.png diff --git a/assets/icons/azure/other/00970-icon-service-Peering-Service.svg b/core/assets/icons/azure/other/00970-icon-service-Peering-Service.svg similarity index 100% rename from assets/icons/azure/other/00970-icon-service-Peering-Service.svg rename to core/assets/icons/azure/other/00970-icon-service-Peering-Service.svg diff --git a/assets/icons/azure/other/01001-icon-service-Azure-Network-Function-Manager-Functions.png b/core/assets/icons/azure/other/01001-icon-service-Azure-Network-Function-Manager-Functions.png similarity index 100% rename from assets/icons/azure/other/01001-icon-service-Azure-Network-Function-Manager-Functions.png rename to core/assets/icons/azure/other/01001-icon-service-Azure-Network-Function-Manager-Functions.png diff --git a/assets/icons/azure/other/01001-icon-service-Azure-Network-Function-Manager-Functions.svg b/core/assets/icons/azure/other/01001-icon-service-Azure-Network-Function-Manager-Functions.svg similarity index 100% rename from assets/icons/azure/other/01001-icon-service-Azure-Network-Function-Manager-Functions.svg rename to core/assets/icons/azure/other/01001-icon-service-Azure-Network-Function-Manager-Functions.svg diff --git a/assets/icons/azure/other/01085-icon-service-Log-Analytics-Query-Pack.png b/core/assets/icons/azure/other/01085-icon-service-Log-Analytics-Query-Pack.png similarity index 100% rename from assets/icons/azure/other/01085-icon-service-Log-Analytics-Query-Pack.png rename to core/assets/icons/azure/other/01085-icon-service-Log-Analytics-Query-Pack.png diff --git a/assets/icons/azure/other/01085-icon-service-Log-Analytics-Query-Pack.svg b/core/assets/icons/azure/other/01085-icon-service-Log-Analytics-Query-Pack.svg similarity index 100% rename from assets/icons/azure/other/01085-icon-service-Log-Analytics-Query-Pack.svg rename to core/assets/icons/azure/other/01085-icon-service-Log-Analytics-Query-Pack.svg diff --git a/assets/icons/azure/other/01088-icon-service-Arc-Kubernetes.png b/core/assets/icons/azure/other/01088-icon-service-Arc-Kubernetes.png similarity index 100% rename from assets/icons/azure/other/01088-icon-service-Arc-Kubernetes.png rename to core/assets/icons/azure/other/01088-icon-service-Arc-Kubernetes.png diff --git a/assets/icons/azure/other/01088-icon-service-Arc-Kubernetes.svg b/core/assets/icons/azure/other/01088-icon-service-Arc-Kubernetes.svg similarity index 100% rename from assets/icons/azure/other/01088-icon-service-Arc-Kubernetes.svg rename to core/assets/icons/azure/other/01088-icon-service-Arc-Kubernetes.svg diff --git a/assets/icons/azure/other/01219-icon-service-Azure-VMware-Solution.png b/core/assets/icons/azure/other/01219-icon-service-Azure-VMware-Solution.png similarity index 100% rename from assets/icons/azure/other/01219-icon-service-Azure-VMware-Solution.png rename to core/assets/icons/azure/other/01219-icon-service-Azure-VMware-Solution.png diff --git a/assets/icons/azure/other/01219-icon-service-Azure-VMware-Solution.svg b/core/assets/icons/azure/other/01219-icon-service-Azure-VMware-Solution.svg similarity index 100% rename from assets/icons/azure/other/01219-icon-service-Azure-VMware-Solution.svg rename to core/assets/icons/azure/other/01219-icon-service-Azure-VMware-Solution.svg diff --git a/assets/icons/azure/other/01285-icon-service-Peerings.png b/core/assets/icons/azure/other/01285-icon-service-Peerings.png similarity index 100% rename from assets/icons/azure/other/01285-icon-service-Peerings.png rename to core/assets/icons/azure/other/01285-icon-service-Peerings.png diff --git a/assets/icons/azure/other/01285-icon-service-Peerings.svg b/core/assets/icons/azure/other/01285-icon-service-Peerings.svg similarity index 100% rename from assets/icons/azure/other/01285-icon-service-Peerings.svg rename to core/assets/icons/azure/other/01285-icon-service-Peerings.svg diff --git a/assets/icons/azure/other/01757-icon-service-Dashboard-Hub.png b/core/assets/icons/azure/other/01757-icon-service-Dashboard-Hub.png similarity index 100% rename from assets/icons/azure/other/01757-icon-service-Dashboard-Hub.png rename to core/assets/icons/azure/other/01757-icon-service-Dashboard-Hub.png diff --git a/assets/icons/azure/other/01757-icon-service-Dashboard-Hub.svg b/core/assets/icons/azure/other/01757-icon-service-Dashboard-Hub.svg similarity index 100% rename from assets/icons/azure/other/01757-icon-service-Dashboard-Hub.svg rename to core/assets/icons/azure/other/01757-icon-service-Dashboard-Hub.svg diff --git a/assets/icons/azure/other/01800-icon-service-Azure-Video-Indexer.png b/core/assets/icons/azure/other/01800-icon-service-Azure-Video-Indexer.png similarity index 100% rename from assets/icons/azure/other/01800-icon-service-Azure-Video-Indexer.png rename to core/assets/icons/azure/other/01800-icon-service-Azure-Video-Indexer.png diff --git a/assets/icons/azure/other/01800-icon-service-Azure-Video-Indexer.svg b/core/assets/icons/azure/other/01800-icon-service-Azure-Video-Indexer.svg similarity index 100% rename from assets/icons/azure/other/01800-icon-service-Azure-Video-Indexer.svg rename to core/assets/icons/azure/other/01800-icon-service-Azure-Video-Indexer.svg diff --git a/assets/icons/azure/other/01846-icon-service-AVS-VM.png b/core/assets/icons/azure/other/01846-icon-service-AVS-VM.png similarity index 100% rename from assets/icons/azure/other/01846-icon-service-AVS-VM.png rename to core/assets/icons/azure/other/01846-icon-service-AVS-VM.png diff --git a/assets/icons/azure/other/01846-icon-service-AVS-VM.svg b/core/assets/icons/azure/other/01846-icon-service-AVS-VM.svg similarity index 100% rename from assets/icons/azure/other/01846-icon-service-AVS-VM.svg rename to core/assets/icons/azure/other/01846-icon-service-AVS-VM.svg diff --git a/assets/icons/azure/other/01848-icon-service-Arc-PostgreSQL .png b/core/assets/icons/azure/other/01848-icon-service-Arc-PostgreSQL .png similarity index 100% rename from assets/icons/azure/other/01848-icon-service-Arc-PostgreSQL .png rename to core/assets/icons/azure/other/01848-icon-service-Arc-PostgreSQL .png diff --git a/assets/icons/azure/other/01848-icon-service-Arc-PostgreSQL .svg b/core/assets/icons/azure/other/01848-icon-service-Arc-PostgreSQL .svg similarity index 100% rename from assets/icons/azure/other/01848-icon-service-Arc-PostgreSQL .svg rename to core/assets/icons/azure/other/01848-icon-service-Arc-PostgreSQL .svg diff --git a/assets/icons/azure/other/01849-icon-service-Arc-SQL-Managed-Instance.png b/core/assets/icons/azure/other/01849-icon-service-Arc-SQL-Managed-Instance.png similarity index 100% rename from assets/icons/azure/other/01849-icon-service-Arc-SQL-Managed-Instance.png rename to core/assets/icons/azure/other/01849-icon-service-Arc-SQL-Managed-Instance.png diff --git a/assets/icons/azure/other/01849-icon-service-Arc-SQL-Managed-Instance.svg b/core/assets/icons/azure/other/01849-icon-service-Arc-SQL-Managed-Instance.svg similarity index 100% rename from assets/icons/azure/other/01849-icon-service-Arc-SQL-Managed-Instance.svg rename to core/assets/icons/azure/other/01849-icon-service-Arc-SQL-Managed-Instance.svg diff --git a/assets/icons/azure/other/01850-icon-service-Arc-SQL-Server.png b/core/assets/icons/azure/other/01850-icon-service-Arc-SQL-Server.png similarity index 100% rename from assets/icons/azure/other/01850-icon-service-Arc-SQL-Server.png rename to core/assets/icons/azure/other/01850-icon-service-Arc-SQL-Server.png diff --git a/assets/icons/azure/other/01850-icon-service-Arc-SQL-Server.svg b/core/assets/icons/azure/other/01850-icon-service-Arc-SQL-Server.svg similarity index 100% rename from assets/icons/azure/other/01850-icon-service-Arc-SQL-Server.svg rename to core/assets/icons/azure/other/01850-icon-service-Arc-SQL-Server.svg diff --git a/assets/icons/azure/other/01857-icon-service-Data-Collection-Rules.png b/core/assets/icons/azure/other/01857-icon-service-Data-Collection-Rules.png similarity index 100% rename from assets/icons/azure/other/01857-icon-service-Data-Collection-Rules.png rename to core/assets/icons/azure/other/01857-icon-service-Data-Collection-Rules.png diff --git a/assets/icons/azure/other/01857-icon-service-Data-Collection-Rules.svg b/core/assets/icons/azure/other/01857-icon-service-Data-Collection-Rules.svg similarity index 100% rename from assets/icons/azure/other/01857-icon-service-Data-Collection-Rules.svg rename to core/assets/icons/azure/other/01857-icon-service-Data-Collection-Rules.svg diff --git a/assets/icons/azure/other/02200-icon-service-Resource-Mover.png b/core/assets/icons/azure/other/02200-icon-service-Resource-Mover.png similarity index 100% rename from assets/icons/azure/other/02200-icon-service-Resource-Mover.png rename to core/assets/icons/azure/other/02200-icon-service-Resource-Mover.png diff --git a/assets/icons/azure/other/02200-icon-service-Resource-Mover.svg b/core/assets/icons/azure/other/02200-icon-service-Resource-Mover.svg similarity index 100% rename from assets/icons/azure/other/02200-icon-service-Resource-Mover.svg rename to core/assets/icons/azure/other/02200-icon-service-Resource-Mover.svg diff --git a/assets/icons/azure/other/02223-icon-service-Azure-Chaos-Studio.png b/core/assets/icons/azure/other/02223-icon-service-Azure-Chaos-Studio.png similarity index 100% rename from assets/icons/azure/other/02223-icon-service-Azure-Chaos-Studio.png rename to core/assets/icons/azure/other/02223-icon-service-Azure-Chaos-Studio.png diff --git a/assets/icons/azure/other/02223-icon-service-Azure-Chaos-Studio.svg b/core/assets/icons/azure/other/02223-icon-service-Azure-Chaos-Studio.svg similarity index 100% rename from assets/icons/azure/other/02223-icon-service-Azure-Chaos-Studio.svg rename to core/assets/icons/azure/other/02223-icon-service-Azure-Chaos-Studio.svg diff --git a/assets/icons/azure/other/02237-icon-service-Network-Managers.png b/core/assets/icons/azure/other/02237-icon-service-Network-Managers.png similarity index 100% rename from assets/icons/azure/other/02237-icon-service-Network-Managers.png rename to core/assets/icons/azure/other/02237-icon-service-Network-Managers.png diff --git a/assets/icons/azure/other/02237-icon-service-Network-Managers.svg b/core/assets/icons/azure/other/02237-icon-service-Network-Managers.svg similarity index 100% rename from assets/icons/azure/other/02237-icon-service-Network-Managers.svg rename to core/assets/icons/azure/other/02237-icon-service-Network-Managers.svg diff --git a/assets/icons/azure/other/02322-icon-service-Dedicated-HSM.png b/core/assets/icons/azure/other/02322-icon-service-Dedicated-HSM.png similarity index 100% rename from assets/icons/azure/other/02322-icon-service-Dedicated-HSM.png rename to core/assets/icons/azure/other/02322-icon-service-Dedicated-HSM.png diff --git a/assets/icons/azure/other/02322-icon-service-Dedicated-HSM.svg b/core/assets/icons/azure/other/02322-icon-service-Dedicated-HSM.svg similarity index 100% rename from assets/icons/azure/other/02322-icon-service-Dedicated-HSM.svg rename to core/assets/icons/azure/other/02322-icon-service-Dedicated-HSM.svg diff --git a/assets/icons/azure/other/02323-icon-service-Modular-Data-Center.png b/core/assets/icons/azure/other/02323-icon-service-Modular-Data-Center.png similarity index 100% rename from assets/icons/azure/other/02323-icon-service-Modular-Data-Center.png rename to core/assets/icons/azure/other/02323-icon-service-Modular-Data-Center.png diff --git a/assets/icons/azure/other/02323-icon-service-Modular-Data-Center.svg b/core/assets/icons/azure/other/02323-icon-service-Modular-Data-Center.svg similarity index 100% rename from assets/icons/azure/other/02323-icon-service-Modular-Data-Center.svg rename to core/assets/icons/azure/other/02323-icon-service-Modular-Data-Center.svg diff --git a/assets/icons/azure/other/02340-icon-service-Template-Specs.png b/core/assets/icons/azure/other/02340-icon-service-Template-Specs.png similarity index 100% rename from assets/icons/azure/other/02340-icon-service-Template-Specs.png rename to core/assets/icons/azure/other/02340-icon-service-Template-Specs.png diff --git a/assets/icons/azure/other/02340-icon-service-Template-Specs.svg b/core/assets/icons/azure/other/02340-icon-service-Template-Specs.svg similarity index 100% rename from assets/icons/azure/other/02340-icon-service-Template-Specs.svg rename to core/assets/icons/azure/other/02340-icon-service-Template-Specs.svg diff --git a/assets/icons/azure/other/02356-icon-service-Arc-Data-services.png b/core/assets/icons/azure/other/02356-icon-service-Arc-Data-services.png similarity index 100% rename from assets/icons/azure/other/02356-icon-service-Arc-Data-services.png rename to core/assets/icons/azure/other/02356-icon-service-Arc-Data-services.png diff --git a/assets/icons/azure/other/02356-icon-service-Arc-Data-services.svg b/core/assets/icons/azure/other/02356-icon-service-Arc-Data-services.svg similarity index 100% rename from assets/icons/azure/other/02356-icon-service-Arc-Data-services.svg rename to core/assets/icons/azure/other/02356-icon-service-Arc-Data-services.svg diff --git a/assets/icons/azure/other/02360-icon-service-Azure-Backup-Center.png b/core/assets/icons/azure/other/02360-icon-service-Azure-Backup-Center.png similarity index 100% rename from assets/icons/azure/other/02360-icon-service-Azure-Backup-Center.png rename to core/assets/icons/azure/other/02360-icon-service-Azure-Backup-Center.png diff --git a/assets/icons/azure/other/02360-icon-service-Azure-Backup-Center.svg b/core/assets/icons/azure/other/02360-icon-service-Azure-Backup-Center.svg similarity index 100% rename from assets/icons/azure/other/02360-icon-service-Azure-Backup-Center.svg rename to core/assets/icons/azure/other/02360-icon-service-Azure-Backup-Center.svg diff --git a/assets/icons/azure/other/02361-icon-service-Backup-Vault.png b/core/assets/icons/azure/other/02361-icon-service-Backup-Vault.png similarity index 100% rename from assets/icons/azure/other/02361-icon-service-Backup-Vault.png rename to core/assets/icons/azure/other/02361-icon-service-Backup-Vault.png diff --git a/assets/icons/azure/other/02361-icon-service-Backup-Vault.svg b/core/assets/icons/azure/other/02361-icon-service-Backup-Vault.svg similarity index 100% rename from assets/icons/azure/other/02361-icon-service-Backup-Vault.svg rename to core/assets/icons/azure/other/02361-icon-service-Backup-Vault.svg diff --git a/assets/icons/azure/other/02475-icon-service-Device-Update-IoT-Hub.png b/core/assets/icons/azure/other/02475-icon-service-Device-Update-IoT-Hub.png similarity index 100% rename from assets/icons/azure/other/02475-icon-service-Device-Update-IoT-Hub.png rename to core/assets/icons/azure/other/02475-icon-service-Device-Update-IoT-Hub.png diff --git a/assets/icons/azure/other/02475-icon-service-Device-Update-IoT-Hub.svg b/core/assets/icons/azure/other/02475-icon-service-Device-Update-IoT-Hub.svg similarity index 100% rename from assets/icons/azure/other/02475-icon-service-Device-Update-IoT-Hub.svg rename to core/assets/icons/azure/other/02475-icon-service-Device-Update-IoT-Hub.svg diff --git a/assets/icons/azure/other/02486-icon-service-Fiji.png b/core/assets/icons/azure/other/02486-icon-service-Fiji.png similarity index 100% rename from assets/icons/azure/other/02486-icon-service-Fiji.png rename to core/assets/icons/azure/other/02486-icon-service-Fiji.png diff --git a/assets/icons/azure/other/02486-icon-service-Fiji.svg b/core/assets/icons/azure/other/02486-icon-service-Fiji.svg similarity index 100% rename from assets/icons/azure/other/02486-icon-service-Fiji.svg rename to core/assets/icons/azure/other/02486-icon-service-Fiji.svg diff --git a/assets/icons/azure/other/02488-icon-service-Azure-Monitor-Dashboard.png b/core/assets/icons/azure/other/02488-icon-service-Azure-Monitor-Dashboard.png similarity index 100% rename from assets/icons/azure/other/02488-icon-service-Azure-Monitor-Dashboard.png rename to core/assets/icons/azure/other/02488-icon-service-Azure-Monitor-Dashboard.png diff --git a/assets/icons/azure/other/02488-icon-service-Azure-Monitor-Dashboard.svg b/core/assets/icons/azure/other/02488-icon-service-Azure-Monitor-Dashboard.svg similarity index 100% rename from assets/icons/azure/other/02488-icon-service-Azure-Monitor-Dashboard.svg rename to core/assets/icons/azure/other/02488-icon-service-Azure-Monitor-Dashboard.svg diff --git a/assets/icons/azure/other/02503-icon-service-SCVMM-Management-Servers.png b/core/assets/icons/azure/other/02503-icon-service-SCVMM-Management-Servers.png similarity index 100% rename from assets/icons/azure/other/02503-icon-service-SCVMM-Management-Servers.png rename to core/assets/icons/azure/other/02503-icon-service-SCVMM-Management-Servers.png diff --git a/assets/icons/azure/other/02503-icon-service-SCVMM-Management-Servers.svg b/core/assets/icons/azure/other/02503-icon-service-SCVMM-Management-Servers.svg similarity index 100% rename from assets/icons/azure/other/02503-icon-service-SCVMM-Management-Servers.svg rename to core/assets/icons/azure/other/02503-icon-service-SCVMM-Management-Servers.svg diff --git a/assets/icons/azure/other/02505-icon-service-Cloud-Services-(extended-support).png b/core/assets/icons/azure/other/02505-icon-service-Cloud-Services-(extended-support).png similarity index 100% rename from assets/icons/azure/other/02505-icon-service-Cloud-Services-(extended-support).png rename to core/assets/icons/azure/other/02505-icon-service-Cloud-Services-(extended-support).png diff --git a/assets/icons/azure/other/02505-icon-service-Cloud-Services-(extended-support).svg b/core/assets/icons/azure/other/02505-icon-service-Cloud-Services-(extended-support).svg similarity index 100% rename from assets/icons/azure/other/02505-icon-service-Cloud-Services-(extended-support).svg rename to core/assets/icons/azure/other/02505-icon-service-Cloud-Services-(extended-support).svg diff --git a/assets/icons/azure/other/02507-icon-service-Azure-Support-Center-Blue.png b/core/assets/icons/azure/other/02507-icon-service-Azure-Support-Center-Blue.png similarity index 100% rename from assets/icons/azure/other/02507-icon-service-Azure-Support-Center-Blue.png rename to core/assets/icons/azure/other/02507-icon-service-Azure-Support-Center-Blue.png diff --git a/assets/icons/azure/other/02507-icon-service-Azure-Support-Center-Blue.svg b/core/assets/icons/azure/other/02507-icon-service-Azure-Support-Center-Blue.svg similarity index 100% rename from assets/icons/azure/other/02507-icon-service-Azure-Support-Center-Blue.svg rename to core/assets/icons/azure/other/02507-icon-service-Azure-Support-Center-Blue.svg diff --git a/assets/icons/azure/other/02515-icon-service-Web-App-+-Database.png b/core/assets/icons/azure/other/02515-icon-service-Web-App-+-Database.png similarity index 100% rename from assets/icons/azure/other/02515-icon-service-Web-App-+-Database.png rename to core/assets/icons/azure/other/02515-icon-service-Web-App-+-Database.png diff --git a/assets/icons/azure/other/02515-icon-service-Web-App-+-Database.svg b/core/assets/icons/azure/other/02515-icon-service-Web-App-+-Database.svg similarity index 100% rename from assets/icons/azure/other/02515-icon-service-Web-App-+-Database.svg rename to core/assets/icons/azure/other/02515-icon-service-Web-App-+-Database.svg diff --git a/assets/icons/azure/other/02518-icon-service-Azure-HPC-Workbenches.png b/core/assets/icons/azure/other/02518-icon-service-Azure-HPC-Workbenches.png similarity index 100% rename from assets/icons/azure/other/02518-icon-service-Azure-HPC-Workbenches.png rename to core/assets/icons/azure/other/02518-icon-service-Azure-HPC-Workbenches.png diff --git a/assets/icons/azure/other/02518-icon-service-Azure-HPC-Workbenches.svg b/core/assets/icons/azure/other/02518-icon-service-Azure-HPC-Workbenches.svg similarity index 100% rename from assets/icons/azure/other/02518-icon-service-Azure-HPC-Workbenches.svg rename to core/assets/icons/azure/other/02518-icon-service-Azure-HPC-Workbenches.svg diff --git a/assets/icons/azure/other/02560-icon-service-Disk-Pool.png b/core/assets/icons/azure/other/02560-icon-service-Disk-Pool.png similarity index 100% rename from assets/icons/azure/other/02560-icon-service-Disk-Pool.png rename to core/assets/icons/azure/other/02560-icon-service-Disk-Pool.png diff --git a/assets/icons/azure/other/02560-icon-service-Disk-Pool.svg b/core/assets/icons/azure/other/02560-icon-service-Disk-Pool.svg similarity index 100% rename from assets/icons/azure/other/02560-icon-service-Disk-Pool.svg rename to core/assets/icons/azure/other/02560-icon-service-Disk-Pool.svg diff --git a/assets/icons/azure/other/02561-icon-service-Bare-Metal-Infrastructure.png b/core/assets/icons/azure/other/02561-icon-service-Bare-Metal-Infrastructure.png similarity index 100% rename from assets/icons/azure/other/02561-icon-service-Bare-Metal-Infrastructure.png rename to core/assets/icons/azure/other/02561-icon-service-Bare-Metal-Infrastructure.png diff --git a/assets/icons/azure/other/02561-icon-service-Bare-Metal-Infrastructure.svg b/core/assets/icons/azure/other/02561-icon-service-Bare-Metal-Infrastructure.svg similarity index 100% rename from assets/icons/azure/other/02561-icon-service-Bare-Metal-Infrastructure.svg rename to core/assets/icons/azure/other/02561-icon-service-Bare-Metal-Infrastructure.svg diff --git a/assets/icons/azure/other/02579-icon-service-Private-Endpoints.png b/core/assets/icons/azure/other/02579-icon-service-Private-Endpoints.png similarity index 100% rename from assets/icons/azure/other/02579-icon-service-Private-Endpoints.png rename to core/assets/icons/azure/other/02579-icon-service-Private-Endpoints.png diff --git a/assets/icons/azure/other/02579-icon-service-Private-Endpoints.svg b/core/assets/icons/azure/other/02579-icon-service-Private-Endpoints.svg similarity index 100% rename from assets/icons/azure/other/02579-icon-service-Private-Endpoints.svg rename to core/assets/icons/azure/other/02579-icon-service-Private-Endpoints.svg diff --git a/assets/icons/azure/other/02602-icon-service-Open-Supply-Chain-Platform.png b/core/assets/icons/azure/other/02602-icon-service-Open-Supply-Chain-Platform.png similarity index 100% rename from assets/icons/azure/other/02602-icon-service-Open-Supply-Chain-Platform.png rename to core/assets/icons/azure/other/02602-icon-service-Open-Supply-Chain-Platform.png diff --git a/assets/icons/azure/other/02602-icon-service-Open-Supply-Chain-Platform.svg b/core/assets/icons/azure/other/02602-icon-service-Open-Supply-Chain-Platform.svg similarity index 100% rename from assets/icons/azure/other/02602-icon-service-Open-Supply-Chain-Platform.svg rename to core/assets/icons/azure/other/02602-icon-service-Open-Supply-Chain-Platform.svg diff --git a/assets/icons/azure/other/02625-icon-service-Aquila.png b/core/assets/icons/azure/other/02625-icon-service-Aquila.png similarity index 100% rename from assets/icons/azure/other/02625-icon-service-Aquila.png rename to core/assets/icons/azure/other/02625-icon-service-Aquila.png diff --git a/assets/icons/azure/other/02625-icon-service-Aquila.svg b/core/assets/icons/azure/other/02625-icon-service-Aquila.svg similarity index 100% rename from assets/icons/azure/other/02625-icon-service-Aquila.svg rename to core/assets/icons/azure/other/02625-icon-service-Aquila.svg diff --git a/assets/icons/azure/other/02638-icon-service-Reserved-Capacity.png b/core/assets/icons/azure/other/02638-icon-service-Reserved-Capacity.png similarity index 100% rename from assets/icons/azure/other/02638-icon-service-Reserved-Capacity.png rename to core/assets/icons/azure/other/02638-icon-service-Reserved-Capacity.png diff --git a/assets/icons/azure/other/02638-icon-service-Reserved-Capacity.svg b/core/assets/icons/azure/other/02638-icon-service-Reserved-Capacity.svg similarity index 100% rename from assets/icons/azure/other/02638-icon-service-Reserved-Capacity.svg rename to core/assets/icons/azure/other/02638-icon-service-Reserved-Capacity.svg diff --git a/assets/icons/azure/other/02654-icon-service-Custom-IP-Prefix.png b/core/assets/icons/azure/other/02654-icon-service-Custom-IP-Prefix.png similarity index 100% rename from assets/icons/azure/other/02654-icon-service-Custom-IP-Prefix.png rename to core/assets/icons/azure/other/02654-icon-service-Custom-IP-Prefix.png diff --git a/assets/icons/azure/other/02654-icon-service-Custom-IP-Prefix.svg b/core/assets/icons/azure/other/02654-icon-service-Custom-IP-Prefix.svg similarity index 100% rename from assets/icons/azure/other/02654-icon-service-Custom-IP-Prefix.svg rename to core/assets/icons/azure/other/02654-icon-service-Custom-IP-Prefix.svg diff --git a/assets/icons/azure/other/02658-icon-service-FHIR-Service.png b/core/assets/icons/azure/other/02658-icon-service-FHIR-Service.png similarity index 100% rename from assets/icons/azure/other/02658-icon-service-FHIR-Service.png rename to core/assets/icons/azure/other/02658-icon-service-FHIR-Service.png diff --git a/assets/icons/azure/other/02658-icon-service-FHIR-Service.svg b/core/assets/icons/azure/other/02658-icon-service-FHIR-Service.svg similarity index 100% rename from assets/icons/azure/other/02658-icon-service-FHIR-Service.svg rename to core/assets/icons/azure/other/02658-icon-service-FHIR-Service.svg diff --git a/assets/icons/azure/other/02660-icon-service-MedTech-Service.png b/core/assets/icons/azure/other/02660-icon-service-MedTech-Service.png similarity index 100% rename from assets/icons/azure/other/02660-icon-service-MedTech-Service.png rename to core/assets/icons/azure/other/02660-icon-service-MedTech-Service.png diff --git a/assets/icons/azure/other/02660-icon-service-MedTech-Service.svg b/core/assets/icons/azure/other/02660-icon-service-MedTech-Service.svg similarity index 100% rename from assets/icons/azure/other/02660-icon-service-MedTech-Service.svg rename to core/assets/icons/azure/other/02660-icon-service-MedTech-Service.svg diff --git a/assets/icons/azure/other/02663-icon-service-Managed-Instance-Apache-Cassandra.png b/core/assets/icons/azure/other/02663-icon-service-Managed-Instance-Apache-Cassandra.png similarity index 100% rename from assets/icons/azure/other/02663-icon-service-Managed-Instance-Apache-Cassandra.png rename to core/assets/icons/azure/other/02663-icon-service-Managed-Instance-Apache-Cassandra.png diff --git a/assets/icons/azure/other/02663-icon-service-Managed-Instance-Apache-Cassandra.svg b/core/assets/icons/azure/other/02663-icon-service-Managed-Instance-Apache-Cassandra.svg similarity index 100% rename from assets/icons/azure/other/02663-icon-service-Managed-Instance-Apache-Cassandra.svg rename to core/assets/icons/azure/other/02663-icon-service-Managed-Instance-Apache-Cassandra.svg diff --git a/assets/icons/azure/other/02668-icon-service-Confidential-Ledgers.png b/core/assets/icons/azure/other/02668-icon-service-Confidential-Ledgers.png similarity index 100% rename from assets/icons/azure/other/02668-icon-service-Confidential-Ledgers.png rename to core/assets/icons/azure/other/02668-icon-service-Confidential-Ledgers.png diff --git a/assets/icons/azure/other/02668-icon-service-Confidential-Ledgers.svg b/core/assets/icons/azure/other/02668-icon-service-Confidential-Ledgers.svg similarity index 100% rename from assets/icons/azure/other/02668-icon-service-Confidential-Ledgers.svg rename to core/assets/icons/azure/other/02668-icon-service-Confidential-Ledgers.svg diff --git a/assets/icons/azure/other/02681-icon-service-AAD-Licenses.png b/core/assets/icons/azure/other/02681-icon-service-AAD-Licenses.png similarity index 100% rename from assets/icons/azure/other/02681-icon-service-AAD-Licenses.png rename to core/assets/icons/azure/other/02681-icon-service-AAD-Licenses.png diff --git a/assets/icons/azure/other/02681-icon-service-AAD-Licenses.svg b/core/assets/icons/azure/other/02681-icon-service-AAD-Licenses.svg similarity index 100% rename from assets/icons/azure/other/02681-icon-service-AAD-Licenses.svg rename to core/assets/icons/azure/other/02681-icon-service-AAD-Licenses.svg diff --git a/assets/icons/azure/other/02681-icon-service-Entra-Identity-Licenses.png b/core/assets/icons/azure/other/02681-icon-service-Entra-Identity-Licenses.png similarity index 100% rename from assets/icons/azure/other/02681-icon-service-Entra-Identity-Licenses.png rename to core/assets/icons/azure/other/02681-icon-service-Entra-Identity-Licenses.png diff --git a/assets/icons/azure/other/02681-icon-service-Entra-Identity-Licenses.svg b/core/assets/icons/azure/other/02681-icon-service-Entra-Identity-Licenses.svg similarity index 100% rename from assets/icons/azure/other/02681-icon-service-Entra-Identity-Licenses.svg rename to core/assets/icons/azure/other/02681-icon-service-Entra-Identity-Licenses.svg diff --git a/assets/icons/azure/other/02691-icon-service-Test-Base.png b/core/assets/icons/azure/other/02691-icon-service-Test-Base.png similarity index 100% rename from assets/icons/azure/other/02691-icon-service-Test-Base.png rename to core/assets/icons/azure/other/02691-icon-service-Test-Base.png diff --git a/assets/icons/azure/other/02691-icon-service-Test-Base.svg b/core/assets/icons/azure/other/02691-icon-service-Test-Base.svg similarity index 100% rename from assets/icons/azure/other/02691-icon-service-Test-Base.svg rename to core/assets/icons/azure/other/02691-icon-service-Test-Base.svg diff --git a/assets/icons/azure/other/02697-icon-service-Azure-Orbital.png b/core/assets/icons/azure/other/02697-icon-service-Azure-Orbital.png similarity index 100% rename from assets/icons/azure/other/02697-icon-service-Azure-Orbital.png rename to core/assets/icons/azure/other/02697-icon-service-Azure-Orbital.png diff --git a/assets/icons/azure/other/02697-icon-service-Azure-Orbital.svg b/core/assets/icons/azure/other/02697-icon-service-Azure-Orbital.svg similarity index 100% rename from assets/icons/azure/other/02697-icon-service-Azure-Orbital.svg rename to core/assets/icons/azure/other/02697-icon-service-Azure-Orbital.svg diff --git a/assets/icons/azure/other/02732-icon-service-Capacity-Reservation-Groups.png b/core/assets/icons/azure/other/02732-icon-service-Capacity-Reservation-Groups.png similarity index 100% rename from assets/icons/azure/other/02732-icon-service-Capacity-Reservation-Groups.png rename to core/assets/icons/azure/other/02732-icon-service-Capacity-Reservation-Groups.png diff --git a/assets/icons/azure/other/02732-icon-service-Capacity-Reservation-Groups.svg b/core/assets/icons/azure/other/02732-icon-service-Capacity-Reservation-Groups.svg similarity index 100% rename from assets/icons/azure/other/02732-icon-service-Capacity-Reservation-Groups.svg rename to core/assets/icons/azure/other/02732-icon-service-Capacity-Reservation-Groups.svg diff --git a/assets/icons/azure/other/02733-icon-service-Windows-Notification-Services.png b/core/assets/icons/azure/other/02733-icon-service-Windows-Notification-Services.png similarity index 100% rename from assets/icons/azure/other/02733-icon-service-Windows-Notification-Services.png rename to core/assets/icons/azure/other/02733-icon-service-Windows-Notification-Services.png diff --git a/assets/icons/azure/other/02733-icon-service-Windows-Notification-Services.svg b/core/assets/icons/azure/other/02733-icon-service-Windows-Notification-Services.svg similarity index 100% rename from assets/icons/azure/other/02733-icon-service-Windows-Notification-Services.svg rename to core/assets/icons/azure/other/02733-icon-service-Windows-Notification-Services.svg diff --git a/assets/icons/azure/other/02740-icon-service-Azure-Network-Function-Manager.png b/core/assets/icons/azure/other/02740-icon-service-Azure-Network-Function-Manager.png similarity index 100% rename from assets/icons/azure/other/02740-icon-service-Azure-Network-Function-Manager.png rename to core/assets/icons/azure/other/02740-icon-service-Azure-Network-Function-Manager.png diff --git a/assets/icons/azure/other/02740-icon-service-Azure-Network-Function-Manager.svg b/core/assets/icons/azure/other/02740-icon-service-Azure-Network-Function-Manager.svg similarity index 100% rename from assets/icons/azure/other/02740-icon-service-Azure-Network-Function-Manager.svg rename to core/assets/icons/azure/other/02740-icon-service-Azure-Network-Function-Manager.svg diff --git a/assets/icons/azure/other/02793-icon-service-Mission-Landing-Zone.png b/core/assets/icons/azure/other/02793-icon-service-Mission-Landing-Zone.png similarity index 100% rename from assets/icons/azure/other/02793-icon-service-Mission-Landing-Zone.png rename to core/assets/icons/azure/other/02793-icon-service-Mission-Landing-Zone.png diff --git a/assets/icons/azure/other/02793-icon-service-Mission-Landing-Zone.svg b/core/assets/icons/azure/other/02793-icon-service-Mission-Landing-Zone.svg similarity index 100% rename from assets/icons/azure/other/02793-icon-service-Mission-Landing-Zone.svg rename to core/assets/icons/azure/other/02793-icon-service-Mission-Landing-Zone.svg diff --git a/assets/icons/azure/other/02794-icon-service-Mobile-Networks.png b/core/assets/icons/azure/other/02794-icon-service-Mobile-Networks.png similarity index 100% rename from assets/icons/azure/other/02794-icon-service-Mobile-Networks.png rename to core/assets/icons/azure/other/02794-icon-service-Mobile-Networks.png diff --git a/assets/icons/azure/other/02794-icon-service-Mobile-Networks.svg b/core/assets/icons/azure/other/02794-icon-service-Mobile-Networks.svg similarity index 100% rename from assets/icons/azure/other/02794-icon-service-Mobile-Networks.svg rename to core/assets/icons/azure/other/02794-icon-service-Mobile-Networks.svg diff --git a/assets/icons/azure/other/02805-icon-service-VM-App-Definitions.png b/core/assets/icons/azure/other/02805-icon-service-VM-App-Definitions.png similarity index 100% rename from assets/icons/azure/other/02805-icon-service-VM-App-Definitions.png rename to core/assets/icons/azure/other/02805-icon-service-VM-App-Definitions.png diff --git a/assets/icons/azure/other/02805-icon-service-VM-App-Definitions.svg b/core/assets/icons/azure/other/02805-icon-service-VM-App-Definitions.svg similarity index 100% rename from assets/icons/azure/other/02805-icon-service-VM-App-Definitions.svg rename to core/assets/icons/azure/other/02805-icon-service-VM-App-Definitions.svg diff --git a/assets/icons/azure/other/02806-icon-service-VM-App-Versions.png b/core/assets/icons/azure/other/02806-icon-service-VM-App-Versions.png similarity index 100% rename from assets/icons/azure/other/02806-icon-service-VM-App-Versions.png rename to core/assets/icons/azure/other/02806-icon-service-VM-App-Versions.png diff --git a/assets/icons/azure/other/02806-icon-service-VM-App-Versions.svg b/core/assets/icons/azure/other/02806-icon-service-VM-App-Versions.svg similarity index 100% rename from assets/icons/azure/other/02806-icon-service-VM-App-Versions.svg rename to core/assets/icons/azure/other/02806-icon-service-VM-App-Versions.svg diff --git a/assets/icons/azure/other/02810-icon-service-Azure-Edge-Hardware-Center.png b/core/assets/icons/azure/other/02810-icon-service-Azure-Edge-Hardware-Center.png similarity index 100% rename from assets/icons/azure/other/02810-icon-service-Azure-Edge-Hardware-Center.png rename to core/assets/icons/azure/other/02810-icon-service-Azure-Edge-Hardware-Center.png diff --git a/assets/icons/azure/other/02810-icon-service-Azure-Edge-Hardware-Center.svg b/core/assets/icons/azure/other/02810-icon-service-Azure-Edge-Hardware-Center.svg similarity index 100% rename from assets/icons/azure/other/02810-icon-service-Azure-Edge-Hardware-Center.svg rename to core/assets/icons/azure/other/02810-icon-service-Azure-Edge-Hardware-Center.svg diff --git a/assets/icons/azure/other/02819-icon-service-Resource-Guard.png b/core/assets/icons/azure/other/02819-icon-service-Resource-Guard.png similarity index 100% rename from assets/icons/azure/other/02819-icon-service-Resource-Guard.png rename to core/assets/icons/azure/other/02819-icon-service-Resource-Guard.png diff --git a/assets/icons/azure/other/02819-icon-service-Resource-Guard.svg b/core/assets/icons/azure/other/02819-icon-service-Resource-Guard.svg similarity index 100% rename from assets/icons/azure/other/02819-icon-service-Resource-Guard.svg rename to core/assets/icons/azure/other/02819-icon-service-Resource-Guard.svg diff --git a/assets/icons/azure/other/02828-icon-service-Ceres.png b/core/assets/icons/azure/other/02828-icon-service-Ceres.png similarity index 100% rename from assets/icons/azure/other/02828-icon-service-Ceres.png rename to core/assets/icons/azure/other/02828-icon-service-Ceres.png diff --git a/assets/icons/azure/other/02828-icon-service-Ceres.svg b/core/assets/icons/azure/other/02828-icon-service-Ceres.svg similarity index 100% rename from assets/icons/azure/other/02828-icon-service-Ceres.svg rename to core/assets/icons/azure/other/02828-icon-service-Ceres.svg diff --git a/assets/icons/azure/other/02842-icon-service-Azurite.png b/core/assets/icons/azure/other/02842-icon-service-Azurite.png similarity index 100% rename from assets/icons/azure/other/02842-icon-service-Azurite.png rename to core/assets/icons/azure/other/02842-icon-service-Azurite.png diff --git a/assets/icons/azure/other/02842-icon-service-Azurite.svg b/core/assets/icons/azure/other/02842-icon-service-Azurite.svg similarity index 100% rename from assets/icons/azure/other/02842-icon-service-Azurite.svg rename to core/assets/icons/azure/other/02842-icon-service-Azurite.svg diff --git a/assets/icons/azure/other/02846-icon-service-Update-Management-Center.png b/core/assets/icons/azure/other/02846-icon-service-Update-Management-Center.png similarity index 100% rename from assets/icons/azure/other/02846-icon-service-Update-Management-Center.png rename to core/assets/icons/azure/other/02846-icon-service-Update-Management-Center.png diff --git a/assets/icons/azure/other/02846-icon-service-Update-Management-Center.svg b/core/assets/icons/azure/other/02846-icon-service-Update-Management-Center.svg similarity index 100% rename from assets/icons/azure/other/02846-icon-service-Update-Management-Center.svg rename to core/assets/icons/azure/other/02846-icon-service-Update-Management-Center.svg diff --git a/assets/icons/azure/other/02864-icon-service-Azure-Compute-Galleries.png b/core/assets/icons/azure/other/02864-icon-service-Azure-Compute-Galleries.png similarity index 100% rename from assets/icons/azure/other/02864-icon-service-Azure-Compute-Galleries.png rename to core/assets/icons/azure/other/02864-icon-service-Azure-Compute-Galleries.png diff --git a/assets/icons/azure/other/02864-icon-service-Azure-Compute-Galleries.svg b/core/assets/icons/azure/other/02864-icon-service-Azure-Compute-Galleries.svg similarity index 100% rename from assets/icons/azure/other/02864-icon-service-Azure-Compute-Galleries.svg rename to core/assets/icons/azure/other/02864-icon-service-Azure-Compute-Galleries.svg diff --git a/assets/icons/azure/other/02865-icon-service-Community-Images.png b/core/assets/icons/azure/other/02865-icon-service-Community-Images.png similarity index 100% rename from assets/icons/azure/other/02865-icon-service-Community-Images.png rename to core/assets/icons/azure/other/02865-icon-service-Community-Images.png diff --git a/assets/icons/azure/other/02865-icon-service-Community-Images.svg b/core/assets/icons/azure/other/02865-icon-service-Community-Images.svg similarity index 100% rename from assets/icons/azure/other/02865-icon-service-Community-Images.svg rename to core/assets/icons/azure/other/02865-icon-service-Community-Images.svg diff --git a/assets/icons/azure/other/02866-icon-service-VM-Image-Version.png b/core/assets/icons/azure/other/02866-icon-service-VM-Image-Version.png similarity index 100% rename from assets/icons/azure/other/02866-icon-service-VM-Image-Version.png rename to core/assets/icons/azure/other/02866-icon-service-VM-Image-Version.png diff --git a/assets/icons/azure/other/02866-icon-service-VM-Image-Version.svg b/core/assets/icons/azure/other/02866-icon-service-VM-Image-Version.svg similarity index 100% rename from assets/icons/azure/other/02866-icon-service-VM-Image-Version.svg rename to core/assets/icons/azure/other/02866-icon-service-VM-Image-Version.svg diff --git a/assets/icons/azure/other/02880-icon-service-Savings-Plans.png b/core/assets/icons/azure/other/02880-icon-service-Savings-Plans.png similarity index 100% rename from assets/icons/azure/other/02880-icon-service-Savings-Plans.png rename to core/assets/icons/azure/other/02880-icon-service-Savings-Plans.png diff --git a/assets/icons/azure/other/02880-icon-service-Savings-Plans.svg b/core/assets/icons/azure/other/02880-icon-service-Savings-Plans.svg similarity index 100% rename from assets/icons/azure/other/02880-icon-service-Savings-Plans.svg rename to core/assets/icons/azure/other/02880-icon-service-Savings-Plans.svg diff --git a/assets/icons/azure/other/02884-icon-service-Worker-Container-App.png b/core/assets/icons/azure/other/02884-icon-service-Worker-Container-App.png similarity index 100% rename from assets/icons/azure/other/02884-icon-service-Worker-Container-App.png rename to core/assets/icons/azure/other/02884-icon-service-Worker-Container-App.png diff --git a/assets/icons/azure/other/02884-icon-service-Worker-Container-App.svg b/core/assets/icons/azure/other/02884-icon-service-Worker-Container-App.svg similarity index 100% rename from assets/icons/azure/other/02884-icon-service-Worker-Container-App.svg rename to core/assets/icons/azure/other/02884-icon-service-Worker-Container-App.svg diff --git a/assets/icons/azure/other/02905-icon-service-Azure-Managed-Grafana.png b/core/assets/icons/azure/other/02905-icon-service-Azure-Managed-Grafana.png similarity index 100% rename from assets/icons/azure/other/02905-icon-service-Azure-Managed-Grafana.png rename to core/assets/icons/azure/other/02905-icon-service-Azure-Managed-Grafana.png diff --git a/assets/icons/azure/other/02905-icon-service-Azure-Managed-Grafana.svg b/core/assets/icons/azure/other/02905-icon-service-Azure-Managed-Grafana.svg similarity index 100% rename from assets/icons/azure/other/02905-icon-service-Azure-Managed-Grafana.svg rename to core/assets/icons/azure/other/02905-icon-service-Azure-Managed-Grafana.svg diff --git a/assets/icons/azure/other/02908-icon-service-Targets-Management.png b/core/assets/icons/azure/other/02908-icon-service-Targets-Management.png similarity index 100% rename from assets/icons/azure/other/02908-icon-service-Targets-Management.png rename to core/assets/icons/azure/other/02908-icon-service-Targets-Management.png diff --git a/assets/icons/azure/other/02908-icon-service-Targets-Management.svg b/core/assets/icons/azure/other/02908-icon-service-Targets-Management.svg similarity index 100% rename from assets/icons/azure/other/02908-icon-service-Targets-Management.svg rename to core/assets/icons/azure/other/02908-icon-service-Targets-Management.svg diff --git a/assets/icons/azure/other/02909-icon-service-Storage-Functions.png b/core/assets/icons/azure/other/02909-icon-service-Storage-Functions.png similarity index 100% rename from assets/icons/azure/other/02909-icon-service-Storage-Functions.png rename to core/assets/icons/azure/other/02909-icon-service-Storage-Functions.png diff --git a/assets/icons/azure/other/02909-icon-service-Storage-Functions.svg b/core/assets/icons/azure/other/02909-icon-service-Storage-Functions.svg similarity index 100% rename from assets/icons/azure/other/02909-icon-service-Storage-Functions.svg rename to core/assets/icons/azure/other/02909-icon-service-Storage-Functions.svg diff --git a/assets/icons/azure/other/02909-icon-service-Storage-Tasks.png b/core/assets/icons/azure/other/02909-icon-service-Storage-Tasks.png similarity index 100% rename from assets/icons/azure/other/02909-icon-service-Storage-Tasks.png rename to core/assets/icons/azure/other/02909-icon-service-Storage-Tasks.png diff --git a/assets/icons/azure/other/02909-icon-service-Storage-Tasks.svg b/core/assets/icons/azure/other/02909-icon-service-Storage-Tasks.svg similarity index 100% rename from assets/icons/azure/other/02909-icon-service-Storage-Tasks.svg rename to core/assets/icons/azure/other/02909-icon-service-Storage-Tasks.svg diff --git a/assets/icons/azure/other/02922-icon-service-Sonic-Dash.png b/core/assets/icons/azure/other/02922-icon-service-Sonic-Dash.png similarity index 100% rename from assets/icons/azure/other/02922-icon-service-Sonic-Dash.png rename to core/assets/icons/azure/other/02922-icon-service-Sonic-Dash.png diff --git a/assets/icons/azure/other/02922-icon-service-Sonic-Dash.svg b/core/assets/icons/azure/other/02922-icon-service-Sonic-Dash.svg similarity index 100% rename from assets/icons/azure/other/02922-icon-service-Sonic-Dash.svg rename to core/assets/icons/azure/other/02922-icon-service-Sonic-Dash.svg diff --git a/assets/icons/azure/other/02931-icon-service-Compliance-Center.png b/core/assets/icons/azure/other/02931-icon-service-Compliance-Center.png similarity index 100% rename from assets/icons/azure/other/02931-icon-service-Compliance-Center.png rename to core/assets/icons/azure/other/02931-icon-service-Compliance-Center.png diff --git a/assets/icons/azure/other/02931-icon-service-Compliance-Center.svg b/core/assets/icons/azure/other/02931-icon-service-Compliance-Center.svg similarity index 100% rename from assets/icons/azure/other/02931-icon-service-Compliance-Center.svg rename to core/assets/icons/azure/other/02931-icon-service-Compliance-Center.svg diff --git a/assets/icons/azure/other/02939-icon-service-Network-Security-Perimeters.png b/core/assets/icons/azure/other/02939-icon-service-Network-Security-Perimeters.png similarity index 100% rename from assets/icons/azure/other/02939-icon-service-Network-Security-Perimeters.png rename to core/assets/icons/azure/other/02939-icon-service-Network-Security-Perimeters.png diff --git a/assets/icons/azure/other/02939-icon-service-Network-Security-Perimeters.svg b/core/assets/icons/azure/other/02939-icon-service-Network-Security-Perimeters.svg similarity index 100% rename from assets/icons/azure/other/02939-icon-service-Network-Security-Perimeters.svg rename to core/assets/icons/azure/other/02939-icon-service-Network-Security-Perimeters.svg diff --git a/assets/icons/azure/other/02944-icon-service-Azure-Load-Testing.png b/core/assets/icons/azure/other/02944-icon-service-Azure-Load-Testing.png similarity index 100% rename from assets/icons/azure/other/02944-icon-service-Azure-Load-Testing.png rename to core/assets/icons/azure/other/02944-icon-service-Azure-Load-Testing.png diff --git a/assets/icons/azure/other/02944-icon-service-Azure-Load-Testing.svg b/core/assets/icons/azure/other/02944-icon-service-Azure-Load-Testing.svg similarity index 100% rename from assets/icons/azure/other/02944-icon-service-Azure-Load-Testing.svg rename to core/assets/icons/azure/other/02944-icon-service-Azure-Load-Testing.svg diff --git a/assets/icons/azure/other/02949-icon-service-Virtual-Visits-Builder.png b/core/assets/icons/azure/other/02949-icon-service-Virtual-Visits-Builder.png similarity index 100% rename from assets/icons/azure/other/02949-icon-service-Virtual-Visits-Builder.png rename to core/assets/icons/azure/other/02949-icon-service-Virtual-Visits-Builder.png diff --git a/assets/icons/azure/other/02949-icon-service-Virtual-Visits-Builder.svg b/core/assets/icons/azure/other/02949-icon-service-Virtual-Visits-Builder.svg similarity index 100% rename from assets/icons/azure/other/02949-icon-service-Virtual-Visits-Builder.svg rename to core/assets/icons/azure/other/02949-icon-service-Virtual-Visits-Builder.svg diff --git a/assets/icons/azure/other/02951-icon-service-Azure-Quotas.png b/core/assets/icons/azure/other/02951-icon-service-Azure-Quotas.png similarity index 100% rename from assets/icons/azure/other/02951-icon-service-Azure-Quotas.png rename to core/assets/icons/azure/other/02951-icon-service-Azure-Quotas.png diff --git a/assets/icons/azure/other/02951-icon-service-Azure-Quotas.svg b/core/assets/icons/azure/other/02951-icon-service-Azure-Quotas.svg similarity index 100% rename from assets/icons/azure/other/02951-icon-service-Azure-Quotas.svg rename to core/assets/icons/azure/other/02951-icon-service-Azure-Quotas.svg diff --git a/assets/icons/azure/other/02989-icon-service-Container-Apps-Environments.png b/core/assets/icons/azure/other/02989-icon-service-Container-Apps-Environments.png similarity index 100% rename from assets/icons/azure/other/02989-icon-service-Container-Apps-Environments.png rename to core/assets/icons/azure/other/02989-icon-service-Container-Apps-Environments.png diff --git a/assets/icons/azure/other/02989-icon-service-Container-Apps-Environments.svg b/core/assets/icons/azure/other/02989-icon-service-Container-Apps-Environments.svg similarity index 100% rename from assets/icons/azure/other/02989-icon-service-Container-Apps-Environments.svg rename to core/assets/icons/azure/other/02989-icon-service-Container-Apps-Environments.svg diff --git a/assets/icons/azure/other/03071-icon-service-App-Compliance-Automation.png b/core/assets/icons/azure/other/03071-icon-service-App-Compliance-Automation.png similarity index 100% rename from assets/icons/azure/other/03071-icon-service-App-Compliance-Automation.png rename to core/assets/icons/azure/other/03071-icon-service-App-Compliance-Automation.png diff --git a/assets/icons/azure/other/03071-icon-service-App-Compliance-Automation.svg b/core/assets/icons/azure/other/03071-icon-service-App-Compliance-Automation.svg similarity index 100% rename from assets/icons/azure/other/03071-icon-service-App-Compliance-Automation.svg rename to core/assets/icons/azure/other/03071-icon-service-App-Compliance-Automation.svg diff --git a/assets/icons/azure/other/03089-icon-service-Virtual-Instance-for-SAP.png b/core/assets/icons/azure/other/03089-icon-service-Virtual-Instance-for-SAP.png similarity index 100% rename from assets/icons/azure/other/03089-icon-service-Virtual-Instance-for-SAP.png rename to core/assets/icons/azure/other/03089-icon-service-Virtual-Instance-for-SAP.png diff --git a/assets/icons/azure/other/03089-icon-service-Virtual-Instance-for-SAP.svg b/core/assets/icons/azure/other/03089-icon-service-Virtual-Instance-for-SAP.svg similarity index 100% rename from assets/icons/azure/other/03089-icon-service-Virtual-Instance-for-SAP.svg rename to core/assets/icons/azure/other/03089-icon-service-Virtual-Instance-for-SAP.svg diff --git a/assets/icons/azure/other/03090-icon-service-Azure-Center-for-SAP.png b/core/assets/icons/azure/other/03090-icon-service-Azure-Center-for-SAP.png similarity index 100% rename from assets/icons/azure/other/03090-icon-service-Azure-Center-for-SAP.png rename to core/assets/icons/azure/other/03090-icon-service-Azure-Center-for-SAP.png diff --git a/assets/icons/azure/other/03090-icon-service-Azure-Center-for-SAP.svg b/core/assets/icons/azure/other/03090-icon-service-Azure-Center-for-SAP.svg similarity index 100% rename from assets/icons/azure/other/03090-icon-service-Azure-Center-for-SAP.svg rename to core/assets/icons/azure/other/03090-icon-service-Azure-Center-for-SAP.svg diff --git a/assets/icons/azure/other/03091-icon-service-Azure-Storage-Mover.png b/core/assets/icons/azure/other/03091-icon-service-Azure-Storage-Mover.png similarity index 100% rename from assets/icons/azure/other/03091-icon-service-Azure-Storage-Mover.png rename to core/assets/icons/azure/other/03091-icon-service-Azure-Storage-Mover.png diff --git a/assets/icons/azure/other/03091-icon-service-Azure-Storage-Mover.svg b/core/assets/icons/azure/other/03091-icon-service-Azure-Storage-Mover.svg similarity index 100% rename from assets/icons/azure/other/03091-icon-service-Azure-Storage-Mover.svg rename to core/assets/icons/azure/other/03091-icon-service-Azure-Storage-Mover.svg diff --git a/assets/icons/azure/other/03092-icon-service-Central-Service-Instance-For-SAP.png b/core/assets/icons/azure/other/03092-icon-service-Central-Service-Instance-For-SAP.png similarity index 100% rename from assets/icons/azure/other/03092-icon-service-Central-Service-Instance-For-SAP.png rename to core/assets/icons/azure/other/03092-icon-service-Central-Service-Instance-For-SAP.png diff --git a/assets/icons/azure/other/03092-icon-service-Central-Service-Instance-For-SAP.svg b/core/assets/icons/azure/other/03092-icon-service-Central-Service-Instance-For-SAP.svg similarity index 100% rename from assets/icons/azure/other/03092-icon-service-Central-Service-Instance-For-SAP.svg rename to core/assets/icons/azure/other/03092-icon-service-Central-Service-Instance-For-SAP.svg diff --git a/assets/icons/azure/other/03134-icon-service-Kubernetes-Fleet-Manager.png b/core/assets/icons/azure/other/03134-icon-service-Kubernetes-Fleet-Manager.png similarity index 100% rename from assets/icons/azure/other/03134-icon-service-Kubernetes-Fleet-Manager.png rename to core/assets/icons/azure/other/03134-icon-service-Kubernetes-Fleet-Manager.png diff --git a/assets/icons/azure/other/03134-icon-service-Kubernetes-Fleet-Manager.svg b/core/assets/icons/azure/other/03134-icon-service-Kubernetes-Fleet-Manager.svg similarity index 100% rename from assets/icons/azure/other/03134-icon-service-Kubernetes-Fleet-Manager.svg rename to core/assets/icons/azure/other/03134-icon-service-Kubernetes-Fleet-Manager.svg diff --git a/assets/icons/azure/other/03137-icon-service-Express-Route-Traffic-Collector.png b/core/assets/icons/azure/other/03137-icon-service-Express-Route-Traffic-Collector.png similarity index 100% rename from assets/icons/azure/other/03137-icon-service-Express-Route-Traffic-Collector.png rename to core/assets/icons/azure/other/03137-icon-service-Express-Route-Traffic-Collector.png diff --git a/assets/icons/azure/other/03137-icon-service-Express-Route-Traffic-Collector.svg b/core/assets/icons/azure/other/03137-icon-service-Express-Route-Traffic-Collector.svg similarity index 100% rename from assets/icons/azure/other/03137-icon-service-Express-Route-Traffic-Collector.svg rename to core/assets/icons/azure/other/03137-icon-service-Express-Route-Traffic-Collector.svg diff --git a/assets/icons/azure/other/03175-icon-service-Database-Instance-For-SAP.png b/core/assets/icons/azure/other/03175-icon-service-Database-Instance-For-SAP.png similarity index 100% rename from assets/icons/azure/other/03175-icon-service-Database-Instance-For-SAP.png rename to core/assets/icons/azure/other/03175-icon-service-Database-Instance-For-SAP.png diff --git a/assets/icons/azure/other/03175-icon-service-Database-Instance-For-SAP.svg b/core/assets/icons/azure/other/03175-icon-service-Database-Instance-For-SAP.svg similarity index 100% rename from assets/icons/azure/other/03175-icon-service-Database-Instance-For-SAP.svg rename to core/assets/icons/azure/other/03175-icon-service-Database-Instance-For-SAP.svg diff --git a/assets/icons/azure/other/03190-icon-service-Elastic-SAN.png b/core/assets/icons/azure/other/03190-icon-service-Elastic-SAN.png similarity index 100% rename from assets/icons/azure/other/03190-icon-service-Elastic-SAN.png rename to core/assets/icons/azure/other/03190-icon-service-Elastic-SAN.png diff --git a/assets/icons/azure/other/03190-icon-service-Elastic-SAN.svg b/core/assets/icons/azure/other/03190-icon-service-Elastic-SAN.svg similarity index 100% rename from assets/icons/azure/other/03190-icon-service-Elastic-SAN.svg rename to core/assets/icons/azure/other/03190-icon-service-Elastic-SAN.svg diff --git a/assets/icons/azure/other/03250-icon-service-Microsoft-Dev-Box.png b/core/assets/icons/azure/other/03250-icon-service-Microsoft-Dev-Box.png similarity index 100% rename from assets/icons/azure/other/03250-icon-service-Microsoft-Dev-Box.png rename to core/assets/icons/azure/other/03250-icon-service-Microsoft-Dev-Box.png diff --git a/assets/icons/azure/other/03250-icon-service-Microsoft-Dev-Box.svg b/core/assets/icons/azure/other/03250-icon-service-Microsoft-Dev-Box.svg similarity index 100% rename from assets/icons/azure/other/03250-icon-service-Microsoft-Dev-Box.svg rename to core/assets/icons/azure/other/03250-icon-service-Microsoft-Dev-Box.svg diff --git a/assets/icons/azure/other/03251-icon-service-Azure-Deployment-Environments.png b/core/assets/icons/azure/other/03251-icon-service-Azure-Deployment-Environments.png similarity index 100% rename from assets/icons/azure/other/03251-icon-service-Azure-Deployment-Environments.png rename to core/assets/icons/azure/other/03251-icon-service-Azure-Deployment-Environments.png diff --git a/assets/icons/azure/other/03251-icon-service-Azure-Deployment-Environments.svg b/core/assets/icons/azure/other/03251-icon-service-Azure-Deployment-Environments.svg similarity index 100% rename from assets/icons/azure/other/03251-icon-service-Azure-Deployment-Environments.svg rename to core/assets/icons/azure/other/03251-icon-service-Azure-Deployment-Environments.svg diff --git a/assets/icons/azure/other/032869111-icon-service-Microsoft-Discovery.png b/core/assets/icons/azure/other/032869111-icon-service-Microsoft-Discovery.png similarity index 100% rename from assets/icons/azure/other/032869111-icon-service-Microsoft-Discovery.png rename to core/assets/icons/azure/other/032869111-icon-service-Microsoft-Discovery.png diff --git a/assets/icons/azure/other/032869111-icon-service-Microsoft-Discovery.svg b/core/assets/icons/azure/other/032869111-icon-service-Microsoft-Discovery.svg similarity index 100% rename from assets/icons/azure/other/032869111-icon-service-Microsoft-Discovery.svg rename to core/assets/icons/azure/other/032869111-icon-service-Microsoft-Discovery.svg diff --git a/assets/icons/azure/other/03308-icon-service-Azure-Dev-Tunnels.png b/core/assets/icons/azure/other/03308-icon-service-Azure-Dev-Tunnels.png similarity index 100% rename from assets/icons/azure/other/03308-icon-service-Azure-Dev-Tunnels.png rename to core/assets/icons/azure/other/03308-icon-service-Azure-Dev-Tunnels.png diff --git a/assets/icons/azure/other/03308-icon-service-Azure-Dev-Tunnels.svg b/core/assets/icons/azure/other/03308-icon-service-Azure-Dev-Tunnels.svg similarity index 100% rename from assets/icons/azure/other/03308-icon-service-Azure-Dev-Tunnels.svg rename to core/assets/icons/azure/other/03308-icon-service-Azure-Dev-Tunnels.svg diff --git a/assets/icons/azure/other/033112809-icon-service-promethus.png b/core/assets/icons/azure/other/033112809-icon-service-promethus.png similarity index 100% rename from assets/icons/azure/other/033112809-icon-service-promethus.png rename to core/assets/icons/azure/other/033112809-icon-service-promethus.png diff --git a/assets/icons/azure/other/033112809-icon-service-promethus.svg b/core/assets/icons/azure/other/033112809-icon-service-promethus.svg similarity index 100% rename from assets/icons/azure/other/033112809-icon-service-promethus.svg rename to core/assets/icons/azure/other/033112809-icon-service-promethus.svg diff --git a/assets/icons/azure/other/03314-icon-service-Azure-Sustainability.png b/core/assets/icons/azure/other/03314-icon-service-Azure-Sustainability.png similarity index 100% rename from assets/icons/azure/other/03314-icon-service-Azure-Sustainability.png rename to core/assets/icons/azure/other/03314-icon-service-Azure-Sustainability.png diff --git a/assets/icons/azure/other/03314-icon-service-Azure-Sustainability.svg b/core/assets/icons/azure/other/03314-icon-service-Azure-Sustainability.svg similarity index 100% rename from assets/icons/azure/other/03314-icon-service-Azure-Sustainability.svg rename to core/assets/icons/azure/other/03314-icon-service-Azure-Sustainability.svg diff --git a/assets/icons/azure/other/03361-icon-service-IcM-Troubleshooting.png b/core/assets/icons/azure/other/03361-icon-service-IcM-Troubleshooting.png similarity index 100% rename from assets/icons/azure/other/03361-icon-service-IcM-Troubleshooting.png rename to core/assets/icons/azure/other/03361-icon-service-IcM-Troubleshooting.png diff --git a/assets/icons/azure/other/03361-icon-service-IcM-Troubleshooting.svg b/core/assets/icons/azure/other/03361-icon-service-IcM-Troubleshooting.svg similarity index 100% rename from assets/icons/azure/other/03361-icon-service-IcM-Troubleshooting.svg rename to core/assets/icons/azure/other/03361-icon-service-IcM-Troubleshooting.svg diff --git a/assets/icons/azure/other/03365-icon-service-OSConfig.png b/core/assets/icons/azure/other/03365-icon-service-OSConfig.png similarity index 100% rename from assets/icons/azure/other/03365-icon-service-OSConfig.png rename to core/assets/icons/azure/other/03365-icon-service-OSConfig.png diff --git a/assets/icons/azure/other/03365-icon-service-OSConfig.svg b/core/assets/icons/azure/other/03365-icon-service-OSConfig.svg similarity index 100% rename from assets/icons/azure/other/03365-icon-service-OSConfig.svg rename to core/assets/icons/azure/other/03365-icon-service-OSConfig.svg diff --git a/assets/icons/azure/other/03394-icon-service-Azure-Enclaves.png b/core/assets/icons/azure/other/03394-icon-service-Azure-Enclaves.png similarity index 100% rename from assets/icons/azure/other/03394-icon-service-Azure-Enclaves.png rename to core/assets/icons/azure/other/03394-icon-service-Azure-Enclaves.png diff --git a/assets/icons/azure/other/03394-icon-service-Azure-Enclaves.svg b/core/assets/icons/azure/other/03394-icon-service-Azure-Enclaves.svg similarity index 100% rename from assets/icons/azure/other/03394-icon-service-Azure-Enclaves.svg rename to core/assets/icons/azure/other/03394-icon-service-Azure-Enclaves.svg diff --git a/assets/icons/azure/other/03399-icon-service-AKS-Istio.png b/core/assets/icons/azure/other/03399-icon-service-AKS-Istio.png similarity index 100% rename from assets/icons/azure/other/03399-icon-service-AKS-Istio.png rename to core/assets/icons/azure/other/03399-icon-service-AKS-Istio.png diff --git a/assets/icons/azure/other/03399-icon-service-AKS-Istio.svg b/core/assets/icons/azure/other/03399-icon-service-AKS-Istio.svg similarity index 100% rename from assets/icons/azure/other/03399-icon-service-AKS-Istio.svg rename to core/assets/icons/azure/other/03399-icon-service-AKS-Istio.svg diff --git a/assets/icons/azure/other/03402-icon-service-Defender-CM-Local-Manager.png b/core/assets/icons/azure/other/03402-icon-service-Defender-CM-Local-Manager.png similarity index 100% rename from assets/icons/azure/other/03402-icon-service-Defender-CM-Local-Manager.png rename to core/assets/icons/azure/other/03402-icon-service-Defender-CM-Local-Manager.png diff --git a/assets/icons/azure/other/03402-icon-service-Defender-CM-Local-Manager.svg b/core/assets/icons/azure/other/03402-icon-service-Defender-CM-Local-Manager.svg similarity index 100% rename from assets/icons/azure/other/03402-icon-service-Defender-CM-Local-Manager.svg rename to core/assets/icons/azure/other/03402-icon-service-Defender-CM-Local-Manager.svg diff --git a/assets/icons/azure/other/03403-icon-service-Defender-External-Management.png b/core/assets/icons/azure/other/03403-icon-service-Defender-External-Management.png similarity index 100% rename from assets/icons/azure/other/03403-icon-service-Defender-External-Management.png rename to core/assets/icons/azure/other/03403-icon-service-Defender-External-Management.png diff --git a/assets/icons/azure/other/03403-icon-service-Defender-External-Management.svg b/core/assets/icons/azure/other/03403-icon-service-Defender-External-Management.svg similarity index 100% rename from assets/icons/azure/other/03403-icon-service-Defender-External-Management.svg rename to core/assets/icons/azure/other/03403-icon-service-Defender-External-Management.svg diff --git a/assets/icons/azure/other/03404-icon-service-Defender-Freezer-Monitor.png b/core/assets/icons/azure/other/03404-icon-service-Defender-Freezer-Monitor.png similarity index 100% rename from assets/icons/azure/other/03404-icon-service-Defender-Freezer-Monitor.png rename to core/assets/icons/azure/other/03404-icon-service-Defender-Freezer-Monitor.png diff --git a/assets/icons/azure/other/03404-icon-service-Defender-Freezer-Monitor.svg b/core/assets/icons/azure/other/03404-icon-service-Defender-Freezer-Monitor.svg similarity index 100% rename from assets/icons/azure/other/03404-icon-service-Defender-Freezer-Monitor.svg rename to core/assets/icons/azure/other/03404-icon-service-Defender-Freezer-Monitor.svg diff --git a/assets/icons/azure/other/03405-icon-service-Defender-Historian.png b/core/assets/icons/azure/other/03405-icon-service-Defender-Historian.png similarity index 100% rename from assets/icons/azure/other/03405-icon-service-Defender-Historian.png rename to core/assets/icons/azure/other/03405-icon-service-Defender-Historian.png diff --git a/assets/icons/azure/other/03405-icon-service-Defender-Historian.svg b/core/assets/icons/azure/other/03405-icon-service-Defender-Historian.svg similarity index 100% rename from assets/icons/azure/other/03405-icon-service-Defender-Historian.svg rename to core/assets/icons/azure/other/03405-icon-service-Defender-Historian.svg diff --git a/assets/icons/azure/other/03406-icon-service-Defender-HMI.png b/core/assets/icons/azure/other/03406-icon-service-Defender-HMI.png similarity index 100% rename from assets/icons/azure/other/03406-icon-service-Defender-HMI.png rename to core/assets/icons/azure/other/03406-icon-service-Defender-HMI.png diff --git a/assets/icons/azure/other/03406-icon-service-Defender-HMI.svg b/core/assets/icons/azure/other/03406-icon-service-Defender-HMI.svg similarity index 100% rename from assets/icons/azure/other/03406-icon-service-Defender-HMI.svg rename to core/assets/icons/azure/other/03406-icon-service-Defender-HMI.svg diff --git a/assets/icons/azure/other/03407-icon-service-Defender-Marquee.png b/core/assets/icons/azure/other/03407-icon-service-Defender-Marquee.png similarity index 100% rename from assets/icons/azure/other/03407-icon-service-Defender-Marquee.png rename to core/assets/icons/azure/other/03407-icon-service-Defender-Marquee.png diff --git a/assets/icons/azure/other/03407-icon-service-Defender-Marquee.svg b/core/assets/icons/azure/other/03407-icon-service-Defender-Marquee.svg similarity index 100% rename from assets/icons/azure/other/03407-icon-service-Defender-Marquee.svg rename to core/assets/icons/azure/other/03407-icon-service-Defender-Marquee.svg diff --git a/assets/icons/azure/other/03408-icon-service-Defender-Robot-Controller.png b/core/assets/icons/azure/other/03408-icon-service-Defender-Robot-Controller.png similarity index 100% rename from assets/icons/azure/other/03408-icon-service-Defender-Robot-Controller.png rename to core/assets/icons/azure/other/03408-icon-service-Defender-Robot-Controller.png diff --git a/assets/icons/azure/other/03408-icon-service-Defender-Robot-Controller.svg b/core/assets/icons/azure/other/03408-icon-service-Defender-Robot-Controller.svg similarity index 100% rename from assets/icons/azure/other/03408-icon-service-Defender-Robot-Controller.svg rename to core/assets/icons/azure/other/03408-icon-service-Defender-Robot-Controller.svg diff --git a/assets/icons/azure/other/03409-icon-service-Defender-Sensor.png b/core/assets/icons/azure/other/03409-icon-service-Defender-Sensor.png similarity index 100% rename from assets/icons/azure/other/03409-icon-service-Defender-Sensor.png rename to core/assets/icons/azure/other/03409-icon-service-Defender-Sensor.png diff --git a/assets/icons/azure/other/03409-icon-service-Defender-Sensor.svg b/core/assets/icons/azure/other/03409-icon-service-Defender-Sensor.svg similarity index 100% rename from assets/icons/azure/other/03409-icon-service-Defender-Sensor.svg rename to core/assets/icons/azure/other/03409-icon-service-Defender-Sensor.svg diff --git a/assets/icons/azure/other/03410-icon-service-Defender-Slot.png b/core/assets/icons/azure/other/03410-icon-service-Defender-Slot.png similarity index 100% rename from assets/icons/azure/other/03410-icon-service-Defender-Slot.png rename to core/assets/icons/azure/other/03410-icon-service-Defender-Slot.png diff --git a/assets/icons/azure/other/03410-icon-service-Defender-Slot.svg b/core/assets/icons/azure/other/03410-icon-service-Defender-Slot.svg similarity index 100% rename from assets/icons/azure/other/03410-icon-service-Defender-Slot.svg rename to core/assets/icons/azure/other/03410-icon-service-Defender-Slot.svg diff --git a/assets/icons/azure/other/03411-icon-service-Defender-Web-Guiding-System.png b/core/assets/icons/azure/other/03411-icon-service-Defender-Web-Guiding-System.png similarity index 100% rename from assets/icons/azure/other/03411-icon-service-Defender-Web-Guiding-System.png rename to core/assets/icons/azure/other/03411-icon-service-Defender-Web-Guiding-System.png diff --git a/assets/icons/azure/other/03411-icon-service-Defender-Web-Guiding-System.svg b/core/assets/icons/azure/other/03411-icon-service-Defender-Web-Guiding-System.svg similarity index 100% rename from assets/icons/azure/other/03411-icon-service-Defender-Web-Guiding-System.svg rename to core/assets/icons/azure/other/03411-icon-service-Defender-Web-Guiding-System.svg diff --git a/assets/icons/azure/other/03412-icon-service-Defender-DCS-Controller.png b/core/assets/icons/azure/other/03412-icon-service-Defender-DCS-Controller.png similarity index 100% rename from assets/icons/azure/other/03412-icon-service-Defender-DCS-Controller.png rename to core/assets/icons/azure/other/03412-icon-service-Defender-DCS-Controller.png diff --git a/assets/icons/azure/other/03412-icon-service-Defender-DCS-Controller.svg b/core/assets/icons/azure/other/03412-icon-service-Defender-DCS-Controller.svg similarity index 100% rename from assets/icons/azure/other/03412-icon-service-Defender-DCS-Controller.svg rename to core/assets/icons/azure/other/03412-icon-service-Defender-DCS-Controller.svg diff --git a/assets/icons/azure/other/03413-icon-service-Defender-Distributer-Control-System.png b/core/assets/icons/azure/other/03413-icon-service-Defender-Distributer-Control-System.png similarity index 100% rename from assets/icons/azure/other/03413-icon-service-Defender-Distributer-Control-System.png rename to core/assets/icons/azure/other/03413-icon-service-Defender-Distributer-Control-System.png diff --git a/assets/icons/azure/other/03413-icon-service-Defender-Distributer-Control-System.svg b/core/assets/icons/azure/other/03413-icon-service-Defender-Distributer-Control-System.svg similarity index 100% rename from assets/icons/azure/other/03413-icon-service-Defender-Distributer-Control-System.svg rename to core/assets/icons/azure/other/03413-icon-service-Defender-Distributer-Control-System.svg diff --git a/assets/icons/azure/other/03414-icon-service-Defender-Engineering-Station.png b/core/assets/icons/azure/other/03414-icon-service-Defender-Engineering-Station.png similarity index 100% rename from assets/icons/azure/other/03414-icon-service-Defender-Engineering-Station.png rename to core/assets/icons/azure/other/03414-icon-service-Defender-Engineering-Station.png diff --git a/assets/icons/azure/other/03414-icon-service-Defender-Engineering-Station.svg b/core/assets/icons/azure/other/03414-icon-service-Defender-Engineering-Station.svg similarity index 100% rename from assets/icons/azure/other/03414-icon-service-Defender-Engineering-Station.svg rename to core/assets/icons/azure/other/03414-icon-service-Defender-Engineering-Station.svg diff --git a/assets/icons/azure/other/03415-icon-service-Defender-Industrial-Packaging-System.png b/core/assets/icons/azure/other/03415-icon-service-Defender-Industrial-Packaging-System.png similarity index 100% rename from assets/icons/azure/other/03415-icon-service-Defender-Industrial-Packaging-System.png rename to core/assets/icons/azure/other/03415-icon-service-Defender-Industrial-Packaging-System.png diff --git a/assets/icons/azure/other/03415-icon-service-Defender-Industrial-Packaging-System.svg b/core/assets/icons/azure/other/03415-icon-service-Defender-Industrial-Packaging-System.svg similarity index 100% rename from assets/icons/azure/other/03415-icon-service-Defender-Industrial-Packaging-System.svg rename to core/assets/icons/azure/other/03415-icon-service-Defender-Industrial-Packaging-System.svg diff --git a/assets/icons/azure/other/03416-icon-service-Defender-Industrial-Printer.png b/core/assets/icons/azure/other/03416-icon-service-Defender-Industrial-Printer.png similarity index 100% rename from assets/icons/azure/other/03416-icon-service-Defender-Industrial-Printer.png rename to core/assets/icons/azure/other/03416-icon-service-Defender-Industrial-Printer.png diff --git a/assets/icons/azure/other/03416-icon-service-Defender-Industrial-Printer.svg b/core/assets/icons/azure/other/03416-icon-service-Defender-Industrial-Printer.svg similarity index 100% rename from assets/icons/azure/other/03416-icon-service-Defender-Industrial-Printer.svg rename to core/assets/icons/azure/other/03416-icon-service-Defender-Industrial-Printer.svg diff --git a/assets/icons/azure/other/03417-icon-service-Defender-Industrial-Scale-System.png b/core/assets/icons/azure/other/03417-icon-service-Defender-Industrial-Scale-System.png similarity index 100% rename from assets/icons/azure/other/03417-icon-service-Defender-Industrial-Scale-System.png rename to core/assets/icons/azure/other/03417-icon-service-Defender-Industrial-Scale-System.png diff --git a/assets/icons/azure/other/03417-icon-service-Defender-Industrial-Scale-System.svg b/core/assets/icons/azure/other/03417-icon-service-Defender-Industrial-Scale-System.svg similarity index 100% rename from assets/icons/azure/other/03417-icon-service-Defender-Industrial-Scale-System.svg rename to core/assets/icons/azure/other/03417-icon-service-Defender-Industrial-Scale-System.svg diff --git a/assets/icons/azure/other/03418-icon-service-Defender-Industrial-Robot.png b/core/assets/icons/azure/other/03418-icon-service-Defender-Industrial-Robot.png similarity index 100% rename from assets/icons/azure/other/03418-icon-service-Defender-Industrial-Robot.png rename to core/assets/icons/azure/other/03418-icon-service-Defender-Industrial-Robot.png diff --git a/assets/icons/azure/other/03418-icon-service-Defender-Industrial-Robot.svg b/core/assets/icons/azure/other/03418-icon-service-Defender-Industrial-Robot.svg similarity index 100% rename from assets/icons/azure/other/03418-icon-service-Defender-Industrial-Robot.svg rename to core/assets/icons/azure/other/03418-icon-service-Defender-Industrial-Robot.svg diff --git a/assets/icons/azure/other/03419-icon-service-Defender-Meter.png b/core/assets/icons/azure/other/03419-icon-service-Defender-Meter.png similarity index 100% rename from assets/icons/azure/other/03419-icon-service-Defender-Meter.png rename to core/assets/icons/azure/other/03419-icon-service-Defender-Meter.png diff --git a/assets/icons/azure/other/03419-icon-service-Defender-Meter.svg b/core/assets/icons/azure/other/03419-icon-service-Defender-Meter.svg similarity index 100% rename from assets/icons/azure/other/03419-icon-service-Defender-Meter.svg rename to core/assets/icons/azure/other/03419-icon-service-Defender-Meter.svg diff --git a/assets/icons/azure/other/03420-icon-service-Defender-PLC.png b/core/assets/icons/azure/other/03420-icon-service-Defender-PLC.png similarity index 100% rename from assets/icons/azure/other/03420-icon-service-Defender-PLC.png rename to core/assets/icons/azure/other/03420-icon-service-Defender-PLC.png diff --git a/assets/icons/azure/other/03420-icon-service-Defender-PLC.svg b/core/assets/icons/azure/other/03420-icon-service-Defender-PLC.svg similarity index 100% rename from assets/icons/azure/other/03420-icon-service-Defender-PLC.svg rename to core/assets/icons/azure/other/03420-icon-service-Defender-PLC.svg diff --git a/assets/icons/azure/other/03421-icon-service-Defender-Pneumatic-Device.png b/core/assets/icons/azure/other/03421-icon-service-Defender-Pneumatic-Device.png similarity index 100% rename from assets/icons/azure/other/03421-icon-service-Defender-Pneumatic-Device.png rename to core/assets/icons/azure/other/03421-icon-service-Defender-Pneumatic-Device.png diff --git a/assets/icons/azure/other/03421-icon-service-Defender-Pneumatic-Device.svg b/core/assets/icons/azure/other/03421-icon-service-Defender-Pneumatic-Device.svg similarity index 100% rename from assets/icons/azure/other/03421-icon-service-Defender-Pneumatic-Device.svg rename to core/assets/icons/azure/other/03421-icon-service-Defender-Pneumatic-Device.svg diff --git a/assets/icons/azure/other/03422-icon-service-Defender-Programable-Board.png b/core/assets/icons/azure/other/03422-icon-service-Defender-Programable-Board.png similarity index 100% rename from assets/icons/azure/other/03422-icon-service-Defender-Programable-Board.png rename to core/assets/icons/azure/other/03422-icon-service-Defender-Programable-Board.png diff --git a/assets/icons/azure/other/03422-icon-service-Defender-Programable-Board.svg b/core/assets/icons/azure/other/03422-icon-service-Defender-Programable-Board.svg similarity index 100% rename from assets/icons/azure/other/03422-icon-service-Defender-Programable-Board.svg rename to core/assets/icons/azure/other/03422-icon-service-Defender-Programable-Board.svg diff --git a/assets/icons/azure/other/03423-icon-service-Defender-Relay.png b/core/assets/icons/azure/other/03423-icon-service-Defender-Relay.png similarity index 100% rename from assets/icons/azure/other/03423-icon-service-Defender-Relay.png rename to core/assets/icons/azure/other/03423-icon-service-Defender-Relay.png diff --git a/assets/icons/azure/other/03423-icon-service-Defender-Relay.svg b/core/assets/icons/azure/other/03423-icon-service-Defender-Relay.svg similarity index 100% rename from assets/icons/azure/other/03423-icon-service-Defender-Relay.svg rename to core/assets/icons/azure/other/03423-icon-service-Defender-Relay.svg diff --git a/assets/icons/azure/other/03424-icon-service-Defender-RTU.png b/core/assets/icons/azure/other/03424-icon-service-Defender-RTU.png similarity index 100% rename from assets/icons/azure/other/03424-icon-service-Defender-RTU.png rename to core/assets/icons/azure/other/03424-icon-service-Defender-RTU.png diff --git a/assets/icons/azure/other/03424-icon-service-Defender-RTU.svg b/core/assets/icons/azure/other/03424-icon-service-Defender-RTU.svg similarity index 100% rename from assets/icons/azure/other/03424-icon-service-Defender-RTU.svg rename to core/assets/icons/azure/other/03424-icon-service-Defender-RTU.svg diff --git a/assets/icons/azure/other/03468-icon-service-HDI-AKS-Cluster.png b/core/assets/icons/azure/other/03468-icon-service-HDI-AKS-Cluster.png similarity index 100% rename from assets/icons/azure/other/03468-icon-service-HDI-AKS-Cluster.png rename to core/assets/icons/azure/other/03468-icon-service-HDI-AKS-Cluster.png diff --git a/assets/icons/azure/other/03468-icon-service-HDI-AKS-Cluster.svg b/core/assets/icons/azure/other/03468-icon-service-HDI-AKS-Cluster.svg similarity index 100% rename from assets/icons/azure/other/03468-icon-service-HDI-AKS-Cluster.svg rename to core/assets/icons/azure/other/03468-icon-service-HDI-AKS-Cluster.svg diff --git a/assets/icons/azure/other/03495-icon-service-Azure-Consumption-Commitment.png b/core/assets/icons/azure/other/03495-icon-service-Azure-Consumption-Commitment.png similarity index 100% rename from assets/icons/azure/other/03495-icon-service-Azure-Consumption-Commitment.png rename to core/assets/icons/azure/other/03495-icon-service-Azure-Consumption-Commitment.png diff --git a/assets/icons/azure/other/03495-icon-service-Azure-Consumption-Commitment.svg b/core/assets/icons/azure/other/03495-icon-service-Azure-Consumption-Commitment.svg similarity index 100% rename from assets/icons/azure/other/03495-icon-service-Azure-Consumption-Commitment.svg rename to core/assets/icons/azure/other/03495-icon-service-Azure-Consumption-Commitment.svg diff --git a/assets/icons/azure/other/03528-icon-service-Monitor-Health-Models.png b/core/assets/icons/azure/other/03528-icon-service-Monitor-Health-Models.png similarity index 100% rename from assets/icons/azure/other/03528-icon-service-Monitor-Health-Models.png rename to core/assets/icons/azure/other/03528-icon-service-Monitor-Health-Models.png diff --git a/assets/icons/azure/other/03528-icon-service-Monitor-Health-Models.svg b/core/assets/icons/azure/other/03528-icon-service-Monitor-Health-Models.svg similarity index 100% rename from assets/icons/azure/other/03528-icon-service-Monitor-Health-Models.svg rename to core/assets/icons/azure/other/03528-icon-service-Monitor-Health-Models.svg diff --git a/assets/icons/azure/other/03563-icon-service-WAC-Installer.png b/core/assets/icons/azure/other/03563-icon-service-WAC-Installer.png similarity index 100% rename from assets/icons/azure/other/03563-icon-service-WAC-Installer.png rename to core/assets/icons/azure/other/03563-icon-service-WAC-Installer.png diff --git a/assets/icons/azure/other/03563-icon-service-WAC-Installer.svg b/core/assets/icons/azure/other/03563-icon-service-WAC-Installer.svg similarity index 100% rename from assets/icons/azure/other/03563-icon-service-WAC-Installer.svg rename to core/assets/icons/azure/other/03563-icon-service-WAC-Installer.svg diff --git a/assets/icons/azure/other/03633-icon-service-Breeze.png b/core/assets/icons/azure/other/03633-icon-service-Breeze.png similarity index 100% rename from assets/icons/azure/other/03633-icon-service-Breeze.png rename to core/assets/icons/azure/other/03633-icon-service-Breeze.png diff --git a/assets/icons/azure/other/03633-icon-service-Breeze.svg b/core/assets/icons/azure/other/03633-icon-service-Breeze.svg similarity index 100% rename from assets/icons/azure/other/03633-icon-service-Breeze.svg rename to core/assets/icons/azure/other/03633-icon-service-Breeze.svg diff --git a/assets/icons/azure/other/10018-icon-service-Azure-A.png b/core/assets/icons/azure/other/10018-icon-service-Azure-A.png similarity index 100% rename from assets/icons/azure/other/10018-icon-service-Azure-A.png rename to core/assets/icons/azure/other/10018-icon-service-Azure-A.png diff --git a/assets/icons/azure/other/10018-icon-service-Azure-A.svg b/core/assets/icons/azure/other/10018-icon-service-Azure-A.svg similarity index 100% rename from assets/icons/azure/other/10018-icon-service-Azure-A.svg rename to core/assets/icons/azure/other/10018-icon-service-Azure-A.svg diff --git a/assets/icons/azure/other/10077-icon-service-Local-Network-Gateways.png b/core/assets/icons/azure/other/10077-icon-service-Local-Network-Gateways.png similarity index 100% rename from assets/icons/azure/other/10077-icon-service-Local-Network-Gateways.png rename to core/assets/icons/azure/other/10077-icon-service-Local-Network-Gateways.png diff --git a/assets/icons/azure/other/10077-icon-service-Local-Network-Gateways.svg b/core/assets/icons/azure/other/10077-icon-service-Local-Network-Gateways.svg similarity index 100% rename from assets/icons/azure/other/10077-icon-service-Local-Network-Gateways.svg rename to core/assets/icons/azure/other/10077-icon-service-Local-Network-Gateways.svg diff --git a/assets/icons/azure/other/10117-icon-service-Edge-Management.png b/core/assets/icons/azure/other/10117-icon-service-Edge-Management.png similarity index 100% rename from assets/icons/azure/other/10117-icon-service-Edge-Management.png rename to core/assets/icons/azure/other/10117-icon-service-Edge-Management.png diff --git a/assets/icons/azure/other/10117-icon-service-Edge-Management.svg b/core/assets/icons/azure/other/10117-icon-service-Edge-Management.svg similarity index 100% rename from assets/icons/azure/other/10117-icon-service-Edge-Management.svg rename to core/assets/icons/azure/other/10117-icon-service-Edge-Management.svg diff --git a/assets/icons/azure/other/10139-icon-service-Instance-Pools.png b/core/assets/icons/azure/other/10139-icon-service-Instance-Pools.png similarity index 100% rename from assets/icons/azure/other/10139-icon-service-Instance-Pools.png rename to core/assets/icons/azure/other/10139-icon-service-Instance-Pools.png diff --git a/assets/icons/azure/other/10139-icon-service-Instance-Pools.svg b/core/assets/icons/azure/other/10139-icon-service-Instance-Pools.svg similarity index 100% rename from assets/icons/azure/other/10139-icon-service-Instance-Pools.svg rename to core/assets/icons/azure/other/10139-icon-service-Instance-Pools.svg diff --git a/assets/icons/azure/other/10190-icon-service-Azure-Sphere.png b/core/assets/icons/azure/other/10190-icon-service-Azure-Sphere.png similarity index 100% rename from assets/icons/azure/other/10190-icon-service-Azure-Sphere.png rename to core/assets/icons/azure/other/10190-icon-service-Azure-Sphere.png diff --git a/assets/icons/azure/other/10190-icon-service-Azure-Sphere.svg b/core/assets/icons/azure/other/10190-icon-service-Azure-Sphere.svg similarity index 100% rename from assets/icons/azure/other/10190-icon-service-Azure-Sphere.svg rename to core/assets/icons/azure/other/10190-icon-service-Azure-Sphere.svg diff --git a/assets/icons/azure/other/10232-icon-service-App-Registrations.png b/core/assets/icons/azure/other/10232-icon-service-App-Registrations.png similarity index 100% rename from assets/icons/azure/other/10232-icon-service-App-Registrations.png rename to core/assets/icons/azure/other/10232-icon-service-App-Registrations.png diff --git a/assets/icons/azure/other/10232-icon-service-App-Registrations.svg b/core/assets/icons/azure/other/10232-icon-service-App-Registrations.svg similarity index 100% rename from assets/icons/azure/other/10232-icon-service-App-Registrations.svg rename to core/assets/icons/azure/other/10232-icon-service-App-Registrations.svg diff --git a/assets/icons/azure/other/10334-icon-service-Exchange-On-Premises-Access.png b/core/assets/icons/azure/other/10334-icon-service-Exchange-On-Premises-Access.png similarity index 100% rename from assets/icons/azure/other/10334-icon-service-Exchange-On-Premises-Access.png rename to core/assets/icons/azure/other/10334-icon-service-Exchange-On-Premises-Access.png diff --git a/assets/icons/azure/other/10334-icon-service-Exchange-On-Premises-Access.svg b/core/assets/icons/azure/other/10334-icon-service-Exchange-On-Premises-Access.svg similarity index 100% rename from assets/icons/azure/other/10334-icon-service-Exchange-On-Premises-Access.svg rename to core/assets/icons/azure/other/10334-icon-service-Exchange-On-Premises-Access.svg diff --git a/assets/icons/azure/other/10355-icon-service-WAC.png b/core/assets/icons/azure/other/10355-icon-service-WAC.png similarity index 100% rename from assets/icons/azure/other/10355-icon-service-WAC.png rename to core/assets/icons/azure/other/10355-icon-service-WAC.png diff --git a/assets/icons/azure/other/10355-icon-service-WAC.svg b/core/assets/icons/azure/other/10355-icon-service-WAC.svg similarity index 100% rename from assets/icons/azure/other/10355-icon-service-WAC.svg rename to core/assets/icons/azure/other/10355-icon-service-WAC.svg diff --git a/assets/icons/azure/other/10422-icon-service-AzureAttestation.png b/core/assets/icons/azure/other/10422-icon-service-AzureAttestation.png similarity index 100% rename from assets/icons/azure/other/10422-icon-service-AzureAttestation.png rename to core/assets/icons/azure/other/10422-icon-service-AzureAttestation.png diff --git a/assets/icons/azure/other/10422-icon-service-AzureAttestation.svg b/core/assets/icons/azure/other/10422-icon-service-AzureAttestation.svg similarity index 100% rename from assets/icons/azure/other/10422-icon-service-AzureAttestation.svg rename to core/assets/icons/azure/other/10422-icon-service-AzureAttestation.svg diff --git a/assets/icons/azure/other/10778-icon-service-RTOS.png b/core/assets/icons/azure/other/10778-icon-service-RTOS.png similarity index 100% rename from assets/icons/azure/other/10778-icon-service-RTOS.png rename to core/assets/icons/azure/other/10778-icon-service-RTOS.png diff --git a/assets/icons/azure/other/10778-icon-service-RTOS.svg b/core/assets/icons/azure/other/10778-icon-service-RTOS.svg similarity index 100% rename from assets/icons/azure/other/10778-icon-service-RTOS.svg rename to core/assets/icons/azure/other/10778-icon-service-RTOS.svg diff --git a/assets/icons/azure/other/10846-icon-service-Web-Jobs.png b/core/assets/icons/azure/other/10846-icon-service-Web-Jobs.png similarity index 100% rename from assets/icons/azure/other/10846-icon-service-Web-Jobs.png rename to core/assets/icons/azure/other/10846-icon-service-Web-Jobs.png diff --git a/assets/icons/azure/other/10846-icon-service-Web-Jobs.svg b/core/assets/icons/azure/other/10846-icon-service-Web-Jobs.svg similarity index 100% rename from assets/icons/azure/other/10846-icon-service-Web-Jobs.svg rename to core/assets/icons/azure/other/10846-icon-service-Web-Jobs.svg diff --git a/assets/icons/azure/security/00378-icon-service-Detonation.png b/core/assets/icons/azure/security/00378-icon-service-Detonation.png similarity index 100% rename from assets/icons/azure/security/00378-icon-service-Detonation.png rename to core/assets/icons/azure/security/00378-icon-service-Detonation.png diff --git a/assets/icons/azure/security/00378-icon-service-Detonation.svg b/core/assets/icons/azure/security/00378-icon-service-Detonation.svg similarity index 100% rename from assets/icons/azure/security/00378-icon-service-Detonation.svg rename to core/assets/icons/azure/security/00378-icon-service-Detonation.svg diff --git a/assets/icons/azure/security/02247-icon-service-Microsoft-Defender-for-IoT.png b/core/assets/icons/azure/security/02247-icon-service-Microsoft-Defender-for-IoT.png similarity index 100% rename from assets/icons/azure/security/02247-icon-service-Microsoft-Defender-for-IoT.png rename to core/assets/icons/azure/security/02247-icon-service-Microsoft-Defender-for-IoT.png diff --git a/assets/icons/azure/security/02247-icon-service-Microsoft-Defender-for-IoT.svg b/core/assets/icons/azure/security/02247-icon-service-Microsoft-Defender-for-IoT.svg similarity index 100% rename from assets/icons/azure/security/02247-icon-service-Microsoft-Defender-for-IoT.svg rename to core/assets/icons/azure/security/02247-icon-service-Microsoft-Defender-for-IoT.svg diff --git a/assets/icons/azure/security/02251-icon-service-Azure-AD-Privleged-Identity-Management.png b/core/assets/icons/azure/security/02251-icon-service-Azure-AD-Privleged-Identity-Management.png similarity index 100% rename from assets/icons/azure/security/02251-icon-service-Azure-AD-Privleged-Identity-Management.png rename to core/assets/icons/azure/security/02251-icon-service-Azure-AD-Privleged-Identity-Management.png diff --git a/assets/icons/azure/security/02251-icon-service-Azure-AD-Privleged-Identity-Management.svg b/core/assets/icons/azure/security/02251-icon-service-Azure-AD-Privleged-Identity-Management.svg similarity index 100% rename from assets/icons/azure/security/02251-icon-service-Azure-AD-Privleged-Identity-Management.svg rename to core/assets/icons/azure/security/02251-icon-service-Azure-AD-Privleged-Identity-Management.svg diff --git a/assets/icons/azure/security/03336-icon-service-Microsoft-Defender-EASM.png b/core/assets/icons/azure/security/03336-icon-service-Microsoft-Defender-EASM.png similarity index 100% rename from assets/icons/azure/security/03336-icon-service-Microsoft-Defender-EASM.png rename to core/assets/icons/azure/security/03336-icon-service-Microsoft-Defender-EASM.png diff --git a/assets/icons/azure/security/03336-icon-service-Microsoft-Defender-EASM.svg b/core/assets/icons/azure/security/03336-icon-service-Microsoft-Defender-EASM.svg similarity index 100% rename from assets/icons/azure/security/03336-icon-service-Microsoft-Defender-EASM.svg rename to core/assets/icons/azure/security/03336-icon-service-Microsoft-Defender-EASM.svg diff --git a/assets/icons/azure/security/03340-icon-service-Identity-Secure-Score.png b/core/assets/icons/azure/security/03340-icon-service-Identity-Secure-Score.png similarity index 100% rename from assets/icons/azure/security/03340-icon-service-Identity-Secure-Score.png rename to core/assets/icons/azure/security/03340-icon-service-Identity-Secure-Score.png diff --git a/assets/icons/azure/security/03340-icon-service-Identity-Secure-Score.svg b/core/assets/icons/azure/security/03340-icon-service-Identity-Secure-Score.svg similarity index 100% rename from assets/icons/azure/security/03340-icon-service-Identity-Secure-Score.svg rename to core/assets/icons/azure/security/03340-icon-service-Identity-Secure-Score.svg diff --git a/assets/icons/azure/security/03341-icon-service-Azure-AD-Risky-Signins.png b/core/assets/icons/azure/security/03341-icon-service-Azure-AD-Risky-Signins.png similarity index 100% rename from assets/icons/azure/security/03341-icon-service-Azure-AD-Risky-Signins.png rename to core/assets/icons/azure/security/03341-icon-service-Azure-AD-Risky-Signins.png diff --git a/assets/icons/azure/security/03341-icon-service-Azure-AD-Risky-Signins.svg b/core/assets/icons/azure/security/03341-icon-service-Azure-AD-Risky-Signins.svg similarity index 100% rename from assets/icons/azure/security/03341-icon-service-Azure-AD-Risky-Signins.svg rename to core/assets/icons/azure/security/03341-icon-service-Azure-AD-Risky-Signins.svg diff --git a/assets/icons/azure/security/03341-icon-service-Entra-Identity-Risky-Signins.png b/core/assets/icons/azure/security/03341-icon-service-Entra-Identity-Risky-Signins.png similarity index 100% rename from assets/icons/azure/security/03341-icon-service-Entra-Identity-Risky-Signins.png rename to core/assets/icons/azure/security/03341-icon-service-Entra-Identity-Risky-Signins.png diff --git a/assets/icons/azure/security/03341-icon-service-Entra-Identity-Risky-Signins.svg b/core/assets/icons/azure/security/03341-icon-service-Entra-Identity-Risky-Signins.svg similarity index 100% rename from assets/icons/azure/security/03341-icon-service-Entra-Identity-Risky-Signins.svg rename to core/assets/icons/azure/security/03341-icon-service-Entra-Identity-Risky-Signins.svg diff --git a/assets/icons/azure/security/03342-icon-service-Azure-AD-Risky-Users.png b/core/assets/icons/azure/security/03342-icon-service-Azure-AD-Risky-Users.png similarity index 100% rename from assets/icons/azure/security/03342-icon-service-Azure-AD-Risky-Users.png rename to core/assets/icons/azure/security/03342-icon-service-Azure-AD-Risky-Users.png diff --git a/assets/icons/azure/security/03342-icon-service-Azure-AD-Risky-Users.svg b/core/assets/icons/azure/security/03342-icon-service-Azure-AD-Risky-Users.svg similarity index 100% rename from assets/icons/azure/security/03342-icon-service-Azure-AD-Risky-Users.svg rename to core/assets/icons/azure/security/03342-icon-service-Azure-AD-Risky-Users.svg diff --git a/assets/icons/azure/security/03342-icon-service-Entra-Identity-Risky-Users.png b/core/assets/icons/azure/security/03342-icon-service-Entra-Identity-Risky-Users.png similarity index 100% rename from assets/icons/azure/security/03342-icon-service-Entra-Identity-Risky-Users.png rename to core/assets/icons/azure/security/03342-icon-service-Entra-Identity-Risky-Users.png diff --git a/assets/icons/azure/security/03342-icon-service-Entra-Identity-Risky-Users.svg b/core/assets/icons/azure/security/03342-icon-service-Entra-Identity-Risky-Users.svg similarity index 100% rename from assets/icons/azure/security/03342-icon-service-Entra-Identity-Risky-Users.svg rename to core/assets/icons/azure/security/03342-icon-service-Entra-Identity-Risky-Users.svg diff --git a/assets/icons/azure/security/03343-icon-service-Azure-AD-Authentication-Methods.png b/core/assets/icons/azure/security/03343-icon-service-Azure-AD-Authentication-Methods.png similarity index 100% rename from assets/icons/azure/security/03343-icon-service-Azure-AD-Authentication-Methods.png rename to core/assets/icons/azure/security/03343-icon-service-Azure-AD-Authentication-Methods.png diff --git a/assets/icons/azure/security/03343-icon-service-Azure-AD-Authentication-Methods.svg b/core/assets/icons/azure/security/03343-icon-service-Azure-AD-Authentication-Methods.svg similarity index 100% rename from assets/icons/azure/security/03343-icon-service-Azure-AD-Authentication-Methods.svg rename to core/assets/icons/azure/security/03343-icon-service-Azure-AD-Authentication-Methods.svg diff --git a/assets/icons/azure/security/03344-icon-service-Multifactor-Authentication.png b/core/assets/icons/azure/security/03344-icon-service-Multifactor-Authentication.png similarity index 100% rename from assets/icons/azure/security/03344-icon-service-Multifactor-Authentication.png rename to core/assets/icons/azure/security/03344-icon-service-Multifactor-Authentication.png diff --git a/assets/icons/azure/security/03344-icon-service-Multifactor-Authentication.svg b/core/assets/icons/azure/security/03344-icon-service-Multifactor-Authentication.svg similarity index 100% rename from assets/icons/azure/security/03344-icon-service-Multifactor-Authentication.svg rename to core/assets/icons/azure/security/03344-icon-service-Multifactor-Authentication.svg diff --git a/assets/icons/azure/security/10229-icon-service-Azure-Information-Protection.png b/core/assets/icons/azure/security/10229-icon-service-Azure-Information-Protection.png similarity index 100% rename from assets/icons/azure/security/10229-icon-service-Azure-Information-Protection.png rename to core/assets/icons/azure/security/10229-icon-service-Azure-Information-Protection.png diff --git a/assets/icons/azure/security/10229-icon-service-Azure-Information-Protection.svg b/core/assets/icons/azure/security/10229-icon-service-Azure-Information-Protection.svg similarity index 100% rename from assets/icons/azure/security/10229-icon-service-Azure-Information-Protection.svg rename to core/assets/icons/azure/security/10229-icon-service-Azure-Information-Protection.svg diff --git a/assets/icons/azure/security/10231-icon-service-Azure-AD-Identity-Protection.png b/core/assets/icons/azure/security/10231-icon-service-Azure-AD-Identity-Protection.png similarity index 100% rename from assets/icons/azure/security/10231-icon-service-Azure-AD-Identity-Protection.png rename to core/assets/icons/azure/security/10231-icon-service-Azure-AD-Identity-Protection.png diff --git a/assets/icons/azure/security/10231-icon-service-Azure-AD-Identity-Protection.svg b/core/assets/icons/azure/security/10231-icon-service-Azure-AD-Identity-Protection.svg similarity index 100% rename from assets/icons/azure/security/10231-icon-service-Azure-AD-Identity-Protection.svg rename to core/assets/icons/azure/security/10231-icon-service-Azure-AD-Identity-Protection.svg diff --git a/assets/icons/azure/security/10233-icon-service-Conditional-Access.png b/core/assets/icons/azure/security/10233-icon-service-Conditional-Access.png similarity index 100% rename from assets/icons/azure/security/10233-icon-service-Conditional-Access.png rename to core/assets/icons/azure/security/10233-icon-service-Conditional-Access.png diff --git a/assets/icons/azure/security/10233-icon-service-Conditional-Access.svg b/core/assets/icons/azure/security/10233-icon-service-Conditional-Access.svg similarity index 100% rename from assets/icons/azure/security/10233-icon-service-Conditional-Access.svg rename to core/assets/icons/azure/security/10233-icon-service-Conditional-Access.svg diff --git a/assets/icons/azure/security/10241-icon-service-Microsoft-Defender-for-Cloud.png b/core/assets/icons/azure/security/10241-icon-service-Microsoft-Defender-for-Cloud.png similarity index 100% rename from assets/icons/azure/security/10241-icon-service-Microsoft-Defender-for-Cloud.png rename to core/assets/icons/azure/security/10241-icon-service-Microsoft-Defender-for-Cloud.png diff --git a/assets/icons/azure/security/10241-icon-service-Microsoft-Defender-for-Cloud.svg b/core/assets/icons/azure/security/10241-icon-service-Microsoft-Defender-for-Cloud.svg similarity index 100% rename from assets/icons/azure/security/10241-icon-service-Microsoft-Defender-for-Cloud.svg rename to core/assets/icons/azure/security/10241-icon-service-Microsoft-Defender-for-Cloud.svg diff --git a/assets/icons/azure/security/10244-icon-service-Application-Security-Groups.png b/core/assets/icons/azure/security/10244-icon-service-Application-Security-Groups.png similarity index 100% rename from assets/icons/azure/security/10244-icon-service-Application-Security-Groups.png rename to core/assets/icons/azure/security/10244-icon-service-Application-Security-Groups.png diff --git a/assets/icons/azure/security/10244-icon-service-Application-Security-Groups.svg b/core/assets/icons/azure/security/10244-icon-service-Application-Security-Groups.svg similarity index 100% rename from assets/icons/azure/security/10244-icon-service-Application-Security-Groups.svg rename to core/assets/icons/azure/security/10244-icon-service-Application-Security-Groups.svg diff --git a/assets/icons/azure/security/10245-icon-service-Key-Vaults.png b/core/assets/icons/azure/security/10245-icon-service-Key-Vaults.png similarity index 100% rename from assets/icons/azure/security/10245-icon-service-Key-Vaults.png rename to core/assets/icons/azure/security/10245-icon-service-Key-Vaults.png diff --git a/assets/icons/azure/security/10245-icon-service-Key-Vaults.svg b/core/assets/icons/azure/security/10245-icon-service-Key-Vaults.svg similarity index 100% rename from assets/icons/azure/security/10245-icon-service-Key-Vaults.svg rename to core/assets/icons/azure/security/10245-icon-service-Key-Vaults.svg diff --git a/assets/icons/azure/security/10248-icon-service-Azure-Sentinel.png b/core/assets/icons/azure/security/10248-icon-service-Azure-Sentinel.png similarity index 100% rename from assets/icons/azure/security/10248-icon-service-Azure-Sentinel.png rename to core/assets/icons/azure/security/10248-icon-service-Azure-Sentinel.png diff --git a/assets/icons/azure/security/10248-icon-service-Azure-Sentinel.svg b/core/assets/icons/azure/security/10248-icon-service-Azure-Sentinel.svg similarity index 100% rename from assets/icons/azure/security/10248-icon-service-Azure-Sentinel.svg rename to core/assets/icons/azure/security/10248-icon-service-Azure-Sentinel.svg diff --git a/assets/icons/azure/security/10433-icon-service-User-Settings.png b/core/assets/icons/azure/security/10433-icon-service-User-Settings.png similarity index 100% rename from assets/icons/azure/security/10433-icon-service-User-Settings.png rename to core/assets/icons/azure/security/10433-icon-service-User-Settings.png diff --git a/assets/icons/azure/security/10433-icon-service-User-Settings.svg b/core/assets/icons/azure/security/10433-icon-service-User-Settings.svg similarity index 100% rename from assets/icons/azure/security/10433-icon-service-User-Settings.svg rename to core/assets/icons/azure/security/10433-icon-service-User-Settings.svg diff --git a/assets/icons/azure/security/10572-icon-service-ExtendedSecurityUpdates.png b/core/assets/icons/azure/security/10572-icon-service-ExtendedSecurityUpdates.png similarity index 100% rename from assets/icons/azure/security/10572-icon-service-ExtendedSecurityUpdates.png rename to core/assets/icons/azure/security/10572-icon-service-ExtendedSecurityUpdates.png diff --git a/assets/icons/azure/security/10572-icon-service-ExtendedSecurityUpdates.svg b/core/assets/icons/azure/security/10572-icon-service-ExtendedSecurityUpdates.svg similarity index 100% rename from assets/icons/azure/security/10572-icon-service-ExtendedSecurityUpdates.svg rename to core/assets/icons/azure/security/10572-icon-service-ExtendedSecurityUpdates.svg diff --git a/assets/icons/azure/storage/00017-icon-service-Recovery-Services-Vaults.png b/core/assets/icons/azure/storage/00017-icon-service-Recovery-Services-Vaults.png similarity index 100% rename from assets/icons/azure/storage/00017-icon-service-Recovery-Services-Vaults.png rename to core/assets/icons/azure/storage/00017-icon-service-Recovery-Services-Vaults.png diff --git a/assets/icons/azure/storage/00017-icon-service-Recovery-Services-Vaults.svg b/core/assets/icons/azure/storage/00017-icon-service-Recovery-Services-Vaults.svg similarity index 100% rename from assets/icons/azure/storage/00017-icon-service-Recovery-Services-Vaults.svg rename to core/assets/icons/azure/storage/00017-icon-service-Recovery-Services-Vaults.svg diff --git a/assets/icons/azure/storage/00691-icon-service-Azure-Databox-Gateway.png b/core/assets/icons/azure/storage/00691-icon-service-Azure-Databox-Gateway.png similarity index 100% rename from assets/icons/azure/storage/00691-icon-service-Azure-Databox-Gateway.png rename to core/assets/icons/azure/storage/00691-icon-service-Azure-Databox-Gateway.png diff --git a/assets/icons/azure/storage/00691-icon-service-Azure-Databox-Gateway.svg b/core/assets/icons/azure/storage/00691-icon-service-Azure-Databox-Gateway.svg similarity index 100% rename from assets/icons/azure/storage/00691-icon-service-Azure-Databox-Gateway.svg rename to core/assets/icons/azure/storage/00691-icon-service-Azure-Databox-Gateway.svg diff --git a/assets/icons/azure/storage/00776-icon-service-Azure-HCP-Cache.png b/core/assets/icons/azure/storage/00776-icon-service-Azure-HCP-Cache.png similarity index 100% rename from assets/icons/azure/storage/00776-icon-service-Azure-HCP-Cache.png rename to core/assets/icons/azure/storage/00776-icon-service-Azure-HCP-Cache.png diff --git a/assets/icons/azure/storage/00776-icon-service-Azure-HCP-Cache.svg b/core/assets/icons/azure/storage/00776-icon-service-Azure-HCP-Cache.svg similarity index 100% rename from assets/icons/azure/storage/00776-icon-service-Azure-HCP-Cache.svg rename to core/assets/icons/azure/storage/00776-icon-service-Azure-HCP-Cache.svg diff --git a/assets/icons/azure/storage/029203566-icon-service-Data-Virtualization.png b/core/assets/icons/azure/storage/029203566-icon-service-Data-Virtualization.png similarity index 100% rename from assets/icons/azure/storage/029203566-icon-service-Data-Virtualization.png rename to core/assets/icons/azure/storage/029203566-icon-service-Data-Virtualization.png diff --git a/assets/icons/azure/storage/029203566-icon-service-Data-Virtualization.svg b/core/assets/icons/azure/storage/029203566-icon-service-Data-Virtualization.svg similarity index 100% rename from assets/icons/azure/storage/029203566-icon-service-Data-Virtualization.svg rename to core/assets/icons/azure/storage/029203566-icon-service-Data-Virtualization.svg diff --git a/assets/icons/azure/storage/030486388-icon-service-Storage-Hubs.png b/core/assets/icons/azure/storage/030486388-icon-service-Storage-Hubs.png similarity index 100% rename from assets/icons/azure/storage/030486388-icon-service-Storage-Hubs.png rename to core/assets/icons/azure/storage/030486388-icon-service-Storage-Hubs.png diff --git a/assets/icons/azure/storage/030486388-icon-service-Storage-Hubs.svg b/core/assets/icons/azure/storage/030486388-icon-service-Storage-Hubs.svg similarity index 100% rename from assets/icons/azure/storage/030486388-icon-service-Storage-Hubs.svg rename to core/assets/icons/azure/storage/030486388-icon-service-Storage-Hubs.svg diff --git a/assets/icons/azure/storage/03502-icon-service-Storage-Actions.png b/core/assets/icons/azure/storage/03502-icon-service-Storage-Actions.png similarity index 100% rename from assets/icons/azure/storage/03502-icon-service-Storage-Actions.png rename to core/assets/icons/azure/storage/03502-icon-service-Storage-Actions.png diff --git a/assets/icons/azure/storage/03502-icon-service-Storage-Actions.svg b/core/assets/icons/azure/storage/03502-icon-service-Storage-Actions.svg similarity index 100% rename from assets/icons/azure/storage/03502-icon-service-Storage-Actions.svg rename to core/assets/icons/azure/storage/03502-icon-service-Storage-Actions.svg diff --git a/assets/icons/azure/storage/03549-icon-service-Managed-File-Shares.png b/core/assets/icons/azure/storage/03549-icon-service-Managed-File-Shares.png similarity index 100% rename from assets/icons/azure/storage/03549-icon-service-Managed-File-Shares.png rename to core/assets/icons/azure/storage/03549-icon-service-Managed-File-Shares.png diff --git a/assets/icons/azure/storage/03549-icon-service-Managed-File-Shares.svg b/core/assets/icons/azure/storage/03549-icon-service-Managed-File-Shares.svg similarity index 100% rename from assets/icons/azure/storage/03549-icon-service-Managed-File-Shares.svg rename to core/assets/icons/azure/storage/03549-icon-service-Managed-File-Shares.svg diff --git a/assets/icons/azure/storage/10086-icon-service-Storage-Accounts.png b/core/assets/icons/azure/storage/10086-icon-service-Storage-Accounts.png similarity index 100% rename from assets/icons/azure/storage/10086-icon-service-Storage-Accounts.png rename to core/assets/icons/azure/storage/10086-icon-service-Storage-Accounts.png diff --git a/assets/icons/azure/storage/10086-icon-service-Storage-Accounts.svg b/core/assets/icons/azure/storage/10086-icon-service-Storage-Accounts.svg similarity index 100% rename from assets/icons/azure/storage/10086-icon-service-Storage-Accounts.svg rename to core/assets/icons/azure/storage/10086-icon-service-Storage-Accounts.svg diff --git a/assets/icons/azure/storage/10087-icon-service-Storage-Accounts-(Classic).png b/core/assets/icons/azure/storage/10087-icon-service-Storage-Accounts-(Classic).png similarity index 100% rename from assets/icons/azure/storage/10087-icon-service-Storage-Accounts-(Classic).png rename to core/assets/icons/azure/storage/10087-icon-service-Storage-Accounts-(Classic).png diff --git a/assets/icons/azure/storage/10087-icon-service-Storage-Accounts-(Classic).svg b/core/assets/icons/azure/storage/10087-icon-service-Storage-Accounts-(Classic).svg similarity index 100% rename from assets/icons/azure/storage/10087-icon-service-Storage-Accounts-(Classic).svg rename to core/assets/icons/azure/storage/10087-icon-service-Storage-Accounts-(Classic).svg diff --git a/assets/icons/azure/storage/10089-icon-service-StorSimple-Device-Managers.png b/core/assets/icons/azure/storage/10089-icon-service-StorSimple-Device-Managers.png similarity index 100% rename from assets/icons/azure/storage/10089-icon-service-StorSimple-Device-Managers.png rename to core/assets/icons/azure/storage/10089-icon-service-StorSimple-Device-Managers.png diff --git a/assets/icons/azure/storage/10089-icon-service-StorSimple-Device-Managers.svg b/core/assets/icons/azure/storage/10089-icon-service-StorSimple-Device-Managers.svg similarity index 100% rename from assets/icons/azure/storage/10089-icon-service-StorSimple-Device-Managers.svg rename to core/assets/icons/azure/storage/10089-icon-service-StorSimple-Device-Managers.svg diff --git a/assets/icons/azure/storage/10090-icon-service-Data-Lake-Storage-Gen1.png b/core/assets/icons/azure/storage/10090-icon-service-Data-Lake-Storage-Gen1.png similarity index 100% rename from assets/icons/azure/storage/10090-icon-service-Data-Lake-Storage-Gen1.png rename to core/assets/icons/azure/storage/10090-icon-service-Data-Lake-Storage-Gen1.png diff --git a/assets/icons/azure/storage/10090-icon-service-Data-Lake-Storage-Gen1.svg b/core/assets/icons/azure/storage/10090-icon-service-Data-Lake-Storage-Gen1.svg similarity index 100% rename from assets/icons/azure/storage/10090-icon-service-Data-Lake-Storage-Gen1.svg rename to core/assets/icons/azure/storage/10090-icon-service-Data-Lake-Storage-Gen1.svg diff --git a/assets/icons/azure/storage/10091-icon-service-Storage-Explorer.png b/core/assets/icons/azure/storage/10091-icon-service-Storage-Explorer.png similarity index 100% rename from assets/icons/azure/storage/10091-icon-service-Storage-Explorer.png rename to core/assets/icons/azure/storage/10091-icon-service-Storage-Explorer.png diff --git a/assets/icons/azure/storage/10091-icon-service-Storage-Explorer.svg b/core/assets/icons/azure/storage/10091-icon-service-Storage-Explorer.svg similarity index 100% rename from assets/icons/azure/storage/10091-icon-service-Storage-Explorer.svg rename to core/assets/icons/azure/storage/10091-icon-service-Storage-Explorer.svg diff --git a/assets/icons/azure/storage/10092-icon-service-StorSimple-Data-Managers.png b/core/assets/icons/azure/storage/10092-icon-service-StorSimple-Data-Managers.png similarity index 100% rename from assets/icons/azure/storage/10092-icon-service-StorSimple-Data-Managers.png rename to core/assets/icons/azure/storage/10092-icon-service-StorSimple-Data-Managers.png diff --git a/assets/icons/azure/storage/10092-icon-service-StorSimple-Data-Managers.svg b/core/assets/icons/azure/storage/10092-icon-service-StorSimple-Data-Managers.svg similarity index 100% rename from assets/icons/azure/storage/10092-icon-service-StorSimple-Data-Managers.svg rename to core/assets/icons/azure/storage/10092-icon-service-StorSimple-Data-Managers.svg diff --git a/assets/icons/azure/storage/10093-icon-service-Storage-Sync-Services.png b/core/assets/icons/azure/storage/10093-icon-service-Storage-Sync-Services.png similarity index 100% rename from assets/icons/azure/storage/10093-icon-service-Storage-Sync-Services.png rename to core/assets/icons/azure/storage/10093-icon-service-Storage-Sync-Services.png diff --git a/assets/icons/azure/storage/10093-icon-service-Storage-Sync-Services.svg b/core/assets/icons/azure/storage/10093-icon-service-Storage-Sync-Services.svg similarity index 100% rename from assets/icons/azure/storage/10093-icon-service-Storage-Sync-Services.svg rename to core/assets/icons/azure/storage/10093-icon-service-Storage-Sync-Services.svg diff --git a/assets/icons/azure/storage/10094-icon-service-Data-Box.png b/core/assets/icons/azure/storage/10094-icon-service-Data-Box.png similarity index 100% rename from assets/icons/azure/storage/10094-icon-service-Data-Box.png rename to core/assets/icons/azure/storage/10094-icon-service-Data-Box.png diff --git a/assets/icons/azure/storage/10094-icon-service-Data-Box.svg b/core/assets/icons/azure/storage/10094-icon-service-Data-Box.svg similarity index 100% rename from assets/icons/azure/storage/10094-icon-service-Data-Box.svg rename to core/assets/icons/azure/storage/10094-icon-service-Data-Box.svg diff --git a/assets/icons/azure/storage/10095-icon-service-Azure-Stack-Edge.png b/core/assets/icons/azure/storage/10095-icon-service-Azure-Stack-Edge.png similarity index 100% rename from assets/icons/azure/storage/10095-icon-service-Azure-Stack-Edge.png rename to core/assets/icons/azure/storage/10095-icon-service-Azure-Stack-Edge.png diff --git a/assets/icons/azure/storage/10095-icon-service-Azure-Stack-Edge.svg b/core/assets/icons/azure/storage/10095-icon-service-Azure-Stack-Edge.svg similarity index 100% rename from assets/icons/azure/storage/10095-icon-service-Azure-Stack-Edge.svg rename to core/assets/icons/azure/storage/10095-icon-service-Azure-Stack-Edge.svg diff --git a/assets/icons/azure/storage/10096-icon-service-Azure-NetApp-Files.png b/core/assets/icons/azure/storage/10096-icon-service-Azure-NetApp-Files.png similarity index 100% rename from assets/icons/azure/storage/10096-icon-service-Azure-NetApp-Files.png rename to core/assets/icons/azure/storage/10096-icon-service-Azure-NetApp-Files.png diff --git a/assets/icons/azure/storage/10096-icon-service-Azure-NetApp-Files.svg b/core/assets/icons/azure/storage/10096-icon-service-Azure-NetApp-Files.svg similarity index 100% rename from assets/icons/azure/storage/10096-icon-service-Azure-NetApp-Files.svg rename to core/assets/icons/azure/storage/10096-icon-service-Azure-NetApp-Files.svg diff --git a/assets/icons/azure/storage/10097-icon-service-Data-Share-Invitations.png b/core/assets/icons/azure/storage/10097-icon-service-Data-Share-Invitations.png similarity index 100% rename from assets/icons/azure/storage/10097-icon-service-Data-Share-Invitations.png rename to core/assets/icons/azure/storage/10097-icon-service-Data-Share-Invitations.png diff --git a/assets/icons/azure/storage/10097-icon-service-Data-Share-Invitations.svg b/core/assets/icons/azure/storage/10097-icon-service-Data-Share-Invitations.svg similarity index 100% rename from assets/icons/azure/storage/10097-icon-service-Data-Share-Invitations.svg rename to core/assets/icons/azure/storage/10097-icon-service-Data-Share-Invitations.svg diff --git a/assets/icons/azure/storage/10098-icon-service-Data-Shares.png b/core/assets/icons/azure/storage/10098-icon-service-Data-Shares.png similarity index 100% rename from assets/icons/azure/storage/10098-icon-service-Data-Shares.png rename to core/assets/icons/azure/storage/10098-icon-service-Data-Shares.png diff --git a/assets/icons/azure/storage/10098-icon-service-Data-Shares.svg b/core/assets/icons/azure/storage/10098-icon-service-Data-Shares.svg similarity index 100% rename from assets/icons/azure/storage/10098-icon-service-Data-Shares.svg rename to core/assets/icons/azure/storage/10098-icon-service-Data-Shares.svg diff --git a/assets/icons/azure/storage/10100-icon-service-Import-Export-Jobs.png b/core/assets/icons/azure/storage/10100-icon-service-Import-Export-Jobs.png similarity index 100% rename from assets/icons/azure/storage/10100-icon-service-Import-Export-Jobs.png rename to core/assets/icons/azure/storage/10100-icon-service-Import-Export-Jobs.png diff --git a/assets/icons/azure/storage/10100-icon-service-Import-Export-Jobs.svg b/core/assets/icons/azure/storage/10100-icon-service-Import-Export-Jobs.svg similarity index 100% rename from assets/icons/azure/storage/10100-icon-service-Import-Export-Jobs.svg rename to core/assets/icons/azure/storage/10100-icon-service-Import-Export-Jobs.svg diff --git a/assets/icons/azure/storage/10400-icon-service-Azure-Fileshares.png b/core/assets/icons/azure/storage/10400-icon-service-Azure-Fileshares.png similarity index 100% rename from assets/icons/azure/storage/10400-icon-service-Azure-Fileshares.png rename to core/assets/icons/azure/storage/10400-icon-service-Azure-Fileshares.png diff --git a/assets/icons/azure/storage/10400-icon-service-Azure-Fileshares.svg b/core/assets/icons/azure/storage/10400-icon-service-Azure-Fileshares.svg similarity index 100% rename from assets/icons/azure/storage/10400-icon-service-Azure-Fileshares.svg rename to core/assets/icons/azure/storage/10400-icon-service-Azure-Fileshares.svg diff --git a/assets/icons/azure/web/00046-icon-service-App-Service-Plans.png b/core/assets/icons/azure/web/00046-icon-service-App-Service-Plans.png similarity index 100% rename from assets/icons/azure/web/00046-icon-service-App-Service-Plans.png rename to core/assets/icons/azure/web/00046-icon-service-App-Service-Plans.png diff --git a/assets/icons/azure/web/00046-icon-service-App-Service-Plans.svg b/core/assets/icons/azure/web/00046-icon-service-App-Service-Plans.svg similarity index 100% rename from assets/icons/azure/web/00046-icon-service-App-Service-Plans.svg rename to core/assets/icons/azure/web/00046-icon-service-App-Service-Plans.svg diff --git a/assets/icons/azure/web/00049-icon-service-App-Service-Certificates.png b/core/assets/icons/azure/web/00049-icon-service-App-Service-Certificates.png similarity index 100% rename from assets/icons/azure/web/00049-icon-service-App-Service-Certificates.png rename to core/assets/icons/azure/web/00049-icon-service-App-Service-Certificates.png diff --git a/assets/icons/azure/web/00049-icon-service-App-Service-Certificates.svg b/core/assets/icons/azure/web/00049-icon-service-App-Service-Certificates.svg similarity index 100% rename from assets/icons/azure/web/00049-icon-service-App-Service-Certificates.svg rename to core/assets/icons/azure/web/00049-icon-service-App-Service-Certificates.svg diff --git a/assets/icons/azure/web/00050-icon-service-App-Service-Domains.png b/core/assets/icons/azure/web/00050-icon-service-App-Service-Domains.png similarity index 100% rename from assets/icons/azure/web/00050-icon-service-App-Service-Domains.png rename to core/assets/icons/azure/web/00050-icon-service-App-Service-Domains.png diff --git a/assets/icons/azure/web/00050-icon-service-App-Service-Domains.svg b/core/assets/icons/azure/web/00050-icon-service-App-Service-Domains.svg similarity index 100% rename from assets/icons/azure/web/00050-icon-service-App-Service-Domains.svg rename to core/assets/icons/azure/web/00050-icon-service-App-Service-Domains.svg diff --git a/assets/icons/azure/web/01007-icon-service-Static-Apps.png b/core/assets/icons/azure/web/01007-icon-service-Static-Apps.png similarity index 100% rename from assets/icons/azure/web/01007-icon-service-Static-Apps.png rename to core/assets/icons/azure/web/01007-icon-service-Static-Apps.png diff --git a/assets/icons/azure/web/01007-icon-service-Static-Apps.svg b/core/assets/icons/azure/web/01007-icon-service-Static-Apps.svg similarity index 100% rename from assets/icons/azure/web/01007-icon-service-Static-Apps.svg rename to core/assets/icons/azure/web/01007-icon-service-Static-Apps.svg diff --git a/assets/icons/azure/web/03291-icon-service-API-Center.png b/core/assets/icons/azure/web/03291-icon-service-API-Center.png similarity index 100% rename from assets/icons/azure/web/03291-icon-service-API-Center.png rename to core/assets/icons/azure/web/03291-icon-service-API-Center.png diff --git a/assets/icons/azure/web/03291-icon-service-API-Center.svg b/core/assets/icons/azure/web/03291-icon-service-API-Center.svg similarity index 100% rename from assets/icons/azure/web/03291-icon-service-API-Center.svg rename to core/assets/icons/azure/web/03291-icon-service-API-Center.svg diff --git a/assets/icons/azure/web/03335-icon-service-Power-Platform.png b/core/assets/icons/azure/web/03335-icon-service-Power-Platform.png similarity index 100% rename from assets/icons/azure/web/03335-icon-service-Power-Platform.png rename to core/assets/icons/azure/web/03335-icon-service-Power-Platform.png diff --git a/assets/icons/azure/web/03335-icon-service-Power-Platform.svg b/core/assets/icons/azure/web/03335-icon-service-Power-Platform.svg similarity index 100% rename from assets/icons/azure/web/03335-icon-service-Power-Platform.svg rename to core/assets/icons/azure/web/03335-icon-service-Power-Platform.svg diff --git a/assets/icons/azure/web/03397-icon-service-App-Space.png b/core/assets/icons/azure/web/03397-icon-service-App-Space.png similarity index 100% rename from assets/icons/azure/web/03397-icon-service-App-Space.png rename to core/assets/icons/azure/web/03397-icon-service-App-Space.png diff --git a/assets/icons/azure/web/03397-icon-service-App-Space.svg b/core/assets/icons/azure/web/03397-icon-service-App-Space.svg similarity index 100% rename from assets/icons/azure/web/03397-icon-service-App-Space.svg rename to core/assets/icons/azure/web/03397-icon-service-App-Space.svg diff --git a/assets/icons/azure/web/03595-icon-service-App-Space-Component.png b/core/assets/icons/azure/web/03595-icon-service-App-Space-Component.png similarity index 100% rename from assets/icons/azure/web/03595-icon-service-App-Space-Component.png rename to core/assets/icons/azure/web/03595-icon-service-App-Space-Component.png diff --git a/assets/icons/azure/web/03595-icon-service-App-Space-Component.svg b/core/assets/icons/azure/web/03595-icon-service-App-Space-Component.svg similarity index 100% rename from assets/icons/azure/web/03595-icon-service-App-Space-Component.svg rename to core/assets/icons/azure/web/03595-icon-service-App-Space-Component.svg diff --git a/assets/icons/azure/web/10035-icon-service-App-Services.png b/core/assets/icons/azure/web/10035-icon-service-App-Services.png similarity index 100% rename from assets/icons/azure/web/10035-icon-service-App-Services.png rename to core/assets/icons/azure/web/10035-icon-service-App-Services.png diff --git a/assets/icons/azure/web/10035-icon-service-App-Services.svg b/core/assets/icons/azure/web/10035-icon-service-App-Services.svg similarity index 100% rename from assets/icons/azure/web/10035-icon-service-App-Services.svg rename to core/assets/icons/azure/web/10035-icon-service-App-Services.svg diff --git a/assets/icons/azure/web/10042-icon-service-API-Management-Services.png b/core/assets/icons/azure/web/10042-icon-service-API-Management-Services.png similarity index 100% rename from assets/icons/azure/web/10042-icon-service-API-Management-Services.png rename to core/assets/icons/azure/web/10042-icon-service-API-Management-Services.png diff --git a/assets/icons/azure/web/10042-icon-service-API-Management-Services.svg b/core/assets/icons/azure/web/10042-icon-service-API-Management-Services.svg similarity index 100% rename from assets/icons/azure/web/10042-icon-service-API-Management-Services.svg rename to core/assets/icons/azure/web/10042-icon-service-API-Management-Services.svg diff --git a/assets/icons/azure/web/10044-icon-service-Cognitive-Search.png b/core/assets/icons/azure/web/10044-icon-service-Cognitive-Search.png similarity index 100% rename from assets/icons/azure/web/10044-icon-service-Cognitive-Search.png rename to core/assets/icons/azure/web/10044-icon-service-Cognitive-Search.png diff --git a/assets/icons/azure/web/10044-icon-service-Cognitive-Search.svg b/core/assets/icons/azure/web/10044-icon-service-Cognitive-Search.svg similarity index 100% rename from assets/icons/azure/web/10044-icon-service-Cognitive-Search.svg rename to core/assets/icons/azure/web/10044-icon-service-Cognitive-Search.svg diff --git a/assets/icons/azure/web/10047-icon-service-App-Service-Environments.png b/core/assets/icons/azure/web/10047-icon-service-App-Service-Environments.png similarity index 100% rename from assets/icons/azure/web/10047-icon-service-App-Service-Environments.png rename to core/assets/icons/azure/web/10047-icon-service-App-Service-Environments.png diff --git a/assets/icons/azure/web/10047-icon-service-App-Service-Environments.svg b/core/assets/icons/azure/web/10047-icon-service-App-Service-Environments.svg similarity index 100% rename from assets/icons/azure/web/10047-icon-service-App-Service-Environments.svg rename to core/assets/icons/azure/web/10047-icon-service-App-Service-Environments.svg diff --git a/assets/icons/azure/web/10048-icon-service-API-Connections.png b/core/assets/icons/azure/web/10048-icon-service-API-Connections.png similarity index 100% rename from assets/icons/azure/web/10048-icon-service-API-Connections.png rename to core/assets/icons/azure/web/10048-icon-service-API-Connections.png diff --git a/assets/icons/azure/web/10048-icon-service-API-Connections.svg b/core/assets/icons/azure/web/10048-icon-service-API-Connections.svg similarity index 100% rename from assets/icons/azure/web/10048-icon-service-API-Connections.svg rename to core/assets/icons/azure/web/10048-icon-service-API-Connections.svg diff --git a/assets/icons/azure/web/10052-icon-service-SignalR.png b/core/assets/icons/azure/web/10052-icon-service-SignalR.png similarity index 100% rename from assets/icons/azure/web/10052-icon-service-SignalR.png rename to core/assets/icons/azure/web/10052-icon-service-SignalR.png diff --git a/assets/icons/azure/web/10052-icon-service-SignalR.svg b/core/assets/icons/azure/web/10052-icon-service-SignalR.svg similarity index 100% rename from assets/icons/azure/web/10052-icon-service-SignalR.svg rename to core/assets/icons/azure/web/10052-icon-service-SignalR.svg diff --git a/assets/icons/azure/web/10053-icon-service-Notification-Hub-Namespaces.png b/core/assets/icons/azure/web/10053-icon-service-Notification-Hub-Namespaces.png similarity index 100% rename from assets/icons/azure/web/10053-icon-service-Notification-Hub-Namespaces.png rename to core/assets/icons/azure/web/10053-icon-service-Notification-Hub-Namespaces.png diff --git a/assets/icons/azure/web/10053-icon-service-Notification-Hub-Namespaces.svg b/core/assets/icons/azure/web/10053-icon-service-Notification-Hub-Namespaces.svg similarity index 100% rename from assets/icons/azure/web/10053-icon-service-Notification-Hub-Namespaces.svg rename to core/assets/icons/azure/web/10053-icon-service-Notification-Hub-Namespaces.svg diff --git a/assets/icons/azure/web/10073-icon-service-Front-Door-and-CDN-Profiles.png b/core/assets/icons/azure/web/10073-icon-service-Front-Door-and-CDN-Profiles.png similarity index 100% rename from assets/icons/azure/web/10073-icon-service-Front-Door-and-CDN-Profiles.png rename to core/assets/icons/azure/web/10073-icon-service-Front-Door-and-CDN-Profiles.png diff --git a/assets/icons/azure/web/10073-icon-service-Front-Door-and-CDN-Profiles.svg b/core/assets/icons/azure/web/10073-icon-service-Front-Door-and-CDN-Profiles.svg similarity index 100% rename from assets/icons/azure/web/10073-icon-service-Front-Door-and-CDN-Profiles.svg rename to core/assets/icons/azure/web/10073-icon-service-Front-Door-and-CDN-Profiles.svg diff --git a/assets/icons/azure/web/10162-icon-service-Cognitive-Services.png b/core/assets/icons/azure/web/10162-icon-service-Cognitive-Services.png similarity index 100% rename from assets/icons/azure/web/10162-icon-service-Cognitive-Services.png rename to core/assets/icons/azure/web/10162-icon-service-Cognitive-Services.png diff --git a/assets/icons/azure/web/10162-icon-service-Cognitive-Services.svg b/core/assets/icons/azure/web/10162-icon-service-Cognitive-Services.svg similarity index 100% rename from assets/icons/azure/web/10162-icon-service-Cognitive-Services.svg rename to core/assets/icons/azure/web/10162-icon-service-Cognitive-Services.svg diff --git a/assets/icons/azure/web/10309-icon-service-Azure-Media-Service.png b/core/assets/icons/azure/web/10309-icon-service-Azure-Media-Service.png similarity index 100% rename from assets/icons/azure/web/10309-icon-service-Azure-Media-Service.png rename to core/assets/icons/azure/web/10309-icon-service-Azure-Media-Service.png diff --git a/assets/icons/azure/web/10309-icon-service-Azure-Media-Service.svg b/core/assets/icons/azure/web/10309-icon-service-Azure-Media-Service.svg similarity index 100% rename from assets/icons/azure/web/10309-icon-service-Azure-Media-Service.svg rename to core/assets/icons/azure/web/10309-icon-service-Azure-Media-Service.svg diff --git a/assets/icons/azure/web/10370-icon-service-Azure-Spring-Apps.png b/core/assets/icons/azure/web/10370-icon-service-Azure-Spring-Apps.png similarity index 100% rename from assets/icons/azure/web/10370-icon-service-Azure-Spring-Apps.png rename to core/assets/icons/azure/web/10370-icon-service-Azure-Spring-Apps.png diff --git a/assets/icons/azure/web/10370-icon-service-Azure-Spring-Apps.svg b/core/assets/icons/azure/web/10370-icon-service-Azure-Spring-Apps.svg similarity index 100% rename from assets/icons/azure/web/10370-icon-service-Azure-Spring-Apps.svg rename to core/assets/icons/azure/web/10370-icon-service-Azure-Spring-Apps.svg diff --git a/assets/icons/misc/no_image.png b/core/assets/icons/misc/no_image.png similarity index 100% rename from assets/icons/misc/no_image.png rename to core/assets/icons/misc/no_image.png diff --git a/assets/icons/severity/high.png b/core/assets/icons/severity/high.png similarity index 100% rename from assets/icons/severity/high.png rename to core/assets/icons/severity/high.png diff --git a/assets/icons/severity/low.png b/core/assets/icons/severity/low.png similarity index 100% rename from assets/icons/severity/low.png rename to core/assets/icons/severity/low.png diff --git a/assets/icons/severity/medium.png b/core/assets/icons/severity/medium.png similarity index 100% rename from assets/icons/severity/medium.png rename to core/assets/icons/severity/medium.png diff --git a/assets/img/logo/favicon.ico b/core/assets/img/logo/favicon.ico similarity index 100% rename from assets/img/logo/favicon.ico rename to core/assets/img/logo/favicon.ico diff --git a/assets/img/logo/favicon.png b/core/assets/img/logo/favicon.png similarity index 100% rename from assets/img/logo/favicon.png rename to core/assets/img/logo/favicon.png diff --git a/assets/img/logo/logo.png b/core/assets/img/logo/logo.png similarity index 100% rename from assets/img/logo/logo.png rename to core/assets/img/logo/logo.png diff --git a/assets/img/logo/report_logo.png b/core/assets/img/logo/report_logo.png similarity index 100% rename from assets/img/logo/report_logo.png rename to core/assets/img/logo/report_logo.png diff --git a/assets/template/egress.html b/core/assets/template/egress.html similarity index 100% rename from assets/template/egress.html rename to core/assets/template/egress.html diff --git a/assets/template/index.html b/core/assets/template/index.html similarity index 100% rename from assets/template/index.html rename to core/assets/template/index.html diff --git a/core/report/context.py b/core/report/context.py index 32ead79..f5300bd 100644 --- a/core/report/context.py +++ b/core/report/context.py @@ -3,6 +3,7 @@ from datetime import datetime, timezone from typing import Any +from core.resources import find_asset from core.utils_report_common import summarize_alternative_technologies from utils.data import CatalogueError, catalogue_info from utils.version import __version__ @@ -165,11 +166,9 @@ def completed(self): def _icon_path(report_path, icon): - relative = str(icon or "").lstrip("/") - for base in (report_path, "."): - candidate = os.path.join(base, "assets", relative) - if relative and os.path.isfile(candidate): - return candidate + found = find_asset(report_path, str(icon or "")) + if found is not None: + return str(found) return os.path.join(report_path, "assets", MISSING_ICON) diff --git a/core/report/cover.py b/core/report/cover.py index 4d70657..c6abec7 100644 --- a/core/report/cover.py +++ b/core/report/cover.py @@ -10,6 +10,8 @@ from reportlab.lib.utils import ImageReader from reportlab.platypus import Paragraph +from core.resources import find_asset + from .fonts import FONT, FONT_BOLD, FONT_MONO from .helpers import strategy_name from .palette import INK, MUTED, RULE, SUBTITLE @@ -75,12 +77,10 @@ def fit_logo(width, height, max_width=LOGO_MAX_WIDTH, max_height=LOGO_MAX_HEIGHT def logo_reader(report_path): - # The report folder carries its own copy of the assets. - for base in (os.path.join(report_path, "assets"), "assets"): - path = os.path.join(base, LOGO_FILE) - if os.path.isfile(path): - return ImageReader(path) - raise FileNotFoundError(f"report logo not found under {report_path}/assets") + path = find_asset(report_path, LOGO_FILE) + if path is None: + raise FileNotFoundError(f"report logo not found under {report_path}/assets") + return ImageReader(str(path)) def _paragraph(value, style): diff --git a/core/report/fonts.py b/core/report/fonts.py index 568a0d3..1426b8c 100644 --- a/core/report/fonts.py +++ b/core/report/fonts.py @@ -1,12 +1,17 @@ -from pathlib import Path - from reportlab.graphics.shapes import STATE_DEFAULTS from reportlab.lib.fonts import addMapping from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.platypus.tables import CellStyle -FONT_DIR = Path(__file__).resolve().parents[2] / "assets" / "fonts" +from ..resources import asset_path + +FONT_DIR = asset_path("fonts") + + +class MissingFontError(FileNotFoundError): + pass + FONT = "IBMPlexSans" FONT_MEDIUM = "IBMPlexSans-Medium" @@ -34,7 +39,13 @@ def register_fonts(): # A missing file raises: a report must never switch typeface without # anyone noticing. for name, filename in FONT_FILES.items(): - pdfmetrics.registerFont(TTFont(name, str(FONT_DIR / filename))) + path = FONT_DIR / filename + if not path.is_file(): + raise MissingFontError( + f"the font file {filename} is missing from {FONT_DIR}; " + "the cloudexit installation is incomplete" + ) + pdfmetrics.registerFont(TTFont(name, str(path))) # Families let and in Paragraph markup find the right face. pdfmetrics.registerFontFamily( FONT, diff --git a/core/resources.py b/core/resources.py new file mode 100644 index 0000000..1f8dd44 --- /dev/null +++ b/core/resources.py @@ -0,0 +1,25 @@ +from importlib.resources import files +from pathlib import Path, PurePosixPath + + +def asset_path(*parts: str) -> Path: + root = files("core") + if not isinstance(root, Path): + raise RuntimeError( + "the cloudexit assets are not on the file system " + f"({type(root).__name__}); install the package into a directory" + ) + return root.joinpath("assets", *parts) + + +def find_asset(report_path: str | Path, relative: str) -> Path | None: + # The report folder carries its own copy of the assets; the installed + # package is the fallback. + parts = PurePosixPath(str(relative).lstrip("/")).parts + if not parts or ".." in parts: + return None + for base in (Path(report_path) / "assets", asset_path()): + candidate = base.joinpath(*parts) + if candidate.is_file(): + return candidate + return None diff --git a/core/utils.py b/core/utils.py index d4f355f..61b00a9 100644 --- a/core/utils.py +++ b/core/utils.py @@ -5,6 +5,8 @@ from utils.data import active_catalogue_path +from .resources import asset_path + logger = logging.getLogger("core.engine.utils") # Icon folder per cloud service provider id @@ -45,7 +47,7 @@ def copy_assets(report_path: str, cloud_service_provider: int) -> None: os.makedirs(assets_path, exist_ok=True) for folder in ("css", "img"): - src_path = os.path.join("assets", folder) + src_path = asset_path(folder) dest_path = os.path.join(assets_path, folder) # Only copy if the destination doesn't already exist @@ -54,7 +56,7 @@ def copy_assets(report_path: str, cloud_service_provider: int) -> None: # Only the assessed provider's icons travel with the report for folder in _icon_dirs(cloud_service_provider): - src_path = os.path.join("assets", "icons", folder) + src_path = asset_path("icons", folder) dest_path = os.path.join(assets_path, "icons", folder) # Only copy if the destination doesn't already exist diff --git a/core/utils_report.py b/core/utils_report.py index 396b48a..415d62f 100644 --- a/core/utils_report.py +++ b/core/utils_report.py @@ -18,6 +18,7 @@ ) # Utils +from core.resources import asset_path from core.utils_report_html import ( transform_cost_inventory_for_html, transform_risk_inventory_for_html, @@ -120,9 +121,7 @@ def generate_html_report( } # Render the HTML template - template_path = os.path.join("assets", "template", "index.html") - with open(template_path, "r") as file: - template_content = file.read() + template_content = asset_path("template", "index.html").read_text(encoding="utf-8") env = Environment(autoescape=True) template = env.from_string(template_content) diff --git a/core/utils_report_egress.py b/core/utils_report_egress.py index 5b67e00..03cd271 100644 --- a/core/utils_report_egress.py +++ b/core/utils_report_egress.py @@ -22,6 +22,7 @@ TableStyle, ) +from core.resources import asset_path, find_asset from core.utils_db import load_data from core.utils_egress import ( GIB, @@ -418,9 +419,9 @@ def generate_egress_html_report(report_path: str, json_path: str) -> dict[str, A fee_labels = [group["label"] for group in fee_groups] fee_values = [round(group["fee"], 2) for group in fee_groups] - template_path = os.path.join("assets", "template", "egress.html") - with open(template_path, "r") as file: - template_content = file.read() + template_content = asset_path("template", "egress.html").read_text( + encoding="utf-8" + ) env = Environment(autoescape=True) template = env.from_string(template_content) @@ -461,12 +462,10 @@ def generate_egress_html_report(report_path: str, json_path: str) -> dict[str, A def _icon_flowable(report_path: str, icon: str) -> Any: - icon_path = os.path.join(report_path, "assets") + icon - if not os.path.exists(icon_path): - icon_path = os.path.join(report_path, "assets") + DEFAULT_ICON - if not os.path.exists(icon_path): + icon_path = find_asset(report_path, icon) or find_asset(report_path, DEFAULT_ICON) + if icon_path is None: return "" - return Image(icon_path, width=20, height=20) + return Image(str(icon_path), width=20, height=20) def _fee_breakdown_display(group: dict[str, Any]) -> str: diff --git a/core/utils_report_pdf.py b/core/utils_report_pdf.py index 48d86a1..95333b8 100644 --- a/core/utils_report_pdf.py +++ b/core/utils_report_pdf.py @@ -17,6 +17,7 @@ from reportlab.graphics.charts.piecharts import Pie from reportlab.graphics.charts.barcharts import VerticalBarChart +from core.resources import find_asset from core.utils_report_common import ( enrich_resource_inventory, summarize_alternative_technologies, @@ -165,8 +166,8 @@ def draw_header_footer( table.drawOn(canvas, doc.leftMargin, height - doc.topMargin) # Add the logo - logo_path = f"{report_path}/assets/img/logo/report_logo.png" - logo = Image(logo_path, width=150, height=30) + logo_path = find_asset(report_path, "img/logo/report_logo.png") + logo = Image(str(logo_path), width=150, height=30) # Aligning logo vertically with the text logo_y = height - doc.topMargin + 5 diff --git a/pyproject.toml b/pyproject.toml index 5bbd5d4..1c6f0bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,3 +48,6 @@ py-modules = ["main"] [tool.setuptools.packages.find] where = ["."] include = ["core", "core.*", "utils", "utils.*"] + +[tool.setuptools.package-data] +core = ["assets/**/*"] diff --git a/tests/fixtures/smoke/aws.tfstate b/tests/fixtures/smoke/aws.tfstate new file mode 100644 index 0000000..879619b --- /dev/null +++ b/tests/fixtures/smoke/aws.tfstate @@ -0,0 +1,74 @@ +{ + "version": 4, + "terraform_version": "1.14.6", + "serial": 1, + "lineage": "00000000-0000-0000-0000-000000000000", + "resources": [ + { + "mode": "managed", + "type": "aws_s3_bucket", + "name": "assets", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "region": "eu-central-1", + "bucket": "example-assets", + "arn": "arn:aws:s3:::example-assets" + } + } + ] + }, + { + "mode": "managed", + "type": "aws_instance", + "name": "web", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "availability_zone": "eu-central-1a", + "arn": "arn:aws:ec2:eu-central-1:123456789012:instance/i-0123456789abcdef0" + } + }, + { + "schema_version": 0, + "attributes": { + "availability_zone": "eu-central-1b", + "arn": "arn:aws:ec2:eu-central-1:123456789012:instance/i-0123456789abcdef1" + } + } + ] + }, + { + "mode": "managed", + "type": "aws_lambda_function", + "name": "worker", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "arn": "arn:aws:lambda:eu-central-1:123456789012:function:example-worker" + } + } + ] + }, + { + "mode": "managed", + "type": "aws_dynamodb_table", + "name": "items", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "arn": "arn:aws:dynamodb:eu-central-1:123456789012:table/example-items" + } + } + ] + } + ] +} diff --git a/tests/make_smoke_dataset.py b/tests/make_smoke_dataset.py new file mode 100644 index 0000000..9bf04d7 --- /dev/null +++ b/tests/make_smoke_dataset.py @@ -0,0 +1,106 @@ +import sqlite3 +import sys + +from tests.catalogue_fixtures import build_catalogue + +RESOURCE_TYPES = [ + ( + 1, + 2, + "AWS.EC2.DescribeInstances", + "EC2 Instance", + "/icons/aws/Compute/EC2.png", + "aws_instance", + ), + ( + 2, + 2, + "AWS.S3.ListBuckets", + "S3 Bucket", + "/icons/aws/Storage/S3-on-Outposts.png", + "aws_s3_bucket", + ), + ( + 3, + 2, + "AWS.Lambda.ListFunctions", + "Lambda Function", + "/icons/aws/Compute/Lambda.png", + "aws_lambda_function", + ), + ( + 4, + 2, + "AWS.DynamoDB.ListTables", + "DynamoDB Table", + "/icons/aws/Database/DynamoDB.png", + "aws_dynamodb_table", + ), +] + +TABLES = """ +DROP TABLE risk; +CREATE TABLE risk ( + id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL, + severity TEXT NOT NULL +); +CREATE TABLE risk_inventory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, resource_type INTEGER, risk INTEGER +); +CREATE TABLE alternative ( + id INTEGER PRIMARY KEY, resource_type INTEGER NOT NULL, + alternative_technology INTEGER NOT NULL, strategy_type INTEGER NOT NULL +); +CREATE TABLE alternativetechnology ( + id INTEGER PRIMARY KEY, product_name TEXT NOT NULL, product_description TEXT, + product_url TEXT, open_source TEXT NOT NULL, support_plan TEXT NOT NULL, + status TEXT NOT NULL, organization_id INTEGER NULL +); +CREATE TABLE resource_inventory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, resource_type INTEGER NOT NULL, + location TEXT NOT NULL, count INTEGER NOT NULL, UNIQUE(resource_type, location) +); +CREATE TABLE cost_inventory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, month TEXT NOT NULL, cost REAL NOT NULL, + currency TEXT NOT NULL, UNIQUE(month) +); +CREATE TABLE egress_inventory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, resource_type INTEGER NOT NULL, + name TEXT NOT NULL, size_bytes INTEGER, size_unknown INTEGER NOT NULL DEFAULT 0, + flags TEXT, notes TEXT +); +CREATE TABLE egress_inventory_tier ( + id INTEGER PRIMARY KEY AUTOINCREMENT, egress_inventory_id INTEGER NOT NULL, + tier TEXT NOT NULL, size_bytes INTEGER NOT NULL, + is_archive INTEGER NOT NULL DEFAULT 0 +); +CREATE TABLE alternativetechnologyorganization ( + id INTEGER PRIMARY KEY, name TEXT NOT NULL, url TEXT NULL, + stability_tier INTEGER NULL, country_code TEXT NULL, + years_in_business INTEGER NULL +); +""" + +RISKS = [ + (1, "Few alternatives", "There are few alternatives.", "medium"), + (2, "No alternatives", "There are no alternatives.", "high"), +] + + +def main(path): + build_catalogue(path, version="2026-11-02") + conn = sqlite3.connect(path) + conn.executescript(TABLES) + conn.execute("DELETE FROM resourcetype") + conn.executemany( + "INSERT INTO resourcetype (id, csp, code, name, icon, status, tf_code) " + "VALUES (?, ?, ?, ?, ?, 't', ?)", + RESOURCE_TYPES, + ) + conn.executemany("INSERT INTO risk VALUES (?, ?, ?, ?)", RISKS) + conn.commit() + conn.close() + + +if __name__ == "__main__": + main(sys.argv[1]) diff --git a/tests/report_fixtures.py b/tests/report_fixtures.py index 0ec9977..b5825fe 100644 --- a/tests/report_fixtures.py +++ b/tests/report_fixtures.py @@ -1,6 +1,8 @@ import shutil from pathlib import Path +from core.resources import asset_path + def build_report_fixture(): metadata = { @@ -117,7 +119,7 @@ def stage_report_assets(report_path: str) -> None: report_assets = Path(report_path) / "assets" report_assets.mkdir(parents=True, exist_ok=True) - source_assets = Path("assets") + source_assets = asset_path() for folder in ("css", "img", "icons"): shutil.copytree( source_assets / folder, @@ -171,9 +173,9 @@ def stage_minimal_assets(report_path: str) -> None: # What the Basic report needs: the logo and the fallback icon. Copying the # whole icon set for every render would dominate the test time. report_assets = Path(report_path) / "assets" - shutil.copytree(Path("assets") / "img", report_assets / "img", dirs_exist_ok=True) + shutil.copytree(asset_path("img"), report_assets / "img", dirs_exist_ok=True) shutil.copytree( - Path("assets") / "icons" / "misc", + asset_path("icons", "misc"), report_assets / "icons" / "misc", dirs_exist_ok=True, ) diff --git a/tests/test_copy_assets.py b/tests/test_copy_assets.py index 63d6499..489e9e5 100644 --- a/tests/test_copy_assets.py +++ b/tests/test_copy_assets.py @@ -6,10 +6,11 @@ from pathlib import Path from unittest import mock +from core.resources import asset_path from core.utils import copy_assets from utils.data import active_catalogue_path -SOURCE_ICONS = Path("assets/icons") +SOURCE_ICONS = asset_path("icons") @contextmanager diff --git a/tests/test_packaging.py b/tests/test_packaging.py new file mode 100644 index 0000000..9b0c62e --- /dev/null +++ b/tests/test_packaging.py @@ -0,0 +1,249 @@ +import os +import re +import shutil +import subprocess +import sys +import tempfile +import unittest +import zipfile +from contextlib import contextmanager +from pathlib import Path +from unittest import mock + +from core import resources +from core.engine import generate_report, perform_risk_assessment +from core.report import fonts +from core.resources import asset_path, find_asset +from core.utils import copy_assets +from tests.flow_fixtures import build_flow_run +from tests.test_report_flow import AWS_RESOURCES, CONFIG, COSTS + +ROOT = Path(__file__).resolve().parent.parent +SOURCE_FILES = ("pyproject.toml", "README.md", "main.py") +SOURCE_DIRS = ("core", "utils") + + +@contextmanager +def working_directory(path): + previous = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(previous) + + +def asset_files(root): + return sorted( + path.relative_to(root).as_posix() + for path in Path(root).rglob("*") + if path.is_file() and not path.name.startswith(".") + ) + + +class WheelContentsTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + tmp = tempfile.TemporaryDirectory() + cls.addClassCleanup(tmp.cleanup) + # Build from a copy so no stale build directory in the checkout can + # hide a file that the package data does not pick up. + source = Path(tmp.name) / "source" + source.mkdir() + for name in SOURCE_FILES: + shutil.copy(ROOT / name, source / name) + for name in SOURCE_DIRS: + shutil.copytree( + ROOT / name, source / name, ignore=shutil.ignore_patterns("__pycache__") + ) + out = Path(tmp.name) / "wheel" + subprocess.run( + [sys.executable, "-m", "pip", "wheel", ".", "--no-deps", "-w", str(out)], + cwd=source, + check=True, + capture_output=True, + ) + (wheel,) = out.glob("*.whl") + cls.names = set(zipfile.ZipFile(wheel).namelist()) + + def test_the_wheel_contains_every_asset_file(self): + expected = {f"core/assets/{name}" for name in asset_files(asset_path())} + packaged = {n for n in self.names if n.startswith("core/assets/")} + self.assertEqual(sorted(expected - packaged), []) + self.assertEqual(sorted(packaged - expected), []) + self.assertGreater(len(expected), 2000) + + def test_the_files_a_report_needs_are_in_the_wheel(self): + for name in ( + "core/assets/fonts/IBMPlexSans-Regular.ttf", + "core/assets/fonts/IBMPlexMono-SemiBold.ttf", + "core/assets/fonts/OFL.txt", + "core/assets/template/index.html", + "core/assets/template/egress.html", + "core/assets/css/style.css", + "core/assets/img/logo/report_logo.png", + "core/assets/icons/misc/no_image.png", + "core/assets/icons/aws/Compute/EC2.png", + ): + with self.subTest(name): + self.assertIn(name, self.names) + + def test_the_wheel_has_no_loose_assets_directory(self): + self.assertFalse([n for n in self.names if n.startswith("assets/")]) + + +class AssetLookupTests(unittest.TestCase): + def test_the_bundled_files_exist(self): + for parts in ( + ("template", "index.html"), + ("template", "egress.html"), + ("fonts", "IBMPlexSans-Regular.ttf"), + ("img", "logo", "report_logo.png"), + ): + with self.subTest(parts=parts): + self.assertTrue(asset_path(*parts).is_file()) + + def test_the_assets_are_found_through_the_package_not_the_current_directory(self): + self.assertEqual(asset_path(), ROOT / "core" / "assets") + with tempfile.TemporaryDirectory() as empty, working_directory(empty): + self.assertTrue(asset_path("template", "index.html").is_file()) + self.assertTrue(asset_path("fonts", "IBMPlexSans-Regular.ttf").is_file()) + + def test_assets_that_are_not_on_the_file_system_are_a_clear_error(self): + with mock.patch.object(resources, "files", return_value=object()): + with self.assertRaises(RuntimeError) as ctx: + asset_path("fonts") + self.assertIn("not on the file system", str(ctx.exception)) + + def test_a_report_copy_wins_over_the_bundled_file(self): + with tempfile.TemporaryDirectory() as report: + copied = Path(report) / "assets" / "img" / "logo" / "report_logo.png" + copied.parent.mkdir(parents=True) + copied.write_bytes(b"copy") + + self.assertEqual(find_asset(report, "img/logo/report_logo.png"), copied) + self.assertEqual( + find_asset(report, "/icons/misc/no_image.png"), + asset_path("icons", "misc", "no_image.png"), + ) + + def test_find_asset_refuses_paths_that_leave_the_assets(self): + with tempfile.TemporaryDirectory() as report: + for relative in ("", "/", "../core/utils.py", "icons/../../main.py"): + with self.subTest(relative=relative): + self.assertIsNone(find_asset(report, relative)) + self.assertIsNone(find_asset(report, "icons/aws/missing.png")) + + +class RunFromAnotherDirectoryTests(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.tmp = Path(tmp.name) + patcher = mock.patch.dict( + os.environ, + { + "XDG_DATA_HOME": str(self.tmp / "data"), + "XDG_CACHE_HOME": str(self.tmp / "cache"), + }, + ) + patcher.start() + self.addCleanup(patcher.stop) + self.elsewhere = self.tmp / "elsewhere" + self.elsewhere.mkdir() + + def test_the_html_and_pdf_reports_are_written_from_an_empty_directory(self): + report = build_flow_run( + self.tmp / "report", resources=AWS_RESOURCES, costs=COSTS + ) + with working_directory(self.elsewhere): + perform_risk_assessment(1, report) + result = generate_report( + 2, + CONFIG["providerDetails"], + 1, + 1, + "Flow", + report, + str(Path(report) / "raw_data"), + ) + + self.assertTrue(result["success"], result.get("logs")) + self.assertGreater((Path(report) / "index.html").stat().st_size, 1000) + self.assertGreater((Path(report) / "report.pdf").stat().st_size, 5000) + self.assertEqual(os.listdir(self.elsewhere), []) + + def test_the_report_folder_still_gets_its_own_copy_of_the_assets(self): + report = self.tmp / "report" + (report / "data").mkdir(parents=True) + (report / "data" / "assessment.db").write_bytes(b"") + with working_directory(self.elsewhere): + copy_assets(str(report), 2) + + assets = report / "assets" + self.assertTrue((assets / "css" / "style.css").is_file()) + self.assertTrue((assets / "img" / "logo" / "report_logo.png").is_file()) + self.assertTrue(list((assets / "icons" / "aws").rglob("*.png"))) + self.assertFalse((assets / "icons" / "azure").exists()) + self.assertEqual(os.listdir(self.elsewhere), []) + + +class MissingFontTests(unittest.TestCase): + def test_a_missing_font_names_the_file_and_the_incomplete_installation(self): + with tempfile.TemporaryDirectory() as empty: + with ( + mock.patch.object(fonts, "FONT_DIR", Path(empty)), + mock.patch.object( + fonts.pdfmetrics, "getRegisteredFontNames", return_value=[] + ), + ): + with self.assertRaises(fonts.MissingFontError) as ctx: + fonts.register_fonts() + + message = str(ctx.exception) + self.assertIn("IBMPlexSans-Regular.ttf", message) + self.assertIn("installation is incomplete", message) + self.assertIsInstance(ctx.exception, FileNotFoundError) + + +class NoCurrentDirectoryLookupTests(unittest.TestCase): + PATTERNS = ( + re.compile(r"(?:join|Path)\(\s*[\"']assets[\"']"), + re.compile(r"[\"']assets/"), + re.compile(r"[\"']\./assets"), + re.compile(r"parents\[\d\]\s*/\s*[\"']assets[\"']"), + ) + # Paths inside the report folder, or links inside the HTML report. + ALLOWED = ('"icon", "assets/icons/default.png"',) + + def sources(self): + for name in SOURCE_DIRS: + for path in sorted((ROOT / name).rglob("*.py")): + yield path + yield ROOT / "main.py" + + def test_no_code_reads_assets_relative_to_the_current_directory(self): + found = [] + for path in self.sources(): + for number, line in enumerate( + path.read_text(encoding="utf-8").splitlines(), 1 + ): + if any(allowed in line for allowed in self.ALLOWED): + continue + if any(pattern.search(line) for pattern in self.PATTERNS): + found.append(f"{path.relative_to(ROOT)}:{number}: {line.strip()}") + self.assertEqual(found, []) + + def test_the_check_would_catch_the_old_lookups(self): + for line in ( + 'os.path.join("assets", "template", "index.html")', + 'Path("assets") / "fonts"', + 'src = "assets/css"', + 'Path(__file__).resolve().parents[2] / "assets" / "fonts"', + ): + with self.subTest(line=line): + self.assertTrue(any(p.search(line) for p in self.PATTERNS)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_utils_report_egress.py b/tests/test_utils_report_egress.py index 7e3b6f8..31d8e35 100644 --- a/tests/test_utils_report_egress.py +++ b/tests/test_utils_report_egress.py @@ -1,7 +1,6 @@ # tests/test_utils_report_egress.py import json import os -import pathlib import sqlite3 import tempfile import unittest @@ -12,6 +11,7 @@ from tests.report_fixtures import stage_report_assets +from core.resources import asset_path from core.utils_egress import GIB, write_egress_inventory from utils.data import active_catalogue_path from core.utils_report_egress import ( @@ -211,7 +211,7 @@ def test_allocated_categories_have_a_colour_and_a_label(self): self.assertIsNotNone(info["label"], key) def test_template_styles_every_category_badge(self): - css = pathlib.Path("assets/template/egress.html").read_text(encoding="utf-8") + css = asset_path("template", "egress.html").read_text(encoding="utf-8") for key in CATEGORIES: self.assertIn(f".category-{key} {{", css, f"no badge rule for {key!r}") From 1a77eb768b5bdd4e1a2d4898b6d37a2071ae9ba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Sat, 24 Oct 2026 06:37:33 +0200 Subject: [PATCH 10/61] refresh dependencies and move to the new azure sdks --- core/engine.py | 9 +- core/utils_azure.py | 49 +++- core/utils_egress_azure.py | 6 +- main.py | 10 +- pyproject.toml | 9 +- requirements-dev.txt | 2 +- requirements.txt | 9 +- tests/test_azure_sdk_shapes.py | 452 +++++++++++++++++++++++++++++++ tests/test_utils_egress_azure.py | 2 +- 9 files changed, 519 insertions(+), 29 deletions(-) create mode 100644 tests/test_azure_sdk_shapes.py diff --git a/core/engine.py b/core/engine.py index 54233c1..353f940 100644 --- a/core/engine.py +++ b/core/engine.py @@ -6,14 +6,17 @@ from datetime import datetime, timezone, timedelta from typing import Any from azure.identity import ClientSecretCredential -from azure.mgmt.resource import ResourceManagementClient from azure.core.exceptions import ClientAuthenticationError from azure.mgmt.authorization import AuthorizationManagementClient from botocore.exceptions import NoCredentialsError from .utils import copy_assets from .utils_aws import build_aws_resource_inventory, build_aws_cost_inventory -from .utils_azure import build_azure_resource_inventory, build_azure_cost_inventory +from .utils_azure import ( + build_azure_cost_inventory, + build_azure_resource_inventory, + create_resource_client, +) from .utils_db import connect, load_data from .utils_tfstate import build_tfstate_resource_inventory from .report.context import build_context, collect_provenance @@ -43,7 +46,7 @@ def verify_credentials( client_id=provider_details["clientId"], client_secret=provider_details["clientSecret"], ) - resource_client = ResourceManagementClient( + resource_client = create_resource_client( credential, provider_details["subscriptionId"] ) list( diff --git a/core/utils_azure.py b/core/utils_azure.py index 2820568..fc6b098 100644 --- a/core/utils_azure.py +++ b/core/utils_azure.py @@ -8,7 +8,8 @@ from dateutil.relativedelta import relativedelta from collections import defaultdict from azure.identity import ClientSecretCredential -from azure.mgmt.resource import ResourceManagementClient +from azure.mgmt.resource.resources import ResourceManagementClient +from azure.mgmt.resource.subscriptions import SubscriptionClient from azure.mgmt.costmanagement import CostManagementClient from azure.mgmt.costmanagement.models import QueryDefinition, TimeframeType from azure.core.exceptions import AzureError, ClientAuthenticationError @@ -18,12 +19,46 @@ logger = logging.getLogger("core.engine.azure") logging.getLogger("azure").setLevel(logging.WARNING) +MANAGEMENT_URL = "https://management.azure.com" + + +def create_resource_client(credential: Any, subscription_id: str) -> Any: + return ResourceManagementClient(credential, subscription_id) + + +def create_subscription_client(credential: Any) -> Any: + return SubscriptionClient(credential) + + +def create_cost_client(credential: Any) -> Any: + return CostManagementClient(credential, base_url=MANAGEMENT_URL) + + +def plain_json(value: Any) -> Any: + # SDK models are mappings (and datetimes appear in some of them); the raw + # data files hold plain JSON. + if hasattr(value, "keys"): + return {key: plain_json(value[key]) for key in value.keys()} + if isinstance(value, (list, tuple)): + return [plain_json(item) for item in value] + if isinstance(value, (datetime, date)): + return value.isoformat() + return value + + +def cost_result_dict(result: Any) -> dict[str, Any]: + # The REST shape nests the columns and rows under "properties"; the raw + # file keeps them at the top level. + data = plain_json(result.as_dict()) + properties = data.pop("properties", None) or {} + return {**data, **properties} + def is_resource_inventory_empty( credential: Any, subscription_id: str, resource_group_name: str ) -> bool: try: - resource_client = ResourceManagementClient(credential, subscription_id) + resource_client = create_resource_client(credential, subscription_id) # logger.info("Checking Azure resource inventory...") resources = list( resource_client.resources.list_by_resource_group(resource_group_name) @@ -70,13 +105,13 @@ def build_azure_resource_inventory( ) return - resource_client = ResourceManagementClient(credential, subscription_id) + resource_client = create_resource_client(credential, subscription_id) # Fetch resources and serialize to raw JSON resources = list( resource_client.resources.list_by_resource_group(resource_group_name) ) - raw_data = [resource.serialize(True) for resource in resources] + raw_data = [plain_json(resource) for resource in resources] # Save raw data to a JSON file raw_file_path = os.path.join(raw_data_path, "resource_inventory_raw_data.json") @@ -174,9 +209,7 @@ def build_azure_cost_inventory( client_id=provider_details["clientId"], client_secret=provider_details["clientSecret"], ) - cost_management_client = CostManagementClient( - credential, base_url="https://management.azure.com" - ) + cost_management_client = create_cost_client(credential) db_path = os.path.join(report_path, "data", "assessment.db") @@ -206,7 +239,7 @@ def build_azure_cost_inventory( raw_data_path, "cost_inventory_raw_data.json" ) with open(cost_inventory_raw_path, "w", encoding="utf-8") as raw_file: - json.dump(cost_data.as_dict(), raw_file, indent=4) + json.dump(cost_result_dict(cost_data), raw_file, indent=4) # Insert structured cost data into SQLite currency = "USD" diff --git a/core/utils_egress_azure.py b/core/utils_egress_azure.py index fb360bb..bfcfbc4 100644 --- a/core/utils_egress_azure.py +++ b/core/utils_egress_azure.py @@ -4,8 +4,8 @@ from typing import Any from datetime import datetime, timedelta, timezone from azure.identity import ClientSecretCredential -from azure.mgmt.resource import ResourceManagementClient +from .utils_azure import create_resource_client from .utils_egress import GIB, format_bytes, load_egress_registry, new_row logger = logging.getLogger("core.engine.egress.azure") @@ -187,7 +187,7 @@ def _collect_allocated_size( row["flags"].append("allocated (upper bound)") full_resource = resource_client.resources.get_by_id( - resource.id, entry["api_version"] + resource.id, api_version=entry["api_version"] ) size_gb = (full_resource.properties or {}).get(entry["size_property"]) if size_gb: @@ -289,7 +289,7 @@ def collect_azure_egress( subscription_id = provider_details["subscriptionId"] resource_group_name = provider_details["resourceGroupName"] - resource_client = ResourceManagementClient(credential, subscription_id) + resource_client = create_resource_client(credential, subscription_id) resources = list( resource_client.resources.list_by_resource_group(resource_group_name) ) diff --git a/main.py b/main.py index 46a5b1c..07d0251 100644 --- a/main.py +++ b/main.py @@ -14,7 +14,6 @@ from datetime import datetime from botocore.exceptions import NoCredentialsError, ProfileNotFound from azure.identity import DefaultAzureCredential, ClientSecretCredential -from azure.mgmt.resource import SubscriptionClient, ResourceManagementClient # Import the functions from core.engine import ( @@ -25,6 +24,7 @@ perform_risk_assessment, generate_report, ) +from core.utils_azure import create_resource_client, create_subscription_client from core.utils_egress import estimate_egress from core.utils_report_egress import ( generate_egress_html_report, @@ -379,7 +379,7 @@ def _azure_provider_from_cli() -> dict: credential = _azure_cli_credential() try: tenant_id = input("Enter Azure Tenant ID: ").strip() - subscription_client = SubscriptionClient(credential) + subscription_client = create_subscription_client(credential) subscriptions = list(subscription_client.subscriptions.list()) if not subscriptions: logger.error("No subscriptions found for the provided Azure credentials.") @@ -391,7 +391,7 @@ def _azure_provider_from_cli() -> dict: selected_subscription = select_subscription(subscriptions) subscription_id = selected_subscription.subscription_id - resource_client = ResourceManagementClient(credential, subscription_id) + resource_client = create_resource_client(credential, subscription_id) resource_groups = list(resource_client.resource_groups.list()) if not resource_groups: logger.error("No resource groups found in the selected subscription.") @@ -425,7 +425,7 @@ def _azure_provider_from_prompt() -> dict: credential = ClientSecretCredential( tenant_id=tenant_id, client_id=client_id, client_secret=client_secret ) - subscription_client = SubscriptionClient(credential) + subscription_client = create_subscription_client(credential) subscriptions = list(subscription_client.subscriptions.list()) if not subscriptions: @@ -437,7 +437,7 @@ def _azure_provider_from_prompt() -> dict: selected_subscription = select_subscription(subscriptions) subscription_id = selected_subscription.subscription_id - resource_client = ResourceManagementClient(credential, subscription_id) + resource_client = create_resource_client(credential, subscription_id) resource_groups = list(resource_client.resource_groups.list()) if not resource_groups: console.print( diff --git a/pyproject.toml b/pyproject.toml index 1c6f0bf..66c5514 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,11 +10,12 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "azure-identity==1.26.0", - "azure-mgmt-resource==24.0.0", + "azure-mgmt-resource==26.0.0", + "azure-mgmt-resource-subscriptions==1.0.0", "azure-mgmt-authorization==4.0.0", - "azure-mgmt-costmanagement==4.0.1", - "boto3==1.43.117", - "botocore==1.43.117", + "azure-mgmt-costmanagement==5.0.0", + "boto3==1.43.118", + "botocore==1.43.118", "rich==15.0.0", "Jinja2==3.1.6", "reportlab==5.1.0", diff --git a/requirements-dev.txt b/requirements-dev.txt index e0c0617..6db042f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,6 @@ -r requirements.txt black==26.10.1 -ruff==0.17.1 +ruff==0.17.2 jsonschema==4.26.0 PyYAML==6.0.3 pypdf==6.21.2 diff --git a/requirements.txt b/requirements.txt index 1f484de..6850a8d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,10 @@ azure-identity==1.26.0 -azure-mgmt-resource==24.0.0 +azure-mgmt-resource==26.0.0 +azure-mgmt-resource-subscriptions==1.0.0 azure-mgmt-authorization==4.0.0 -azure-mgmt-costmanagement==4.0.1 -boto3==1.43.117 -botocore==1.43.117 +azure-mgmt-costmanagement==5.0.0 +boto3==1.43.118 +botocore==1.43.118 rich==15.0.0 Jinja2==3.1.6 reportlab==5.1.0 diff --git a/tests/test_azure_sdk_shapes.py b/tests/test_azure_sdk_shapes.py new file mode 100644 index 0000000..01de7ef --- /dev/null +++ b/tests/test_azure_sdk_shapes.py @@ -0,0 +1,452 @@ +import json +import sqlite3 +import tempfile +import time +import unittest +from datetime import date +from pathlib import Path +from unittest import mock + +from azure.core.credentials import AccessToken +from dateutil.relativedelta import relativedelta + +from core import utils_azure +from core.utils_azure import ( + build_azure_cost_inventory, + build_azure_resource_inventory, + cost_result_dict, + create_cost_client, + create_resource_client, + create_subscription_client, + is_resource_inventory_empty, + plain_json, +) +from core.utils_egress import GIB +from core.utils_egress_azure import build_egress_inventory +from tests.http_fixtures import fake_http, make_response +from utils.azure import select_resource_group, select_subscription + +SUBSCRIPTION = "00000000-0000-0000-0000-000000000000" +GROUP = "rg-demo" +GROUP_ID = f"/subscriptions/{SUBSCRIPTION}/resourceGroups/{GROUP}" +STORAGE_ID = f"{GROUP_ID}/providers/Microsoft.Storage/storageAccounts/stdemo" +NETWORK_ID = f"{GROUP_ID}/providers/Microsoft.Network/virtualNetworks/vnet" +DISK_ID = f"{GROUP_ID}/providers/Microsoft.Compute/disks/disk1" + +STORAGE_ACCOUNT = { + "id": STORAGE_ID, + "name": "stdemo", + "type": "Microsoft.Storage/storageAccounts", + "location": "westeurope", + "tags": {"env": "demo"}, + "sku": {"name": "Standard_GRS", "tier": "Standard"}, + "kind": "StorageV2", + "identity": { + "type": "SystemAssigned", + "principalId": "11111111-1111-1111-1111-111111111111", + "tenantId": "22222222-2222-2222-2222-222222222222", + }, + "managedBy": f"{GROUP_ID}/providers/Microsoft.Web/sites/app", + "createdTime": "2026-01-02T03:04:05.0000000Z", + "changedTime": "2026-02-03T04:05:06.0000000Z", + "provisioningState": "Succeeded", +} +VIRTUAL_NETWORK = { + "id": NETWORK_ID, + "name": "vnet", + "type": "Microsoft.Network/virtualNetworks", + "location": " WestEurope ", +} +VIRTUAL_NETWORK_TWO = dict(VIRTUAL_NETWORK, id=NETWORK_ID + "-2", name="vnet-2") +DISK = { + "id": DISK_ID, + "name": "disk1", + "type": "Microsoft.Compute/disks", + "location": "westeurope", +} + + +class FakeCredential: + def get_token(self, *scopes, **kwargs): + return AccessToken("not-a-token", int(time.time()) + 3600) + + +def json_reply(body, status=200): + response = make_response(status, json_body=body) + # The SDK's transport sets a flag on, and closes, the urllib3 response + response.raw = mock.MagicMock() + response._content_consumed = True + return response + + +def listing(*resources): + return json_reply({"value": list(resources)}) + + +class ShapeTestCase(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.tmp = Path(tmp.name) + self.credential = FakeCredential() + self.details = { + "credential": self.credential, + "subscriptionId": SUBSCRIPTION, + "resourceGroupName": GROUP, + } + # The mapping of resource types is cached on the function for a run + self.addCleanup(self.forget_type_cache) + self.forget_type_cache() + + def forget_type_cache(self): + if hasattr(build_azure_resource_inventory, "_resource_type_cache"): + del build_azure_resource_inventory._resource_type_cache + + def report(self, *tables): + report = self.tmp / "report" + (report / "data").mkdir(parents=True, exist_ok=True) + raw = self.tmp / "raw" + raw.mkdir(exist_ok=True) + conn = sqlite3.connect(report / "data" / "assessment.db") + conn.executescript(";".join(tables)) + conn.commit() + conn.close() + return str(report), str(raw) + + def rows(self, report, sql): + conn = sqlite3.connect(Path(report) / "data" / "assessment.db") + try: + return conn.execute(sql).fetchall() + finally: + conn.close() + + +class ClientSurfaceTests(ShapeTestCase): + def test_the_factories_build_clients_with_the_operations_the_cli_calls(self): + resource_client = create_resource_client(self.credential, SUBSCRIPTION) + self.assertTrue(callable(resource_client.resources.list_by_resource_group)) + self.assertTrue(callable(resource_client.resources.get_by_id)) + self.assertTrue(callable(resource_client.resource_groups.list)) + + subscription_client = create_subscription_client(self.credential) + self.assertTrue(callable(subscription_client.subscriptions.list)) + + cost_client = create_cost_client(self.credential) + self.assertTrue(callable(cost_client.query.usage)) + + def test_the_clients_come_from_the_packages_the_sdk_now_splits_them_into(self): + self.assertEqual( + type(create_resource_client(self.credential, SUBSCRIPTION)).__module__, + "azure.mgmt.resource.resources._client", + ) + self.assertEqual( + type(create_subscription_client(self.credential)).__module__, + "azure.mgmt.resource.subscriptions._client", + ) + + def test_the_authorization_client_still_has_role_assignments(self): + from azure.mgmt.authorization import AuthorizationManagementClient + + client = AuthorizationManagementClient(self.credential, SUBSCRIPTION) + self.assertTrue(callable(client.role_assignments.list_for_scope)) + + +class ListedResourceTests(ShapeTestCase): + def listed(self, *resources): + client = create_resource_client(self.credential, SUBSCRIPTION) + with fake_http(listing(*resources)) as fake: + return list(client.resources.list_by_resource_group(GROUP)), fake + + def test_a_listed_resource_exposes_what_the_inventory_reads(self): + resources, fake = self.listed(STORAGE_ACCOUNT, VIRTUAL_NETWORK) + + storage, network = resources + self.assertEqual(storage.type, "Microsoft.Storage/storageAccounts") + self.assertEqual(storage.location, "westeurope") + self.assertEqual(storage.id, STORAGE_ID) + self.assertEqual(storage.name, "stdemo") + self.assertEqual(network.location, " WestEurope ") + self.assertIn( + f"/resourcegroups/{GROUP}/resources", fake.requests[0].url.lower() + ) + self.assertEqual(fake.requests[0].method, "GET") + + def test_the_sku_name_is_read_the_way_the_egress_collector_reads_it(self): + (storage,), _ = self.listed(STORAGE_ACCOUNT) + self.assertEqual( + getattr(getattr(storage, "sku", None), "name", None), "Standard_GRS" + ) + + (network,), _ = self.listed(VIRTUAL_NETWORK) + self.assertIsNone(getattr(getattr(network, "sku", None), "name", None)) + + def test_a_resource_without_a_location_reads_as_none(self): + (resource,), _ = self.listed({k: v for k, v in DISK.items() if k != "location"}) + self.assertIsNone(resource.location) + + def test_the_raw_data_form_is_plain_json_with_the_rest_names(self): + resources, _ = self.listed(STORAGE_ACCOUNT, VIRTUAL_NETWORK) + + plain = json.loads(json.dumps([plain_json(r) for r in resources])) + + self.assertEqual(plain[0], STORAGE_ACCOUNT) + self.assertEqual(plain[1], VIRTUAL_NETWORK) + + def test_plain_json_turns_models_lists_and_dates_into_json_types(self): + (storage,), _ = self.listed(STORAGE_ACCOUNT) + self.assertEqual( + plain_json(storage.identity)["principalId"], + STORAGE_ACCOUNT["identity"]["principalId"], + ) + self.assertEqual(plain_json((1, [date(2026, 1, 2)])), [1, ["2026-01-02"]]) + self.assertEqual(plain_json(None), None) + + +class ResourceInventoryTests(ShapeTestCase): + TABLES = ( + "CREATE TABLE resourcetype (id INTEGER PRIMARY KEY, csp INTEGER, code TEXT, " + "name TEXT, icon TEXT, status TEXT)", + "INSERT INTO resourcetype VALUES " + "(11, 1, 'Microsoft.Storage/storageAccounts', 'Storage Account', '/i.png', 't')", + "INSERT INTO resourcetype VALUES " + "(12, 1, 'microsoft.network/virtualnetworks', 'Virtual Network', '/i.png', 't')", + "CREATE TABLE resource_inventory (id INTEGER PRIMARY KEY AUTOINCREMENT, " + "resource_type INTEGER NOT NULL, location TEXT NOT NULL, " + "count INTEGER NOT NULL, UNIQUE(resource_type, location))", + ) + + def test_types_and_locations_are_counted_as_before(self): + report, raw = self.report(*self.TABLES) + resources = [STORAGE_ACCOUNT, VIRTUAL_NETWORK, VIRTUAL_NETWORK_TWO] + # One listing for the emptiness check, one for the inventory + with fake_http(listing(*resources), listing(*resources)) as fake: + build_azure_resource_inventory(1, self.details, report, raw) + + self.assertEqual( + self.rows( + report, + "SELECT resource_type, location, count FROM resource_inventory ORDER BY 1", + ), + [(11, "westeurope", 1), (12, "westeurope", 2)], + ) + self.assertEqual(len(fake.requests), 2) + + def test_the_raw_file_holds_the_rest_shaped_resources(self): + report, raw = self.report(*self.TABLES) + with fake_http(listing(STORAGE_ACCOUNT), listing(STORAGE_ACCOUNT)): + build_azure_resource_inventory(1, self.details, report, raw) + + saved = json.loads( + (Path(raw) / "resource_inventory_raw_data.json").read_text(encoding="utf-8") + ) + self.assertEqual(saved, [STORAGE_ACCOUNT]) + + def test_an_empty_group_is_detected(self): + with fake_http(listing()): + self.assertTrue( + is_resource_inventory_empty(self.credential, SUBSCRIPTION, GROUP) + ) + with fake_http(listing(DISK)): + self.assertFalse( + is_resource_inventory_empty(self.credential, SUBSCRIPTION, GROUP) + ) + + +class SelectionTests(ShapeTestCase): + def test_subscriptions_and_resource_groups_are_read_by_the_selection_prompts(self): + subscription_client = create_subscription_client(self.credential) + subscription = { + "id": f"/subscriptions/{SUBSCRIPTION}", + "subscriptionId": SUBSCRIPTION, + "displayName": "Demo subscription", + "state": "Enabled", + } + with fake_http(listing(subscription)): + subscriptions = list(subscription_client.subscriptions.list()) + + resource_client = create_resource_client(self.credential, SUBSCRIPTION) + group = {"id": GROUP_ID, "name": GROUP, "location": "westeurope"} + with fake_http(listing(group)): + groups = list(resource_client.resource_groups.list()) + + with ( + mock.patch("builtins.input", return_value="1"), + mock.patch("utils.azure.console.print") as printed, + ): + chosen = select_subscription(subscriptions) + name = select_resource_group(groups) + + self.assertEqual(chosen.subscription_id, SUBSCRIPTION) + self.assertEqual(chosen.display_name, "Demo subscription") + self.assertEqual(name, GROUP) + self.assertIn( + f"1. Demo subscription ({SUBSCRIPTION})", + [call.args[0] for call in printed.call_args_list], + ) + + +class CostQueryTests(ShapeTestCase): + TABLES = ( + "CREATE TABLE cost_inventory (id INTEGER PRIMARY KEY AUTOINCREMENT, " + "month TEXT NOT NULL, cost REAL NOT NULL, currency TEXT NOT NULL, " + "UNIQUE(month))", + ) + + def months(self, count): + first = date.today().replace(day=1) + return [first - relativedelta(months=back) for back in range(count)] + + def result(self, rows): + return json_reply( + { + "id": "providers/Microsoft.CostManagement/query/demo", + "name": "demo", + "type": "Microsoft.CostManagement/query", + "properties": { + "columns": [ + {"name": "Cost", "type": "Number"}, + {"name": "UsageDate", "type": "Datetime"}, + {"name": "Currency", "type": "String"}, + ], + "rows": rows, + }, + } + ) + + def test_the_query_is_sent_with_its_period_and_dataset(self): + report, raw = self.report(*self.TABLES) + with fake_http(self.result([])) as fake: + build_azure_cost_inventory(1, self.details, report, raw) + + (request,) = fake.requests + self.assertEqual(request.method, "POST") + self.assertIn( + f"{GROUP_ID}/providers/Microsoft.CostManagement/query", request.url + ) + body = json.loads(request.kwargs["data"]) + self.assertEqual(body["type"], "Usage") + self.assertEqual(body["timeframe"], "Custom") + self.assertEqual( + sorted(body["timePeriod"]), + ["from", "to"], + ) + self.assertEqual(body["dataset"]["granularity"], "Monthly") + self.assertEqual( + body["dataset"]["aggregation"], + {"totalCost": {"name": "Cost", "function": "Sum"}}, + ) + + def test_cost_rows_become_months_and_the_gaps_are_filled_with_zero(self): + report, raw = self.report(*self.TABLES) + current, previous = self.months(2) + rows = [ + [100.5, f"{previous.isoformat()}T00:00:00", "EUR"], + [20.25, f"{current.isoformat()}T00:00:00", "EUR"], + ] + with fake_http(self.result(rows)): + build_azure_cost_inventory(1, self.details, report, raw) + + stored = dict( + (month, (cost, currency)) + for month, cost, currency in self.rows( + report, "SELECT month, cost, currency FROM cost_inventory" + ) + ) + self.assertEqual(stored[previous.isoformat()], (100.5, "EUR")) + self.assertEqual(stored[current.isoformat()], (20.25, "EUR")) + zero_months = [m for m, (cost, _) in stored.items() if cost == 0] + self.assertEqual(len(zero_months), 4) + self.assertEqual(len(stored), 6) + + def test_the_raw_file_keeps_the_columns_and_rows_at_the_top(self): + report, raw = self.report(*self.TABLES) + current = self.months(1)[0] + rows = [[1.5, f"{current.isoformat()}T00:00:00", "USD"]] + with fake_http(self.result(rows)): + build_azure_cost_inventory(1, self.details, report, raw) + + saved = json.loads( + (Path(raw) / "cost_inventory_raw_data.json").read_text(encoding="utf-8") + ) + self.assertEqual(saved["rows"], rows) + self.assertEqual( + [c["name"] for c in saved["columns"]], ["Cost", "UsageDate", "Currency"] + ) + self.assertNotIn("properties", saved) + + def test_cost_result_dict_flattens_the_properties(self): + client = create_cost_client(self.credential) + with fake_http(self.result([[1, "2026-01-01T00:00:00", "USD"]])): + result = client.query.usage( + GROUP_ID, + utils_azure.QueryDefinition( + type="Usage", + timeframe=utils_azure.TimeframeType.CUSTOM, + time_period={ + "from": "2026-01-01T00:00:00Z", + "to": "2026-02-01T00:00:00Z", + }, + dataset={"granularity": "Monthly"}, + ), + ) + + self.assertEqual(result.rows, [[1, "2026-01-01T00:00:00", "USD"]]) + flat = cost_result_dict(result) + self.assertEqual(flat["rows"], result.rows) + self.assertIn("columns", flat) + self.assertNotIn("properties", flat) + + +REGISTRY = { + "microsoft.storage/storageaccounts": { + "category": "object", + "label": "Storage Account", + "strategy": "storage_account_metrics", + }, + "microsoft.compute/disks": { + "category": "block", + "label": "Managed Disks", + "strategy": "allocated_size_property", + "api_version": "2024-03-02", + "size_property": "diskSizeGB", + }, +} + + +@mock.patch("core.utils_egress_azure.load_egress_registry", return_value=REGISTRY) +class EgressSizingTests(ShapeTestCase): + def test_an_allocated_size_is_read_with_the_api_version_as_a_keyword(self, _): + client = create_resource_client(self.credential, SUBSCRIPTION) + disk = dict(DISK, properties={"diskSizeGB": 128}) + with fake_http(listing(DISK), json_reply(disk)) as fake: + resources = list(client.resources.list_by_resource_group(GROUP)) + rows, findings = build_egress_inventory(self.credential, client, resources) + + self.assertEqual(findings, []) + self.assertEqual(rows[0]["size_bytes"], 128 * GIB) + self.assertIn("allocated (upper bound)", rows[0]["flags"]) + lookup = fake.requests[1] + self.assertEqual(lookup.method, "GET") + self.assertIn(DISK_ID, lookup.url) + self.assertIn("api-version=2024-03-02", lookup.url) + + def test_a_storage_account_sku_marks_geo_replication(self, _): + client = create_resource_client(self.credential, SUBSCRIPTION) + with fake_http(listing(STORAGE_ACCOUNT)): + resources = list(client.resources.list_by_resource_group(GROUP)) + metrics = [ + {"UsedCapacity": [{"dimension": None, "value": float(GIB)}]}, + {"BlobCapacity": []}, + ] + with mock.patch( + "core.utils_egress_azure.fetch_monitor_metrics", side_effect=metrics + ): + rows, findings = build_egress_inventory(self.credential, client, resources) + + self.assertEqual(rows[0]["size_bytes"], GIB) + self.assertIn("geo-replicated (Standard_GRS)", rows[0]["notes"]) + self.assertEqual([f["severity"] for f in findings], ["info"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_utils_egress_azure.py b/tests/test_utils_egress_azure.py index 97c8ff6..caaf9bd 100644 --- a/tests/test_utils_egress_azure.py +++ b/tests/test_utils_egress_azure.py @@ -367,7 +367,7 @@ def test_vault_produces_warning_finding_and_no_size(self, mock_fetch): class CollectAzureEgressTests(unittest.TestCase): @patch("core.utils_egress_azure.fetch_monitor_metrics") - @patch("core.utils_egress_azure.ResourceManagementClient") + @patch("core.utils_azure.ResourceManagementClient") def test_returns_rows_and_archive_tiers(self, mock_rmc_cls, mock_fetch): mock_fetch.side_effect = [ {"UsedCapacity": [{"dimension": None, "value": float(10 * GIB)}]}, From 553a55d1e5d0f9a4ce666f68e729498cd9df419e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Sat, 24 Oct 2026 06:45:06 +0200 Subject: [PATCH 11/61] count azure resources without a location as global --- core/utils_azure.py | 3 ++- tests/test_azure_sdk_shapes.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core/utils_azure.py b/core/utils_azure.py index fc6b098..c512085 100644 --- a/core/utils_azure.py +++ b/core/utils_azure.py @@ -134,7 +134,8 @@ def build_azure_resource_inventory( aggregated_resources = defaultdict(int) for resource in resources: resource_type_code = resource.type.strip().lower() - resource_location = resource.location.strip().lower() + # Non-regional resources have no location; Azure calls them "global" + resource_location = (resource.location or "").strip().lower() or "global" aggregated_resources[(resource_type_code, resource_location)] += 1 # Insert data into SQLite diff --git a/tests/test_azure_sdk_shapes.py b/tests/test_azure_sdk_shapes.py index 01de7ef..6af3701 100644 --- a/tests/test_azure_sdk_shapes.py +++ b/tests/test_azure_sdk_shapes.py @@ -231,6 +231,22 @@ def test_types_and_locations_are_counted_as_before(self): ) self.assertEqual(len(fake.requests), 2) + def test_a_resource_without_a_location_is_counted_as_global(self): + report, raw = self.report(*self.TABLES) + global_resource = {k: v for k, v in VIRTUAL_NETWORK.items() if k != "location"} + blank_resource = dict(VIRTUAL_NETWORK, id=NETWORK_ID + "-2", location=" ") + resources = [STORAGE_ACCOUNT, global_resource, blank_resource] + with fake_http(listing(*resources), listing(*resources)): + build_azure_resource_inventory(1, self.details, report, raw) + + self.assertEqual( + self.rows( + report, + "SELECT resource_type, location, count FROM resource_inventory ORDER BY 1, 2", + ), + [(11, "westeurope", 1), (12, "global", 2)], + ) + def test_the_raw_file_holds_the_rest_shaped_resources(self): report, raw = self.report(*self.TABLES) with fake_http(listing(STORAGE_ACCOUNT), listing(STORAGE_ACCOUNT)): From d32a8db71b5cf2032e935b52110a6058e0936b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Sat, 24 Oct 2026 07:12:21 +0200 Subject: [PATCH 12/61] mark release/2.0 builds as 2.0.0.dev0 --- tests/test_api_v2_client.py | 6 ++++++ utils/version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_api_v2_client.py b/tests/test_api_v2_client.py index 5b20136..52a4400 100644 --- a/tests/test_api_v2_client.py +++ b/tests/test_api_v2_client.py @@ -450,6 +450,12 @@ def test_the_default_cache_is_in_the_users_cache_directory(self): class PolicyTests(ClientTestCase): + def setUp(self): + super().setUp() + # The policies below are written against this client version, not the + # one in utils/version.py, which changes with every release + self.enterContext(patch.object(api_v2, "__version__", "1.1.7")) + def test_a_blocked_or_too_old_client_is_refused_before_the_push(self): for label, clients in ( ("blocked", {"blocked": ["1.1.7"]}), diff --git a/utils/version.py b/utils/version.py index bf78826..9fc20a7 100644 --- a/utils/version.py +++ b/utils/version.py @@ -1 +1 @@ -__version__ = "1.1.7" +__version__ = "2.0.0.dev0" From c9cddb6b0cf9d448363cc04eb3794c83aa622ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Sat, 24 Oct 2026 07:35:49 +0200 Subject: [PATCH 13/61] make azure imports lazy and fix upgrades from 1.x --- .github/workflows/pr-checks.yml | 86 ++++++++ README.md | 2 + core/engine.py | 28 ++- core/utils_azure.py | 51 +++-- core/utils_egress_azure.py | 9 +- main.py | 28 ++- pyproject.toml | 2 +- requirements.txt | 2 +- tests/test_azure_sdk_shapes.py | 8 +- tests/test_lazy_azure.py | 323 +++++++++++++++++++++++++++++++ tests/test_utils_and_main.py | 14 +- tests/test_utils_azure.py | 16 +- tests/test_utils_egress_azure.py | 2 +- utils/azure.py | 78 +++++++- 14 files changed, 586 insertions(+), 63 deletions(-) create mode 100644 tests/test_lazy_azure.py diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index cb61abc..76c8c64 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -102,6 +102,92 @@ jobs: "$cloudexit" --version "$cloudexit" config path + "$RUNNER_TEMP/venv/bin/python" - <<'PY' + from core.utils_azure import ( + create_cost_client, + create_resource_client, + create_subscription_client, + ) + + + class Credential: + def get_token(self, *scopes, **kwargs): + raise RuntimeError("no network in this check") + + + create_resource_client(Credential(), "00000000-0000-0000-0000-000000000000") + create_subscription_client(Credential()) + create_cost_client(Credential()) + print("the Azure clients import and build") + PY + "$cloudexit" aws \ + --tfstate "$GITHUB_WORKSPACE/tests/fixtures/smoke/aws.tfstate" \ + --catalogue "$RUNNER_TEMP/smoke.db" \ + --non-interactive + + report="$(ls -d reports/*/ | head -n 1)" + test -f "${report}index.html" + test -f "${report}report.pdf" + test -d "${report}assets/css" + test -d "${report}assets/img" + test -n "$(find "${report}assets/icons/aws" -name '*.png' | head -n 1)" + test "$(wc -c < "${report}report.pdf")" -gt 10000 + + upgrade-smoke: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install the 1.1.7 environment + run: | + python -m venv "$RUNNER_TEMP/venv" + "$RUNNER_TEMP/venv/bin/python" -m pip install --upgrade pip + git show v1.1.7:requirements.txt > "$RUNNER_TEMP/req-1.1.7.txt" + "$RUNNER_TEMP/venv/bin/python" -m pip install -r "$RUNNER_TEMP/req-1.1.7.txt" + + - name: Upgrade it to this branch + run: '"$RUNNER_TEMP/venv/bin/python" -m pip install .' + + - name: Build the smoke dataset + run: '"$RUNNER_TEMP/venv/bin/python" -m tests.make_smoke_dataset "$RUNNER_TEMP/smoke.db"' + + - name: Run from an unrelated directory + env: + ESC_EXIT_STRATEGY: "1" + ESC_ASSESSMENT_TYPE: "1" + run: | + cloudexit="$RUNNER_TEMP/venv/bin/cloudexit" + workdir="$(mktemp -d)" + cd "$workdir" + + "$cloudexit" --version + "$cloudexit" config path + "$RUNNER_TEMP/venv/bin/python" - <<'PY' + from core.utils_azure import ( + create_cost_client, + create_resource_client, + create_subscription_client, + ) + + + class Credential: + def get_token(self, *scopes, **kwargs): + raise RuntimeError("no network in this check") + + + create_resource_client(Credential(), "00000000-0000-0000-0000-000000000000") + create_subscription_client(Credential()) + create_cost_client(Credential()) + print("the Azure clients import and build") + PY "$cloudexit" aws \ --tfstate "$GITHUB_WORKSPACE/tests/fixtures/smoke/aws.tfstate" \ --catalogue "$RUNNER_TEMP/smoke.db" \ diff --git a/README.md b/README.md index dfeea92..570150d 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ python -m venv .venv && source .venv/bin/activate pip install -r requirements.txt ``` +Upgrading from 1.x? Install into a new virtual environment, not over the old one. + Run your first assessment: ```bash diff --git a/core/engine.py b/core/engine.py index 353f940..ba9f2ec 100644 --- a/core/engine.py +++ b/core/engine.py @@ -5,9 +5,6 @@ import boto3 from datetime import datetime, timezone, timedelta from typing import Any -from azure.identity import ClientSecretCredential -from azure.core.exceptions import ClientAuthenticationError -from azure.mgmt.authorization import AuthorizationManagementClient from botocore.exceptions import NoCredentialsError from .utils import copy_assets @@ -15,8 +12,10 @@ from .utils_azure import ( build_azure_cost_inventory, build_azure_resource_inventory, + create_credential, create_resource_client, ) +from utils.azure import azure_import from .utils_db import connect, load_data from .utils_tfstate import build_tfstate_resource_inventory from .report.context import build_context, collect_provenance @@ -39,13 +38,11 @@ def verify_credentials( logs = "" if cloud_service_provider == 1: # Azure + ClientAuthenticationError = azure_import( + "azure.core.exceptions", "ClientAuthenticationError" + ) try: - # Use DefaultAzureCredential if provided, else use client secrets - credential = provider_details.get("credential") or ClientSecretCredential( - tenant_id=provider_details["tenantId"], - client_id=provider_details["clientId"], - client_secret=provider_details["clientSecret"], - ) + credential = create_credential(provider_details) resource_client = create_resource_client( credential, provider_details["subscriptionId"] ) @@ -93,13 +90,14 @@ def test_permissions( logs = "" if cloud_service_provider == 1: # Azure + ClientAuthenticationError = azure_import( + "azure.core.exceptions", "ClientAuthenticationError" + ) + AuthorizationManagementClient = azure_import( + "azure.mgmt.authorization", "AuthorizationManagementClient" + ) try: - # Use DefaultAzureCredential if provided, else use client secrets - credential = provider_details.get("credential") or ClientSecretCredential( - tenant_id=provider_details["tenantId"], - client_id=provider_details["clientId"], - client_secret=provider_details["clientSecret"], - ) + credential = create_credential(provider_details) resource_group_scope = f"/subscriptions/{provider_details['subscriptionId']}/resourceGroups/{provider_details['resourceGroupName']}" # Check role assignments diff --git a/core/utils_azure.py b/core/utils_azure.py index c512085..1eba9bc 100644 --- a/core/utils_azure.py +++ b/core/utils_azure.py @@ -7,12 +7,8 @@ from datetime import date, datetime from dateutil.relativedelta import relativedelta from collections import defaultdict -from azure.identity import ClientSecretCredential -from azure.mgmt.resource.resources import ResourceManagementClient -from azure.mgmt.resource.subscriptions import SubscriptionClient -from azure.mgmt.costmanagement import CostManagementClient -from azure.mgmt.costmanagement.models import QueryDefinition, TimeframeType -from azure.core.exceptions import AzureError, ClientAuthenticationError + +from utils.azure import azure_import from .utils_db import connect, load_data @@ -22,15 +18,36 @@ MANAGEMENT_URL = "https://management.azure.com" +def create_credential(provider_details: dict[str, Any]) -> Any: + # A credential from the Azure CLI or the environment wins; otherwise the + # service principal from the configuration is used. + credential = provider_details.get("credential") + if credential: + return credential + ClientSecretCredential = azure_import("azure.identity", "ClientSecretCredential") + return ClientSecretCredential( + tenant_id=provider_details["tenantId"], + client_id=provider_details["clientId"], + client_secret=provider_details["clientSecret"], + ) + + def create_resource_client(credential: Any, subscription_id: str) -> Any: + ResourceManagementClient = azure_import( + "azure.mgmt.resource.resources", "ResourceManagementClient" + ) return ResourceManagementClient(credential, subscription_id) def create_subscription_client(credential: Any) -> Any: + SubscriptionClient = azure_import("azure.mgmt.subscription", "SubscriptionClient") return SubscriptionClient(credential) def create_cost_client(credential: Any) -> Any: + CostManagementClient = azure_import( + "azure.mgmt.costmanagement", "CostManagementClient" + ) return CostManagementClient(credential, base_url=MANAGEMENT_URL) @@ -57,6 +74,7 @@ def cost_result_dict(result: Any) -> dict[str, Any]: def is_resource_inventory_empty( credential: Any, subscription_id: str, resource_group_name: str ) -> bool: + AzureError = azure_import("azure.core.exceptions", "AzureError") try: resource_client = create_resource_client(credential, subscription_id) # logger.info("Checking Azure resource inventory...") @@ -84,13 +102,11 @@ def build_azure_resource_inventory( report_path: str, raw_data_path: str, ) -> None: + ClientAuthenticationError = azure_import( + "azure.core.exceptions", "ClientAuthenticationError" + ) try: - # Use DefaultAzureCredential if provided, otherwise fall back to ClientSecretCredential - credential = provider_details.get("credential") or ClientSecretCredential( - tenant_id=provider_details["tenantId"], - client_id=provider_details["clientId"], - client_secret=provider_details["clientSecret"], - ) + credential = create_credential(provider_details) subscription_id = provider_details["subscriptionId"] resource_group_name = provider_details["resourceGroupName"] @@ -203,13 +219,12 @@ def build_azure_cost_inventory( report_path: str, raw_data_path: str, ) -> None: + QueryDefinition = azure_import( + "azure.mgmt.costmanagement.models", "QueryDefinition" + ) + TimeframeType = azure_import("azure.mgmt.costmanagement.models", "TimeframeType") try: - # Use DefaultAzureCredential if provided, otherwise fall back to ClientSecretCredential - credential = provider_details.get("credential") or ClientSecretCredential( - tenant_id=provider_details["tenantId"], - client_id=provider_details["clientId"], - client_secret=provider_details["clientSecret"], - ) + credential = create_credential(provider_details) cost_management_client = create_cost_client(credential) db_path = os.path.join(report_path, "data", "assessment.db") diff --git a/core/utils_egress_azure.py b/core/utils_egress_azure.py index bfcfbc4..0079dbe 100644 --- a/core/utils_egress_azure.py +++ b/core/utils_egress_azure.py @@ -3,9 +3,8 @@ import requests from typing import Any from datetime import datetime, timedelta, timezone -from azure.identity import ClientSecretCredential -from .utils_azure import create_resource_client +from .utils_azure import create_credential, create_resource_client from .utils_egress import GIB, format_bytes, load_egress_registry, new_row logger = logging.getLogger("core.engine.egress.azure") @@ -281,11 +280,7 @@ def build_egress_inventory( def collect_azure_egress( provider_details: dict[str, Any], ) -> tuple[list[dict[str, Any]], set[str]]: - credential = provider_details.get("credential") or ClientSecretCredential( - tenant_id=provider_details["tenantId"], - client_id=provider_details["clientId"], - client_secret=provider_details["clientSecret"], - ) + credential = create_credential(provider_details) subscription_id = provider_details["subscriptionId"] resource_group_name = provider_details["resourceGroupName"] diff --git a/main.py b/main.py index 07d0251..9a9d7d5 100644 --- a/main.py +++ b/main.py @@ -8,12 +8,12 @@ import getpass import traceback from dataclasses import replace +from typing import Any from rich.console import Console from rich.logging import RichHandler from rich.markup import escape from datetime import datetime from botocore.exceptions import NoCredentialsError, ProfileNotFound -from azure.identity import DefaultAzureCredential, ClientSecretCredential # Import the functions from core.engine import ( @@ -44,6 +44,9 @@ prepare_sync_document, ) from utils.azure import ( + AzureSdkError, + azure_import, + require_azure_sdk, select_subscription, select_resource_group, is_azure_cli_installed, @@ -313,7 +316,15 @@ def handle_aws(args): ) -def _azure_cli_credential() -> DefaultAzureCredential: +def _require_azure_sdk() -> None: + try: + require_azure_sdk() + except AzureSdkError as e: + console.print(f"[red]{e}[/red]") + sys.exit(codes.CONFIG) + + +def _azure_cli_credential() -> Any: if not is_azure_cli_installed(): console.print( "[red]Azure CLI is not installed. Install it from https://aka.ms/install-azure-cli.[/red]" @@ -330,6 +341,7 @@ def _azure_cli_credential() -> DefaultAzureCredential: "[bold cyan]az login --scope https://management.azure.com/.default[/bold cyan]" ) raise ConfigError + DefaultAzureCredential = azure_import("azure.identity", "DefaultAzureCredential") return DefaultAzureCredential() @@ -349,6 +361,9 @@ def _azure_provider_noninteractive(args) -> dict: "[red]--non-interactive with AZURE_CLIENT_SECRET also requires AZURE_CLIENT_ID.[/red]" ) raise ConfigError + ClientSecretCredential = azure_import( + "azure.identity", "ClientSecretCredential" + ) credential = ClientSecretCredential( tenant_id=tenant_id, client_id=client_id, @@ -360,6 +375,9 @@ def _azure_provider_noninteractive(args) -> dict: "[red]--non-interactive Azure OIDC requires AZURE_CLIENT_ID when AZURE_CLIENT_SECRET is not set.[/red]" ) raise ConfigError + DefaultAzureCredential = azure_import( + "azure.identity", "DefaultAzureCredential" + ) credential = DefaultAzureCredential() provider_details = { @@ -421,6 +439,7 @@ def _azure_provider_from_prompt() -> dict: client_id = input("Enter Service Principal / Client ID: ").strip() client_secret = getpass.getpass("Enter Client Secret (input hidden): ").strip() + ClientSecretCredential = azure_import("azure.identity", "ClientSecretCredential") try: credential = ClientSecretCredential( tenant_id=tenant_id, client_id=client_id, client_secret=client_secret @@ -467,6 +486,9 @@ def handle_azure(args): _reject_egress_with_tfstate(args) tfstate_path = getattr(args, "tfstate", None) + # Fail before any prompt; a config file is checked when the run starts + if not tfstate_path and not args.config: + _require_azure_sdk() if args.config: config = load_config(args.config) @@ -564,6 +586,8 @@ def run_assessment( # tfstate mode reads a local state file: no credentials, no permission # check, no cost data — and no egress estimation, which needs live APIs. is_tfstate = bool(config["providerDetails"].get("tfstatePath")) + if config["cloudServiceProvider"] == 1 and not is_tfstate: + _require_azure_sdk() if is_tfstate and egress: print_step( "Configuration validation failed.", diff --git a/pyproject.toml b/pyproject.toml index 66c5514..488b4f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ requires-python = ">=3.12" dependencies = [ "azure-identity==1.26.0", "azure-mgmt-resource==26.0.0", - "azure-mgmt-resource-subscriptions==1.0.0", + "azure-mgmt-subscription==3.1.1", "azure-mgmt-authorization==4.0.0", "azure-mgmt-costmanagement==5.0.0", "boto3==1.43.118", diff --git a/requirements.txt b/requirements.txt index 6850a8d..fde0901 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ azure-identity==1.26.0 azure-mgmt-resource==26.0.0 -azure-mgmt-resource-subscriptions==1.0.0 +azure-mgmt-subscription==3.1.1 azure-mgmt-authorization==4.0.0 azure-mgmt-costmanagement==5.0.0 boto3==1.43.118 diff --git a/tests/test_azure_sdk_shapes.py b/tests/test_azure_sdk_shapes.py index 6af3701..9e2c88e 100644 --- a/tests/test_azure_sdk_shapes.py +++ b/tests/test_azure_sdk_shapes.py @@ -8,9 +8,9 @@ from unittest import mock from azure.core.credentials import AccessToken +from azure.mgmt.costmanagement.models import QueryDefinition, TimeframeType from dateutil.relativedelta import relativedelta -from core import utils_azure from core.utils_azure import ( build_azure_cost_inventory, build_azure_resource_inventory, @@ -141,7 +141,7 @@ def test_the_clients_come_from_the_packages_the_sdk_now_splits_them_into(self): ) self.assertEqual( type(create_subscription_client(self.credential)).__module__, - "azure.mgmt.resource.subscriptions._client", + "azure.mgmt.subscription._subscription_client", ) def test_the_authorization_client_still_has_role_assignments(self): @@ -395,9 +395,9 @@ def test_cost_result_dict_flattens_the_properties(self): with fake_http(self.result([[1, "2026-01-01T00:00:00", "USD"]])): result = client.query.usage( GROUP_ID, - utils_azure.QueryDefinition( + QueryDefinition( type="Usage", - timeframe=utils_azure.TimeframeType.CUSTOM, + timeframe=TimeframeType.CUSTOM, time_period={ "from": "2026-01-01T00:00:00Z", "to": "2026-02-01T00:00:00Z", diff --git a/tests/test_lazy_azure.py b/tests/test_lazy_azure.py new file mode 100644 index 0000000..67cb8f1 --- /dev/null +++ b/tests/test_lazy_azure.py @@ -0,0 +1,323 @@ +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +import unittest +from importlib.metadata import PackageNotFoundError +from pathlib import Path +from unittest import mock + +import main +from tests.make_smoke_dataset import main as make_smoke_dataset +from utils import codes +from utils import azure as azure_utils +from utils.azure import AzureSdkError, azure_import, require_azure_sdk + +ROOT = Path(__file__).resolve().parent.parent +SMOKE_STATE = ROOT / "tests" / "fixtures" / "smoke" / "aws.tfstate" + +DRIVER = """ +import json, runpy, sys +root, mode, *argv = sys.argv[1:] +sys.path.insert(0, root) +if mode == "broken": + sys.modules["azure"] = None +sys.argv = ["cloudexit", *argv] +code = 0 +try: + runpy.run_module("main", run_name="__main__") +except SystemExit as exc: + code = exc.code or 0 +loaded = sorted( + name + for name, module in sys.modules.items() + if (name == "azure" or name.startswith("azure.")) and module is not None +) +print("AZURE_MODULES=" + json.dumps(loaded)) +sys.exit(code) +""" + + +class SubprocessTestCase(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.tmp = Path(tmp.name) + self.work = self.tmp / "work" + self.work.mkdir() + self.env = { + **os.environ, + "XDG_DATA_HOME": str(self.tmp / "data"), + "XDG_CONFIG_HOME": str(self.tmp / "config"), + "XDG_CACHE_HOME": str(self.tmp / "cache"), + "CLOUDEXIT_CATALOGUE_URL": "http://127.0.0.1:9", + "ESC_EXIT_STRATEGY": "1", + "ESC_ASSESSMENT_TYPE": "1", + "COLUMNS": "200", + } + + def dataset(self): + path = self.tmp / "smoke.db" + if not path.exists(): + make_smoke_dataset(str(path)) + return str(path) + + def run_cli(self, mode, *argv): + result = subprocess.run( + [sys.executable, "-c", DRIVER, str(ROOT), mode, *argv], + cwd=self.work, + env=self.env, + capture_output=True, + text=True, + timeout=120, + ) + output = result.stdout + result.stderr + match = re.search(r"AZURE_MODULES=(\[.*\])", result.stdout) + loaded = json.loads(match.group(1)) if match else None + return result.returncode, loaded, " ".join(output.split()) + + +class NonAzureCommandsTests(SubprocessTestCase): + COMMANDS = ( + ("--version",), + ("config", "path"), + ("catalogue", "status"), + ) + + def test_no_azure_module_is_imported_for_commands_that_never_use_azure(self): + for argv in self.COMMANDS: + with self.subTest(argv=" ".join(argv)): + code, loaded, output = self.run_cli("open", *argv) + self.assertEqual(code, 0, output) + self.assertEqual(loaded, []) + + def test_a_broken_azure_sdk_does_not_stop_those_commands(self): + for argv in self.COMMANDS: + with self.subTest(argv=" ".join(argv)): + code, loaded, output = self.run_cli("broken", *argv) + self.assertEqual(code, 0, output) + self.assertEqual(loaded, []) + self.assertNotIn("Traceback", output) + + def test_an_offline_aws_run_needs_no_azure_sdk(self): + argv = ( + "aws", + "--tfstate", + str(SMOKE_STATE), + "--catalogue", + self.dataset(), + "--non-interactive", + "--dry-run", + ) + for mode in ("open", "broken"): + with self.subTest(mode=mode): + code, loaded, output = self.run_cli(mode, *argv) + self.assertEqual(code, 0, output) + self.assertEqual(loaded, []) + self.assertTrue(list(self.work.glob("reports/*/index.html"))) + + +class BrokenSdkOnAzurePathsTests(SubprocessTestCase): + def assert_guard_message(self, code, output): + self.assertEqual(code, codes.CONFIG, output) + self.assertNotIn("Traceback", output) + self.assertIn("The Azure SDK in this environment is incomplete", output) + self.assertRegex( + output, r"pip install --force-reinstall --no-deps azure-[a-z-]+" + ) + self.assertIn("or install cloudexit into a new virtual environment", output) + + def test_a_live_azure_run_stops_with_the_repair_message_before_any_prompt(self): + for argv in ( + ("azure", "--non-interactive"), + ("azure", "--non-interactive", "--cli"), + ): + with self.subTest(argv=" ".join(argv)): + code, _, output = self.run_cli( + "broken", *argv, "--catalogue", self.dataset() + ) + self.assert_guard_message(code, output) + + def test_a_config_file_run_stops_after_the_configuration_is_validated(self): + config = self.work / "azure.json" + shutil.copy(ROOT / "config" / "azure_example.json", config) + + code, _, output = self.run_cli( + "broken", + "azure", + "--config", + str(config), + "--non-interactive", + "--catalogue", + self.dataset(), + ) + + self.assert_guard_message(code, output) + self.assertIn("Configuration successfully validated", output) + self.assertFalse(list(self.work.glob("reports/*"))) + + +class AzureImportTests(unittest.TestCase): + def test_the_symbols_the_azure_paths_use_import_in_a_complete_environment(self): + require_azure_sdk() + self.assertEqual( + azure_import("azure.identity", "ClientSecretCredential").__name__, + "ClientSecretCredential", + ) + + def test_a_missing_symbol_names_what_could_not_be_imported(self): + with self.assertRaises(AzureSdkError) as ctx: + azure_import("azure.mgmt.resource.resources", "NoSuchClient") + + self.assertEqual(ctx.exception.module, "azure.mgmt.resource.resources") + self.assertIn( + "cannot import azure.mgmt.resource.resources.NoSuchClient", + str(ctx.exception), + ) + self.assertIsNone(ctx.exception.__cause__) + + def test_a_missing_package_is_the_same_error(self): + with mock.patch.dict(sys.modules, {"azure.mgmt.subscription": None}): + with self.assertRaises(AzureSdkError): + azure_import("azure.mgmt.subscription", "SubscriptionClient") + + def test_the_repair_command_names_the_package_and_the_pinned_version(self): + pins = [ + "azure-identity==1.26.0", + "azure-mgmt-resource==26.0.0", + "azure-mgmt-subscription==3.1.1", + "boto3==1.43.118", + "black ; extra == 'dev'", + ] + cases = { + "azure.identity": "azure-identity==1.26.0", + "azure.mgmt.resource.resources": "azure-mgmt-resource==26.0.0", + "azure.mgmt.subscription": "azure-mgmt-subscription==3.1.1", + "azure.core.exceptions": "azure-core", + "azure.mgmt.costmanagement.models": "azure-mgmt-costmanagement", + } + with mock.patch.object(azure_utils, "requires", return_value=pins): + for module, expected in cases.items(): + with self.subTest(module=module): + message = str(AzureSdkError(module, "Thing")) + self.assertIn(f"--force-reinstall --no-deps {expected},", message) + + def test_without_an_installed_cloudexit_the_package_name_alone_is_given(self): + with mock.patch.object( + azure_utils, "requires", side_effect=PackageNotFoundError("cloudexit") + ): + message = str( + AzureSdkError("azure.mgmt.subscription", "SubscriptionClient") + ) + self.assertIn("--no-deps azure-mgmt-subscription,", message) + + def test_every_symbol_has_a_distribution_the_message_can_name(self): + for module, _ in azure_utils.SDK_SYMBOLS: + with self.subTest(module=module): + self.assertTrue( + any( + module == prefix or module.startswith(f"{prefix}.") + for prefix in azure_utils.SDK_DISTRIBUTIONS + ) + ) + + +class GuardPlacementTests(unittest.TestCase): + def args(self, **overrides): + values = { + "tfstate": None, + "config": None, + "cli": False, + "non_interactive": True, + "name": None, + "egress": False, + "dry_run": False, + } + values.update(overrides) + return argparse.Namespace(**values) + + def run_handle(self, args): + calls = [] + with ( + mock.patch( + "main._require_azure_sdk", side_effect=lambda: calls.append("sdk") + ), + mock.patch("main.load_config", return_value={"name": "n"}), + mock.patch("main.require_env_int", return_value=1), + mock.patch( + "main._azure_provider_noninteractive", + side_effect=lambda a: calls.append("provider") or {}, + ), + mock.patch("main.build_config", return_value={}), + mock.patch("main.run_assessment"), + ): + main.handle_azure(args) + return calls + + def test_a_live_run_checks_the_sdk_before_it_asks_for_anything(self): + self.assertEqual(self.run_handle(self.args()), ["sdk", "provider"]) + + def test_a_terraform_state_run_does_not_need_the_sdk(self): + self.assertEqual(self.run_handle(self.args(tfstate="state.tfstate")), []) + + def test_a_config_file_run_is_checked_when_the_run_starts(self): + self.assertEqual(self.run_handle(self.args(config="azure.json")), []) + + +class RunAssessmentGuardTests(unittest.TestCase): + CONFIG = { + "name": "Guard", + "cloudServiceProvider": 1, + "exitStrategy": 1, + "assessmentType": 1, + "providerDetails": { + "tenantId": "t", + "clientId": "c", + "clientSecret": "s", + "subscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroupName": "rg", + }, + } + + def run_with(self, config, error=None): + with ( + mock.patch("main.validate_config"), + mock.patch("main.require_azure_sdk", side_effect=error) as guard, + mock.patch("main.console.print") as printed, + mock.patch("main.print_step"), + mock.patch( + "main.create_directory", side_effect=RuntimeError("stop") + ) as made, + ): + with self.assertRaises(SystemExit) as ctx: + main.run_assessment(config, "azure") + return ctx.exception.code, guard, printed, made + + def test_an_incomplete_sdk_ends_the_run_with_the_configuration_code(self): + error = AzureSdkError("azure.identity", "ClientSecretCredential") + code, _, printed, made = self.run_with(dict(self.CONFIG), error) + + self.assertEqual(code, codes.CONFIG) + made.assert_not_called() + self.assertIn("incomplete", str(printed.call_args_list)) + + def test_a_terraform_state_run_never_asks_for_the_sdk(self): + config = dict(self.CONFIG, providerDetails={"tfstatePath": "state.tfstate"}) + code, guard, _, made = self.run_with(config) + + guard.assert_not_called() + made.assert_called_once() + self.assertEqual(code, codes.CONFIG) + + def test_an_aws_run_never_asks_for_the_sdk(self): + config = dict(self.CONFIG, cloudServiceProvider=2) + _, guard, _, _ = self.run_with(config) + guard.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_utils_and_main.py b/tests/test_utils_and_main.py index 7e98d84..5a43a24 100644 --- a/tests/test_utils_and_main.py +++ b/tests/test_utils_and_main.py @@ -504,7 +504,9 @@ def test_builds_config_from_env_and_calls_run_assessment(self): mock_credential = MagicMock() with ( patch.dict(os.environ, self._BASE_ENV, clear=False), - patch("main.ClientSecretCredential", return_value=mock_credential), + patch( + "azure.identity.ClientSecretCredential", return_value=mock_credential + ), patch("main.run_assessment") as mock_run, patch("main.console.print"), ): @@ -522,8 +524,10 @@ def test_uses_default_credential_when_client_secret_missing(self): mock_credential = MagicMock() with ( patch.dict(os.environ, env, clear=False), - patch("main.DefaultAzureCredential", return_value=mock_credential), - patch("main.ClientSecretCredential") as mock_client_secret_cred, + patch( + "azure.identity.DefaultAzureCredential", return_value=mock_credential + ), + patch("azure.identity.ClientSecretCredential") as mock_client_secret_cred, patch("main.run_assessment") as mock_run, patch("main.console.print"), ): @@ -783,7 +787,9 @@ def test_handle_azure_passes_egress_flag(self): mock_credential = MagicMock() with ( patch.dict(os.environ, NonInteractiveAzureTests._BASE_ENV, clear=False), - patch("main.ClientSecretCredential", return_value=mock_credential), + patch( + "azure.identity.ClientSecretCredential", return_value=mock_credential + ), patch("main.run_assessment") as mock_run, patch("main.console.print"), ): diff --git a/tests/test_utils_azure.py b/tests/test_utils_azure.py index 7507677..ecb3fd8 100644 --- a/tests/test_utils_azure.py +++ b/tests/test_utils_azure.py @@ -53,7 +53,7 @@ def test_returns_all_when_none_processed(self): class IsResourceInventoryEmptyTests(unittest.TestCase): - @patch("core.utils_azure.ResourceManagementClient") + @patch("azure.mgmt.resource.resources.ResourceManagementClient") def test_returns_true_when_no_resources(self, mock_rmc_cls): mock_client = MagicMock() mock_rmc_cls.return_value = mock_client @@ -62,7 +62,7 @@ def test_returns_true_when_no_resources(self, mock_rmc_cls): result = is_resource_inventory_empty(MagicMock(), "sub-123", "rg-test") self.assertTrue(result) - @patch("core.utils_azure.ResourceManagementClient") + @patch("azure.mgmt.resource.resources.ResourceManagementClient") def test_returns_false_when_resources_exist(self, mock_rmc_cls): mock_client = MagicMock() mock_rmc_cls.return_value = mock_client @@ -74,7 +74,7 @@ def test_returns_false_when_resources_exist(self, mock_rmc_cls): result = is_resource_inventory_empty(MagicMock(), "sub-123", "rg-test") self.assertFalse(result) - @patch("core.utils_azure.ResourceManagementClient") + @patch("azure.mgmt.resource.resources.ResourceManagementClient") def test_raises_on_azure_error(self, mock_rmc_cls): mock_client = MagicMock() mock_rmc_cls.return_value = mock_client @@ -88,7 +88,7 @@ def test_raises_on_azure_error(self, mock_rmc_cls): class BuildAzureResourceInventoryErrorTests(unittest.TestCase): @patch("core.utils_azure.is_resource_inventory_empty") - @patch("core.utils_azure.ClientSecretCredential") + @patch("azure.identity.ClientSecretCredential") def test_auth_error_is_reraised(self, mock_cred_cls, mock_empty_check): mock_cred_cls.side_effect = ClientAuthenticationError( message="Invalid credentials" @@ -111,7 +111,7 @@ def test_auth_error_is_reraised(self, mock_cred_cls, mock_empty_check): ) @patch("core.utils_azure.is_resource_inventory_empty", return_value=True) - @patch("core.utils_azure.ClientSecretCredential") + @patch("azure.identity.ClientSecretCredential") def test_returns_early_when_inventory_empty(self, mock_cred_cls, mock_empty_check): from core.utils_azure import build_azure_resource_inventory @@ -133,7 +133,7 @@ def test_returns_early_when_inventory_empty(self, mock_cred_cls, mock_empty_chec class BuildAzureCostInventoryErrorTests(unittest.TestCase): - @patch("core.utils_azure.ClientSecretCredential") + @patch("azure.identity.ClientSecretCredential") def test_auth_error_is_reraised(self, mock_cred_cls): mock_cred_cls.side_effect = ClientAuthenticationError( message="Invalid credentials" @@ -156,8 +156,8 @@ def test_auth_error_is_reraised(self, mock_cred_cls): ) @patch("core.utils_azure.connect") - @patch("core.utils_azure.CostManagementClient") - @patch("core.utils_azure.ClientSecretCredential") + @patch("azure.mgmt.costmanagement.CostManagementClient") + @patch("azure.identity.ClientSecretCredential") def test_sqlite_error_is_reraised(self, mock_cred_cls, mock_cost_cls, mock_connect): import sqlite3 diff --git a/tests/test_utils_egress_azure.py b/tests/test_utils_egress_azure.py index caaf9bd..1e312d9 100644 --- a/tests/test_utils_egress_azure.py +++ b/tests/test_utils_egress_azure.py @@ -367,7 +367,7 @@ def test_vault_produces_warning_finding_and_no_size(self, mock_fetch): class CollectAzureEgressTests(unittest.TestCase): @patch("core.utils_egress_azure.fetch_monitor_metrics") - @patch("core.utils_azure.ResourceManagementClient") + @patch("azure.mgmt.resource.resources.ResourceManagementClient") def test_returns_rows_and_archive_tiers(self, mock_rmc_cls, mock_fetch): mock_fetch.side_effect = [ {"UsedCapacity": [{"dimension": None, "value": float(10 * GIB)}]}, diff --git a/utils/azure.py b/utils/azure.py index 95ead3d..faaab5c 100644 --- a/utils/azure.py +++ b/utils/azure.py @@ -1,15 +1,85 @@ # utils/azure.py +import importlib import logging import shutil import subprocess +from importlib.metadata import PackageNotFoundError, requires from typing import Any from rich.console import Console -from azure.identity import AzureCliCredential -from azure.core.exceptions import ClientAuthenticationError logger = logging.getLogger("main.utils.azure") console = Console() +# The SDK classes the Azure paths use, and the distribution that provides each +SDK_SYMBOLS = ( + ("azure.identity", "AzureCliCredential"), + ("azure.identity", "ClientSecretCredential"), + ("azure.identity", "DefaultAzureCredential"), + ("azure.core.exceptions", "AzureError"), + ("azure.core.exceptions", "ClientAuthenticationError"), + ("azure.mgmt.resource.resources", "ResourceManagementClient"), + ("azure.mgmt.subscription", "SubscriptionClient"), + ("azure.mgmt.costmanagement", "CostManagementClient"), + ("azure.mgmt.costmanagement.models", "QueryDefinition"), + ("azure.mgmt.costmanagement.models", "TimeframeType"), + ("azure.mgmt.authorization", "AuthorizationManagementClient"), +) +SDK_DISTRIBUTIONS = { + "azure.identity": "azure-identity", + "azure.core": "azure-core", + "azure.mgmt.resource": "azure-mgmt-resource", + "azure.mgmt.subscription": "azure-mgmt-subscription", + "azure.mgmt.costmanagement": "azure-mgmt-costmanagement", + "azure.mgmt.authorization": "azure-mgmt-authorization", +} + + +class AzureSdkError(Exception): + def __init__(self, module: str, name: str): + self.module = module + self.name = name + super().__init__( + f"The Azure SDK in this environment is incomplete (cannot import " + f"{module}.{name}). Reinstall it with: pip install --force-reinstall " + f"--no-deps {_requirement(module)}, or install cloudexit into a new " + "virtual environment." + ) + + +def _requirement(module: str) -> str: + distribution = next( + ( + name + for prefix, name in SDK_DISTRIBUTIONS.items() + if module == prefix or module.startswith(f"{prefix}.") + ), + module, + ) + try: + # The pinned version this installation of cloudexit asks for + for line in requires("cloudexit") or []: + requirement = line.split(";")[0].replace(" ", "") + if requirement.lower().startswith(f"{distribution}=="): + return requirement + except PackageNotFoundError: + pass + return distribution + + +def azure_import(module: str, name: str) -> Any: + # The Azure SDK is imported where it is used, so a broken or missing SDK + # cannot stop commands that never touch Azure. + try: + return getattr(importlib.import_module(module), name) + except (ImportError, AttributeError) as exc: + logger.debug("Cannot import %s.%s: %s", module, name, exc) + raise AzureSdkError(module, name) from None + + +def require_azure_sdk() -> None: + for module, name in SDK_SYMBOLS: + azure_import(module, name) + def is_azure_cli_installed() -> bool: return shutil.which("az") is not None @@ -30,6 +100,10 @@ def is_azure_cli_logged_in() -> bool: def is_azure_cli_token_expired() -> bool: + AzureCliCredential = azure_import("azure.identity", "AzureCliCredential") + ClientAuthenticationError = azure_import( + "azure.core.exceptions", "ClientAuthenticationError" + ) credential = AzureCliCredential() try: credential.get_token("https://management.azure.com/.default") From 94cc828645fe2f4da8bcf723edbcb54d623c7b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Sat, 24 Oct 2026 09:06:47 +0200 Subject: [PATCH 14/61] replace shared icon images with a drawn placeholder --- core/assets/css/style.css | 2199 +++++++++++++------------ core/assets/icons/misc/no_image.png | Bin 5151 -> 0 bytes core/assets/icons/severity/high.png | Bin 882 -> 0 bytes core/assets/icons/severity/low.png | Bin 895 -> 0 bytes core/assets/icons/severity/medium.png | Bin 1205 -> 0 bytes core/assets/template/egress.html | 19 + core/assets/template/index.html | 4 + core/report/context.py | 11 +- core/report/helpers.py | 12 + core/report/palette.py | 2 + core/report/placeholder.py | 61 + core/report/sections.py | 5 + core/utils.py | 7 +- core/utils_report.py | 22 +- core/utils_report_common.py | 4 +- core/utils_report_egress.py | 16 +- core/utils_report_pdf.py | 4 +- tests/flow_fixtures.py | 2 +- tests/report_fixtures.py | 12 +- tests/test_copy_assets.py | 23 +- tests/test_icon_placeholder.py | 412 +++++ tests/test_packaging.py | 7 +- tests/test_report_transforms.py | 8 +- tests/test_utils_report_egress.py | 6 +- tests/test_utils_tfstate.py | 2 +- 25 files changed, 1683 insertions(+), 1155 deletions(-) delete mode 100644 core/assets/icons/misc/no_image.png delete mode 100644 core/assets/icons/severity/high.png delete mode 100644 core/assets/icons/severity/low.png delete mode 100644 core/assets/icons/severity/medium.png create mode 100644 core/report/placeholder.py create mode 100644 tests/test_icon_placeholder.py diff --git a/core/assets/css/style.css b/core/assets/css/style.css index 8e2234f..0835fdc 100644 --- a/core/assets/css/style.css +++ b/core/assets/css/style.css @@ -1,1092 +1,1107 @@ -/* ======================================================== - Base: Variables - ======================================================== */ -:root { - /* Blue */ - --blue-100: #dbe6fe; - --blue-800: #1e4baf; - - /* Green */ - --green-100: #dcfce7; - --green-600: #16a34a; - --green-700: #047854; - - /* Neutral */ - --neutral-50: #f9fbfb; - --neutral-100: #f3f6f6; - --neutral-200: #e5ebeb; - --neutral-300: #d1dbdb; - --neutral-400: #9cafae; - --neutral-600: #4b6361; - --neutral-800: #1f3735; - --neutral-900: #112726; - - /* Primary */ - --primary-600: #0d948b; - --primary-800: #115e59; - --primary-950: #042f2c; - - /* Red */ - --red-50: #fef2f2; - --red-100: #fee2e2; - --red-700: #b91c1c; - --red-800: #991b1b; - - /* Yellow */ - --yellow-100: #fee4c7; - --yellow-850: #92400e; - /* Color Palette */ - --white: #fff; - /* Transition Defaults */ - --transition-speed: 0.3s; - - /* Font Sizes */ - --text-heading-2: 24px; - --text-heading-3: 20px; - --text-body: 16px; - --text-label: 14px; - --text-label-small: 12px; - /* Radius */ - --rounded-md: 12px; - --rounded-sm: 8px; - --rounded-xs: 4px; - /* Sidebar */ -} - -/* ======================================================== - Base: Reset - ======================================================== */ -* { - margin: 0; - padding: 0; - -webkit-box-sizing: border-box; - box-sizing: border-box; -} - -body { - font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; - line-height: 1.375; - font-weight: 400; - color: var(--neutral-900); - background: var(--neutral-100); - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-font-smoothing: antialiased; -} - -img { - max-width: 100%; - height: auto; -} - -ul { - list-style: none; - padding: 0; - margin: 0; -} - -.divider-border { - height: 1px; - background: var(--neutral-200); -} - -.container { -} - -/* ======================================================== - Base: Typography - ======================================================== */ -a { - text-decoration: none; - -webkit-transition: all var(--transition-speed) ease; - -o-transition: all var(--transition-speed) ease; - transition: all var(--transition-speed) ease; -} - -a:hover { - text-decoration: none !important; -} - - -h2, -h3 { - color: var(--neutral-900); - font-weight: 500; -} - - -h2, -h3, -li, -p { - margin: 0; -} - - -h2 { - font-size: var(--text-heading-2); - line-height: 1.33; -} - - -h3 { - font-size: var(--text-heading-3); - line-height: 1.4; -} - - -body, -p { - font-size: var(--text-body); - line-height: 1.375; -} - -.label, -label { - font-size: var(--text-label); - line-height: 1.43; -} - -.small, -label.small { - font-size: var(--text-label-small); - line-height: 1.33; -} - -.bg-white { - background: var(--white); -} - -a*:active, -a*:focus { - outline: 0; - border: 0; -} - -main { - padding-bottom: 32px; - padding-top: 88px; -} - -/* Scrollbars */ -::-webkit-scrollbar { - width: 5px; - border-radius: var(--rounded-sm); -} - -::-webkit-scrollbar-track { - background: var(--neutral-100); - border-radius: var(--rounded-sm); -} - -::-webkit-scrollbar-thumb { - background: var(--primary-600); - border-radius: var(--rounded-sm); -} - -::-webkit-scrollbar-thumb:hover { - background: var(--primary-800); - border-radius: var(--rounded-sm); -} - -.dropdown-item span { - font-size: var(--text-label); - font-weight: 400; - color: var(--neutral-900); -} - -.dropdown-item { - padding-top: 8px; - padding-bottom: 8px; -} - -.dropdown-menu { - padding: 0; -} - -.dropdown-menu li a { - line-height: 1.5; -} - -/* ======================================================== - Layout: Shared Cards And Charts - ======================================================== */ - -.chart-card-head { - padding: 16px 24px; -} - -.chart-card-head h3 { - margin-bottom: 5px; - color: var(--neutral-900); -} -.chart-card-head h6 { - color: var(--neutral-600); - font-size: var(--text-label); - font-weight: 400; -} -.chart-card-head h6 span { - display: inline-block; - font-weight: 500; -} - -.risk-header { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - /* padding: 16px 24px; */ -} - -.risk-dashboard { - display: flex; - flex-direction: column; - height: 100%; -} - -.risk-count h2 { - color: var(--neutral-600); - font-size: var(--text-label); - font-weight: 400; -} - -.risk-count .count { - font-size: var(--text-heading-3); - font-weight: 500; - color: var(--neutral-900); - margin: 0; -} - -.chart-container { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - flex: 1; -} - -.chart-box { - height: 350px; -} - -.chart-empty-state { - flex: 1; - min-height: 220px; - display: flex; - align-items: center; - justify-content: center; - text-align: center; -} - -.chart-empty-state-inner { - display: flex; - flex-direction: column; - align-items: center; - gap: 10px; -} - -.chart-empty-state i { - font-size: 28px; - line-height: 1; - color: var(--neutral-900); -} - -.chart-empty-state p { - margin: 0; - color: var(--neutral-700); -} - -.alt-tech-empty-state { - min-height: 260px; -} - -.scoring-empty-state { - min-height: 300px; -} - -.scoring-card { - overflow: hidden; -} - -.scoring-inner { - display: flex; - justify-content: center; - align-items: center; - height: 100%; - min-height: 300px; -} - -.chart-wrapper { - max-width: 350px; - overflow: hidden; - position: relative; - border-radius: 12px; - mask-image: linear-gradient(to bottom, black 85%, transparent 100%); - -webkit-mask-image: linear-gradient(to bottom, black 85%, transparent 100%); -} - -.scoring { - width: 100%; - max-width: 420px; - height: 320px !important; - margin: 0 auto; -} - -.form-title { - font-size: var(--text-heading-3); - font-weight: 500; - line-height: 1.33; - color: var(--neutral-900); -} - -.shadow-s { - -webkit-box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1); - box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1); -} - -.input { - border: 1px solid var(--neutral-300); -} - -/* ======================================================== - Components: Buttons - ======================================================== */ - -.dropdown-toggle::after { - float: right; - margin-top: 8px; -} - -/* ======================================================== - Components: Summary, Scoring, Resources, Alt Tech Cards - ======================================================== */ -::-webkit-input-placeholder { - color: var(--neutral-600); - font-size: var(--text-body); - font-style: normal; - font-weight: 400; - line-height: 38px; -} - -::-moz-placeholder { - color: var(--neutral-600); - font-size: var(--text-body); - font-style: normal; - font-weight: 400; - line-height: 38px; -} - -.resource-card { - border: 1px solid var(--neutral-200); -} - -.resource-card h3 { - font-weight: 500; - min-height: 44px; - font-size: var(--text-label); - font-size: 16px; - font-weight: 500; - font-stretch: normal; - line-height: 1.38; - color: var(--neutral-900); -} - -.resource-card h6 { - font-size: var(--text-label); - font-weight: 400; - margin: 0; -} - -.resource-card { - height: 100%; -} - -.resource-card p { - margin-top: 16px; - margin-bottom: 16px; -} - -.resource-card img { - max-width: 100%; - margin: 0 0 16px; - height: 32px; -} - -.gapy-3 { - gap: 16px 0; -} - -.alttech-card { - border: 1px solid var(--neutral-200); - height: 100%; -} - -.alttech-title { -} - -.alttech-title h5 { - font-size: var(--text-body); - margin: 0; - color: var(--neutral-900); -} - -.alttech-title h6 { - font-size: var(--text-label-small); - font-weight: 400; -} - -.green-700 { - color: var(--green-700); -} - -.alttech-text p { - /*overflow: hidden;*/ - /*display: -webkit-box;*/ - font-size: 14px; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -} - -.sync-status { - gap: 28px 0; -} - -.sync-status-text { -} - -.sync-status-text h5 { - margin: 0; - font-size: var(--text-body); - color: var(--neutral-900); -} - -.sync-status-text h6 { - color: var(--neutral-600); - font-size: var(--text-label); - margin-bottom: 5px; - font-weight: 400; -} - -.sync-status-icon span{ - border-radius: 8px; - background:rgba(5, 81, 96, 0.10); - height: 32px; - width: 32px; - display: flex; - align-items: center; - justify-content: center; -} - -.view-more button { - background: transparent; - border: 0; -} - -/* ======================================================== - Components: Risk Table - ======================================================== */ -.risk-title-cell { - position: relative; - padding-left: 40px !important; - cursor: pointer; -} - -.chevron-icon { - position: absolute; - left: 16px; - top: 50%; - -webkit-transform: translateY(-50%); - -ms-transform: translateY(-50%); - transform: translateY(-50%); - font-size: var(--text-label-small); - color: var(--neutral-600); - -webkit-transition: -webkit-transform 0.2s ease; - transition: -webkit-transform 0.2s ease; - -o-transition: transform 0.2s ease; - transition: transform 0.2s ease; - transition: - transform 0.2s ease, - -webkit-transform 0.2s ease; -} - -.risk-row.expanded .chevron-icon { - -webkit-transform: translateY(-50%) rotate(90deg); - -ms-transform: translateY(-50%) rotate(90deg); - transform: translateY(-50%) rotate(90deg); -} - -.expandable-content { - display: none; - background-color: #f5f5f5; -} - -.expandable-content.show { - display: table-row; -} - -.expandable-content td { - padding-left: 20px; -} - -.risk-table-container .table tr th { - font-weight: 500; - font-size: var(--text-label); -} - -.risk-table-container .table th:first-child, -.risk-table-container .table td:first-child { - width: auto !important; -} - -.risk-table-container .table tr th:nth-child(3), -.risk-table-container .table tr th:nth-child(4) { - text-align: center; -} - -.impacted-count { - text-align: center; -} - -.description { - background: var(--neutral-200); - padding-top: 12px; - padding-bottom: 12px; -} - -.description-section { - margin-bottom: 10px; -} - -.risk-title { - color: var(--neutral-900); -} - -.section-label { - font-size: var(--text-label-small); - font-weight: 400; - color: var(--neutral-600); -} - -.section-content { - font-size: var(--text-label); -} - -.impacted-resources-content { - font-size: var(--text-label); -} - -.severity-badge { - color: var(--colors-red-800); - font-size: var(--text-label); - font-weight: 500; - padding: 3px 12px; - border-radius: var(--rounded-md); -} - -.severity-high { - background-color: var(--red-100); - color: var(--red-800); -} - -.severity-medium { - background: var(--yellow-100); - color: var(--yellow-850); -} - -.severity-low { - background: var(--blue-100); - color: var(--blue-800); -} - -.btn { - border-radius: 6px; - line-height: 40px; - padding: 0 16px; - font-weight: 500; - -webkit-transition: 0.4s; - -o-transition: 0.4s; - transition: 0.4s; -} - -.btn-primary { - background: var(--primary-800); - color: var(--white); -} - -.btn-outline-primary { - color: var(--primary-800); -} - -.btn-light { - background: var(--neutral-100); - border: 1px solid var(--neutral-100); - color: var(--neutral-600); -} - -.btn-outline-primary, -.btn-primary { - border: 1px solid var(--primary-800); -} - -.btn-sm { - font-size: var(--text-label); - font-weight: 500; - line-height: 34px; -} - -.btn-primary:hover { - background: var(--primary-950); - border-color: var(--primary-950); -} - -.btn-primary:hover svg:not(.filter-icon) path { - fill: var(--white); -} - -.btn-outline-primary:focus svg:not path, -.btn-outline-primary:active svg:not path, -.btn-outline-primary:hover svg:not path { - fill: var(--white); -} - -.btn-outline-primary:hover svg.filter-icon path { - stroke: var(--white); -} - -.btn-outline-primary:active, -.btn-outline-primary:hover { - background: var(--primary-800) !important; - border-color: var(--primary-800) !important; - color: var(--white) !important; -} -.btn-outline-primary:active svg path, -.btn-outline-primary:hover svg path { - fill: white; -} - -.btn-light:hover { - background: var(--neutral-200); - border-color: var(--neutral-200); -} - -.dropdown-toggle::after { - border: 0; - display: none; -} - -.dropdown-toggle { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - gap: 8px; - padding-left: 16px; - padding-right: 16px; -} - -.btn-sm.dropdown-toggle { - padding-left: 12px; - padding-right: 12px; -} - -.btn:focus-visible, -.btn.show:focus-visible, -.btn:first-child:active:focus-visible, -:not(.btn-check) + .btn:active:focus-visible { - -webkit-box-shadow: none !important; - box-shadow: none !important; -} - -.btn-primary, -.btn-outline-primary { - /* --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-focus-shadow-rgb: 13, 110, 253; - --bs-btn-disabled-bg: transparent; - --bs-btn-active-color: var(--neutral-white); - --bs-btn-hover-color: var(--neutral-white); - --bs-gradient: none; */ -} - -.btn-outline-primary { - --bs-btn-color: var(--primary-800); - --bs-btn-border-color: var(--primary-800); - --bs-btn-hover-color: var(--white); - --bs-btn-hover-bg: var(--primary-800); - --bs-btn-hover-border-color: var(--primary-800); - --bs-btn-active-color: var(--white); - --bs-btn-active-bg: var(--primary-800); - --bs-btn-active-border-color: var(--primary-800); - --bs-btn-disabled-color: var(--neutral-400); - --bs-btn-disabled-border-color: var(--neutral-300); - --bs-btn-focus-shadow-rgb: 17, 94, 89; -} - -.btn-outline-primary.show, -.btn-check:checked + .btn-outline-primary, -.btn-check:active + .btn-outline-primary, -.btn-outline-primary:focus, -.btn-outline-primary:focus-visible { - background: var(--primary-800) !important; - border-color: var(--primary-800) !important; - color: var(--white) !important; -} - -.btn-outline-primary.show svg.filter-icon path, -.btn-check:checked + .btn-outline-primary svg.filter-icon path, -.btn-check:active + .btn-outline-primary svg.filter-icon path, -.btn-outline-primary:focus svg.filter-icon path, -.btn-outline-primary:focus-visible svg.filter-icon path { - stroke: var(--white); -} - -.btn-primary.disabled, -.btn-primary:disabled { - border-color: var(--neutral-400); - background: var(--neutral-400); - color: var(--white); -} - -.btn-outline-primary.disabled, -.btn-outline-primary:disabled { - border-color: var(--neutral-300); - color: var(--neutral-400); -} - -/* ======================================================== - Components: Filter Toggles - ======================================================== */ -.toggle-switch { - position: relative; - width: 44px; - height: 24px; -} - -.toggle-switch input[type="checkbox"] { - opacity: 0; - width: 0; - height: 0; -} - -.toggle-slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: var(--neutral-200); - -webkit-transition: 0.3s ease; - -o-transition: 0.3s ease; - transition: 0.3s ease; - border-radius: var(--rounded-md); -} - -.toggle-slider:before { - position: absolute; - content: ""; - height: 20px; - width: 20px; - left: 2px; - bottom: 2px; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Ccircle cx='10' cy='10' r='9.25' fill='white' stroke='%23D1DBDB' stroke-width='1.5'/%3E%3Cpath d='M6.5 6.5L13.5 13.5M13.5 6.5L6.5 13.5' stroke='%234B6361' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E"); -} - -.toggle-slider:before { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Ccircle cx='10' cy='10' r='9.25' fill='white' stroke='%23D1DBDB' stroke-width='1.5'/%3E%3Cpath d='M6.5 6.5L13.5 13.5M13.5 6.5L6.5 13.5' stroke='%234B6361' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: center; - background-size: 20px 20px; -} - -input:checked + .toggle-slider { - background-color: var(--primary-800); -} - -input:checked + .toggle-slider:before { - -webkit-transform: translateX(20px); - -ms-transform: translateX(20px); - transform: translateX(20px); - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Ccircle cx='10' cy='10' r='9.25' fill='white' stroke='%230D948B' stroke-width='1.5'/%3E%3Cpath d='M5.5 10.5L8.5 13.5L14.5 7.5' stroke='%230D948B' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: center; - background-size: 20px 20px; -} - -/* ======================================================== - Components: Tables - ======================================================== */ -table:not("#assessmentsTable") { - table-layout: fixed; - width: 100%; -} - -.table > :not(caption) > * > * { - color: var(--neutral-900); - font-size: var(--text-body); - padding-top: 16px; - padding-bottom: 16px; - border-color: var(--neutral-200); -} - -tr:last-child td { - border-bottom: 0; -} - -.risk-table-container { - overflow-x: auto; - -webkit-overflow-scrolling: touch; -} - -.risk-table-container table { - min-width: 420px; -} - -.risk-table-container::-webkit-scrollbar { - height: 20px; -} - -.risk-table-container::-webkit-scrollbar-track { - background: var(--white); -} - -.risk-table-container::-webkit-scrollbar-thumb { - background: var(--neutral-300); - border-radius: 20px; -} - -.risk-table-container::-webkit-scrollbar-thumb:hover { - background: var(--neutral-600); -} - -th:first-child, -td:first-child { - width: 50px !important; - font-weight: 500; -} - -td .number, -td button { - margin-left: auto; - margin-right: auto; -} - -/* ======================================================== - Layout: Main Content And Forms - ======================================================== */ - -#main-content { - height: 100%; - width: 100%; - overflow: hidden; - padding: 20px 60px 20px 60px; - transition: all 0.3s linear; - -webkit-transition: all 0.3s linear; -} - -.visit span { - font-size: 14px; - word-break: break-all; -} - -.form-control, -.form-select { - color: var(--neutral-600); - font-size: var(--text-body); - font-style: normal; - font-weight: 400; - border-radius: 8px; - border: 1px solid var(--neutral-300); - background: var(--white); - -webkit-transition: border-color 0.3s ease, box-shadow 0.3s ease; - -o-transition: border-color 0.3s ease, box-shadow 0.3s ease; - transition: border-color 0.3s ease, box-shadow 0.3s ease; - -webkit-box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); -} - -.form-select { - -webkit-appearance: none; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 6L8 10L12 6' stroke='%23115E59' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 12px center; - background-size: 16px 16px; - padding-right: 40px; -} - -.form-control:focus, -.form-select:focus { - border-color: var(--primary-800); - box-shadow: 0 0 0 0.2rem rgba(17, 94, 89, 0.12); -} - -.custom-search { - min-width: 240px; - height: 36px; - border-radius: 8px; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M9 3.5C5.96243 3.5 3.5 5.96243 3.5 9C3.5 12.0376 5.96243 14.5 9 14.5C10.519 14.5 11.893 13.8852 12.8891 12.8891C13.8852 11.893 14.5 10.519 14.5 9C14.5 5.96243 12.0376 3.5 9 3.5ZM2 9C2 5.13401 5.13401 2 9 2C12.866 2 16 5.13401 16 9C16 10.6625 15.4197 12.1906 14.4517 13.3911L17.7803 16.7197C18.0732 17.0126 18.0732 17.4874 17.7803 17.7803C17.4874 18.0732 17.0126 18.0732 16.7197 17.7803L13.3911 14.4517C12.1906 15.4197 10.6625 16 9 16C5.13401 16 2 12.866 2 9Z' fill='%236B807F'/%3E%3C/svg%3E") !important; - background-repeat: no-repeat !important; - background-position: 16px center !important; - padding: 0 16px 0 48px !important; - width: 400px; -} - -.btn-clear:hover { - text-decoration: underline; -} - -.alt-tech-card { - background: #f5f5f5; - border-radius: 10px; -} - -.alt-tech-card h3 { - font-size: 18px; - margin: 0; -} - -/* ======================================================== - Components: Alternative Technology Status And Hints - ======================================================== */ - -.verified { - background: var(--green-600); - color: var(--white); -} -.green-100 { - background: var(--green-100); -} -.green-700 { - color: var(--green-700); -} -.tags span { - background: var(--neutral-100); - padding: 4px 8px; - border-radius: 6px; - font-size: var(--text-label); - color: var(--neutral-700); - font-weight: 500; -} -.verified span { - font-size: var(--text-label); -} -.red-700 { - color: var(--red-700); -} -.red-50 { - background: var(--red-50); -} - -.info-hint-box { - position: relative; - display: inline-block; - margin-left: 6px; - cursor: pointer; -} -.info-hint-box i { - font-size: 16px; - color: var(--primary-600, #007bff); - transition: color 0.2s; -} -.info-hint-box:hover i { - color: var(--primary-800, #0056b3); -} -.hint-hoverbox { - display: none; - position: absolute; - top: 28px; - right: 0; - z-index: 1000; - background: #fff; - border: 1px solid var(--neutral-200, #e5e7eb); - border-radius: 8px; - padding: 12px 14px; - width: 280px; - box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1); -} -.info-hint-box:hover .hint-hoverbox { - display: block; -} -.hint-hoverbox { - opacity: 0; - visibility: hidden; - transition: all 0.2s ease-in-out; -} -.info-hint-box:hover .hint-hoverbox { - opacity: 1; - visibility: visible; -} -.hint-hoverbox h5 { - font-size: 14px; - font-weight: 600; - margin-bottom: 6px; -} -.hint-hoverbox p { - font-size: 13px; - color: #555; - margin: 0; - line-height: 1.4; -} - -/* ======================================================== - Responsive - ======================================================== */ - -@media (max-width: 991px) { - - .alt-tech-card div { - width: 100%; - /* justify-content: flex-end; */ - } - - .alt-tech-card { - -ms-flex-wrap: wrap; - flex-wrap: wrap; - gap: 16px; - } - - .chart-card-head { - padding-left: 16px; - padding-right: 16px; - } - - #main-content { - margin-left: 0; - } -} - -@media (max-width: 767px) { - main { - padding-top: 24px; - } - - .btn { - padding-left: 12px; - padding-right: 12px; - } - - main { - padding-top: 88px; - } - -} - -@media (max-width: 575px) { - -} +/* ======================================================== + Base: Variables + ======================================================== */ +:root { + /* Blue */ + --blue-100: #dbe6fe; + --blue-800: #1e4baf; + + /* Green */ + --green-100: #dcfce7; + --green-600: #16a34a; + --green-700: #047854; + + /* Neutral */ + --neutral-50: #f9fbfb; + --neutral-100: #f3f6f6; + --neutral-200: #e5ebeb; + --neutral-300: #d1dbdb; + --neutral-400: #9cafae; + --neutral-600: #4b6361; + --neutral-800: #1f3735; + --neutral-900: #112726; + + /* Primary */ + --primary-600: #0d948b; + --primary-800: #115e59; + --primary-950: #042f2c; + + /* Red */ + --red-50: #fef2f2; + --red-100: #fee2e2; + --red-700: #b91c1c; + --red-800: #991b1b; + + /* Yellow */ + --yellow-100: #fee4c7; + --yellow-850: #92400e; + /* Color Palette */ + --white: #fff; + /* Transition Defaults */ + --transition-speed: 0.3s; + + /* Font Sizes */ + --text-heading-2: 24px; + --text-heading-3: 20px; + --text-body: 16px; + --text-label: 14px; + --text-label-small: 12px; + /* Radius */ + --rounded-md: 12px; + --rounded-sm: 8px; + --rounded-xs: 4px; + /* Sidebar */ +} + +/* ======================================================== + Base: Reset + ======================================================== */ +* { + margin: 0; + padding: 0; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +body { + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; + line-height: 1.375; + font-weight: 400; + color: var(--neutral-900); + background: var(--neutral-100); + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; +} + +img { + max-width: 100%; + height: auto; +} + +ul { + list-style: none; + padding: 0; + margin: 0; +} + +.divider-border { + height: 1px; + background: var(--neutral-200); +} + +.container { +} + +/* ======================================================== + Base: Typography + ======================================================== */ +a { + text-decoration: none; + -webkit-transition: all var(--transition-speed) ease; + -o-transition: all var(--transition-speed) ease; + transition: all var(--transition-speed) ease; +} + +a:hover { + text-decoration: none !important; +} + + +h2, +h3 { + color: var(--neutral-900); + font-weight: 500; +} + + +h2, +h3, +li, +p { + margin: 0; +} + + +h2 { + font-size: var(--text-heading-2); + line-height: 1.33; +} + + +h3 { + font-size: var(--text-heading-3); + line-height: 1.4; +} + + +body, +p { + font-size: var(--text-body); + line-height: 1.375; +} + +.label, +label { + font-size: var(--text-label); + line-height: 1.43; +} + +.small, +label.small { + font-size: var(--text-label-small); + line-height: 1.33; +} + +.bg-white { + background: var(--white); +} + +a*:active, +a*:focus { + outline: 0; + border: 0; +} + +main { + padding-bottom: 32px; + padding-top: 88px; +} + +/* Scrollbars */ +::-webkit-scrollbar { + width: 5px; + border-radius: var(--rounded-sm); +} + +::-webkit-scrollbar-track { + background: var(--neutral-100); + border-radius: var(--rounded-sm); +} + +::-webkit-scrollbar-thumb { + background: var(--primary-600); + border-radius: var(--rounded-sm); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--primary-800); + border-radius: var(--rounded-sm); +} + +.dropdown-item span { + font-size: var(--text-label); + font-weight: 400; + color: var(--neutral-900); +} + +.dropdown-item { + padding-top: 8px; + padding-bottom: 8px; +} + +.dropdown-menu { + padding: 0; +} + +.dropdown-menu li a { + line-height: 1.5; +} + +/* ======================================================== + Layout: Shared Cards And Charts + ======================================================== */ + +.chart-card-head { + padding: 16px 24px; +} + +.chart-card-head h3 { + margin-bottom: 5px; + color: var(--neutral-900); +} +.chart-card-head h6 { + color: var(--neutral-600); + font-size: var(--text-label); + font-weight: 400; +} +.chart-card-head h6 span { + display: inline-block; + font-weight: 500; +} + +.risk-header { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + /* padding: 16px 24px; */ +} + +.risk-dashboard { + display: flex; + flex-direction: column; + height: 100%; +} + +.risk-count h2 { + color: var(--neutral-600); + font-size: var(--text-label); + font-weight: 400; +} + +.risk-count .count { + font-size: var(--text-heading-3); + font-weight: 500; + color: var(--neutral-900); + margin: 0; +} + +.chart-container { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + flex: 1; +} + +.chart-box { + height: 350px; +} + +.chart-empty-state { + flex: 1; + min-height: 220px; + display: flex; + align-items: center; + justify-content: center; + text-align: center; +} + +.chart-empty-state-inner { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.chart-empty-state i { + font-size: 28px; + line-height: 1; + color: var(--neutral-900); +} + +.chart-empty-state p { + margin: 0; + color: var(--neutral-700); +} + +.alt-tech-empty-state { + min-height: 260px; +} + +.scoring-empty-state { + min-height: 300px; +} + +.scoring-card { + overflow: hidden; +} + +.scoring-inner { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + min-height: 300px; +} + +.chart-wrapper { + max-width: 350px; + overflow: hidden; + position: relative; + border-radius: 12px; + mask-image: linear-gradient(to bottom, black 85%, transparent 100%); + -webkit-mask-image: linear-gradient(to bottom, black 85%, transparent 100%); +} + +.scoring { + width: 100%; + max-width: 420px; + height: 320px !important; + margin: 0 auto; +} + +.form-title { + font-size: var(--text-heading-3); + font-weight: 500; + line-height: 1.33; + color: var(--neutral-900); +} + +.shadow-s { + -webkit-box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1); +} + +.input { + border: 1px solid var(--neutral-300); +} + +/* ======================================================== + Components: Buttons + ======================================================== */ + +.dropdown-toggle::after { + float: right; + margin-top: 8px; +} + +/* ======================================================== + Components: Summary, Scoring, Resources, Alt Tech Cards + ======================================================== */ +::-webkit-input-placeholder { + color: var(--neutral-600); + font-size: var(--text-body); + font-style: normal; + font-weight: 400; + line-height: 38px; +} + +::-moz-placeholder { + color: var(--neutral-600); + font-size: var(--text-body); + font-style: normal; + font-weight: 400; + line-height: 38px; +} + +.resource-card { + border: 1px solid var(--neutral-200); +} + +.resource-card h3 { + font-weight: 500; + min-height: 44px; + font-size: var(--text-label); + font-size: 16px; + font-weight: 500; + font-stretch: normal; + line-height: 1.38; + color: var(--neutral-900); +} + +.resource-card h6 { + font-size: var(--text-label); + font-weight: 400; + margin: 0; +} + +.resource-card { + height: 100%; +} + +.resource-card p { + margin-top: 16px; + margin-bottom: 16px; +} + +.resource-card img { + max-width: 100%; + margin: 0 0 16px; + height: 32px; +} + +.resource-icon-placeholder { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + margin: 0 0 16px; + border-radius: 7px; + background: #e3e7ea; + color: #4a5560; + font-size: 13px; + font-weight: 600; + line-height: 1; +} + +.gapy-3 { + gap: 16px 0; +} + +.alttech-card { + border: 1px solid var(--neutral-200); + height: 100%; +} + +.alttech-title { +} + +.alttech-title h5 { + font-size: var(--text-body); + margin: 0; + color: var(--neutral-900); +} + +.alttech-title h6 { + font-size: var(--text-label-small); + font-weight: 400; +} + +.green-700 { + color: var(--green-700); +} + +.alttech-text p { + /*overflow: hidden;*/ + /*display: -webkit-box;*/ + font-size: 14px; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.sync-status { + gap: 28px 0; +} + +.sync-status-text { +} + +.sync-status-text h5 { + margin: 0; + font-size: var(--text-body); + color: var(--neutral-900); +} + +.sync-status-text h6 { + color: var(--neutral-600); + font-size: var(--text-label); + margin-bottom: 5px; + font-weight: 400; +} + +.sync-status-icon span{ + border-radius: 8px; + background:rgba(5, 81, 96, 0.10); + height: 32px; + width: 32px; + display: flex; + align-items: center; + justify-content: center; +} + +.view-more button { + background: transparent; + border: 0; +} + +/* ======================================================== + Components: Risk Table + ======================================================== */ +.risk-title-cell { + position: relative; + padding-left: 40px !important; + cursor: pointer; +} + +.chevron-icon { + position: absolute; + left: 16px; + top: 50%; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + font-size: var(--text-label-small); + color: var(--neutral-600); + -webkit-transition: -webkit-transform 0.2s ease; + transition: -webkit-transform 0.2s ease; + -o-transition: transform 0.2s ease; + transition: transform 0.2s ease; + transition: + transform 0.2s ease, + -webkit-transform 0.2s ease; +} + +.risk-row.expanded .chevron-icon { + -webkit-transform: translateY(-50%) rotate(90deg); + -ms-transform: translateY(-50%) rotate(90deg); + transform: translateY(-50%) rotate(90deg); +} + +.expandable-content { + display: none; + background-color: #f5f5f5; +} + +.expandable-content.show { + display: table-row; +} + +.expandable-content td { + padding-left: 20px; +} + +.risk-table-container .table tr th { + font-weight: 500; + font-size: var(--text-label); +} + +.risk-table-container .table th:first-child, +.risk-table-container .table td:first-child { + width: auto !important; +} + +.risk-table-container .table tr th:nth-child(3), +.risk-table-container .table tr th:nth-child(4) { + text-align: center; +} + +.impacted-count { + text-align: center; +} + +.description { + background: var(--neutral-200); + padding-top: 12px; + padding-bottom: 12px; +} + +.description-section { + margin-bottom: 10px; +} + +.risk-title { + color: var(--neutral-900); +} + +.section-label { + font-size: var(--text-label-small); + font-weight: 400; + color: var(--neutral-600); +} + +.section-content { + font-size: var(--text-label); +} + +.impacted-resources-content { + font-size: var(--text-label); +} + +.severity-badge { + color: var(--colors-red-800); + font-size: var(--text-label); + font-weight: 500; + padding: 3px 12px; + border-radius: var(--rounded-md); +} + +.severity-high { + background-color: var(--red-100); + color: var(--red-800); +} + +.severity-medium { + background: var(--yellow-100); + color: var(--yellow-850); +} + +.severity-low { + background: var(--blue-100); + color: var(--blue-800); +} + +.btn { + border-radius: 6px; + line-height: 40px; + padding: 0 16px; + font-weight: 500; + -webkit-transition: 0.4s; + -o-transition: 0.4s; + transition: 0.4s; +} + +.btn-primary { + background: var(--primary-800); + color: var(--white); +} + +.btn-outline-primary { + color: var(--primary-800); +} + +.btn-light { + background: var(--neutral-100); + border: 1px solid var(--neutral-100); + color: var(--neutral-600); +} + +.btn-outline-primary, +.btn-primary { + border: 1px solid var(--primary-800); +} + +.btn-sm { + font-size: var(--text-label); + font-weight: 500; + line-height: 34px; +} + +.btn-primary:hover { + background: var(--primary-950); + border-color: var(--primary-950); +} + +.btn-primary:hover svg:not(.filter-icon) path { + fill: var(--white); +} + +.btn-outline-primary:focus svg:not path, +.btn-outline-primary:active svg:not path, +.btn-outline-primary:hover svg:not path { + fill: var(--white); +} + +.btn-outline-primary:hover svg.filter-icon path { + stroke: var(--white); +} + +.btn-outline-primary:active, +.btn-outline-primary:hover { + background: var(--primary-800) !important; + border-color: var(--primary-800) !important; + color: var(--white) !important; +} +.btn-outline-primary:active svg path, +.btn-outline-primary:hover svg path { + fill: white; +} + +.btn-light:hover { + background: var(--neutral-200); + border-color: var(--neutral-200); +} + +.dropdown-toggle::after { + border: 0; + display: none; +} + +.dropdown-toggle { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + gap: 8px; + padding-left: 16px; + padding-right: 16px; +} + +.btn-sm.dropdown-toggle { + padding-left: 12px; + padding-right: 12px; +} + +.btn:focus-visible, +.btn.show:focus-visible, +.btn:first-child:active:focus-visible, +:not(.btn-check) + .btn:active:focus-visible { + -webkit-box-shadow: none !important; + box-shadow: none !important; +} + +.btn-primary, +.btn-outline-primary { + /* --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-focus-shadow-rgb: 13, 110, 253; + --bs-btn-disabled-bg: transparent; + --bs-btn-active-color: var(--neutral-white); + --bs-btn-hover-color: var(--neutral-white); + --bs-gradient: none; */ +} + +.btn-outline-primary { + --bs-btn-color: var(--primary-800); + --bs-btn-border-color: var(--primary-800); + --bs-btn-hover-color: var(--white); + --bs-btn-hover-bg: var(--primary-800); + --bs-btn-hover-border-color: var(--primary-800); + --bs-btn-active-color: var(--white); + --bs-btn-active-bg: var(--primary-800); + --bs-btn-active-border-color: var(--primary-800); + --bs-btn-disabled-color: var(--neutral-400); + --bs-btn-disabled-border-color: var(--neutral-300); + --bs-btn-focus-shadow-rgb: 17, 94, 89; +} + +.btn-outline-primary.show, +.btn-check:checked + .btn-outline-primary, +.btn-check:active + .btn-outline-primary, +.btn-outline-primary:focus, +.btn-outline-primary:focus-visible { + background: var(--primary-800) !important; + border-color: var(--primary-800) !important; + color: var(--white) !important; +} + +.btn-outline-primary.show svg.filter-icon path, +.btn-check:checked + .btn-outline-primary svg.filter-icon path, +.btn-check:active + .btn-outline-primary svg.filter-icon path, +.btn-outline-primary:focus svg.filter-icon path, +.btn-outline-primary:focus-visible svg.filter-icon path { + stroke: var(--white); +} + +.btn-primary.disabled, +.btn-primary:disabled { + border-color: var(--neutral-400); + background: var(--neutral-400); + color: var(--white); +} + +.btn-outline-primary.disabled, +.btn-outline-primary:disabled { + border-color: var(--neutral-300); + color: var(--neutral-400); +} + +/* ======================================================== + Components: Filter Toggles + ======================================================== */ +.toggle-switch { + position: relative; + width: 44px; + height: 24px; +} + +.toggle-switch input[type="checkbox"] { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--neutral-200); + -webkit-transition: 0.3s ease; + -o-transition: 0.3s ease; + transition: 0.3s ease; + border-radius: var(--rounded-md); +} + +.toggle-slider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 2px; + bottom: 2px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Ccircle cx='10' cy='10' r='9.25' fill='white' stroke='%23D1DBDB' stroke-width='1.5'/%3E%3Cpath d='M6.5 6.5L13.5 13.5M13.5 6.5L6.5 13.5' stroke='%234B6361' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E"); +} + +.toggle-slider:before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Ccircle cx='10' cy='10' r='9.25' fill='white' stroke='%23D1DBDB' stroke-width='1.5'/%3E%3Cpath d='M6.5 6.5L13.5 13.5M13.5 6.5L6.5 13.5' stroke='%234B6361' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center; + background-size: 20px 20px; +} + +input:checked + .toggle-slider { + background-color: var(--primary-800); +} + +input:checked + .toggle-slider:before { + -webkit-transform: translateX(20px); + -ms-transform: translateX(20px); + transform: translateX(20px); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Ccircle cx='10' cy='10' r='9.25' fill='white' stroke='%230D948B' stroke-width='1.5'/%3E%3Cpath d='M5.5 10.5L8.5 13.5L14.5 7.5' stroke='%230D948B' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center; + background-size: 20px 20px; +} + +/* ======================================================== + Components: Tables + ======================================================== */ +table:not("#assessmentsTable") { + table-layout: fixed; + width: 100%; +} + +.table > :not(caption) > * > * { + color: var(--neutral-900); + font-size: var(--text-body); + padding-top: 16px; + padding-bottom: 16px; + border-color: var(--neutral-200); +} + +tr:last-child td { + border-bottom: 0; +} + +.risk-table-container { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.risk-table-container table { + min-width: 420px; +} + +.risk-table-container::-webkit-scrollbar { + height: 20px; +} + +.risk-table-container::-webkit-scrollbar-track { + background: var(--white); +} + +.risk-table-container::-webkit-scrollbar-thumb { + background: var(--neutral-300); + border-radius: 20px; +} + +.risk-table-container::-webkit-scrollbar-thumb:hover { + background: var(--neutral-600); +} + +th:first-child, +td:first-child { + width: 50px !important; + font-weight: 500; +} + +td .number, +td button { + margin-left: auto; + margin-right: auto; +} + +/* ======================================================== + Layout: Main Content And Forms + ======================================================== */ + +#main-content { + height: 100%; + width: 100%; + overflow: hidden; + padding: 20px 60px 20px 60px; + transition: all 0.3s linear; + -webkit-transition: all 0.3s linear; +} + +.visit span { + font-size: 14px; + word-break: break-all; +} + +.form-control, +.form-select { + color: var(--neutral-600); + font-size: var(--text-body); + font-style: normal; + font-weight: 400; + border-radius: 8px; + border: 1px solid var(--neutral-300); + background: var(--white); + -webkit-transition: border-color 0.3s ease, box-shadow 0.3s ease; + -o-transition: border-color 0.3s ease, box-shadow 0.3s ease; + transition: border-color 0.3s ease, box-shadow 0.3s ease; + -webkit-box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); +} + +.form-select { + -webkit-appearance: none; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 6L8 10L12 6' stroke='%23115E59' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + background-size: 16px 16px; + padding-right: 40px; +} + +.form-control:focus, +.form-select:focus { + border-color: var(--primary-800); + box-shadow: 0 0 0 0.2rem rgba(17, 94, 89, 0.12); +} + +.custom-search { + min-width: 240px; + height: 36px; + border-radius: 8px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M9 3.5C5.96243 3.5 3.5 5.96243 3.5 9C3.5 12.0376 5.96243 14.5 9 14.5C10.519 14.5 11.893 13.8852 12.8891 12.8891C13.8852 11.893 14.5 10.519 14.5 9C14.5 5.96243 12.0376 3.5 9 3.5ZM2 9C2 5.13401 5.13401 2 9 2C12.866 2 16 5.13401 16 9C16 10.6625 15.4197 12.1906 14.4517 13.3911L17.7803 16.7197C18.0732 17.0126 18.0732 17.4874 17.7803 17.7803C17.4874 18.0732 17.0126 18.0732 16.7197 17.7803L13.3911 14.4517C12.1906 15.4197 10.6625 16 9 16C5.13401 16 2 12.866 2 9Z' fill='%236B807F'/%3E%3C/svg%3E") !important; + background-repeat: no-repeat !important; + background-position: 16px center !important; + padding: 0 16px 0 48px !important; + width: 400px; +} + +.btn-clear:hover { + text-decoration: underline; +} + +.alt-tech-card { + background: #f5f5f5; + border-radius: 10px; +} + +.alt-tech-card h3 { + font-size: 18px; + margin: 0; +} + +/* ======================================================== + Components: Alternative Technology Status And Hints + ======================================================== */ + +.verified { + background: var(--green-600); + color: var(--white); +} +.green-100 { + background: var(--green-100); +} +.green-700 { + color: var(--green-700); +} +.tags span { + background: var(--neutral-100); + padding: 4px 8px; + border-radius: 6px; + font-size: var(--text-label); + color: var(--neutral-700); + font-weight: 500; +} +.verified span { + font-size: var(--text-label); +} +.red-700 { + color: var(--red-700); +} +.red-50 { + background: var(--red-50); +} + +.info-hint-box { + position: relative; + display: inline-block; + margin-left: 6px; + cursor: pointer; +} +.info-hint-box i { + font-size: 16px; + color: var(--primary-600, #007bff); + transition: color 0.2s; +} +.info-hint-box:hover i { + color: var(--primary-800, #0056b3); +} +.hint-hoverbox { + display: none; + position: absolute; + top: 28px; + right: 0; + z-index: 1000; + background: #fff; + border: 1px solid var(--neutral-200, #e5e7eb); + border-radius: 8px; + padding: 12px 14px; + width: 280px; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1); +} +.info-hint-box:hover .hint-hoverbox { + display: block; +} +.hint-hoverbox { + opacity: 0; + visibility: hidden; + transition: all 0.2s ease-in-out; +} +.info-hint-box:hover .hint-hoverbox { + opacity: 1; + visibility: visible; +} +.hint-hoverbox h5 { + font-size: 14px; + font-weight: 600; + margin-bottom: 6px; +} +.hint-hoverbox p { + font-size: 13px; + color: #555; + margin: 0; + line-height: 1.4; +} + +/* ======================================================== + Responsive + ======================================================== */ + +@media (max-width: 991px) { + + .alt-tech-card div { + width: 100%; + /* justify-content: flex-end; */ + } + + .alt-tech-card { + -ms-flex-wrap: wrap; + flex-wrap: wrap; + gap: 16px; + } + + .chart-card-head { + padding-left: 16px; + padding-right: 16px; + } + + #main-content { + margin-left: 0; + } +} + +@media (max-width: 767px) { + main { + padding-top: 24px; + } + + .btn { + padding-left: 12px; + padding-right: 12px; + } + + main { + padding-top: 88px; + } + +} + +@media (max-width: 575px) { + +} diff --git a/core/assets/icons/misc/no_image.png b/core/assets/icons/misc/no_image.png deleted file mode 100644 index ad786e5544d6f39d033a776803e316ec8d1237bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5151 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 diff --git a/core/assets/icons/severity/high.png b/core/assets/icons/severity/high.png deleted file mode 100644 index 243b7ac6306eb718d661519be3c419fb69625546..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 882 zcmV-&1C9KNP)j%QsbgqYjo2DI=PsLiHlQlaH^(Fm*UnYMUyVA zE=~mpCm2Lc%49fRd1oFrDRfGX|sJ`(NB*YlIWx6aBS2fsZ}k$x(YC{ICTIq zfUWaJ-rdFX7Z<@A_x^#2t+Tm9)S(bUghX%eM)BGZl*ObBQ-^~&ytVcBD;|oR9%FFD zF6w+_gv0S1F?b(yHrqN`MC>M;r8c9BV-U%ZG-Wjo%4T-|+S{Y|V`G#}r>pKoW@qW! z^0JZ>2M6?Na*`6!=zsq%_O}3jlrgQKX)|OIx`&3yGcrQmJ|DFk%qwW;4D*UCg45+9 z-_#VjdwPnxbSy@{<8h?}@juBVxjY_q5C-7NNw)e;E_c75?>7tPHDTxPXJip_?6yan zo8;cvQAA?9yUO(-!=Ym=gTV0IT#+1zMORkT!NBdY=cT^CNq9ldbRvoBu~iB@kgLW? zLl0sjOG`AmxTthzkqDh^Z+0aCo!gZo&Mbbi7$^sboA0ZuG|F)YAeOAMoEnL=LZNN_C zJ)NFb#T~@Pf->c$y~2PFb^??oV>bjia*r8AM{8zFefj(OxIi%kVtx% z6l$KC4x}EcMjiu9FvJAcsYAiUHkeq*xUPDKqFd0i5y&EBbYf-6vF-~Z?y6zM*Yx=D zlZVrVS}%x1qg^`58GUjpa1{>07*qo IM6N<$g4&#wt^fc4 diff --git a/core/assets/icons/severity/low.png b/core/assets/icons/severity/low.png deleted file mode 100644 index eb799613fa6cdc7e8e37a6d22c8a14062f73b68e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 895 zcmV-_1AzRAP)wO^#TO+U=K9$pqw=6O%s8WN07ubR1=MO;bi2b(KAL)f*urO zy@)9oV@VLGn$Q|CsR{NV+m17{ZEb5y=%$b^U-Gj1-p(fT?Kkt@%qrLdtE*V6AAmtX z^dLZkq5->)D2;gloU$wJX`@qx3K^<=i>qO5&=DWdtp~~s3h|*$(|q|zX}Jr_5ga5U z0VqQv3{bin71EIF2dPMQ&2|m@G+w4n79ag z0Q$bf5QUk#06Tz%;JPTtBl07bUY~jWHEaI)%P(2WM6S)Ypi~d%Ob3)0ckTyZ_@*EH z?;|oU559oOXUkwX`VF2;ucU47J^lz|WWVrfUFJ-V=Nqw2C|`eTyeA|-ZVaE6grAu;$g+diuXd*G+}%tn zK&1ZV28R}tVb#n)r(Q>Dv!%7fOW1*u|ND9jsw*o@e(CLldz_YIwgDj%q>{#N;X+fV zLerHhG+olQp${wx^tqBd7Mu_}vP4+U3|Na3iNjs{_y(NgUBAG-Od4Fhd z4%sj_kSO9O$N>Z-=Hgu9807Gy_+SMH5MdARd$u=ryf(xJ^Ams4$g?|dc4xo& z-g`4+MLD2nC#$LK@qVqTjV~J#_PN|Ptg4ryUW%oN14LVAHbG41u zFf`F|!$t#E1R5E}w=eD2Q9Vg(oHT${wOdEkWHjXMv1nU{p1XWd>sUXPkO8HrU)8k? znAgdQC@rSlR6Ts)@1JykW8kOL~BYGeWa zCz2)^qBc#tSJVB<&`J8EtDVMA9JOq}KYvf!Q3?V2`^QJ6KG^pY4gS{s29QT>Sf&}V zsNO|+Vwyk7>O-!0PSDFnUGt|@vtc8ZS5(mJM$Eq0V^^fUr94o+?gRF2&E$=cv!gSu zE#$vJl{K}Zyj0BbeG~vhprj&j%}e2%0qNVkZAV&L<7ch(WbuL2@!XK#$M?Q4iNtf! z4!hL$>5C&Wxyfgup1cQJ$A(5)EtE38wzJ=E%CZj5i83BAUhH2%99#;2bJR*JTnw|U*R`Z3CwD&>XaWYzIA$*UaNSE)jt|-9u8gO%?{LNhz5p8Rg(L+G(l_jp*-pNE`obtR zfAtMjI%-Wj5Q#x<-+RDRQj>oNc@F(7MBzh0+P1EJ<|K@tJZh>lUsN(dJA3v?-E-&{ z@}2RRV}LsU?fqT|&xbgJ3P9Z5oTjos&>YWt(+W46zcnMjwz3sLUt;JR3l*_5X+bK0IH(JXLqTx=va378#<6~*{6a;kE{wPb z72@6!6A$u}F$+mzHAp6o;y_OsyoNhZ1wdS^-6in@A70;-884tfC=tqpJBvCK$)d`U z$e^5|$BrEl!r;FDbMnZx^r%XZKQ%lg_5oIM;HsC31vz#yo<1&kf9mXrF9jtj)YMwq zxPCn>N#Gw2$banx(L=Pew_og0w*PoQV&Pz4w`AN+oN>$Ew{}<9BOCjMPl-Je6y9Ad z$XOjKvCza>D$+3$)VB2KL7K~^0`U-py~slMdCz(k(4>yr>ObF8+T^banm74sd6SQd zCS!m~$O%rQ=4r6Gj$`?`IZ>3z%K|>&9Y~(V0U3L(Xy{UpqS#w;%v2oc2$Yse8j$tR znpl*Niiv)pwT*MiMtiLOtjW2%j}xeO#ZOS=FyLKRVGsptTjS|_ZuRlzNH)V~*tQE7 TEA1jF00000NkvXXu0mjfH7PmO diff --git a/core/assets/template/egress.html b/core/assets/template/egress.html index 5353767..b8c3249 100644 --- a/core/assets/template/egress.html +++ b/core/assets/template/egress.html @@ -49,6 +49,21 @@ margin: 0 8px; object-fit: contain; } + .resource-icon-placeholder { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + margin: 0 8px; + border-radius: 5px; + background: #e3e7ea; + color: #4a5560; + font-size: 10px; + font-weight: 600; + line-height: 1; + vertical-align: middle; + } diff --git a/cloudexit/assets/template/index.html b/cloudexit/assets/template/index.html index 2c787c1..0087909 100644 --- a/cloudexit/assets/template/index.html +++ b/cloudexit/assets/template/index.html @@ -294,6 +294,9 @@

Costs Overview

{% endif %} + {% if summary.costs_egress_note %} +

{{ summary.costs_egress_note }}

+ {% endif %} diff --git a/cloudexit/cli/app.py b/cloudexit/cli/app.py index 8667119..69e218c 100644 --- a/cloudexit/cli/app.py +++ b/cloudexit/cli/app.py @@ -29,10 +29,7 @@ create_subscription_client, ) from cloudexit.egress.estimate import estimate_egress -from cloudexit.report.egress import ( - generate_egress_html_report, - generate_egress_pdf_report, -) +from cloudexit.report.egress import generate_egress_html_report from cloudexit.platform.client import ApiError, check_key from cloudexit.platform.run_output import build_run_json, exit_code_of from cloudexit.platform.status import host_catalogue_lines, run_status @@ -992,7 +989,6 @@ def run_assessment( egress_collected = False egress_json_path = None egress_html_path = None - egress_pdf_path = None if egress: console.print("Stage #5 – Egress Estimation", style="bold") @@ -1014,7 +1010,10 @@ def run_assessment( with console.status("Generating egress report...", spinner="dots"): egress_report_result = generate_egress_html_report( - report_path, egress_json_path + report_path, + egress_json_path, + config["providerDetails"], + sync_id, ) if egress_report_result["success"]: @@ -1027,22 +1026,6 @@ def run_assessment( logs=egress_report_result["logs"], ) egress_failed = True - - with console.status("Generating egress PDF...", spinner="dots"): - egress_pdf_result = generate_egress_pdf_report( - report_path, egress_json_path, config["providerDetails"] - ) - - if egress_pdf_result["success"]: - print_step("Generating egress PDF...", status="ok") - egress_pdf_path = egress_pdf_result.get("pdf_path") - else: - print_step( - "Generating egress PDF...", - status="error", - logs=egress_pdf_result["logs"], - ) - egress_failed = True else: print_step( "Estimating egress data...", @@ -1170,6 +1153,7 @@ def run_assessment( raw_data_path, upload=report_info(upload_result), sync_id=sync_id, + egress_json_path=egress_json_path, ) # Handle the result @@ -1181,6 +1165,20 @@ def run_assessment( ) sys.exit(codes.REPORT) + # The egress PDF is written with the report, after the upload, so it can + # show the run and the platform link. Its failure is an egress failure, + # not a report failure. + egress_pdf_path = report_status.get("reports", {}).get("EGRESS_PDF") + if egress_pdf_path: + print_step("Generating egress PDF...", status="ok") + elif egress_json_path: + print_step( + "Generating egress PDF...", + status="error", + logs=report_status.get("egress_pdf_logs", "no egress PDF was written"), + ) + egress_failed = True + # Output the report path after the separator if not (dry_run or non_interactive): console.print("-------------------------------------------") diff --git a/cloudexit/engine.py b/cloudexit/engine.py index 2c3efa6..f5da2a8 100644 --- a/cloudexit/engine.py +++ b/cloudexit/engine.py @@ -22,7 +22,12 @@ from cloudexit.providers.azure.sdk import azure_import from cloudexit.catalogue.db import connect, load_data from cloudexit.tfstate import build_tfstate_resource_inventory -from cloudexit.report.context import build_context, collect_provenance +from cloudexit.report.context import ( + EGRESS_DOCUMENT_CODE, + build_context, + collect_provenance, +) +from cloudexit.report.egress_report import generate_egress_report from cloudexit.report.executive_summary import generate_executive_summary from cloudexit.report.html_sections import html_sections from cloudexit.report.writers import ( @@ -432,6 +437,7 @@ def generate_report( raw_data_path: str, upload: dict[str, str] | None = None, sync_id: str | None = None, + egress_json_path: str | None = None, ) -> dict[str, Any]: try: db_path = os.path.join(report_path, "data", "assessment.db") @@ -493,6 +499,8 @@ def generate_report( provenance=provenance, report_path=report_path, ) + if egress_json_path: + context.egress_document_id = context.document_id_for(EGRESS_DOCUMENT_CODE) # Generate HTML report reports["HTML"] = generate_html_report( @@ -530,7 +538,20 @@ def generate_report( provenance=provenance.as_dict(), ) - return {"success": True, "reports": reports} + result = {"success": True, "reports": reports} + + # The Egress & Data Landscape is written last, from the same context: a + # problem with it never costs the Executive Summary + if egress_json_path: + try: + reports["EGRESS_PDF"] = generate_egress_report( + context, egress_json_path + ) + except Exception as e: + logger.error("Error generating the egress PDF: %s", e, exc_info=True) + result["egress_pdf_logs"] = str(e) + + return result except Exception as e: return {"success": False, "logs": f"Error generating report: {str(e)}"} diff --git a/cloudexit/report/context.py b/cloudexit/report/context.py index e56e52e..9f24c96 100644 --- a/cloudexit/report/context.py +++ b/cloudexit/report/context.py @@ -14,6 +14,16 @@ STRATEGY_CODES = {1: "repatriation", 2: "hybrid_cloud", 3: "alternate_cloud"} COST_MONTHS = 6 DOCUMENT_CODE = "ES" +EGRESS_DOCUMENT_CODE = "EDL" + + +def short_run_id(run_id): + # The end of a UUIDv7 is random; its start is the time. + return run_id.replace("-", "")[-8:] if run_id else "" + + +def format_document_id(code, run_id): + return f"{code}-{short_run_id(run_id) or 'LOCAL'}-EN" @dataclass(frozen=True) @@ -31,8 +41,7 @@ class Provenance: @property def run_short_id(self): - # The end of a UUIDv7 is random; its start is the time. - return self.run_id.replace("-", "")[-8:] if self.run_id else "" + return short_run_id(self.run_id) @property def collected_date(self): @@ -146,10 +155,16 @@ class ReportContext: alternatives: list report_path: str appearance: Appearance = DEFAULT_APPEARANCE + # Set when the run has an egress estimate, so the Executive Summary can name + # the document that carries it. + egress_document_id: str | None = None + + def document_id_for(self, code): + return format_document_id(code, self.provenance.run_id) @property def document_id(self): - return f"{DOCUMENT_CODE}-{self.provenance.run_short_id or 'LOCAL'}-EN" + return self.document_id_for(DOCUMENT_CODE) @property def total_resources(self): diff --git a/cloudexit/report/cover.py b/cloudexit/report/cover.py index 4af84e6..6e9c0a6 100644 --- a/cloudexit/report/cover.py +++ b/cloudexit/report/cover.py @@ -88,10 +88,10 @@ def _paragraph(value, style): class RecordCover: - def __init__(self, title, context, b): + def __init__(self, title, context, b, document_id=None): self.title = title self.project_name = context.name - self.document_id = context.document_id + self.document_id = document_id or context.document_id self.appearance = context.appearance self.report_path = context.report_path self.provenance = context.provenance diff --git a/cloudexit/report/egress.py b/cloudexit/report/egress.py index 36bd1ca..d4ca4a0 100644 --- a/cloudexit/report/egress.py +++ b/cloudexit/report/egress.py @@ -3,49 +3,37 @@ import logging import os import sqlite3 +from collections import Counter from datetime import datetime from typing import Any -from xml.sax.saxutils import escape from jinja2 import Environment -from reportlab.lib import colors -from reportlab.lib.colors import HexColor -from reportlab.graphics.charts.piecharts import Pie -from reportlab.graphics.shapes import Drawing, Rect, String -from reportlab.lib.pagesizes import A4 -from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet -from reportlab.lib.units import cm -from reportlab.platypus import ( - Image, - PageBreak, - Paragraph, - SimpleDocTemplate, - Spacer, - Table, - TableStyle, -) -from cloudexit.report.helpers import initials -from cloudexit.report.placeholder import IconPlaceholder -from cloudexit.resources import asset_path, find_asset +from cloudexit.report.helpers import format_gib, gib_number, initials, share_label +from cloudexit.resources import asset_path from cloudexit.catalogue.db import load_data from cloudexit.egress.model import ( GIB, compute_totals, - format_bytes, read_egress_inventory, ) from cloudexit.providers.aws.egress import ARCHIVE_TIERS as AWS_ARCHIVE_TIERS from cloudexit.providers.azure.egress import ARCHIVE_TIERS as AZURE_ARCHIVE_TIERS -from cloudexit.report.context import scope_facts -from cloudexit.report.notices import notices_html +from cloudexit.report.context import ( + DOCUMENT_CODE, + EGRESS_DOCUMENT_CODE, + PROVIDER_NAMES, + format_document_id, + scope_facts, +) +from cloudexit.report.notices import notices_html, text_parts +from cloudexit.report.sections import BOOK_DEMO_URL, PRODUCT_SITES from cloudexit.report.wording import BLOCKS -PDF_HEADER_TITLE = "EscapeCloud Community Edition - Data & Egress" - logger = logging.getLogger(__name__) DEFAULT_PRICING_ZONE = "zone1" UNIT_DIVISORS = {"GB": 10**9, "GiB": 2**30} +COMPONENTS = ("internet_egress", "archive_retrieval") ARCHIVE_TIERS_BY_CSP = {1: AZURE_ARCHIVE_TIERS, 2: AWS_ARCHIVE_TIERS} @@ -81,46 +69,6 @@ "in_allocation": False, }, } -EGRESS_FEE_CHART_COLORS = [ - HexColor("#115e59"), - HexColor("#ffae1f"), - HexColor("#539bff"), - HexColor("#a855f7"), - HexColor("#22c55e"), - HexColor("#991b1b"), -] - -PROVIDER_NAMES = {1: "Microsoft Azure", 2: "Amazon Web Services"} -COMPONENT_LABELS = { - "internet_egress": "Internet Egress", - "archive_retrieval": "Archive Retrieval", -} -COVERAGE_NOTES = { - 1: [ - ("Storage Accounts", "Capacity metrics can lag by up to roughly 48 hours"), - ("Managed Disks, Snapshots", "Provisioned size; counted as an upper bound"), - ("SQL Databases", "Used space from database metrics"), - ("Cosmos DB Accounts", "Used space from database metrics"), - ("PostgreSQL / MySQL Flexible Servers", "Used space from database metrics"), - ("Recovery Services Vaults", "Not sized; recovery points cannot be exported"), - ], - 2: [ - ("S3 Buckets", "Metrics can lag by up to roughly 48 hours"), - ( - "EBS Volumes, Snapshots", - "Upper bound; snapshots can over-count shared blocks", - ), - ("RDS Instances", "Aurora is not sized"), - ("DynamoDB Tables", ""), - ("AWS Backup Vaults", "Not sized; recovery points cannot be exported"), - ], -} - -ALPHA_NOTE = ( - "NOTE: This Data Landscape & Egress Estimation feature is currently in " - "alpha version. Estimated values may be incomplete or inaccurate in some " - "environments." -) PRICING_SOURCE_LINKS = { 1: { @@ -135,6 +83,8 @@ }, } +COVERAGE_NOTES_KEYS = {1: "eg_coverage_notes_azure", 2: "eg_coverage_notes_aws"} + FALLBACK_ICONS = { "microsoft.compute/disks": "/icons/azure/compute/10032-icon-service-Disks.png", "microsoft.compute/snapshots": ( @@ -148,187 +98,6 @@ } -def default_table_style() -> TableStyle: - """Shared header style used by summary and scope tables.""" - return TableStyle( - [ - ("BACKGROUND", (0, 0), (-1, 0), HexColor("#115e59")), - ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), - ("GRID", (0, 0), (-1, -1), 1, HexColor("#000000")), - ("ALIGN", (0, 0), (-1, -1), "LEFT"), - ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), - ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), - ("FONTSIZE", (0, 0), (-1, 0), 11), - ("BOTTOMPADDING", (0, 0), (-1, 0), 12), - ("TOPPADDING", (0, 0), (-1, 0), 12), - ] - ) - - -def build_summary_section(metadata, styles, content_style): - """Page 1: Summary table.""" - content = [] - content.append(Spacer(1, 12)) - content.append(Paragraph("Summary", styles["Heading1"])) - content.append(Paragraph("Quick overview of the assessment:", content_style)) - - cloud_service_provider_map = { - "1": "Microsoft Azure", - "2": "Amazon Web Services", - "3": "Alibaba Cloud", - "4": "Google Cloud", - } - exit_strategy_map = { - "1": "Repatriation to On-Premises", - "2": "Hybrid Cloud Adoption", - "3": "Migration to Alternate Cloud", - } - type_map = {"1": "Basic", "2": "Standard"} - - summary_data = [ - ["Name", "Value"], - [ - "Cloud Service Provider", - cloud_service_provider_map.get( - str(metadata["cloud_service_provider"]), "Unknown" - ), - ], - [ - "Exit Strategy", - exit_strategy_map.get(str(metadata["exit_strategy"]), "Unknown"), - ], - ["Assessment Type", type_map.get(str(metadata["assessment_type"]), "Unknown")], - ["TimeStamp", metadata["timestamp"]], - ] - - summary_table = Table(summary_data, colWidths=[4 * cm, 11.5 * cm]) - summary_table.setStyle(default_table_style()) - content.append(summary_table) - content.append(Spacer(1, 12)) - - upload = metadata.get("upload") - if upload: - if upload.get("status") == "uploaded": - note = "Uploaded to the platform." - if upload.get("url"): - url = escape(upload["url"]) - note += f' {url}' - else: - note = f"Not uploaded: {escape(upload.get('reason', 'unknown'))}" - content.append(Paragraph(note, content_style)) - return content - - -# Cap on how many subscriptions / resource groups / regions a scope row lists -# before it summarises the remainder. The full list stays in the manifest. -MAX_SCOPE_VALUES = 3 - - -def _shorten(value: str, head: int = 4, tail: int = 4) -> str: - if not isinstance(value, str): - return "N/A" - if len(value) <= head + tail + 1: - return value - return f"{value[:head]}…{value[-tail:]}" - - -def _join_scope_values(values) -> str: - if not values: - return "Not determinable from state" - shown = list(values)[:MAX_SCOPE_VALUES] - remaining = len(values) - len(shown) - text = ", ".join(shown) - return f"{text}, +{remaining} more" if remaining > 0 else text - - -def _build_tfstate_scope_rows(metadata, scope): - rows = [] - - file_name = scope.get("file") or "N/A" - sha256 = scope.get("sha256") - if sha256: - file_name = f"{file_name} (SHA-256 {_shorten(sha256)})" - rows.append(["File", file_name]) - - lineage = scope.get("lineage") - serial = scope.get("serial") - if lineage and serial is not None: - rows.append(["State", f"{lineage} (Serial: {serial})"]) - elif lineage: - rows.append(["State", lineage]) - elif serial is not None: - rows.append(["State", f"Serial: {serial}"]) - - if metadata["cloud_service_provider"] == 1: # Azure - rows.append(["Subscription", _join_scope_values(scope.get("subscriptions"))]) - rows.append( - ["Resource Group(s)", _join_scope_values(scope.get("resource_groups"))] - ) - else: # AWS - rows.append(["Region(s)", _join_scope_values(scope.get("locations"))]) - - return rows - - -def _live_scope_rows(cloud_service_provider, provider_details): - # The same facts as the Assessment Scope on page 2 of the Executive - # Summary, and nothing that identifies the credentials in use - b = BLOCKS - scope = scope_facts(provider_details, cloud_service_provider, None) - values = dict(scope.rows) - if scope.kind == "azure": - return [ - [b["ev_tenant_id"], values.get("tenant") or b["ev_dash"]], - [b["ev_subscription_id"], values.get("subscription") or b["ev_dash"]], - [b["ev_resource_group"], values.get("resource_group") or b["ev_dash"]], - ] - rows = [] - if values.get("account"): - rows.append([b["ev_account_id"], values["account"]]) - rows.append([b["ev_region"], values.get("region") or b["ev_dash"]]) - return rows - - -def build_scope_section( - metadata, provider_details, styles, content_style, tfstate_scope=None -): - """Page 1: Scope of Assessment table.""" - content = [] - content.append(Paragraph("Scope of Assessment", styles["Heading2"])) - - tfstate_path = provider_details.get("tfstatePath") - if tfstate_path: - content.append( - Paragraph( - "Assessed from Terraform state. Resources not managed by " - "Terraform are not included.", - content_style, - ) - ) - else: - content.append(Paragraph("Defined scope of assessment:", content_style)) - - scope_data = [["Name", "Value"]] - - if tfstate_path: - # Fall back to the little we know from the config when the manifest is - # unavailable, rather than rendering a table of N/A. - scope = tfstate_scope or {"file": os.path.basename(tfstate_path)} - scope_data.extend(_build_tfstate_scope_rows(metadata, scope)) - elif metadata["cloud_service_provider"] in (1, 2): - scope_data.extend( - _live_scope_rows(metadata["cloud_service_provider"], provider_details) - ) - else: - scope_data.append(["N/A", "N/A"]) - - scope_table = Table(scope_data, colWidths=[4 * cm, 11.5 * cm]) - scope_table.setStyle(default_table_style()) - content.append(scope_table) - content.append(Spacer(1, 12)) - return content - - def _unit_divisor(unit: str) -> float: return UNIT_DIVISORS.get(unit, GIB) @@ -417,9 +186,9 @@ def format_fee(fee: float | None) -> str: return f"${fee:,.2f}" -def free_tier_limit_display( +def _free_tier( prices: list[dict[str, Any]], cloud_service_provider: int -) -> str | None: +) -> dict[str, Any] | None: tiers = sorted( ( price @@ -431,10 +200,24 @@ def free_tier_limit_display( key=lambda tier: tier["tier_from"], ) if tiers and tiers[0]["price_per_unit"] == 0.0 and tiers[0]["tier_to"]: - return f"{tiers[0]['tier_to']:g} {tiers[0]['unit']}" + return tiers[0] return None +def free_tier_limit_display( + prices: list[dict[str, Any]], cloud_service_provider: int +) -> str | None: + tier = _free_tier(prices, cloud_service_provider) + return f"{tier['tier_to']:g} {tier['unit']}" if tier else None + + +def free_allowance_bytes( + prices: list[dict[str, Any]], cloud_service_provider: int +) -> float | None: + tier = _free_tier(prices, cloud_service_provider) + return tier["tier_to"] * _unit_divisor(tier["unit"]) if tier else None + + def _summarize_totals( totals: dict[str, Any], total_fee: float, @@ -448,8 +231,8 @@ def _summarize_totals( and free_tier_limit is not None ) unknown_count = totals["resources_with_unknown_size"] - total_fee_display = "$0" if free_tier else format_fee(total_fee) - total_data_display = format_bytes(totals["known_size_bytes"]) + total_fee_display = format_fee(total_fee) + total_data_display = format_gib(totals["known_size_bytes"]) if unknown_count > 0: total_data_display = f"{total_data_display}+" if not free_tier: @@ -547,7 +330,7 @@ def _build_type_groups( ) for group in groups.values(): - group["size_display"] = format_bytes(group["size_bytes"]) + group["size_display"] = format_gib(group["size_bytes"]) group["fee_display"] = format_fee(group["fee"]) return sorted( @@ -583,6 +366,23 @@ def _load_estimate( return meta, rows, totals, pricing, total_fee, type_groups +def load_egress_estimate(report_path: str, json_path: str) -> dict[str, Any]: + meta, rows, totals, pricing, total_fee, type_groups = _load_estimate( + report_path, json_path + ) + return { + "meta": meta, + "rows": rows, + "totals": totals, + "pricing": pricing, + "total_fee": total_fee, + "type_groups": type_groups, + "summary": _summarize_totals( + totals, total_fee, pricing, meta["cloud_service_provider"] + ), + } + + def _generated_at(timestamp): # The run's own time, as the estimate stored it; now when it cannot be read try: @@ -591,652 +391,452 @@ def _generated_at(timestamp): return datetime.now() -def generate_egress_html_report(report_path: str, json_path: str) -> dict[str, Any]: - try: - meta, rows, totals, pricing, total_fee, type_groups = _load_estimate( - report_path, json_path - ) +def _share(part, whole): + return part / whole * 100 if whole else 0 - summary = _summarize_totals( - totals, total_fee, pricing, meta["cloud_service_provider"] - ) - allocation_labels, allocation_values, allocation_colors = _build_allocation( - rows - ) +def _plus(text, estimate): + # Resources without a size are left out, so the figure is a lower bound + return f"{text}+" if estimate["summary"]["unknown_count"] > 0 else text - fee_groups = [group for group in type_groups if (group["fee"] or 0.0) > 0] - fee_labels = [group["label"] for group in fee_groups] - fee_values = [round(group["fee"], 2) for group in fee_groups] - template_content = asset_path("template", "egress.html").read_text( - encoding="utf-8" - ) - - env = Environment(autoescape=True) - template = env.from_string(template_content) - html_content = template.render( - name=meta["name"], - cloud_service_provider=meta["cloud_service_provider"], - assessment_type=meta["assessment_type"], - timestamp=meta["timestamp"], - total_data_display=summary["total_data_display"], - total_fee_display=summary["total_fee_display"], - unknown_count=summary["unknown_count"], - has_allocation=bool(allocation_values), - allocation_labels_json=json.dumps(allocation_labels), - allocation_values_json=json.dumps(allocation_values), - allocation_colors_json=json.dumps(allocation_colors), - has_fees=bool(fee_values), - fee_labels_json=json.dumps(fee_labels), - fee_values_json=json.dumps(fee_values), - free_tier=summary["free_tier"], - free_tier_limit_display=summary["free_tier_limit"], - known_data_display=format_bytes(totals["known_size_bytes"]), - type_groups=type_groups, - notices=notices_html( - meta["cloud_service_provider"], _generated_at(meta["timestamp"]) - ), - ) - - html_path = os.path.join(report_path, "egress.html") - with open(html_path, "w", encoding="utf-8") as report_file: - report_file.write(html_content) - - return { - "success": True, - "logs": "Egress report generated successfully.", - "html_path": html_path, - } - - except Exception as e: - logger.error(f"Error generating egress report: {str(e)}", exc_info=True) - return {"success": False, "logs": str(e)} - - -def _icon_flowable(report_path: str, icon: str | None, label: str) -> Any: - icon_path = find_asset(report_path, icon) if icon else None - if icon_path is None: - return IconPlaceholder(label, 20) - return Image(str(icon_path), width=20, height=20) +def introduction_facts(scope, b=BLOCKS): + return { + "heading": b["eg_heading_introduction"], + "purpose_heading": b["eg_heading_purpose"], + "purpose": b["eg_purpose_block"], + "scope_heading": b["eg_heading_scope"], + "scope": scope_sentence(scope, b), + "limitations_heading": b["eg_heading_limitations"], + "limitations": [ + b["eg_limitations_block"], + b["eg_limitations_scope"], + b["eg_limitations_data"], + ], + } -def _fee_breakdown_display(group: dict[str, Any]) -> str: - if group["fee"] is None: - return "n/a (not sized)" - return group["fee_display"] +def scope_sentence(scope, b=BLOCKS): + if scope.kind == "azure": + return b["eg_scope_azure"].format( + resource_group=scope.resource_group or b["ev_dash"] + ) + if scope.kind == "aws": + return b["eg_scope_aws"].format(region=scope.region or b["ev_dash"]) + return b["eg_scope_plain"] -def _egress_fee_chart_groups( - type_groups: list[dict[str, Any]], max_segments: int = 5 -) -> list[tuple[str, float]]: - fee_groups = [ - (group["label"], group["fee"]) - for group in type_groups - if group.get("fee") is not None and group["fee"] > 0 - ] - if len(fee_groups) <= max_segments: - return fee_groups - visible = fee_groups[: max_segments - 1] - other_total = sum(fee for _, fee in fee_groups[max_segments - 1 :]) - if other_total > 0: - visible.append(("Other Components", other_total)) - return visible - - -def _draw_egress_fee_chart(type_groups: list[dict[str, Any]]) -> Drawing | None: - chart_groups = _egress_fee_chart_groups(type_groups) - if not chart_groups: +def coverage_facts(cloud_service_provider, b=BLOCKS): + key = COVERAGE_NOTES_KEYS.get(cloud_service_provider) + if not key: return None - - total_fee = sum(fee for _, fee in chart_groups) - drawing = Drawing(360, 170) - - pie = Pie() - pie.x = 35 - pie.y = 20 - pie.width = 125 - pie.height = 125 - pie.data = [fee for _, fee in chart_groups] - pie.innerRadiusFraction = 0.55 - for index, _ in enumerate(chart_groups): - color = EGRESS_FEE_CHART_COLORS[index % len(EGRESS_FEE_CHART_COLORS)] - pie.slices[index].fillColor = color - pie.slices[index].strokeColor = colors.white - pie.slices[index].strokeWidth = 1 - drawing.add(pie) - - legend_x = 190 - legend_y = 130 - for index, (label, fee) in enumerate(chart_groups): - y = legend_y - index * 22 - color = EGRESS_FEE_CHART_COLORS[index % len(EGRESS_FEE_CHART_COLORS)] - share = (fee / total_fee * 100) if total_fee else 0 - drawing.add(Rect(legend_x, y, 8, 8, fillColor=color, strokeColor=color)) - drawing.add( - String( - legend_x + 14, - y - 1, - f"{label}: {share:.1f}%", - fontName="Helvetica", - fontSize=8, - fillColor=HexColor("#112726"), - ) - ) - return drawing + return { + "heading": b["eg_heading_coverage"], + "intro": b["eg_coverage_intro"], + "headers": (b["eg_th_resource_type"], b["eg_th_measured"]), + "rows": [(name, note or b["ev_dash"]) for name, note in b[key]], + } -def _build_coverage_section(cloud_service_provider, styles, content_style): - rows = COVERAGE_NOTES.get(cloud_service_provider) - if not rows: - return [] - - content = [] - content.append( - Paragraph( - "The estimation covers the following data-related resource types " - "within the assessed scope:", - content_style, - ) +def landscape_facts(estimate, b=BLOCKS): + rows = estimate["rows"] + summary = estimate["summary"] + total = estimate["totals"]["known_size_bytes"] + resources = len(rows) + unsized = [row for row in rows if row["size_bytes"] is None] + sized = resources - len(unsized) + counts = Counter(row["label"] for row in rows) + groups = sorted( + (group for group in estimate["type_groups"] if group["size_bytes"] is not None), + key=lambda group: -group["size_bytes"], ) - table_data = [["Name", "Notes"]] + [[name, notes] for name, notes in rows] - coverage_table = Table(table_data, colWidths=[7 * cm, 8.5 * cm]) - coverage_table.setStyle(default_table_style()) - content.append(coverage_table) - content.append(Spacer(1, 6)) - note_style = ParagraphStyle( - "CoverageNote", - fontSize=8, - leading=10, - textColor=HexColor("#6c757d"), - ) - content.append(Paragraph(ALPHA_NOTE, note_style)) - content.append(PageBreak()) - return content - + top = groups[0] if groups else None -def _build_estimated_costs_section( - total_fee, totals, free_tier, free_tier_limit, type_groups, styles, content_style -): - content = [] - content.append(Spacer(1, 12)) - content.append(Paragraph("Estimated Costs", styles["Heading2"])) - - unknown_count = totals["resources_with_unknown_size"] - if free_tier: - fee_display = "$0" - intro = ( - f"The scanned footprint ({format_bytes(totals['known_size_bytes'])}) " - "falls entirely within the cloud provider's complimentary monthly " - f"internet egress allowance ({free_tier_limit} free tier)." - ) + if not resources: + lead = b["eg_no_data_notice"] + elif not sized: + key = "eg_land_none_sized_one" if resources == 1 else "eg_land_none_sized" + lead = b[key].format(resources=resources) else: - fee_display = format_fee(total_fee) - if unknown_count > 0: - fee_display = f"{fee_display}+" - intro = ( - "Estimated one-time internet egress fee for transferring the data " - "identified on the previous page out of the cloud provider." - ) - content.append(Paragraph(intro, content_style)) - - card_title_style = ParagraphStyle( - "EgressCostCardTitle", - parent=content_style, - fontName="Helvetica-Bold", - fontSize=8, - leading=10, - textColor=HexColor("#115e59"), - ) - card_amount_style = ParagraphStyle( - "EgressCostCardAmount", - parent=content_style, - fontName="Helvetica-Bold", - fontSize=20, - leading=24, - textColor=HexColor("#112726"), - ) - warning_style = ParagraphStyle( - "EgressCostWarning", - parent=content_style, - fontSize=8, - leading=10, - textColor=HexColor("#664d03"), - ) - breakdown_header_style = ParagraphStyle( - "EgressCostBreakdownHeader", - parent=card_title_style, - textColor=colors.white, - ) - total_card_data = [ - [ - Paragraph("TOTAL ESTIMATED NETWORK TRANSIT FEE", card_title_style), - ], - [Paragraph(fee_display, card_amount_style)], - ] - if unknown_count > 0: - total_card_data.append( - [ - Paragraph( - f"Excludes {unknown_count} " - f"resource{'s' if unknown_count != 1 else ''} with " - "unavailable size calculations.", - warning_style, + sentences = [ + b["eg_land_lead_one" if resources == 1 else "eg_land_lead"].format( + total=format_gib(total), sized=sized, resources=resources + ) + ] + if len(groups) > 1 and total > 0 and top["size_bytes"]: + sentences.append( + b["eg_land_top"].format( + type=top["label"], + size=format_gib(top["size_bytes"]), + share=share_label(_share(top["size_bytes"], total)), ) - ] - ) - total_card = Table(total_card_data, colWidths=[15.5 * cm]) - total_card.setStyle( - TableStyle( - [ - ("BACKGROUND", (0, 0), (-1, -1), HexColor("#f8fafc")), - ("BOX", (0, 0), (-1, -1), 1, HexColor("#d7e3e1")), - ("LEFTPADDING", (0, 0), (-1, -1), 12), - ("RIGHTPADDING", (0, 0), (-1, -1), 12), - ("TOPPADDING", (0, 0), (-1, -1), 8), - ("BOTTOMPADDING", (0, 0), (-1, -1), 8), - ] - ) - ) - content.append(total_card) - content.append(Spacer(1, 12)) - - chart = _draw_egress_fee_chart(type_groups) - if chart: - chart_table = Table([[chart]], colWidths=[15.5 * cm]) - chart_table.setStyle( - TableStyle( - [ - ("BOX", (0, 0), (-1, -1), 1, HexColor("#d7e3e1")), - ("ALIGN", (0, 0), (-1, -1), "CENTER"), - ("TOPPADDING", (0, 0), (-1, -1), 10), - ("BOTTOMPADDING", (0, 0), (-1, -1), 10), - ] ) - ) - content.append(chart_table) - content.append(Spacer(1, 12)) + if len(unsized) == 1: + sentences.append(b["eg_land_unsized_one"]) + elif unsized: + sentences.append(b["eg_land_unsized_many"].format(count=len(unsized))) + lead = " ".join(sentences) - breakdown_data = [ - [ - Paragraph("Resource Type", breakdown_header_style), - Paragraph("Estimated Cost (List Rate)", breakdown_header_style), - ] - ] - for group in type_groups: - breakdown_data.append([group["label"], _fee_breakdown_display(group)]) - breakdown_data.append( - [ - Paragraph("Estimated Total Egress Fee", breakdown_header_style), - Paragraph(fee_display, breakdown_header_style), - ] - ) - breakdown_table = Table(breakdown_data, colWidths=[10 * cm, 5.5 * cm]) - breakdown_table.setStyle( - TableStyle( - [ - ("BACKGROUND", (0, 0), (-1, 0), HexColor("#115e59")), - ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), - ("BACKGROUND", (0, -1), (-1, -1), HexColor("#115e59")), - ("TEXTCOLOR", (0, -1), (-1, -1), colors.white), - ("BOX", (0, 0), (-1, -1), 1, HexColor("#112726")), - ("INNERGRID", (0, 0), (-1, -1), 0.5, HexColor("#d7e3e1")), - ("ALIGN", (1, 1), (1, -1), "RIGHT"), - ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), - ("LEFTPADDING", (0, 0), (-1, -1), 8), - ("RIGHTPADDING", (0, 0), (-1, -1), 8), - ("TOPPADDING", (0, 0), (-1, -1), 7), - ("BOTTOMPADDING", (0, 0), (-1, -1), 7), - ] + unsized_counts = Counter(row["label"] for row in unsized) + not_sized = [ + b["eg_land_not_sized_one" if count == 1 else "eg_land_not_sized_many"].format( + type=label, count=count ) - ) - content.append(Paragraph("Cost Breakdown", styles["Heading2"])) - content.append(breakdown_table) - content.append(PageBreak()) - return content - - -def _build_data_landscape_section( - type_groups, - total_size_display, - report_path, - styles, - content_style, -): - content = [] - content.append(Spacer(1, 12)) - content.append(Paragraph("Data Landscape", styles["Heading1"])) - content.append( - Paragraph( - "The Data Landscape summarizes the data-bearing cloud resources " - "identified within the defined scope, with the amount of data per " - "resource type:", - content_style, + for label, count in sorted( + unsized_counts.items(), key=lambda item: (-item[1], item[0]) ) + ] + return { + "heading": b["eg_heading_landscape"], + "empty": not resources, + "lead": lead, + "stats": [ + (b["eg_st_volume"], summary["total_data_display"], False), + ( + b["eg_st_sized"], + b["eg_st_sized_value"].format(sized=sized, resources=resources), + False, + ), + (b["eg_st_not_sized"], str(len(unsized)), bool(unsized)), + (b["eg_st_largest"], top["label"] if top else b["ev_dash"], False), + ], + "headers": ( + b["eg_th_resource_type"], + b["eg_th_resources"], + b["eg_th_size_gib"], + b["eg_th_share"], + ), + "rows": [ + { + "label": group["label"], + "icon": group["icon"], + "initials": group["initials"], + "resources": counts[group["label"]], + "size": gib_number(group["size_bytes"]), + "share": _share(group["size_bytes"], total), + "share_text": share_label(_share(group["size_bytes"], total)), + } + for group in groups + ], + "total": (b["eg_total"], resources, _plus(gib_number(total), estimate)), + "not_sized": ( + b["eg_land_not_sized"].format(types=", ".join(not_sized)) + if not_sized + else None + ), + "caption": b["eg_land_caption"], + "source_note": b["eg_land_source_note"], + } + + +def _allowance_text(prices, cloud_service_provider): + # The provider's own figure, plus the GiB value when it is published in GB + tier = _free_tier(prices, cloud_service_provider) + if tier is None: + return None + limit = f"{tier['tier_to']:g} {tier['unit']}" + if tier["unit"] == "GiB": + return limit + return ( + f"{limit} ({format_gib(free_allowance_bytes(prices, cloud_service_provider))})" ) - content.append(Spacer(1, 12)) - table_data = [["#", "Resource Type", "", "Size"]] - for index, group in enumerate(type_groups, start=1): - table_data.append( - [ - str(index), - group["label"], - _icon_flowable(report_path, group["icon"], group["label"]), - group["size_display"], - ] - ) - table_data.append(["Total", "", "", total_size_display]) - landscape_table = Table(table_data, colWidths=[1 * cm, 9 * cm, 1.5 * cm, 4 * cm]) - landscape_table.setStyle( - TableStyle( +def cost_facts(estimate, b=BLOCKS): + rows = estimate["rows"] + summary = estimate["summary"] + csp = estimate["meta"]["cloud_service_provider"] + total = estimate["totals"]["known_size_bytes"] + total_fee = estimate["total_fee"] + resources = len(rows) + unsized = sum(1 for row in rows if row["size_bytes"] is None) + sized = resources - unsized + allowance = free_allowance_bytes(estimate["pricing"], csp) + allowance_text = _allowance_text(estimate["pricing"], csp) + groups = [ + group for group in estimate["type_groups"] if group["size_bytes"] is not None + ] + billable = total if allowance is None else max(0, total - allowance) + + if not resources: + sentences = [b["eg_no_data_notice"]] + elif not sized: + sentences = [b["eg_cost_lead_none"]] + elif summary["free_tier"]: + sentences = [ + b["eg_cost_lead_free"].format(fee=format_fee(total_fee)), + b["eg_cost_headroom"].format( + used=format_gib(total), allowance=allowance_text + ), + ] + else: + sentences = [ + b["eg_cost_lead_paid"].format( + total=format_gib(total), fee=format_fee(total_fee) + ) + ] + if allowance is not None: + sentences.append( + b["eg_cost_lead_allowance"].format( + allowance=allowance_text, billable=format_gib(billable) + ) + ) + top = max(groups, key=lambda group: group["fee"] or 0) + if total_fee > 0 and top["fee"] and len(groups) > 1: + sentences.append( + b["eg_cost_lead_top"].format( + type=top["label"], + share=share_label(_share(top["fee"], total_fee)), + ) + ) + if sized and unsized == 1: + sentences.append(b["eg_cost_unsized_one"]) + elif sized and unsized: + sentences.append(b["eg_cost_unsized_many"].format(count=unsized)) + + if summary["free_tier"]: + volume_stat = (b["eg_st_allowance"], summary["free_tier_limit"], False) + else: + volume_stat = (b["eg_st_billable"], format_gib(billable), False) + return { + "heading": b["eg_heading_costs"], + "empty": not resources, + "lead": " ".join(sentences), + "stats": [ + (b["eg_st_fee"], summary["total_fee_display"], False), + (b["eg_st_volume"], summary["total_data_display"], False), + volume_stat, + (b["eg_st_not_sized"], str(unsized), bool(unsized)), + ], + "headers": ( + b["eg_th_resource_type"], + b["eg_th_size_gib"], + b["eg_th_share"], + b["eg_th_fee"], + ), + # A fee of zero has no split to show + "rows": ( [ - ("BACKGROUND", (0, 0), (-1, 0), HexColor("#115e59")), - ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), - ("BACKGROUND", (0, -1), (-1, -1), HexColor("#115e59")), - ("TEXTCOLOR", (0, -1), (-1, -1), colors.white), - ("BOX", (0, 0), (-1, -1), 1, HexColor("#112726")), - ("BOTTOMPADDING", (0, 0), (-1, 0), 12), - ("TOPPADDING", (0, 0), (-1, 0), 12), - ("ALIGN", (0, 1), (1, -2), "LEFT"), - ("ALIGN", (2, 1), (2, -2), "CENTER"), - ("ALIGN", (3, 0), (-1, -1), "CENTER"), - ("VALIGN", (0, 1), (-1, -1), "MIDDLE"), + { + "label": group["label"], + "icon": group["icon"], + "initials": group["initials"], + "size": gib_number(group["size_bytes"]), + "share": _share(group["size_bytes"], total), + "share_text": share_label(_share(group["size_bytes"], total)), + "fee": format_fee(group["fee"]), + } + for group in groups ] - ) - ) - content.append(landscape_table) - content.append(PageBreak()) - return content + if total_fee > 0 + else [] + ), + "total": ( + b["eg_total"], + _plus(gib_number(total), estimate), + summary["total_fee_display"], + ), + "caption": b["eg_cost_caption"], + } -def _tier_display(row: dict[str, Any]) -> str: +def _tier_display(row, b): unit = row["unit"] if row["tier_from"] == 0 and row["tier_to"] is None: - return "all volumes" + return b["eg_tier_all_volumes"] if row["tier_from"] == 0 and row["price_per_unit"] == 0: - return f"first {row['tier_to']:,.0f} {unit} / month" + return b["eg_tier_first"].format(to=f"{row['tier_to']:,.0f}", unit=unit) if row["tier_to"] is None: - return f"over {row['tier_from']:,.0f} {unit}" - return f"{row['tier_from']:,.0f} {unit} – {row['tier_to']:,.0f} {unit}" + return b["eg_tier_over"].format(from_=f"{row['tier_from']:,.0f}", unit=unit) + return b["eg_tier_range"].format( + from_=f"{row['tier_from']:,.0f}", to=f"{row['tier_to']:,.0f}", unit=unit + ) -def _price_display(row: dict[str, Any]) -> str: +def _price_display(row, b): if row["price_per_unit"] == 0: - return "Free" - return f"${row['price_per_unit']:g} / {row['unit']}" + return b["eg_price_free"] + return f"{row['price_per_unit']:g} / {row['unit']}" -def _build_pricing_basis_section(prices, cloud_service_provider, styles, content_style): +def pricing_facts(estimate, b=BLOCKS): + csp = estimate["meta"]["cloud_service_provider"] rows = sorted( ( price - for price in prices - if price["csp"] == cloud_service_provider + for price in estimate["pricing"] + if price["csp"] == csp and price["zone"] == DEFAULT_PRICING_ZONE - and price["component"] in COMPONENT_LABELS - ), - key=lambda price: ( - list(COMPONENT_LABELS).index(price["component"]), - price["tier_from"], + and price["component"] in COMPONENTS ), + key=lambda price: (COMPONENTS.index(price["component"]), price["tier_from"]), ) if not rows: - return [] - - content = [] - content.append(Spacer(1, 12)) - content.append(Paragraph("Appendix – Egress Prices", styles["Heading1"])) - - provider_name = PROVIDER_NAMES.get(cloud_service_provider, "Unknown Provider") - content.append( - Paragraph( - "Our calculations and estimations are based on the following " - f"{provider_name} list prices (Zone 1 – US/Europe, public " - "internet routing):", - content_style, - ) - ) - content.append(Spacer(1, 12)) + return None - show_valid_from = len({row["valid_from"] for row in rows}) > 1 - header = ["#", "Component", "Tier", "Price"] + provider = PROVIDER_NAMES.get(csp) or b["eg_provider_fallback"] + valid_dates = {row["valid_from"] for row in rows} + show_valid_from = len(valid_dates) > 1 + if show_valid_from: + intro = b["eg_pricing_intro_mixed"].format(provider=provider) + else: + intro = b["eg_pricing_intro"].format( + provider=provider, valid=next(iter(valid_dates)) + ) + headers = [b["eg_th_component"], b["eg_th_tier"], b["eg_th_price"]] if show_valid_from: - header.append("Valid from") - table_data = [header] - for index, row in enumerate(rows, start=1): + headers.append(b["eg_th_valid_from"]) + + table = [] + for row in rows: table_row = [ - str(index), - COMPONENT_LABELS[row["component"]], - _tier_display(row), - _price_display(row), + b[f"eg_component_{row['component']}"], + _tier_display(row, b), + _price_display(row, b), ] if show_valid_from: table_row.append(row["valid_from"]) - table_data.append(table_row) + table.append(table_row) - col_widths = ( - [1 * cm, 4 * cm, 5.5 * cm, 2.5 * cm, 2.5 * cm] - if show_valid_from - else [1 * cm, 4.5 * cm, 6 * cm, 4 * cm] - ) - pricing_table = Table(table_data, colWidths=col_widths) - pricing_table.setStyle( - TableStyle( - [ - ("BACKGROUND", (0, 0), (-1, 0), HexColor("#115e59")), - ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), - ("BOX", (0, 0), (-1, -1), 1, HexColor("#112726")), - ("BOTTOMPADDING", (0, 0), (-1, 0), 12), - ("TOPPADDING", (0, 0), (-1, 0), 12), - ("ALIGN", (0, 1), (2, -1), "LEFT"), - ("ALIGN", (3, 0), (-1, -1), "CENTER"), - ("VALIGN", (0, 1), (-1, -1), "MIDDLE"), - ] - ) + unit = next( + (row["unit"] for row in rows if row["component"] == "internet_egress"), None ) - content.append(pricing_table) - content.append(Spacer(1, 12)) - - footnote_style = ParagraphStyle( - "PricingFootnote", - fontSize=8, - leading=10, - textColor=HexColor("#6c757d"), - spaceAfter=4, - ) - if not any(row["component"] == "archive_retrieval" for row in rows): - content.append( - Paragraph( - "Archive retrieval pricing was not available for this provider " - "at assessment time; rehydration fees are not included in the " - "estimate.", - footnote_style, - ) - ) - links = PRICING_SOURCE_LINKS.get(cloud_service_provider, {}) - components_shown = {row["component"] for row in rows} - source_lines = [ - (COMPONENT_LABELS[component], links[component]) - for component in COMPONENT_LABELS - if component in components_shown and component in links + units = {"GB": "eg_pricing_units", "GiB": "eg_pricing_units_gib"}.get(unit) + shown = {row["component"] for row in rows} + links = PRICING_SOURCE_LINKS.get(csp, {}) + sources = [ + (component, links[component]) + for component in COMPONENTS + if component in shown and component in links ] - if len(source_lines) == 1: - content.append(Paragraph(f"Source: {source_lines[0][1]}", footnote_style)) + if len(sources) == 1: + source_lines = [(b["eg_pricing_source"], sources[0][1])] else: - for label, link in source_lines: - content.append(Paragraph(f"Source ({label}): {link}", footnote_style)) - content.append( - Paragraph( - "List prices at the time of the assessment; excludes " - "request/transaction costs and cross-region traffic.", - footnote_style, - ) - ) - return content - - -def draw_header_footer( - report_path: str, - canvas, - doc, - title: str = "EscapeCloud Community Edition - Report", -) -> None: - # Save the state of the canvas to not affect the drawing - canvas.saveState() - width, height = A4 - - # Include the date in the format mm-dd-yyyy - current_date = datetime.now().strftime("%m-%d-%Y") - left_text_content1 = title - left_text_content2 = f"Date: {current_date}" - - # Define the header content with Paragraphs - header_style = ParagraphStyle( - "HeaderStyle", fontSize=10, textColor=HexColor("#9cafae") - ) - header_data = [ - [Paragraph(left_text_content1, header_style), "", ""], - [Paragraph(left_text_content2, header_style), "", ""], - ] + source_lines = [ + ( + b["eg_pricing_source_component"].replace( + "{component}", b[f"eg_component_{component}"] + ), + link, + ) + for component, link in sources + ] + return { + "heading": b["eg_heading_pricing"], + "intro": intro, + "headers": headers, + "rows": table, + "units": b[units].format(provider=provider) if units else None, + "no_archive": ( + None if "archive_retrieval" in shown else b["eg_pricing_no_archive"] + ), + # (template with a {link} field, url) + "sources": source_lines, + "note": b["eg_pricing_note"], + "glossary_heading": b["eg_heading_glossary"], + "glossary": [ + (entry["term"], entry["definition"]) + for entry in sorted( + b["eg_glossary"], key=lambda entry: entry["term"].lower() + ) + ], + } - # Create the header table - table = Table( - header_data, colWidths=[width - 188 - doc.rightMargin - doc.leftMargin, 10, 150] - ) - # Define the style for the table - table.setStyle( - TableStyle( - [ - ("SPAN", (1, 0), (1, 1)), # Merge Column 2 in both rows - ("SPAN", (2, 0), (2, 1)), # Merge Column 3 in both rows - ( - "ALIGN", - (0, 0), - (0, 0), - "LEFT", - ), # Align left_text_content1 to the left - ( - "ALIGN", - (0, 1), - (0, 1), - "LEFT", - ), # Align left_text_content2 to the left - ("ALIGN", (2, 0), (2, 1), "RIGHT"), # Align logo to the right - ("VALIGN", (0, 0), (0, 1), "TOP"), # Vertically align to the top - ("VALIGN", (2, 0), (2, 1), "MIDDLE"), # Vertically align to the middle - # ('GRID', (0, 0), (-1, -1), 0.5, colors.red), # Temporary borders for visualization - ] +def next_steps_facts( + executive_id, uploaded=False, platform_url=None, assessment_id=None, b=BLOCKS +): + # Paragraphs as text and (text, url) links, like the notices + sites = dict(PRODUCT_SITES) + paragraphs = [text_parts(b["eg_next_lead"], {}, {"executive_id": executive_id})] + if uploaded: + paragraphs.append([b["eg_next_uploaded"]]) + if platform_url: + paragraphs.append(text_parts(b["fa_uploaded_link"], {"link": platform_url})) + elif assessment_id: + paragraphs.append( + text_parts(b["fa_uploaded_id"], {}, {"assessment_id": assessment_id}) + ) + else: + paragraphs.append( + text_parts( + b["eg_next_platforms"], + {"exitcloud_url": sites["exitcloud"], "book_demo_url": BOOK_DEMO_URL}, + ) ) - ) - # Build the header table and draw it on the canvas - table.wrapOn(canvas, doc.leftMargin, height - doc.topMargin) - table.drawOn(canvas, doc.leftMargin, height - doc.topMargin) - - # Add the logo - logo_path = find_asset(report_path, "img/logo/report_logo.png") - logo = Image(str(logo_path), width=150, height=30) - - # Aligning logo vertically with the text - logo_y = height - doc.topMargin + 5 - logo.drawOn(canvas, width - 150 - doc.rightMargin, logo_y) - - # Line below the header - canvas.setStrokeColor(HexColor("#115e59")) - canvas.setLineWidth(1) - line_y = height - doc.topMargin - 10 - canvas.line(doc.leftMargin, line_y, width - doc.rightMargin, line_y) - - # Footer - footer_padding = 15 # Add padding under the page number - canvas.setStrokeColor(HexColor("#115e59")) - canvas.line(40, 60 + footer_padding, A4[0] - 40, 60 + footer_padding) - - canvas.setFont("Helvetica", 8) - canvas.drawString(A4[0] / 2 - 30, 60 + footer_padding - 15, f"Page {doc.page}") - - canvas.setFont("Helvetica-Oblique", 8) - canvas.setFillColor(HexColor("#9cafae")) - canvas.drawCentredString( - A4[0] / 2, - 40, - "EscapeCloud Community Edition - This report is provided 'As Is,' without any warranty of any kind.", - ) - canvas.drawCentredString( - A4[0] / 2, - 30, - "EscapeCloud makes no warranty that the information contained in this report is complete or error-free. Copyright 2024-2026", - ) + return {"heading": b["eg_heading_next_steps"], "paragraphs": paragraphs} - # Restore the state of the canvas - canvas.restoreState() - -def generate_egress_pdf_report( - report_path: str, json_path: str, provider_details: dict[str, Any] +def generate_egress_html_report( + report_path: str, + json_path: str, + provider_details: dict[str, Any] | None = None, + run_id: str | None = None, ) -> dict[str, Any]: try: - meta, rows, totals, pricing, total_fee, type_groups = _load_estimate( - report_path, json_path - ) + estimate = load_egress_estimate(report_path, json_path) + meta, rows = estimate["meta"], estimate["rows"] + csp = meta["cloud_service_provider"] + summary = estimate["summary"] + b = BLOCKS - summary = _summarize_totals( - totals, total_fee, pricing, meta["cloud_service_provider"] + allocation_labels, allocation_values, allocation_colors = _build_allocation( + rows ) - pdf_path = os.path.join(report_path, "egress.pdf") - - def header_footer(canvas, doc): - draw_header_footer(report_path, canvas, doc, title=PDF_HEADER_TITLE) + type_groups = estimate["type_groups"] + fee_groups = [group for group in type_groups if (group["fee"] or 0.0) > 0] + fee_labels = [group["label"] for group in fee_groups] + fee_values = [round(group["fee"], 2) for group in fee_groups] - doc = SimpleDocTemplate( - pdf_path, pagesize=A4, title="EscapeCloud_-_Data_and_Egress_Report" - ) - styles = getSampleStyleSheet() - content_style = ParagraphStyle( - "ContentStyle", fontSize=10, leading=12, spaceAfter=10 - ) - styles["Heading1"].leading = 1.5 * styles["Heading1"].fontSize - styles["Heading1"].textColor = HexColor("#112726") - styles["Heading2"].leading = 1.5 * styles["Heading2"].fontSize - styles["Heading2"].textColor = HexColor("#112726") - - content = [] - content += build_summary_section(meta, styles, content_style) - content += build_scope_section(meta, provider_details, styles, content_style) - content += _build_coverage_section( - meta["cloud_service_provider"], styles, content_style - ) - content += _build_data_landscape_section( - type_groups, - summary["total_data_display"], - report_path, - styles, - content_style, - ) - content += _build_estimated_costs_section( - total_fee, - totals, - summary["free_tier"], - summary["free_tier_limit"], - type_groups, - styles, - content_style, + template_content = asset_path("template", "egress.html").read_text( + encoding="utf-8" ) - content += _build_pricing_basis_section( - pricing, meta["cloud_service_provider"], styles, content_style + + scope = scope_facts(provider_details or {}, csp, None) + executive_id = format_document_id(DOCUMENT_CODE, run_id) + env = Environment(autoescape=True) + template = env.from_string(template_content) + html_content = template.render( + page_title=f"{b['cover_title_egress']} – {meta['name']}", + document_id=format_document_id(EGRESS_DOCUMENT_CODE, run_id), + name=meta["name"], + cloud_service_provider=csp, + assessment_type=meta["assessment_type"], + timestamp=meta["timestamp"], + total_data_display=summary["total_data_display"], + total_fee_display=summary["total_fee_display"], + unknown_count=summary["unknown_count"], + has_allocation=bool(allocation_values), + allocation_labels_json=json.dumps(allocation_labels), + allocation_values_json=json.dumps(allocation_values), + allocation_colors_json=json.dumps(allocation_colors), + has_fees=bool(fee_values), + fee_labels_json=json.dumps(fee_labels), + fee_values_json=json.dumps(fee_values), + free_tier=summary["free_tier"], + free_tier_limit_display=summary["free_tier_limit"], + known_data_display=format_gib(estimate["totals"]["known_size_bytes"]), + type_groups=type_groups, + introduction=introduction_facts(scope, b), + coverage=coverage_facts(csp, b), + landscape=landscape_facts(estimate, b), + costs=cost_facts(estimate, b), + pricing=pricing_facts(estimate, b), + next_steps=next_steps_facts(executive_id, b=b), + notices=notices_html(csp, _generated_at(meta["timestamp"]), b, egress=True), ) - doc.build(content, onFirstPage=header_footer, onLaterPages=header_footer) + html_path = os.path.join(report_path, "egress.html") + with open(html_path, "w", encoding="utf-8") as report_file: + report_file.write(html_content) return { "success": True, - "logs": "Egress PDF report generated successfully.", - "pdf_path": pdf_path, + "logs": "Egress report generated successfully.", + "html_path": html_path, } except Exception as e: - logger.error(f"Error generating egress PDF report: {str(e)}", exc_info=True) + logger.error(f"Error generating egress report: {str(e)}", exc_info=True) return {"success": False, "logs": str(e)} diff --git a/cloudexit/report/egress_report.py b/cloudexit/report/egress_report.py new file mode 100644 index 0000000..6acc846 --- /dev/null +++ b/cloudexit/report/egress_report.py @@ -0,0 +1,76 @@ +import os +from functools import partial + +from reportlab.lib.pagesizes import A4 +from reportlab.lib.units import cm +from reportlab.platypus import Spacer + +from .base import NumberedCanvas, ReportDocTemplate, set_pdf_metadata +from .context import EGRESS_DOCUMENT_CODE +from .cover import RecordCover +from .egress import load_egress_estimate +from .egress_sections import ( + appendix, + coverage, + data_landscape, + estimated_costs, + introduction, + next_steps, +) +from .evidence import build_document_information_page +from .flow import chapter_break, section_break +from .notices import legal_notices +from .styles import load_styles +from .wording import BLOCKS + + +def build_egress_content(context, estimate, styles, b): + content = [Spacer(1, 0), chapter_break()] + content += build_document_information_page( + context, + b, + document_id=context.document_id_for(EGRESS_DOCUMENT_CODE), + document_type=b["ev_egress_report"], + companion_id=context.document_id, + ) + content += introduction(context, styles, b) + content += coverage(context, styles, b) + content.append(chapter_break()) + content += data_landscape(context, estimate, styles, b) + content.append(section_break()) + content += estimated_costs(context, estimate, styles, b) + content.append(chapter_break()) + content += appendix(context, estimate, styles, b) + content.append(Spacer(1, 12)) + content += next_steps(context, styles, b) + content.append(chapter_break()) + content += legal_notices(context, styles, b, egress=True) + return content + + +def generate_egress_report(context, json_path, path=None): + path = path or os.path.join(context.report_path, "egress.pdf") + b = BLOCKS + estimate = load_egress_estimate(context.report_path, json_path) + cover = RecordCover( + b["cover_title_egress"], + context, + b, + document_id=context.document_id_for(EGRESS_DOCUMENT_CODE), + ) + doc = ReportDocTemplate( + path, + context, + cover=cover, + pagesize=A4, + rightMargin=2.5 * cm, + leftMargin=2.5 * cm, + topMargin=2.5 * cm, + bottomMargin=2.5 * cm, + ) + set_pdf_metadata(doc, context, b) + doc.multiBuild( + build_egress_content(context, estimate, load_styles(context.appearance), b), + canvasmaker=partial(NumberedCanvas, page_of=b["footer_page_of"]), + ) + return path diff --git a/cloudexit/report/egress_sections.py b/cloudexit/report/egress_sections.py new file mode 100644 index 0000000..553a46a --- /dev/null +++ b/cloudexit/report/egress_sections.py @@ -0,0 +1,324 @@ +from xml.sax.saxutils import escape, quoteattr + +from reportlab.lib.colors import HexColor +from reportlab.lib.enums import TA_RIGHT +from reportlab.lib.units import cm +from reportlab.platypus import Paragraph, Spacer, Table, TableStyle + +from cloudexit.resources import find_asset + +from .egress import ( + coverage_facts, + cost_facts, + introduction_facts, + landscape_facts, + next_steps_facts, + pricing_facts, +) +from .evidence import link_markup +from .flow import section +from .notices import parts_markup +from .palette import HAIRLINE, HOTSPOT_COUNT, INK, MUTED, PROVENANCE_BACKGROUND, RULE +from .placeholder import LabelWithPlaceholder +from .sections import GLOSSARY_TERM_WIDTH +from .table_style import data_table, header_cell, share_bar, summary_strip, text_style + +BODY = text_style("EgressBody", 8.5, 11) +BODY_RIGHT = text_style("EgressBodyRight", 8.5, 11, alignment=TA_RIGHT) +BODY_BOLD = text_style("EgressBodyBold", 8.5, 11, bold=True) +BODY_BOLD_RIGHT = text_style( + "EgressBodyBoldRight", 8.5, 11, bold=True, alignment=TA_RIGHT +) +CAPTION = text_style("EgressCaption", 7.5, 10, MUTED) + + +def _text(value, style=BODY): + return Paragraph(escape(str(value)), style) + + +def _right_header(text, appearance): + return header_cell(text, appearance, TA_RIGHT, split_long_words=False) + + +def _total_row_style(rows): + return [ + ("LINEABOVE", (0, len(rows)), (-1, len(rows)), 0.8, HexColor(INK)), + ("NOSPLIT", (0, len(rows) - 1), (-1, len(rows))), + ] + + +def _type_cell(row, report_path): + label = f"{escape(row['label'])}" + icon = find_asset(report_path, row["icon"]) if row["icon"] else None + if icon is None: + # A missing icon must not cost the whole document + return LabelWithPlaceholder(row["label"], label, BODY) + return Paragraph( + f' ' + f"{label}", + BODY, + ) + + +def _heading(facts, styles, key="heading", style="Heading1"): + return Paragraph(escape(facts[key]), styles[style]) + + +def introduction(context, styles, b): + facts = introduction_facts(context.scope, b) + body = styles["ContentStyle"] + content = [ + _heading(facts, styles), + _heading(facts, styles, "purpose_heading", "Heading2"), + Paragraph(escape(facts["purpose"]), body), + _heading(facts, styles, "scope_heading", "Heading2"), + Paragraph(escape(facts["scope"]), body), + _heading(facts, styles, "limitations_heading", "Heading2"), + ] + for index, text in enumerate(facts["limitations"]): + if index: + content.append(Spacer(1, 6)) + content.append(Paragraph(escape(text), body)) + return content + + +def coverage(context, styles, b): + facts = coverage_facts(context.cloud_service_provider, b) + if facts is None: + return [] + rows = [list(facts["headers"])] + [ + [_text(name, BODY_BOLD), _text(note)] for name, note in facts["rows"] + ] + return [ + _heading(facts, styles, style="Heading2"), + Paragraph(escape(facts["intro"]), styles["ContentStyle"]), + Spacer(1, 8), + data_table(rows, [6 * cm, 10 * cm], context.appearance), + ] + + +def _landscape_table(facts, context): + appearance = context.appearance + types, resources, size, share = facts["headers"] + rows = [ + [ + types, + _right_header(resources, appearance), + _right_header(size, appearance), + share, + ] + ] + for row in facts["rows"]: + rows.append( + [ + _type_cell(row, context.report_path), + _text(row["resources"], BODY_RIGHT), + _text(row["size"], BODY_RIGHT), + share_bar(row["share"], appearance), + ] + ) + extra = _total_row_style(rows) + label, count, total = facts["total"] + rows.append( + [ + _text(label, BODY_BOLD), + _text(count, BODY_BOLD_RIGHT), + _text(total, BODY_BOLD_RIGHT), + "", + ] + ) + return data_table(rows, [5.9 * cm, 2.0 * cm, 2.6 * cm, 5.5 * cm], appearance, extra) + + +def _stats(facts): + return summary_strip( + [ + (label, value, HOTSPOT_COUNT if attention else INK) + for label, value, attention in facts["stats"] + ] + ) + + +def data_landscape(context, estimate, styles, b): + facts = landscape_facts(estimate, b) + head = [ + _heading(facts, styles), + Paragraph(escape(facts["lead"]), styles["ContentStyle"]), + Spacer(1, 8), + ] + if facts["empty"]: + return section(head, []) + + body = [_stats(facts), Spacer(1, 12)] + if facts["rows"]: + body += [_landscape_table(facts, context), Spacer(1, 6)] + if facts["not_sized"]: + body += [Paragraph(escape(facts["not_sized"]), CAPTION), Spacer(1, 2)] + body += [ + Paragraph(escape(facts["caption"]), CAPTION), + Spacer(1, 2), + Paragraph(escape(facts["source_note"]), CAPTION), + ] + return section(head, body) + + +def _cost_table(facts, context): + appearance = context.appearance + types, size, share, fee = facts["headers"] + rows = [ + [ + types, + _right_header(size, appearance), + share, + _right_header(fee, appearance), + ] + ] + for row in facts["rows"]: + rows.append( + [ + _type_cell(row, context.report_path), + _text(row["size"], BODY_RIGHT), + share_bar(row["share"], appearance), + _text(row["fee"], BODY_RIGHT), + ] + ) + extra = _total_row_style(rows) + label, size_total, fee_total = facts["total"] + rows.append( + [ + _text(label, BODY_BOLD), + _text(size_total, BODY_BOLD_RIGHT), + "", + _text(fee_total, BODY_BOLD_RIGHT), + ] + ) + return data_table(rows, [5.4 * cm, 2.6 * cm, 5.5 * cm, 2.5 * cm], appearance, extra) + + +def estimated_costs(context, estimate, styles, b): + facts = cost_facts(estimate, b) + head = [ + _heading(facts, styles), + Paragraph(escape(facts["lead"]), styles["ContentStyle"]), + Spacer(1, 8), + ] + if facts["empty"]: + return section(head, []) + + body = [_stats(facts), Spacer(1, 12)] + if facts["rows"]: + body += [_cost_table(facts, context), Spacer(1, 6)] + body.append(Paragraph(escape(facts["caption"]), CAPTION)) + return section(head, body) + + +def _glossary_table(entries, appearance): + table = Table( + [[_text(term, BODY_BOLD), _text(definition)] for term, definition in entries], + colWidths=[GLOSSARY_TERM_WIDTH, 16 * cm - GLOSSARY_TERM_WIDTH], + hAlign="LEFT", + ) + table.setStyle( + TableStyle( + [ + ( + "LINEABOVE", + (0, 0), + (-1, 0), + 1, + HexColor(appearance.header_row_color), + ), + ("LINEBELOW", (0, 0), (-1, -1), 0.4, HexColor(HAIRLINE)), + ("LEFTPADDING", (0, 0), (-1, -1), 5), + ("RIGHTPADDING", (0, 0), (-1, -1), 5), + ("TOPPADDING", (0, 0), (-1, -1), 3.5), + ("BOTTOMPADDING", (0, 0), (-1, -1), 3.5), + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ] + ) + ) + return table + + +def _prices(facts, context, styles): + appearance = context.appearance + headers = list(facts["headers"]) + # Price is the third column, right aligned like the other figures + headers[2] = _right_header(headers[2], appearance) + rows = [headers] + for row in facts["rows"]: + cells = [_text(cell) for cell in row] + cells[2] = _text(row[2], BODY_RIGHT) + rows.append(cells) + widths = ( + [4.0 * cm, 5.4 * cm, 3.6 * cm, 3.0 * cm] + if len(headers) == 4 + else [4.6 * cm, 6.8 * cm, 4.6 * cm] + ) + content = [ + _heading(facts, styles), + Paragraph(escape(facts["intro"]), styles["ContentStyle"]), + Spacer(1, 8), + data_table(rows, widths, appearance), + Spacer(1, 6), + ] + color = appearance.heading2_color + for text in (facts["units"], facts["no_archive"]): + if text: + content += [Paragraph(escape(text), CAPTION), Spacer(1, 2)] + for template, url in facts["sources"]: + content += [ + Paragraph(template.format(link=link_markup(url, color)), CAPTION), + Spacer(1, 2), + ] + content.append(Paragraph(escape(facts["note"]), CAPTION)) + return content + + +def appendix(context, estimate, styles, b): + facts = pricing_facts(estimate, b) + content = _prices(facts, context, styles) + content += [ + Spacer(1, 12), + _heading(facts, styles, "glossary_heading", "Heading2"), + Spacer(1, 4), + _glossary_table(facts["glossary"], context.appearance), + ] + return content + + +def next_steps(context, styles, b): + provenance = context.provenance + facts = next_steps_facts( + context.document_id, + provenance.uploaded, + provenance.platform_url, + provenance.assessment_id, + b, + ) + color = context.appearance.heading2_color + box = Table( + [ + [ + [ + Paragraph(parts_markup(parts, color), styles["ContentStyle"]) + for parts in facts["paragraphs"] + ] + ] + ], + colWidths=[16 * cm], + hAlign="LEFT", + ) + box.setStyle( + TableStyle( + [ + ("BACKGROUND", (0, 0), (-1, -1), HexColor(PROVENANCE_BACKGROUND)), + ("BOX", (0, 0), (-1, -1), 0.6, HexColor(RULE)), + ("LEFTPADDING", (0, 0), (-1, -1), 10), + ("RIGHTPADDING", (0, 0), (-1, -1), 10), + ("TOPPADDING", (0, 0), (-1, -1), 4), + ("BOTTOMPADDING", (0, 0), (-1, -1), 4), + ] + ) + ) + return [_heading(facts, styles), Spacer(1, 4), box] diff --git a/cloudexit/report/evidence.py b/cloudexit/report/evidence.py index daf91cc..63600e3 100644 --- a/cloudexit/report/evidence.py +++ b/cloudexit/report/evidence.py @@ -27,11 +27,21 @@ def _mono_value(text): return Paragraph(escape(text), KV_MONO) if text else None -def build_document_table(context, b): +def build_document_table( + context, b, document_id=None, document_type=None, companion_id=None +): provenance = context.provenance rows = [ - (b["ev_document_id"], Paragraph(escape(context.document_id), KV_MONO)), - (b["ev_document_type"], b["ev_executive_report"]), + ( + b["ev_document_id"], + Paragraph(escape(document_id or context.document_id), KV_MONO), + ), + (b["ev_document_type"], document_type or b["ev_executive_report"]), + *( + [(b["ev_companion_document"], Paragraph(escape(companion_id), KV_MONO))] + if companion_id + else [] + ), (b["ev_language"], b["language_name"]), ( b["ev_collected_by"], @@ -125,11 +135,13 @@ def build_provenance_table(context, b): return key_value_table(rows, context.appearance) -def build_document_information_page(context, b): +def build_document_information_page( + context, b, document_id=None, document_type=None, companion_id=None +): heading = _section_heading(context.appearance) return [ Paragraph(b["ev_document_information_heading"], heading), - build_document_table(context, b), + build_document_table(context, b, document_id, document_type, companion_id), Paragraph(b["ev_assessment_scope_heading"], heading), build_scope_table(context, b), Paragraph(b["ev_provenance_heading"], heading), diff --git a/cloudexit/report/helpers.py b/cloudexit/report/helpers.py index 0dbd794..b79acf4 100644 --- a/cloudexit/report/helpers.py +++ b/cloudexit/report/helpers.py @@ -19,6 +19,25 @@ def format_money(value, currency): return f"{symbol}{amount}" if symbol else f"{currency} {amount}" +def gib_number(size_bytes): + # One decimal; a size that is not zero never reads as 0.0 + gib = size_bytes / 2**30 + return "<0.1" if 0 < gib < 0.1 else f"{gib:,.1f}" + + +def format_gib(size_bytes): + if size_bytes is None: + return "n/a" + return f"{gib_number(size_bytes)} GiB" + + +def share_label(share): + # A share that is not zero never reads as "0 %" + if 0 < share < 0.5: + return "<1 %" + return f"{share:.0f} %" + + def format_change(previous, current): if not previous or current is None: return "–" diff --git a/cloudexit/report/html_sections.py b/cloudexit/report/html_sections.py index 14127b5..a087a15 100644 --- a/cloudexit/report/html_sections.py +++ b/cloudexit/report/html_sections.py @@ -4,7 +4,7 @@ from .helpers import severity_label from .notices import notices_html from .palette import SEVERITIES -from .sections import glossary_entries, scope_boundary, scope_strategy +from .sections import egress_note, glossary_entries, scope_boundary, scope_strategy from .wording import BLOCKS @@ -70,6 +70,7 @@ def html_sections(context, b=BLOCKS): "platform_url": provenance.platform_url, "document_id": context.document_id, }, + "costs_egress_note": egress_note(context, b), "introduction": { "heading": b["es_heading_introduction"], "purpose_heading": b["es_heading_purpose"], diff --git a/cloudexit/report/notices.py b/cloudexit/report/notices.py index e404996..866a7c8 100644 --- a/cloudexit/report/notices.py +++ b/cloudexit/report/notices.py @@ -20,7 +20,7 @@ FIELD = re.compile(r"({[a-z_]+})") -def _parts(template, links, values=None): +def text_parts(template, links, values=None): # A template split into plain text and (text, url) links, so the PDF and # the HTML are built from the same content parts = [] @@ -37,18 +37,26 @@ def _parts(template, links, values=None): def _icon_notice(provider, b): if provider == 2: - return _parts( + return text_parts( b["notices_icons_aws"], {"icons_url": AWS_ICONS_URL, "guidelines_url": AWS_TRADEMARK_URL}, ) if provider == 1: - return _parts(b["notices_icons_azure"], {"icons_url": AZURE_ICONS_URL}) + return text_parts(b["notices_icons_azure"], {"icons_url": AZURE_ICONS_URL}) return None -def notices_html(provider, generated_at, b=BLOCKS): +def notices_html(provider, generated_at, b=BLOCKS, egress=False): # The notices as plain data. A paragraph is a list of text and # (text, url) links. Only the notice for the report's own provider's icons. + # The Egress & Data Landscape adds what its figures are and are not. + disclaimer = [ + [b["notices_disclaimer"]], + [b["notices_generated"]], + [b["notices_purpose"]], + [b["notices_figures"]], + *([[b["notices_egress"]]] if egress else []), + ] trademarks = [[b["notices_trademark_own"]], [b["notices_trademark_others"]]] icons = _icon_notice(provider, b) if icons: @@ -58,18 +66,13 @@ def notices_html(provider, generated_at, b=BLOCKS): "blocks": [ { "heading": b["notices_disclaimer_heading"], - "paragraphs": [ - [b["notices_disclaimer"]], - [b["notices_generated"]], - [b["notices_purpose"]], - [b["notices_figures"]], - ], + "paragraphs": disclaimer, }, {"heading": b["notices_trademarks_heading"], "paragraphs": trademarks}, { "heading": b["notices_software_heading"], "paragraphs": [ - _parts( + text_parts( b["notices_software"], {"repository_url": REPOSITORY_URL, "licence_url": LICENCE_URL}, ) @@ -80,7 +83,7 @@ def notices_html(provider, generated_at, b=BLOCKS): } -def _markup(parts, color): +def parts_markup(parts, color): return "".join( link_markup(part[1], color) if isinstance(part, tuple) else escape(part) for part in parts @@ -104,9 +107,9 @@ def _styles(styles): ) -def legal_notices(context, styles, b): +def legal_notices(context, styles, b, egress=False): notices = notices_html( - context.cloud_service_provider, context.provenance.generated_at, b + context.cloud_service_provider, context.provenance.generated_at, b, egress ) color = context.appearance.heading2_color body, label = _styles(styles) @@ -114,7 +117,7 @@ def legal_notices(context, styles, b): for block in notices["blocks"]: content.append(Paragraph(escape(block["heading"]), label)) for parts in block["paragraphs"]: - content.append(Paragraph(_markup(parts, color), body)) + content.append(Paragraph(parts_markup(parts, color), body)) content.append(Spacer(1, 10)) content.append(Paragraph(escape(notices["copyright"]), body)) return content diff --git a/cloudexit/report/sections.py b/cloudexit/report/sections.py index 8e5e7e8..c9d5584 100644 --- a/cloudexit/report/sections.py +++ b/cloudexit/report/sections.py @@ -66,6 +66,7 @@ ("escapecloud", "https://escapecloud.io"), ("exitcloud", "https://exitcloud.io"), ) +BOOK_DEMO_URL = "https://escapecloud.io/book-demo/" AWS_ICONS_URL = "https://aws.amazon.com/architecture/icons/" AWS_TRADEMARK_URL = "https://aws.amazon.com/trademark-guidelines/" AZURE_ICONS_URL = "https://learn.microsoft.com/en-us/azure/architecture/icons/" @@ -252,15 +253,24 @@ def _cost_table(context, b): ] +def egress_note(context, b): + # The companion document, when the run has an egress estimate + if not context.egress_document_id: + return None + return b["es_costs_egress_note"].format(document_id=context.egress_document_id) + + def costs_overview(context, facts, styles, b): head = [ Paragraph(b["es_heading_costs_overview"], styles["Heading1"]), Paragraph(cost_finding(facts, b), styles["ContentStyle"]), Spacer(1, 8), ] + note = egress_note(context, b) + closing = [Paragraph(escape(note), styles["ContentStyle"])] if note else [] problem = cost_problem(context, b) if problem is not None: - return section(head, []) + return section(head, closing) entries = context.costs total = sum(entry.amount for entry in entries) latest = entries[-1] @@ -293,6 +303,7 @@ def costs_overview(context, facts, styles, b): *_cost_table(context, b), Spacer(1, 12), Paragraph(b["es_cost_note"], styles["ContentStyle"]), + *closing, ] return section(head, items) diff --git a/cloudexit/report/table_style.py b/cloudexit/report/table_style.py index 5b54247..5333dd7 100644 --- a/cloudexit/report/table_style.py +++ b/cloudexit/report/table_style.py @@ -1,5 +1,6 @@ from xml.sax.saxutils import escape +from reportlab.graphics.shapes import Drawing, Rect, String from reportlab.lib.colors import HexColor from reportlab.lib.enums import TA_LEFT from reportlab.lib.styles import ParagraphStyle @@ -7,6 +8,7 @@ from reportlab.platypus import Paragraph, Table, TableStyle from .fonts import FONT, FONT_BOLD, FONT_ITALIC, FONT_MONO +from .helpers import share_label from .palette import HAIRLINE, INK, MUTED, SUBTITLE, tint SECONDARY = SUBTITLE @@ -165,3 +167,32 @@ def summary_strip(stats, widths=None): ) ) return strip + + +def share_bar(share, appearance, width=5.0 * cm): + # A share of 0-100 as a bar with its label; the track is the full width + track = width - 34 + drawing = Drawing(width, 9) + drawing.add(Rect(0, 1, track, 7, fillColor=HexColor(HAIRLINE), strokeColor=None)) + if share: + drawing.add( + Rect( + 0, + 1, + track * share / 100, + 7, + fillColor=HexColor(appearance.header_row_color), + strokeColor=None, + ) + ) + drawing.add( + String( + track + 4, + 1.5, + share_label(share), + fontName=FONT, + fontSize=7.8, + fillColor=HexColor(INK), + ) + ) + return drawing diff --git a/cloudexit/report/wording.py b/cloudexit/report/wording.py index 7e95447..246033b 100644 --- a/cloudexit/report/wording.py +++ b/cloudexit/report/wording.py @@ -7,8 +7,10 @@ "ev_assessment_scope_heading": "Assessment Scope", "ev_provenance_heading": "Provenance", "ev_executive_report": "Executive Summary", + "ev_egress_report": "Egress & Data Landscape", "ev_document_id": "Document ID", "ev_document_type": "Document Type", + "ev_companion_document": "Companion Document", "ev_collected_by": "Collected by", "ev_collected_by_value": "cloudexit {version}", "ev_collected_at": "Collected on", @@ -41,6 +43,7 @@ "unchanged. Later runs are reported in their own documents." ), "cover_title_executive_summary": "Executive Summary", + "cover_title_egress": "Egress & Data Landscape", "cover_doc_id": "Document ID", "cover_edition": "Edition", "cover_edition_value": "Community", @@ -206,6 +209,10 @@ "cost_caption_provisional": "Provisional = the month was still accruing when the data was collected.", "cost_tfstate": "Terraform state files hold no billing data, so there are no costs to show for this assessment.", "es_cost_note": "Costs are shown for context. They do not affect the risk evaluation and are not an estimate of exit costs.", + "es_costs_egress_note": ( + "A separate Egress & Data Landscape ({document_id}) estimates the data " + "held in scope and the cost of moving it out." + ), "es_heading_resource_inventory": "Resource Inventory", "es_res_lead": ( "Cloud resources discovered in the defined scope. Each resource type " @@ -348,10 +355,6 @@ "Scored Alternatives", "the alternative technologies for each resource type, rated and ranked.", ), - ( - "Egress & Data Landscape", - "the data held by the resources in scope and an estimate of the one-time fee for moving it out.", - ), ], "fa_how_heading": "How to Get Them", "fa_how": ( @@ -369,6 +372,247 @@ ), "fa_uploaded_link": "Open the reports: {link}", "fa_uploaded_id": "Assessment ID: {assessment_id}", + "eg_heading_introduction": "Introduction", + "eg_heading_purpose": "Purpose", + "eg_heading_scope": "Scope", + "eg_heading_limitations": "Limitations", + "eg_purpose_block": ( + "This document quantifies the data held by the data-bearing resources " + "in the defined scope and estimates the one-time fee for moving that " + "data out of the provider at list prices. The fee is one component of " + "the cost of an exit; it is shown early so that it can be planned " + "rather than discovered during migration. It accompanies the " + "Executive Summary of the same run." + ), + "eg_scope_azure": ( + "The estimate covers the data-bearing resources in Microsoft Azure " + "resource group {resource_group}. Resources outside this resource " + "group are not included." + ), + "eg_scope_aws": ( + "The estimate covers the data-bearing resources in AWS region " + "{region}. Other regions are not included." + ), + "eg_scope_plain": ( + "The estimate covers the data-bearing resources in the defined scope." + ), + "eg_limitations_block": ( + "Sizes are collected from provider metrics and, for some resource " + "types, represent provisioned rather than used capacity. Resources " + "that cannot be sized through the provider APIs are listed but " + "excluded from the fee estimate. Fees are calculated from public list " + "prices at the time of the run and exclude request and transaction " + "charges, cross-region traffic, negotiated discounts and any " + "committed-use agreements already in place." + ), + "eg_limitations_scope": ( + "The estimate covers data transfer fees only. It is not an estimate " + "of the full cost of an exit, which also includes migration effort, " + "parallel operation and contract terms. Figures are estimates and can " + "be incomplete in some environments." + ), + "eg_limitations_data": ( + "Figures reflect the data of the run shown on page 2 and may change " + "as the environment changes." + ), + "eg_heading_coverage": "Scope of the Estimation", + "eg_coverage_intro": ( + "The estimate covers the following data-bearing resource types. Each " + "note states how the size is measured, or why a type is not sized." + ), + "eg_th_resource_type": "Resource Type", + "eg_th_measured": "How the Size Is Measured", + "eg_coverage_notes_azure": [ + ("Storage Accounts", "Capacity metrics can lag by up to roughly 48 hours"), + ( + "Managed Disks and Point-in-Time Copies", + "Provisioned size; counted as an upper bound", + ), + ("SQL Databases", "Used space from database metrics"), + ("Cosmos DB Accounts", "Used space from database metrics"), + ("PostgreSQL / MySQL Flexible Servers", "Used space from database metrics"), + ("Recovery Services Vaults", "Not sized; recovery points cannot be exported"), + ], + "eg_coverage_notes_aws": [ + ("S3 Buckets", "Metrics can lag by up to roughly 48 hours"), + ( + "EBS Volumes and Point-in-Time Copies", + "Upper bound; copies can over-count shared blocks", + ), + ("RDS Instances", "Aurora is not sized"), + ("DynamoDB Tables", ""), + ("AWS Backup Vaults", "Not sized; recovery points cannot be exported"), + ], + "eg_heading_landscape": "Data Landscape", + "eg_no_data_notice": ( + "No data-bearing resources were identified in the defined scope, so " + "there is no data to show and no egress fee to estimate." + ), + "eg_land_lead": ( + "The defined scope holds {total} of data in {sized} of {resources} " + "data-bearing resources." + ), + "eg_land_lead_one": ( + "The defined scope holds {total} of data in its only data-bearing " "resource." + ), + "eg_land_top": "{type} holds the largest share ({size}, {share}).", + "eg_land_unsized_one": "1 resource could not be sized and is not in the totals.", + "eg_land_unsized_many": ( + "{count} resources could not be sized and are not in the totals." + ), + "eg_land_none_sized": ( + "None of the {resources} data-bearing resources could be sized." + ), + "eg_land_none_sized_one": "The data-bearing resource could not be sized.", + "eg_st_volume": "Data Volume", + "eg_st_sized": "Resources Sized", + "eg_st_sized_value": "{sized} of {resources}", + "eg_st_not_sized": "Not Sized", + "eg_st_largest": "Largest Type", + "eg_th_resources": "Resources", + "eg_th_size_gib": "Size (GiB)", + "eg_th_share": "Share of Volume", + "eg_total": "Total", + "eg_land_not_sized": "Not sized: {types}.", + "eg_land_not_sized_one": "{type} (1 resource)", + "eg_land_not_sized_many": "{type} ({count} resources)", + "eg_land_caption": ( + "All sizes in GiB (1 GiB = 1,073,741,824 bytes). Some resource types " + "report provisioned rather than used capacity; see Scope of the " + "Estimation." + ), + "eg_land_source_note": ( + "Data-bearing resources are collected separately from the resource " + "inventory of the Executive Summary and can include types it does " + "not list, such as managed disks." + ), + "eg_heading_costs": "Estimated Costs", + "eg_cost_lead_free": ( + "The one-time egress fee for the sized data is estimated at {fee}: it " + "stays within the provider's free monthly internet egress allowance." + ), + "eg_cost_headroom": ( + "The sized data takes up {used} of the {allowance} monthly free tier." + ), + "eg_cost_lead_paid": ( + "Moving the sized data ({total}) out of the provider is estimated to " + "cost {fee} once, at list prices." + ), + "eg_cost_lead_allowance": ( + "The first {allowance} per month are free; the remaining {billable} " + "are billed at tiered rates (see the Appendix)." + ), + "eg_cost_lead_top": "{type} accounts for the largest share ({share}).", + "eg_cost_lead_none": ( + "No fee can be estimated because none of the data-bearing resources " + "could be sized." + ), + "eg_cost_unsized_one": "1 resource could not be sized and is not included.", + "eg_cost_unsized_many": ( + "{count} resources could not be sized and are not included." + ), + "eg_st_fee": "Estimated Fee (USD)", + "eg_st_allowance": "Free Allowance / Month", + "eg_st_billable": "Billable Volume", + "eg_th_fee": "Estimated Fee (USD)", + "eg_cost_caption": ( + "Amounts in USD: the provider publishes egress list prices in USD and " + "they are not converted. Internet egress is billed on the whole " + "volume, so the fee of each resource type is its share of the total, " + "allocated by size. Excludes request and transaction charges, " + "cross-region traffic and negotiated discounts. A + means that " + "resources without a size are left out, so the figure is a lower " + "bound." + ), + "eg_heading_pricing": "Appendix – Egress Prices", + "eg_th_component": "Component", + "eg_th_tier": "Tier", + "eg_th_price": "Price (USD)", + "eg_th_valid_from": "Valid From", + "eg_tier_all_volumes": "all volumes", + "eg_tier_first": "first {to} {unit} / month", + "eg_tier_over": "over {from_} {unit}", + "eg_tier_range": "{from_} – {to} {unit}", + "eg_price_free": "Free", + "eg_component_internet_egress": "Internet egress", + "eg_component_archive_retrieval": "Archive retrieval", + "eg_pricing_intro": ( + "The estimate uses the following {provider} list prices (zone 1 – " + "US/Europe, public internet routing), valid from {valid}." + ), + "eg_pricing_intro_mixed": ( + "The estimate uses the following {provider} list prices (zone 1 – " + "US/Europe, public internet routing)." + ), + "eg_pricing_units": ( + "Prices per GB as published by {provider} (1 GB = 1,000,000,000 " + "bytes). Sizes in this document are in GiB (1,073,741,824 bytes); " + "1 GiB = 1.074 GB." + ), + "eg_pricing_units_gib": "Prices per GiB as published by {provider}.", + "eg_provider_fallback": "the cloud provider", + "eg_pricing_no_archive": ( + "Archive retrieval pricing was not available for this provider at the " + "time of the run; rehydration fees are not included in the estimate." + ), + "eg_pricing_note": ( + "List prices at the time of the run; they exclude request and " + "transaction charges and cross-region traffic." + ), + "eg_pricing_source": "Source: {link}", + "eg_pricing_source_component": "Source ({component}): {link}", + "eg_heading_glossary": "Glossary", + "eg_glossary": [ + { + "id": "egress", + "term": "Egress", + "definition": "Data that leaves a cloud provider's network, for example to the internet, an on-premises site or another provider.", + }, + { + "id": "free_tier", + "term": "Free tier", + "definition": "The monthly volume of internet egress that a provider does not charge for.", + }, + { + "id": "gib", + "term": "GiB", + "definition": "Gibibyte, 1,073,741,824 bytes. Sizes in this document are in GiB.", + }, + { + "id": "list_price", + "term": "List price", + "definition": "The price a provider publishes, before any negotiated discount or commitment.", + }, + { + "id": "switching", + "term": "Switching charges", + "definition": "Fees a provider charges for moving data or workloads to another provider, including data egress.", + }, + ], + "eg_heading_next_steps": "Next Steps", + "eg_next_lead": ( + "Use this estimate together with the Executive Summary of the same " + "run ({executive_id}) when planning an exit." + ), + "eg_next_platforms": ( + "Connected ({exitcloud_url}) and Enterprise ({book_demo_url}) combine " + "this estimate with the Exit Readiness Report and version history." + ), + "eg_next_uploaded": ( + "This run was uploaded. The platform combines this estimate with the " + "Exit Readiness Report and version history." + ), + "notices_egress": ( + "Egress figures are estimates based on the provider's public list " + "prices at the time of the run. Actual invoices can differ: " + "negotiated discounts and commitments, request charges, cross-region " + "traffic and private connections are not included. Under the EU Data " + "Act, providers may charge only reduced, cost-based fees for " + "switching, including data egress, until 12 January 2027, and no " + "switching charges after that date. Several providers already waive " + "egress fees on request for customers who leave. This estimate " + "assumes no waiver." + ), "notices_heading": "Legal Notices", "notices_disclaimer_heading": "Disclaimer", "notices_disclaimer": ( @@ -444,6 +688,37 @@ "ev_run_id", "ev_platform_label", "cover_title_executive_summary", + "cover_title_egress", + "ev_egress_report", + "ev_companion_document", + "eg_heading_introduction", + "eg_heading_purpose", + "eg_heading_scope", + "eg_heading_limitations", + "eg_heading_coverage", + "eg_heading_landscape", + "eg_heading_costs", + "eg_heading_pricing", + "eg_heading_glossary", + "eg_heading_next_steps", + "eg_th_resource_type", + "eg_th_measured", + "eg_th_resources", + "eg_th_size_gib", + "eg_th_share", + "eg_th_fee", + "eg_th_component", + "eg_th_tier", + "eg_th_price", + "eg_th_valid_from", + "eg_total", + "eg_st_volume", + "eg_st_sized", + "eg_st_not_sized", + "eg_st_largest", + "eg_st_fee", + "eg_st_allowance", + "eg_st_billable", "cover_doc_id", "cover_edition", "cover_edition_value", diff --git a/tests/cli/test_app.py b/tests/cli/test_app.py index bae1436..7fe1f83 100644 --- a/tests/cli/test_app.py +++ b/tests/cli/test_app.py @@ -629,10 +629,18 @@ class EgressStageTests(unittest.TestCase): "logs": "", "html_path": "/tmp/report/egress.html", } - _PDF_OK = { + # The egress PDF is written with the report, from the same context + _WITH_PDF = { "success": True, - "logs": "", - "pdf_path": "/tmp/report/egress.pdf", + "reports": { + "HTML": "/tmp/report/index.html", + "EGRESS_PDF": "/tmp/report/egress.pdf", + }, + } + _PDF_FAILED = { + "success": True, + "reports": {"HTML": "/tmp/report/index.html"}, + "egress_pdf_logs": "reportlab error", } @staticmethod @@ -650,21 +658,38 @@ def _azure_config(): }, } - def test_egress_module_not_invoked_without_flag(self): + def _run(self, config, provider, report=None, estimate=None, html=None, **kwargs): patches = _base_patches() for p in patches: p.start() - mock_estimate = patch("cloudexit.cli.app.estimate_egress").start() - mock_render = patch("cloudexit.cli.app.generate_egress_html_report").start() - mock_pdf = patch("cloudexit.cli.app.generate_egress_pdf_report").start() + mocks = { + "report": patch( + "cloudexit.cli.app.generate_report", + return_value=report or self._WITH_PDF, + ).start(), + "estimate": patch( + "cloudexit.cli.app.estimate_egress", + return_value=estimate or self._ESTIMATE_OK, + ).start(), + "html": patch( + "cloudexit.cli.app.generate_egress_html_report", + return_value=html or self._REPORT_OK, + ).start(), + "print": patch("cloudexit.cli.app.console.print").start(), + "step": patch("cloudexit.cli.app.print_step").start(), + } try: - app.run_assessment(self._azure_config(), "azure") + app.run_assessment(config, provider, **kwargs) finally: patch.stopall() + return mocks - mock_estimate.assert_not_called() - mock_render.assert_not_called() - mock_pdf.assert_not_called() + def test_egress_module_not_invoked_without_flag(self): + mocks = self._run(self._azure_config(), "azure") + + mocks["estimate"].assert_not_called() + mocks["html"].assert_not_called() + self.assertIsNone(mocks["report"].call_args.kwargs["egress_json_path"]) def test_egress_runs_in_the_collection_stages_before_the_report(self): manager = MagicMock() @@ -672,8 +697,7 @@ def test_egress_runs_in_the_collection_stages_before_the_report(self): for p in patches: p.start() mock_report = patch( - "cloudexit.cli.app.generate_report", - return_value={"success": True, "reports": {}}, + "cloudexit.cli.app.generate_report", return_value=self._WITH_PDF ).start() mock_estimate = patch( "cloudexit.cli.app.estimate_egress", return_value=self._ESTIMATE_OK @@ -682,13 +706,9 @@ def test_egress_runs_in_the_collection_stages_before_the_report(self): "cloudexit.cli.app.generate_egress_html_report", return_value=self._REPORT_OK, ).start() - mock_pdf = patch( - "cloudexit.cli.app.generate_egress_pdf_report", return_value=self._PDF_OK - ).start() manager.attach_mock(mock_report, "generate_report") manager.attach_mock(mock_estimate, "estimate_egress") manager.attach_mock(mock_render, "generate_egress_html_report") - manager.attach_mock(mock_pdf, "generate_egress_pdf_report") config = self._azure_config() try: app.run_assessment(config, "azure", egress=True) @@ -704,29 +724,40 @@ def test_egress_runs_in_the_collection_stages_before_the_report(self): exit_strategy=config["exitStrategy"], assessment_type=config["assessmentType"], ) + # The HTML carries the scope and the run, so its ID matches the PDF's mock_render.assert_called_once_with( - "/tmp/report", "/tmp/report/raw/egress_inventory_raw_data.json" - ) - mock_pdf.assert_called_once_with( "/tmp/report", "/tmp/report/raw/egress_inventory_raw_data.json", config["providerDetails"], + ANY, + ) + sync_id = mock_render.call_args.args[3] + self.assertEqual( + mock_report.call_args.kwargs["egress_json_path"], + "/tmp/report/raw/egress_inventory_raw_data.json", ) + self.assertEqual(mock_report.call_args.kwargs["sync_id"], sync_id) call_names = [name for name, _, _ in manager.mock_calls] self.assertLess( call_names.index("estimate_egress"), call_names.index("generate_egress_html_report"), ) + # The PDF is part of the report, after the upload self.assertLess( call_names.index("generate_egress_html_report"), - call_names.index("generate_egress_pdf_report"), - ) - # The estimate is part of the collection stages, before the local report - self.assertLess( - call_names.index("generate_egress_pdf_report"), call_names.index("generate_report"), ) + def test_the_egress_pdf_step_is_printed_and_the_path_listed(self): + mocks = self._run(self._azure_config(), "azure", egress=True) + + steps = [call.args[0] for call in mocks["step"].call_args_list] + self.assertIn("Generating egress PDF...", steps) + printed = [str(call) for call in mocks["print"].call_args_list] + self.assertTrue( + any("Egress PDF: /tmp/report/egress.pdf" in line for line in printed) + ) + def test_egress_failure_still_prints_outputs_and_exits_egress(self): patches = _base_patches() for p in patches: @@ -736,7 +767,6 @@ def test_egress_failure_still_prints_outputs_and_exits_egress(self): return_value={"success": False, "logs": "metrics api error"}, ).start() mock_render = patch("cloudexit.cli.app.generate_egress_html_report").start() - mock_pdf = patch("cloudexit.cli.app.generate_egress_pdf_report").start() mock_print = patch("cloudexit.cli.app.console.print").start() try: with self.assertRaises(SystemExit) as ctx: @@ -746,7 +776,6 @@ def test_egress_failure_still_prints_outputs_and_exits_egress(self): self.assertEqual(ctx.exception.code, codes.EGRESS) mock_render.assert_not_called() - mock_pdf.assert_not_called() printed = [str(call) for call in mock_print.call_args_list] self.assertTrue(any("Outputs:" in line for line in printed)) @@ -761,9 +790,7 @@ def test_egress_report_failure_still_prints_outputs_and_exits_egress(self): "cloudexit.cli.app.generate_egress_html_report", return_value={"success": False, "logs": "template error"}, ).start() - patch( - "cloudexit.cli.app.generate_egress_pdf_report", return_value=self._PDF_OK - ).start() + patch("cloudexit.cli.app.generate_report", return_value=self._WITH_PDF).start() mock_print = patch("cloudexit.cli.app.console.print").start() try: with self.assertRaises(SystemExit) as ctx: @@ -775,8 +802,8 @@ def test_egress_report_failure_still_prints_outputs_and_exits_egress(self): printed = [str(call) for call in mock_print.call_args_list] self.assertTrue(any("Outputs:" in line for line in printed)) # The JSON path is internal input for the reports; it is never listed - # in Outputs. The failed HTML report is not listed, but the PDF ran - # independently and succeeded, so it is. + # in Outputs. The failed HTML report is not listed, but the PDF is + # written independently and succeeded, so it is. self.assertFalse(any("Egress Estimate:" in line for line in printed)) self.assertFalse(any("Egress Report:" in line for line in printed)) self.assertTrue(any("Egress PDF:" in line for line in printed)) @@ -793,9 +820,9 @@ def test_egress_pdf_failure_still_prints_outputs_and_exits_egress(self): return_value=self._REPORT_OK, ).start() patch( - "cloudexit.cli.app.generate_egress_pdf_report", - return_value={"success": False, "logs": "reportlab error"}, + "cloudexit.cli.app.generate_report", return_value=self._PDF_FAILED ).start() + mock_step = patch("cloudexit.cli.app.print_step").start() mock_print = patch("cloudexit.cli.app.console.print").start() try: with self.assertRaises(SystemExit) as ctx: @@ -803,11 +830,27 @@ def test_egress_pdf_failure_still_prints_outputs_and_exits_egress(self): finally: patch.stopall() + # The Executive Summary was written; only the egress exit code is set self.assertEqual(ctx.exception.code, codes.EGRESS) printed = [str(call) for call in mock_print.call_args_list] self.assertTrue(any("Outputs:" in line for line in printed)) + self.assertTrue(any("HTML Report:" in line for line in printed)) self.assertTrue(any("Egress Report:" in line for line in printed)) self.assertFalse(any("Egress PDF:" in line for line in printed)) + failed = [ + call + for call in mock_step.call_args_list + if call.args[:1] == ("Generating egress PDF...",) + ] + self.assertEqual(failed[0].kwargs["status"], "error") + self.assertEqual(failed[0].kwargs["logs"], "reportlab error") + + def test_a_failed_report_is_still_a_report_failure(self): + mocks_report = {"success": False, "logs": "boom"} + with self.assertRaises(SystemExit) as ctx: + self._run(self._azure_config(), "azure", report=mocks_report, egress=True) + + self.assertEqual(ctx.exception.code, codes.REPORT) def test_egress_invoked_for_aws_with_provider_code(self): patches = _base_patches() @@ -820,9 +863,7 @@ def test_egress_invoked_for_aws_with_provider_code(self): "cloudexit.cli.app.generate_egress_html_report", return_value=self._REPORT_OK, ).start() - patch( - "cloudexit.cli.app.generate_egress_pdf_report", return_value=self._PDF_OK - ).start() + patch("cloudexit.cli.app.generate_report", return_value=self._WITH_PDF).start() config = VALID_CONFIG.copy() try: app.run_assessment(config, "aws", egress=True) diff --git a/tests/platform/test_document.py b/tests/platform/test_document.py index 94a8e78..6397e75 100644 --- a/tests/platform/test_document.py +++ b/tests/platform/test_document.py @@ -848,7 +848,10 @@ def run_assessment(self, report, *, egress_ok=None, **kwargs): ), patch( "cloudexit.cli.app.generate_report", - return_value={"success": True, "reports": {}}, + return_value={ + "success": True, + "reports": {"EGRESS_PDF": "p"} if egress_ok else {}, + }, ), patch("cloudexit.cli.app.add_run_log_handler"), patch("cloudexit.cli.app.print_step"), @@ -864,10 +867,6 @@ def run_assessment(self, report, *, egress_ok=None, **kwargs): "cloudexit.cli.app.generate_egress_html_report", return_value={"success": True, "html_path": "h"}, ), - patch( - "cloudexit.cli.app.generate_egress_pdf_report", - return_value={"success": True, "pdf_path": "p"}, - ), ] for item in patches: item.start() diff --git a/tests/report/test_basic_report.py b/tests/report/test_basic_report.py index a1aea40..5da2f82 100644 --- a/tests/report/test_basic_report.py +++ b/tests/report/test_basic_report.py @@ -600,9 +600,13 @@ def test_the_closing_page_only_names_what_the_full_assessment_adds(self): "Exit Readiness Grade", "Vendor Lock-in Score", "Scored Alternatives", - "Egress & Data Landscape", ): self.assertIn(name, closing) + # The Community Edition produces it itself, so the platforms do not add it + self.assertNotIn("Egress & Data Landscape", closing) + self.assertFalse( + any("Egress" in term for term, _text in BLOCKS["fa_items"]), + ) self.assertNotRegex(closing, r"\d+ / (100|5)") def test_the_html_report_has_no_scores_before_the_closing_section(self): @@ -979,6 +983,21 @@ def test_the_new_labels_are_in_the_title_case_list(self): ): self.assertIn(key, TITLE_CASE_KEYS) + def test_the_egress_labels_are_in_the_title_case_list(self): + for key in ( + "cover_title_egress", + "ev_egress_report", + "ev_companion_document", + "eg_heading_coverage", + "eg_heading_landscape", + "eg_heading_costs", + "eg_heading_pricing", + "eg_heading_next_steps", + "eg_th_size_gib", + "eg_st_largest", + ): + self.assertIn(key, TITLE_CASE_KEYS) + def test_the_retired_classification_is_gone(self): self.assertNotIn("cover_classification", BLOCKS) self.assertNotIn("cover_classification_value", BLOCKS) @@ -1021,7 +1040,8 @@ def test_no_wording_uses_a_retired_term_or_american_spelling(self): def test_no_wording_carries_regulatory_words_or_scores(self): for key, text in self.strings(): - if key.startswith("fa_"): + # The pages that point to the platforms name what they add + if key.startswith(("fa_", "eg_next_")): continue with self.subTest(key): self.assertEqual(find_terms(text), []) diff --git a/tests/report/test_egress.py b/tests/report/test_egress.py index 77cb804..b1c33f9 100644 --- a/tests/report/test_egress.py +++ b/tests/report/test_egress.py @@ -6,10 +6,6 @@ import unittest from unittest.mock import patch -from reportlab.graphics.shapes import Drawing -from reportlab.platypus import PageBreak, Paragraph, Table - -from tests.support.report import stage_report_assets from cloudexit.resources import asset_path from cloudexit.egress.model import GIB, write_egress_inventory @@ -17,10 +13,6 @@ from cloudexit.report.egress import ( CATEGORIES, _build_allocation, - _build_coverage_section, - _build_data_landscape_section, - _build_estimated_costs_section, - _build_pricing_basis_section, _build_type_groups, _resolve_icon, build_fee_estimate, @@ -28,7 +20,6 @@ format_fee, free_tier_limit_display, generate_egress_html_report, - generate_egress_pdf_report, load_pricing, ) @@ -525,11 +516,9 @@ def test_renders_report_with_fees_table_and_charts(self): self.assertTrue(result["success"], result["logs"]) self.assertIn("Exit Assessment Test", html) normalized_html = " ".join(html.split()) - self.assertIn( - "The Data Landscape & Egress Estimation feature is currently in", - html, - ) - self.assertIn("mailto:beta@escapecloud.io", html) + # The alpha banner is a limitation in the Introduction now + self.assertNotIn("alpha", html) + self.assertNotIn("mailto:beta@escapecloud.io", html) # (2560 - 100) GiB * 0.087 = $214.02 total internet egress fee self.assertIn("$214.02", html) self.assertIn("Estimated Egress Fee", html) @@ -556,7 +545,8 @@ def test_unknown_sizes_add_plus_suffix_to_headlines(self): self.assertTrue(result["success"], result["logs"]) # 2048 GiB known: (2048 - 100) * 0.087 = $169.48, "+" marks the lower bound - self.assertIn("2.0 TiB+", html) + self.assertIn("2,048.0 GiB+", html) + self.assertNotIn("TiB", html) self.assertIn("$169.48+", html) self.assertIn("Excludes 1 resource", html) self.assertIn("n/a", html) @@ -567,9 +557,9 @@ def test_free_tier_environment_shows_included_in_free_tier_card(self): self.assertTrue(result["success"], result["logs"]) self.assertIn("Included in Free Tier", html) - # The fee headline stays visible above the card, as a clean "$0". + # The fee headline stays visible above the card, in the one money format self.assertIn("Estimated Egress Fee", html) - self.assertIn('
$0
', html) + self.assertIn('
$0.00
', html) self.assertIn("5.0 GiB", html) self.assertIn("100 GiB free tier", html) self.assertNotIn("No fee estimate available.", html) @@ -597,369 +587,3 @@ def test_missing_json_returns_failure(self): ) self.assertFalse(result["success"]) - - -def _make_pdf_styles(): - from reportlab.lib.colors import HexColor - from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet - - styles = getSampleStyleSheet() - content_style = ParagraphStyle( - "ContentStyle", fontSize=10, leading=12, spaceAfter=10 - ) - styles["Heading1"].textColor = HexColor("#112726") - styles["Heading2"].textColor = HexColor("#112726") - return styles, content_style - - -class BuildEstimatedCostsSectionTests(unittest.TestCase): - def setUp(self): - self.styles, self.content_style = _make_pdf_styles() - self.type_groups = [ - { - "label": "Storage Account", - "fee": 180.0, - "fee_display": "$180.00", - }, - { - "label": "Managed Disk", - "fee": 34.02, - "fee_display": "$34.02", - }, - { - "label": "Backup Vault", - "fee": None, - "fee_display": "n/a", - }, - ] - - def _totals(self, known, unknown_count=0): - return { - "known_size_bytes": known, - "archive_tier_bytes": 0, - "resources_discovered": 1, - "resources_with_unknown_size": unknown_count, - } - - def _tables(self, content): - tables = [f for f in content if isinstance(f, Table)] - self.assertGreaterEqual(len(tables), 2) - return tables - - def test_normal_fee_shell(self): - content = _build_estimated_costs_section( - 214.02, - self._totals(2560 * GIB), - False, - "100 GiB", - self.type_groups, - self.styles, - self.content_style, - ) - - tables = self._tables(content) - self.assertEqual(tables[0]._cellvalues[1][0].text, "$214.02") - self.assertIsInstance(tables[1]._cellvalues[0][0], Drawing) - breakdown = tables[-1]._cellvalues - self.assertEqual(breakdown[1], ["Storage Account", "$180.00"]) - self.assertEqual(breakdown[2], ["Managed Disk", "$34.02"]) - self.assertEqual(breakdown[3], ["Backup Vault", "n/a (not sized)"]) - self.assertEqual(breakdown[-1][0].text, "Estimated Total Egress Fee") - self.assertEqual(breakdown[-1][1].text, "$214.02") - paragraphs = [f.text for f in content if isinstance(f, Paragraph)] - self.assertTrue(any("Estimated Costs" in text for text in paragraphs)) - self.assertTrue(any("Cost Breakdown" in text for text in paragraphs)) - - def test_unknowns_add_plus_suffix_to_fee(self): - content = _build_estimated_costs_section( - 100.0, - self._totals(2000 * GIB, unknown_count=2), - False, - "100 GiB", - self.type_groups, - self.styles, - self.content_style, - ) - - tables = self._tables(content) - self.assertEqual(tables[0]._cellvalues[1][0].text, "$100.00+") - self.assertIn("Excludes 2 resources", tables[0]._cellvalues[2][0].text) - paragraphs = [f.text for f in content if isinstance(f, Paragraph)] - self.assertTrue(any("Estimated Costs" in text for text in paragraphs)) - - def test_free_tier_variant(self): - content = _build_estimated_costs_section( - 0.0, - self._totals(5 * GIB), - True, - "100 GiB", - [{"label": "Storage Account", "fee": 0.0, "fee_display": "$0.00"}], - self.styles, - self.content_style, - ) - - tables = self._tables(content) - self.assertEqual(tables[0]._cellvalues[1][0].text, "$0") - self.assertEqual(len(tables), 2) - paragraphs = [f.text for f in content if isinstance(f, Paragraph)] - self.assertTrue(any("100 GiB free tier" in text for text in paragraphs)) - - -class BuildDataLandscapeSectionTests(unittest.TestCase): - def test_table_rows_and_total(self): - styles, content_style = _make_pdf_styles() - type_groups = [ - { - "label": "S3 Bucket", - "icon": "/icons/aws/Storage/Simple-Storage-Service.png", - "fee_display": "$200.00", - "size_display": "20.0 MiB", - }, - { - "label": "Backup Vault", - "icon": "/icons/aws/Storage/Backup.png", - "fee_display": "n/a", - "size_display": "n/a", - }, - ] - - with tempfile.TemporaryDirectory() as tmp_dir: - content = _build_data_landscape_section( - type_groups, - "20.0 MiB+", - tmp_dir, - styles, - content_style, - ) - - tables = [f for f in content if isinstance(f, Table)] - self.assertEqual(len(tables), 1) - cells = tables[0]._cellvalues - # Costs column deliberately absent — dollars live on the Estimated - # Costs page so each type is not shown twice in the report. - self.assertEqual(cells[0], ["#", "Resource Type", "", "Size"]) - self.assertEqual(cells[1][1], "S3 Bucket") - self.assertEqual(cells[1][3], "20.0 MiB") - self.assertEqual(cells[2][3], "n/a") - self.assertEqual(cells[-1][0], "Total") - self.assertEqual(cells[-1][3], "20.0 MiB+") - for row in cells: - for cell in row: - self.assertNotIn("$", str(cell)) - - -class BuildCoverageSectionTests(unittest.TestCase): - def setUp(self): - self.styles, self.content_style = _make_pdf_styles() - - def _cells(self, content): - tables = [f for f in content if isinstance(f, Table)] - self.assertEqual(len(tables), 1) - return tables[0]._cellvalues - - def test_aws_coverage_rows_and_alpha_note(self): - content = _build_coverage_section(2, self.styles, self.content_style) - - cells = self._cells(content) - self.assertEqual(cells[0], ["Name", "Notes"]) - names = [row[0] for row in cells[1:]] - self.assertIn("S3 Buckets", names) - self.assertIn("AWS Backup Vaults", names) - texts = [f.text for f in content if isinstance(f, Paragraph)] - self.assertTrue(any(text.startswith("NOTE:") for text in texts)) - self.assertTrue(any("alpha version" in text for text in texts)) - # Estimated Costs starts on a fresh page after the coverage block. - self.assertIsInstance(content[-1], PageBreak) - - def test_azure_coverage_rows(self): - content = _build_coverage_section(1, self.styles, self.content_style) - - names = [row[0] for row in self._cells(content)[1:]] - self.assertIn("Storage Accounts", names) - self.assertIn("Recovery Services Vaults", names) - - def test_unknown_provider_renders_nothing(self): - self.assertEqual( - _build_coverage_section(9, self.styles, self.content_style), [] - ) - - -class BuildPricingBasisSectionTests(unittest.TestCase): - def setUp(self): - self.styles, self.content_style = _make_pdf_styles() - - def _texts(self, content): - return [f.text for f in content if isinstance(f, Paragraph)] - - def _table(self, content): - tables = [f for f in content if isinstance(f, Table)] - self.assertEqual(len(tables), 1) - return tables[0]._cellvalues - - def test_renders_used_price_rows_with_free_tier(self): - content = _build_pricing_basis_section( - RENDER_PRICING, 1, self.styles, self.content_style - ) - - texts = self._texts(content) - self.assertTrue(any("Appendix – Egress Prices" in text for text in texts)) - self.assertTrue( - any( - "Microsoft Azure list prices (Zone 1 – US/Europe, public " - "internet routing):" in text - for text in texts - ) - ) - self.assertFalse(any("effective as of" in text for text in texts)) - cells = self._table(content) - self.assertEqual(cells[0], ["#", "Component", "Tier", "Price"]) - self.assertEqual(cells[1][1], "Internet Egress") - self.assertEqual(cells[1][2], "first 100 GiB / month") - self.assertEqual(cells[1][3], "Free") - self.assertEqual(cells[2][3], "$0.087 / GiB") - self.assertEqual(cells[-1][1], "Archive Retrieval") - self.assertEqual(cells[-1][2], "all volumes") - self.assertEqual(cells[-1][3], "$0.02 / GiB") - self.assertFalse(any("rehydration fees" in text for text in texts)) - self.assertTrue( - any( - "Source (Internet Egress): " - "https://azure.microsoft.com/pricing/details/bandwidth/" in text - for text in texts - ) - ) - self.assertTrue( - any( - "Source (Archive Retrieval): " - "https://azure.microsoft.com/pricing/details/storage/blobs/" in text - for text in texts - ) - ) - self.assertTrue( - any("List prices at the time of the assessment" in text for text in texts) - ) - - def test_missing_retrieval_rows_add_note(self): - content = _build_pricing_basis_section( - AZURE_INTERNET_TIERS, 1, self.styles, self.content_style - ) - - texts = self._texts(content) - self.assertTrue( - any("rehydration fees are not included" in text for text in texts) - ) - self.assertTrue( - any( - "Source: https://azure.microsoft.com/pricing/details/bandwidth/" in text - for text in texts - ) - ) - self.assertFalse(any("storage/blobs" in text for text in texts)) - - def test_unused_zone_component_and_provider_rows_are_excluded(self): - prices = RENDER_PRICING + [ - _price_row(1, "internet_egress", 0, None, 9.9, zone="zone3"), - _price_row(1, "internet_egress_transit_isp", 0, None, 9.9), - _price_row(2, "internet_egress", 0, None, 9.9), - ] - - content = _build_pricing_basis_section( - prices, 1, self.styles, self.content_style - ) - - cells = self._table(content) - self.assertEqual(len(cells) - 1, len(RENDER_PRICING)) - - def test_mixed_valid_from_adds_column_without_effective_date_claim(self): - retrieval = dict( - _price_row(1, "archive_retrieval", 0, None, 0.02), - valid_from="2026-07-14", - ) - prices = AZURE_INTERNET_TIERS + [retrieval] - - content = _build_pricing_basis_section( - prices, 1, self.styles, self.content_style - ) - - texts = self._texts(content) - self.assertFalse(any("effective as of" in text for text in texts)) - cells = self._table(content) - self.assertEqual(cells[0][-1], "Valid from") - self.assertEqual(cells[-1][-1], "2026-07-14") - - def test_empty_pricing_renders_nothing(self): - self.assertEqual( - _build_pricing_basis_section([], 1, self.styles, self.content_style), [] - ) - - -class GenerateEgressPdfReportTests(unittest.TestCase): - _PROVIDER_DETAILS = { - "tenantId": "tenant-id", - "subscriptionId": "sub-id", - "resourceGroupName": "my-rg", - } - - def _generate(self, payload): - with tempfile.TemporaryDirectory() as tmp_dir: - stage_report_assets(tmp_dir) - json_path = os.path.join(tmp_dir, "egress_inventory_raw_data.json") - with open(json_path, "w", encoding="utf-8") as json_file: - json.dump(payload, json_file) - _seed_egress_inventory( - tmp_dir, - payload["data"]["resources"], - _archive_tiers(payload["data"]["resources"]), - ) - - with patch( - "cloudexit.report.egress.load_data", - side_effect=_fake_load_data, - ): - result = generate_egress_pdf_report( - tmp_dir, json_path, self._PROVIDER_DETAILS - ) - - pdf_size = 0 - if result["success"]: - self.assertEqual( - result["pdf_path"], os.path.join(tmp_dir, "egress.pdf") - ) - pdf_size = os.path.getsize(result["pdf_path"]) - return result, pdf_size - - def test_generates_pdf_document(self): - rows = [ - _row("proddata", "Storage Account", "object", 2048 * GIB), - _row( - "vm-osdisk", - "Managed Disk", - "block", - 512 * GIB, - flags=["allocated (upper bound)"], - ), - _row("vault1", "Recovery Services Vault", "backup", None), - ] - result, pdf_size = self._generate(_payload(rows)) - - self.assertTrue(result["success"], result["logs"]) - self.assertGreater(pdf_size, 0) - - def test_free_tier_footprint_generates_pdf(self): - rows = [_row("sa1", "Storage Account", "object", 5 * GIB)] - result, pdf_size = self._generate(_payload(rows)) - - self.assertTrue(result["success"], result["logs"]) - self.assertGreater(pdf_size, 0) - - def test_missing_json_returns_failure(self): - with tempfile.TemporaryDirectory() as tmp_dir: - result = generate_egress_pdf_report( - tmp_dir, os.path.join(tmp_dir, "missing.json"), {} - ) - - self.assertFalse(result["success"]) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/report/test_egress_report.py b/tests/report/test_egress_report.py new file mode 100644 index 0000000..efd0352 --- /dev/null +++ b/tests/report/test_egress_report.py @@ -0,0 +1,600 @@ +import copy +import json +import re +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from pypdf import PdfReader + +from cloudexit.engine import generate_report, perform_risk_assessment +from cloudexit.report.context import ( + EGRESS_DOCUMENT_CODE, + Provenance, + format_document_id, +) +from cloudexit.report.egress import generate_egress_html_report +from cloudexit.report.egress_report import generate_egress_report +from cloudexit.report.sections import AWS_ICONS_URL, AZURE_ICONS_URL +from cloudexit.report.wording import BLOCKS +from cloudexit.version import __version__ +from tests.report import test_egress as egress_helpers +from tests.report.test_basic_report import ( + COLLECTED, + URL, + html_text, + metadata_texts, + outline_entries, + page_texts, + provenance, + uploaded, +) +from tests.report.test_no_credentials import pricing_for_both_clouds +from tests.support.flow import build_flow_run +from tests.support.report import ( + build_fixture_context, + build_report_fixture, + stage_minimal_assets, +) +from tests.support.report_terms import american_hits, find_terms, retired_term_hits +from tests.support.sync import SYNC_ID + +GIB = egress_helpers.GIB +ES_ID = format_document_id("ES", SYNC_ID) +EDL_ID = format_document_id(EGRESS_DOCUMENT_CODE, SYNC_ID) +OUTLINE = [ + (0, "Introduction"), + (1, "Purpose"), + (1, "Scope"), + (1, "Limitations"), + (1, "Scope of the Estimation"), + (0, "Data Landscape"), + (0, "Estimated Costs"), + (0, "Appendix – Egress Prices"), + (1, "Glossary"), + (0, "Next Steps"), + (0, "Legal Notices"), +] +MULTI_TIB = [ + egress_helpers._row("proddata", "Storage Account", "object", 2048 * GIB), + egress_helpers._row("vm-osdisk", "Managed Disk", "block", 512 * GIB), + egress_helpers._row("vault1", "Recovery Services Vault", "backup", None), +] +FREE_TIER = [egress_helpers._row("sa1", "Storage Account", "object", 20 * GIB)] +WITH_UNKNOWN = [ + egress_helpers._row("sa1", "Storage Account", "object", 2048 * GIB), + egress_helpers._row("db1", "SQL Database", "database", None, size_unknown=True), +] + + +class EgressReportTestCase(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.tmp = Path(tmp.name) + self.count = 0 + + def fixture(self, provider): + fixture = copy.deepcopy(build_report_fixture()) + fixture["metadata"]["cloud_service_provider"] = provider + if provider == 1: + fixture["provider_details"] = { + "tenantId": "11111111-1111-4111-8111-111111111111", + "subscriptionId": "22222222-2222-4222-8222-222222222222", + "resourceGroupName": "Prod-RG", + "clientSecret": "very-secret-client-value", + } + return fixture + + def prepare(self, rows, provider): + self.count += 1 + folder = self.tmp / f"report-{self.count}" + folder.mkdir() + stage_minimal_assets(str(folder)) + payload = egress_helpers._payload(rows) + payload["meta"]["cloud_service_provider"] = provider + json_path = folder / "egress_inventory_raw_data.json" + json_path.write_text(json.dumps(payload), encoding="utf-8") + egress_helpers._seed_egress_inventory( + str(folder), rows, egress_helpers._archive_tiers(rows) + ) + return folder, str(json_path) + + def render(self, rows=MULTI_TIB, provider=1, prov=None): + folder, json_path = self.prepare(rows, provider) + context = build_fixture_context( + self.fixture(provider), folder, provenance=prov or provenance() + ) + with patch( + "cloudexit.report.egress.load_data", side_effect=pricing_for_both_clouds + ): + path = generate_egress_report(context, json_path) + return path, context, PdfReader(path) + + def render_html(self, rows=MULTI_TIB, provider=1, details=None): + folder, json_path = self.prepare(rows, provider) + with patch( + "cloudexit.report.egress.load_data", side_effect=pricing_for_both_clouds + ): + result = generate_egress_html_report( + str(folder), json_path, details, SYNC_ID + ) + self.assertTrue(result["success"], result["logs"]) + html = Path(result["html_path"]).read_text(encoding="utf-8") + return html, html_text(html) + + def scenarios(self): + for provider, name in ((1, "azure"), (2, "aws")): + for state, prov in (("offline", provenance()), ("uploaded", uploaded())): + yield f"{name} {state}", provider, prov + + +class IdentityTests(unittest.TestCase): + def test_a_context_names_a_document_per_code(self): + context = build_fixture_context( + build_report_fixture(), tempfile.gettempdir(), provenance=provenance() + ) + + self.assertEqual(context.document_id, ES_ID) + self.assertEqual(context.document_id_for("EDL"), EDL_ID) + self.assertRegex(EDL_ID, r"^EDL-[0-9a-f]{8}-EN$") + # Both end with the same run, so the pair can be matched by eye + self.assertEqual(ES_ID[3:], EDL_ID[4:]) + self.assertIsNone(context.egress_document_id) + + def test_a_run_without_an_id_is_local(self): + self.assertEqual(format_document_id("EDL", None), "EDL-LOCAL-EN") + + +class CoverAndPageTwoTests(EgressReportTestCase): + def test_the_cover_names_the_document_and_the_edition(self): + for name, provider, prov in self.scenarios(): + with self.subTest(name): + _path, _context, reader = self.render(provider=provider, prov=prov) + cover = page_texts(reader)[0] + + self.assertIn("Egress & Data Landscape", cover) + self.assertIn(EDL_ID, cover) + self.assertIn("EDITION Community", cover) + self.assertNotIn("Classification", cover) + self.assertNotIn("CLASSIFICATION", cover) + everything = " ".join(page_texts(reader)).lower() + self.assertNotIn("confidential", everything) + self.assertNotIn("Page 1 of", cover) + + def test_page_two_is_the_document_information_with_the_companion(self): + for name, provider, prov in self.scenarios(): + with self.subTest(name): + _path, _context, reader = self.render(provider=provider, prov=prov) + pages = page_texts(reader) + + self.assertIn("Document Information", pages[1]) + self.assertIn("Assessment Scope", pages[1]) + self.assertIn("Provenance", pages[1]) + self.assertIn("Document Type Egress & Data Landscape", pages[1]) + self.assertIn(f"Companion Document {ES_ID}", pages[1]) + self.assertIn(EDL_ID, pages[1]) + self.assertIn(f"Page 2 of {len(pages)}", pages[1]) + + def test_an_uploaded_run_shows_the_platform_link_on_page_two(self): + # The PDF is written after the upload, so it knows about it + _path, _context, reader = self.render(prov=uploaded()) + self.assertIn(URL, page_texts(reader)[1]) + self.assertIn("Uploaded to the platform", page_texts(reader)[1]) + + _path, _context, offline = self.render(prov=provenance()) + self.assertNotIn(URL, page_texts(offline)[1]) + self.assertIn("Self-collected data", page_texts(offline)[1]) + + def test_the_pdf_metadata_follows_the_cover(self): + _path, _context, reader = self.render() + info = reader.metadata + + self.assertEqual(info.title, "Egress & Data Landscape – Smoke Test Assessment") + self.assertEqual(info.author, "cloudexit") + self.assertEqual(info.creator, f"cloudexit {__version__}") + self.assertIn(EDL_ID, info.subject) + for keyword in (EDL_ID, SYNC_ID): + self.assertIn(keyword, info["/Keywords"]) + for text in metadata_texts(reader): + self.assertNotIn("EscapeCloud_-_", text) + + +class StructureTests(EgressReportTestCase): + def test_the_outline_is_the_planned_list(self): + for name, provider, prov in self.scenarios(): + with self.subTest(name): + _path, _context, reader = self.render(provider=provider, prov=prov) + self.assertEqual(outline_entries(reader.outline), OUTLINE) + + def test_the_outline_does_not_depend_on_the_data(self): + for rows in (MULTI_TIB, FREE_TIER, WITH_UNKNOWN, []): + with self.subTest(rows=len(rows)): + _path, _context, reader = self.render(rows) + self.assertEqual(outline_entries(reader.outline), OUTLINE) + + def test_the_last_page_is_the_legal_notices_and_the_document_is_short(self): + for name, provider, prov in self.scenarios(): + with self.subTest(name): + _path, _context, reader = self.render(provider=provider, prov=prov) + pages = page_texts(reader) + + self.assertIn("Legal Notices", pages[-1][:200]) + self.assertNotIn("Legal Notices", " ".join(pages[:-1])) + self.assertTrue(5 <= len(pages) <= 7, len(pages)) + + def test_chapters_start_pages(self): + _path, _context, reader = self.render() + pages = page_texts(reader) + + self.assertIn("Introduction", pages[2]) + self.assertIn("Data Landscape", pages[3]) + starts = { + entry.title: reader.get_destination_page_number(entry) + for entry in reader.outline + if not isinstance(entry, list) + } + self.assertEqual(starts["Introduction"], 2) + self.assertEqual(starts["Data Landscape"], 3) + self.assertGreater(starts["Appendix – Egress Prices"], starts["Data Landscape"]) + + def test_every_running_header_and_footer_names_the_document(self): + _path, _context, reader = self.render() + pages = page_texts(reader) + + for index, text in enumerate(pages[1:], start=2): + self.assertIn("Smoke Test Assessment", text) + self.assertIn(EDL_ID, text) + self.assertIn("COMMUNITY EDITION", text) + self.assertIn(f"Page {index} of {len(pages)}", text) + + def test_the_fonts_are_plex_and_nothing_else(self): + path, _context, _reader = self.render() + names = set(re.findall(rb"/BaseFont\s*/(\S+)", Path(path).read_bytes())) + + self.assertTrue(names) + self.assertTrue(all(b"IBMPlex" in name for name in names), names) + + +class ContentTests(EgressReportTestCase): + def text(self, rows, **kwargs): + _path, _context, reader = self.render(rows, **kwargs) + return " ".join(page_texts(reader)) + + def test_the_scope_of_the_estimation_lists_the_providers_types(self): + azure = self.text(MULTI_TIB, provider=1) + aws = self.text(MULTI_TIB, provider=2) + + self.assertIn("Recovery Services Vaults", azure) + self.assertNotIn("S3 Buckets", azure) + self.assertIn("S3 Buckets", aws) + self.assertIn("Aurora is not sized", aws) + self.assertNotIn("Recovery Services Vaults", aws) + self.assertIn("Prod-RG", azure) + + def test_a_multi_tib_footprint_shows_the_cost_table_and_the_total(self): + text = self.text(MULTI_TIB) + + # (2560 - 100) GiB at 0.087 per GiB + self.assertIn("$214.02", text) + self.assertIn("2,560.0 GiB", text) + self.assertEqual(text.count("Share of Volume"), 2) + self.assertIn("Storage Account", text) + self.assertIn("BILLABLE VOLUME", text) + self.assertNotIn("TiB", text) + + def test_a_free_tier_footprint_says_so_and_leaves_out_the_cost_table(self): + text = self.text(FREE_TIER) + + self.assertIn("20.0 GiB of the 100 GiB monthly free tier", text) + self.assertIn("$0.00", text) + # Only the landscape table has the share column + self.assertEqual(text.count("Share of Volume"), 1) + self.assertNotIn("BILLABLE VOLUME", text) + self.assertIn("FREE ALLOWANCE / MONTH", text) + + def test_money_has_one_format_everywhere(self): + for rows in (MULTI_TIB, FREE_TIER, WITH_UNKNOWN): + with self.subTest(rows=len(rows)): + text = self.text(rows) + amounts = re.findall(r"\$[\d,]+(?:\.\d+)?", text) + + self.assertTrue(amounts) + for amount in amounts: + self.assertRegex(amount, r"^\$[\d,]+\.\d\d$") + + def test_unsized_types_are_listed_and_the_totals_carry_a_plus(self): + text = self.text(MULTI_TIB) + self.assertIn("Not sized: Recovery Services Vault (1 resource).", text) + self.assertIn("1 resource could not be sized", text) + + text = self.text(WITH_UNKNOWN) + self.assertIn("Not sized: SQL Database (1 resource).", text) + # (2048 - 100) GiB * 0.087, and the + marks the lower bound + self.assertIn("2,048.0 GiB+", text) + self.assertIn("$169.48+", text) + + def test_an_empty_inventory_renders_the_notice_and_no_tables(self): + text = self.text([]) + + self.assertIn("No data-bearing resources were identified", text) + self.assertIn("Estimated Costs", text) + self.assertEqual(text.count("Share of Volume"), 0) + + def test_the_appendix_names_the_prices_the_sources_and_the_units(self): + text = self.text(MULTI_TIB) + + for expected in ( + "Appendix – Egress Prices", + "first 100 GiB / month", + "0.087 / GiB", + "Prices per GiB as published by Microsoft Azure", + "https://azure.microsoft.com/pricing/details/bandwidth/", + "https://azure.microsoft.com/pricing/details/storage/blobs/", + "Glossary", + "Switching charges", + "Free tier", + ): + self.assertIn(expected, text) + + def test_next_steps_name_the_companion_and_the_platforms(self): + text = self.text(MULTI_TIB, prov=provenance()) + self.assertIn(f"Executive Summary of the same run ({ES_ID})", text) + self.assertIn("Connected (https://exitcloud.io)", text) + self.assertIn("Enterprise (https://escapecloud.io/book-demo/)", text) + + text = self.text(MULTI_TIB, prov=uploaded()) + self.assertIn(f"Open the reports: {URL}", text) + self.assertNotIn("Connected (https://exitcloud.io)", text) + + def test_no_date_is_written_month_first(self): + for name, provider, prov in self.scenarios(): + with self.subTest(name): + _path, _context, reader = self.render(provider=provider, prov=prov) + for text in page_texts(reader): + self.assertIsNone(re.search(r"\b\d{2}-\d{2}-\d{4}\b", text)) + + def test_no_credential_is_shown(self): + _path, _context, reader = self.render(provider=1) + text = " ".join(page_texts(reader) + metadata_texts(reader)) + + self.assertNotIn("very-secret-client-value", text) + self.assertNotIn("Client", text) + + def test_the_figures_do_not_move_with_the_presentation(self): + # The same rows give the same fee as the data functions + folder, json_path = self.prepare(MULTI_TIB, 1) + from cloudexit.report.egress import load_egress_estimate + + with patch( + "cloudexit.report.egress.load_data", side_effect=pricing_for_both_clouds + ): + estimate = load_egress_estimate(str(folder), json_path) + + self.assertAlmostEqual(estimate["total_fee"], 2460 * 0.087) + self.assertEqual(estimate["totals"]["known_size_bytes"], 2560 * GIB) + + +class NoticesTests(EgressReportTestCase): + MARKERS = {1: "Microsoft group", 2: "Amazon.com"} + + def test_the_notices_carry_the_egress_paragraph_and_the_providers_icons(self): + for name, provider, prov in self.scenarios(): + with self.subTest(name): + _path, _context, reader = self.render(provider=provider, prov=prov) + pages = page_texts(reader) + other = 1 if provider == 2 else 2 + + self.assertIn(BLOCKS["notices_egress"], pages[-1]) + self.assertIn("12 January 2027", pages[-1]) + self.assertIn(self.MARKERS[provider], pages[-1]) + self.assertNotIn(self.MARKERS[other], " ".join(pages)) + self.assertIn(BLOCKS["notices_disclaimer"], pages[-1]) + + def test_the_egress_paragraph_comes_before_the_trademarks(self): + _path, _context, reader = self.render() + last = page_texts(reader)[-1] + + self.assertLess(last.index(BLOCKS["notices_egress"]), last.index("Trademarks")) + + def test_the_executive_summary_notices_do_not_carry_it(self): + from cloudexit.report.notices import notices_html + + plain = notices_html(1, COLLECTED, BLOCKS) + egress = notices_html(1, COLLECTED, BLOCKS, egress=True) + + self.assertNotIn([BLOCKS["notices_egress"]], plain["blocks"][0]["paragraphs"]) + self.assertIn([BLOCKS["notices_egress"]], egress["blocks"][0]["paragraphs"]) + self.assertEqual( + egress["blocks"][1:], plain["blocks"][1:], "only the disclaimer grows" + ) + + def test_the_icon_links_follow_the_provider(self): + for provider, own, other in ( + (1, AZURE_ICONS_URL, AWS_ICONS_URL), + (2, AWS_ICONS_URL, AZURE_ICONS_URL), + ): + with self.subTest(provider=provider): + path, _context, _reader = self.render(provider=provider) + from tests.report.test_basic_report import link_targets + + links = link_targets(PdfReader(path)) + self.assertIn(own, links) + self.assertNotIn(other, links) + + +class GuardTests(EgressReportTestCase): + def test_the_term_wording_and_retired_term_guards_pass_on_every_page(self): + for name, provider, prov in self.scenarios(): + for rows in (MULTI_TIB, FREE_TIER, []): + with self.subTest(name, rows=len(rows)): + _path, _context, reader = self.render(rows, provider, prov) + for text in metadata_texts(reader): + self.assertEqual(find_terms(text), []) + for text in page_texts(reader): + self.assertEqual(find_terms(text), []) + self.assertEqual(retired_term_hits(text), []) + self.assertEqual(american_hits(text), []) + + def test_the_html_follows_the_pdf_and_passes_the_guards(self): + for provider, details in ( + (1, {"resourceGroupName": "Prod-RG", "tenantId": "t-1"}), + (2, {"region": "eu-central-1", "accountId": "123456789012"}), + ): + with self.subTest(provider=provider): + html, text = self.render_html(MULTI_TIB, provider, details) + + position = 0 + for heading in ( + "Introduction", + "Purpose", + "Scope of the Estimation", + "Data Landscape", + "Estimated Costs", + "Appendix – Egress Prices", + "Glossary", + "Next Steps", + "Legal Notices", + ): + position = text.index(heading, position) + self.assertIn(EDL_ID, text) + self.assertIn(f"({ES_ID})", text) + self.assertTrue( + text.rstrip().endswith("© 2024–2026 EscapeCloud"), text[-80:] + ) + self.assertEqual(find_terms(text), []) + self.assertEqual(find_terms(html.split("Egress & Data Landscape – Exit Assessment Test", html + ) + self.assertNotIn("EscapeCloud Community Edition -", html.split("")[0]) + self.assertNotIn("TiB", html) + self.assertIn(BLOCKS["notices_egress"], html.replace("'", "'")) + + def test_the_html_scope_names_no_account_or_tenant(self): + _html, text = self.render_html( + MULTI_TIB, 2, {"region": "eu-central-1", "accountId": "123456789012"} + ) + + self.assertIn("AWS region eu-central-1", text) + self.assertNotIn("123456789012", text) + + +class GenerationOrderTests(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.report = build_flow_run( + Path(tmp.name) / "run", + resources=[(1, "eu-central-1", 3)], + costs=[("2026-09-01", 10.0, "USD")], + ) + perform_risk_assessment(1, self.report) + + def generate(self, **kwargs): + return generate_report( + 2, + {"region": "eu-central-1"}, + 1, + 1, + "Flow", + self.report, + str(Path(self.report) / "raw_data"), + sync_id=SYNC_ID, + **kwargs, + ) + + def es_text(self): + reader = PdfReader(str(Path(self.report) / "report.pdf")) + return " ".join(page_texts(reader)) + + def test_the_egress_pdf_is_made_from_the_context_of_the_report(self): + upload = {"status": "uploaded", "url": URL, "assessment_id": "a-1"} + seen = {} + + def fake(context, json_path): + seen["context"] = context + seen["json_path"] = json_path + return "egress.pdf" + + with patch("cloudexit.engine.generate_egress_report", side_effect=fake): + result = self.generate(upload=upload, egress_json_path="estimate.json") + + self.assertTrue(result["success"]) + self.assertEqual(result["reports"]["EGRESS_PDF"], "egress.pdf") + self.assertNotIn("egress_pdf_logs", result) + context = seen["context"] + # The upload is known by the time the PDF is made + self.assertTrue(context.provenance.uploaded) + self.assertEqual(context.provenance.platform_url, URL) + self.assertEqual(context.egress_document_id, EDL_ID) + self.assertEqual(seen["json_path"], "estimate.json") + + def test_the_summary_names_the_egress_document_only_when_there_is_one(self): + with patch( + "cloudexit.engine.generate_egress_report", return_value="egress.pdf" + ): + self.generate(egress_json_path="estimate.json") + with_egress = self.es_text() + html = (Path(self.report) / "index.html").read_text(encoding="utf-8") + + sentence = BLOCKS["es_costs_egress_note"].format(document_id=EDL_ID) + self.assertIn(sentence, with_egress) + self.assertIn("A separate Egress & Data Landscape", html) + self.assertIn(EDL_ID, html) + + self.generate() + self.assertNotIn("A separate Egress", self.es_text()) + html = (Path(self.report) / "index.html").read_text(encoding="utf-8") + self.assertNotIn("A separate Egress", html) + + def test_a_failing_egress_pdf_leaves_the_summary_written(self): + (Path(self.report) / "report.pdf").unlink(missing_ok=True) + with patch( + "cloudexit.engine.generate_egress_report", + side_effect=RuntimeError("reportlab went wrong"), + ): + result = self.generate(egress_json_path="estimate.json") + + self.assertTrue(result["success"]) + self.assertTrue((Path(self.report) / "report.pdf").is_file()) + self.assertTrue((Path(self.report) / "index.html").is_file()) + self.assertNotIn("EGRESS_PDF", result["reports"]) + self.assertEqual(result["egress_pdf_logs"], "reportlab went wrong") + + def test_an_unreadable_estimate_is_the_same_failure(self): + result = self.generate(egress_json_path="/nowhere/estimate.json") + + self.assertTrue(result["success"]) + self.assertIn("egress_pdf_logs", result) + self.assertTrue((Path(self.report) / "report.pdf").is_file()) + + def test_without_an_estimate_nothing_is_added(self): + result = self.generate() + + self.assertTrue(result["success"]) + self.assertNotIn("EGRESS_PDF", result["reports"]) + self.assertNotIn("egress_pdf_logs", result) + + +class ProvenanceDefaultsTests(unittest.TestCase): + def test_the_provenance_helpers_still_name_the_run(self): + prov = Provenance( + collector_version="2.0.0", + collected_at=COLLECTED, + generated_at=COLLECTED, + run_id=SYNC_ID, + ) + + self.assertEqual(prov.run_short_id, SYNC_ID.replace("-", "")[-8:]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/report/test_icon_placeholder.py b/tests/report/test_icon_placeholder.py index 35f6a25..1efc2a0 100644 --- a/tests/report/test_icon_placeholder.py +++ b/tests/report/test_icon_placeholder.py @@ -20,11 +20,9 @@ from cloudexit.report.sections import ALT_BODY from cloudexit.egress.model import GIB from cloudexit.report.writers import generate_html_report -from cloudexit.report.egress import ( - _icon_flowable, - generate_egress_html_report, - generate_egress_pdf_report, -) +from cloudexit.report.egress import generate_egress_html_report +from cloudexit.report.egress_report import generate_egress_report +from cloudexit.report.egress_sections import _type_cell from cloudexit.report.aggregate import enrich_resource_inventory from tests.support.report import ( build_fixture_context, @@ -350,27 +348,35 @@ def run_report(self, generate, icons): self.assertTrue(result["success"], result.get("logs")) return result - def test_the_icon_flowable_is_a_placeholder_without_an_icon_file(self): + def run_pdf(self, icons): + directory, json_path, icons = self.prepare(icons) + fixture = build_report_fixture() + fixture["metadata"]["cloud_service_provider"] = 1 + context = build_fixture_context(fixture, directory) + with ( + patch("cloudexit.report.egress.load_data", side_effect=_fake_load_data), + patch("cloudexit.report.egress._build_icon_lookup", return_value=icons), + ): + return generate_egress_report(context, json_path) + + def test_the_type_cell_is_a_placeholder_without_an_icon_file(self): directory = self.directory() stage_report_assets(str(directory)) + def cell(icon): + return _type_cell({"label": "Managed Disk", "icon": icon}, str(directory)) + + self.assertIsInstance(cell(None), LabelWithPlaceholder) self.assertIsInstance( - _icon_flowable(str(directory), None, "Managed Disk"), IconPlaceholder - ) - self.assertIsInstance( - _icon_flowable(str(directory), "/icons/aws/Compute/NoSuchIcon.png", "X"), - IconPlaceholder, + cell("/icons/aws/Compute/NoSuchIcon.png"), LabelWithPlaceholder ) - image = _icon_flowable(str(directory), EC2_ICON, "Managed Disk") - self.assertNotIsInstance(image, IconPlaceholder) + self.assertNotIsInstance(cell(EC2_ICON), LabelWithPlaceholder) def test_the_pdf_shows_a_placeholder_for_a_type_without_an_icon(self): icons = {"microsoft.storage/storageaccounts": EC2_ICON} - result = self.run_report( - lambda d, j: generate_egress_pdf_report(d, j, {}), icons - ) - text = pdf_text(result["pdf_path"]) + text = pdf_text(self.run_pdf(icons)) + self.assertRegex(text, r"MD") self.assertIn("Managed Disk", text) @@ -380,15 +386,9 @@ def test_the_pdf_has_one_image_less_when_a_type_loses_its_icon(self): "some/type": LAMBDA_ICON, } one = {"microsoft.storage/storageaccounts": EC2_ICON} - with_both = self.run_report( - lambda d, j: generate_egress_pdf_report(d, j, {}), both - ) - count_both = image_count(with_both["pdf_path"]) - with_one = self.run_report( - lambda d, j: generate_egress_pdf_report(d, j, {}), one - ) + count_both = image_count(self.run_pdf(both)) - self.assertEqual(image_count(with_one["pdf_path"]), count_both - 1) + self.assertEqual(image_count(self.run_pdf(one)), count_both - 1) def test_the_html_shows_a_placeholder_and_keeps_the_images(self): icons = {"microsoft.storage/storageaccounts": EC2_ICON} diff --git a/tests/report/test_no_credentials.py b/tests/report/test_no_credentials.py index 38b4fa1..5edd280 100644 --- a/tests/report/test_no_credentials.py +++ b/tests/report/test_no_credentials.py @@ -202,7 +202,7 @@ def test_the_egress_pdf_shows_the_scope_without_the_credentials(self): report = self.run_with_egress(1, AZURE_DETAILS) text = folder_text(report)["egress.pdf"] - self.assertIn("Scope of Assessment", text) + self.assertIn("Assessment Scope", text) self.assertIn("Subscription ID", text) self.assertIn(AZURE_DETAILS["subscriptionId"], text) self.assertNotIn("Client", text) diff --git a/tests/report/test_pdf_sections.py b/tests/report/test_pdf_sections.py deleted file mode 100644 index 08aef73..0000000 --- a/tests/report/test_pdf_sections.py +++ /dev/null @@ -1,276 +0,0 @@ -import unittest - -from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle -from reportlab.lib.colors import HexColor -from reportlab.platypus import Paragraph, Table, TableStyle - -from cloudexit.report.egress import ( - build_scope_section, - build_summary_section, - default_table_style, -) -from cloudexit.report.context import scope_facts -from cloudexit.report.wording import BLOCKS -from tests.support.credentials import AWS_DETAILS, AZURE_DETAILS, leaked_fragments -from tests.support.report import ( - build_report_fixture, -) - - -def _make_styles(): - styles = getSampleStyleSheet() - content_style = ParagraphStyle( - "ContentStyle", fontSize=10, leading=12, spaceAfter=10 - ) - styles["Heading1"].leading = 1.5 * styles["Heading1"].fontSize - styles["Heading1"].textColor = HexColor("#112726") - styles["Heading2"].leading = 1.5 * styles["Heading2"].fontSize - styles["Heading2"].textColor = HexColor("#112726") - return styles, content_style - - -class DefaultTableStyleTests(unittest.TestCase): - def test_returns_table_style_instance(self): - style = default_table_style() - self.assertIsInstance(style, TableStyle) - - def test_style_has_expected_commands(self): - style = default_table_style() - commands = style.getCommands() - command_names = [cmd[0] for cmd in commands] - self.assertIn("BACKGROUND", command_names) - self.assertIn("TEXTCOLOR", command_names) - self.assertIn("GRID", command_names) - self.assertIn("FONTNAME", command_names) - - -class BuildSummarySectionTests(unittest.TestCase): - def setUp(self): - self.styles, self.content_style = _make_styles() - self.fixture = build_report_fixture() - - def test_returns_non_empty_list(self): - content = build_summary_section( - self.fixture["metadata"], self.styles, self.content_style - ) - self.assertIsInstance(content, list) - self.assertGreater(len(content), 0) - - def test_contains_heading_paragraph(self): - content = build_summary_section( - self.fixture["metadata"], self.styles, self.content_style - ) - paragraphs = [item for item in content if isinstance(item, Paragraph)] - heading_texts = [p.text for p in paragraphs] - self.assertIn("Summary", heading_texts) - - def test_contains_table_with_metadata(self): - content = build_summary_section( - self.fixture["metadata"], self.styles, self.content_style - ) - tables = [item for item in content if isinstance(item, Table)] - self.assertEqual(len(tables), 1) - - def test_azure_provider_maps_correctly(self): - metadata = {**self.fixture["metadata"], "cloud_service_provider": 1} - content = build_summary_section(metadata, self.styles, self.content_style) - tables = [item for item in content if isinstance(item, Table)] - self.assertEqual(len(tables), 1) - - -class BuildScopeSectionTests(unittest.TestCase): - def setUp(self): - self.styles, self.content_style = _make_styles() - self.fixture = build_report_fixture() - self.aws = {**self.fixture["metadata"], "cloud_service_provider": 2} - self.azure = {**self.fixture["metadata"], "cloud_service_provider": 1} - - def rows(self, metadata, provider_details): - content = build_scope_section( - metadata, provider_details, self.styles, self.content_style - ) - tables = [item for item in content if isinstance(item, Table)] - self.assertEqual(len(tables), 1) - return [list(row) for row in tables[0]._cellvalues[1:]] - - def test_aws_scope_is_the_account_and_the_region(self): - rows = self.rows(self.aws, AWS_DETAILS) - - self.assertEqual( - rows, [["Account ID", "123456789012"], ["Region", "eu-central-1"]] - ) - - def test_aws_scope_leaves_out_an_account_that_is_not_known(self): - details = {k: v for k, v in AWS_DETAILS.items() if k != "accountId"} - - self.assertEqual(self.rows(self.aws, details), [["Region", "eu-central-1"]]) - - def test_a_missing_value_is_a_dash_not_na(self): - self.assertEqual(self.rows(self.aws, {}), [["Region", "–"]]) - self.assertEqual( - self.rows(self.azure, {}), - [["Tenant ID", "–"], ["Subscription ID", "–"], ["Resource Group", "–"]], - ) - - def test_azure_scope_is_the_tenant_the_subscription_and_the_resource_group(self): - rows = self.rows(self.azure, AZURE_DETAILS) - - self.assertEqual( - rows, - [ - ["Tenant ID", AZURE_DETAILS["tenantId"]], - ["Subscription ID", AZURE_DETAILS["subscriptionId"]], - ["Resource Group", "rg-leakcheck"], - ], - ) - - def test_no_row_names_or_shows_a_credential(self): - for metadata, details in ((self.aws, AWS_DETAILS), (self.azure, AZURE_DETAILS)): - with self.subTest(provider=metadata["cloud_service_provider"]): - rows = self.rows(metadata, details) - for label, value in rows: - for word in ("Key", "Secret", "Client", "Token"): - self.assertNotIn(word, label) - text = " ".join(map(str, sum(rows, []))) - for name, fragment in leaked_fragments(details).items(): - self.assertNotIn(fragment, text, name) - - def test_the_rows_agree_with_the_scope_on_page_two(self): - # Both documents take the facts from scope_facts and name them the same way - self.assertEqual( - [row[0] for row in self.rows(self.azure, AZURE_DETAILS)], - [ - BLOCKS["ev_tenant_id"], - BLOCKS["ev_subscription_id"], - BLOCKS["ev_resource_group"], - ], - ) - self.assertEqual( - [row[0] for row in self.rows(self.aws, AWS_DETAILS)], - [BLOCKS["ev_account_id"], BLOCKS["ev_region"]], - ) - facts = dict(scope_facts(AWS_DETAILS, 2, None).rows) - self.assertEqual(self.rows(self.aws, AWS_DETAILS)[0][1], facts["account"]) - - def test_unknown_provider_returns_na(self): - metadata = {**self.fixture["metadata"], "cloud_service_provider": 99} - - self.assertEqual(self.rows(metadata, {}), [["N/A", "N/A"]]) - - -class BuildTfstateScopeSectionTests(unittest.TestCase): - _SHA = "a3f1" + "b" * 56 + "9c2e" - _LINEAGE = "121f3456-7d0f-a7be-ceb0-c91d44c6a689" - - def setUp(self): - self.styles, self.content_style = _make_styles() - self.fixture = build_report_fixture() - - def _rows(self, metadata, provider_details, tfstate_scope=None): - content = build_scope_section( - metadata, - provider_details, - self.styles, - self.content_style, - tfstate_scope, - ) - tables = [item for item in content if isinstance(item, Table)] - self.assertEqual(len(tables), 1) - return {row[0]: row[1] for row in tables[0]._cellvalues[1:]} - - def _aws_scope(self, **overrides): - scope = { - "file": "infra.tfstate", - "sha256": self._SHA, - "lineage": self._LINEAGE, - "serial": 7, - "locations": ["eu-central-1"], - "subscriptions": [], - "resource_groups": [], - } - scope.update(overrides) - return scope - - def test_aws_renders_file_state_and_regions(self): - rows = self._rows( - self.fixture["metadata"], - {"tfstatePath": "/tmp/infra.tfstate"}, - self._aws_scope(), - ) - - self.assertEqual(list(rows), ["File", "State", "Region(s)"]) - self.assertEqual(rows["File"], "infra.tfstate (SHA-256 a3f1…9c2e)") - self.assertEqual(rows["State"], f"{self._LINEAGE} (Serial: 7)") - self.assertEqual(rows["Region(s)"], "eu-central-1") - - def test_azure_renders_subscription_and_resource_groups(self): - metadata = {**self.fixture["metadata"], "cloud_service_provider": 1} - scope = self._aws_scope( - subscriptions=["0299bf7a-8ca8-479b-8659-c62e62cd7bae"], - resource_groups=["escape-test-rg-1", "escape-test-rg-2"], - ) - - rows = self._rows(metadata, {"tfstatePath": "/tmp/infra.tfstate"}, scope) - - self.assertEqual( - list(rows), ["File", "State", "Subscription", "Resource Group(s)"] - ) - self.assertEqual(rows["Subscription"], "0299bf7a-8ca8-479b-8659-c62e62cd7bae") - self.assertEqual( - rows["Resource Group(s)"], "escape-test-rg-1, escape-test-rg-2" - ) - # Azure deliberately mirrors the live table, which shows no location. - self.assertNotIn("Location(s)", rows) - self.assertNotIn("Region(s)", rows) - - def test_many_values_are_capped_with_a_remainder(self): - metadata = {**self.fixture["metadata"], "cloud_service_provider": 1} - scope = self._aws_scope( - subscriptions=["sub-1"], - resource_groups=[f"rg-{i}" for i in range(6)], - ) - - rows = self._rows(metadata, {"tfstatePath": "/tmp/infra.tfstate"}, scope) - - self.assertEqual(rows["Resource Group(s)"], "rg-0, rg-1, rg-2, +3 more") - - def test_missing_values_report_not_determinable(self): - rows = self._rows( - self.fixture["metadata"], - {"tfstatePath": "/tmp/infra.tfstate"}, - self._aws_scope(locations=[]), - ) - - self.assertEqual(rows["Region(s)"], "Not determinable from state") - - def test_state_row_omitted_when_lineage_and_serial_absent(self): - rows = self._rows( - self.fixture["metadata"], - {"tfstatePath": "/tmp/infra.tfstate"}, - self._aws_scope(lineage=None, serial=None), - ) - - self.assertEqual(list(rows), ["File", "Region(s)"]) - self.assertEqual(rows["File"], "infra.tfstate (SHA-256 a3f1…9c2e)") - - def test_missing_manifest_falls_back_to_file_name_only(self): - rows = self._rows( - self.fixture["metadata"], {"tfstatePath": "/tmp/infra.tfstate"}, None - ) - - self.assertEqual(rows["File"], "infra.tfstate") - self.assertEqual(rows["Region(s)"], "Not determinable from state") - # Never the credential rows, which would all be N/A here. - self.assertNotIn("Access Key", rows) - - def test_live_mode_table_has_no_credential_rows(self): - rows = self._rows( - self.fixture["metadata"], self.fixture["provider_details"], None - ) - - self.assertEqual(list(rows), ["Region"]) - self.assertEqual(rows["Region"], "eu-central-1") - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/report/test_report_flow.py b/tests/report/test_report_flow.py index 599b01a..2361328 100644 --- a/tests/report/test_report_flow.py +++ b/tests/report/test_report_flow.py @@ -426,9 +426,10 @@ def run_assessment( "cloudexit.cli.app.generate_egress_html_report", return_value={"success": True, "html_path": "h"}, ), + # The PDF is written by generate_report, from the estimate file patch( - "cloudexit.cli.app.generate_egress_pdf_report", - return_value={"success": True, "pdf_path": "p"}, + "cloudexit.engine.generate_egress_report", + return_value="egress.pdf", ), ] for item in patches: @@ -740,10 +741,7 @@ def traced_risk(*args, **kwargs): "cloudexit.cli.app.generate_egress_html_report", return_value={"success": True}, ), - patch( - "cloudexit.cli.app.generate_egress_pdf_report", - return_value={"success": True}, - ), + patch("cloudexit.engine.generate_egress_report", return_value="egress.pdf"), ): self.run_assessment( report, From 93359c87510ce3bde58360797530f4fecde05fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Sun, 25 Oct 2026 09:19:51 +0100 Subject: [PATCH 52/61] add a sample report matrix rendered through the real pipeline --- .gitignore | 4 + tests/fixtures/samples/aws_no_egress.json | 97 ++++ tests/fixtures/samples/aws_typical.json | 157 ++++++ tests/fixtures/samples/azure_typical.json | 133 +++++ tests/fixtures/samples/azure_uploaded.json | 138 +++++ tests/fixtures/samples/egress_free_tier.json | 66 +++ tests/fixtures/samples/egress_multi_tib.json | 80 +++ tests/fixtures/samples/empty.json | 26 + tests/fixtures/samples/no_risks.json | 47 ++ tests/fixtures/samples/stress.json | 211 ++++++++ .../samples/tfstate/sample_aws.tfstate | 104 ++++ .../samples/tfstate/sample_azure.tfstate | 76 +++ tests/fixtures/samples/tfstate_aws.json | 15 + tests/fixtures/samples/tfstate_azure.json | 15 + tests/fixtures/samples/uploaded_no_link.json | 46 ++ tests/report/test_basic_report.py | 27 +- tests/report/test_egress_report.py | 2 +- tests/samples/__init__.py | 0 tests/samples/__main__.py | 178 +++++++ tests/samples/catalogue.py | 222 ++++++++ tests/samples/render.py | 293 +++++++++++ tests/samples/scenarios.py | 290 +++++++++++ tests/samples/test_samples.py | 484 ++++++++++++++++++ tests/samples/test_scenarios.py | 245 +++++++++ tests/support/html.py | 58 +++ 25 files changed, 2987 insertions(+), 27 deletions(-) create mode 100644 tests/fixtures/samples/aws_no_egress.json create mode 100644 tests/fixtures/samples/aws_typical.json create mode 100644 tests/fixtures/samples/azure_typical.json create mode 100644 tests/fixtures/samples/azure_uploaded.json create mode 100644 tests/fixtures/samples/egress_free_tier.json create mode 100644 tests/fixtures/samples/egress_multi_tib.json create mode 100644 tests/fixtures/samples/empty.json create mode 100644 tests/fixtures/samples/no_risks.json create mode 100644 tests/fixtures/samples/stress.json create mode 100644 tests/fixtures/samples/tfstate/sample_aws.tfstate create mode 100644 tests/fixtures/samples/tfstate/sample_azure.tfstate create mode 100644 tests/fixtures/samples/tfstate_aws.json create mode 100644 tests/fixtures/samples/tfstate_azure.json create mode 100644 tests/fixtures/samples/uploaded_no_link.json create mode 100644 tests/samples/__init__.py create mode 100644 tests/samples/__main__.py create mode 100644 tests/samples/catalogue.py create mode 100644 tests/samples/render.py create mode 100644 tests/samples/scenarios.py create mode 100644 tests/samples/test_samples.py create mode 100644 tests/samples/test_scenarios.py create mode 100644 tests/support/html.py diff --git a/.gitignore b/.gitignore index b1aa32b..bca627c 100644 --- a/.gitignore +++ b/.gitignore @@ -100,6 +100,9 @@ web_modules/ # Generated reports reports/ +# Rendered sample reports (python -m tests.samples) +/samples/ + # Local configs (only the example templates are tracked) config/* !config/*_example.json @@ -107,6 +110,7 @@ config/* # Terraform state and downloaded datasets *.tfstate !tests/fixtures/smoke/aws.tfstate +!tests/fixtures/samples/tfstate/*.tfstate datasets/ # TypeScript cache diff --git a/tests/fixtures/samples/aws_no_egress.json b/tests/fixtures/samples/aws_no_egress.json new file mode 100644 index 0000000..1d056cb --- /dev/null +++ b/tests/fixtures/samples/aws_no_egress.json @@ -0,0 +1,97 @@ +{ + "name": "Sample – Internal Billing Platform", + "purpose": "The same estate without egress: no egress document, no egress sentence.", + "provider": "aws", + "exit_strategy": 3, + "scope": { + "accountId": "123456789012", + "region": "eu-central-1" + }, + "resources": [ + { + "type": "AWS.ec2.describe_instances.Reservations", + "region": "eu-central-1", + "count": 14 + }, + { + "type": "AWS.ec2.describe_instances.Reservations", + "region": "eu-west-1", + "count": 6 + }, + { + "type": "AWS.s3.list_buckets.Buckets", + "region": "eu-central-1", + "count": 9 + }, + { + "type": "AWS.s3.list_buckets.Buckets", + "region": "eu-west-1", + "count": 3 + }, + { + "type": "AWS.ec2.describe_volumes.Volumes", + "region": "eu-central-1", + "count": 22 + }, + { + "type": "AWS.ec2.describe_volumes.Volumes", + "region": "eu-west-1", + "count": 8 + }, + { + "type": "AWS.rds.describe_db_instances.DBInstances", + "region": "eu-central-1", + "count": 3 + }, + { + "type": "AWS.rds.describe_db_instances.DBInstances", + "region": "eu-west-1", + "count": 1 + }, + { + "type": "AWS.dynamodb.list_tables.TableNames", + "region": "eu-central-1", + "count": 5 + }, + { + "type": "AWS.lambda.list_functions.Functions", + "region": "eu-central-1", + "count": 18 + }, + { + "type": "AWS.lambda.list_functions.Functions", + "region": "eu-west-1", + "count": 6 + }, + { + "type": "AWS.eks.list_clusters.clusters", + "region": "eu-central-1", + "count": 2 + }, + { + "type": "AWS.sqs.list_queues.QueueUrls", + "region": "eu-central-1", + "count": 7 + }, + { + "type": "AWS.sns.list_topics.Topics", + "region": "eu-central-1", + "count": 4 + } + ], + "costs": { + "currency": "USD", + "months": { + "2026-06": 8420.35, + "2026-07": 8791.1, + "2026-08": 9054.8, + "2026-09": 8632.45, + "2026-10": 9310.2, + "2026-11": 4120.75 + } + }, + "egress": null, + "upload": null, + "clock": "2026-11-12T09:30:00Z", + "run_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0002" +} diff --git a/tests/fixtures/samples/aws_typical.json b/tests/fixtures/samples/aws_typical.json new file mode 100644 index 0000000..74ac827 --- /dev/null +++ b/tests/fixtures/samples/aws_typical.json @@ -0,0 +1,157 @@ +{ + "name": "Sample – Customer Self-Service Portal", + "purpose": "Several risk severities, USD costs with a provisional month, egress with archive tiers.", + "provider": "aws", + "exit_strategy": 3, + "scope": { + "accountId": "123456789012", + "region": "eu-central-1" + }, + "resources": [ + { + "type": "AWS.ec2.describe_instances.Reservations", + "region": "eu-central-1", + "count": 14 + }, + { + "type": "AWS.ec2.describe_instances.Reservations", + "region": "eu-west-1", + "count": 6 + }, + { + "type": "AWS.s3.list_buckets.Buckets", + "region": "eu-central-1", + "count": 9 + }, + { + "type": "AWS.s3.list_buckets.Buckets", + "region": "eu-west-1", + "count": 3 + }, + { + "type": "AWS.ec2.describe_volumes.Volumes", + "region": "eu-central-1", + "count": 22 + }, + { + "type": "AWS.ec2.describe_volumes.Volumes", + "region": "eu-west-1", + "count": 8 + }, + { + "type": "AWS.rds.describe_db_instances.DBInstances", + "region": "eu-central-1", + "count": 3 + }, + { + "type": "AWS.rds.describe_db_instances.DBInstances", + "region": "eu-west-1", + "count": 1 + }, + { + "type": "AWS.dynamodb.list_tables.TableNames", + "region": "eu-central-1", + "count": 5 + }, + { + "type": "AWS.lambda.list_functions.Functions", + "region": "eu-central-1", + "count": 18 + }, + { + "type": "AWS.lambda.list_functions.Functions", + "region": "eu-west-1", + "count": 6 + }, + { + "type": "AWS.eks.list_clusters.clusters", + "region": "eu-central-1", + "count": 2 + }, + { + "type": "AWS.sqs.list_queues.QueueUrls", + "region": "eu-central-1", + "count": 7 + }, + { + "type": "AWS.sns.list_topics.Topics", + "region": "eu-central-1", + "count": 4 + } + ], + "costs": { + "currency": "USD", + "months": { + "2026-06": 8420.35, + "2026-07": 8791.1, + "2026-08": 9054.8, + "2026-09": 8632.45, + "2026-10": 9310.2, + "2026-11": 4120.75 + } + }, + "egress": { + "archive_tiers": [ + "Archive", + "Glacier", + "Deep Archive" + ], + "resources": [ + { + "type": "AWS.s3.list_buckets.Buckets", + "name": "sample-portal-assets", + "size_gib": 1800, + "tiers": { + "Standard": 1200, + "Standard-IA": 300, + "Glacier": 300 + } + }, + { + "type": "AWS.s3.list_buckets.Buckets", + "name": "sample-portal-archive", + "size_gib": 5200, + "tiers": { + "Standard": 400, + "Glacier": 3800, + "Deep Archive": 1000 + } + }, + { + "type": "AWS.ec2.describe_volumes.Volumes", + "name": "vol-0a1b2c3d4e5f60789", + "size_gib": 500, + "flags": [ + "allocated (upper bound)" + ] + }, + { + "type": "AWS.ec2.describe_volumes.Volumes", + "name": "vol-0123456789abcdef0", + "size_gib": 250, + "flags": [ + "allocated (upper bound)" + ] + }, + { + "type": "AWS.rds.describe_db_instances.DBInstances", + "name": "sample-portal-db", + "size_gib": 320, + "flags": [ + "allocated (upper bound)" + ], + "notes": [ + "allocated: 320.0 GiB" + ] + }, + { + "type": "AWS.dynamodb.list_tables.TableNames", + "name": "sample-sessions", + "size_gib": 12 + } + ] + }, + "upload": null, + "clock": "2026-11-12T09:30:00Z", + "run_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0001" +} diff --git a/tests/fixtures/samples/azure_typical.json b/tests/fixtures/samples/azure_typical.json new file mode 100644 index 0000000..45d861f --- /dev/null +++ b/tests/fixtures/samples/azure_typical.json @@ -0,0 +1,133 @@ +{ + "name": "Sample – Document Archive", + "purpose": "EUR costs, a type without alternatives, egress with unsized types so the totals carry a +.", + "provider": "azure", + "exit_strategy": 1, + "scope": { + "tenantId": "00000000-0000-4000-8000-0000000000a1", + "subscriptionId": "00000000-0000-4000-8000-0000000000a2", + "resourceGroupName": "rg-sample-archive" + }, + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines", + "region": "westeurope", + "count": 12 + }, + { + "type": "Microsoft.Compute/virtualMachines", + "region": "northeurope", + "count": 4 + }, + { + "type": "Microsoft.ContainerService/managedClusters", + "region": "westeurope", + "count": 2 + }, + { + "type": "Microsoft.Storage/storageAccounts", + "region": "westeurope", + "count": 6 + }, + { + "type": "Microsoft.Storage/storageAccounts", + "region": "northeurope", + "count": 2 + }, + { + "type": "Microsoft.Sql/servers/databases", + "region": "westeurope", + "count": 5 + }, + { + "type": "Microsoft.DocumentDB/databaseAccounts", + "region": "westeurope", + "count": 1 + }, + { + "type": "Microsoft.DBforPostgreSQL/flexibleServers", + "region": "northeurope", + "count": 2 + }, + { + "type": "Microsoft.RecoveryServices/vaults", + "region": "westeurope", + "count": 1 + }, + { + "type": "Microsoft.ContainerRegistry/registries", + "region": "westeurope", + "count": 1 + } + ], + "costs": { + "currency": "EUR", + "months": { + "2026-06": 5120.4, + "2026-07": 5388.15, + "2026-08": 5290.0, + "2026-09": 5602.75, + "2026-10": 5711.3, + "2026-11": 2204.9 + } + }, + "egress": { + "archive_tiers": [ + "Archive" + ], + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "sampledocsprod", + "size_gib": 2048, + "tiers": { + "Hot": 1500, + "Cool": 300, + "Archive": 248 + } + }, + { + "type": "Microsoft.Storage/storageAccounts", + "name": "sampledocsbackup", + "size_gib": 900, + "tiers": { + "Cool": 400, + "Archive": 500 + } + }, + { + "type": "Microsoft.Compute/disks", + "name": "sample-docs-vm-osdisk", + "size_gib": 256, + "flags": [ + "allocated (upper bound)" + ] + }, + { + "type": "Microsoft.Sql/servers/databases", + "name": "sample-docs-metadata", + "size_gib": 140 + }, + { + "type": "Microsoft.Sql/servers/databases", + "name": "sample-docs-reporting", + "size_gib": null, + "unknown": true + }, + { + "type": "Microsoft.DocumentDB/databaseAccounts", + "name": "sample-docs-search", + "size_gib": null, + "unknown": true + }, + { + "type": "Microsoft.RecoveryServices/vaults", + "name": "sample-docs-vault", + "size_gib": null + } + ] + }, + "upload": null, + "clock": "2026-11-12T09:30:00Z", + "run_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0003" +} diff --git a/tests/fixtures/samples/azure_uploaded.json b/tests/fixtures/samples/azure_uploaded.json new file mode 100644 index 0000000..6d65415 --- /dev/null +++ b/tests/fixtures/samples/azure_uploaded.json @@ -0,0 +1,138 @@ +{ + "name": "Sample – Document Archive", + "purpose": "The same run, uploaded with a link: the uploaded closing page and the link on page 2 of both documents.", + "provider": "azure", + "exit_strategy": 1, + "scope": { + "tenantId": "00000000-0000-4000-8000-0000000000a1", + "subscriptionId": "00000000-0000-4000-8000-0000000000a2", + "resourceGroupName": "rg-sample-archive" + }, + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines", + "region": "westeurope", + "count": 12 + }, + { + "type": "Microsoft.Compute/virtualMachines", + "region": "northeurope", + "count": 4 + }, + { + "type": "Microsoft.ContainerService/managedClusters", + "region": "westeurope", + "count": 2 + }, + { + "type": "Microsoft.Storage/storageAccounts", + "region": "westeurope", + "count": 6 + }, + { + "type": "Microsoft.Storage/storageAccounts", + "region": "northeurope", + "count": 2 + }, + { + "type": "Microsoft.Sql/servers/databases", + "region": "westeurope", + "count": 5 + }, + { + "type": "Microsoft.DocumentDB/databaseAccounts", + "region": "westeurope", + "count": 1 + }, + { + "type": "Microsoft.DBforPostgreSQL/flexibleServers", + "region": "northeurope", + "count": 2 + }, + { + "type": "Microsoft.RecoveryServices/vaults", + "region": "westeurope", + "count": 1 + }, + { + "type": "Microsoft.ContainerRegistry/registries", + "region": "westeurope", + "count": 1 + } + ], + "costs": { + "currency": "EUR", + "months": { + "2026-06": 5120.4, + "2026-07": 5388.15, + "2026-08": 5290.0, + "2026-09": 5602.75, + "2026-10": 5711.3, + "2026-11": 2204.9 + } + }, + "egress": { + "archive_tiers": [ + "Archive" + ], + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "sampledocsprod", + "size_gib": 2048, + "tiers": { + "Hot": 1500, + "Cool": 300, + "Archive": 248 + } + }, + { + "type": "Microsoft.Storage/storageAccounts", + "name": "sampledocsbackup", + "size_gib": 900, + "tiers": { + "Cool": 400, + "Archive": 500 + } + }, + { + "type": "Microsoft.Compute/disks", + "name": "sample-docs-vm-osdisk", + "size_gib": 256, + "flags": [ + "allocated (upper bound)" + ] + }, + { + "type": "Microsoft.Sql/servers/databases", + "name": "sample-docs-metadata", + "size_gib": 140 + }, + { + "type": "Microsoft.Sql/servers/databases", + "name": "sample-docs-reporting", + "size_gib": null, + "unknown": true + }, + { + "type": "Microsoft.DocumentDB/databaseAccounts", + "name": "sample-docs-search", + "size_gib": null, + "unknown": true + }, + { + "type": "Microsoft.RecoveryServices/vaults", + "name": "sample-docs-vault", + "size_gib": null + } + ] + }, + "upload": { + "status": "uploaded", + "url": "https://platform.example.test/assessments/00000000-0000-4000-8000-0000000000b1", + "assessment_id": "00000000-0000-4000-8000-0000000000b1", + "sync_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0004" + }, + "clock": "2026-11-12T09:30:00Z", + "run_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0004" +} diff --git a/tests/fixtures/samples/egress_free_tier.json b/tests/fixtures/samples/egress_free_tier.json new file mode 100644 index 0000000..ede6e4a --- /dev/null +++ b/tests/fixtures/samples/egress_free_tier.json @@ -0,0 +1,66 @@ +{ + "name": "Sample – Small Data Platform", + "purpose": "Egress below the free tier: a fee of $0.00 and the headroom sentence.", + "provider": "aws", + "exit_strategy": 3, + "scope": { + "accountId": "123456789012", + "region": "eu-central-1" + }, + "resources": [ + { + "type": "AWS.s3.list_buckets.Buckets", + "region": "eu-central-1", + "count": 3 + }, + { + "type": "AWS.ec2.describe_instances.Reservations", + "region": "eu-central-1", + "count": 2 + }, + { + "type": "AWS.rds.describe_db_instances.DBInstances", + "region": "eu-central-1", + "count": 1 + } + ], + "costs": { + "currency": "USD", + "months": { + "2026-06": 220.45, + "2026-07": 231.1, + "2026-08": 228.6, + "2026-09": 240.05, + "2026-10": 236.9, + "2026-11": 98.4 + } + }, + "egress": { + "archive_tiers": [ + "Archive", + "Glacier", + "Deep Archive" + ], + "resources": [ + { + "type": "AWS.s3.list_buckets.Buckets", + "name": "sample-data-landing", + "size_gib": 14, + "tiers": { + "Standard": 14 + } + }, + { + "type": "AWS.rds.describe_db_instances.DBInstances", + "name": "sample-data-db", + "size_gib": 6, + "flags": [ + "allocated (upper bound)" + ] + } + ] + }, + "upload": null, + "clock": "2026-11-12T09:30:00Z", + "run_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0010" +} diff --git a/tests/fixtures/samples/egress_multi_tib.json b/tests/fixtures/samples/egress_multi_tib.json new file mode 100644 index 0000000..a539a66 --- /dev/null +++ b/tests/fixtures/samples/egress_multi_tib.json @@ -0,0 +1,80 @@ +{ + "name": "Sample – Media Library", + "purpose": "Egress of tens of TiB: several price tiers and a large fee.", + "provider": "azure", + "exit_strategy": 3, + "scope": { + "tenantId": "00000000-0000-4000-8000-0000000000a1", + "subscriptionId": "00000000-0000-4000-8000-0000000000a2", + "resourceGroupName": "rg-sample-media" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "region": "westeurope", + "count": 14 + }, + { + "type": "Microsoft.Compute/virtualMachines", + "region": "westeurope", + "count": 6 + }, + { + "type": "Microsoft.Sql/servers/databases", + "region": "westeurope", + "count": 2 + } + ], + "costs": { + "currency": "EUR", + "months": { + "2026-06": 18200.0, + "2026-07": 18950.5, + "2026-08": 19120.75, + "2026-09": 19810.2, + "2026-10": 20344.0, + "2026-11": 8120.6 + } + }, + "egress": { + "archive_tiers": [ + "Archive" + ], + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "samplemediaoriginals", + "size_gib": 36864, + "tiers": { + "Hot": 12000, + "Cool": 14864, + "Archive": 10000 + } + }, + { + "type": "Microsoft.Storage/storageAccounts", + "name": "samplemediaproxies", + "size_gib": 12288, + "tiers": { + "Hot": 12288 + } + }, + { + "type": "Microsoft.Compute/disks", + "name": "sample-media-render-disk", + "size_gib": 8192, + "flags": [ + "allocated (upper bound)" + ] + }, + { + "type": "Microsoft.Sql/servers/databases", + "name": "sample-media-metadata", + "size_gib": 1024 + } + ] + }, + "upload": null, + "clock": "2026-11-12T09:30:00Z", + "run_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0011" +} diff --git a/tests/fixtures/samples/empty.json b/tests/fixtures/samples/empty.json new file mode 100644 index 0000000..761edce --- /dev/null +++ b/tests/fixtures/samples/empty.json @@ -0,0 +1,26 @@ +{ + "name": "Sample – Empty Scope", + "purpose": "No resources and no costs.", + "provider": "aws", + "exit_strategy": 1, + "scope": { + "accountId": "123456789012", + "region": "eu-central-1" + }, + "resources": [], + "costs": { + "currency": "USD", + "months": { + "2026-06": 0, + "2026-07": 0, + "2026-08": 0, + "2026-09": 0, + "2026-10": 0, + "2026-11": 0 + } + }, + "egress": null, + "upload": null, + "clock": "2026-11-12T09:30:00Z", + "run_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0009" +} diff --git a/tests/fixtures/samples/no_risks.json b/tests/fixtures/samples/no_risks.json new file mode 100644 index 0000000..5a7feae --- /dev/null +++ b/tests/fixtures/samples/no_risks.json @@ -0,0 +1,47 @@ +{ + "name": "Sample – Static Website", + "purpose": "Every type has enough supported alternatives, so the Risk Inventory is empty.", + "provider": "aws", + "exit_strategy": 2, + "scope": { + "accountId": "123456789012", + "region": "eu-central-1" + }, + "resources": [ + { + "type": "AWS.apigateway.get_rest_apis.items", + "region": "eu-central-1", + "count": 2 + }, + { + "type": "AWS.acm.list_certificates.CertificateSummaryList", + "region": "eu-central-1", + "count": 3 + }, + { + "type": "AWS.cloudwatch.describe_alarms.MetricAlarms", + "region": "eu-central-1", + "count": 6 + }, + { + "type": "AWS.backup.list_backup_plans.BackupPlansList", + "region": "eu-central-1", + "count": 1 + } + ], + "costs": { + "currency": "USD", + "months": { + "2026-06": 42.1, + "2026-07": 44.35, + "2026-08": 41.9, + "2026-09": 45.2, + "2026-10": 43.75, + "2026-11": 19.1 + } + }, + "egress": null, + "upload": null, + "clock": "2026-11-12T09:30:00Z", + "run_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0008" +} diff --git a/tests/fixtures/samples/stress.json b/tests/fixtures/samples/stress.json new file mode 100644 index 0000000..17c7217 --- /dev/null +++ b/tests/fixtures/samples/stress.json @@ -0,0 +1,211 @@ +{ + "name": "Sample – A Deliberately Long Project Name That Keeps Going To Check How Titles, Headers And The Cover Wrap Over Several Lines", + "purpose": "Many types, long names, big counts, cent-level amounts and mixed currencies.", + "provider": "aws", + "exit_strategy": 1, + "scope": { + "accountId": "123456789012", + "region": "eu-central-1" + }, + "resources": [ + { + "type": "AWS.acm-pca", + "region": "eu-central-1", + "count": 1 + }, + { + "type": "AWS.acm.list_certificates.CertificateSummaryList", + "region": "eu-west-1", + "count": 8 + }, + { + "type": "AWS.amplify.list_apps.apps", + "region": "us-east-1", + "count": 15 + }, + { + "type": "AWS.apigateway.get_rest_apis.items", + "region": "eu-central-1", + "count": 22 + }, + { + "type": "AWS.appconfig.list_applications.Items", + "region": "eu-west-1", + "count": 29 + }, + { + "type": "AWS.appflow.list_flows.flows", + "region": "us-east-1", + "count": 36 + }, + { + "type": "AWS.application-autoscaling", + "region": "eu-central-1", + "count": 2 + }, + { + "type": "AWS.appmesh.list_meshes.meshes", + "region": "eu-west-1", + "count": 9 + }, + { + "type": "AWS.apprunner.list_services.ServiceSummaryList", + "region": "us-east-1", + "count": 16 + }, + { + "type": "AWS.appstream.describe_images.Images", + "region": "eu-central-1", + "count": 23 + }, + { + "type": "AWS.appsync.list_graphql_apis.graphqlApis", + "region": "eu-west-1", + "count": 30 + }, + { + "type": "AWS.athena.list_databases.DatabaseList", + "region": "us-east-1", + "count": 37 + }, + { + "type": "AWS.auditmanager.list_assessments.assessmentMetadata", + "region": "eu-central-1", + "count": 3 + }, + { + "type": "AWS.backup.list_backup_plans.BackupPlansList", + "region": "eu-west-1", + "count": 10 + }, + { + "type": "AWS.batch.describe_job_queues.jobQueues", + "region": "us-east-1", + "count": 17 + }, + { + "type": "AWS.cloud9.list_environments.environmentIds", + "region": "eu-central-1", + "count": 24 + }, + { + "type": "AWS.cloudcontrol.list_resources.ResourceDescriptions", + "region": "eu-west-1", + "count": 31 + }, + { + "type": "AWS.cloudformation.list_stacks.StackSummaries", + "region": "us-east-1", + "count": 38 + }, + { + "type": "AWS.cloudfront.list_distributions.DistributionList.Items", + "region": "eu-central-1", + "count": 4 + }, + { + "type": "AWS.cloudtrail.describe_trails.trailList", + "region": "eu-west-1", + "count": 11 + }, + { + "type": "AWS.cloudwatch.describe_alarms.MetricAlarms", + "region": "us-east-1", + "count": 18 + }, + { + "type": "AWS.codeartifact.list_repositories.repositories", + "region": "eu-central-1", + "count": 25 + }, + { + "type": "AWS.codebuild.list_projects.projects", + "region": "eu-west-1", + "count": 32 + }, + { + "type": "AWS.codecommit.list_repositories.repositories", + "region": "us-east-1", + "count": 39 + }, + { + "type": "AWS.dynamodb.list_tables.TableNames", + "region": "eu-central-1", + "count": 5 + }, + { + "type": "AWS.ec2.describe_volumes.Volumes", + "region": "eu-west-1", + "count": 12 + }, + { + "type": "AWS.ec2.describe_instances.Reservations", + "region": "us-east-1", + "count": 19 + }, + { + "type": "AWS.eks.list_clusters.clusters", + "region": "eu-central-1", + "count": 26 + }, + { + "type": "AWS.elasticache.describe_cache_clusters.CacheClusters", + "region": "eu-west-1", + "count": 33 + }, + { + "type": "AWS.kinesis.list_streams.StreamNames", + "region": "us-east-1", + "count": 40 + }, + { + "type": "AWS.lambda.list_functions.Functions", + "region": "eu-central-1", + "count": 6 + }, + { + "type": "AWS.rds.describe_db_instances.DBInstances", + "region": "eu-west-1", + "count": 13 + }, + { + "type": "AWS.route53.list_hosted_zones.HostedZones", + "region": "us-east-1", + "count": 20 + }, + { + "type": "AWS.s3.list_buckets.Buckets", + "region": "eu-central-1", + "count": 27 + }, + { + "type": "AWS.sns.list_topics.Topics", + "region": "eu-west-1", + "count": 34 + }, + { + "type": "AWS.sqs.list_queues.QueueUrls", + "region": "us-east-1", + "count": 41 + }, + { + "type": "AWS.ec2.describe_instances.Reservations", + "region": "ap-southeast-2", + "count": 123 + } + ], + "costs": { + "currency": "USD", + "months": { + "2026-10": 0.07, + "2026-11": 0.03 + }, + "currencies": { + "2026-11": "EUR" + } + }, + "egress": null, + "upload": null, + "clock": "2026-11-12T09:30:00Z", + "run_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0012" +} diff --git a/tests/fixtures/samples/tfstate/sample_aws.tfstate b/tests/fixtures/samples/tfstate/sample_aws.tfstate new file mode 100644 index 0000000..23ec7c1 --- /dev/null +++ b/tests/fixtures/samples/tfstate/sample_aws.tfstate @@ -0,0 +1,104 @@ +{ + "version": 4, + "terraform_version": "1.14.6", + "serial": 12, + "lineage": "00000000-0000-4000-8000-0000000000c1", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "aws_instance", + "name": "portal", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "availability_zone": "eu-central-1a", + "arn": "arn:aws:ec2:eu-central-1:123456789012:instance/i-0123456789abcdef0" + } + }, + { + "schema_version": 0, + "attributes": { + "availability_zone": "eu-central-1b", + "arn": "arn:aws:ec2:eu-central-1:123456789012:instance/i-0123456789abcdef1" + } + }, + { + "schema_version": 0, + "attributes": { + "availability_zone": "eu-west-1a", + "arn": "arn:aws:ec2:eu-west-1:123456789012:instance/i-0123456789abcdef2" + } + } + ] + }, + { + "mode": "managed", + "type": "aws_s3_bucket", + "name": "assets", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "region": "eu-central-1", + "bucket": "sample-portal-assets", + "arn": "arn:aws:s3:::sample-portal-assets" + } + }, + { + "schema_version": 0, + "attributes": { + "bucket": "sample-portal-exports", + "arn": "arn:aws:s3:::sample-portal-exports" + } + } + ] + }, + { + "mode": "managed", + "type": "aws_lambda_function", + "name": "worker", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "arn": "arn:aws:lambda:eu-central-1:123456789012:function:sample-worker" + } + } + ] + }, + { + "mode": "managed", + "type": "aws_sqs_queue", + "name": "jobs", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "region": "eu-central-1", + "arn": "arn:aws:sqs:eu-central-1:123456789012:sample-jobs" + } + } + ] + }, + { + "mode": "managed", + "type": "aws_iam_role", + "name": "worker", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "arn": "arn:aws:iam::123456789012:role/sample-worker" + } + } + ] + } + ] +} diff --git a/tests/fixtures/samples/tfstate/sample_azure.tfstate b/tests/fixtures/samples/tfstate/sample_azure.tfstate new file mode 100644 index 0000000..b5d5b52 --- /dev/null +++ b/tests/fixtures/samples/tfstate/sample_azure.tfstate @@ -0,0 +1,76 @@ +{ + "version": 4, + "terraform_version": "1.14.6", + "serial": 12, + "lineage": "00000000-0000-4000-8000-0000000000c2", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "azurerm_virtual_machine", + "name": "app", + "provider": "provider[\"registry.terraform.io/hashicorp/azurerm\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "/subscriptions/00000000-0000-4000-8000-0000000000a2/resourceGroups/rg-sample-archive/providers/Microsoft.Compute/virtualMachines/sample-app-1", + "location": "westeurope" + } + }, + { + "schema_version": 0, + "attributes": { + "id": "/subscriptions/00000000-0000-4000-8000-0000000000a2/resourceGroups/rg-sample-archive/providers/Microsoft.Compute/virtualMachines/sample-app-2", + "location": "northeurope" + } + } + ] + }, + { + "mode": "managed", + "type": "azurerm_storage_account", + "name": "docs", + "provider": "provider[\"registry.terraform.io/hashicorp/azurerm\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "/subscriptions/00000000-0000-4000-8000-0000000000a2/resourceGroups/rg-sample-archive/providers/Microsoft.Storage/storageAccounts/sampledocsprod", + "location": "westeurope" + } + } + ] + }, + { + "mode": "managed", + "type": "azurerm_mssql_database", + "name": "metadata", + "provider": "provider[\"registry.terraform.io/hashicorp/azurerm\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "/subscriptions/00000000-0000-4000-8000-0000000000a2/resourceGroups/rg-sample-archive/providers/Microsoft.Sql/servers/sample-sql/databases/metadata", + "location": "westeurope" + } + } + ] + }, + { + "mode": "managed", + "type": "azurerm_resource_group", + "name": "main", + "provider": "provider[\"registry.terraform.io/hashicorp/azurerm\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "/subscriptions/00000000-0000-4000-8000-0000000000a2/resourceGroups/rg-sample-archive", + "location": "westeurope" + } + } + ] + } + ] +} diff --git a/tests/fixtures/samples/tfstate_aws.json b/tests/fixtures/samples/tfstate_aws.json new file mode 100644 index 0000000..97561ba --- /dev/null +++ b/tests/fixtures/samples/tfstate_aws.json @@ -0,0 +1,15 @@ +{ + "name": "Sample – Customer Self-Service Portal (Terraform)", + "purpose": "A Terraform state: no costs, a resource without a region, never egress.", + "provider": "aws", + "exit_strategy": 1, + "scope": { + "tfstate": "sample_aws.tfstate" + }, + "resources": [], + "costs": null, + "egress": null, + "upload": null, + "clock": "2026-11-12T09:30:00Z", + "run_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0006" +} diff --git a/tests/fixtures/samples/tfstate_azure.json b/tests/fixtures/samples/tfstate_azure.json new file mode 100644 index 0000000..3956583 --- /dev/null +++ b/tests/fixtures/samples/tfstate_azure.json @@ -0,0 +1,15 @@ +{ + "name": "Sample – Document Archive (Terraform)", + "purpose": "A Terraform state of an Azure resource group: the state scope rows.", + "provider": "azure", + "exit_strategy": 1, + "scope": { + "tfstate": "sample_azure.tfstate" + }, + "resources": [], + "costs": null, + "egress": null, + "upload": null, + "clock": "2026-11-12T09:30:00Z", + "run_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0007" +} diff --git a/tests/fixtures/samples/uploaded_no_link.json b/tests/fixtures/samples/uploaded_no_link.json new file mode 100644 index 0000000..742bf06 --- /dev/null +++ b/tests/fixtures/samples/uploaded_no_link.json @@ -0,0 +1,46 @@ +{ + "name": "Sample – Field Service Scheduler", + "purpose": "Uploaded with an assessment ID and no link.", + "provider": "aws", + "exit_strategy": 2, + "scope": { + "accountId": "123456789012", + "region": "eu-central-1" + }, + "resources": [ + { + "type": "AWS.ec2.describe_instances.Reservations", + "region": "eu-central-1", + "count": 3 + }, + { + "type": "AWS.s3.list_buckets.Buckets", + "region": "eu-central-1", + "count": 2 + }, + { + "type": "AWS.lambda.list_functions.Functions", + "region": "eu-central-1", + "count": 4 + } + ], + "costs": { + "currency": "USD", + "months": { + "2026-06": 310.2, + "2026-07": 295.85, + "2026-08": 322.4, + "2026-09": 301.1, + "2026-10": 318.65, + "2026-11": 140.3 + } + }, + "egress": null, + "upload": { + "status": "uploaded", + "assessment_id": "sample-assessment-0001", + "sync_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0005" + }, + "clock": "2026-11-12T09:30:00Z", + "run_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0005" +} diff --git a/tests/report/test_basic_report.py b/tests/report/test_basic_report.py index 5da2f82..a4b7dae 100644 --- a/tests/report/test_basic_report.py +++ b/tests/report/test_basic_report.py @@ -5,7 +5,6 @@ import tempfile import unittest from datetime import datetime, timezone -from html.parser import HTMLParser from pathlib import Path from unittest.mock import patch @@ -35,6 +34,7 @@ from tests.report import test_egress as egress_helpers from tests.report.test_no_credentials import pricing_for_both_clouds from tests.support.flow import build_flow_run +from tests.support.html import html_text from tests.support.report import ( build_empty_report_fixture, build_fixture_context, @@ -118,31 +118,6 @@ def uploaded(**changes): return provenance(uploaded=True, platform_url=URL, assessment_id="a-1", **changes) -class TextOfHtml(HTMLParser): - def __init__(self): - super().__init__() - self.parts = [] - self.skip = 0 - - def handle_starttag(self, tag, attrs): - if tag in ("script", "style"): - self.skip += 1 - - def handle_endtag(self, tag): - if tag in ("script", "style") and self.skip: - self.skip -= 1 - - def handle_data(self, data): - if not self.skip: - self.parts.append(data) - - -def html_text(html): - parser = TextOfHtml() - parser.feed(html) - return " ".join(" ".join(parser.parts).split()) - - def page_texts(reader): return [" ".join(page.extract_text().split()) for page in reader.pages] diff --git a/tests/report/test_egress_report.py b/tests/report/test_egress_report.py index efd0352..d1bfc4d 100644 --- a/tests/report/test_egress_report.py +++ b/tests/report/test_egress_report.py @@ -23,7 +23,6 @@ from tests.report.test_basic_report import ( COLLECTED, URL, - html_text, metadata_texts, outline_entries, page_texts, @@ -32,6 +31,7 @@ ) from tests.report.test_no_credentials import pricing_for_both_clouds from tests.support.flow import build_flow_run +from tests.support.html import html_text from tests.support.report import ( build_fixture_context, build_report_fixture, diff --git a/tests/samples/__init__.py b/tests/samples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/samples/__main__.py b/tests/samples/__main__.py new file mode 100644 index 0000000..1a7856c --- /dev/null +++ b/tests/samples/__main__.py @@ -0,0 +1,178 @@ +import argparse +import sys +from html import escape +from pathlib import Path + +from cloudexit.catalogue.store import active_catalogue_path +from tests.samples.render import render_cases +from tests.samples.scenarios import ScenarioError, load_scenarios + +DOCUMENTS = ( + ("report.pdf", "Executive Summary"), + ("egress.pdf", "Egress & Data Landscape"), +) +PAGE = (595, 842) + +STYLE = """ +body { font: 14px/1.45 system-ui, sans-serif; margin: 2rem; color: #1a1f24; } +table { border-collapse: collapse; width: 100%; } +th, td { text-align: left; vertical-align: top; padding: .45rem .7rem; + border-bottom: 1px solid #e3e7ea; } +th { background: #f5f7f8; } +a { color: #005470; } +.failed { color: #b8322a; font-weight: 600; } +.muted { color: #6b7680; } +""" + + +def parse(argv): + parser = argparse.ArgumentParser( + prog="python -m tests.samples", + description="Render the sample assessments through the real report pipeline.", + ) + parser.add_argument("--case", nargs="+", metavar="NAME", help="cases to render") + parser.add_argument("--out", default="samples", help="output folder") + parser.add_argument( + "--catalogue", help="catalogue to render with (default: the installed one)" + ) + parser.add_argument( + "--sheets", action="store_true", help="contact sheets, needs PyMuPDF" + ) + return parser.parse_args(argv) + + +def load_pymupdf(): + try: + import pymupdf + except ImportError: + try: + import fitz as pymupdf + except ImportError: + return None + return pymupdf + + +def write_sheets(result, pymupdf): + # Four pages to a sheet, two by two + sheets, number = [], 0 + for name, _title in DOCUMENTS: + if name not in result.pages: + continue + source = pymupdf.open(str(result.directory / name)) + for first in range(0, source.page_count, 4): + number += 1 + last = min(first + 4, source.page_count) + sheet = pymupdf.open() + page = sheet.new_page(width=2 * PAGE[0], height=2 * PAGE[1]) + for slot, index in enumerate(range(first, last)): + x, y = (slot % 2) * PAGE[0], (slot // 2) * PAGE[1] + page.show_pdf_page( + pymupdf.Rect(x, y, x + PAGE[0], y + PAGE[1]), source, index + ) + file = f"sheet-{number}.png" + page.get_pixmap(dpi=110).save(str(result.directory / file)) + sheets.append((f"{name} {first + 1}–{last}", file)) + source.close() + return sheets + + +def link(case, file, label): + return f'{escape(label)}' + + +def overview(scenarios, results, failures, sheets): + by_case = {result.case: result for result in results} + rows = [] + for case, scenario in scenarios.items(): + result = by_case.get(case) + if result is None: + rows.append( + f"{escape(case)}{escape(scenario.name)}" + f'{escape(scenario.provider)}' + f"{escape(failures.get(case, 'not rendered'))}" + ) + continue + pages = ", ".join( + f"{title}: {result.pages[name]}" + for name, title in DOCUMENTS + if name in result.pages + ) + links = [ + link(case, name, title) + for name, title in ( + ("report.pdf", "Executive Summary PDF"), + ("index.html", "Executive Summary HTML"), + ("egress.pdf", "Egress PDF"), + ("egress.html", "Egress HTML"), + ) + if name in result.files + ] + links += [link(case, file, label) for label, file in sheets.get(case, [])] + rows.append( + f"{escape(case)}{escape(scenario.name)}" + f"{escape(scenario.provider)}{escape(scenario.purpose)}" + f"{escape(pages)}{'
'.join(links)}" + ) + return ( + '' + '' + f"Sample Reports" + '

Sample reports

Fictional data, rendered through ' + "the real report pipeline.

" + "" + f"{''.join(rows)}
CaseNameProviderPurposePagesOpen
\n" + ) + + +def main(argv=None): + args = parse(argv) + try: + scenarios = load_scenarios() + except ScenarioError as exc: + print(f"Scenario error: {exc}", file=sys.stderr) + return 2 + + if args.case: + unknown = [case for case in args.case if case not in scenarios] + if unknown: + print( + f"No such case: {', '.join(unknown)}. Cases: {', '.join(scenarios)}", + file=sys.stderr, + ) + return 2 + scenarios = {case: scenarios[case] for case in scenarios if case in args.case} + + catalogue = Path(args.catalogue) if args.catalogue else active_catalogue_path() + if not catalogue.is_file(): + print( + f"No catalogue at {catalogue}. Run cloudexit once so that the " + "catalogue is downloaded, or pass --catalogue PATH.", + file=sys.stderr, + ) + return 2 + + out = Path(args.out) + out.mkdir(parents=True, exist_ok=True) + results, failures = render_cases(scenarios.values(), out, catalogue_path=catalogue) + + sheets = {} + if args.sheets: + pymupdf = load_pymupdf() + if pymupdf is None: + print("PyMuPDF is not installed: no contact sheets (pip install pymupdf).") + else: + sheets = {result.case: write_sheets(result, pymupdf) for result in results} + + (out / "index.html").write_text( + overview(scenarios, results, failures, sheets), encoding="utf-8" + ) + for result in results: + print(f"rendered {result.case}") + for message in failures.values(): + print(f"FAILED {message}", file=sys.stderr) + print(f"Overview: {out / 'index.html'}") + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/samples/catalogue.py b/tests/samples/catalogue.py new file mode 100644 index 0000000..668a2a0 --- /dev/null +++ b/tests/samples/catalogue.py @@ -0,0 +1,222 @@ +import sqlite3 + +from tests.make_smoke_dataset import TABLES +from tests.support.catalogue import build_catalogue +from tests.support.flow import RISKS + +EXTRA_TABLES = """ +CREATE TABLE resourcetype_data ( + id INTEGER PRIMARY KEY, resource_type INTEGER NOT NULL UNIQUE, + data_category TEXT NOT NULL, strategy TEXT NOT NULL, params TEXT, + status TEXT NOT NULL +); +CREATE TABLE egresspricing ( + id INTEGER PRIMARY KEY, csp INTEGER NOT NULL, component TEXT NOT NULL, + zone TEXT NOT NULL DEFAULT 'zone1', tier_from REAL NOT NULL, tier_to REAL, + price_per_unit REAL NOT NULL, currency TEXT NOT NULL DEFAULT 'USD', + unit TEXT NOT NULL DEFAULT 'GiB', valid_from TEXT NOT NULL, valid_to TEXT +); +""" + +# Terraform type to resource type code, for the state file fixtures +TF_CODES = { + "aws_instance": "AWS.ec2.describe_instances.Reservations", + "aws_s3_bucket": "AWS.s3.list_buckets.Buckets", + "aws_lambda_function": "AWS.lambda.list_functions.Functions", + "aws_sqs_queue": "AWS.sqs.list_queues.QueueUrls", + "azurerm_virtual_machine": "Microsoft.Compute/virtualMachines", + "azurerm_storage_account": "Microsoft.Storage/storageAccounts", + "azurerm_mssql_database": "Microsoft.Sql/servers/databases", +} + +NAMES = { + "AWS.ec2.describe_instances.Reservations": "EC2 Instance", + "AWS.s3.list_buckets.Buckets": "S3 Bucket", + "AWS.ec2.describe_volumes.Volumes": "EBS Volume", + "AWS.rds.describe_db_instances.DBInstances": "RDS Instance", + "AWS.dynamodb.list_tables.TableNames": "DynamoDB Table", + "AWS.lambda.list_functions.Functions": "Lambda Function", + "AWS.eks.list_clusters.clusters": "EKS Cluster", + "AWS.sqs.list_queues.QueueUrls": "SQS Queue", + "AWS.sns.list_topics.Topics": "SNS Topic", + "AWS.apigateway.get_rest_apis.items": "API Gateway", + "AWS.acm.list_certificates.CertificateSummaryList": "Certificate Manager", + "AWS.cloudwatch.describe_alarms.MetricAlarms": "CloudWatch Alarm", + "AWS.backup.list_backup_plans.BackupPlansList": "Backup Plan", + "Microsoft.Compute/virtualMachines": "Virtual Machine", + "Microsoft.ContainerService/managedClusters": "Kubernetes Cluster", + "Microsoft.Storage/storageAccounts": "Storage Account", + "Microsoft.Sql/servers/databases": "SQL Database", + "Microsoft.DocumentDB/databaseAccounts": "Cosmos DB Account", + "Microsoft.DBforPostgreSQL/flexibleServers": "PostgreSQL Flexible Server", + "Microsoft.RecoveryServices/vaults": "Recovery Services Vault", + "Microsoft.ContainerRegistry/registries": "Container Registry", + "Microsoft.Compute/disks": "Managed Disk", + # Deliberately long, for the stress case + "AWS.cloudformation.list_stacks.StackSummaries": ( + "Sample Infrastructure Stack Definition With An Unreasonably Long Name" + ), + "AWS.codeartifact.list_repositories.repositories": ( + "Sample Artifact Repository For Internally Built And Mirrored Packages" + ), + "AWS.cloudfront.list_distributions.DistributionList.Items": ( + "Sample Content Delivery Distribution For Static And Dynamic Content" + ), +} + +ICONS = { + "AWS.ec2.describe_instances.Reservations": "/icons/aws/Compute/EC2.png", + "AWS.s3.list_buckets.Buckets": "/icons/aws/Storage/Simple-Storage-Service.png", + "AWS.ec2.describe_volumes.Volumes": "/icons/aws/Storage/Elastic-Block-Store.png", + "AWS.rds.describe_db_instances.DBInstances": "/icons/aws/Database/RDS.png", + "AWS.dynamodb.list_tables.TableNames": "/icons/aws/Database/DynamoDB.png", + "AWS.lambda.list_functions.Functions": "/icons/aws/Compute/Lambda.png", + "Microsoft.Compute/virtualMachines": ( + "/icons/azure/compute/10021-icon-service-Virtual-Machine.png" + ), + "Microsoft.Storage/storageAccounts": ( + "/icons/azure/storage/10086-icon-service-Storage-Accounts.png" + ), + "Microsoft.Sql/servers/databases": ( + "/icons/azure/databases/10130-icon-service-SQL-Database.png" + ), + "Microsoft.Compute/disks": "/icons/azure/compute/10032-icon-service-Disks.png", +} + +# The data a type carries, for the egress estimate +CATEGORIES = { + "AWS.s3.list_buckets.Buckets": "object", + "AWS.ec2.describe_volumes.Volumes": "block", + "AWS.rds.describe_db_instances.DBInstances": "database", + "AWS.dynamodb.list_tables.TableNames": "database", + "Microsoft.Storage/storageAccounts": "object", + "Microsoft.Compute/disks": "block", + "Microsoft.Sql/servers/databases": "database", + "Microsoft.DocumentDB/databaseAccounts": "database", + "Microsoft.RecoveryServices/vaults": "backup", +} + +# Types that are not in the inventory catalogue but still carry data +INACTIVE = {"Microsoft.Compute/disks"} + +# (alternatives, of them with a support plan) per type; the rest has enough +ALTERNATIVES = { + "AWS.ec2.describe_volumes.Volumes": (0, 0), + "AWS.dynamodb.list_tables.TableNames": (0, 0), + "AWS.s3.list_buckets.Buckets": (2, 1), + "AWS.rds.describe_db_instances.DBInstances": (1, 0), + "AWS.eks.list_clusters.clusters": (4, 2), + "Microsoft.ContainerRegistry/registries": (0, 0), + "Microsoft.DocumentDB/databaseAccounts": (2, 2), + "Microsoft.Compute/virtualMachines": (5, 4), +} +DEFAULT_ALTERNATIVES = (3, 3) + +LADDERS = { + 1: (0.087, 0.083, 0.07, 0.05), + 2: (0.09, 0.085, 0.07, 0.05), +} + + +def placeholder_name(code): + if code in NAMES: + return NAMES[code] + service = code.split(".")[1] if code.startswith("AWS.") else code.split("/")[0] + return f"Sample {service} Resource" + + +def provider_of(code): + return 2 if code.startswith("AWS.") else 1 + + +def _pricing_rows(): + rows = [] + for csp, (first, second, third, fourth) in LADDERS.items(): + tiers = ( + (0, 100, 0.0), + (100, 10240, first), + (10240, 51200, second), + (51200, 153600, third), + (153600, None, fourth), + ) + for lower, upper, price in tiers: + rows.append((csp, "internet_egress", lower, upper, price)) + rows.append((csp, "archive_retrieval", 0, None, 0.02)) + return rows + + +def build_sample_catalogue(path, scenarios): + # The codes the scenarios use, and the ones the state files map from + codes = set(TF_CODES.values()) + for scenario in scenarios: + codes |= scenario.type_codes() + codes = sorted(codes) + tf_codes = {code: tf for tf, code in TF_CODES.items()} + + build_catalogue(path, version="2026-11-02") + conn = sqlite3.connect(path) + conn.executescript(TABLES) + conn.executescript(EXTRA_TABLES) + conn.execute("DELETE FROM resourcetype") + conn.executemany("INSERT INTO risk VALUES (?, ?, ?, ?)", RISKS) + + ids = {} + for index, code in enumerate(codes, start=100): + ids[code] = index + conn.execute( + "INSERT INTO resourcetype (id, csp, code, name, icon, status, tf_code) " + "VALUES (?, ?, ?, ?, ?, ?, ?)", + ( + index, + provider_of(code), + code, + placeholder_name(code), + ICONS.get(code, ""), + "f" if code in INACTIVE else "t", + tf_codes.get(code), + ), + ) + if code in CATEGORIES: + conn.execute( + "INSERT INTO resourcetype_data (resource_type, data_category, " + "strategy, params, status) VALUES (?, ?, 'sample', '{}', 't')", + (index, CATEGORIES[code]), + ) + + conn.execute( + "INSERT INTO alternativetechnologyorganization (id, name, url, " + "stability_tier, country_code, years_in_business) " + "VALUES (1, 'Sample Vendor', NULL, 3, 'DE', 12)" + ) + technology = 0 + for code, resource_type in ids.items(): + total, supported = ALTERNATIVES.get(code, DEFAULT_ALTERNATIVES) + for position in range(total): + technology += 1 + conn.execute( + "INSERT INTO alternativetechnology (id, product_name, " + "product_description, product_url, open_source, support_plan, " + "status, organization_id) VALUES (?, ?, '', NULL, ?, ?, 't', 1)", + ( + technology, + f"Sample Technology {technology}", + "t" if position % 2 else "f", + "t" if position < supported else "f", + ), + ) + for strategy in (1, 2, 3): + conn.execute( + "INSERT INTO alternative (resource_type, " + "alternative_technology, strategy_type) VALUES (?, ?, ?)", + (resource_type, technology, strategy), + ) + + conn.executemany( + "INSERT INTO egresspricing (csp, component, zone, tier_from, tier_to, " + "price_per_unit, currency, unit, valid_from, valid_to) " + "VALUES (?, ?, 'zone1', ?, ?, ?, 'USD', 'GiB', '2026-07-01', NULL)", + _pricing_rows(), + ) + conn.commit() + conn.close() + return ids diff --git a/tests/samples/render.py b/tests/samples/render.py new file mode 100644 index 0000000..afe9947 --- /dev/null +++ b/tests/samples/render.py @@ -0,0 +1,293 @@ +import logging +import shutil +import sqlite3 +from contextlib import ExitStack +from dataclasses import dataclass, field +from datetime import datetime +from pathlib import Path +from unittest.mock import patch + +from pypdf import PdfReader + +from cloudexit.catalogue import store +from cloudexit.catalogue.db import connect +from cloudexit.egress.estimate import estimate_egress +from cloudexit.egress.model import GIB, load_egress_registry, new_row +from cloudexit.engine import ( + create_resource_inventory, + generate_report, + perform_risk_assessment, +) +from cloudexit.report.assets import copy_assets +from cloudexit.report.egress import generate_egress_html_report +from tests.support.html import html_blocks + +# What a rendered case holds. Both HTML reports have a text dump, so a change +# to either can be reviewed with a diff. +EXPECTED_FILES = { + "always": ("report.pdf", "index.html", "texts/report.txt", "texts/index-html.txt"), + "egress": ( + "egress.pdf", + "egress.html", + "texts/egress.txt", + "texts/egress-html.txt", + ), +} +ASSESSMENT_TYPE = 1 + + +class RenderError(Exception): + pass + + +@dataclass +class RenderResult: + case: str + directory: Path + files: list = field(default_factory=list) + pages: dict = field(default_factory=dict) + + +def expected_files(scenario): + files = list(EXPECTED_FILES["always"]) + if scenario.egress is not None: + files += EXPECTED_FILES["egress"] + return files + + +def frozen_datetime(moment): + class FrozenDatetime(datetime): + @classmethod + def now(cls, tz=None): + return moment.astimezone(tz) if tz else moment.replace(tzinfo=None) + + return FrozenDatetime + + +def pdf_dump(path): + reader = PdfReader(str(path)) + parts = [] + for number, page in enumerate(reader.pages, start=1): + lines = [line.rstrip() for line in (page.extract_text() or "").splitlines()] + parts.append(f"--- page {number} ---\n" + "\n".join(lines)) + return "\n".join(parts) + "\n", len(reader.pages) + + +def _catalogue_types(catalogue_path, csp): + uri = f"{Path(catalogue_path).resolve().as_uri()}?mode=ro" + conn = sqlite3.connect(uri, uri=True) + try: + rows = conn.execute( + "SELECT code, id, status FROM resourcetype WHERE csp = ?", (csp,) + ).fetchall() + finally: + conn.close() + return {code: (type_id, status == "t") for code, type_id, status in rows} + + +def _check_types(scenario, catalogue_path): + types = _catalogue_types(catalogue_path, scenario.csp) + for resource in scenario.resources: + code = resource["type"] + if code not in types: + raise RenderError(f"resource type {code} is not in the catalogue") + if not types[code][1]: + raise RenderError(f"resource type {code} is not active in the catalogue") + return {code: type_id for code, (type_id, _active) in types.items()} + + +def _insert_inventory(scenario, report_path, type_ids): + db_path = Path(report_path) / "data" / "assessment.db" + with connect(db_path=db_path) as conn: + conn.executemany( + "INSERT INTO resource_inventory (resource_type, location, count) " + "VALUES (?, ?, ?) ON CONFLICT(resource_type, location) " + "DO UPDATE SET count = excluded.count", + [ + (type_ids[item["type"]], item["region"], item["count"]) + for item in scenario.resources + ], + ) + costs = scenario.costs + if costs: + currencies = costs.get("currencies") or {} + conn.executemany( + "INSERT INTO cost_inventory (month, cost, currency) VALUES (?, ?, ?) " + "ON CONFLICT(month) DO UPDATE SET cost = excluded.cost, " + "currency = excluded.currency", + [ + (f"{month}-01", amount, currencies.get(month, costs["currency"])) + for month, amount in sorted(costs["months"].items()) + ], + ) + conn.commit() + + +def _egress_rows(scenario): + # What the collectors would have found, in the shape they return + registry = load_egress_registry(scenario.csp) + rows = [] + for index, item in enumerate(scenario.egress["resources"], start=1): + key = item["type"].strip().lower() if scenario.csp == 1 else item["type"] + if key not in registry: + raise RenderError(f"egress type {item['type']} is not in the registry") + entry = registry[key] + row = new_row( + f"/sample/{index}", + item["name"], + item["type"], + entry["label"], + entry["category"], + ) + if item["size_gib"] is not None: + row["size_bytes"] = round(item["size_gib"] * GIB) + row["size_unknown"] = bool(item.get("unknown")) + if item.get("tiers"): + row["tier_bytes"] = { + tier: round(gib * GIB) for tier, gib in item["tiers"].items() + } + row["flags"] = list(item.get("flags", [])) + row["notes"] = list(item.get("notes", [])) + rows.append(row) + return rows, set(scenario.egress["archive_tiers"]) + + +def _estimate_egress(scenario, report_path, raw_data_path): + rows, archive_tiers = _egress_rows(scenario) + with ( + patch( + "cloudexit.providers.aws.egress.collect_aws_egress", + return_value=(rows, archive_tiers), + ), + patch( + "cloudexit.providers.azure.egress.collect_azure_egress", + return_value=(rows, archive_tiers), + ), + ): + result = estimate_egress( + scenario.csp, + scenario.provider_details(), + raw_data_path, + report_path=report_path, + name=scenario.name, + exit_strategy=scenario.exit_strategy, + assessment_type=ASSESSMENT_TYPE, + ) + if not result["success"]: + raise RenderError(f"egress estimate failed: {result['logs']}") + return result["json_path"] + + +def _write_dumps(scenario, directory, result): + texts = directory / "texts" + texts.mkdir() + for pdf, dump in (("report.pdf", "report.txt"), ("egress.pdf", "egress.txt")): + if (directory / pdf).is_file(): + text, pages = pdf_dump(directory / pdf) + (texts / dump).write_text(text, encoding="utf-8") + result.pages[pdf] = pages + for html, dump in ( + ("index.html", "index-html.txt"), + ("egress.html", "egress-html.txt"), + ): + if (directory / html).is_file(): + source = (directory / html).read_text(encoding="utf-8") + (texts / dump).write_text(html_blocks(source), encoding="utf-8") + + +def _render(scenario, directory, catalogue_path): + type_ids = _check_types(scenario, catalogue_path) + report_path = str(directory) + raw_data_path = str(directory / "raw_data") + (directory / "raw_data").mkdir(parents=True) + details = scenario.provider_details() + + if scenario.tfstate: + # The real inventory stage, which also copies the assets and the catalogue + result = create_resource_inventory( + scenario.csp, details, report_path, raw_data_path + ) + if not result["success"]: + raise RenderError(f"resource inventory failed: {result['logs']}") + else: + copy_assets(report_path, scenario.csp) + _insert_inventory(scenario, report_path, type_ids) + + egress_json = None + if scenario.egress is not None: + egress_json = _estimate_egress(scenario, report_path, raw_data_path) + + risks = perform_risk_assessment(scenario.exit_strategy, report_path) + if not risks["success"]: + raise RenderError(f"risk assessment failed: {risks['logs']}") + + if egress_json: + html = generate_egress_html_report( + report_path, egress_json, details, scenario.run_id + ) + if not html["success"]: + raise RenderError(f"egress.html failed: {html['logs']}") + + report = generate_report( + scenario.csp, + details, + scenario.exit_strategy, + ASSESSMENT_TYPE, + scenario.name, + report_path, + raw_data_path, + upload=scenario.upload, + sync_id=scenario.run_id, + egress_json_path=egress_json, + ) + if not report["success"]: + raise RenderError(f"report failed: {report['logs']}") + if report.get("egress_pdf_logs"): + raise RenderError(f"egress.pdf failed: {report['egress_pdf_logs']}") + + +def render_case(scenario, out_dir, *, catalogue_path): + directory = Path(out_dir) / scenario.case + if directory.exists(): + shutil.rmtree(directory) + directory.mkdir(parents=True) + + previous = store._active_override + clock = frozen_datetime(scenario.clock) + try: + store.set_active_catalogue(catalogue_path) + with ExitStack() as stack: + # The warnings of the pipeline are about the sample data, not news + stack.callback(logging.disable, logging.root.manager.disable) + logging.disable(logging.WARNING) + for module in ("report.context", "engine", "egress.estimate"): + stack.enter_context(patch(f"cloudexit.{module}.datetime", clock)) + _render(scenario, directory, catalogue_path) + except RenderError as exc: + raise RenderError(f"{scenario.case}: {exc}") from exc + finally: + store.set_active_catalogue(previous) + + result = RenderResult(scenario.case, directory) + _write_dumps(scenario, directory, result) + result.files = [ + name for name in expected_files(scenario) if (directory / name).is_file() + ] + return result + + +def render_cases(scenarios, out_dir, *, catalogue_path): + # A case that fails is reported, and the others still render + results, failures = [], {} + for scenario in scenarios: + try: + results.append( + render_case(scenario, out_dir, catalogue_path=catalogue_path) + ) + except Exception as exc: + failures[scenario.case] = ( + str(exc) + if isinstance(exc, RenderError) + else f"{scenario.case}: {exc!r}" + ) + return results, failures diff --git a/tests/samples/scenarios.py b/tests/samples/scenarios.py new file mode 100644 index 0000000..94a2f46 --- /dev/null +++ b/tests/samples/scenarios.py @@ -0,0 +1,290 @@ +import json +import re +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path + +from tests.support.paths import ROOT + +SAMPLES_DIR = ROOT / "tests" / "fixtures" / "samples" +TFSTATE_DIR = SAMPLES_DIR / "tfstate" + +PROVIDER_IDS = {"azure": 1, "aws": 2} +FIELDS = ( + "name", + "purpose", + "provider", + "exit_strategy", + "scope", + "resources", + "costs", + "egress", + "upload", + "clock", + "run_id", +) +SCOPE_FIELDS = { + "aws": ("accountId", "region"), + "azure": ("tenantId", "subscriptionId", "resourceGroupName"), +} +UPLOAD_FIELDS = ("status", "url", "assessment_id", "sync_id", "reason") +MONTH = re.compile(r"^\d{4}-(0[1-9]|1[0-2])$") +CURRENCY = re.compile(r"^[A-Z]{3}$") +UUID7 = re.compile( + r"^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" +) + + +class ScenarioError(ValueError): + pass + + +@dataclass(frozen=True) +class Scenario: + case: str + path: Path + name: str + purpose: str + provider: str + exit_strategy: int + scope: dict + resources: tuple + costs: dict | None + egress: dict | None + upload: dict | None + clock: datetime + run_id: str + + @property + def csp(self): + return PROVIDER_IDS[self.provider] + + @property + def tfstate(self): + name = self.scope.get("tfstate") + return str(TFSTATE_DIR / name) if name else None + + def provider_details(self): + # What the collectors would have been given, without any credential + if self.tfstate: + return {"tfstatePath": self.tfstate} + return dict(self.scope) + + def type_codes(self): + codes = {resource["type"] for resource in self.resources} + if self.egress: + codes |= {resource["type"] for resource in self.egress["resources"]} + return codes + + +def _fail(path, field, message): + raise ScenarioError(f"{Path(path).name}: {field}: {message}") + + +def _text(path, field, value): + if not isinstance(value, str) or not value.strip(): + _fail(path, field, "expected a non-empty string") + return value + + +def _number(path, field, value, *, allow_none=False): + if value is None and allow_none: + return None + if isinstance(value, bool) or not isinstance(value, (int, float)) or value < 0: + _fail(path, field, "expected a number that is not negative") + return value + + +def _count(path, field, value): + if isinstance(value, bool) or not isinstance(value, int) or value < 1: + _fail(path, field, "expected a whole number from 1") + return value + + +def _only(path, field, data, allowed): + for key in data: + if key not in allowed: + _fail(path, f"{field}.{key}", "unknown field") + + +def _scope(path, provider, scope): + if not isinstance(scope, dict): + _fail(path, "scope", "expected an object") + if "tfstate" in scope: + _only(path, "scope", scope, ("tfstate",)) + name = _text(path, "scope.tfstate", scope["tfstate"]) + if Path(name).name != name or not (TFSTATE_DIR / name).is_file(): + _fail(path, "scope.tfstate", f"no such file in {TFSTATE_DIR.name}/: {name}") + return scope + fields = SCOPE_FIELDS[provider] + _only(path, "scope", scope, fields) + for field in fields: + if field not in scope: + _fail(path, f"scope.{field}", "missing") + _text(path, f"scope.{field}", scope[field]) + return scope + + +def _resources(path, resources): + if not isinstance(resources, list): + _fail(path, "resources", "expected a list") + seen = set() + for index, resource in enumerate(resources): + field = f"resources[{index}]" + if not isinstance(resource, dict): + _fail(path, field, "expected an object") + _only(path, field, resource, ("type", "region", "count")) + for key in ("type", "region", "count"): + if key not in resource: + _fail(path, f"{field}.{key}", "missing") + _text(path, f"{field}.type", resource["type"]) + _text(path, f"{field}.region", resource["region"]) + _count(path, f"{field}.count", resource["count"]) + key = (resource["type"], resource["region"]) + if key in seen: + _fail(path, field, f"{key[0]} in {key[1]} is listed twice") + seen.add(key) + return tuple(resources) + + +def _costs(path, costs): + if costs is None: + return None + if not isinstance(costs, dict): + _fail(path, "costs", "expected an object or null") + _only(path, "costs", costs, ("currency", "months", "currencies")) + for key in ("currency", "months"): + if key not in costs: + _fail(path, f"costs.{key}", "missing") + if not CURRENCY.match(str(costs["currency"])): + _fail(path, "costs.currency", "expected a three-letter code such as USD") + if not isinstance(costs["months"], dict): + _fail(path, "costs.months", "expected an object of YYYY-MM: amount") + for month, amount in costs["months"].items(): + if not MONTH.match(month): + _fail(path, f"costs.months.{month}", "expected a month as YYYY-MM") + _number(path, f"costs.months.{month}", amount) + for month, currency in (costs.get("currencies") or {}).items(): + if month not in costs["months"]: + _fail(path, f"costs.currencies.{month}", "not a month of costs.months") + if not CURRENCY.match(str(currency)): + _fail(path, f"costs.currencies.{month}", "expected a three-letter code") + return costs + + +def _egress(path, egress): + if egress is None: + return None + if not isinstance(egress, dict): + _fail(path, "egress", "expected an object or null") + _only(path, "egress", egress, ("resources", "archive_tiers")) + tiers = egress.get("archive_tiers", []) + if not isinstance(tiers, list) or not all(isinstance(t, str) for t in tiers): + _fail(path, "egress.archive_tiers", "expected a list of tier names") + resources = egress.get("resources") + if not isinstance(resources, list): + _fail(path, "egress.resources", "expected a list") + for index, resource in enumerate(resources): + field = f"egress.resources[{index}]" + if not isinstance(resource, dict): + _fail(path, field, "expected an object") + _only( + path, + field, + resource, + ("type", "name", "size_gib", "unknown", "tiers", "flags", "notes"), + ) + for key in ("type", "name", "size_gib"): + if key not in resource: + _fail(path, f"{field}.{key}", "missing") + _text(path, f"{field}.type", resource["type"]) + _text(path, f"{field}.name", resource["name"]) + _number(path, f"{field}.size_gib", resource["size_gib"], allow_none=True) + for key in ("tiers",): + for tier, gib in (resource.get(key) or {}).items(): + _number(path, f"{field}.{key}.{tier}", gib) + for key in ("flags", "notes"): + values = resource.get(key, []) + if not isinstance(values, list) or not all( + isinstance(v, str) for v in values + ): + _fail(path, f"{field}.{key}", "expected a list of strings") + return egress + + +def _upload(path, upload): + if upload is None: + return None + if not isinstance(upload, dict): + _fail(path, "upload", "expected an object or null") + _only(path, "upload", upload, UPLOAD_FIELDS) + _text(path, "upload.status", upload.get("status")) + return upload + + +def _clock(path, value): + text = _text(path, "clock", value) + if not text.endswith("Z"): + _fail(path, "clock", "expected an ISO time in UTC ending in Z") + try: + return datetime.fromisoformat(text).astimezone(timezone.utc) + except ValueError: + _fail(path, "clock", f"not an ISO time: {text}") + + +def load_scenario(path): + path = Path(path) + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise ScenarioError(f"{path.name}: cannot read the file: {exc}") from exc + if not isinstance(data, dict): + _fail(path, "(file)", "expected an object") + for field in FIELDS: + if field not in data: + _fail(path, field, "missing") + _only(path, "(file)", data, FIELDS) + + provider = data["provider"] + if provider not in PROVIDER_IDS: + _fail( + path, + "provider", + f"expected one of {sorted(PROVIDER_IDS)}, got {provider!r}", + ) + strategy = data["exit_strategy"] + if strategy not in (1, 2, 3) or isinstance(strategy, bool): + _fail(path, "exit_strategy", "expected 1, 2 or 3") + run_id = _text(path, "run_id", data["run_id"]) + if not UUID7.match(run_id): + _fail(path, "run_id", "expected a UUIDv7-shaped string") + + scope = _scope(path, provider, data["scope"]) + resources = _resources(path, data["resources"]) + costs = _costs(path, data["costs"]) + egress = _egress(path, data["egress"]) + if "tfstate" in scope and (resources or costs or egress): + _fail(path, "scope.tfstate", "a state file has no resources, costs or egress") + + return Scenario( + case=path.stem, + path=path, + name=_text(path, "name", data["name"]), + purpose=_text(path, "purpose", data["purpose"]), + provider=provider, + exit_strategy=strategy, + scope=scope, + resources=resources, + costs=costs, + egress=egress, + upload=_upload(path, data["upload"]), + clock=_clock(path, data["clock"]), + run_id=run_id, + ) + + +def load_scenarios(directory=SAMPLES_DIR): + return { + path.stem: load_scenario(path) + for path in sorted(Path(directory).glob("*.json")) + } diff --git a/tests/samples/test_samples.py b/tests/samples/test_samples.py new file mode 100644 index 0000000..fea1e96 --- /dev/null +++ b/tests/samples/test_samples.py @@ -0,0 +1,484 @@ +import io +import re +import sqlite3 +import tempfile +import unittest +from contextlib import redirect_stderr, redirect_stdout +from pathlib import Path +from unittest.mock import patch + +from pypdf import PdfReader + +from cloudexit.report.sections import AWS_ICONS_URL, AZURE_ICONS_URL +from cloudexit.report.wording import BLOCKS +from tests.report.test_basic_report import metadata_texts, page_texts +from tests.report.test_no_credentials import find_leaks, folder_text +from tests.samples import __main__ as samples_main +from tests.samples.catalogue import build_sample_catalogue +from tests.samples.render import ( + expected_files, + pdf_dump, + render_case, + render_cases, +) +from tests.samples.scenarios import load_scenarios +from tests.support.credentials import AWS_DETAILS, AZURE_DETAILS +from tests.support.html import html_blocks, html_text +from tests.support.report_terms import american_hits, find_terms, retired_term_hits +from tests.support.sync import SECRETS + +IDENTIFYING = ("accountId", "tenantId", "subscriptionId", "resourceGroupName") +ICON_MARKERS = {"azure": "Microsoft group", "aws": "Amazon.com"} +EGRESS_FILES = ( + "egress.pdf", + "egress.html", + "texts/egress.txt", + "texts/egress-html.txt", +) + +STATE = {} + + +def squashed(text): + return text.replace(" ", "") + + +def setUpModule(): + tmp = tempfile.TemporaryDirectory() + STATE["tmp"] = tmp + root = Path(tmp.name) + STATE["scenarios"] = scenarios = load_scenarios() + STATE["catalogue"] = catalogue = root / "catalogue.db" + build_sample_catalogue(catalogue, scenarios.values()) + STATE["out"] = root / "out" + results, failures = render_cases( + scenarios.values(), STATE["out"], catalogue_path=catalogue + ) + STATE["results"] = {result.case: result for result in results} + STATE["failures"] = failures + + +def tearDownModule(): + STATE["tmp"].cleanup() + STATE.clear() + + +class RenderedTestCase(unittest.TestCase): + def cases(self): + yield from STATE["scenarios"].items() + + def folder(self, case): + return STATE["results"][case].directory + + def text(self, case, name): + return (self.folder(case) / name).read_text(encoding="utf-8") + + def pdf_pages(self, case, name): + return page_texts(PdfReader(str(self.folder(case) / name))) + + +class RenderTests(RenderedTestCase): + def test_every_case_renders(self): + self.assertEqual(STATE["failures"], {}) + self.assertEqual(set(STATE["results"]), set(STATE["scenarios"])) + + def test_the_expected_files_exist_and_no_others_of_the_list(self): + for case, scenario in self.cases(): + with self.subTest(case): + for name in expected_files(scenario): + self.assertTrue((self.folder(case) / name).is_file(), name) + if scenario.egress is None: + for name in EGRESS_FILES: + self.assertFalse((self.folder(case) / name).exists(), name) + self.assertEqual(STATE["results"][case].files, expected_files(scenario)) + + def test_a_state_file_never_produces_egress(self): + for case, scenario in self.cases(): + if scenario.tfstate: + with self.subTest(case): + self.assertEqual(scenario.egress, None) + self.assertFalse((self.folder(case) / "egress.pdf").exists()) + + def test_the_dumps_match_the_files(self): + for case, scenario in self.cases(): + with self.subTest(case): + folder = self.folder(case) + for pdf, dump in ( + ("report.pdf", "report.txt"), + ("egress.pdf", "egress.txt"), + ): + if (folder / pdf).is_file(): + text, pages = pdf_dump(folder / pdf) + self.assertEqual(self.text(case, f"texts/{dump}"), text) + self.assertEqual(text.count("--- page "), pages) + for html, dump in ( + ("index.html", "index-html.txt"), + ("egress.html", "egress-html.txt"), + ): + if (folder / html).is_file(): + self.assertEqual( + self.text(case, f"texts/{dump}"), + html_blocks(self.text(case, html)), + ) + + def test_rendering_a_case_twice_gives_the_same_texts(self): + scenario = STATE["scenarios"]["aws_typical"] + with tempfile.TemporaryDirectory() as tmp: + again = render_case(scenario, tmp, catalogue_path=STATE["catalogue"]) + for name in expected_files(scenario): + if name.startswith("texts/"): + self.assertEqual( + (again.directory / name).read_text(encoding="utf-8"), + self.text("aws_typical", name), + name, + ) + + +class GuardTests(RenderedTestCase): + def test_no_credential_and_no_value_of_another_case(self): + for case, scenario in self.cases(): + with self.subTest(case): + folder = self.folder(case) + state = "" + if scenario.tfstate: + state = Path(scenario.tfstate).read_text(encoding="utf-8") + own = {v for k, v in scenario.scope.items() if k in IDENTIFYING} + others = { + v + for other, candidate in self.cases() + if other != case + for k, v in candidate.scope.items() + if k in IDENTIFYING + } + forbidden = {v for v in others - own if v not in state} + texts = folder_text(folder) + for name, text in texts.items(): + for value in forbidden: + self.assertNotIn(value, text, f"{name}: {value}") + for secret in SECRETS: + self.assertNotIn(secret, text, name) + self.assertIsNone(re.search(r"\bAKIA[0-9A-Z]{16}\b", text), name) + for details in (AWS_DETAILS, AZURE_DETAILS): + self.assertEqual(find_leaks(folder, details), []) + + def test_the_term_wording_and_retired_term_guards(self): + for case, scenario in self.cases(): + with self.subTest(case): + folder = self.folder(case) + texts = [] + for pdf in ("report.pdf", "egress.pdf"): + if (folder / pdf).is_file(): + reader = PdfReader(str(folder / pdf)) + for text in metadata_texts(reader): + self.assertEqual(find_terms(text), []) + texts += page_texts(reader) + for html in ("index.html", "egress.html"): + if (folder / html).is_file(): + source = self.text(case, html) + texts.append(html_text(source)) + self.assertEqual(find_terms(source.split(" Date: Sun, 25 Oct 2026 12:57:33 +0100 Subject: [PATCH 53/61] replace the two html reports with one offline page --- cloudexit/assets/css/driver.css | 1 + cloudexit/assets/css/report.css | 305 ++++ cloudexit/assets/css/style.css | 1107 -------------- cloudexit/assets/img/csp/alibaba.svg | 12 + cloudexit/assets/img/csp/aws.svg | 15 + cloudexit/assets/img/csp/azure.svg | 23 + cloudexit/assets/img/csp/gcp.svg | 13 + cloudexit/assets/img/logo/README.md | 3 + cloudexit/assets/js/driver.js.LICENSE.txt | 21 + cloudexit/assets/js/driver.js.iife.js | 2 + cloudexit/assets/template/egress.html | 775 ---------- cloudexit/assets/template/index.html | 1625 +++++++-------------- cloudexit/cli/app.py | 23 - cloudexit/engine.py | 37 +- cloudexit/report/aggregate.py | 23 - cloudexit/report/assets.py | 2 +- cloudexit/report/context.py | 17 +- cloudexit/report/egress.py | 179 +-- cloudexit/report/egress_report.py | 4 +- cloudexit/report/evidence.py | 34 +- cloudexit/report/helpers.py | 27 +- cloudexit/report/html_data.py | 63 - cloudexit/report/html_egress.py | 248 ++++ cloudexit/report/html_sections.py | 894 ++++++++++-- cloudexit/report/notices.py | 4 +- cloudexit/report/wording.py | 415 +++++- cloudexit/report/writers.py | 116 +- tests/catalogue/test_dataset_shapes.py | 8 +- tests/cli/test_app.py | 109 +- tests/platform/test_document.py | 4 - tests/report/test_basic_report.py | 172 +-- tests/report/test_egress.py | 228 +-- tests/report/test_egress_report.py | 124 +- tests/report/test_html_report.py | 955 ++++++++++++ tests/report/test_icon_placeholder.py | 57 +- tests/report/test_no_credentials.py | 1 - tests/report/test_report_flow.py | 12 +- tests/report/test_report_pipeline.py | 90 +- tests/report/test_report_transforms.py | 61 - tests/samples/__main__.py | 3 +- tests/samples/catalogue.py | 6 +- tests/samples/render.py | 28 +- tests/samples/rendered.py | 31 + tests/samples/test_samples.py | 49 +- tests/support/egress.py | 47 + tests/support/report_terms.py | 3 + tests/test_packaging.py | 32 +- 47 files changed, 3782 insertions(+), 4226 deletions(-) create mode 100644 cloudexit/assets/css/driver.css create mode 100644 cloudexit/assets/css/report.css delete mode 100644 cloudexit/assets/css/style.css create mode 100644 cloudexit/assets/img/csp/alibaba.svg create mode 100644 cloudexit/assets/img/csp/aws.svg create mode 100644 cloudexit/assets/img/csp/azure.svg create mode 100644 cloudexit/assets/img/csp/gcp.svg create mode 100644 cloudexit/assets/img/logo/README.md create mode 100644 cloudexit/assets/js/driver.js.LICENSE.txt create mode 100644 cloudexit/assets/js/driver.js.iife.js delete mode 100644 cloudexit/assets/template/egress.html delete mode 100644 cloudexit/report/html_data.py create mode 100644 cloudexit/report/html_egress.py create mode 100644 tests/report/test_html_report.py create mode 100644 tests/samples/rendered.py create mode 100644 tests/support/egress.py diff --git a/cloudexit/assets/css/driver.css b/cloudexit/assets/css/driver.css new file mode 100644 index 0000000..7ce0bd6 --- /dev/null +++ b/cloudexit/assets/css/driver.css @@ -0,0 +1 @@ +.driver-active .driver-overlay,.driver-active *{pointer-events:none}.driver-active .driver-active-element,.driver-active .driver-active-element *,.driver-popover,.driver-popover *{pointer-events:auto}@keyframes animate-fade-in{0%{opacity:0}to{opacity:1}}.driver-fade .driver-overlay{animation:animate-fade-in .2s ease-in-out}.driver-fade .driver-popover{animation:animate-fade-in .2s}.driver-popover{all:unset;box-sizing:border-box;color:#2d2d2d;margin:0;padding:15px;border-radius:5px;min-width:250px;max-width:300px;box-shadow:0 1px 10px #0006;z-index:1000000000;position:fixed;top:0;right:0;background-color:#fff}.driver-popover *{font-family:Helvetica Neue,Inter,ui-sans-serif,"Apple Color Emoji",Helvetica,Arial,sans-serif}.driver-popover-title{font:19px/normal sans-serif;font-weight:700;display:block;position:relative;line-height:1.5;zoom:1;margin:0}.driver-popover-close-btn{all:unset;position:absolute;top:0;right:0;width:32px;height:28px;cursor:pointer;font-size:18px;font-weight:500;color:#d2d2d2;z-index:1;text-align:center;transition:color;transition-duration:.2s}.driver-popover-close-btn:hover,.driver-popover-close-btn:focus{color:#2d2d2d}.driver-popover-title[style*=block]+.driver-popover-description{margin-top:5px}.driver-popover-description{margin-bottom:0;font:14px/normal sans-serif;line-height:1.5;font-weight:400;zoom:1}.driver-popover-footer{margin-top:15px;text-align:right;zoom:1;display:flex;align-items:center;justify-content:space-between}.driver-popover-progress-text{font-size:13px;font-weight:400;color:#727272;zoom:1}.driver-popover-footer button{all:unset;display:inline-block;box-sizing:border-box;padding:3px 7px;text-decoration:none;text-shadow:1px 1px 0 #fff;background-color:#fff;color:#2d2d2d;font:12px/normal sans-serif;cursor:pointer;outline:0;zoom:1;line-height:1.3;border:1px solid #ccc;border-radius:3px}.driver-popover-footer .driver-popover-btn-disabled{opacity:.5;pointer-events:none}:not(body):has(>.driver-active-element){overflow:hidden!important}.driver-no-interaction,.driver-no-interaction *{pointer-events:none!important}.driver-popover-footer button:hover,.driver-popover-footer button:focus{background-color:#f7f7f7}.driver-popover-navigation-btns{display:flex;flex-grow:1;justify-content:flex-end}.driver-popover-navigation-btns button+button{margin-left:4px}.driver-popover-arrow{content:"";position:absolute;border:5px solid #fff}.driver-popover-arrow-side-over{display:none}.driver-popover-arrow-side-left{left:100%;border-right-color:transparent;border-bottom-color:transparent;border-top-color:transparent}.driver-popover-arrow-side-right{right:100%;border-left-color:transparent;border-bottom-color:transparent;border-top-color:transparent}.driver-popover-arrow-side-top{top:100%;border-right-color:transparent;border-bottom-color:transparent;border-left-color:transparent}.driver-popover-arrow-side-bottom{bottom:100%;border-left-color:transparent;border-top-color:transparent;border-right-color:transparent}.driver-popover-arrow-side-center{display:none}.driver-popover-arrow-side-left.driver-popover-arrow-align-start,.driver-popover-arrow-side-right.driver-popover-arrow-align-start{top:15px}.driver-popover-arrow-side-top.driver-popover-arrow-align-start,.driver-popover-arrow-side-bottom.driver-popover-arrow-align-start{left:15px}.driver-popover-arrow-align-end.driver-popover-arrow-side-left,.driver-popover-arrow-align-end.driver-popover-arrow-side-right{bottom:15px}.driver-popover-arrow-side-top.driver-popover-arrow-align-end,.driver-popover-arrow-side-bottom.driver-popover-arrow-align-end{right:15px}.driver-popover-arrow-side-left.driver-popover-arrow-align-center,.driver-popover-arrow-side-right.driver-popover-arrow-align-center{top:50%;margin-top:-5px}.driver-popover-arrow-side-top.driver-popover-arrow-align-center,.driver-popover-arrow-side-bottom.driver-popover-arrow-align-center{left:50%;margin-left:-5px}.driver-popover-arrow-none{display:none} diff --git a/cloudexit/assets/css/report.css b/cloudexit/assets/css/report.css new file mode 100644 index 0000000..70479be --- /dev/null +++ b/cloudexit/assets/css/report.css @@ -0,0 +1,305 @@ +@font-face { font-family: 'IBM Plex Sans'; font-weight: 400; font-style: normal; font-display: swap; src: url('../fonts/IBMPlexSans-Regular.ttf') format('truetype'); } +@font-face { font-family: 'IBM Plex Sans'; font-weight: 400; font-style: italic; font-display: swap; src: url('../fonts/IBMPlexSans-Italic.ttf') format('truetype'); } +@font-face { font-family: 'IBM Plex Sans'; font-weight: 600; font-style: normal; font-display: swap; src: url('../fonts/IBMPlexSans-SemiBold.ttf') format('truetype'); } +@font-face { font-family: 'IBM Plex Sans'; font-weight: 600; font-style: italic; font-display: swap; src: url('../fonts/IBMPlexSans-SemiBoldItalic.ttf') format('truetype'); } +@font-face { font-family: 'IBM Plex Mono'; font-weight: 400; font-style: normal; font-display: swap; src: url('../fonts/IBMPlexMono-Regular.ttf') format('truetype'); } +@font-face { font-family: 'IBM Plex Mono'; font-weight: 600; font-style: normal; font-display: swap; src: url('../fonts/IBMPlexMono-SemiBold.ttf') format('truetype'); } + +:root { + --primary-50: #f0fdfc; --primary-100: #ccfbf8; --primary-600: #0d948b; --primary-700: #0f766f; --primary-800: #115e59; --primary-900: #134e4a; + --neutral-100: #f3f6f6; --neutral-200: #e5ebeb; --neutral-300: #d1dbdb; --neutral-600: #4b6361; --neutral-900: #112726; + + --surface: #fff; --surface-muted: var(--neutral-100); --border: var(--neutral-200); --border-strong: var(--neutral-300); + --text: var(--neutral-900); --text-secondary: var(--neutral-600); --text-muted: #6B7680; --brand: var(--primary-800); + --thead-bg: #E9F0F2; --thead-rule: #C9D0D6; + + --status-success: #047854; --status-success-bg: #ecfdf8; + --status-pending: #4b6361; --status-pending-bg: #eef2f2; + + --sev-critical: #6E1A14; --sev-high: #B8322A; --sev-medium: #E8A33D; --sev-low: #4A7FB5; --sev-off: #E3E7EA; + + --hotspot-bg: #FDF6EC; --hotspot-ink: #B45309; + + --radius-card: 16px; --radius-control: 8px; --radius-chip: 6px; --radius-pill: 999px; + --shadow-card: 0 1px 4px rgba(0,0,0,.10); --shadow-popover: 0 8px 24px rgba(0,0,0,.12); + --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-5: 20px; --space-6: 24px; --space-8: 32px; + --topbar-h: 64px; +} + +* { box-sizing: border-box; } +html { scroll-padding-top: 120px; } +body { margin: 0; background: var(--surface-muted); color: var(--text-secondary); font-family: 'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', sans-serif; font-size: 14px; line-height: 1.43; -webkit-font-smoothing: antialiased; } +a { color: var(--brand); } +h1, h2, h3, p { margin: 0; } +img { display: block; } +:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 4px; } +.visually-hidden { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } +.muted { color: var(--text-muted); } +code, .mono { font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; } + +.t-title { font-size: 24px; line-height: 32px; font-weight: 500; color: var(--text); } +.t-section { font-size: 20px; line-height: 28px; font-weight: 500; color: var(--text); } +.t-sub { font-size: 16px; line-height: 24px; font-weight: 600; color: var(--text); } +.t-label { font-size: 12px; line-height: 16px; font-weight: 500; letter-spacing: .04em; text-transform: uppercase; color: var(--text-muted); } +.t-value { font-size: 14px; line-height: 20px; font-weight: 500; color: var(--text); } +.t-figure { font-size: 28px; line-height: 32px; font-weight: 500; color: var(--text); font-variant-numeric: tabular-nums; } +.t-caption { font-size: 12px; line-height: 16px; color: var(--text-muted); } + +/* icons: an inline SVG sprite in the page, no icon font */ +.i { width: 16px; height: 16px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; vertical-align: -3px; } +.i.sm { width: 13px; height: 13px; vertical-align: -2px; } + +/* header: logo, edition tag and Help */ +.app-header { position: relative; z-index: 50; height: var(--topbar-h); background: var(--surface); border-bottom: 1px solid var(--border); } +.app-header .inner { max-width: 1184px; height: 100%; margin: 0 auto; padding: 0 var(--space-4); display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); } +.brand { display: inline-flex; align-items: center; gap: 8px; text-decoration: none; } +.brand img { display: block; height: 28px; width: auto; } +.brand-tag { font-size: 11px; line-height: 16px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--brand); background: var(--primary-50); border: 1px solid var(--primary-100); border-radius: 6px; padding: 1px 7px; } +.help-btn { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--border-strong); background: var(--surface); color: var(--text-secondary); font: inherit; font-weight: 500; padding: 6px 12px; border-radius: var(--radius-pill); cursor: pointer; } +.help-btn:hover { color: var(--brand); border-color: var(--brand); } +.help-btn .i { width: 16px; height: 16px; } + +/* layout: one column with the sticky section bar; from 1200 px the bar is a sidebar */ +.card { background: var(--surface); border-radius: var(--radius-card); box-shadow: var(--shadow-card); } +.card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; padding: var(--space-4) var(--space-6); border-bottom: 1px solid var(--border); } +.card-body { padding: var(--space-5) var(--space-6) var(--space-6); } +@media (max-width: 575.98px) { .card-head, .card-body { padding-left: var(--space-4); padding-right: var(--space-4); } } +.shell { max-width: 1184px; margin: 0 auto; padding: var(--space-5) var(--space-4) 64px; display: grid; grid-template-columns: minmax(0, 1fr); grid-template-areas: "title" "nav" "main"; } +.shell > .titlebar { grid-area: title; } +.shell > .secnav { grid-area: nav; } +.shell > main { grid-area: main; min-width: 0; } +@media (min-width: 1200px) { + .shell { max-width: 1400px; grid-template-columns: 208px minmax(0, 1fr); grid-template-areas: "nav title" "nav main"; column-gap: var(--space-8); } + .shell > .titlebar { margin-bottom: var(--space-6); } + .secnav { grid-row: 1 / span 2; align-self: start; top: var(--space-4); margin: 0; background: none; backdrop-filter: none; border-bottom: 0; } + .secnav ul { flex-direction: column; overflow: visible; gap: 2px; } + .secnav a { display: flex; justify-content: space-between; padding: 8px 12px; border-bottom: 0; border-radius: 8px; white-space: normal; } + .secnav a:hover { background: var(--surface); color: var(--text); } + .secnav a[aria-current="true"] { background: var(--surface); box-shadow: inset 3px 0 0 var(--brand); } + .app-header .inner { max-width: 1400px; } +} + +/* buttons, chips, menus */ +.btn { display: inline-flex; align-items: center; gap: 8px; border-radius: var(--radius-control); border: 1px solid var(--brand); background: var(--surface); color: var(--brand); font: inherit; font-weight: 500; padding: 8px 14px; text-decoration: none; cursor: pointer; white-space: nowrap; } +.chip { display: inline-flex; align-items: center; gap: 6px; border-radius: var(--radius-pill); padding: 2px 10px; font-size: 12px; font-weight: 600; white-space: nowrap; line-height: 20px; } +.chip.success { background: var(--status-success-bg); color: var(--status-success); } +.chip.offline { background: var(--status-pending-bg); color: var(--status-pending); } +.menu { position: relative; } +.menu > summary { list-style: none; } +.menu > summary::-webkit-details-marker { display: none; } +.menu-list { position: absolute; right: 0; top: calc(100% + 6px); z-index: 40; min-width: 260px; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow-popover); padding: 6px; display: grid; } +.menu-list a { display: flex; gap: 10px; align-items: center; padding: 8px 10px; border-radius: 8px; color: var(--text); text-decoration: none; } +.menu-list a:hover, .menu-list a:focus-visible { background: var(--surface-muted); outline: none; } + +/* title bar */ +.titlebar { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; padding: var(--space-4) var(--space-6); } +.titlebar .name { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; min-width: 0; } +.titlebar .name img.provider-logo { width: 28px; height: 24px; object-fit: contain; } +.titlebar > div:first-child { flex: 1 1 520px; min-width: 0; } +.titlebar .actions { display: flex; gap: var(--space-3); align-self: flex-start; } +a.platform-link { text-decoration: none; } +a.platform-link:hover { background: var(--primary-100); } +@media (max-width: 575.98px) { .titlebar { padding: var(--space-4); } .titlebar .actions { width: 100%; } .titlebar .actions > * { flex: 1; } .titlebar .actions .btn, .titlebar .actions summary { width: 100%; justify-content: center; } } + +/* section nav */ +.secnav { position: sticky; top: 0; z-index: 20; background: rgba(243,246,246,.94); backdrop-filter: blur(6px); border-bottom: 1px solid var(--border); margin: var(--space-4) 0 var(--space-6); } +.secnav ul { list-style: none; margin: 0; padding: 0; display: flex; gap: 2px; overflow-x: auto; scrollbar-width: none; } +.secnav a { display: block; padding: 12px; color: var(--text-muted); text-decoration: none; white-space: nowrap; font-weight: 500; border-bottom: 2px solid transparent; } +.secnav a[aria-current="true"] { color: var(--brand); border-bottom-color: var(--brand); } +.secnav .n { font-weight: 400; margin-left: 4px; } +.secnav a .i { margin-left: 6px; color: var(--text-muted); } +section.block { margin-bottom: var(--space-6); scroll-margin-top: 64px; } + +/* overview cards */ +.overview { display: grid; grid-template-columns: 1.05fr 1fr 1.15fr; gap: var(--space-6); } +@media (max-width: 1099.98px) { .overview { grid-template-columns: 1fr 1fr; } .overview .costs { grid-column: 1 / -1; } } +@media (max-width: 767.98px) { .overview { grid-template-columns: 1fr; } } +.overview .card { padding: var(--space-5) var(--space-6); } +dl.scope { display: grid; grid-template-columns: auto 1fr; gap: 10px 16px; margin: var(--space-3) 0 0; } +dl.scope dt { color: var(--text-muted); } +dl.scope dd { margin: 0; color: var(--text); font-weight: 500; overflow-wrap: anywhere; } +dl.scope dd small { display: block; font-weight: 400; color: var(--text-muted); font-size: 12px; } +.sev { display: inline-flex; align-items: center; gap: 6px; font-weight: 500; white-space: nowrap; color: var(--text); } +.pips { display: inline-flex; gap: 2px; } +.pips i { width: 8px; height: 8px; border-radius: 1px; background: var(--sev-off); } +.sev.critical .pips i { background: var(--sev-critical); } +.sev.high .pips i:nth-child(-n+3) { background: var(--sev-high); } +.sev.medium .pips i:nth-child(-n+2) { background: var(--sev-medium); } +.sev.low .pips i:nth-child(-n+1) { background: var(--sev-low); } +.sevchart { display: grid; gap: 14px; margin-top: var(--space-5); } +.sevrow { display: grid; grid-template-columns: 96px 1fr 24px; gap: 12px; align-items: center; } +.sevrow b { text-align: right; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; } +.sevbar { height: 14px; border-radius: 4px; background: var(--neutral-100); overflow: hidden; } +.sevbar span { display: block; height: 100%; border-radius: 4px; } +.sevbar span.critical { background: var(--sev-critical); } +.sevbar span.high { background: var(--sev-high); } +.sevbar span.medium { background: var(--sev-medium); } +.sevbar span.low { background: var(--sev-low); } +.kpis { display: flex; gap: var(--space-6); flex-wrap: wrap; margin-top: var(--space-2); } +.bars { display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; align-items: end; height: 132px; margin-top: var(--space-4); } +.bars .col { display: flex; flex-direction: column; justify-content: flex-end; align-items: center; gap: 6px; height: 100%; } +.bars .track { flex: 1; width: 100%; display: flex; align-items: flex-end; justify-content: center; } +.bars .b { width: 100%; max-width: 40px; min-height: 2px; background: var(--primary-700); border-radius: 4px 4px 0 0; } +.bars .b.zero { background: var(--neutral-300); } +.bars .b.prov { background: repeating-linear-gradient(135deg, var(--primary-100) 0 4px, #fff 4px 8px); border: 1.5px dashed var(--primary-700); border-bottom: 0; } +.bars .m { font-size: 12px; color: var(--text-muted); } +.no-costs { display: grid; gap: 8px; margin-top: 6px; } +.no-costs p { color: var(--text-secondary); } + +/* scores: platform-only results, shown without values */ +.scores { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-6); } +@media (max-width: 767.98px) { .scores { grid-template-columns: 1fr; } } +.grade { display: grid; grid-template-columns: auto 1fr; gap: var(--space-6); align-items: center; } +.grade-tile { width: 96px; height: 96px; border: 2px dashed var(--border-strong); border-radius: 12px; display: grid; place-items: center; text-align: center; } +.grade-tile .l { font-size: 44px; line-height: 1; font-weight: 600; color: var(--text-muted); } +.bands { display: flex; gap: 2px; } +.bands span { flex: 1; height: 24px; display: grid; place-items: center; font-size: 11px; font-weight: 600; background: var(--neutral-200); color: var(--text-muted); } +.dims { display: grid; gap: var(--space-3); } +.dim { display: grid; grid-template-columns: 110px 1fr 48px; gap: 12px; align-items: center; color: var(--text); } +.dim .track { height: 10px; border-radius: 999px; background: var(--neutral-200); } +.ghost { opacity: .55; } +.lock-note { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--brand); background: var(--primary-50); border: 1px solid var(--primary-100); border-radius: var(--radius-pill); padding: 2px 10px; white-space: nowrap; } +button.lock-note { font: inherit; font-size: 12px; font-weight: 600; cursor: pointer; } +button.lock-note:hover, button.lock-note[aria-expanded="true"] { background: var(--primary-100); } +.hint { position: relative; display: inline-flex; } +.hint-pop { position: absolute; right: 0; top: calc(100% + 8px); z-index: 30; width: min(340px, 86vw); padding: 14px 16px; border-radius: 12px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow-popover); font-size: 13px; line-height: 1.55; color: var(--text-secondary); text-align: left; } +.hint-pop b { display: block; color: var(--text); font-weight: 600; margin-bottom: 4px; } +.hint-pop p + p { margin-top: 8px; } +.hint-pop .foot { font-size: 12px; color: var(--text-muted); } +.hint-pop a { color: var(--brand); } +.hint-pop.flip { right: auto; left: 0; } +@media (max-width: 575.98px) { .hint-pop, .hint-pop.flip { position: fixed; left: 12px; right: 12px; top: auto; bottom: 12px; width: auto; } } + +/* tables */ +.table-wrap { overflow-x: auto; } +table.data { width: 100%; border-collapse: collapse; } +table.data thead th { text-align: left; font-size: 12px; font-weight: 600; color: var(--brand); background: var(--thead-bg); padding: 9px 12px; border-bottom: 2px solid var(--thead-rule); white-space: nowrap; } +table.data th.num, table.data td.num { text-align: right; font-variant-numeric: tabular-nums; } +table.data tbody td { padding: 10px 12px; border-bottom: 1px solid var(--border); vertical-align: top; color: var(--text); } +table.data tr.group td { font-size: 12px; font-weight: 600; color: var(--text-muted); background: #fafbfb; padding: 8px 12px; } +table.data tr.total td { font-weight: 600; border-bottom: 0; } +.share { display: inline-flex; align-items: center; gap: 8px; min-width: 120px; } +.share .bar { flex: 1; height: 6px; background: var(--neutral-200); border-radius: 3px; overflow: hidden; min-width: 60px; } +.share .bar span { display: block; height: 100%; background: var(--primary-700); } +.share b { font-weight: 500; min-width: 36px; text-align: right; font-variant-numeric: tabular-nums; } +.rt { display: flex; gap: 10px; align-items: center; font-weight: 500; } +.rt img { width: 22px; height: 22px; object-fit: contain; flex: none; } +.rt .ph { width: 22px; height: 22px; flex: none; border-radius: 5px; background: var(--neutral-200); color: var(--text-secondary); font-size: 10px; font-weight: 600; display: grid; place-items: center; } +.loc { color: var(--text-muted); font-size: 12px; white-space: nowrap; } +@media (max-width: 575.98px) { .hide-sm { display: none; } } +.empty-state { display: grid; gap: 4px; justify-items: center; text-align: center; padding: 28px 16px; border: 1px dashed var(--border-strong); border-radius: 12px; color: var(--text-muted); } +.empty-state b { color: var(--text); font-weight: 600; } + +/* risks */ +.rowbtn { all: unset; cursor: pointer; display: inline-flex; gap: 8px; align-items: baseline; } +.rowbtn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 4px; } +.rowbtn .chev { transition: transform .15s; color: var(--text-muted); } +.rowbtn[aria-expanded="true"] .chev { transform: rotate(90deg); } +.rname { font-weight: 500; } +tr.detail td { background: #fafbfb; color: var(--text-secondary); font-size: 13px; } +tr.detail[hidden] { display: none !important; } +@media (max-width: 575.98px) { + table.risks thead { display: none; } + table.risks tr.r { display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; padding: 12px 0; border-bottom: 1px solid var(--border); } + table.risks tr.r td { border: 0; padding: 0 4px; } + table.risks tr.r td.sevc { grid-row: 1; justify-self: end; } + table.risks tr.r td.name, table.risks tr.r td.aff { grid-column: 1 / -1; } + table.risks tr.group, table.risks tr.detail, table.risks tr.group td, table.risks tr.detail td { display: block; } +} + +/* alternatives */ +.toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } +.searchbox { position: relative; display: inline-block; } +.searchbox .i { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); color: var(--text-muted); pointer-events: none; } +.search { border: 1px solid var(--border-strong); border-radius: var(--radius-control); padding: 7px 10px 7px 32px; font: inherit; width: 240px; background: #fff; color: var(--text); } +@media (max-width: 575.98px) { .search, .searchbox { width: 100%; } } +.fchip { border: 1px solid var(--border-strong); border-radius: var(--radius-pill); padding: 6px 12px; background: #fff; font: inherit; font-size: 13px; color: var(--text); cursor: pointer; } +.fchip[aria-pressed="true"] { background: var(--brand); color: #fff; border-color: var(--brand); } +details.grp { border: 1px solid var(--border); border-radius: 12px; margin-bottom: var(--space-3); overflow: hidden; background: #fff; } +details.grp > summary { list-style: none; cursor: pointer; display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 12px 16px; } +details.grp > summary::-webkit-details-marker { display: none; } +details.grp > summary .t { font-weight: 600; color: var(--text); display: flex; gap: 8px; align-items: center; } +details.grp > summary .t img { width: 20px; height: 20px; object-fit: contain; } +details.grp > summary .m { color: var(--text-muted); font-size: 12px; text-align: right; } +details.grp[open] > summary { border-bottom: 1px solid var(--border); } +details.grp > summary .t::before, details.disc > summary::before { content: ""; width: 6px; height: 6px; border-right: 1.6px solid var(--text-muted); border-bottom: 1.6px solid var(--text-muted); transform: rotate(-45deg); transition: transform .15s; flex: none; margin: 0 2px; } +details.grp[open] > summary .t::before, details.disc[open] > summary::before { transform: rotate(45deg); } +details.grp table.data thead th { background: #f6f9fa; } +.alt-name { font-weight: 500; } +.alt-meta { display: block; color: var(--text-muted); font-size: 12px; } +.cc { display: inline-block; font-size: 10px; font-weight: 600; letter-spacing: .04em; color: var(--text-secondary); border: 1px solid var(--border-strong); border-radius: 4px; padding: 0 4px; margin-left: 4px; vertical-align: 1px; } +.more { padding: 10px 16px; font-weight: 500; } +.more a { text-decoration: none; } +.alt-empty { padding: 16px; border: 1px dashed var(--border-strong); border-radius: 12px; color: var(--text-muted); text-align: center; } +table.alts th:nth-child(n+2), table.alts td:nth-child(n+2) { text-align: center; width: 120px; } +table.alts .mark { width: 18px; height: 18px; stroke-width: 2.2; vertical-align: middle; } +table.alts .yes .mark { color: var(--status-success); } +table.alts .no .mark { color: var(--neutral-300); stroke-width: 1.8; } + +/* egress */ +.kpi-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-4); } +@media (max-width: 767.98px) { .kpi-row { grid-template-columns: 1fr 1fr; } } +.kpi { border: 1px solid var(--border); border-radius: 12px; padding: var(--space-4); } +.kpi.primary { background: var(--primary-50); border-color: var(--primary-100); } +.kpi .t-figure { margin-top: 6px; font-size: 24px; } +.kpi .t-caption { margin-top: 4px; } +.headroom { margin-top: 10px; height: 8px; border-radius: 4px; background: var(--neutral-200); overflow: hidden; } +.headroom span { display: block; height: 100%; background: var(--primary-700); } +.vol { display: flex; height: 14px; gap: 2px; border-radius: 7px; overflow: hidden; margin: var(--space-6) 0 var(--space-2); } +.vol-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 12px; color: var(--text); } +.vol-legend i { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 6px; vertical-align: -1px; } +.unsized { display: inline-flex; align-items: center; gap: 4px; color: var(--hotspot-ink); font-size: 12px; font-weight: 500; white-space: nowrap; } +.fine { margin-top: var(--space-4); display: grid; gap: 6px; } +details.prices { margin-top: var(--space-4); border: 1px solid var(--border); border-radius: 12px; } +details.prices > summary { cursor: pointer; padding: 10px 14px; font-weight: 500; color: var(--brand); } +details.prices[open] > summary { border-bottom: 1px solid var(--border); } +details.prices .inner { padding: 4px 14px 14px; } + +/* about this report */ +.kv { width: 100%; border-collapse: collapse; } +.kv th { text-align: left; font-weight: 500; color: var(--text-muted); width: 200px; padding: 8px 12px 8px 0; border-bottom: 1px solid var(--border); vertical-align: top; } +.kv td { color: var(--text); padding: 8px 0; border-bottom: 1px solid var(--border); overflow-wrap: anywhere; } +.kv-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-6); } +@media (max-width: 991.98px) { .kv-grid { grid-template-columns: 1fr; } } +.kv-grid h3 { margin-bottom: var(--space-2); } +.kv-grid .kv th { width: 44%; } +details.disc { border-top: 1px solid var(--border); } +details.disc:last-child { border-bottom: 1px solid var(--border); } +details.disc > summary { cursor: pointer; list-style: none; display: flex; align-items: center; gap: 8px; padding: 14px 0; font-weight: 600; color: var(--text); } +details.disc > summary::-webkit-details-marker { display: none; } +details.disc > summary .m { margin-left: auto; font-weight: 400; font-size: 12px; color: var(--text-muted); } +details.disc .inner { padding: 0 0 var(--space-5) 20px; display: grid; gap: 10px; color: var(--text-secondary); max-width: 860px; } +details.disc .inner ul { margin: 0; padding-left: 18px; display: grid; gap: 4px; } + +/* disclaimer and legal notices */ +.disclaimer { margin-top: var(--space-8); display: grid; grid-template-columns: 22px 1fr; gap: 12px; background: var(--hotspot-bg); border: 1px solid #F3D9A8; border-radius: 12px; padding: var(--space-4) var(--space-5); color: #5C4A1F; font-size: 13px; line-height: 1.55; } +.disclaimer > .i { width: 20px; height: 20px; color: var(--hotspot-ink); margin-top: 1px; } +.disclaimer h2 { font-size: 14px; font-weight: 600; color: #3D2F10; margin-bottom: 4px; } +.disclaimer p + p { margin-top: 6px; } +.disclaimer a { color: #6B4E12; } +.notices { border-top: 1px solid var(--border); margin-top: var(--space-8); padding-top: var(--space-6); display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-6); font-size: 12px; line-height: 1.55; color: var(--text-muted); } +@media (max-width: 899.98px) { .notices { grid-template-columns: 1fr; } } +.notices h2 { font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--text-secondary); margin-bottom: 6px; } +.notices p + p { margin-top: 6px; } +.notices a { color: var(--text-secondary); } +.disclaimer + .notices { margin-top: var(--space-5); border-top: 0; padding-top: 0; } +.copyright { grid-column: 1 / -1; display: flex; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; padding-top: var(--space-4); border-top: 1px solid var(--border); } + +/* guided tour (driver.js, assets/js) */ +.driver-popover.cx-tour { font-family: 'IBM Plex Sans', system-ui, sans-serif; border-radius: 12px; padding: 16px 18px; max-width: 340px; box-shadow: var(--shadow-popover); color: var(--text-secondary); } +.driver-popover.cx-tour .driver-popover-title { font-family: inherit; font-size: 15px; font-weight: 600; color: var(--text); } +.driver-popover.cx-tour .driver-popover-description { font-family: inherit; font-size: 13px; line-height: 1.55; color: var(--text-secondary); margin-top: 4px; } +.driver-popover.cx-tour .driver-popover-progress-text { font-family: inherit; font-size: 12px; color: var(--text-muted); } +.driver-popover.cx-tour .driver-popover-footer button { font-family: inherit; font-size: 13px; font-weight: 500; text-shadow: none; border: 1px solid var(--brand); color: var(--brand); background: var(--surface); border-radius: var(--radius-control); padding: 5px 12px; } +.driver-popover.cx-tour .driver-popover-footer .driver-popover-next-btn { background: var(--brand); color: #fff; } +.driver-popover.cx-tour .driver-popover-close-btn { color: var(--text-muted); } + +@media print { + body { background: #fff; } + .help-btn, .menu, .secnav, .hint-pop, .toolbar, .more, .driver-popover, .driver-overlay, .driver-active-element { display: none !important; } + .app-header { height: auto; border-bottom: 0; } + .shell { display: block; max-width: none; padding: 0; } + .card { box-shadow: none; border: 1px solid var(--border); } + section.block, details.grp, .kpi { break-inside: avoid; } + tr.detail[hidden], table.alts tr[hidden] { display: table-row !important; } +} diff --git a/cloudexit/assets/css/style.css b/cloudexit/assets/css/style.css deleted file mode 100644 index 0835fdc..0000000 --- a/cloudexit/assets/css/style.css +++ /dev/null @@ -1,1107 +0,0 @@ -/* ======================================================== - Base: Variables - ======================================================== */ -:root { - /* Blue */ - --blue-100: #dbe6fe; - --blue-800: #1e4baf; - - /* Green */ - --green-100: #dcfce7; - --green-600: #16a34a; - --green-700: #047854; - - /* Neutral */ - --neutral-50: #f9fbfb; - --neutral-100: #f3f6f6; - --neutral-200: #e5ebeb; - --neutral-300: #d1dbdb; - --neutral-400: #9cafae; - --neutral-600: #4b6361; - --neutral-800: #1f3735; - --neutral-900: #112726; - - /* Primary */ - --primary-600: #0d948b; - --primary-800: #115e59; - --primary-950: #042f2c; - - /* Red */ - --red-50: #fef2f2; - --red-100: #fee2e2; - --red-700: #b91c1c; - --red-800: #991b1b; - - /* Yellow */ - --yellow-100: #fee4c7; - --yellow-850: #92400e; - /* Color Palette */ - --white: #fff; - /* Transition Defaults */ - --transition-speed: 0.3s; - - /* Font Sizes */ - --text-heading-2: 24px; - --text-heading-3: 20px; - --text-body: 16px; - --text-label: 14px; - --text-label-small: 12px; - /* Radius */ - --rounded-md: 12px; - --rounded-sm: 8px; - --rounded-xs: 4px; - /* Sidebar */ -} - -/* ======================================================== - Base: Reset - ======================================================== */ -* { - margin: 0; - padding: 0; - -webkit-box-sizing: border-box; - box-sizing: border-box; -} - -body { - font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; - line-height: 1.375; - font-weight: 400; - color: var(--neutral-900); - background: var(--neutral-100); - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-font-smoothing: antialiased; -} - -img { - max-width: 100%; - height: auto; -} - -ul { - list-style: none; - padding: 0; - margin: 0; -} - -.divider-border { - height: 1px; - background: var(--neutral-200); -} - -.container { -} - -/* ======================================================== - Base: Typography - ======================================================== */ -a { - text-decoration: none; - -webkit-transition: all var(--transition-speed) ease; - -o-transition: all var(--transition-speed) ease; - transition: all var(--transition-speed) ease; -} - -a:hover { - text-decoration: none !important; -} - - -h2, -h3 { - color: var(--neutral-900); - font-weight: 500; -} - - -h2, -h3, -li, -p { - margin: 0; -} - - -h2 { - font-size: var(--text-heading-2); - line-height: 1.33; -} - - -h3 { - font-size: var(--text-heading-3); - line-height: 1.4; -} - - -body, -p { - font-size: var(--text-body); - line-height: 1.375; -} - -.label, -label { - font-size: var(--text-label); - line-height: 1.43; -} - -.small, -label.small { - font-size: var(--text-label-small); - line-height: 1.33; -} - -.bg-white { - background: var(--white); -} - -a*:active, -a*:focus { - outline: 0; - border: 0; -} - -main { - padding-bottom: 32px; - padding-top: 88px; -} - -/* Scrollbars */ -::-webkit-scrollbar { - width: 5px; - border-radius: var(--rounded-sm); -} - -::-webkit-scrollbar-track { - background: var(--neutral-100); - border-radius: var(--rounded-sm); -} - -::-webkit-scrollbar-thumb { - background: var(--primary-600); - border-radius: var(--rounded-sm); -} - -::-webkit-scrollbar-thumb:hover { - background: var(--primary-800); - border-radius: var(--rounded-sm); -} - -.dropdown-item span { - font-size: var(--text-label); - font-weight: 400; - color: var(--neutral-900); -} - -.dropdown-item { - padding-top: 8px; - padding-bottom: 8px; -} - -.dropdown-menu { - padding: 0; -} - -.dropdown-menu li a { - line-height: 1.5; -} - -/* ======================================================== - Layout: Shared Cards And Charts - ======================================================== */ - -.chart-card-head { - padding: 16px 24px; -} - -.chart-card-head h3 { - margin-bottom: 5px; - color: var(--neutral-900); -} -.chart-card-head h6 { - color: var(--neutral-600); - font-size: var(--text-label); - font-weight: 400; -} -.chart-card-head h6 span { - display: inline-block; - font-weight: 500; -} - -.risk-header { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - /* padding: 16px 24px; */ -} - -.risk-dashboard { - display: flex; - flex-direction: column; - height: 100%; -} - -.risk-count h2 { - color: var(--neutral-600); - font-size: var(--text-label); - font-weight: 400; -} - -.risk-count .count { - font-size: var(--text-heading-3); - font-weight: 500; - color: var(--neutral-900); - margin: 0; -} - -.chart-container { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - flex: 1; -} - -.chart-box { - height: 350px; -} - -.chart-empty-state { - flex: 1; - min-height: 220px; - display: flex; - align-items: center; - justify-content: center; - text-align: center; -} - -.chart-empty-state-inner { - display: flex; - flex-direction: column; - align-items: center; - gap: 10px; -} - -.chart-empty-state i { - font-size: 28px; - line-height: 1; - color: var(--neutral-900); -} - -.chart-empty-state p { - margin: 0; - color: var(--neutral-700); -} - -.alt-tech-empty-state { - min-height: 260px; -} - -.scoring-empty-state { - min-height: 300px; -} - -.scoring-card { - overflow: hidden; -} - -.scoring-inner { - display: flex; - justify-content: center; - align-items: center; - height: 100%; - min-height: 300px; -} - -.chart-wrapper { - max-width: 350px; - overflow: hidden; - position: relative; - border-radius: 12px; - mask-image: linear-gradient(to bottom, black 85%, transparent 100%); - -webkit-mask-image: linear-gradient(to bottom, black 85%, transparent 100%); -} - -.scoring { - width: 100%; - max-width: 420px; - height: 320px !important; - margin: 0 auto; -} - -.form-title { - font-size: var(--text-heading-3); - font-weight: 500; - line-height: 1.33; - color: var(--neutral-900); -} - -.shadow-s { - -webkit-box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1); - box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1); -} - -.input { - border: 1px solid var(--neutral-300); -} - -/* ======================================================== - Components: Buttons - ======================================================== */ - -.dropdown-toggle::after { - float: right; - margin-top: 8px; -} - -/* ======================================================== - Components: Summary, Scoring, Resources, Alt Tech Cards - ======================================================== */ -::-webkit-input-placeholder { - color: var(--neutral-600); - font-size: var(--text-body); - font-style: normal; - font-weight: 400; - line-height: 38px; -} - -::-moz-placeholder { - color: var(--neutral-600); - font-size: var(--text-body); - font-style: normal; - font-weight: 400; - line-height: 38px; -} - -.resource-card { - border: 1px solid var(--neutral-200); -} - -.resource-card h3 { - font-weight: 500; - min-height: 44px; - font-size: var(--text-label); - font-size: 16px; - font-weight: 500; - font-stretch: normal; - line-height: 1.38; - color: var(--neutral-900); -} - -.resource-card h6 { - font-size: var(--text-label); - font-weight: 400; - margin: 0; -} - -.resource-card { - height: 100%; -} - -.resource-card p { - margin-top: 16px; - margin-bottom: 16px; -} - -.resource-card img { - max-width: 100%; - margin: 0 0 16px; - height: 32px; -} - -.resource-icon-placeholder { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - margin: 0 0 16px; - border-radius: 7px; - background: #e3e7ea; - color: #4a5560; - font-size: 13px; - font-weight: 600; - line-height: 1; -} - -.gapy-3 { - gap: 16px 0; -} - -.alttech-card { - border: 1px solid var(--neutral-200); - height: 100%; -} - -.alttech-title { -} - -.alttech-title h5 { - font-size: var(--text-body); - margin: 0; - color: var(--neutral-900); -} - -.alttech-title h6 { - font-size: var(--text-label-small); - font-weight: 400; -} - -.green-700 { - color: var(--green-700); -} - -.alttech-text p { - /*overflow: hidden;*/ - /*display: -webkit-box;*/ - font-size: 14px; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -} - -.sync-status { - gap: 28px 0; -} - -.sync-status-text { -} - -.sync-status-text h5 { - margin: 0; - font-size: var(--text-body); - color: var(--neutral-900); -} - -.sync-status-text h6 { - color: var(--neutral-600); - font-size: var(--text-label); - margin-bottom: 5px; - font-weight: 400; -} - -.sync-status-icon span{ - border-radius: 8px; - background:rgba(5, 81, 96, 0.10); - height: 32px; - width: 32px; - display: flex; - align-items: center; - justify-content: center; -} - -.view-more button { - background: transparent; - border: 0; -} - -/* ======================================================== - Components: Risk Table - ======================================================== */ -.risk-title-cell { - position: relative; - padding-left: 40px !important; - cursor: pointer; -} - -.chevron-icon { - position: absolute; - left: 16px; - top: 50%; - -webkit-transform: translateY(-50%); - -ms-transform: translateY(-50%); - transform: translateY(-50%); - font-size: var(--text-label-small); - color: var(--neutral-600); - -webkit-transition: -webkit-transform 0.2s ease; - transition: -webkit-transform 0.2s ease; - -o-transition: transform 0.2s ease; - transition: transform 0.2s ease; - transition: - transform 0.2s ease, - -webkit-transform 0.2s ease; -} - -.risk-row.expanded .chevron-icon { - -webkit-transform: translateY(-50%) rotate(90deg); - -ms-transform: translateY(-50%) rotate(90deg); - transform: translateY(-50%) rotate(90deg); -} - -.expandable-content { - display: none; - background-color: #f5f5f5; -} - -.expandable-content.show { - display: table-row; -} - -.expandable-content td { - padding-left: 20px; -} - -.risk-table-container .table tr th { - font-weight: 500; - font-size: var(--text-label); -} - -.risk-table-container .table th:first-child, -.risk-table-container .table td:first-child { - width: auto !important; -} - -.risk-table-container .table tr th:nth-child(3), -.risk-table-container .table tr th:nth-child(4) { - text-align: center; -} - -.impacted-count { - text-align: center; -} - -.description { - background: var(--neutral-200); - padding-top: 12px; - padding-bottom: 12px; -} - -.description-section { - margin-bottom: 10px; -} - -.risk-title { - color: var(--neutral-900); -} - -.section-label { - font-size: var(--text-label-small); - font-weight: 400; - color: var(--neutral-600); -} - -.section-content { - font-size: var(--text-label); -} - -.impacted-resources-content { - font-size: var(--text-label); -} - -.severity-badge { - color: var(--colors-red-800); - font-size: var(--text-label); - font-weight: 500; - padding: 3px 12px; - border-radius: var(--rounded-md); -} - -.severity-high { - background-color: var(--red-100); - color: var(--red-800); -} - -.severity-medium { - background: var(--yellow-100); - color: var(--yellow-850); -} - -.severity-low { - background: var(--blue-100); - color: var(--blue-800); -} - -.btn { - border-radius: 6px; - line-height: 40px; - padding: 0 16px; - font-weight: 500; - -webkit-transition: 0.4s; - -o-transition: 0.4s; - transition: 0.4s; -} - -.btn-primary { - background: var(--primary-800); - color: var(--white); -} - -.btn-outline-primary { - color: var(--primary-800); -} - -.btn-light { - background: var(--neutral-100); - border: 1px solid var(--neutral-100); - color: var(--neutral-600); -} - -.btn-outline-primary, -.btn-primary { - border: 1px solid var(--primary-800); -} - -.btn-sm { - font-size: var(--text-label); - font-weight: 500; - line-height: 34px; -} - -.btn-primary:hover { - background: var(--primary-950); - border-color: var(--primary-950); -} - -.btn-primary:hover svg:not(.filter-icon) path { - fill: var(--white); -} - -.btn-outline-primary:focus svg:not path, -.btn-outline-primary:active svg:not path, -.btn-outline-primary:hover svg:not path { - fill: var(--white); -} - -.btn-outline-primary:hover svg.filter-icon path { - stroke: var(--white); -} - -.btn-outline-primary:active, -.btn-outline-primary:hover { - background: var(--primary-800) !important; - border-color: var(--primary-800) !important; - color: var(--white) !important; -} -.btn-outline-primary:active svg path, -.btn-outline-primary:hover svg path { - fill: white; -} - -.btn-light:hover { - background: var(--neutral-200); - border-color: var(--neutral-200); -} - -.dropdown-toggle::after { - border: 0; - display: none; -} - -.dropdown-toggle { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - gap: 8px; - padding-left: 16px; - padding-right: 16px; -} - -.btn-sm.dropdown-toggle { - padding-left: 12px; - padding-right: 12px; -} - -.btn:focus-visible, -.btn.show:focus-visible, -.btn:first-child:active:focus-visible, -:not(.btn-check) + .btn:active:focus-visible { - -webkit-box-shadow: none !important; - box-shadow: none !important; -} - -.btn-primary, -.btn-outline-primary { - /* --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-focus-shadow-rgb: 13, 110, 253; - --bs-btn-disabled-bg: transparent; - --bs-btn-active-color: var(--neutral-white); - --bs-btn-hover-color: var(--neutral-white); - --bs-gradient: none; */ -} - -.btn-outline-primary { - --bs-btn-color: var(--primary-800); - --bs-btn-border-color: var(--primary-800); - --bs-btn-hover-color: var(--white); - --bs-btn-hover-bg: var(--primary-800); - --bs-btn-hover-border-color: var(--primary-800); - --bs-btn-active-color: var(--white); - --bs-btn-active-bg: var(--primary-800); - --bs-btn-active-border-color: var(--primary-800); - --bs-btn-disabled-color: var(--neutral-400); - --bs-btn-disabled-border-color: var(--neutral-300); - --bs-btn-focus-shadow-rgb: 17, 94, 89; -} - -.btn-outline-primary.show, -.btn-check:checked + .btn-outline-primary, -.btn-check:active + .btn-outline-primary, -.btn-outline-primary:focus, -.btn-outline-primary:focus-visible { - background: var(--primary-800) !important; - border-color: var(--primary-800) !important; - color: var(--white) !important; -} - -.btn-outline-primary.show svg.filter-icon path, -.btn-check:checked + .btn-outline-primary svg.filter-icon path, -.btn-check:active + .btn-outline-primary svg.filter-icon path, -.btn-outline-primary:focus svg.filter-icon path, -.btn-outline-primary:focus-visible svg.filter-icon path { - stroke: var(--white); -} - -.btn-primary.disabled, -.btn-primary:disabled { - border-color: var(--neutral-400); - background: var(--neutral-400); - color: var(--white); -} - -.btn-outline-primary.disabled, -.btn-outline-primary:disabled { - border-color: var(--neutral-300); - color: var(--neutral-400); -} - -/* ======================================================== - Components: Filter Toggles - ======================================================== */ -.toggle-switch { - position: relative; - width: 44px; - height: 24px; -} - -.toggle-switch input[type="checkbox"] { - opacity: 0; - width: 0; - height: 0; -} - -.toggle-slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: var(--neutral-200); - -webkit-transition: 0.3s ease; - -o-transition: 0.3s ease; - transition: 0.3s ease; - border-radius: var(--rounded-md); -} - -.toggle-slider:before { - position: absolute; - content: ""; - height: 20px; - width: 20px; - left: 2px; - bottom: 2px; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Ccircle cx='10' cy='10' r='9.25' fill='white' stroke='%23D1DBDB' stroke-width='1.5'/%3E%3Cpath d='M6.5 6.5L13.5 13.5M13.5 6.5L6.5 13.5' stroke='%234B6361' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E"); -} - -.toggle-slider:before { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Ccircle cx='10' cy='10' r='9.25' fill='white' stroke='%23D1DBDB' stroke-width='1.5'/%3E%3Cpath d='M6.5 6.5L13.5 13.5M13.5 6.5L6.5 13.5' stroke='%234B6361' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: center; - background-size: 20px 20px; -} - -input:checked + .toggle-slider { - background-color: var(--primary-800); -} - -input:checked + .toggle-slider:before { - -webkit-transform: translateX(20px); - -ms-transform: translateX(20px); - transform: translateX(20px); - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Ccircle cx='10' cy='10' r='9.25' fill='white' stroke='%230D948B' stroke-width='1.5'/%3E%3Cpath d='M5.5 10.5L8.5 13.5L14.5 7.5' stroke='%230D948B' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: center; - background-size: 20px 20px; -} - -/* ======================================================== - Components: Tables - ======================================================== */ -table:not("#assessmentsTable") { - table-layout: fixed; - width: 100%; -} - -.table > :not(caption) > * > * { - color: var(--neutral-900); - font-size: var(--text-body); - padding-top: 16px; - padding-bottom: 16px; - border-color: var(--neutral-200); -} - -tr:last-child td { - border-bottom: 0; -} - -.risk-table-container { - overflow-x: auto; - -webkit-overflow-scrolling: touch; -} - -.risk-table-container table { - min-width: 420px; -} - -.risk-table-container::-webkit-scrollbar { - height: 20px; -} - -.risk-table-container::-webkit-scrollbar-track { - background: var(--white); -} - -.risk-table-container::-webkit-scrollbar-thumb { - background: var(--neutral-300); - border-radius: 20px; -} - -.risk-table-container::-webkit-scrollbar-thumb:hover { - background: var(--neutral-600); -} - -th:first-child, -td:first-child { - width: 50px !important; - font-weight: 500; -} - -td .number, -td button { - margin-left: auto; - margin-right: auto; -} - -/* ======================================================== - Layout: Main Content And Forms - ======================================================== */ - -#main-content { - height: 100%; - width: 100%; - overflow: hidden; - padding: 20px 60px 20px 60px; - transition: all 0.3s linear; - -webkit-transition: all 0.3s linear; -} - -.visit span { - font-size: 14px; - word-break: break-all; -} - -.form-control, -.form-select { - color: var(--neutral-600); - font-size: var(--text-body); - font-style: normal; - font-weight: 400; - border-radius: 8px; - border: 1px solid var(--neutral-300); - background: var(--white); - -webkit-transition: border-color 0.3s ease, box-shadow 0.3s ease; - -o-transition: border-color 0.3s ease, box-shadow 0.3s ease; - transition: border-color 0.3s ease, box-shadow 0.3s ease; - -webkit-box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); -} - -.form-select { - -webkit-appearance: none; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 6L8 10L12 6' stroke='%23115E59' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 12px center; - background-size: 16px 16px; - padding-right: 40px; -} - -.form-control:focus, -.form-select:focus { - border-color: var(--primary-800); - box-shadow: 0 0 0 0.2rem rgba(17, 94, 89, 0.12); -} - -.custom-search { - min-width: 240px; - height: 36px; - border-radius: 8px; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M9 3.5C5.96243 3.5 3.5 5.96243 3.5 9C3.5 12.0376 5.96243 14.5 9 14.5C10.519 14.5 11.893 13.8852 12.8891 12.8891C13.8852 11.893 14.5 10.519 14.5 9C14.5 5.96243 12.0376 3.5 9 3.5ZM2 9C2 5.13401 5.13401 2 9 2C12.866 2 16 5.13401 16 9C16 10.6625 15.4197 12.1906 14.4517 13.3911L17.7803 16.7197C18.0732 17.0126 18.0732 17.4874 17.7803 17.7803C17.4874 18.0732 17.0126 18.0732 16.7197 17.7803L13.3911 14.4517C12.1906 15.4197 10.6625 16 9 16C5.13401 16 2 12.866 2 9Z' fill='%236B807F'/%3E%3C/svg%3E") !important; - background-repeat: no-repeat !important; - background-position: 16px center !important; - padding: 0 16px 0 48px !important; - width: 400px; -} - -.btn-clear:hover { - text-decoration: underline; -} - -.alt-tech-card { - background: #f5f5f5; - border-radius: 10px; -} - -.alt-tech-card h3 { - font-size: 18px; - margin: 0; -} - -/* ======================================================== - Components: Alternative Technology Status And Hints - ======================================================== */ - -.verified { - background: var(--green-600); - color: var(--white); -} -.green-100 { - background: var(--green-100); -} -.green-700 { - color: var(--green-700); -} -.tags span { - background: var(--neutral-100); - padding: 4px 8px; - border-radius: 6px; - font-size: var(--text-label); - color: var(--neutral-700); - font-weight: 500; -} -.verified span { - font-size: var(--text-label); -} -.red-700 { - color: var(--red-700); -} -.red-50 { - background: var(--red-50); -} - -.info-hint-box { - position: relative; - display: inline-block; - margin-left: 6px; - cursor: pointer; -} -.info-hint-box i { - font-size: 16px; - color: var(--primary-600, #007bff); - transition: color 0.2s; -} -.info-hint-box:hover i { - color: var(--primary-800, #0056b3); -} -.hint-hoverbox { - display: none; - position: absolute; - top: 28px; - right: 0; - z-index: 1000; - background: #fff; - border: 1px solid var(--neutral-200, #e5e7eb); - border-radius: 8px; - padding: 12px 14px; - width: 280px; - box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1); -} -.info-hint-box:hover .hint-hoverbox { - display: block; -} -.hint-hoverbox { - opacity: 0; - visibility: hidden; - transition: all 0.2s ease-in-out; -} -.info-hint-box:hover .hint-hoverbox { - opacity: 1; - visibility: visible; -} -.hint-hoverbox h5 { - font-size: 14px; - font-weight: 600; - margin-bottom: 6px; -} -.hint-hoverbox p { - font-size: 13px; - color: #555; - margin: 0; - line-height: 1.4; -} - -/* ======================================================== - Responsive - ======================================================== */ - -@media (max-width: 991px) { - - .alt-tech-card div { - width: 100%; - /* justify-content: flex-end; */ - } - - .alt-tech-card { - -ms-flex-wrap: wrap; - flex-wrap: wrap; - gap: 16px; - } - - .chart-card-head { - padding-left: 16px; - padding-right: 16px; - } - - #main-content { - margin-left: 0; - } -} - -@media (max-width: 767px) { - main { - padding-top: 24px; - } - - .btn { - padding-left: 12px; - padding-right: 12px; - } - - main { - padding-top: 88px; - } - -} - -@media (max-width: 575px) { - -} diff --git a/cloudexit/assets/img/csp/alibaba.svg b/cloudexit/assets/img/csp/alibaba.svg new file mode 100644 index 0000000..f352ee9 --- /dev/null +++ b/cloudexit/assets/img/csp/alibaba.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/cloudexit/assets/img/csp/aws.svg b/cloudexit/assets/img/csp/aws.svg new file mode 100644 index 0000000..08b8262 --- /dev/null +++ b/cloudexit/assets/img/csp/aws.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/cloudexit/assets/img/csp/azure.svg b/cloudexit/assets/img/csp/azure.svg new file mode 100644 index 0000000..61cb535 --- /dev/null +++ b/cloudexit/assets/img/csp/azure.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cloudexit/assets/img/csp/gcp.svg b/cloudexit/assets/img/csp/gcp.svg new file mode 100644 index 0000000..bc3b271 --- /dev/null +++ b/cloudexit/assets/img/csp/gcp.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/cloudexit/assets/img/logo/README.md b/cloudexit/assets/img/logo/README.md new file mode 100644 index 0000000..13ca4ea --- /dev/null +++ b/cloudexit/assets/img/logo/README.md @@ -0,0 +1,3 @@ +## EscapeCloud logo files + +The EscapeCloud logo files in this directory are not covered by this project's LICENSE. All rights reserved. diff --git a/cloudexit/assets/js/driver.js.LICENSE.txt b/cloudexit/assets/js/driver.js.LICENSE.txt new file mode 100644 index 0000000..7ceaeef --- /dev/null +++ b/cloudexit/assets/js/driver.js.LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) Kamran Ahmed + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/cloudexit/assets/js/driver.js.iife.js b/cloudexit/assets/js/driver.js.iife.js new file mode 100644 index 0000000..87acd6e --- /dev/null +++ b/cloudexit/assets/js/driver.js.iife.js @@ -0,0 +1,2 @@ +this.driver=this.driver||{};this.driver.js=function(D){"use strict";let F={};function z(e={}){F={animate:!0,allowClose:!0,overlayOpacity:.7,smoothScroll:!1,disableActiveInteraction:!1,showProgress:!1,stagePadding:10,stageRadius:5,popoverOffset:10,showButtons:["next","previous","close"],disableButtons:[],overlayColor:"#000",...e}}function a(e){return e?F[e]:F}function W(e,o,t,i){return(e/=i/2)<1?t/2*e*e+o:-t/2*(--e*(e-2)-1)+o}function q(e){const o='a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])';return e.flatMap(t=>{const i=t.matches(o),p=Array.from(t.querySelectorAll(o));return[...i?[t]:[],...p]}).filter(t=>getComputedStyle(t).pointerEvents!=="none"&&ae(t))}function V(e){if(!e||se(e))return;const o=a("smoothScroll");e.scrollIntoView({behavior:!o||re(e)?"auto":"smooth",inline:"center",block:"center"})}function re(e){if(!e||!e.parentElement)return;const o=e.parentElement;return o.scrollHeight>o.clientHeight}function se(e){const o=e.getBoundingClientRect();return o.top>=0&&o.left>=0&&o.bottom<=(window.innerHeight||document.documentElement.clientHeight)&&o.right<=(window.innerWidth||document.documentElement.clientWidth)}function ae(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)}let N={};function b(e,o){N[e]=o}function l(e){return e?N[e]:N}function K(){N={}}let E={};function O(e,o){E[e]=o}function _(e){var o;(o=E[e])==null||o.call(E)}function ce(){E={}}function le(e,o,t,i){let p=l("__activeStagePosition");const n=p||t.getBoundingClientRect(),f=i.getBoundingClientRect(),w=W(e,n.x,f.x-n.x,o),r=W(e,n.y,f.y-n.y,o),v=W(e,n.width,f.width-n.width,o),s=W(e,n.height,f.height-n.height,o);p={x:w,y:r,width:v,height:s},Y(p),b("__activeStagePosition",p)}function X(e){if(!e)return;const o=e.getBoundingClientRect(),t={x:o.x,y:o.y,width:o.width,height:o.height};b("__activeStagePosition",t),Y(t)}function de(){const e=l("__activeStagePosition"),o=l("__overlaySvg");if(!e)return;if(!o){console.warn("No stage svg found.");return}const t=window.innerWidth,i=window.innerHeight;o.setAttribute("viewBox",`0 0 ${t} ${i}`)}function pe(e){const o=ue(e);document.body.appendChild(o),G(o,t=>{t.target.tagName==="path"&&_("overlayClick")}),b("__overlaySvg",o)}function Y(e){const o=l("__overlaySvg");if(!o){pe(e);return}const t=o.firstElementChild;if((t==null?void 0:t.tagName)!=="path")throw new Error("no path element found in stage svg");t.setAttribute("d",j(e))}function ue(e){const o=window.innerWidth,t=window.innerHeight,i=document.createElementNS("http://www.w3.org/2000/svg","svg");i.classList.add("driver-overlay","driver-overlay-animated"),i.setAttribute("viewBox",`0 0 ${o} ${t}`),i.setAttribute("xmlSpace","preserve"),i.setAttribute("xmlnsXlink","http://www.w3.org/1999/xlink"),i.setAttribute("version","1.1"),i.setAttribute("preserveAspectRatio","xMinYMin slice"),i.style.fillRule="evenodd",i.style.clipRule="evenodd",i.style.strokeLinejoin="round",i.style.strokeMiterlimit="2",i.style.zIndex="10000",i.style.position="fixed",i.style.top="0",i.style.left="0",i.style.width="100%",i.style.height="100%";const p=document.createElementNS("http://www.w3.org/2000/svg","path");return p.setAttribute("d",j(e)),p.style.fill=a("overlayColor")||"rgb(0,0,0)",p.style.opacity=`${a("overlayOpacity")}`,p.style.pointerEvents="auto",p.style.cursor="auto",i.appendChild(p),i}function j(e){const o=window.innerWidth,t=window.innerHeight,i=a("stagePadding")||0,p=a("stageRadius")||0,n=e.width+i*2,f=e.height+i*2,w=Math.min(p,n/2,f/2),r=Math.floor(Math.max(w,0)),v=e.x-i+r,s=e.y-i,c=n-r*2,d=f-r*2;return`M${o},0L0,0L0,${t}L${o},${t}L${o},0Z + M${v},${s} h${c} a${r},${r} 0 0 1 ${r},${r} v${d} a${r},${r} 0 0 1 -${r},${r} h-${c} a${r},${r} 0 0 1 -${r},-${r} v-${d} a${r},${r} 0 0 1 ${r},-${r} z`}function ve(){const e=l("__overlaySvg");e&&e.remove()}function fe(){const e=document.getElementById("driver-dummy-element");if(e)return e;let o=document.createElement("div");return o.id="driver-dummy-element",o.style.width="0",o.style.height="0",o.style.pointerEvents="none",o.style.opacity="0",o.style.position="fixed",o.style.top="50%",o.style.left="50%",document.body.appendChild(o),o}function Q(e){const{element:o}=e;let t=typeof o=="string"?document.querySelector(o):o;t||(t=fe()),ge(t,e)}function he(){const e=l("__activeElement"),o=l("__activeStep");e&&(X(e),de(),oe(e,o))}function ge(e,o){const i=Date.now(),p=l("__activeStep"),n=l("__activeElement")||e,f=!n||n===e,w=e.id==="driver-dummy-element",r=n.id==="driver-dummy-element",v=a("animate"),s=o.onHighlightStarted||a("onHighlightStarted"),c=(o==null?void 0:o.onHighlighted)||a("onHighlighted"),d=(p==null?void 0:p.onDeselected)||a("onDeselected"),m=a(),g=l();!f&&d&&d(r?void 0:n,p,{config:m,state:g}),s&&s(w?void 0:e,o,{config:m,state:g});const u=!f&&v;let h=!1;xe(),b("previousStep",p),b("previousElement",n),b("activeStep",o),b("activeElement",e);const P=()=>{if(l("__transitionCallback")!==P)return;const x=Date.now()-i,y=400-x<=400/2;o.popover&&y&&!h&&u&&(J(e,o),h=!0),a("animate")&&x<400?le(x,400,n,e):(X(e),c&&c(w?void 0:e,o,{config:a(),state:l()}),b("__transitionCallback",void 0),b("__previousStep",p),b("__previousElement",n),b("__activeStep",o),b("__activeElement",e)),window.requestAnimationFrame(P)};b("__transitionCallback",P),window.requestAnimationFrame(P),V(e),!u&&o.popover&&J(e,o),n.classList.remove("driver-active-element","driver-no-interaction"),n.removeAttribute("aria-haspopup"),n.removeAttribute("aria-expanded"),n.removeAttribute("aria-controls"),a("disableActiveInteraction")&&e.classList.add("driver-no-interaction"),e.classList.add("driver-active-element"),e.setAttribute("aria-haspopup","dialog"),e.setAttribute("aria-expanded","true"),e.setAttribute("aria-controls","driver-popover-content")}function we(){var e;(e=document.getElementById("driver-dummy-element"))==null||e.remove(),document.querySelectorAll(".driver-active-element").forEach(o=>{o.classList.remove("driver-active-element","driver-no-interaction"),o.removeAttribute("aria-haspopup"),o.removeAttribute("aria-expanded"),o.removeAttribute("aria-controls")})}function A(){const e=l("__resizeTimeout");e&&window.cancelAnimationFrame(e),b("__resizeTimeout",window.requestAnimationFrame(he))}function me(e){var r;if(!l("isInitialized")||!(e.key==="Tab"||e.keyCode===9))return;const i=l("__activeElement"),p=(r=l("popover"))==null?void 0:r.wrapper,n=q([...p?[p]:[],...i?[i]:[]]),f=n[0],w=n[n.length-1];if(e.preventDefault(),e.shiftKey){const v=n[n.indexOf(document.activeElement)-1]||w;v==null||v.focus()}else{const v=n[n.indexOf(document.activeElement)+1]||f;v==null||v.focus()}}function Z(e){var t;((t=a("allowKeyboardControl"))==null||t)&&(e.key==="Escape"?_("escapePress"):e.key==="ArrowRight"?_("arrowRightPress"):e.key==="ArrowLeft"&&_("arrowLeftPress"))}function G(e,o,t){const i=(n,f)=>{const w=n.target;e.contains(w)&&((!t||t(w))&&(n.preventDefault(),n.stopPropagation(),n.stopImmediatePropagation()),f==null||f(n))};document.addEventListener("pointerdown",i,!0),document.addEventListener("mousedown",i,!0),document.addEventListener("pointerup",i,!0),document.addEventListener("mouseup",i,!0),document.addEventListener("click",n=>{i(n,o)},!0)}function ye(){window.addEventListener("keyup",Z,!1),window.addEventListener("keydown",me,!1),window.addEventListener("resize",A),window.addEventListener("scroll",A)}function be(){window.removeEventListener("keyup",Z),window.removeEventListener("resize",A),window.removeEventListener("scroll",A)}function xe(){const e=l("popover");e&&(e.wrapper.style.display="none")}function J(e,o){var C,y;let t=l("popover");t&&document.body.removeChild(t.wrapper),t=Pe(),document.body.appendChild(t.wrapper);const{title:i,description:p,showButtons:n,disableButtons:f,showProgress:w,nextBtnText:r=a("nextBtnText")||"Next →",prevBtnText:v=a("prevBtnText")||"← Previous",progressText:s=a("progressText")||"{current} of {total}"}=o.popover||{};t.nextButton.innerHTML=r,t.previousButton.innerHTML=v,t.progress.innerHTML=s,i?(t.title.innerHTML=i,t.title.style.display="block"):t.title.style.display="none",p?(t.description.innerHTML=p,t.description.style.display="block"):t.description.style.display="none";const c=n||a("showButtons"),d=w||a("showProgress")||!1,m=(c==null?void 0:c.includes("next"))||(c==null?void 0:c.includes("previous"))||d;t.closeButton.style.display=c.includes("close")?"block":"none",m?(t.footer.style.display="flex",t.progress.style.display=d?"block":"none",t.nextButton.style.display=c.includes("next")?"block":"none",t.previousButton.style.display=c.includes("previous")?"block":"none"):t.footer.style.display="none";const g=f||a("disableButtons")||[];g!=null&&g.includes("next")&&(t.nextButton.disabled=!0,t.nextButton.classList.add("driver-popover-btn-disabled")),g!=null&&g.includes("previous")&&(t.previousButton.disabled=!0,t.previousButton.classList.add("driver-popover-btn-disabled")),g!=null&&g.includes("close")&&(t.closeButton.disabled=!0,t.closeButton.classList.add("driver-popover-btn-disabled"));const u=t.wrapper;u.style.display="block",u.style.left="",u.style.top="",u.style.bottom="",u.style.right="",u.id="driver-popover-content",u.setAttribute("role","dialog"),u.setAttribute("aria-labelledby","driver-popover-title"),u.setAttribute("aria-describedby","driver-popover-description");const h=t.arrow;h.className="driver-popover-arrow";const P=((C=o.popover)==null?void 0:C.popoverClass)||a("popoverClass")||"";u.className=`driver-popover ${P}`.trim(),G(t.wrapper,k=>{var M,R,I;const T=k.target,H=((M=o.popover)==null?void 0:M.onNextClick)||a("onNextClick"),$=((R=o.popover)==null?void 0:R.onPrevClick)||a("onPrevClick"),B=((I=o.popover)==null?void 0:I.onCloseClick)||a("onCloseClick");if(T.classList.contains("driver-popover-next-btn"))return H?H(e,o,{config:a(),state:l()}):_("nextClick");if(T.classList.contains("driver-popover-prev-btn"))return $?$(e,o,{config:a(),state:l()}):_("prevClick");if(T.classList.contains("driver-popover-close-btn"))return B?B(e,o,{config:a(),state:l()}):_("closeClick")},k=>!(t!=null&&t.description.contains(k))&&!(t!=null&&t.title.contains(k))&&typeof k.className=="string"&&k.className.includes("driver-popover")),b("popover",t);const S=((y=o.popover)==null?void 0:y.onPopoverRender)||a("onPopoverRender");S&&S(t,{config:a(),state:l()}),oe(e,o),V(u);const L=e.classList.contains("driver-dummy-element"),x=q([u,...L?[]:[e]]);x.length>0&&x[0].focus()}function U(){const e=l("popover");if(!(e!=null&&e.wrapper))return;const o=e.wrapper.getBoundingClientRect(),t=a("stagePadding")||0,i=a("popoverOffset")||0;return{width:o.width+t+i,height:o.height+t+i,realWidth:o.width,realHeight:o.height}}function ee(e,o){const{elementDimensions:t,popoverDimensions:i,popoverPadding:p,popoverArrowDimensions:n}=o;return e==="start"?Math.max(Math.min(t.top-p,window.innerHeight-i.realHeight-n.width),n.width):e==="end"?Math.max(Math.min(t.top-(i==null?void 0:i.realHeight)+t.height+p,window.innerHeight-(i==null?void 0:i.realHeight)-n.width),n.width):e==="center"?Math.max(Math.min(t.top+t.height/2-(i==null?void 0:i.realHeight)/2,window.innerHeight-(i==null?void 0:i.realHeight)-n.width),n.width):0}function te(e,o){const{elementDimensions:t,popoverDimensions:i,popoverPadding:p,popoverArrowDimensions:n}=o;return e==="start"?Math.max(Math.min(t.left-p,window.innerWidth-i.realWidth-n.width),n.width):e==="end"?Math.max(Math.min(t.left-(i==null?void 0:i.realWidth)+t.width+p,window.innerWidth-(i==null?void 0:i.realWidth)-n.width),n.width):e==="center"?Math.max(Math.min(t.left+t.width/2-(i==null?void 0:i.realWidth)/2,window.innerWidth-(i==null?void 0:i.realWidth)-n.width),n.width):0}function oe(e,o){const t=l("popover");if(!t)return;const{align:i="start",side:p="left"}=(o==null?void 0:o.popover)||{},n=i,f=e.id==="driver-dummy-element"?"over":p,w=a("stagePadding")||0,r=U(),v=t.arrow.getBoundingClientRect(),s=e.getBoundingClientRect(),c=s.top-r.height;let d=c>=0;const m=window.innerHeight-(s.bottom+r.height);let g=m>=0;const u=s.left-r.width;let h=u>=0;const P=window.innerWidth-(s.right+r.width);let S=P>=0;const L=!d&&!g&&!h&&!S;let x=f;if(f==="top"&&d?S=h=g=!1:f==="bottom"&&g?S=h=d=!1:f==="left"&&h?S=d=g=!1:f==="right"&&S&&(h=d=g=!1),f==="over"){const C=window.innerWidth/2-r.realWidth/2,y=window.innerHeight/2-r.realHeight/2;t.wrapper.style.left=`${C}px`,t.wrapper.style.right="auto",t.wrapper.style.top=`${y}px`,t.wrapper.style.bottom="auto"}else if(L){const C=window.innerWidth/2-(r==null?void 0:r.realWidth)/2,y=10;t.wrapper.style.left=`${C}px`,t.wrapper.style.right="auto",t.wrapper.style.bottom=`${y}px`,t.wrapper.style.top="auto"}else if(h){const C=Math.min(u,window.innerWidth-(r==null?void 0:r.realWidth)-v.width),y=ee(n,{elementDimensions:s,popoverDimensions:r,popoverPadding:w,popoverArrowDimensions:v});t.wrapper.style.left=`${C}px`,t.wrapper.style.top=`${y}px`,t.wrapper.style.bottom="auto",t.wrapper.style.right="auto",x="left"}else if(S){const C=Math.min(P,window.innerWidth-(r==null?void 0:r.realWidth)-v.width),y=ee(n,{elementDimensions:s,popoverDimensions:r,popoverPadding:w,popoverArrowDimensions:v});t.wrapper.style.right=`${C}px`,t.wrapper.style.top=`${y}px`,t.wrapper.style.bottom="auto",t.wrapper.style.left="auto",x="right"}else if(d){const C=Math.min(c,window.innerHeight-r.realHeight-v.width);let y=te(n,{elementDimensions:s,popoverDimensions:r,popoverPadding:w,popoverArrowDimensions:v});t.wrapper.style.top=`${C}px`,t.wrapper.style.left=`${y}px`,t.wrapper.style.bottom="auto",t.wrapper.style.right="auto",x="top"}else if(g){const C=Math.min(m,window.innerHeight-(r==null?void 0:r.realHeight)-v.width);let y=te(n,{elementDimensions:s,popoverDimensions:r,popoverPadding:w,popoverArrowDimensions:v});t.wrapper.style.left=`${y}px`,t.wrapper.style.bottom=`${C}px`,t.wrapper.style.top="auto",t.wrapper.style.right="auto",x="bottom"}L?t.arrow.classList.add("driver-popover-arrow-none"):Ce(n,x,e)}function Ce(e,o,t){const i=l("popover");if(!i)return;const p=t.getBoundingClientRect(),n=U(),f=i.arrow,w=n.width,r=window.innerWidth,v=p.width,s=p.left,c=n.height,d=window.innerHeight,m=p.top,g=p.height;f.className="driver-popover-arrow";let u=o,h=e;o==="top"?(s+v<=0?(u="right",h="end"):s+v-w<=0&&(u="top",h="start"),s>=r?(u="left",h="end"):s+w>=r&&(u="top",h="end")):o==="bottom"?(s+v<=0?(u="right",h="start"):s+v-w<=0&&(u="bottom",h="start"),s>=r?(u="left",h="start"):s+w>=r&&(u="bottom",h="end")):o==="left"?(m+g<=0?(u="bottom",h="end"):m+g-c<=0&&(u="left",h="start"),m>=d?(u="top",h="end"):m+c>=d&&(u="left",h="end")):o==="right"&&(m+g<=0?(u="bottom",h="start"):m+g-c<=0&&(u="right",h="start"),m>=d?(u="top",h="start"):m+c>=d&&(u="right",h="end")),u?(f.classList.add(`driver-popover-arrow-side-${u}`),f.classList.add(`driver-popover-arrow-align-${h}`)):f.classList.add("driver-popover-arrow-none")}function Pe(){const e=document.createElement("div");e.classList.add("driver-popover");const o=document.createElement("div");o.classList.add("driver-popover-arrow");const t=document.createElement("header");t.id="driver-popover-title",t.classList.add("driver-popover-title"),t.style.display="none",t.innerText="Popover Title";const i=document.createElement("div");i.id="driver-popover-description",i.classList.add("driver-popover-description"),i.style.display="none",i.innerText="Popover description is here";const p=document.createElement("button");p.type="button",p.classList.add("driver-popover-close-btn"),p.setAttribute("aria-label","Close"),p.innerHTML="×";const n=document.createElement("footer");n.classList.add("driver-popover-footer");const f=document.createElement("span");f.classList.add("driver-popover-progress-text"),f.innerText="";const w=document.createElement("span");w.classList.add("driver-popover-navigation-btns");const r=document.createElement("button");r.type="button",r.classList.add("driver-popover-prev-btn"),r.innerHTML="← Previous";const v=document.createElement("button");return v.type="button",v.classList.add("driver-popover-next-btn"),v.innerHTML="Next →",w.appendChild(r),w.appendChild(v),n.appendChild(f),n.appendChild(w),e.appendChild(p),e.appendChild(o),e.appendChild(t),e.appendChild(i),e.appendChild(n),{wrapper:e,arrow:o,title:t,description:i,footer:n,previousButton:r,nextButton:v,closeButton:p,footerButtons:w,progress:f}}function Se(){var o;const e=l("popover");e&&((o=e.wrapper.parentElement)==null||o.removeChild(e.wrapper))}const Le="";function ke(e={}){z(e);function o(){a("allowClose")&&v()}function t(){const s=l("activeIndex"),c=a("steps")||[];if(typeof s=="undefined")return;const d=s+1;c[d]?r(d):v()}function i(){const s=l("activeIndex"),c=a("steps")||[];if(typeof s=="undefined")return;const d=s-1;c[d]?r(d):v()}function p(s){(a("steps")||[])[s]?r(s):v()}function n(){var h;if(l("__transitionCallback"))return;const c=l("activeIndex"),d=l("__activeStep"),m=l("__activeElement");if(typeof c=="undefined"||typeof d=="undefined"||typeof l("activeIndex")=="undefined")return;const u=((h=d.popover)==null?void 0:h.onPrevClick)||a("onPrevClick");if(u)return u(m,d,{config:a(),state:l()});i()}function f(){var u;if(l("__transitionCallback"))return;const c=l("activeIndex"),d=l("__activeStep"),m=l("__activeElement");if(typeof c=="undefined"||typeof d=="undefined")return;const g=((u=d.popover)==null?void 0:u.onNextClick)||a("onNextClick");if(g)return g(m,d,{config:a(),state:l()});t()}function w(){l("isInitialized")||(b("isInitialized",!0),document.body.classList.add("driver-active",a("animate")?"driver-fade":"driver-simple"),ye(),O("overlayClick",o),O("escapePress",o),O("arrowLeftPress",n),O("arrowRightPress",f))}function r(s=0){var H,$,B,M,R,I,ie,ne;const c=a("steps");if(!c){console.error("No steps to drive through"),v();return}if(!c[s]){v();return}b("__activeOnDestroyed",document.activeElement),b("activeIndex",s);const d=c[s],m=c[s+1],g=c[s-1],u=((H=d.popover)==null?void 0:H.doneBtnText)||a("doneBtnText")||"Done",h=a("allowClose"),P=typeof(($=d.popover)==null?void 0:$.showProgress)!="undefined"?(B=d.popover)==null?void 0:B.showProgress:a("showProgress"),L=(((M=d.popover)==null?void 0:M.progressText)||a("progressText")||"{{current}} of {{total}}").replace("{{current}}",`${s+1}`).replace("{{total}}",`${c.length}`),x=((R=d.popover)==null?void 0:R.showButtons)||a("showButtons"),C=["next","previous",...h?["close"]:[]].filter(_e=>!(x!=null&&x.length)||x.includes(_e)),y=((I=d.popover)==null?void 0:I.onNextClick)||a("onNextClick"),k=((ie=d.popover)==null?void 0:ie.onPrevClick)||a("onPrevClick"),T=((ne=d.popover)==null?void 0:ne.onCloseClick)||a("onCloseClick");Q({...d,popover:{showButtons:C,nextBtnText:m?void 0:u,disableButtons:[...g?[]:["previous"]],showProgress:P,progressText:L,onNextClick:y||(()=>{m?r(s+1):v()}),onPrevClick:k||(()=>{r(s-1)}),onCloseClick:T||(()=>{v()}),...(d==null?void 0:d.popover)||{}}})}function v(s=!0){const c=l("__activeElement"),d=l("__activeStep"),m=l("__activeOnDestroyed"),g=a("onDestroyStarted");if(s&&g){const P=!c||(c==null?void 0:c.id)==="driver-dummy-element";g(P?void 0:c,d,{config:a(),state:l()});return}const u=(d==null?void 0:d.onDeselected)||a("onDeselected"),h=a("onDestroyed");if(document.body.classList.remove("driver-active","driver-fade","driver-simple"),be(),Se(),we(),ve(),ce(),K(),c&&d){const P=c.id==="driver-dummy-element";u&&u(P?void 0:c,d,{config:a(),state:l()}),h&&h(P?void 0:c,d,{config:a(),state:l()})}m&&m.focus()}return{isActive:()=>l("isInitialized")||!1,refresh:A,drive:(s=0)=>{w(),r(s)},setConfig:z,setSteps:s=>{K(),z({...a(),steps:s})},getConfig:a,getState:l,getActiveIndex:()=>l("activeIndex"),isFirstStep:()=>l("activeIndex")===0,isLastStep:()=>{const s=a("steps")||[],c=l("activeIndex");return c!==void 0&&c===s.length-1},getActiveStep:()=>l("activeStep"),getActiveElement:()=>l("activeElement"),getPreviousElement:()=>l("previousElement"),getPreviousStep:()=>l("previousStep"),moveNext:t,movePrevious:i,moveTo:p,hasNextStep:()=>{const s=a("steps")||[],c=l("activeIndex");return c!==void 0&&s[c+1]},hasPreviousStep:()=>{const s=a("steps")||[],c=l("activeIndex");return c!==void 0&&s[c-1]},highlight:s=>{w(),Q({...s,popover:s.popover?{showButtons:[],showProgress:!1,progressText:"",...s.popover}:void 0})},destroy:()=>{v(!1)}}}return D.driver=ke,Object.defineProperty(D,Symbol.toStringTag,{value:"Module"}),D}({}); diff --git a/cloudexit/assets/template/egress.html b/cloudexit/assets/template/egress.html deleted file mode 100644 index 117f62f..0000000 --- a/cloudexit/assets/template/egress.html +++ /dev/null @@ -1,775 +0,0 @@ - - - - - - - {{ page_title }} - - - - - - - - - {% set provider_name = "Microsoft Azure" if cloud_service_provider == 1 else "Amazon Web Services" if cloud_service_provider == 2 else "Unknown Provider" %} - {% set assessment_name = "Basic" if assessment_type == 1 else "Standard" if assessment_type == 2 else "Unknown" %} - -
-
-

- EscapeCloud - {{ name }} -

- -
-
-
- -
-
-
-
-
-
-
-
- - {% if cloud_service_provider == 1 %} - - - - - - - - - - - - - - - - - - - - - - - - {% elif cloud_service_provider == 2 %} - - - - - - - - - - - - - {% else %} - {{ provider_name }} - {% endif %} - -
-
-
Cloud Service Provider
-
{{ provider_name }}
-
-
-
-
- -
-
-
Total Data
-
{{ total_data_display }}
-
-
-
-
- -
-
-
Assessment Type
-
{{ assessment_name }}
-
-
-
-
- -
-
-
Document ID
-
{{ document_id }}
-
-
-
-
- -
-
-
Timestamp
-
{{ timestamp }}
-
-
-
-
-
- -
-
-
-
-
-

Estimated Egress Fee

-
{{ total_fee_display }}
-
-
- -
-
Estimated Egress Fee
-
-

- Estimated one-time charge to transfer the identified - in-scope data out of the cloud over the public - internet. Calculated from the detected data volume, - the provider's standard egress price tiers, and - archive retrieval charges where applicable. Excludes - request/transaction fees, cross-region traffic, and - any resources whose size could not be measured. A "+" - means the figure is a lower-bound estimate. -

-
-
-
-
- {% if unknown_count > 0 %} -
- - Excludes {{ unknown_count }} resource{% if unknown_count != 1 %}s{% - endif %} with unavailable size. -
- {% endif %} {% if free_tier %} -
-
- -
-
- Included in Free Tier -
-

- The scanned footprint ({{ known_data_display }}) falls entirely within the cloud provider's complimentary - monthly internet egress allowance - ({{ free_tier_limit_display }} free tier). -

-
- {% elif has_fees %} -
- -
- {% else %} -
-
- -

No fee estimate available.

-
-
- {% endif %} -
-
-
- -
-
-
-
-
-

Data Allocation

-
{{ total_data_display }}
-
-
- -
-
Data Allocation
-
-

- Breakdown of the identified in-scope data volume by - resource category. Based on the sizes detected during - the assessment and intended to show where the data - footprint sits. A "+" indicates that some resources - could not be fully measured. -

-
-
-
-
- {% if has_allocation %} -
- -
- {% else %} -
-
- -

No data volume available.

-
-
- {% endif %} -
-
-
-
-
-
-
-

{{ introduction.heading }}

-

{{ introduction.purpose_heading }}

-

{{ introduction.purpose }}

-

{{ introduction.scope_heading }}

-

{{ introduction.scope }}

-

{{ introduction.limitations_heading }}

- {% for paragraph in introduction.limitations %} -

{{ paragraph }}

- {% endfor %} - {% if coverage %} -

{{ coverage.heading }}

-

{{ coverage.intro }}

- - - - {% for header in coverage.headers %} - - {% endfor %} - - - - {% for name, note in coverage.rows %} - - - - - {% endfor %} - -
{{ header }}
{{ name }}{{ note }}
- {% endif %} -
-
-
- -
-
-
-
-

{{ landscape.heading }}

-
-
-
-

{{ landscape.lead }}

-
- {% for label, value, attention in landscape.stats %} -
-
{{ label }}
-
{{ value }}
-
- {% endfor %} -
-
-
- - - - - - - - - - - {% if type_groups %} {% for group in type_groups %} - - - - - - - - - - - {% endfor %} {% else %} - - - - {% endif %} - -
#Resource TypeCategory{{ landscape.headers[2] }}
{{ loop.index }} - - {% if group.icon %} - {{ group.label | trim }} - {% else %} - - {% endif %} - {{ group.label }} - - {{ group.category_label }} - {{ group.size_display }}
-
-
- -
{{ group.fee_display }}
-
-
- -
- {% for resource in group.resources %} -
- {{ resource.name }}{% if resource.fee_display %} - ({{ resource.fee_display }}){% endif %}{% if - resource.detail %} - – {{ resource.detail }}{% endif %} -
- {% endfor %} -
-
-
-
- No data-bearing resources were discovered during the - assessment. -
-
-
- {% if landscape.not_sized %}

{{ landscape.not_sized }}

{% endif %} -

{{ landscape.caption }}

-

{{ landscape.source_note }}

-
-
-
-
- -
-
-
-

{{ costs.heading }}

-

{{ costs.lead }}

-
- {% for label, value, attention in costs.stats %} -
-
{{ label }}
-
{{ value }}
-
- {% endfor %} -
- {% if costs.rows %} - - - - - - - - - - - {% for row in costs.rows %} - - - - - - - {% endfor %} - - - - - - - -
{{ costs.headers[0] }}{{ costs.headers[1] }}{{ costs.headers[2] }}{{ costs.headers[3] }}
{{ row.label }}{{ row.size }}{{ row.share_text }}{{ row.fee }}
{{ costs.total[0] }}{{ costs.total[1] }}{{ costs.total[2] }}
- {% endif %} -

{{ costs.caption }}

-
-
-
- - {% if pricing %} -
-
-
-

{{ pricing.heading }}

-

{{ pricing.intro }}

- - - - {% for header in pricing.headers %} - - {% endfor %} - - - - {% for row in pricing.rows %} - - {% for cell in row %} - - {% endfor %} - - {% endfor %} - -
{{ header }}
{{ cell }}
-
- {% if pricing.units %}

{{ pricing.units }}

{% endif %} - {% if pricing.no_archive %}

{{ pricing.no_archive }}

{% endif %} - {% for template, url in pricing.sources %} -

{{ template.split("{link}")[0] }}{{ url }}{{ template.split("{link}")[1] }}

- {% endfor %} -

{{ pricing.note }}

-
-

{{ pricing.glossary_heading }}

-
- {% for term, definition in pricing.glossary %} -
{{ term }}
-
{{ definition }}
- {% endfor %} -
-
-
-
- {% endif %} - -
-
-
-

{{ next_steps.heading }}

- {% for paragraph in next_steps.paragraphs %} -

- {% for part in paragraph %}{% if part is string %}{{ part }}{% else %}{{ part[0] }}{% endif %}{% endfor %} -

- {% endfor %} -
-
-
-
- -
-
-

{{ notices.heading }}

- {% for block in notices.blocks %} -

{{ block.heading }}

- {% for paragraph in block.paragraphs %} -

- {% for part in paragraph %}{% if part is string %}{{ part }}{% else %}{{ part[0] }}{% endif %}{% endfor %} -

- {% endfor %} - {% endfor %} -

{{ notices.copyright }}

-
-
- - - - - - {% if has_allocation %} - - {% endif %} - - {% if has_fees %} - - {% endif %} - -
- - diff --git a/cloudexit/assets/template/index.html b/cloudexit/assets/template/index.html index 0087909..00c0873 100644 --- a/cloudexit/assets/template/index.html +++ b/cloudexit/assets/template/index.html @@ -1,1130 +1,573 @@ - - - - - - - Executive Summary – {{ name }} - - - - - - - - - {% set provider_name = "Microsoft Azure" if cloud_service_provider == 1 else "Amazon Web Services" if cloud_service_provider == 2 else "Unknown Provider" %} - {% set strategy_name = "Repatriation to On-Premises" if exit_strategy == 1 else "Migration to Alternate Cloud" if exit_strategy == 3 else "Unknown Strategy" %} - {% set assessment_name = "Basic" if assessment_type == 1 else "Standard" if assessment_type == 2 else "Unknown" %} - {% set total_risks = high_risk_count + medium_risk_count + low_risk_count %} - -
-
-

- cloudexit - {{ name }} -

- +{%- macro parts(items) -%} +{%- for part in items -%} +{%- if part is string -%}{{ part }} +{%- elif part[1].startswith('#') -%}{{ part[0] }} +{%- else -%}{{ part[0] }} +{%- endif -%} +{%- endfor -%} +{%- endmacro -%} +{%- macro icon(src, initials) -%} +{%- if src -%}{%- else -%}{%- endif -%} +{%- endmacro -%} +{%- macro pill(id, s) -%} +{%- if s.platform_url -%} +{{ s.open_platform }} +{%- else -%} + + + +{%- endif -%} +{%- endmacro -%} +{%- macro mark(on, yes, no) -%} +{{ yes if on else no }} +{%- endmacro -%} + + + + + +{{ page.title }} + + + + + + + +
+
+ EscapeCloud{{ page.header.tag }} + +
+
+ +
+
+
+
+ {% if page.titlebar.logo %}{% endif %} +

{{ page.titlebar.name }}

+ {{ page.titlebar.chip }}
-
-
{% if upload %} -
- {% if upload.status == "uploaded" %} -
- Uploaded to the platform.{% if upload.url %} - Open the platform report{% endif %} +
+
+ {% if page.titlebar.platform_url %}{{ page.open_platform }}{% endif %} + +
+
+ + + +
+
+

{{ page.nav[0].label }}

+
+
+ {{ page.overview.scope.label }} +
+ {% for row in page.overview.scope.rows %}
{{ row.label }}
{{ row.value }}{% if row.small %}{{ row.small }}{% endif %}
+ {% endfor %} +
- {% else %} - - {% endif %} -
- {% if summary %} -
-
-

{{ summary.brief.heading }}

-

{{ summary.brief.lead | safe }}

-

{{ summary.brief.findings_heading }}

-
- - - - {% for header in summary.brief.headers %} - - {% endfor %} - - - - {% for row in summary.brief.rows %} - - - - - - {% endfor %} - -
{{ header }}
{{ row.area }}{{ row.finding | safe }}{{ row.details }}
-
-

{{ summary.brief.caption | safe }}

+
+ {% set c = page.overview.costs %} + {{ c.label }} + {% if c.kind == 'message' %} +
+
{{ c.figure }}
+

{{ c.message }}

+ {% if c.hint %}

{{ c.hint }}

{% endif %}
-
-

{{ summary.provenance.heading }}

-
- {% for label, value in summary.provenance.rows %} -
{{ label }}
-
{{ value }}
- {% endfor %} - {% if summary.provenance.platform_url %} -
 
-
- {{ summary.provenance.platform_url }} -
- {% endif %} -
+ {% else %} +
+
{{ c.figure }}
{{ c.caption }}
+
{{ c.change }}
{% if c.change_caption %}
{{ c.change_caption }}
{% endif %}
-
-

{{ summary.introduction.heading }}

-

{{ summary.introduction.purpose_heading }}

-

{{ summary.introduction.purpose }}

-

{{ summary.introduction.scope_heading }}

- {% for paragraph in summary.introduction.scope %} -

{{ paragraph | safe }}

- {% endfor %} -

{{ summary.introduction.method_heading }}

- {% for paragraph in summary.introduction.method %} -

{{ paragraph }}

- {% endfor %} -
    - {% for phase, text in summary.introduction.phases %} -
  • {{ phase }} {{ text }}
  • - {% endfor %} -
-

{{ summary.introduction.method_catalogue }}

-

{{ summary.introduction.limitations_heading }}

- {% for paragraph in summary.introduction.limitations %} -

{{ paragraph }}

+ +

{{ c.footnote }}

+ {% endif %}
- {% endif %} -
-
-
-
-
-
-
- - {% if cloud_service_provider == 1 %} - - - - - - - - - - - - - - - - - - - - - - - - {% elif cloud_service_provider == 2 %} - - - - - - - - - - - - - {% else %} - {{ provider_name }} - {% endif %} - -
-
-
Cloud Service Provider
-
{{ provider_name }}
-
-
-
-
- -
-
-
Exit Strategy
-
{{ strategy_name }}
-
-
-
-
- -
-
-
Assessment Type
-
{{ assessment_name }}
-
-
-
-
- -
-
-
Timestamp
-
{{ timestamp }}
-
-
-
-
-
- -
-
-
-
-
-

Risk Inventory

-
{{ total_risks }}
-
-
- {% if total_risks > 0 %} -
- -
- {% else %} -
-
- -

No risk data available.

-
-
- {% endif %} -
-
-
- -
-
-
-
-
-

Costs Overview

-
- {% if total_cost > 0 %}{{ currency_symbol }}{{ total_cost }}{% else - %}-{% endif %} -
-
-
- {% if total_cost > 0 %} -
- -
- {% else %} -
-
- -

No cost data available.

-
-
- {% endif %} - {% if summary.costs_egress_note %} -

{{ summary.costs_egress_note }}

- {% endif %} -
-
-
+
+
+ +
+

{{ page.nav[1].label }}

+ {% set s = page.scores %} +
+
+

{{ s.grade.title }}

+ {{ pill('hint-grade', s) }}
- -
-
-
-
-

Risk Inventory

-
- - -
-
-
- -
- - - - - - - - - - - {% if risks %} {% for risk in risks %} - - - - - - - - - - - {% endfor %} {% else %} - - - - {% endif %} - -
#RiskImpacted ResourcesSeverity
{{ loop.index }} - - {{ risk.name }} - - {% if risk.impacted_resources_count is none %}-{% else %}{{ - risk.impacted_resources_count }}{% endif %} - - {{ risk.severity | capitalize }} -
-
-
- -
{{ risk.description }}
-
- {% if risk.impacted_resources_count %} -
- -
- {{ risk.impacted_resources | join(", ") }} -
-
- {% endif %} -
-
- No risks were identified for this assessment. -
-
-
+
+
{{ s.grade.placeholder }}
{{ s.grade.scale }}
+
+ +

{{ s.grade.text }}

- -
-
-
-
-

Resource Inventory ({{ total_resources }})

-
-
-
-
-
- {% if resource_inventory %} {% for resource in resource_inventory %} -
-
- {% if resource.icon | trim %} - {{ resource.name | trim }} - {% else %} - - {% endif %} -

{{ resource.name | trim }}

-
- {{ resource.count }} Resource{% if resource.count != 1 %}s{% - endif %} Available -
-
-
- {% endfor %} {% else %} -
-

- No resources were discovered during the assessment. -

-
- {% endif %} -
-
- {% if resource_inventory|length > 8 %} -
-
- -
- {% endif %} -
-
-
+
+
+

{{ s.lock.title }}

+ {{ pill('hint-lock', s) }}
- -
-
-
-
-

- Alternative Technologies ({{ alternative_technologies | length }}) -

- {% if alternative_technologies %} -
-
- -
-
- - - -
-
- {% endif %} -
-
- -
-
-
- {% if alternative_technologies %} {% for alt_tech in - alternative_technologies %} -
-
-
-
-
- - {{ alt_tech.organization_name }} - {% if alt_tech.organization_url %} - - - - {% endif %} - | - {% if alt_tech.organization_flag %}{{ alt_tech.organization_flag }} {% endif %}{{ alt_tech.organization_country_code }} • {{ alt_tech.organization_region_label }} -
-

{{ alt_tech.product_name }}

-
-
-
-

{{ alt_tech.product_description }}

-
-
-
- {% if alt_tech.open_source %} - - {% else %} - - {% endif %} - Open Source -
-
- {% if alt_tech.support_plan %} - - {% else %} - - {% endif %} - Enterprise Support -
-
-
-
- {% if alt_tech.product_url %} - - - Visit {{ alt_tech.product_name }} - - {% endif %} -
-
-
- {% endfor %} {% else %} -
-
-
- -

- No alternative technologies are available for this - assessment. -

-
-
-
- {% endif %} -
-
-
-
+
+ +

{{ s.lock.text }}

+
+
- + {% if e.empty %} +

{{ e.empty }}

+ {% else %} + {% if e.volume %} + +
+ {% for item in e.volume.legend %}{{ item.text }} + {% endfor %} + {% if e.volume.unsized %}{{ e.volume.unsized }}{% endif %} +
+ {% endif %} - {% if total_risks > 0 %} - +
+ + + + {% for row in e.table.rows %} + {% if row.size is none %} + {% else %} + {% endif %} + {% endfor %} + + +
{{ e.table.headers[0] }}{{ e.table.headers[1] }}{{ e.table.headers[2] }}{{ e.table.headers[3] }}
{{ icon(row.icon, row.initials) }}{{ row.label }}
{{ e.table.not_sized }}{{ page.dash }}{{ page.dash }}
{{ icon(row.icon, row.initials) }}{{ row.label }}
{{ row.size }}{{ row.fee }}
{{ e.table.total[0] }}{{ e.table.total[1] }}{{ e.table.total[2] }}
+
{% endif %} - {% if total_cost > 0 %} - + {% if e.sized.facts %} +
+ {{ e.sized.summary }} +
+ + + + {% for name, note in e.sized.facts.rows %} + {% endfor %} + +
{{ e.sized.facts.headers[0] }}{{ e.sized.facts.headers[1] }}
{{ name }}{{ note }}
+
+
{% endif %} - - {% if summary %} -
-
-

{{ summary.severity.heading }}

- {% for paragraph in summary.severity.lead %} -

{{ paragraph }}

- {% endfor %} - - - - {% for header in summary.severity.headers %} - - {% endfor %} - - + {% if e.prices %} +
+ {{ e.prices.summary }} +
+
{{ header }}
+ {% for header in e.prices.facts.headers %}{{ header }}{% endfor %} - {% for level, meaning in summary.severity.rows %} - - - - + {% for row in e.prices.facts.rows %}{% for cell in row %}{{ cell }}{% endfor %} {% endfor %}
{{ level }}{{ meaning }}
+ {% if e.prices.facts.units %}

{{ e.prices.facts.units }}

{% endif %} + {% for template, url in e.prices.facts.sources %}{% set pieces = template.split('{link}') %}

{{ pieces[0] }}{{ url }}{{ pieces[1] }}

+ {% endfor %} + {% if e.prices.facts.no_archive %}

{{ e.prices.facts.no_archive }}

{% endif %} +

{{ e.prices.facts.note }}

+
+ + {% endif %} +
+ + {% endif %} + +
+ {% set a = page.about %} +
+

{{ a.heading }}

{{ a.caption }}

+
+
+
+
+

{{ a.document.heading }}

+ + {% for label, value, mono in a.document.rows %}{{ value }} + {% endfor %} +
{{ label }}
-
-

{{ summary.glossary.heading }}

-

{{ summary.glossary.lead }}

-
- {% for term, definition in summary.glossary.entries %} -
{{ term }}
-
{{ definition }}
+
+

{{ a.scope.heading }}

+ + {% for label, value, mono in a.scope.rows %}{{ value }} {% endfor %} - +
{{ label }}
+
+
+

{{ a.provenance.heading }}

+ + {% if a.provenance.run_id %}{% endif %} + + +
{{ a.provenance.run_id[0] }}{{ a.provenance.run_id[1] }}
{{ a.provenance.catalogue[0] }}{{ a.provenance.catalogue[1] }}{% if a.provenance.catalogue[2] %} · {{ a.provenance.catalogue[2] }}{% endif %}
{{ a.provenance.platform[0] }}{{ parts(a.provenance.platform[1]) }}
-
-

{{ summary.closing.heading }}

-

{{ summary.closing.lead }}

- {% if summary.closing.uploaded %} - {% if summary.closing.url %} -

- {{ summary.closing.url }} -

- {% elif summary.closing.assessment_id %} -

Assessment ID: {{ summary.closing.assessment_id }}

- {% endif %} - {% else %} +
+

{{ a.stored }}

+ +
+ {{ a.purpose.summary }}{{ a.purpose.meta }} +
+ {% for paragraph in a.purpose.paragraphs %}

{{ paragraph }}

+ {% endfor %}
    - {% for term, text in summary.closing.points %} -
  • {{ term }} {{ text }}
  • + {% for name, text in a.purpose.phases %}
  • {{ name }} {{ text }}
  • {% endfor %}
-

{{ summary.closing.how_heading }}

-

{{ summary.closing.how }}

-

{{ summary.closing.command }}

-

- {% for name, url in summary.closing.sites %} - {{ url }}{% if not loop.last %} · {% endif %} - {% endfor %} -

- {% endif %} +

{{ a.purpose.closing }}

-
-

{{ summary.notices.heading }}

- {% for block in summary.notices.blocks %} -

{{ block.heading }}

- {% for paragraph in block.paragraphs %} -

- {% for part in paragraph %}{% if part is string %}{{ part }}{% else %}{{ part[0] }}{% endif %}{% endfor %} -

+
+
+ {{ a.limitations.summary }} +
+ {% for paragraph in a.limitations.paragraphs %}

{{ paragraph }}

{% endfor %} - {% endfor %} -

{{ summary.notices.copyright }}

-
- {% endif %} -
- + +
+ {{ a.severity.summary }}{{ a.severity.meta }} +
+ + + + {% for level, label, meaning in a.severity.rows %} + {% endfor %} + +
{{ a.severity.headers[0] }}{{ a.severity.headers[1] }}
{{ label }}{{ meaning }}
+

{{ a.severity.note }}

+
+
+
+ {{ a.glossary.summary }}{{ a.glossary.meta }} +
+ + {% for term, definition in a.glossary.entries %} + {% endfor %} +
{{ term }}{{ definition }}
+
+
+
+ + + {% set n = page.notices %} + +
+ {% for block in n.blocks %}
+

{{ block.heading }}

+

{{ parts(block.parts) }}

+
+ {% endfor %} + +
+ +
+ + + + + + + diff --git a/cloudexit/cli/app.py b/cloudexit/cli/app.py index 69e218c..1c44cbf 100644 --- a/cloudexit/cli/app.py +++ b/cloudexit/cli/app.py @@ -29,7 +29,6 @@ create_subscription_client, ) from cloudexit.egress.estimate import estimate_egress -from cloudexit.report.egress import generate_egress_html_report from cloudexit.platform.client import ApiError, check_key from cloudexit.platform.run_output import build_run_json, exit_code_of from cloudexit.platform.status import host_catalogue_lines, run_status @@ -988,7 +987,6 @@ def run_assessment( egress_failed = False egress_collected = False egress_json_path = None - egress_html_path = None if egress: console.print("Stage #5 – Egress Estimation", style="bold") @@ -1007,25 +1005,6 @@ def run_assessment( print_step("Estimating egress data...", status="ok") egress_json_path = egress_result.get("json_path") egress_collected = True - - with console.status("Generating egress report...", spinner="dots"): - egress_report_result = generate_egress_html_report( - report_path, - egress_json_path, - config["providerDetails"], - sync_id, - ) - - if egress_report_result["success"]: - print_step("Generating egress report...", status="ok") - egress_html_path = egress_report_result.get("html_path") - else: - print_step( - "Generating egress report...", - status="error", - logs=egress_report_result["logs"], - ) - egress_failed = True else: print_step( "Estimating egress data...", @@ -1219,8 +1198,6 @@ def run_assessment( json_report_path = report_status.get("reports", {}).get("JSON") if json_report_path: console.print(f"JSON Report: {json_report_path}", style="cyan") - if egress_html_path: - console.print(f"Egress Report: {egress_html_path}", style="cyan") if egress_pdf_path: console.print(f"Egress PDF: {egress_pdf_path}", style="cyan") console.print("-------------------------------------------") diff --git a/cloudexit/engine.py b/cloudexit/engine.py index f5da2a8..5507910 100644 --- a/cloudexit/engine.py +++ b/cloudexit/engine.py @@ -27,9 +27,9 @@ build_context, collect_provenance, ) +from cloudexit.report.egress import load_egress_estimate from cloudexit.report.egress_report import generate_egress_report from cloudexit.report.executive_summary import generate_executive_summary -from cloudexit.report.html_sections import html_sections from cloudexit.report.writers import ( generate_html_report, generate_json_report, @@ -499,25 +499,22 @@ def generate_report( provenance=provenance, report_path=report_path, ) + # The estimate is read once, for the page and for the egress PDF. When + # it cannot be read there is no egress in any report, and the failure + # is reported with the egress PDF's. + egress_estimate, egress_error = None, None if egress_json_path: - context.egress_document_id = context.document_id_for(EGRESS_DOCUMENT_CODE) + try: + egress_estimate = load_egress_estimate(report_path, egress_json_path) + context.egress_document_id = context.document_id_for( + EGRESS_DOCUMENT_CODE + ) + except Exception as e: + logger.error("Error reading the egress estimate: %s", e, exc_info=True) + egress_error = str(e) # Generate HTML report - reports["HTML"] = generate_html_report( - report_path, - metadata, - resource_type_mapping, - resource_inventory, - cost_data, - scoring_data, - risk_data, - risk_definitions, - alternatives, - alternative_technologies, - exit_strategy, - alternative_technology_organizations, - summary=html_sections(context), - ) + reports["HTML"] = generate_html_report(context, egress_estimate) # Generate PDF report reports["PDF"] = generate_executive_summary(context) @@ -542,10 +539,12 @@ def generate_report( # The Egress & Data Landscape is written last, from the same context: a # problem with it never costs the Executive Summary - if egress_json_path: + if egress_error: + result["egress_pdf_logs"] = egress_error + elif egress_estimate is not None: try: reports["EGRESS_PDF"] = generate_egress_report( - context, egress_json_path + context, egress_json_path, estimate=egress_estimate ) except Exception as e: logger.error("Error generating the egress PDF: %s", e, exc_info=True) diff --git a/cloudexit/report/aggregate.py b/cloudexit/report/aggregate.py index c983a26..ea24404 100644 --- a/cloudexit/report/aggregate.py +++ b/cloudexit/report/aggregate.py @@ -80,29 +80,6 @@ def sort_cost_data(cost_data: list[dict[str, Any]]) -> list[dict[str, Any]]: return sorted(cost_data, key=lambda x: datetime.strptime(x["month"], "%Y-%m-%d")) -def summarize_costs( - cost_data: list[dict[str, Any]], *, last_n: int | None = None -) -> tuple[list[str], list[float], float, str, str]: - sorted_costs = sort_cost_data(cost_data) - if last_n is not None: - sorted_costs = sorted_costs[-last_n:] - - months = [ - datetime.strptime(item["month"], "%Y-%m-%d").strftime("%b") - for item in sorted_costs - ] - values = [item["cost"] for item in sorted_costs] - total_cost = round(sum(values), 2) - - if sorted_costs: - currency_code = sorted_costs[0].get("currency", "USD") - else: - currency_code = "USD" - currency_symbol = CURRENCY_SYMBOLS.get(currency_code, currency_code) - - return months, values, total_cost, currency_code, currency_symbol - - def summarize_risks( risk_data: list[dict[str, Any]], risk_definitions: list[dict[str, Any]], diff --git a/cloudexit/report/assets.py b/cloudexit/report/assets.py index 0bb3589..784d0dd 100644 --- a/cloudexit/report/assets.py +++ b/cloudexit/report/assets.py @@ -43,7 +43,7 @@ def copy_assets(report_path: str, cloud_service_provider: int) -> None: # Create the 'assets' directory if it doesn't exist os.makedirs(assets_path, exist_ok=True) - for folder in ("css", "img"): + for folder in ("css", "fonts", "img", "js"): src_path = asset_path(folder) dest_path = os.path.join(assets_path, folder) diff --git a/cloudexit/report/context.py b/cloudexit/report/context.py index 9f24c96..da43ca6 100644 --- a/cloudexit/report/context.py +++ b/cloudexit/report/context.py @@ -38,6 +38,8 @@ class Provenance: uploaded: bool = False platform_url: str | None = None assessment_id: str | None = None + # Why a requested upload did not happen, when it did not + upload_reason: str | None = None @property def run_short_id(self): @@ -77,6 +79,9 @@ def collect_provenance(db_path, *, run_id=None, upload=None, collected_at=None): uploaded=upload.get("status") == "uploaded", platform_url=upload.get("url") if upload.get("status") == "uploaded" else None, assessment_id=upload.get("assessment_id"), + upload_reason=( + upload.get("reason") if upload.get("status") != "uploaded" else None + ), ) @@ -87,6 +92,9 @@ class ResourceTypeEntry: icon: str | None resources: int = 0 locations: set = field(default_factory=set) + location_counts: dict = field(default_factory=dict) + # The icon as the catalogue names it, relative to the report's assets + icon_rel: str = "" @dataclass(frozen=True) @@ -127,6 +135,7 @@ class AlternativesEntry: icon: str | None resources: int alternatives: list = field(default_factory=list) + icon_rel: str = "" @dataclass(frozen=True) @@ -197,11 +206,16 @@ def _resource_entries(resource_inventory, resource_type_mapping, report_path): key, (info.get("name") or "").strip() or "–", _icon_path(report_path, info.get("icon")), + icon_rel=str(info.get("icon") or "").lstrip("/"), ), ) - entry.resources += int(row.get("count") or 0) + count = int(row.get("count") or 0) + entry.resources += count if row.get("location"): entry.locations.add(row["location"]) + entry.location_counts[row["location"]] = ( + entry.location_counts.get(row["location"], 0) + count + ) return sorted( entries.values(), key=lambda entry: (-entry.resources, entry.name.lower()) ) @@ -296,6 +310,7 @@ def _alternative_entries( resource.icon, resource.resources, found, + icon_rel=resource.icon_rel, ) ) return entries diff --git a/cloudexit/report/egress.py b/cloudexit/report/egress.py index d4ca4a0..099eacd 100644 --- a/cloudexit/report/egress.py +++ b/cloudexit/report/egress.py @@ -4,12 +4,9 @@ import os import sqlite3 from collections import Counter -from datetime import datetime from typing import Any -from jinja2 import Environment from cloudexit.report.helpers import format_gib, gib_number, initials, share_label -from cloudexit.resources import asset_path from cloudexit.catalogue.db import load_data from cloudexit.egress.model import ( GIB, @@ -18,14 +15,8 @@ ) from cloudexit.providers.aws.egress import ARCHIVE_TIERS as AWS_ARCHIVE_TIERS from cloudexit.providers.azure.egress import ARCHIVE_TIERS as AZURE_ARCHIVE_TIERS -from cloudexit.report.context import ( - DOCUMENT_CODE, - EGRESS_DOCUMENT_CODE, - PROVIDER_NAMES, - format_document_id, - scope_facts, -) -from cloudexit.report.notices import notices_html, text_parts +from cloudexit.report.context import PROVIDER_NAMES +from cloudexit.report.notices import text_parts from cloudexit.report.sections import BOOK_DEMO_URL, PRODUCT_SITES from cloudexit.report.wording import BLOCKS @@ -37,39 +28,6 @@ ARCHIVE_TIERS_BY_CSP = {1: AZURE_ARCHIVE_TIERS, 2: AWS_ARCHIVE_TIERS} -CATEGORIES = { - "object": { - "label": "Object Storage", - "badge": "Object", - "color": "rgba(34, 197, 94, 1)", - "in_allocation": True, - }, - "block": { - "label": "Block (allocated)", - "badge": "Block", - "color": "rgba(83, 155, 255, 1)", - "in_allocation": True, - }, - "file": { - "label": "File Storage", - "badge": "File", - "color": "rgba(245, 158, 11, 1)", - "in_allocation": True, - }, - "database": { - "label": "Databases", - "badge": "Database", - "color": "rgba(168, 85, 247, 1)", - "in_allocation": True, - }, - "backup": { - "label": None, - "badge": "Backup", - "color": None, - "in_allocation": False, - }, -} - PRICING_SOURCE_LINKS = { 1: { "internet_egress": "https://azure.microsoft.com/pricing/details/bandwidth/", @@ -183,7 +141,11 @@ def build_fee_estimate( def format_fee(fee: float | None) -> str: if fee is None: return "n/a" - return f"${fee:,.2f}" + return f"${fee_number(fee)}" + + +def fee_number(fee: float) -> str: + return f"{fee:,.2f}" def _free_tier( @@ -218,6 +180,23 @@ def free_allowance_bytes( return tier["tier_to"] * _unit_divisor(tier["unit"]) if tier else None +def free_tier_headroom( + prices: list[dict[str, Any]], cloud_service_provider: int, known_bytes: int +) -> dict[str, Any] | None: + tier = _free_tier(prices, cloud_service_provider) + if tier is None: + return None + limit = tier["tier_to"] + used = known_bytes / _unit_divisor(tier["unit"]) + return { + "unit": tier["unit"], + "limit": limit, + "used": used, + "left": max(0.0, limit - used), + "used_share": min(100.0, used / limit * 100) if limit else 100.0, + } + + def _summarize_totals( totals: dict[str, Any], total_fee: float, @@ -271,29 +250,6 @@ def _resolve_icon(resource_type: str, icon_lookup: dict[str, str]) -> str | None return FALLBACK_ICONS.get(code) -def _build_allocation(rows: list[dict[str, Any]]) -> tuple[list, list, list]: - allocation_categories = { - key: info for key, info in CATEGORIES.items() if info["in_allocation"] - } - totals_by_category: dict[str, int] = {} - for row in rows: - if row["size_bytes"] is not None: - category = row.get("category") - if category in allocation_categories: - totals_by_category[category] = ( - totals_by_category.get(category, 0) + row["size_bytes"] - ) - - labels, values, colors = [], [], [] - for category, info in allocation_categories.items(): - size_bytes = totals_by_category.get(category, 0) - if size_bytes > 0: - labels.append(info["label"]) - values.append(size_bytes) - colors.append(info["color"]) - return labels, values, colors - - def _build_type_groups( rows: list[dict[str, Any]], fees_by_id: dict[str, float | None], @@ -307,10 +263,6 @@ def _build_type_groups( "label": row["label"], "icon": _resolve_icon(row["type"], icon_lookup), "initials": initials(row["label"]), - "category": row["category"], - "category_label": ( - CATEGORIES.get(row["category"], {}).get("badge") or row["category"] - ), "size_bytes": None, "fee": None, "resources": [], @@ -383,14 +335,6 @@ def load_egress_estimate(report_path: str, json_path: str) -> dict[str, Any]: } -def _generated_at(timestamp): - # The run's own time, as the estimate stored it; now when it cannot be read - try: - return datetime.strptime(str(timestamp), "%Y-%m-%d %H:%M:%S UTC") - except ValueError: - return datetime.now() - - def _share(part, whole): return part / whole * 100 if whole else 0 @@ -765,78 +709,3 @@ def next_steps_facts( ) ) return {"heading": b["eg_heading_next_steps"], "paragraphs": paragraphs} - - -def generate_egress_html_report( - report_path: str, - json_path: str, - provider_details: dict[str, Any] | None = None, - run_id: str | None = None, -) -> dict[str, Any]: - try: - estimate = load_egress_estimate(report_path, json_path) - meta, rows = estimate["meta"], estimate["rows"] - csp = meta["cloud_service_provider"] - summary = estimate["summary"] - b = BLOCKS - - allocation_labels, allocation_values, allocation_colors = _build_allocation( - rows - ) - - type_groups = estimate["type_groups"] - fee_groups = [group for group in type_groups if (group["fee"] or 0.0) > 0] - fee_labels = [group["label"] for group in fee_groups] - fee_values = [round(group["fee"], 2) for group in fee_groups] - - template_content = asset_path("template", "egress.html").read_text( - encoding="utf-8" - ) - - scope = scope_facts(provider_details or {}, csp, None) - executive_id = format_document_id(DOCUMENT_CODE, run_id) - env = Environment(autoescape=True) - template = env.from_string(template_content) - html_content = template.render( - page_title=f"{b['cover_title_egress']} – {meta['name']}", - document_id=format_document_id(EGRESS_DOCUMENT_CODE, run_id), - name=meta["name"], - cloud_service_provider=csp, - assessment_type=meta["assessment_type"], - timestamp=meta["timestamp"], - total_data_display=summary["total_data_display"], - total_fee_display=summary["total_fee_display"], - unknown_count=summary["unknown_count"], - has_allocation=bool(allocation_values), - allocation_labels_json=json.dumps(allocation_labels), - allocation_values_json=json.dumps(allocation_values), - allocation_colors_json=json.dumps(allocation_colors), - has_fees=bool(fee_values), - fee_labels_json=json.dumps(fee_labels), - fee_values_json=json.dumps(fee_values), - free_tier=summary["free_tier"], - free_tier_limit_display=summary["free_tier_limit"], - known_data_display=format_gib(estimate["totals"]["known_size_bytes"]), - type_groups=type_groups, - introduction=introduction_facts(scope, b), - coverage=coverage_facts(csp, b), - landscape=landscape_facts(estimate, b), - costs=cost_facts(estimate, b), - pricing=pricing_facts(estimate, b), - next_steps=next_steps_facts(executive_id, b=b), - notices=notices_html(csp, _generated_at(meta["timestamp"]), b, egress=True), - ) - - html_path = os.path.join(report_path, "egress.html") - with open(html_path, "w", encoding="utf-8") as report_file: - report_file.write(html_content) - - return { - "success": True, - "logs": "Egress report generated successfully.", - "html_path": html_path, - } - - except Exception as e: - logger.error(f"Error generating egress report: {str(e)}", exc_info=True) - return {"success": False, "logs": str(e)} diff --git a/cloudexit/report/egress_report.py b/cloudexit/report/egress_report.py index 6acc846..8e108f6 100644 --- a/cloudexit/report/egress_report.py +++ b/cloudexit/report/egress_report.py @@ -48,10 +48,10 @@ def build_egress_content(context, estimate, styles, b): return content -def generate_egress_report(context, json_path, path=None): +def generate_egress_report(context, json_path, path=None, estimate=None): path = path or os.path.join(context.report_path, "egress.pdf") b = BLOCKS - estimate = load_egress_estimate(context.report_path, json_path) + estimate = estimate or load_egress_estimate(context.report_path, json_path) cover = RecordCover( b["cover_title_egress"], context, diff --git a/cloudexit/report/evidence.py b/cloudexit/report/evidence.py index 63600e3..e0ea97e 100644 --- a/cloudexit/report/evidence.py +++ b/cloudexit/report/evidence.py @@ -53,54 +53,62 @@ def build_document_table( return key_value_table(rows, context.appearance) -def _scope_rows(context, b): +def scope_rows(context, b): + # (label, text, monospaced), the same for the PDF and the HTML report scope = context.scope values = dict(scope.rows) - rows = [(b["ev_cloud_provider"], scope.provider or b["ev_dash"])] + rows = [(b["ev_cloud_provider"], scope.provider or b["ev_dash"], False)] if scope.kind == "aws": if values.get("account"): - rows.append((b["ev_account_id"], _mono_value(values["account"]))) - rows.append((b["ev_region"], values.get("region") or b["ev_dash"])) + rows.append((b["ev_account_id"], values["account"], True)) + rows.append((b["ev_region"], values.get("region") or b["ev_dash"], False)) elif scope.kind == "azure": + rows.append((b["ev_tenant_id"], values.get("tenant") or b["ev_dash"], True)) rows.append( - (b["ev_tenant_id"], _mono_value(values.get("tenant")) or b["ev_dash"]) + (b["ev_subscription_id"], values.get("subscription") or b["ev_dash"], True) ) rows.append( ( - b["ev_subscription_id"], - _mono_value(values.get("subscription")) or b["ev_dash"], + b["ev_resource_group"], + values.get("resource_group") or b["ev_dash"], + False, ) ) - rows.append( - (b["ev_resource_group"], values.get("resource_group") or b["ev_dash"]) - ) else: file_text = values.get("file") or b["ev_dash"] if values.get("sha256"): file_text = f"{file_text} (SHA-256 {_short(values['sha256'])})" - rows.append((b["ev_file"], file_text)) + rows.append((b["ev_file"], file_text, False)) if values.get("lineage"): state = values["lineage"] if values.get("serial") is not None: state = f"{state} (serial {values['serial']})" - rows.append((b["ev_state"], _mono_value(state))) + rows.append((b["ev_state"], state, True)) for key, label in ( ("subscriptions", "ev_subscriptions"), ("resource_groups", "ev_resource_groups"), ("regions", "ev_regions"), ): if key in values: - rows.append((b[label], values[key] or b["ev_not_determinable"])) + rows.append((b[label], values[key] or b["ev_not_determinable"], False)) source = b["ev_source_tfstate" if scope.kind == "tfstate" else "ev_source_live"] rows.append( ( b["ev_source_label"], b["ev_source_value"].format(source=source, completed=context.completed), + False, ) ) return rows +def _scope_rows(context, b): + return [ + (label, _mono_value(text) if mono and text != b["ev_dash"] else text) + for label, text, mono in scope_rows(context, b) + ] + + def build_scope_table(context, b): return key_value_table(_scope_rows(context, b), context.appearance) diff --git a/cloudexit/report/helpers.py b/cloudexit/report/helpers.py index b79acf4..3536541 100644 --- a/cloudexit/report/helpers.py +++ b/cloudexit/report/helpers.py @@ -1,3 +1,5 @@ +import os + CURRENCY_SYMBOLS = {"USD": "$", "EUR": "€", "GBP": "£"} @@ -19,10 +21,13 @@ def format_money(value, currency): return f"{symbol}{amount}" if symbol else f"{currency} {amount}" -def gib_number(size_bytes): - # One decimal; a size that is not zero never reads as 0.0 +def gib_number(size_bytes, places=1): + # A size that is not zero never reads as 0.0 gib = size_bytes / 2**30 - return "<0.1" if 0 < gib < 0.1 else f"{gib:,.1f}" + smallest = 10**-places + if 0 < gib < smallest: + return f"<{smallest:.{places}f}" + return f"{gib:,.{places}f}" def format_gib(size_bytes): @@ -71,3 +76,19 @@ def initials(name): if len(letters) == 2: break return "".join(letters) or "?" + + +REPORT_ASSETS = "assets" + + +def report_icon(report_path, icon_rel): + # The page only points at files in its own folder: an icon the report + # folder does not hold is left out, and the page draws a placeholder + if icon_rel and os.path.isfile(os.path.join(report_path, REPORT_ASSETS, icon_rel)): + return f"{REPORT_ASSETS}/{icon_rel}" + return None + + +def percent(share): + # With a no-break space, so a figure and its sign stay together + return share_label(share).replace(" ", " ") diff --git a/cloudexit/report/html_data.py b/cloudexit/report/html_data.py deleted file mode 100644 index 40377d5..0000000 --- a/cloudexit/report/html_data.py +++ /dev/null @@ -1,63 +0,0 @@ -# cloudexit/report/html_data.py -import logging -from typing import Any - -from cloudexit.report.aggregate import ( - summarize_alternative_technologies, - summarize_costs, - summarize_risks, -) - -# Configure logger -logger = logging.getLogger(__name__) - - -def transform_cost_inventory_for_html( - cost_data: list[dict[str, Any]], -) -> tuple[list[str], list[float], float, str, str]: - return summarize_costs(cost_data) - - -def transform_risk_inventory_for_html( - risk_data: list[dict[str, Any]], - risk_definitions: list[dict[str, Any]], - resource_inventory: dict[str, dict[str, Any]], -) -> tuple[list[dict[str, Any]], dict[str, int]]: - severity_order = {"high": 1, "medium": 2, "low": 3} - resource_name_map = { - str(key): value["name"] for key, value in resource_inventory.items() - } - risks, severity_counts = summarize_risks( - risk_data, - risk_definitions, - resource_name_map=resource_name_map, - ) - risks.sort(key=lambda x: severity_order.get(x["severity"], 4)) - return risks, severity_counts - - -def transform_alt_tech_for_html( - resource_inventory: list[dict[str, Any]], - alternatives: list[dict[str, Any]], - alternative_technologies: list[dict[str, Any]], - exit_strategy: int, - alternative_technology_organizations: list[dict[str, Any]] | None = None, -) -> list[dict[str, Any]]: - alt_tech_data = [] - grouped_alt_tech = summarize_alternative_technologies( - resource_inventory, - alternatives, - alternative_technologies, - exit_strategy, - alternative_technology_organizations=alternative_technology_organizations, - ) - for resource in resource_inventory: - resource_type = str(resource.get("resource_type")) - for tech in grouped_alt_tech.get(resource_type, []): - alt_tech_data.append( - { - "resource_type_id": resource.get("resource_type"), - **tech, - } - ) - return alt_tech_data diff --git a/cloudexit/report/html_egress.py b/cloudexit/report/html_egress.py new file mode 100644 index 0000000..fa19b09 --- /dev/null +++ b/cloudexit/report/html_egress.py @@ -0,0 +1,248 @@ +from .egress import ( + DEFAULT_PRICING_ZONE, + coverage_facts, + fee_number, + free_tier_headroom, + pricing_facts, +) +from .context import EGRESS_DOCUMENT_CODE, PROVIDER_NAMES +from .helpers import gib_number, percent, report_icon +from .wording import BLOCKS + +SEGMENT_COLORS = ( + "var(--primary-800)", + "var(--primary-600)", + "#2dd4c8", + "#5eeae1", + "#99f6f0", +) +OTHER_COLOR = "var(--neutral-300)" + + +def _share(part, whole): + return part / whole * 100 if whole else 0 + + +def _internet_unit(prices, csp): + return next( + ( + price["unit"] + for price in prices + if price["csp"] == csp + and price["component"] == "internet_egress" + and price["zone"] == DEFAULT_PRICING_ZONE + ), + "GiB", + ) + + +def _kpis(estimate, sized, unsized, groups, b): + summary = estimate["summary"] + csp = estimate["meta"]["cloud_service_provider"] + total = estimate["totals"]["known_size_bytes"] + headroom = free_tier_headroom(estimate["pricing"], csp, total) + plus = "+" if summary["unknown_count"] > 0 else "" + + kpis = [ + { + "label": b["html_eg_kpi_fee"], + "figure": summary["total_fee_display"], + "unit": "", + "caption": b[ + "html_eg_fee_free" if summary["free_tier"] else "html_eg_fee_paid" + ], + "primary": True, + }, + { + "label": b["eg_st_volume"], + "figure": gib_number(total), + "unit": f"GiB{plus}", + "caption": ( + b["html_eg_types_sized"].format(sized=len(sized), types=len(groups)) + if groups + else "" + ), + }, + ] + if headroom: + kpis.append( + { + "label": b["html_eg_kpi_headroom"], + "figure": f"{headroom['left']:,.1f}", + "unit": headroom["unit"], + "caption": b["html_eg_headroom_left"].format( + limit=f"{headroom['limit']:g} {headroom['unit']}" + ), + "bar": round(headroom["used_share"]), + "aria": b["html_eg_headroom_aria"].format( + share=percent(headroom["used_share"]) + ), + } + ) + else: + kpis.append( + { + "label": b["eg_st_billable"], + "figure": gib_number(total), + "unit": "GiB", + "caption": b["html_eg_billable_caption"], + } + ) + kpis.append( + { + "label": b["eg_st_not_sized"], + "figure": str(len(unsized)), + "unit": "", + "caption": b["html_eg_not_sized_caption"], + } + ) + return kpis, headroom + + +def _volume(sized, unsized, total, b): + if not sized or total <= 0: + return None + shown, rest = sized[: len(SEGMENT_COLORS)], sized[len(SEGMENT_COLORS) :] + segments = [ + ( + group["label"], + _share(group["size_bytes"], total), + SEGMENT_COLORS[index], + ) + for index, group in enumerate(shown) + ] + if rest: + segments.append( + ( + b["html_eg_vol_other"], + _share(sum(group["size_bytes"] for group in rest), total), + OTHER_COLOR, + ) + ) + legend = [ + {"text": f"{label} {percent(share)}", "color": color} + for label, share, color in segments + ] + return { + "segments": [ + {"flex": round(share, 1), "color": color} for _, share, color in segments + ], + "legend": legend, + "unsized": ( + b["html_eg_vol_unsized"].format( + types=", ".join(group["label"] for group in unsized) + ) + if unsized + else None + ), + "aria": b["html_eg_vol_aria"].format( + parts=", ".join(item["text"] for item in legend) + ), + } + + +def _table(context, estimate, sized, unsized, total, b): + plus = "+" if estimate["summary"]["unknown_count"] > 0 else "" + + def row(group): + icon = group["icon"].lstrip("/") if group["icon"] else "" + return { + "label": group["label"], + "initials": group["initials"], + "icon": report_icon(context.report_path, icon), + } + + rows = [ + { + **row(group), + "size": gib_number(group["size_bytes"], 2), + "share": percent(_share(group["size_bytes"], total)), + "share_value": round(_share(group["size_bytes"], total)), + "fee": fee_number(group["fee"] or 0.0), + } + for group in sized + ] + rows += [ + {**row(group), "size": None, "share": None, "share_value": 0, "fee": None} + for group in unsized + ] + return { + "headers": ( + b["eg_th_resource_type"], + b["eg_th_size_gib"], + b["eg_th_share"], + b["eg_th_fee"], + ), + "rows": rows, + "not_sized": b["html_eg_not_sized"], + "total": ( + b["eg_total"], + f"{gib_number(total, 2)}{plus}", + fee_number(estimate["total_fee"]), + ), + } + + +def _notes(context, estimate, headroom, b): + csp = estimate["meta"]["cloud_service_provider"] + total = estimate["totals"]["known_size_bytes"] + provider = PROVIDER_NAMES.get(csp) or b["eg_provider_fallback"] + unit = _internet_unit(estimate["pricing"], csp) + if unit == "GB" and headroom: + used_gb = total / 10**9 + units = b["html_eg_units_gb"].format( + provider=provider, + gib=gib_number(total), + gb=f"{used_gb:,.1f}", + limit=f"{headroom['limit']:g} {headroom['unit']}", + ) + elif unit == "GB": + units = b["html_eg_units_gb_plain"].format(provider=provider) + else: + units = b["html_eg_units_gib"].format(provider=provider) + + currencies = {entry.currency for entry in context.costs} + if len(currencies) == 1: + amounts = b["html_eg_amounts_costs"].format(currency=next(iter(currencies))) + else: + amounts = b["html_eg_amounts"] + return [f"{units} {amounts}", b["html_eg_scope_note"]] + + +def egress_page(context, estimate, b=BLOCKS): + csp = estimate["meta"]["cloud_service_provider"] + total = estimate["totals"]["known_size_bytes"] + groups = estimate["type_groups"] + sized = sorted( + (group for group in groups if group["size_bytes"] is not None), + key=lambda group: -group["size_bytes"], + ) + unsized = [group for group in groups if group["size_bytes"] is None] + kpis, headroom = _kpis(estimate, sized, unsized, groups, b) + prices = pricing_facts(estimate, b) + return { + "heading": b["ev_egress_report"], + "caption": b["html_eg_caption"].format( + document_id=context.egress_document_id + or context.document_id_for(EGRESS_DOCUMENT_CODE) + ), + "empty": b["eg_no_data_notice"] if not estimate["rows"] else None, + "kpis": kpis, + "volume": _volume(sized, unsized, total, b), + "table": _table(context, estimate, sized, unsized, total, b), + "notes": _notes(context, estimate, headroom, b), + "sized": { + "summary": b["html_eg_sized_summary"], + "facts": coverage_facts(csp, b), + }, + "prices": ( + { + "summary": b["html_eg_prices_summary"].format( + provider=PROVIDER_NAMES.get(csp) or b["eg_provider_fallback"] + ), + "facts": prices, + } + if prices + else None + ), + } diff --git a/cloudexit/report/html_sections.py b/cloudexit/report/html_sections.py index a087a15..e41ef37 100644 --- a/cloudexit/report/html_sections.py +++ b/cloudexit/report/html_sections.py @@ -1,126 +1,854 @@ -from xml.sax.saxutils import escape +from markupsafe import Markup -from .brief_summary import collect_brief_facts, finding_rows, lead_text -from .helpers import severity_label -from .notices import notices_html +from .evidence import scope_rows +from .helpers import ( + format_change, + initials, + format_money, + percent, + report_icon, + severity_label, + strategy_name, +) +from .html_egress import egress_page +from .notices import text_parts from .palette import SEVERITIES -from .sections import egress_note, glossary_entries, scope_boundary, scope_strategy +from .sections import ( + AWS_ICONS_URL, + AWS_TRADEMARK_URL, + AZURE_ICONS_URL, + PRODUCT_SITES, + REPOSITORY_URL, +) from .wording import BLOCKS +ALT_VISIBLE = 8 +ALT_OPEN = 2 +OPEN_RISKS_UP_TO = 3 +AFFECTED_SHOWN = 3 +REGIONS_SHOWN = 5 +PROVIDER_LOGOS = {1: "azure", 2: "aws"} +ABOUT_URL = "#about" -def _version(text): - return f"{escape(text)}" +def _count(b, key, n): + return b[f"{key}_one"] if n == 1 else b[f"{key}_many"].format(n=n) -def html_sections(context, b=BLOCKS): - # The same wording as the PDF; strings that carry markup are already - # escaped, so the template may mark them safe. - facts = collect_brief_facts(context, b) + +def _share(part, whole): + return part / whole * 100 if whole else 0 + + +def _month(month, b): + return b["html_months"][int(month[5:7]) - 1] + + +def _mono(text): + return Markup('{}').format(text) + + +def _regions_line(context, b): + if not context.total_resources: + return b["html_scope_no_resources"] + names = sorted(context.regions) + return f"{_count(b, 'scope_regions', len(names))} · {', '.join(names)}" + + +def _title(context, has_egress, b): provenance = context.provenance + platform_url = provenance.platform_url if provenance.uploaded else None + logo = PROVIDER_LOGOS.get(context.cloud_service_provider) + downloads = [("report.pdf", b["html_download_summary"])] + if has_egress: + downloads.append(("egress.pdf", b["html_download_egress"])) + return { + "logo": f"assets/img/csp/{logo}.svg" if logo else None, + "provider": context.provider, + "name": context.name, + "uploaded": provenance.uploaded, + "chip": b["html_chip_uploaded" if provenance.uploaded else "html_chip_self"], + "chip_title": b[ + ( + "html_chip_uploaded_title" + if provenance.uploaded + else "html_chip_self_title" + ) + ], + "platform_url": platform_url, + "downloads": downloads, + } + - if provenance.catalogue_version and provenance.catalogue_sha256: - catalogue = ( - f"{provenance.catalogue_version} · (sha256 " - f"{provenance.catalogue_sha256[:12]})" +def _scope_card(context, b): + scope = context.scope + values = dict(scope.rows) + regions = _regions_line(context, b) + rows = [{"label": b["ev_cloud_provider"], "value": scope.provider or b["ev_dash"]}] + if scope.kind == "azure": + rows.append( + { + "label": b["ev_resource_group"], + "value": scope.resource_group or b["ev_dash"], + "small": regions, + } + ) + elif scope.kind == "aws": + rows.append( + { + "label": b["ev_region"], + "value": scope.region or b["ev_dash"], + "small": regions, + } ) else: - catalogue = provenance.catalogue_version or b["ev_catalogue_unknown"] + serial = values.get("serial") + small = " · ".join( + part + for part in ( + b["html_scope_serial"].format(serial=serial) if serial else "", + regions, + ) + if part + ) + rows.append( + { + "label": b["ev_file"], + "value": scope.file_name or b["ev_dash"], + "small": small, + } + ) + rows.append( + { + "label": b["cover_exit_strategy"], + "value": strategy_name(context.strategy_code, b), + } + ) + provenance = context.provenance + source = b["html_run_tfstate" if scope.kind == "tfstate" else "html_run_live"] + rows.append( + { + "label": b["html_run_label"], + "value": context.completed, + "small": Markup(b["html_run_small"]).format( + run=_mono(provenance.run_short_id or b["ev_dash"]), source=source + ), + } + ) + sha = provenance.catalogue_sha256 + rows.append( + { + "label": b["ev_catalogue_label"], + "value": provenance.catalogue_version or b["ev_catalogue_unknown"], + "small": b["html_catalogue_sha"].format(sha=sha[:12]) if sha else None, + "small_mono": True, + } + ) + return {"label": b["html_scope_label"], "rows": rows} + +def _risks_card(context, b): + risks = context.risks + counts = { + severity: sum(1 for risk in risks if risk.severity == severity) + for severity in SEVERITIES + } return { - "title": b["ev_executive_report"], - "brief": { - "heading": b["bs_heading"], - "lead": lead_text(facts, b), - "findings_heading": b["bs_findings_heading"], - "headers": (b["bs_th_area"], b["bs_th_finding"], b["bs_th_details"]), - "rows": [ + "label": b["html_risks_label"], + "total": len(risks), + "none": b["bs_find_risks_none"] if not risks else None, + "aria": b["html_sev_aria"].format( + counts=", ".join( + f"{counts[severity]} {severity_label(severity, b).lower()}" + for severity in SEVERITIES + ) + ), + "rows": [ + { + "severity": severity, + "label": severity_label(severity, b), + "count": counts[severity], + "width": round(_share(counts[severity], len(risks))), + } + for severity in SEVERITIES + ], + } + + +def _costs_card(context, b): + if context.scope.kind == "tfstate": + return { + "kind": "message", + "label": b["html_costs_label_short"], + "figure": b["ev_dash"], + "message": b["cost_tfstate"], + "hint": b["html_costs_tfstate_hint"], + } + entries = context.costs + if not entries or len({entry.currency for entry in entries}) > 1: + return { + "kind": "message", + "label": b["html_costs_label"], + "figure": b["ev_dash"], + "message": (b["cost_mixed_currency"] if entries else b["tbl_cost_no_data"]), + "hint": None, + } + + currency = entries[-1].currency + total = sum(entry.amount for entry in entries) + latest = entries[-1] + before = entries[-2] if len(entries) > 1 else None + biggest = max(entry.amount for entry in entries) + provisional = [entry for entry in entries if not entry.final] + + def label(entry): + return _month(entry.month, b) + ("" if entry.final else "*") + + parts = [ + b[ + "html_costs_aria_part" if entry.final else "html_costs_aria_provisional" + ].format( + month=_month(entry.month, b), amount=format_money(entry.amount, currency) + ) + for entry in entries + ] + return { + "kind": "figures", + "label": b["html_costs_label"], + "figure": format_money(total, currency), + "caption": ( + b["html_costs_none"] + if total == 0 + else b["html_costs_average"].format( + amount=format_money(total / len(entries), currency) + ) + ), + "change": format_change( + before.amount if before else None, latest.amount + ).replace(" %", " %"), + "change_caption": ( + b["html_costs_change"].format( + latest=_month(latest.month, b), previous=_month(before.month, b) + ) + if before + else "" + ), + "bars": [ + { + "month": label(entry), + "height": round(_share(entry.amount, biggest)) if biggest else 0, + "zero": entry.amount <= 0, + "provisional": not entry.final, + } + for entry in entries + ], + "aria": b["html_costs_aria"].format(parts=", ".join(parts)), + "footnote": ( + b["html_costs_provisional"].format(month=_month(provisional[0].month, b)) + if provisional + else b["html_costs_context"] + ), + } + + +def _hint(b): + return { + "title": b["html_hint_title"], + "text": b["html_hint_text"], + "sites": [ + (b["html_site_exitcloud"], dict(PRODUCT_SITES)["exitcloud"]), + (b["html_site_escapecloud"], dict(PRODUCT_SITES)["escapecloud"]), + ], + } + + +def _scores(alternatives_total, platform_url, b): + on_platform = f" {b['html_scores_on_platform']}" if platform_url else "" + lock_text = ( + b["html_scores_lock_text"].format( + alternatives=_count(b, "html_alt_n", alternatives_total) + ) + if alternatives_total + else b["html_scores_lock_text_plain"] + ) + return { + "platform_url": platform_url, + "open_platform": b["html_open_platform"], + "pill": b["html_scores_pill"], + "hint": _hint(b), + "grade": { + "title": b["html_scores_grade"], + "placeholder": b["ev_dash"], + "scale": b["html_scores_scale"], + "bands": b["html_scores_bands"], + "text": b["html_scores_grade_text"] + on_platform, + }, + "lock": { + "title": b["html_scores_lock"], + "dims": [ + (name, b["html_scores_dim_value"].format(dash=b["ev_dash"])) + for name in b["html_scores_dims"] + ], + "text": lock_text + on_platform, + }, + } + + +def _risks(context, b): + risks = context.risks + names = {entry.resource_type_id: entry.name for entry in context.resources} + wide = sum(1 for risk in risks if risk.scope_wide) + if not risks: + caption = b["html_risks_none_caption"] + else: + count = _count(b, "html_risks_count", len(risks)) + if not wide: + caption = b["html_risks_caption_only_count"].format(count=count) + else: + if wide == len(risks): + key = "html_risks_wide_sole" if wide == 1 else "html_risks_wide_all" + else: + key = "html_risks_wide_one" if wide == 1 else "html_risks_wide_many" + caption = b["html_risks_caption"].format( + count=count, scope=b[key].format(n=wide) + ) + + def affected(risk): + if risk.scope_wide: + return None + listed = sorted( + names.get(type_id, b["ev_dash"]) for type_id in risk.resource_type_ids + ) + text = ", ".join(listed[:AFFECTED_SHOWN]) + if len(listed) > AFFECTED_SHOWN: + text += " " + b["html_risks_more"].format(n=len(listed) - AFFECTED_SHOWN) + return text + + groups, position = [], 0 + for severity in SEVERITIES: + members = [risk for risk in risks if risk.severity == severity] + rows = [] + for risk in members: + position += 1 + rows.append( { - "area": area, - "finding": finding, - "details": details, - "attention": attention, + "id": f"risk-{position}", + "code": risk.code, + "name": risk.name, + "description": risk.description, + "severity": severity, + "label": severity_label(severity, b), + "affected": affected(risk), } - for area, finding, details, attention in finding_rows(facts, b) - ], - "caption": b["bs_caption"].format( - version=_version(facts.version), completed=escape(facts.completed) + ) + if rows: + groups.append( + { + "heading": b["risk_group"].format( + severity=severity_label(severity, b), n=len(rows) + ), + "rows": rows, + } + ) + return { + "heading": b["html_nav_risks"], + "caption": caption, + "headers": ( + b["risk_th_code"], + b["risk_th_risk"], + b["risk_th_severity"], + b["html_risks_th_affected"], + ), + "scope_wide": b["risk_scope_wide"], + "groups": groups, + "open": len(risks) <= OPEN_RISKS_UP_TO, + "footnote": text_parts( + b["html_risks_footnote"], {"link": (b["html_nav_about"], ABOUT_URL)} + ), + "empty": ( + { + "title": b["bs_find_risks_none"], + "text": b[ + ( + "html_risks_empty_clean" + if context.total_resources + else "html_risks_empty_scope" + ) + ], + } + if not risks + else None + ), + } + + +def _inventory(context, b): + total = context.total_resources + scope = context.scope + regions = {} + for entry in context.resources: + for location, count in entry.location_counts.items(): + regions[location] = regions.get(location, 0) + count + ranked = sorted(regions.items(), key=lambda item: (-item[1], item[0])) + shown = [ + f"{name} {percent(_share(count, total))}" + for name, count in ranked[:REGIONS_SHOWN] + ] + if len(ranked) > REGIONS_SHOWN: + shown.append(b["html_risks_more"].format(n=len(ranked) - REGIONS_SHOWN)) + caption_parts = [_count(b, "scope_resources", total)] + if total: + caption_parts += [ + _count(b, "scope_types", len(context.resources)), + b["html_inv_regions"].format( + regions=_count(b, "scope_regions", len(regions)), + shares=", ".join(shown), ), - }, - "provenance": { - "heading": b["ev_provenance_heading"], + ] + if scope.kind == "tfstate": + caption_parts.append(b["html_inv_not_managed"]) + + if scope.kind == "azure": + empty = b["html_inv_empty_resource_group"].format( + scope=scope.resource_group or b["ev_dash"] + ) + elif scope.kind == "aws": + empty = b["html_inv_empty_region"].format(scope=scope.region or b["ev_dash"]) + else: + empty = b["html_inv_empty_tfstate"].format( + scope=scope.file_name or b["ev_dash"] + ) + + return { + "heading": b["es_heading_resource_inventory"], + "caption": " · ".join(caption_parts), + "headers": ( + b["res_th_resource_type"], + b["html_inv_th_locations"], + b["res_th_share"], + b["res_th_resources"], + ), + "rows": [ + { + "name": entry.name, + "initials": initials(entry.name), + "icon": report_icon(context.report_path, entry.icon_rel), + "locations": ", ".join( + f"{name} · {count}" + for name, count in sorted( + entry.location_counts.items(), + key=lambda item: (-item[1], item[0]), + ) + ), + "share": percent(_share(entry.resources, total)), + "share_value": round(_share(entry.resources, total)), + "count": entry.resources, + } + for entry in context.resources + ], + "total": (b["res_total"], total), + "footnote": b["es_res_caption"], + "empty": ( + {"title": b["html_inv_empty_title"], "text": empty} if not total else None + ), + } + + +def _alternatives(context, b): + entries = sorted( + context.alternatives, + key=lambda entry: ( + not entry.alternatives, + -len(entry.alternatives), + -entry.resources, + entry.name.lower(), + ), + ) + total = sum(len(entry.alternatives) for entry in entries) + with_alternatives = sum(1 for entry in entries if entry.alternatives) + provenance = context.provenance + + groups = [] + for position, entry in enumerate(entries): + rows = [ + { + "name": alt.name, + "vendor": alt.vendor or b["ev_dash"], + "cc": alt.country or "", + "os": alt.open_source, + "sp": alt.support_plan, + "extra": index >= ALT_VISIBLE, + } + for index, alt in enumerate(entry.alternatives) + ] + resources = _count(b, "scope_resources", entry.resources) + hidden = max(0, len(rows) - ALT_VISIBLE) + groups.append( + { + "name": entry.name, + "initials": initials(entry.name), + "icon": report_icon(context.report_path, entry.icon_rel), + "meta": ( + b["html_alt_meta"].format( + resources=resources, + alternatives=_count(b, "html_alt_n", len(rows)), + ) + if rows + else b["html_alt_meta_none"].format(resources=resources) + ), + "rows": rows, + "open": position < ALT_OPEN and bool(rows), + "hidden": hidden, + "more": (_count_more(b, hidden) if hidden else None), + } + ) + + if not entries: + caption = _count(b, "html_alt_n", 0) + else: + key = "html_alt_caption" if len(entries) != 1 else "html_alt_caption_one_type" + caption = b[key].format( + alternatives=_count(b, "html_alt_n", total), + with_alternatives=with_alternatives, + types=len(entries), + ) + return { + "heading": b["es_heading_alternative_technologies"], + "caption": caption, + "total": total, + "search": b["html_alt_search"], + "filters": [ + ("os", b["html_filter_os"]), + ("sp", b["html_filter_sp"]), + ("eu", b["html_filter_eu"]), + ], + "headers": (b["html_alt_th_name"], b["html_filter_os"], b["html_filter_sp"]), + "yes": b["html_yes"], + "no": b["html_no"], + "no_match": b["html_alt_no_match"], + "groups": groups, + "footnote": b["html_alt_footnote"].format( + version=provenance.catalogue_version or b["ev_catalogue_unknown"] + ), + "empty": ( + {"title": b["html_alt_empty_title"], "text": b["html_alt_empty_text"]} + if not entries + else None + ), + } + + +def _count_more(b, n): + return b["html_alt_more_one"] if n == 1 else b["html_alt_more_many"].format(n=n) + + +def _platform_cell(context, b): + provenance = context.provenance + if not provenance.uploaded: + if provenance.upload_reason: + return [ + b["html_platform_not_uploaded"].format(reason=provenance.upload_reason) + ] + return [b["ev_platform_none"]] + parts = [b["ev_platform_uploaded"]] + if provenance.platform_url: + label = provenance.platform_url.split("://", 1)[-1] + if len(label) > 40: + label = label[:40] + "…" + return [f"{parts[0]}: ", (label, provenance.platform_url)] + if provenance.assessment_id: + parts.append(b["fa_uploaded_id"].format(assessment_id=provenance.assessment_id)) + return [" · ".join(parts)] + + +def _scope_table(context, b): + rows = scope_rows(context, b) + if context.scope.kind != "tfstate": + return rows + # A state file names no tenant, subscription or resource group of a run: its + # own file and state, and the regions of what it holds + dropped = {b["ev_subscriptions"], b["ev_resource_groups"]} + rows = [row for row in rows if row[0] not in dropped] + if not any(row[0] == b["ev_regions"] for row in rows): + regions = ", ".join(sorted(context.regions)) or b["ev_not_determinable"] + rows.insert(-1, (b["ev_regions"], regions, False)) + return rows + + +def _about(context, egress, b): + provenance = context.provenance + tfstate = context.scope.kind == "tfstate" + sha = provenance.catalogue_sha256 + glossary = [ + entry + for entry in b["html_glossary"] + if (entry["id"] != "aws" or context.cloud_service_provider == 2) + and (entry["id"] not in ("egress", "free_tier", "gib_gb") or egress is not None) + ] + phases = [ + ( + name, + b["html_method_discovery_tfstate"] if tfstate and index == 0 else text, + ) + for index, (name, text) in enumerate(b["es_method_phases"]) + ] + return { + "heading": b["html_nav_about"], + "caption": b["html_about_caption"], + "document": { + "heading": b["html_about_document"], "rows": [ + (b["ev_document_id"], context.document_id, True), + (b["ev_document_type"], b["ev_executive_report"], False), + (b["ev_language"], b["language_name"], False), ( b["ev_collected_by"], b["ev_collected_by_value"].format( version=provenance.collector_version ), + False, ), - (b["ev_collected_at"], provenance.collected_date), - *([(b["ev_run_id"], provenance.run_id)] if provenance.run_id else []), - (b["ev_catalogue_label"], catalogue), ( - b["ev_platform_label"], - ( - b["ev_platform_uploaded"] - if provenance.uploaded - else b["ev_platform_none"] - ), + b["ev_generated_at"], + provenance.generated_at.strftime("%Y-%m-%d %H:%M UTC"), + False, ), ], - "platform_url": provenance.platform_url, - "document_id": context.document_id, }, - "costs_egress_note": egress_note(context, b), - "introduction": { - "heading": b["es_heading_introduction"], - "purpose_heading": b["es_heading_purpose"], - "purpose": b["es_purpose_block"], - "scope_heading": b["es_heading_scope"], - "scope": [scope_boundary(context, b), scope_strategy(context, b)], - "method_heading": b["es_heading_method"], - "method": [b["es_method_lead"], b["es_method_phases_lead"]], - "phases": b["es_method_phases"], - "method_catalogue": b["es_method_catalogue"], - "limitations_heading": b["es_heading_limitations"], - "limitations": [b["err_limitations"], b["err_limitations_data"]], + "scope": { + "heading": b["ev_assessment_scope_heading"], + "rows": _scope_table(context, b), + }, + "provenance": { + "heading": b["ev_provenance_heading"], + "run_id": ( + (b["ev_run_id"], provenance.run_id) if provenance.run_id else None + ), + "catalogue": ( + b["ev_catalogue_label"], + provenance.catalogue_version or b["ev_catalogue_unknown"], + b["html_catalogue_sha"].format(sha=sha[:12]) if sha else None, + ), + "platform": (b["ev_platform_label"], _platform_cell(context, b)), + }, + "stored": b["ev_stored_footnote"].format( + run=provenance.run_short_id or b["ev_dash"] + ), + "purpose": { + "summary": b["html_about_purpose"], + "meta": b["html_about_phases_meta"].format(n=len(phases)), + "paragraphs": [b["es_purpose_block"], b["html_method_lead"]], + "phases": phases, + "closing": b["html_method_catalogue"], + }, + "limitations": { + "summary": b["es_heading_limitations"], + "paragraphs": [b["err_limitations"], b["html_limitations_data"]], }, "severity": { - "heading": b["es_heading_severity"], - "lead": [b["es_severity_lead"], b["es_severity_note"]], + "summary": b["es_heading_severity"], + "meta": b["html_about_levels_meta"].format(n=len(SEVERITIES)), "headers": (b["risk_th_severity"], b["es_severity_th_meaning"]), "rows": [ - (severity_label(level, b), b[f"es_severity_{level}"]) + (level, severity_label(level, b), b[f"es_severity_{level}"]) for level in SEVERITIES ], + "note": b["es_severity_note"], }, "glossary": { - "heading": b["es_heading_glossary"], - "lead": b["es_glossary_block"], + "summary": b["es_heading_glossary"], + "meta": b["html_about_terms_meta"].format(n=len(glossary)), "entries": [ (entry["term"], entry["definition"]) - for entry in sorted( - glossary_entries(context, b), key=lambda e: e["term"].lower() - ) + for entry in sorted(glossary, key=lambda e: e["term"].lower()) ], }, - "closing": { - "heading": b[ - "fa_heading_uploaded" if provenance.uploaded else "fa_heading" - ], - "uploaded": provenance.uploaded, - "lead": b["fa_uploaded"] if provenance.uploaded else b["fa_lead"], - "points": [] if provenance.uploaded else b["fa_items"], - "how_heading": b["fa_how_heading"], - "how": b["fa_how"], - "command": b["fa_command"], - "sites": [ - ("EscapeCloud", "https://escapecloud.io"), - ("ExitCloud", "https://exitcloud.io"), + } + + +def _notices(context, egress, b): + provenance = context.provenance + icons = None + if context.cloud_service_provider == 2: + icons = text_parts( + b["notices_icons_aws"], + { + "icons_url": (b["html_label_aws_icons"], AWS_ICONS_URL), + "guidelines_url": (b["html_label_aws_guidelines"], AWS_TRADEMARK_URL), + }, + ) + elif context.cloud_service_provider == 1: + icons = text_parts( + b["notices_icons_azure"], + {"icons_url": (b["html_label_azure_icons"], AZURE_ICONS_URL)}, + ) + if context.scope.kind == "tfstate": + figures = b["html_figures_tfstate"] + else: + figures = b["html_figures_egress" if egress is not None else "html_figures"] + return { + "disclaimer": { + "heading": b["notices_disclaimer_heading"], + "paragraphs": [ + [b["notices_disclaimer"]], + [f"{b['notices_generated']} {b['html_notice_advice']}"], + *([icons] if icons else []), ], - "url": provenance.platform_url, - "assessment_id": provenance.assessment_id, }, - "notices": notices_html( - context.cloud_service_provider, provenance.generated_at, b + "blocks": [ + { + "heading": b["notices_trademarks_heading"], + "parts": [ + f"{b['notices_trademark_own']} {b['notices_trademark_others']}" + ], + }, + {"heading": b["html_figures_heading"], "parts": [figures]}, + { + "heading": b["notices_software_heading"], + "parts": text_parts( + b["html_software"], + {"repository_link": (b["html_label_repository"], REPOSITORY_URL)}, + ), + }, + ], + "copyright": b["notices_copyright"].format(year=provenance.generated_at.year), + "ids": b["html_footer_ids"].format( + document_id=context.document_id, version=provenance.collector_version + ), + } + + +def _tour(context, egress, linked, b): + tfstate = context.scope.kind == "tfstate" + sections = [b["html_tour_sections"]] + if not linked: + sections.append(b["html_tour_sections_lock"]) + report = [b["html_tour_report"]] + if linked: + report.append(b["html_tour_report_platform"]) + if egress is not None: + report.append( + b[ + ( + "html_tour_download_both_platform" + if linked + else "html_tour_download_both" + ) + ] + ) + else: + report.append(b["html_tour_download_one"]) + scores = ( + b["html_tour_scores_platform"] + if linked + else f"{b['html_tour_scores_available']} {b['html_hint_text']}" + ) + steps = [ + (".secnav", b["html_tour_sections_title"], " ".join(sections), "right"), + (".titlebar", b["html_tour_report_title"], " ".join(report), "bottom"), + ( + "#overview .overview", + b["html_tour_overview_title"], + b["html_tour_overview_tfstate" if tfstate else "html_tour_overview"], + "bottom", + ), + ("#scores .scores", b["html_tour_scores_title"], scores, "bottom"), + ( + "#risks .card-head", + b["html_tour_risks_title"], + b["html_tour_risks"], + "bottom", ), + ( + "#inventory .card-head", + b["html_tour_inventory_title"], + b["html_tour_inventory"], + "bottom", + ), + ( + "#alternatives .card-head", + b["html_tour_alternatives_title"], + b["html_tour_alternatives"], + "bottom", + ), + ] + if egress is not None: + steps.append( + ( + "#egress .kpi-row", + b["html_tour_egress_title"], + b["html_tour_egress"], + "bottom", + ) + ) + steps.append( + ("#about .kv-grid", b["html_tour_about_title"], b["html_tour_about"], "top") + ) + return [ + {"element": element, "title": title, "description": text, "side": side} + for element, title, text, side in steps + ] + + +def html_sections(context, egress=None, b=BLOCKS): + # Everything the page says, as plain data; the template only lays it out + provenance = context.provenance + platform_url = provenance.platform_url if provenance.uploaded else None + alternatives = _alternatives(context, b) + nav = [ + {"id": "overview", "label": b["html_nav_overview"]}, + { + "id": "scores", + "label": b["html_nav_scores"], + "lock": b["html_lock_label"] if not platform_url else None, + }, + {"id": "risks", "label": b["html_nav_risks"], "count": len(context.risks)}, + { + "id": "inventory", + "label": b["es_heading_resource_inventory"], + "count": context.total_resources, + }, + { + "id": "alternatives", + "label": b["es_heading_alternative_technologies"], + "count": alternatives["total"], + }, + ] + if egress is not None: + nav.append({"id": "egress", "label": b["ev_egress_report"]}) + nav.append({"id": "about", "label": b["html_nav_about"]}) + + return { + "title": f"{b['ev_executive_report']} – {context.name}", + "language": "en", + "dash": b["ev_dash"], + "header": { + "tag": b["cover_edition_value"], + "logo_link": b["html_logo_link"], + "help": b["html_help"], + "help_aria": b["html_help_aria"], + "tour": { + "next": b["html_tour_next"], + "back": b["html_tour_back"], + "done": b["html_tour_done"], + "progress": b["html_tour_progress"], + }, + }, + "titlebar": _title(context, egress is not None, b), + "download": b["html_download"], + "open_platform": b["html_open_platform"], + "nav": nav, + "nav_label": b["html_nav_label"], + "overview": { + "scope": _scope_card(context, b), + "risks": _risks_card(context, b), + "costs": _costs_card(context, b), + }, + "scores": _scores(alternatives["total"], platform_url, b), + "risks": _risks(context, b), + "inventory": _inventory(context, b), + "alternatives": alternatives, + "egress": egress_page(context, egress, b) if egress is not None else None, + "about": _about(context, egress, b), + "notices": _notices(context, egress, b), + "tour": _tour(context, egress, bool(platform_url), b), } diff --git a/cloudexit/report/notices.py b/cloudexit/report/notices.py index 866a7c8..60c96d7 100644 --- a/cloudexit/report/notices.py +++ b/cloudexit/report/notices.py @@ -27,7 +27,9 @@ def text_parts(template, links, values=None): for piece in FIELD.split(template): field = piece[1:-1] if field in links: - parts.append((links[field], links[field])) + link = links[field] + # A (label, url) pair shows the label; a bare url shows itself + parts.append(link if isinstance(link, tuple) else (link, link)) elif values and field in values: parts.append(str(values[field])) elif piece: diff --git a/cloudexit/report/wording.py b/cloudexit/report/wording.py index 246033b..86cc08f 100644 --- a/cloudexit/report/wording.py +++ b/cloudexit/report/wording.py @@ -280,10 +280,10 @@ "alt_hotspot_short": "No alternative in the data catalogue — lock-in hot-spot", "alt_more": "+{k} more", "alt_flag_open_source": "Open source", - "alt_flag_support_plan": "Support plan", + "alt_flag_support_plan": "Enterprise support", "alt_no_resources": "No resources were discovered in this assessment, so there are no alternatives to show.", "alt_empty_set": "No alternative technologies were found for the discovered resource types.", - "es_alttech_caption": "Examples are listed alphabetically; this report does not rank the alternatives. Open source and support plan as recorded in the data catalogue.", + "es_alttech_caption": "Examples are listed alphabetically; this report does not rank the alternatives. Open source and enterprise support as recorded in the data catalogue.", "es_heading_severity": "Risk Severity Levels", "es_severity_lead": ( "Severity expresses the expected impact of a risk on the effort and " @@ -639,14 +639,17 @@ ), "notices_trademarks_heading": "Trademarks", "notices_trademark_own": ( - "EscapeCloud, cloudexit and the EscapeCloud logo are trademarks. The " - "AGPL-3.0 licence does not grant any right to use them; modified " - "versions must not use them in a way that suggests they are official." + "EscapeCloud® is a registered trademark in the European Union and the " + "United States. The AGPL-3.0 licence applies solely to the cloudexit " + "software, not to the EscapeCloud name, logo, or brand assets. You may " + "not use EscapeCloud trademarks in connection with modified versions or " + "downstream distributions in any manner that implies endorsement, " + "affiliation, or origin with EscapeCloud." ), "notices_trademark_others": ( - "Other product and company names in this report, including those of " - "alternative technologies, are trademarks of their respective owners. " - "Naming them does not imply endorsement or affiliation." + "Other product and company names and logos are trademarks of their " + "respective owners; naming them does not imply endorsement or " + "affiliation." ), "notices_icons_aws": ( "AWS service icons are taken from the AWS Architecture Icons " @@ -671,6 +674,365 @@ "licence." ), "notices_copyright": "© 2024–{year} EscapeCloud", + "html_help": "Help", + "html_help_aria": "Help: how to read this report", + "html_logo_link": "EscapeCloud website (opens in a new tab)", + "html_chip_self": "Self-collected", + "html_chip_self_title": "Collected and evaluated on this machine; not uploaded", + "html_chip_uploaded": "Uploaded", + "html_chip_uploaded_title": "Collected on this machine and uploaded to the platform", + "html_download": "Download", + "html_download_summary": "Executive Summary (PDF)", + "html_download_egress": "Egress & Data Landscape (PDF)", + "html_open_platform": "Open on the platform", + "html_nav_label": "Sections", + "html_nav_overview": "Overview", + "html_nav_scores": "Scores", + "html_nav_risks": "Risks", + "html_nav_about": "About This Report", + "html_lock_label": "Light and Enterprise", + "html_scope_label": "Scope", + "html_run_label": "Run", + "html_run_small": "run {run} · {source}", + "html_run_live": "provider APIs", + "html_run_tfstate": "Terraform state", + "html_scope_no_resources": "no resources found", + "html_scope_serial": "serial {serial}", + "html_catalogue_sha": "sha256 {sha}", + "html_risks_label": "Unique Risks", + "html_sev_aria": "Risks by severity: {counts}", + "html_costs_label": "Costs, Last 6 Months", + "html_costs_label_short": "Costs", + "html_costs_average": "{amount} per month on average", + "html_costs_none": "no costs in the last six months", + "html_costs_change": "{latest} vs. {previous}", + "html_costs_aria": "Monthly costs: {parts}", + "html_costs_aria_part": "{month} {amount}", + "html_costs_aria_provisional": "{month} {amount} provisional", + "html_costs_provisional": ( + "* {month} is provisional: the month was still accruing. Costs are " + "context only; they are not an exit-cost estimate." + ), + "html_costs_context": "Costs are context only; they are not an exit-cost estimate.", + "html_costs_tfstate_hint": ( + "Run cloudexit against the provider APIs to include the last six " + "months of costs." + ), + "html_months": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ], + "html_scores_grade": "Exit Readiness Grade", + "html_scores_lock": "Vendor Lock-in Score", + "html_scores_pill": "Light & Enterprise", + "html_scores_scale": "of 100", + "html_scores_bands": ["F", "E", "D", "C", "B", "A"], + "html_scores_dims": ["Human", "Technology", "Operational"], + "html_scores_dim_value": "{dash} / 5", + "html_scores_grade_text": ( + "A grade from A to F for how ready this scope is for an exit, computed " + "from the risks in this report." + ), + "html_scores_lock_text": ( + "How easily each resource type can be replaced, in skills, technology " + "and operations, with the {alternatives} rated and ranked." + ), + "html_scores_lock_text_plain": ( + "How easily each resource type can be replaced, in skills, technology " + "and operations, with the alternatives rated and ranked." + ), + "html_scores_on_platform": "For this run it is on the platform.", + "html_platform_not_uploaded": "Not uploaded: {reason}", + "html_hint_title": "EscapeCloud Light & Enterprise", + "html_hint_text": ( + "EscapeCloud's scoring engine offers several scoring methods to assess " + "how feasible an exit is, and tracks the continuously changing vendor " + "landscape of alternative technologies." + ), + "html_site_exitcloud": "exitcloud.io", + "html_site_escapecloud": "escapecloud.io", + "html_risks_caption": "{count} · {scope} · ordered by severity", + "html_risks_caption_only_count": "{count} · ordered by severity", + "html_risks_count_one": "1 unique risk", + "html_risks_count_many": "{n} unique risks", + "html_risks_wide_sole": "applies to the scope as a whole", + "html_risks_wide_all": "all apply to the scope as a whole", + "html_risks_wide_one": "1 applies to the scope as a whole", + "html_risks_wide_many": "{n} apply to the scope as a whole", + "html_risks_none_caption": "No exit risks", + "html_risks_th_affected": "Affected Resource Types", + "html_risks_more": "+{n} more", + "html_risks_footnote": ( + "Risk codes are stable across runs. Severity levels and their meaning: " + "{link}." + ), + "html_risks_empty_scope": ( + "With no resources in the scope, no risk from the data catalogue applies." + ), + "html_risks_empty_clean": ( + "Every resource type has enough alternatives with enterprise support, " + "and the size of the scope is below the thresholds." + ), + "html_inv_caption": "{resources} · {types} · {regions}", + "html_inv_regions": "{regions} ({shares})", + "html_inv_not_managed": "resources that Terraform does not manage are not assessed", + "html_inv_th_locations": "Locations", + "html_inv_empty_title": "No resources were found in the defined scope.", + "html_inv_empty_resource_group": ( + "Check that the resource group {scope} is the intended scope and that " + "the credentials can read it. Resource types not in the data catalogue " + "are not counted." + ), + "html_inv_empty_region": ( + "Check that the region {scope} is the intended scope and that the " + "credentials can read it. Resource types not in the data catalogue are " + "not counted." + ), + "html_inv_empty_tfstate": ( + "Check that the state file {scope} holds resources of this provider. " + "Resource types not in the data catalogue are not counted." + ), + "html_alt_caption": "{alternatives} for {with_alternatives} of {types} resource types", + "html_alt_caption_one_type": "{alternatives} for {with_alternatives} of 1 resource type", + "html_alt_n_one": "1 alternative", + "html_alt_n_many": "{n} alternatives", + "html_alt_search": "Search alternatives", + "html_filter_os": "Open Source", + "html_filter_sp": "Enterprise Support", + "html_filter_eu": "European Vendors", + "html_alt_th_name": "Alternative", + "html_alt_meta": "{resources} · {alternatives}", + "html_alt_meta_none": "{resources} · no alternative in the data catalogue", + "html_alt_more_one": "Show 1 more alternative", + "html_alt_more_many": "Show {n} more alternatives", + "html_alt_no_match": "No alternative matches the search and filters.", + "html_alt_footnote": ( + "Alternatives are listed alphabetically from the data catalogue " + "({version}); this report doesn't rank them. Open source and " + "enterprise support as recorded in the data catalogue. Country = " + "vendor's registered country." + ), + "html_alt_empty_title": "No resource types to list alternatives for.", + "html_alt_empty_text": ( + "Alternatives appear here for each resource type found in the scope." + ), + "html_yes": "Yes", + "html_no": "No", + "html_eg_caption": ( + "Data held by the data-bearing resources, and the one-off fee to " + "transfer it out at list prices · also as PDF {document_id}" + ), + "html_eg_kpi_fee": "Estimated Transfer Fee", + "html_eg_kpi_headroom": "Free-Tier Headroom", + "html_eg_fee_free": "within the monthly free tier · USD list prices", + "html_eg_fee_paid": "at list prices · USD", + "html_eg_types_sized": "{sized} of {types} types sized", + "html_eg_headroom_left": "of {limit} per month left", + "html_eg_headroom_aria": "{share} of the free tier used", + "html_eg_billable_caption": "no free tier in the prices used", + "html_eg_not_sized_caption": "listed, not in the fee", + "html_eg_not_sized": "not sized", + "html_eg_vol_aria": "Share of data volume: {parts}", + "html_eg_vol_part": "{type} {share}", + "html_eg_vol_other": "Other types", + "html_eg_vol_unsized": "{types} not sized", + "html_eg_units_gb": ( + "Data is shown in GiB; {provider} prices egress per GB (1 GiB = 1.074 " + "GB), so {gib} GiB is {gb} GB of the {limit} monthly free tier." + ), + "html_eg_units_gb_plain": ( + "Data is shown in GiB; {provider} prices egress per GB (1 GiB = 1.074 GB)." + ), + "html_eg_units_gib": "Data is shown in GiB; {provider} prices egress per GiB.", + "html_eg_amounts": "Amounts in USD list prices; not converted.", + "html_eg_amounts_costs": ( + "Amounts in USD list prices; not converted and not added to the costs " + "above ({currency}, monthly)." + ), + "html_eg_scope_note": ( + "Network transfer fees only: request charges, cross-region traffic, " + "private connections and negotiated discounts are not included. Some " + "types report provisioned rather than used capacity. Not an estimate " + "of the full cost of an exit." + ), + "html_eg_sized_summary": "What is sized, and how", + "html_eg_prices_summary": "Prices used ({provider}, zone 1, public internet routing)", + "html_about_caption": ( + "Document, scope and provenance · method, limitations, severity levels " + "and terms" + ), + "html_about_document": "Document", + "html_about_purpose": "Purpose and Method", + "html_about_phases_meta": "{n} phases", + "html_about_levels_meta": "{n} levels", + "html_about_terms_meta": "{n} terms", + "html_method_lead": ( + "The assessment was collected and evaluated locally with cloudexit. " + "Once the scope is defined, it runs three phases:" + ), + "html_method_discovery_tfstate": ( + "reads the resources managed in the Terraform state and classifies " + "them by type and region; a state file holds no costs." + ), + "html_method_catalogue": ( + "Risks and alternatives are taken from the data catalogue version shown " + "above, so the results of this run can be reproduced." + ), + "html_limitations_data": ( + "The analysis assumes that the discovered resources are correctly " + "configured and accessible. Dependencies outside the defined scope, " + "such as on-premises systems or other cloud accounts, are not covered; " + "results are indicative, not exhaustive." + ), + "html_glossary": [ + { + "id": "aws", + "term": "AWS", + "definition": "Amazon Web Services", + }, + { + "id": "data_catalogue", + "term": "Data catalogue", + "definition": "The catalogue of resource types, exit risks and alternative technologies used for this assessment.", + }, + { + "id": "hotspot", + "term": "Lock-in hot-spot", + "definition": "A discovered resource type for which the data catalogue lists no alternative.", + }, + { + "id": "run", + "term": "Run", + "definition": "One completed assessment of the defined scope, identified by its run ID.", + }, + { + "id": "egress", + "term": "Egress", + "definition": "Data transferred out of the provider's network, which the provider bills per unit of volume.", + }, + { + "id": "free_tier", + "term": "Free tier", + "definition": "The volume of internet egress per month that the provider does not bill.", + }, + { + "id": "gib_gb", + "term": "GiB / GB", + "definition": "Gibibyte (2³⁰ bytes) and gigabyte (10⁹ bytes); 1 GiB = 1.074 GB.", + }, + ], + "html_notice_advice": ( + "It is not legal, financial or investment advice, an audit or an exit plan." + ), + "html_label_aws_icons": "AWS Architecture Icons", + "html_label_aws_guidelines": "AWS Trademark Guidelines", + "html_label_azure_icons": "Azure Architecture Icons", + "html_label_repository": "github.com/escapecloud/cloudexit", + "html_figures_heading": "Figures", + "html_figures": ( + "Costs come from the provider's billing data for the scope. " + "Alternatives are entries from the data catalogue, not recommendations." + ), + "html_figures_egress": ( + "Costs come from the provider's billing data for the scope; egress " + "prices are public list prices at the time of the run. Alternatives " + "are entries from the data catalogue, not recommendations." + ), + "html_figures_tfstate": ( + "Resources are read from the Terraform state file; a state file holds " + "no billing data, so this report shows no costs. Alternatives are " + "entries from the data catalogue, not recommendations." + ), + "html_software": ( + "cloudexit is free software, licensed under the GNU Affero General " + "Public License v3.0 (AGPL-3.0). Source code and issues: " + "{repository_link}. The provider icons are not covered by this " + "licence. Fonts: IBM Plex, SIL Open Font License 1.1. Guided tour: " + "driver.js, MIT License, included in this report's assets." + ), + "html_footer_ids": "{document_id} · cloudexit {version}", + "html_tour_next": "Next", + "html_tour_back": "Back", + "html_tour_done": "Done", + "html_tour_progress": "{{current}} of {{total}}", + "html_tour_sections_title": "Sections", + "html_tour_sections": ( + "Jump to any part of the report; the section in view is highlighted." + ), + "html_tour_sections_lock": ( + "Scores, marked with a lock, are available in EscapeCloud Light & " + "Enterprise." + ), + "html_tour_report_title": "This Report", + "html_tour_report": "The assessed scope and its cloud provider.", + "html_tour_report_platform": ( + "This run was uploaded: Open on the platform shows its full results." + ), + "html_tour_download_both": ( + "Download has the same results as PDF: the Executive Summary and the " + "Egress & Data Landscape." + ), + "html_tour_download_both_platform": ( + "Download has the Executive Summary and the Egress & Data Landscape as " "PDF." + ), + "html_tour_download_one": "Download has the Executive Summary as PDF.", + "html_tour_overview_title": "Overview", + "html_tour_overview": ( + "Scope, unique risks by severity and the last six months of costs. The " + "current month is provisional while it is still accruing; costs are " + "context, not an exit-cost estimate." + ), + "html_tour_overview_tfstate": ( + "Scope and unique risks by severity. A Terraform state holds no " + "billing data, so there are no costs; resources that Terraform does " + "not manage are not assessed." + ), + "html_tour_scores_title": "Exit Readiness Grade & Vendor Lock-in Score", + "html_tour_scores_available": "Available in EscapeCloud Light & Enterprise.", + "html_tour_scores_platform": ( + "This run was uploaded. Its Exit Readiness Grade and Vendor Lock-in " + "Score are on the platform: use Open on the platform. This local " + "report never shows platform scores." + ), + "html_tour_risks_title": "Risks", + "html_tour_risks": ( + "Exit risks found in the scope, ordered by severity. Each risk has a " + "stable code, so a finding can be followed from one run to the next. " + "Open a row for its description." + ), + "html_tour_inventory_title": "Resource Inventory", + "html_tour_inventory": ( + "One row per resource type, with its regions and its share of all " + "resources in the scope." + ), + "html_tour_alternatives_title": "Alternative Technologies", + "html_tour_alternatives": ( + "Replacement options from the data catalogue for each resource type, " + "listed alphabetically and not ranked. Search, or filter by open " + "source, enterprise support or European vendors." + ), + "html_tour_egress_title": "Egress & Data Landscape", + "html_tour_egress": ( + "Data held in the scope and the one-off fee to transfer it out at " + "public list prices. Data is shown in GiB; the provider prices per GB. " + "Types marked 'not sized' are listed but not in the fee." + ), + "html_tour_about_title": "About This Report", + "html_tour_about": ( + "Document ID, scope and provenance. The run ID and the data catalogue " + "version make the result reproducible; method, limitations, severity " + "levels and terms are below." + ), } # Labels and headings that follow English Title Case, for the wording test. @@ -784,4 +1146,41 @@ "fa_heading", "fa_how_heading", "fa_heading_uploaded", + "html_download", + "html_download_summary", + "html_download_egress", + "html_nav_overview", + "html_nav_scores", + "html_nav_risks", + "html_nav_about", + "html_scope_label", + "html_risks_label", + "html_costs_label", + "html_costs_label_short", + "html_scores_grade", + "html_scores_lock", + "html_scores_pill", + "html_risks_th_affected", + "html_inv_th_locations", + "html_filter_os", + "html_filter_sp", + "html_filter_eu", + "html_alt_th_name", + "html_eg_kpi_fee", + "html_eg_kpi_headroom", + "html_about_document", + "html_about_purpose", + "html_figures_heading", + "html_tour_sections_title", + "html_tour_report_title", + "html_tour_overview_title", + "html_tour_scores_title", + "html_tour_risks_title", + "html_tour_inventory_title", + "html_tour_alternatives_title", + "html_tour_egress_title", + "html_tour_about_title", + "html_nav_label", + "html_run_label", + "html_help", ) diff --git a/cloudexit/report/writers.py b/cloudexit/report/writers.py index 7be8d5f..6f55445 100644 --- a/cloudexit/report/writers.py +++ b/cloudexit/report/writers.py @@ -5,16 +5,9 @@ from typing import Any from jinja2 import Environment -# ReportLab - -# Utils -from cloudexit.report.helpers import initials +from cloudexit.report.context import ReportContext +from cloudexit.report.html_sections import html_sections from cloudexit.resources import asset_path -from cloudexit.report.html_data import ( - transform_cost_inventory_for_html, - transform_risk_inventory_for_html, - transform_alt_tech_for_html, -) from cloudexit.report.json_data import ( transform_resource_inventory_for_json, transform_cost_inventory_for_json, @@ -27,114 +20,15 @@ def generate_html_report( - report_path: str, - metadata: dict[str, Any], - resource_type_mapping: dict[str, dict[str, Any]], - resource_inventory: list[dict[str, Any]], - cost_data: list[dict[str, Any]], - scoring_data: dict[str, Any] | None, - risk_data: list[dict[str, Any]], - risk_definitions: list[dict[str, Any]], - alternatives: list[dict[str, Any]], - alternative_technologies: list[dict[str, Any]], - exit_strategy: int, - alternative_technology_organizations: list[dict[str, Any]] | None = None, - summary: dict[str, Any] | None = None, + context: ReportContext, egress_estimate: dict[str, Any] | None = None ) -> str: - - # Transform resource inventory - resource_inventory_dict = { - str(item["resource_type"]): { - **item, - "name": resource_type_mapping.get(str(item["resource_type"]), {}).get( - "name", "Unknown Resource" - ), - "icon": ( - "/assets" + icon - if ( - icon := resource_type_mapping.get( - str(item["resource_type"]), {} - ).get("icon") - ) - else "" - ), - } - for item in resource_inventory - } - - # Transform risks - risks, severity_counts = transform_risk_inventory_for_html( - risk_data, risk_definitions, resource_inventory_dict - ) - - # Transform costs - months, cost_values, total_cost, currency, currency_symbol = ( - transform_cost_inventory_for_html(cost_data) - ) - - # Transform resource data with names and icons - resource_counts = [] - for resource_type, resource in resource_inventory_dict.items(): - count = resource.get("count", 0) - resource_info = resource_type_mapping.get(str(resource_type), {}) - name = resource_info.get("name", "Unknown Resource") - icon = (resource_info.get("icon") or "").lstrip("/") - - resource_counts.append( - { - "resource_type": resource_type, - "name": name, - "icon": icon, - "initials": initials(name), - "count": count, - } - ) - - # Calculate total resources - total_resources = sum(item["count"] for item in resource_counts) - - # Transform alternative technologies - alternative_technologies_data = transform_alt_tech_for_html( - resource_inventory, - alternatives, - alternative_technologies, - exit_strategy, - alternative_technology_organizations=alternative_technology_organizations, - ) - - # Scoring Data - scoring_context = { - "scoring_data": bool(scoring_data), - "exit_score": scoring_data.get("exit_score", 0) if scoring_data else 0, - "human": scoring_data.get("human_score", 0) if scoring_data else 0, - "technology": scoring_data.get("technology_score", 0) if scoring_data else 0, - "operational": scoring_data.get("operational_score", 0) if scoring_data else 0, - } - - # Render the HTML template template_content = asset_path("template", "index.html").read_text(encoding="utf-8") env = Environment(autoescape=True) template = env.from_string(template_content) - html_content = template.render( - **metadata, - **scoring_context, - risks=risks, - high_risk_count=severity_counts["high"], - medium_risk_count=severity_counts["medium"], - low_risk_count=severity_counts["low"], - total_cost=total_cost, - months_json=json.dumps(months), - costs_json=json.dumps(cost_values), - currency_symbol=currency_symbol, - total_resources=total_resources, - resource_inventory=resource_counts, - alternative_technologies=alternative_technologies_data, - summary=summary, - ) + html_content = template.render(page=html_sections(context, egress_estimate)) - # Save HTML report - html_path = os.path.join(report_path, "index.html") + html_path = os.path.join(context.report_path, "index.html") with open(html_path, "w", encoding="utf-8") as report_file: report_file.write(html_content) diff --git a/tests/catalogue/test_dataset_shapes.py b/tests/catalogue/test_dataset_shapes.py index acdcdea..be5f09b 100644 --- a/tests/catalogue/test_dataset_shapes.py +++ b/tests/catalogue/test_dataset_shapes.py @@ -118,7 +118,8 @@ def test_a_legacy_dataset_runs_with_an_unknown_version_and_numbered_risks(self): result = self.run_shape("legacy") self.assert_a_complete_offline_run(result) - self.assertIn("High: R2 No alternatives", result.text()) + self.assertRegex(result.text(), r"R2 No alternatives High") + self.assertIn("High · 1", result.text()) self.assertIn("unknown", result.html().lower()) self.assertIsNone(result.report_json()["provenance"]["catalogue_version"]) # Without catalogue_meta the sync document makes no claim about a version @@ -130,7 +131,8 @@ def test_the_current_dataset_gives_real_codes_and_the_version(self): self.assert_a_complete_offline_run(result) text = result.text() # catalogue_severity wins over severity, and risk.code over R - self.assertIn("Critical: LOCK-002 No alternatives", text) + self.assertRegex(text, r"LOCK-002 No alternatives Critical") + self.assertIn("Critical · 1", text) self.assertNotIn("R2 No alternatives", text) self.assertIn(VERSION, text) provenance = result.report_json()["provenance"] @@ -159,7 +161,7 @@ def test_a_future_additive_dataset_works_exactly_like_the_current_one(self): future.report_json()["data"]["resource_inventory"], current.report_json()["data"]["resource_inventory"], ) - self.assertIn("Critical: LOCK-002 No alternatives", future.text()) + self.assertRegex(future.text(), r"LOCK-002 No alternatives Critical") class RejectedShapeTests(ShapeTestCase): diff --git a/tests/cli/test_app.py b/tests/cli/test_app.py index 7fe1f83..0af07c8 100644 --- a/tests/cli/test_app.py +++ b/tests/cli/test_app.py @@ -624,11 +624,6 @@ class EgressStageTests(unittest.TestCase): "logs": "", "json_path": "/tmp/report/raw/egress_inventory_raw_data.json", } - _REPORT_OK = { - "success": True, - "logs": "", - "html_path": "/tmp/report/egress.html", - } # The egress PDF is written with the report, from the same context _WITH_PDF = { "success": True, @@ -658,7 +653,7 @@ def _azure_config(): }, } - def _run(self, config, provider, report=None, estimate=None, html=None, **kwargs): + def _run(self, config, provider, report=None, estimate=None, **kwargs): patches = _base_patches() for p in patches: p.start() @@ -671,10 +666,6 @@ def _run(self, config, provider, report=None, estimate=None, html=None, **kwargs "cloudexit.cli.app.estimate_egress", return_value=estimate or self._ESTIMATE_OK, ).start(), - "html": patch( - "cloudexit.cli.app.generate_egress_html_report", - return_value=html or self._REPORT_OK, - ).start(), "print": patch("cloudexit.cli.app.console.print").start(), "step": patch("cloudexit.cli.app.print_step").start(), } @@ -684,14 +675,19 @@ def _run(self, config, provider, report=None, estimate=None, html=None, **kwargs patch.stopall() return mocks + def _steps(self, mocks): + return [call.args[0] for call in mocks["step"].call_args_list] + + def _printed(self, mocks): + return [str(call) for call in mocks["print"].call_args_list] + def test_egress_module_not_invoked_without_flag(self): mocks = self._run(self._azure_config(), "azure") mocks["estimate"].assert_not_called() - mocks["html"].assert_not_called() self.assertIsNone(mocks["report"].call_args.kwargs["egress_json_path"]) - def test_egress_runs_in_the_collection_stages_before_the_report(self): + def test_the_estimate_comes_before_the_report_and_goes_into_it(self): manager = MagicMock() patches = _base_patches() for p in patches: @@ -702,13 +698,8 @@ def test_egress_runs_in_the_collection_stages_before_the_report(self): mock_estimate = patch( "cloudexit.cli.app.estimate_egress", return_value=self._ESTIMATE_OK ).start() - mock_render = patch( - "cloudexit.cli.app.generate_egress_html_report", - return_value=self._REPORT_OK, - ).start() manager.attach_mock(mock_report, "generate_report") manager.attach_mock(mock_estimate, "estimate_egress") - manager.attach_mock(mock_render, "generate_egress_html_report") config = self._azure_config() try: app.run_assessment(config, "azure", egress=True) @@ -724,39 +715,34 @@ def test_egress_runs_in_the_collection_stages_before_the_report(self): exit_strategy=config["exitStrategy"], assessment_type=config["assessmentType"], ) - # The HTML carries the scope and the run, so its ID matches the PDF's - mock_render.assert_called_once_with( - "/tmp/report", - "/tmp/report/raw/egress_inventory_raw_data.json", - config["providerDetails"], - ANY, - ) - sync_id = mock_render.call_args.args[3] self.assertEqual( mock_report.call_args.kwargs["egress_json_path"], "/tmp/report/raw/egress_inventory_raw_data.json", ) - self.assertEqual(mock_report.call_args.kwargs["sync_id"], sync_id) call_names = [name for name, _, _ in manager.mock_calls] self.assertLess( - call_names.index("estimate_egress"), - call_names.index("generate_egress_html_report"), - ) - # The PDF is part of the report, after the upload - self.assertLess( - call_names.index("generate_egress_html_report"), - call_names.index("generate_report"), + call_names.index("estimate_egress"), call_names.index("generate_report") ) - def test_the_egress_pdf_step_is_printed_and_the_path_listed(self): + def test_there_is_no_separate_egress_report_step_any_more(self): mocks = self._run(self._azure_config(), "azure", egress=True) - steps = [call.args[0] for call in mocks["step"].call_args_list] + steps = self._steps(mocks) + self.assertNotIn("Generating egress report...", steps) + self.assertIn("Estimating egress data...", steps) self.assertIn("Generating egress PDF...", steps) - printed = [str(call) for call in mocks["print"].call_args_list] + printed = self._printed(mocks) + self.assertFalse(any("Egress Report:" in line for line in printed)) + self.assertFalse(any("egress.html" in line for line in printed)) + + def test_the_egress_pdf_path_is_listed(self): + mocks = self._run(self._azure_config(), "azure", egress=True) + + printed = self._printed(mocks) self.assertTrue( any("Egress PDF: /tmp/report/egress.pdf" in line for line in printed) ) + self.assertTrue(any("HTML Report:" in line for line in printed)) def test_egress_failure_still_prints_outputs_and_exits_egress(self): patches = _base_patches() @@ -766,31 +752,8 @@ def test_egress_failure_still_prints_outputs_and_exits_egress(self): "cloudexit.cli.app.estimate_egress", return_value={"success": False, "logs": "metrics api error"}, ).start() - mock_render = patch("cloudexit.cli.app.generate_egress_html_report").start() - mock_print = patch("cloudexit.cli.app.console.print").start() - try: - with self.assertRaises(SystemExit) as ctx: - app.run_assessment(self._azure_config(), "azure", egress=True) - finally: - patch.stopall() - - self.assertEqual(ctx.exception.code, codes.EGRESS) - mock_render.assert_not_called() - printed = [str(call) for call in mock_print.call_args_list] - self.assertTrue(any("Outputs:" in line for line in printed)) - - def test_egress_report_failure_still_prints_outputs_and_exits_egress(self): - patches = _base_patches() - for p in patches: - p.start() - patch( - "cloudexit.cli.app.estimate_egress", return_value=self._ESTIMATE_OK - ).start() - patch( - "cloudexit.cli.app.generate_egress_html_report", - return_value={"success": False, "logs": "template error"}, - ).start() - patch("cloudexit.cli.app.generate_report", return_value=self._WITH_PDF).start() + report = patch("cloudexit.cli.app.generate_report").start() + report.return_value = {"success": True, "reports": {"HTML": "/tmp/index.html"}} mock_print = patch("cloudexit.cli.app.console.print").start() try: with self.assertRaises(SystemExit) as ctx: @@ -801,12 +764,7 @@ def test_egress_report_failure_still_prints_outputs_and_exits_egress(self): self.assertEqual(ctx.exception.code, codes.EGRESS) printed = [str(call) for call in mock_print.call_args_list] self.assertTrue(any("Outputs:" in line for line in printed)) - # The JSON path is internal input for the reports; it is never listed - # in Outputs. The failed HTML report is not listed, but the PDF is - # written independently and succeeded, so it is. - self.assertFalse(any("Egress Estimate:" in line for line in printed)) - self.assertFalse(any("Egress Report:" in line for line in printed)) - self.assertTrue(any("Egress PDF:" in line for line in printed)) + self.assertIsNone(report.call_args.kwargs["egress_json_path"]) def test_egress_pdf_failure_still_prints_outputs_and_exits_egress(self): patches = _base_patches() @@ -815,10 +773,6 @@ def test_egress_pdf_failure_still_prints_outputs_and_exits_egress(self): patch( "cloudexit.cli.app.estimate_egress", return_value=self._ESTIMATE_OK ).start() - patch( - "cloudexit.cli.app.generate_egress_html_report", - return_value=self._REPORT_OK, - ).start() patch( "cloudexit.cli.app.generate_report", return_value=self._PDF_FAILED ).start() @@ -835,7 +789,6 @@ def test_egress_pdf_failure_still_prints_outputs_and_exits_egress(self): printed = [str(call) for call in mock_print.call_args_list] self.assertTrue(any("Outputs:" in line for line in printed)) self.assertTrue(any("HTML Report:" in line for line in printed)) - self.assertTrue(any("Egress Report:" in line for line in printed)) self.assertFalse(any("Egress PDF:" in line for line in printed)) failed = [ call @@ -846,9 +799,13 @@ def test_egress_pdf_failure_still_prints_outputs_and_exits_egress(self): self.assertEqual(failed[0].kwargs["logs"], "reportlab error") def test_a_failed_report_is_still_a_report_failure(self): - mocks_report = {"success": False, "logs": "boom"} with self.assertRaises(SystemExit) as ctx: - self._run(self._azure_config(), "azure", report=mocks_report, egress=True) + self._run( + self._azure_config(), + "azure", + report={"success": False, "logs": "boom"}, + egress=True, + ) self.assertEqual(ctx.exception.code, codes.REPORT) @@ -859,10 +816,6 @@ def test_egress_invoked_for_aws_with_provider_code(self): mock_estimate = patch( "cloudexit.cli.app.estimate_egress", return_value=self._ESTIMATE_OK ).start() - patch( - "cloudexit.cli.app.generate_egress_html_report", - return_value=self._REPORT_OK, - ).start() patch("cloudexit.cli.app.generate_report", return_value=self._WITH_PDF).start() config = VALID_CONFIG.copy() try: diff --git a/tests/platform/test_document.py b/tests/platform/test_document.py index 6397e75..016b86a 100644 --- a/tests/platform/test_document.py +++ b/tests/platform/test_document.py @@ -863,10 +863,6 @@ def run_assessment(self, report, *, egress_ok=None, **kwargs): "cloudexit.cli.app.estimate_egress", return_value={"success": egress_ok, "logs": "x", "json_path": "p"}, ), - patch( - "cloudexit.cli.app.generate_egress_html_report", - return_value={"success": True, "html_path": "h"}, - ), ] for item in patches: item.start() diff --git a/tests/report/test_basic_report.py b/tests/report/test_basic_report.py index a4b7dae..24606d8 100644 --- a/tests/report/test_basic_report.py +++ b/tests/report/test_basic_report.py @@ -6,7 +6,6 @@ import unittest from datetime import datetime, timezone from pathlib import Path -from unittest.mock import patch from pypdf import PdfReader @@ -14,7 +13,6 @@ from cloudexit.report import fonts from cloudexit.report.context import Provenance from cloudexit.report.executive_summary import generate_executive_summary -from cloudexit.report.egress import generate_egress_html_report from cloudexit.report.notices import notices_html from cloudexit.report.palette import ( DEFAULT_APPEARANCE, @@ -31,8 +29,6 @@ REPOSITORY_URL, ) from cloudexit.report.wording import BLOCKS, TITLE_CASE_KEYS -from tests.report import test_egress as egress_helpers -from tests.report.test_no_credentials import pricing_for_both_clouds from tests.support.flow import build_flow_run from tests.support.html import html_text from tests.support.report import ( @@ -584,66 +580,6 @@ def test_the_closing_page_only_names_what_the_full_assessment_adds(self): ) self.assertNotRegex(closing, r"\d+ / (100|5)") - def test_the_html_report_has_no_scores_before_the_closing_section(self): - report = build_flow_run( - self.tmp / "html", - resources=[(1, "eu-central-1", 3)], - costs=[("2026-09-01", 10.0, "USD")], - ) - perform_risk_assessment(1, report) - generate_report( - 2, - {"region": "eu-central-1"}, - 1, - 1, - "Flow", - report, - str(Path(report) / "raw_data"), - sync_id=SYNC_ID, - ) - text = html_text((Path(report) / "index.html").read_text(encoding="utf-8")) - before = text[: text.index("What the Full Assessment Adds")] - - for pattern in SCORE_PATTERNS: - self.assertIsNone(re.search(pattern, before, re.IGNORECASE), pattern) - - def test_the_html_follows_the_same_sections(self): - report = build_flow_run( - self.tmp / "html2", - resources=[(1, "eu-central-1", 3)], - costs=[("2026-09-01", 10.0, "USD")], - ) - perform_risk_assessment(1, report) - generate_report( - 2, - {"region": "eu-central-1"}, - 1, - 1, - "Flow", - report, - str(Path(report) / "raw_data"), - sync_id=SYNC_ID, - ) - text = html_text((Path(report) / "index.html").read_text(encoding="utf-8")) - - # The findings table names the sections too, so each heading is searched - # for after the one before it. - position = 0 - for heading in ( - "Brief Summary", - "Provenance", - "Introduction", - "Risk Inventory", - "Costs Overview", - "Resource Inventory", - "Alternative Technologies", - "Risk Severity Levels", - "Glossary", - "What the Full Assessment Adds", - "Legal Notices", - ): - position = text.index(heading, position) - class TermGuardTests(BasicReportTestCase): def test_no_regulatory_word_or_foreign_brand_in_pdf_html_or_metadata(self): @@ -835,109 +771,6 @@ def test_the_year_comes_from_the_generation_time(self): self.assertEqual(notices["copyright"], f"© 2024–{year} EscapeCloud") -class NoticesHtmlTests(BasicReportTestCase): - def index_text(self, provider, resources): - report = build_flow_run( - self.tmp / f"html-{provider}", - resources=resources, - costs=[("2026-09-01", 10.0, "USD")], - ) - perform_risk_assessment(1, report) - details = ( - {"region": "eu-central-1"} if provider == 2 else {"resourceGroupName": "rg"} - ) - generate_report( - provider, - details, - 1, - 1, - "Flow", - report, - str(Path(report) / "raw_data"), - sync_id=SYNC_ID, - ) - return html_text((Path(report) / "index.html").read_text(encoding="utf-8")) - - def test_index_html_ends_with_the_notices_and_the_providers_icon_notice(self): - for provider, resources, own, other in ( - (2, [(1, "eu-central-1", 3)], "Amazon.com", "Microsoft group"), - (1, [(3, "westeurope", 3)], "Microsoft group", "Amazon.com"), - ): - with self.subTest(provider=provider): - text = self.index_text(provider, resources) - - self.assertGreater( - text.index("Legal Notices"), - text.index("What the Full Assessment Adds"), - ) - tail = text[text.index("Legal Notices") :] - self.assertIn(BLOCKS["notices_disclaimer"], tail) - self.assertIn(own, tail) - self.assertNotIn(other, text) - self.assertIn(REPOSITORY_URL, tail) - self.assertRegex(tail.rstrip(), r"© 2024–\d{4} EscapeCloud$") - self.assertEqual(find_terms(text), []) - - def test_the_links_in_index_html_are_real_links(self): - report = build_flow_run( - self.tmp / "html-links", resources=[(1, "eu-central-1", 3)], costs=[] - ) - perform_risk_assessment(1, report) - generate_report( - 2, - {"region": "eu-central-1"}, - 1, - 1, - "Flow", - report, - str(Path(report) / "raw_data"), - sync_id=SYNC_ID, - ) - html = (Path(report) / "index.html").read_text(encoding="utf-8") - - for url in (REPOSITORY_URL, LICENCE_URL, AWS_ICONS_URL, AWS_TRADEMARK_URL): - self.assertIn(f'href="{url}"', html) - - def egress_text(self, provider): - rows = [ - egress_helpers._row( - "proddata", "Storage", "object", 2048 * egress_helpers.GIB - ) - ] - payload = egress_helpers._payload(rows) - payload["meta"]["cloud_service_provider"] = provider - folder = self.report_dir() - json_path = folder / "egress_inventory_raw_data.json" - json_path.write_text(json.dumps(payload), encoding="utf-8") - egress_helpers._seed_egress_inventory( - str(folder), rows, egress_helpers._archive_tiers(rows) - ) - with patch( - "cloudexit.report.egress.load_data", side_effect=pricing_for_both_clouds - ): - result = generate_egress_html_report(str(folder), str(json_path)) - self.assertTrue(result["success"], result["logs"]) - html = Path(result["html_path"]).read_text(encoding="utf-8") - return html, html_text(html) - - def test_egress_html_ends_with_the_same_notices(self): - for provider, own, other in ( - (1, "Microsoft group", "Amazon.com"), - (2, "Amazon.com", "Microsoft group"), - ): - with self.subTest(provider=provider): - html, text = self.egress_text(provider) - tail = text[text.index("Legal Notices") :] - - self.assertIn(BLOCKS["notices_disclaimer"], tail) - self.assertIn(own, tail) - self.assertNotIn(other, text) - # The timestamp of the estimate gives the year - self.assertTrue(tail.rstrip().endswith("© 2024–2026 EscapeCloud")) - self.assertIn(f'href="{LICENCE_URL}"', html) - self.assertEqual(find_terms(text), []) - - class WordingGuardTests(unittest.TestCase): def test_the_licence_name_is_a_proper_name_and_other_license_is_not(self): self.assertEqual( @@ -1015,8 +848,9 @@ def test_no_wording_uses_a_retired_term_or_american_spelling(self): def test_no_wording_carries_regulatory_words_or_scores(self): for key, text in self.strings(): - # The pages that point to the platforms name what they add - if key.startswith(("fa_", "eg_next_")): + # The pages that point to the platforms name what they add, and the + # Scores row of the HTML report names the results they compute + if key.startswith(("fa_", "eg_next_", "html_scores_", "html_tour_")): continue with self.subTest(key): self.assertEqual(find_terms(text), []) diff --git a/tests/report/test_egress.py b/tests/report/test_egress.py index b1c33f9..3c9b3d6 100644 --- a/tests/report/test_egress.py +++ b/tests/report/test_egress.py @@ -1,5 +1,4 @@ # tests/report/test_egress.py -import json import os import sqlite3 import tempfile @@ -7,19 +6,18 @@ from unittest.mock import patch -from cloudexit.resources import asset_path from cloudexit.egress.model import GIB, write_egress_inventory +from cloudexit.report.helpers import format_gib, gib_number, percent from cloudexit.catalogue.store import active_catalogue_path from cloudexit.report.egress import ( - CATEGORIES, - _build_allocation, _build_type_groups, _resolve_icon, build_fee_estimate, calculate_tiered_cost, + fee_number, format_fee, + free_tier_headroom, free_tier_limit_display, - generate_egress_html_report, load_pricing, ) @@ -180,34 +178,6 @@ def _seed_egress_inventory(report_path, rows, archive_tiers=frozenset()): return db_path -class CategoryCoverageTests(unittest.TestCase): - # The CHECK constraint on resourcetype_data.data_category. A category the - # report does not know about renders an unstyled badge and, worse, drops - # out of the allocation chart while still counting towards the totals. - DATA_CATEGORIES = {"object", "block", "file", "database", "backup"} - - def test_every_master_data_category_is_known_to_the_report(self): - self.assertEqual(set(CATEGORIES), self.DATA_CATEGORIES) - - def test_only_backup_is_left_out_of_the_allocation_chart(self): - excluded = {k for k, v in CATEGORIES.items() if not v["in_allocation"]} - - # Backup is enumerated but never sized, so it has nothing to allocate. - self.assertEqual(excluded, {"backup"}) - - def test_allocated_categories_have_a_colour_and_a_label(self): - for key, info in CATEGORIES.items(): - if info["in_allocation"]: - self.assertIsNotNone(info["color"], key) - self.assertIsNotNone(info["label"], key) - - def test_template_styles_every_category_badge(self): - css = asset_path("template", "egress.html").read_text(encoding="utf-8") - - for key in CATEGORIES: - self.assertIn(f".category-{key} {{", css, f"no badge rule for {key!r}") - - class LoadPricingTests(unittest.TestCase): @staticmethod def _make_report_db(report_path, rows): @@ -392,34 +362,6 @@ def test_unknown_type_has_no_icon(self): self.assertIsNone(_resolve_icon("Vendor.Unknown/things", {})) -class AllocationSeriesTests(unittest.TestCase): - def test_groups_known_bytes_by_category_in_fixed_order(self): - rows = [ - _row("disk1", "Managed Disk", "block", 40 * GIB), - _row("sa1", "Storage Account", "object", 60 * GIB), - _row("db1", "SQL Database", "database", 10 * GIB), - _row("vault1", "Recovery Services Vault", "backup", None), - _row("db2", "SQL Database", "database", None, size_unknown=True), - ] - - labels, values, colors = _build_allocation(rows) - - self.assertEqual( - labels, - ["Object Storage", "Block (allocated)", "Databases"], - ) - self.assertEqual(values, [60 * GIB, 40 * GIB, 10 * GIB]) - self.assertEqual(len(colors), 3) - - def test_zero_categories_are_omitted(self): - rows = [_row("sa1", "Storage Account", "object", 5 * GIB)] - - labels, values, _ = _build_allocation(rows) - - self.assertEqual(labels, ["Object Storage"]) - self.assertEqual(values, [5 * GIB]) - - class TypeGroupTests(unittest.TestCase): def test_groups_by_label_and_sorts_by_fee(self): rows = [ @@ -466,124 +408,52 @@ def test_flags_and_notes_become_resource_detail(self): ) -class GenerateEgressHtmlReportTests(unittest.TestCase): - def _generate(self, payload): - with tempfile.TemporaryDirectory() as tmp_dir: - json_path = os.path.join(tmp_dir, "egress_inventory_raw_data.json") - with open(json_path, "w", encoding="utf-8") as json_file: - json.dump(payload, json_file) - _seed_egress_inventory( - tmp_dir, - payload["data"]["resources"], - _archive_tiers(payload["data"]["resources"]), - ) - - with patch( - "cloudexit.report.egress.load_data", - side_effect=_fake_load_data, - ): - result = generate_egress_html_report(tmp_dir, json_path) - - html = None - if result["success"]: - self.assertEqual( - result["html_path"], os.path.join(tmp_dir, "egress.html") - ) - with open(result["html_path"], encoding="utf-8") as html_file: - html = html_file.read() - return result, html - - def test_renders_report_with_fees_table_and_charts(self): - rows = [ - _row( - "proddata", - "Storage Account", - "object", - 2048 * GIB, - resource_type="Microsoft.Storage/storageAccounts", - ), - _row( - "vm-osdisk", - "Managed Disk", - "block", - 512 * GIB, - resource_type="Microsoft.Compute/disks", - flags=["allocated (upper bound)"], - ), - ] - result, html = self._generate(_payload(rows)) - - self.assertTrue(result["success"], result["logs"]) - self.assertIn("Exit Assessment Test", html) - normalized_html = " ".join(html.split()) - # The alpha banner is a limitation in the Introduction now - self.assertNotIn("alpha", html) - self.assertNotIn("mailto:beta@escapecloud.io", html) - # (2560 - 100) GiB * 0.087 = $214.02 total internet egress fee - self.assertIn("$214.02", html) - self.assertIn("Estimated Egress Fee", html) - self.assertIn("Estimated one-time charge to transfer the identified", html) - self.assertIn( - 'any resources whose size could not be measured. A "+" means', - normalized_html, - ) - self.assertIn("lower-bound estimate.", html) - self.assertIn("feesChart", html) - self.assertIn("allocationChart", html) - self.assertIn("proddata", html) - self.assertIn("category-object", html) - self.assertIn("allocated (upper bound)", html) - self.assertNotIn("Estimates only", html) - self.assertNotIn("At least", html) - - def test_unknown_sizes_add_plus_suffix_to_headlines(self): - rows = [ - _row("sa1", "Storage Account", "object", 2048 * GIB), - _row("db1", "SQL Database", "database", None, size_unknown=True), +class GibAndFeeFormatTests(unittest.TestCase): + def test_a_size_that_is_not_zero_never_reads_as_zero(self): + self.assertEqual(gib_number(0), "0.0") + self.assertEqual(gib_number(GIB // 100), "<0.1") + self.assertEqual(gib_number(GIB // 50, places=2), "0.02") + self.assertEqual(gib_number(GIB // 1000, places=2), "<0.01") + + def test_places_and_grouping(self): + self.assertEqual(gib_number(2048 * GIB), "2,048.0") + self.assertEqual(gib_number(22 * GIB, places=2), "22.00") + self.assertEqual(format_gib(20 * GIB), "20.0 GiB") + self.assertEqual(format_gib(None), "n/a") + + def test_the_fee_has_one_format_with_and_without_the_symbol(self): + self.assertEqual(fee_number(0), "0.00") + self.assertEqual(fee_number(1732.1), "1,732.10") + self.assertEqual(format_fee(1732.1), f"${fee_number(1732.1)}") + + def test_a_share_keeps_its_sign_and_never_reads_zero_when_it_is_not(self): + self.assertEqual(percent(63.4), "63\u00a0%") + self.assertEqual(percent(0.2), "<1\u00a0%") + self.assertEqual(percent(0), "0\u00a0%") + + +class FreeTierHeadroomTests(unittest.TestCase): + def test_the_headroom_is_in_the_unit_the_provider_prices_in(self): + prices = [ + _price_row(1, "internet_egress", 0, 100, 0.0, unit="GB"), + _price_row(1, "internet_egress", 100, None, 0.1, unit="GB"), ] - result, html = self._generate(_payload(rows, unknown_count=1)) - - self.assertTrue(result["success"], result["logs"]) - # 2048 GiB known: (2048 - 100) * 0.087 = $169.48, "+" marks the lower bound - self.assertIn("2,048.0 GiB+", html) - self.assertNotIn("TiB", html) - self.assertIn("$169.48+", html) - self.assertIn("Excludes 1 resource", html) - self.assertIn("n/a", html) - - def test_free_tier_environment_shows_included_in_free_tier_card(self): - rows = [_row("sa1", "Storage Account", "object", 5 * GIB)] - result, html = self._generate(_payload(rows)) - - self.assertTrue(result["success"], result["logs"]) - self.assertIn("Included in Free Tier", html) - # The fee headline stays visible above the card, in the one money format - self.assertIn("Estimated Egress Fee", html) - self.assertIn('
$0.00
', html) - self.assertIn("5.0 GiB", html) - self.assertIn("100 GiB free tier", html) - self.assertNotIn("No fee estimate available.", html) - self.assertNotIn("feesChart", html) - - def test_empty_inventory_renders_empty_states(self): - result, html = self._generate(_payload([])) - - self.assertTrue(result["success"], result["logs"]) - self.assertIn("$0.00", html) - # Zero known bytes is the empty state, not the free-tier success card. - self.assertIn("No fee estimate available.", html) - self.assertNotIn("Included in Free Tier", html) - self.assertIn("No data volume available.", html) - self.assertIn( - "No data-bearing resources were discovered during the", - html, - ) - self.assertNotIn("allocationChart", html) - def test_missing_json_returns_failure(self): - with tempfile.TemporaryDirectory() as tmp_dir: - result = generate_egress_html_report( - tmp_dir, os.path.join(tmp_dir, "missing.json") - ) + headroom = free_tier_headroom(prices, 1, 40 * 10**9) + + self.assertEqual(headroom["unit"], "GB") + self.assertAlmostEqual(headroom["left"], 60.0) + self.assertAlmostEqual(headroom["used_share"], 40.0) + + def test_a_footprint_over_the_free_tier_has_none_left(self): + headroom = free_tier_headroom(SIMPLE_PRICING, 1, 150 * GIB) + + self.assertEqual(headroom["left"], 0.0) + self.assertEqual(headroom["used_share"], 100.0) + + def test_no_free_tier_means_no_headroom(self): + self.assertIsNone(free_tier_headroom(SIMPLE_PRICING, 2, GIB)) + - self.assertFalse(result["success"]) +if __name__ == "__main__": + unittest.main() diff --git a/tests/report/test_egress_report.py b/tests/report/test_egress_report.py index d1bfc4d..85e1087 100644 --- a/tests/report/test_egress_report.py +++ b/tests/report/test_egress_report.py @@ -14,7 +14,6 @@ Provenance, format_document_id, ) -from cloudexit.report.egress import generate_egress_html_report from cloudexit.report.egress_report import generate_egress_report from cloudexit.report.sections import AWS_ICONS_URL, AZURE_ICONS_URL from cloudexit.report.wording import BLOCKS @@ -30,6 +29,7 @@ uploaded, ) from tests.report.test_no_credentials import pricing_for_both_clouds +from tests.support.egress import empty_estimate from tests.support.flow import build_flow_run from tests.support.html import html_text from tests.support.report import ( @@ -112,18 +112,6 @@ def render(self, rows=MULTI_TIB, provider=1, prov=None): path = generate_egress_report(context, json_path) return path, context, PdfReader(path) - def render_html(self, rows=MULTI_TIB, provider=1, details=None): - folder, json_path = self.prepare(rows, provider) - with patch( - "cloudexit.report.egress.load_data", side_effect=pricing_for_both_clouds - ): - result = generate_egress_html_report( - str(folder), json_path, details, SYNC_ID - ) - self.assertTrue(result["success"], result["logs"]) - html = Path(result["html_path"]).read_text(encoding="utf-8") - return html, html_text(html) - def scenarios(self): for provider, name in ((1, "azure"), (2, "aws")): for state, prov in (("offline", provenance()), ("uploaded", uploaded())): @@ -437,55 +425,6 @@ def test_the_term_wording_and_retired_term_guards_pass_on_every_page(self): self.assertEqual(retired_term_hits(text), []) self.assertEqual(american_hits(text), []) - def test_the_html_follows_the_pdf_and_passes_the_guards(self): - for provider, details in ( - (1, {"resourceGroupName": "Prod-RG", "tenantId": "t-1"}), - (2, {"region": "eu-central-1", "accountId": "123456789012"}), - ): - with self.subTest(provider=provider): - html, text = self.render_html(MULTI_TIB, provider, details) - - position = 0 - for heading in ( - "Introduction", - "Purpose", - "Scope of the Estimation", - "Data Landscape", - "Estimated Costs", - "Appendix – Egress Prices", - "Glossary", - "Next Steps", - "Legal Notices", - ): - position = text.index(heading, position) - self.assertIn(EDL_ID, text) - self.assertIn(f"({ES_ID})", text) - self.assertTrue( - text.rstrip().endswith("© 2024–2026 EscapeCloud"), text[-80:] - ) - self.assertEqual(find_terms(text), []) - self.assertEqual(find_terms(html.split("Egress & Data Landscape – Exit Assessment Test", html - ) - self.assertNotIn("EscapeCloud Community Edition -", html.split("")[0]) - self.assertNotIn("TiB", html) - self.assertIn(BLOCKS["notices_egress"], html.replace("'", "'")) - - def test_the_html_scope_names_no_account_or_tenant(self): - _html, text = self.render_html( - MULTI_TIB, 2, {"region": "eu-central-1", "accountId": "123456789012"} - ) - - self.assertIn("AWS region eu-central-1", text) - self.assertNotIn("123456789012", text) - class GenerationOrderTests(unittest.TestCase): def setUp(self): @@ -518,13 +457,18 @@ def es_text(self): def test_the_egress_pdf_is_made_from_the_context_of_the_report(self): upload = {"status": "uploaded", "url": URL, "assessment_id": "a-1"} seen = {} + estimate = empty_estimate() - def fake(context, json_path): - seen["context"] = context - seen["json_path"] = json_path + def fake(context, json_path, path=None, estimate=None): + seen.update(context=context, json_path=json_path, estimate=estimate) return "egress.pdf" - with patch("cloudexit.engine.generate_egress_report", side_effect=fake): + with ( + patch( + "cloudexit.engine.load_egress_estimate", return_value=estimate + ) as load, + patch("cloudexit.engine.generate_egress_report", side_effect=fake), + ): result = self.generate(upload=upload, egress_json_path="estimate.json") self.assertTrue(result["success"]) @@ -536,10 +480,17 @@ def fake(context, json_path): self.assertEqual(context.provenance.platform_url, URL) self.assertEqual(context.egress_document_id, EDL_ID) self.assertEqual(seen["json_path"], "estimate.json") - - def test_the_summary_names_the_egress_document_only_when_there_is_one(self): - with patch( - "cloudexit.engine.generate_egress_report", return_value="egress.pdf" + # The estimate is read once, and the page and the PDF share it + load.assert_called_once_with(self.report, "estimate.json") + self.assertIs(seen["estimate"], estimate) + + def test_the_summary_and_the_page_name_the_egress_document_only_with_one(self): + with ( + patch( + "cloudexit.engine.load_egress_estimate", + return_value=empty_estimate(), + ), + patch("cloudexit.engine.generate_egress_report", return_value="egress.pdf"), ): self.generate(egress_json_path="estimate.json") with_egress = self.es_text() @@ -547,34 +498,49 @@ def test_the_summary_names_the_egress_document_only_when_there_is_one(self): sentence = BLOCKS["es_costs_egress_note"].format(document_id=EDL_ID) self.assertIn(sentence, with_egress) - self.assertIn("A separate Egress & Data Landscape", html) - self.assertIn(EDL_ID, html) + # The page carries it in its egress section, not as a sentence + self.assertIn(f"also as PDF {EDL_ID}", html_text(html)) + self.assertIn('id="egress"', html) + self.assertIn('href="egress.pdf"', html) self.generate() self.assertNotIn("A separate Egress", self.es_text()) html = (Path(self.report) / "index.html").read_text(encoding="utf-8") - self.assertNotIn("A separate Egress", html) + self.assertNotIn(EDL_ID, html) + self.assertNotIn('id="egress"', html) + self.assertNotIn("egress.pdf", html) - def test_a_failing_egress_pdf_leaves_the_summary_written(self): + def test_a_failing_egress_pdf_leaves_the_summary_and_the_page_written(self): (Path(self.report) / "report.pdf").unlink(missing_ok=True) - with patch( - "cloudexit.engine.generate_egress_report", - side_effect=RuntimeError("reportlab went wrong"), + with ( + patch( + "cloudexit.engine.load_egress_estimate", + return_value=empty_estimate(), + ), + patch( + "cloudexit.engine.generate_egress_report", + side_effect=RuntimeError("reportlab went wrong"), + ), ): result = self.generate(egress_json_path="estimate.json") self.assertTrue(result["success"]) self.assertTrue((Path(self.report) / "report.pdf").is_file()) - self.assertTrue((Path(self.report) / "index.html").is_file()) + html = (Path(self.report) / "index.html").read_text(encoding="utf-8") + self.assertIn('id="egress"', html) self.assertNotIn("EGRESS_PDF", result["reports"]) self.assertEqual(result["egress_pdf_logs"], "reportlab went wrong") - def test_an_unreadable_estimate_is_the_same_failure(self): + def test_an_unreadable_estimate_leaves_egress_out_of_every_report(self): result = self.generate(egress_json_path="/nowhere/estimate.json") self.assertTrue(result["success"]) self.assertIn("egress_pdf_logs", result) + self.assertNotIn("EGRESS_PDF", result["reports"]) self.assertTrue((Path(self.report) / "report.pdf").is_file()) + self.assertNotIn("A separate Egress", self.es_text()) + html = (Path(self.report) / "index.html").read_text(encoding="utf-8") + self.assertNotIn('id="egress"', html) def test_without_an_estimate_nothing_is_added(self): result = self.generate() diff --git a/tests/report/test_html_report.py b/tests/report/test_html_report.py new file mode 100644 index 0000000..128569d --- /dev/null +++ b/tests/report/test_html_report.py @@ -0,0 +1,955 @@ +import hashlib +import json +import posixpath +import re +import tempfile +import unittest +from html.parser import HTMLParser +from pathlib import Path +from urllib.parse import urlparse + +from pypdf import PdfReader + +from cloudexit.engine import generate_report, perform_risk_assessment +from cloudexit.report.sections import AWS_ICONS_URL, AZURE_ICONS_URL, REPOSITORY_URL +from cloudexit.report.wording import BLOCKS +from cloudexit.resources import asset_path +from tests.report.test_basic_report import page_texts +from tests.samples.rendered import rendered +from tests.support.flow import SERVER_SCORES, build_flow_run +from tests.support.html import html_text +from tests.support.report_terms import find_terms +from tests.support.sync import SYNC_ID + +DRIVER_JS_SHA256 = "3e3790dea8118c80a7a1d81af9454f8f6fe9a5dd180e843ce8ba638c5b446753" +DRIVER_CSS_SHA256 = "4b398481a7ce8375af4d9f58f39410c73a0b70726fe513686d3d51c10ad76cb5" +BASE_SECTIONS = ["overview", "scores", "risks", "inventory", "alternatives", "about"] +WITH_EGRESS = [ + "overview", + "scores", + "risks", + "inventory", + "alternatives", + "egress", + "about", +] +TOUR_TITLES = [ + "Sections", + "This Report", + "Overview", + "Exit Readiness Grade & Vendor Lock-in Score", + "Risks", + "Resource Inventory", + "Alternative Technologies", + "Egress & Data Landscape", + "About This Report", +] +PROVIDER_LOGOS = {"aws": "Amazon Web Services", "azure": "Microsoft Azure"} +PLATFORM_URL = "https://platform.example.test/assessments/a-1" + +STATE = {} + + +def setUpModule(): + STATE.update(rendered()) + + +def tearDownModule(): + STATE.clear() + + +class References(HTMLParser): + # What the page loads: stylesheets, scripts and images + def __init__(self): + super().__init__() + self.found = [] + + def handle_starttag(self, tag, attrs): + attrs = dict(attrs) + if tag == "link" and attrs.get("href"): + self.found.append((tag, attrs["href"])) + elif tag in ("script", "img") and attrs.get("src"): + self.found.append((tag, attrs["src"])) + + +def references(html): + parser = References() + parser.feed(html) + return parser.found + + +def is_local(value, base=""): + # A relative path that stays inside the report folder; base is the folder of + # the file that holds the reference, relative to the report folder + parsed = urlparse(value) + if parsed.scheme or parsed.netloc or value.startswith(("/", "\\")): + return False + return not posixpath.normpath(posixpath.join(base, value)).startswith("..") + + +def css_references(css): + found = re.findall(r"@import\s+(?:url\()?\s*[\"']?([^\"')\s;]+)", css) + return found + re.findall(r"url\(\s*[\"']?([^\"')]+)", css) + + +def offline_problems(html, stylesheets=()): + # Anything the page would load from outside its own folder; a stylesheet is + # given as (folder, text), the folder relative to the report folder + problems = [ + f"<{tag}> {value}" for tag, value in references(html) if not is_local(value) + ] + for base, css in (("", html),) + tuple(stylesheets): + problems += [ + f"css {value}" for value in css_references(css) if not is_local(value, base) + ] + return problems + + +def composed_of(text, pieces): + # Whether the text is built only from the pieces, joined by single spaces + if not text: + return True + return any( + text.startswith(piece) + and (len(text) == len(piece) or text[len(piece)] == " ") + and composed_of(text[len(piece) :].lstrip(" "), pieces) + for piece in pieces + if piece + ) + + +class SampleTestCase(unittest.TestCase): + def cases(self): + yield from STATE["scenarios"].items() + + def folder(self, case): + return STATE["results"][case].directory + + def page(self, case): + return (self.folder(case) / "index.html").read_text(encoding="utf-8") + + def text(self, case): + return html_text(self.page(case)) + + def section(self, case, name): + match = re.search( + rf']*id="{name}".*?', self.page(case), re.DOTALL + ) + self.assertIsNotNone(match, f"{case}: no section {name}") + return match.group(0) + + def tour(self, case): + match = re.search( + r'', + self.page(case), + re.DOTALL, + ) + return json.loads(match.group(1)) + + def sections(self, case): + return re.findall(r']*id="([a-z]+)"', self.page(case)) + + +class StructureTests(SampleTestCase): + def test_the_sections_come_in_the_planned_order_with_egress_only_when_it_ran(self): + for case, scenario in self.cases(): + with self.subTest(case): + expected = WITH_EGRESS if scenario.egress else BASE_SECTIONS + self.assertEqual(self.sections(case), expected) + nav = re.search(r"", self.page(case), re.DOTALL).group(0) + self.assertEqual(re.findall(r'', nav), expected) + + def test_the_download_menu_has_one_pdf_or_two_and_no_json(self): + for case, scenario in self.cases(): + with self.subTest(case): + menu = re.search( + r'

", footer) + self.assertIn( + f'github.com/escapecloud/cloudexit', + footer, + ) + self.assertIn("Fonts: IBM Plex, SIL Open Font License 1.1.", footer) + self.assertIn( + "Guided tour: driver.js, MIT License, included in this report's assets.", + html_text(footer), + ) + self.assertIn(AWS_ICONS_URL, page) + self.assertNotIn(AZURE_ICONS_URL, page) + + def test_the_figures_notice_follows_the_state(self): + def figures(case): + page = self.page(case) + block = page[page.index("

Figures

") :] + return html_text(block[: block.index("")]) + + self.assertIn("egress prices are public list prices", figures("aws_typical")) + self.assertNotIn("egress", figures("aws_no_egress")) + self.assertIn("a state file holds no billing data", figures("tfstate_aws")) + + def test_the_trademark_sentences_are_in_the_page_and_both_pdfs(self): + for case, scenario in self.cases(): + with self.subTest(case): + text = self.text(case) + self.assertIn(BLOCKS["notices_trademark_own"], text) + self.assertIn(BLOCKS["notices_trademark_others"], text) + for pdf in ("report.pdf", "egress.pdf"): + if (self.folder(case) / pdf).is_file(): + last = page_texts(PdfReader(str(self.folder(case) / pdf)))[-1] + self.assertIn(BLOCKS["notices_trademark_own"], last) + self.assertIn(BLOCKS["notices_trademark_others"], last) + + def test_no_trademark_claim_for_cloudexit_or_the_logo(self): + texts = [value for value in BLOCKS.values() if isinstance(value, str)] + texts += [self.text(case) for case, _scenario in self.cases()] + for text in texts: + self.assertIsNone( + re.search(r"(?i)cloudexit[^.]{0,60}trademark", text), text[:80] + ) + self.assertIsNone( + re.search( + r"(?i)(escapecloud|cloudexit)\b[^.]{0,60}\blogo\b[^.]{0,30}trademark", + text, + ), + text[:80], + ) + for case, _scenario in self.cases(): + self.assertEqual(find_terms(self.text(case)), []) + + def test_the_logo_files_carry_their_own_notice(self): + note = asset_path("img", "logo", "README.md").read_text(encoding="utf-8") + + self.assertIn( + "The EscapeCloud logo files in this directory are not covered by this " + "project's LICENSE. All rights reserved.", + note, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/report/test_icon_placeholder.py b/tests/report/test_icon_placeholder.py index 1efc2a0..fbafdbd 100644 --- a/tests/report/test_icon_placeholder.py +++ b/tests/report/test_icon_placeholder.py @@ -2,6 +2,7 @@ import json import os import re +import shutil import tempfile import unittest from io import BytesIO @@ -19,8 +20,8 @@ from cloudexit.report.placeholder import IconPlaceholder, LabelWithPlaceholder from cloudexit.report.sections import ALT_BODY from cloudexit.egress.model import GIB +from cloudexit.resources import asset_path from cloudexit.report.writers import generate_html_report -from cloudexit.report.egress import generate_egress_html_report from cloudexit.report.egress_report import generate_egress_report from cloudexit.report.egress_sections import _type_cell from cloudexit.report.aggregate import enrich_resource_inventory @@ -40,7 +41,7 @@ EC2_ICON = "/icons/aws/Compute/EC2.png" LAMBDA_ICON = "/icons/aws/Compute/Lambda.png" -PLACEHOLDER = '' +PLACEHOLDER = '' def with_initials(initials_text, name): @@ -231,23 +232,15 @@ def test_an_icon_file_that_does_not_exist_is_a_placeholder_too(self): class HtmlReportTests(TempDirTestCase): - def render(self, fixture): + def render(self, fixture, with_icon=True): directory = self.directory() - html_path = generate_html_report( - str(directory), - fixture["metadata"], - fixture["resource_type_mapping"], - fixture["resource_inventory"], - fixture["cost_data"], - None, - fixture["risk_data"], - fixture["risk_definitions"], - fixture["alternatives"], - fixture["alternative_technologies"], - fixture["exit_strategy"], - fixture["alternative_technology_organizations"], - ) - return Path(html_path).read_text(encoding="utf-8") + if with_icon: + # The page points only at files in its own folder + icon = directory / "assets" / EC2_ICON.lstrip("/") + icon.parent.mkdir(parents=True) + shutil.copyfile(asset_path("icons", "aws", "Compute", "EC2.png"), icon) + context = build_fixture_context(fixture, directory) + return Path(generate_html_report(context)).read_text(encoding="utf-8") def test_a_type_without_an_icon_renders_the_placeholder_not_a_broken_image(self): fixture = fixture_with( @@ -265,8 +258,8 @@ def test_a_type_without_an_icon_renders_the_placeholder_not_a_broken_image(self) html = self.render(fixture) self.assertIn(PLACEHOLDER.format("VM"), html) - # A type missing from the mapping is named "Unknown Resource" - self.assertIn(PLACEHOLDER.format("UR"), html) + # A type missing from the mapping has no name, so its initials are "?" + self.assertIn(PLACEHOLDER.format("?"), html) self.assertNotIn("default.png", html) self.assertNotIn('src="assets/"', html) self.assertNotIn("None", re.findall(r'src="assets/[^"]*"', html)) @@ -275,7 +268,14 @@ def test_a_type_with_an_icon_keeps_its_image(self): html = self.render(fixture_with({})) self.assertIn(f'src="assets{EC2_ICON}"', html) - self.assertNotIn('resource-icon-placeholder" aria-hidden', html) + self.assertNotIn(PLACEHOLDER.format("EI"), html) + + def test_an_icon_the_report_folder_does_not_hold_is_a_placeholder(self): + # The page never points outside the report folder + html = self.render(fixture_with({}), with_icon=False) + + self.assertNotIn(f'src="assets{EC2_ICON}"', html) + self.assertIn(PLACEHOLDER.format("EI"), html) def test_both_kinds_appear_side_by_side(self): fixture = fixture_with( @@ -294,10 +294,8 @@ def test_both_kinds_appear_side_by_side(self): self.assertIn(PLACEHOLDER.format("VM"), html) def test_the_stylesheet_styles_the_placeholder(self): - from cloudexit.resources import asset_path - - css = asset_path("css", "style.css").read_text(encoding="utf-8") - self.assertIn(".resource-icon-placeholder", css) + css = asset_path("css", "report.css").read_text(encoding="utf-8") + self.assertIn(".rt .ph", css) class EnrichedInventoryTests(unittest.TestCase): @@ -390,15 +388,6 @@ def test_the_pdf_has_one_image_less_when_a_type_loses_its_icon(self): self.assertEqual(image_count(self.run_pdf(one)), count_both - 1) - def test_the_html_shows_a_placeholder_and_keeps_the_images(self): - icons = {"microsoft.storage/storageaccounts": EC2_ICON} - result = self.run_report(generate_egress_html_report, icons) - - html = Path(result["html_path"]).read_text(encoding="utf-8") - self.assertRegex(html, rf'src="assets/+{EC2_ICON.lstrip("/")}"') - self.assertIn(PLACEHOLDER.format("MD"), html) - self.assertNotIn("no_image", html) - if __name__ == "__main__": unittest.main() diff --git a/tests/report/test_no_credentials.py b/tests/report/test_no_credentials.py index 5edd280..59783b1 100644 --- a/tests/report/test_no_credentials.py +++ b/tests/report/test_no_credentials.py @@ -176,7 +176,6 @@ def assert_complete_run(self, report): for name in ( "index.html", "report.pdf", - "egress.html", "egress.pdf", "run.log", "raw_data/assessment_result.json", diff --git a/tests/report/test_report_flow.py b/tests/report/test_report_flow.py index 2361328..1990737 100644 --- a/tests/report/test_report_flow.py +++ b/tests/report/test_report_flow.py @@ -37,6 +37,7 @@ sync_body, whoami_reply, ) +from tests.support.egress import empty_estimate from tests.support.flow import LOCAL_RISKS, SERVER_SCORES, build_flow_run from tests.support.http import fake_http, make_response from tests.support.sync import SYNC_ID, egress_row @@ -225,7 +226,7 @@ def test_a_successful_upload_records_only_the_link(self): self.assertTrue(result["success"], result.get("logs")) html = self.html(report) self.assertIn(f'href="{URL}"', html) - self.assertIn("Open the platform report", html) + self.assertIn("Open on the platform", html) self.assertEqual(self.json_report(report)["meta"]["upload"], info) def test_banner_text_is_escaped(self): @@ -422,11 +423,11 @@ def run_assessment( "cloudexit.cli.app.estimate_egress", return_value={"success": egress_ok, "logs": "x", "json_path": "p"}, ), + # The estimate file is a stub: the report is made from this one patch( - "cloudexit.cli.app.generate_egress_html_report", - return_value={"success": True, "html_path": "h"}, + "cloudexit.engine.load_egress_estimate", + return_value=empty_estimate(), ), - # The PDF is written by generate_report, from the estimate file patch( "cloudexit.engine.generate_egress_report", return_value="egress.pdf", @@ -738,8 +739,7 @@ def traced_risk(*args, **kwargs): or {"success": True, "json_path": "p"}, ), patch( - "cloudexit.cli.app.generate_egress_html_report", - return_value={"success": True}, + "cloudexit.engine.load_egress_estimate", return_value=empty_estimate() ), patch("cloudexit.engine.generate_egress_report", return_value="egress.pdf"), ): diff --git a/tests/report/test_report_pipeline.py b/tests/report/test_report_pipeline.py index cb4ee57..de7bcdb 100644 --- a/tests/report/test_report_pipeline.py +++ b/tests/report/test_report_pipeline.py @@ -18,69 +18,35 @@ class ReportPipelineSmokeTests(unittest.TestCase): - def test_generate_html_report_creates_expected_output(self): - fixture = build_report_fixture() - + def html(self, fixture, **overrides): with tempfile.TemporaryDirectory() as report_dir: - html_path = generate_html_report( - report_dir, - fixture["metadata"], - fixture["resource_type_mapping"], - fixture["resource_inventory"], - fixture["cost_data"], - None, - fixture["risk_data"], - fixture["risk_definitions"], - fixture["alternatives"], - fixture["alternative_technologies"], - fixture["exit_strategy"], - fixture["alternative_technology_organizations"], - ) + context = build_fixture_context(fixture, report_dir, **overrides) + html_path = generate_html_report(context) self.assertTrue(Path(html_path).exists()) - html = Path(html_path).read_text(encoding="utf-8") + self.assertEqual(Path(html_path).name, "index.html") + return Path(html_path).read_text(encoding="utf-8") + + def test_generate_html_report_creates_expected_output(self): + html = self.html(build_report_fixture()) self.assertIn("Smoke Test Assessment", html) self.assertIn("Amazon Web Services", html) self.assertIn("OpenStack", html) self.assertIn("OpenInfra Foundation", html) - self.assertIn("All Regions", html) - self.assertIn("data-org-region", html) self.assertIn("EC2 Instance", html) + self.assertIn('data-os="1"', html) def test_generate_html_report_renders_empty_state_output(self): - fixture = build_empty_report_fixture() - - with tempfile.TemporaryDirectory() as report_dir: - html_path = generate_html_report( - report_dir, - fixture["metadata"], - fixture["resource_type_mapping"], - fixture["resource_inventory"], - fixture["cost_data"], - None, - fixture["risk_data"], - fixture["risk_definitions"], - fixture["alternatives"], - fixture["alternative_technologies"], - fixture["exit_strategy"], - fixture["alternative_technology_organizations"], - ) - - self.assertTrue(Path(html_path).exists()) - html = Path(html_path).read_text(encoding="utf-8") + html = self.html(build_empty_report_fixture()) self.assertIn("Empty State Assessment", html) - self.assertIn("No risk data available.", html) - self.assertIn("No cost data available.", html) - self.assertNotIn("exit score", html.lower()) - self.assertNotIn("lock-in score", html.lower()) - self.assertIn("No resources were discovered during the assessment.", html) - self.assertIn("No alternative technologies are available", html) - self.assertNotIn('id="risksChart"', html) - self.assertNotIn('id="costsChart"', html) - self.assertNotIn('id="exitScoreChart"', html) - self.assertNotIn('id="vendorLockInScoreChart"', html) + self.assertIn("No exit risks were identified.", html) + self.assertIn("No cost data available for this assessment.", html) + self.assertIn("No resources were found in the defined scope.", html) + self.assertIn("No resource types to list alternatives for.", html) + # No filters when there is nothing to filter + self.assertNotIn('class="toolbar"', html) def test_generate_json_report_creates_expected_structure(self): fixture = build_report_fixture() @@ -137,29 +103,13 @@ class EmptyCostInventoryReportTests(unittest.TestCase): def test_html_report_renders_resources_without_cost_data(self): fixture = build_report_fixture() - with tempfile.TemporaryDirectory() as report_dir: - html_path = generate_html_report( - report_dir, - fixture["metadata"], - fixture["resource_type_mapping"], - fixture["resource_inventory"], - [], - None, - fixture["risk_data"], - fixture["risk_definitions"], - fixture["alternatives"], - fixture["alternative_technologies"], - fixture["exit_strategy"], - fixture["alternative_technology_organizations"], - ) - - self.assertTrue(Path(html_path).exists()) - html = Path(html_path).read_text(encoding="utf-8") + context = build_fixture_context(fixture, report_dir, cost_data=[]) + html = Path(generate_html_report(context)).read_text(encoding="utf-8") self.assertIn("EC2 Instance", html) - self.assertIn("No cost data available.", html) - self.assertNotIn('id="costsChart"', html) + self.assertIn("No cost data available for this assessment.", html) + self.assertNotIn('class="bars"', html) def test_json_report_has_empty_cost_inventory(self): fixture = build_report_fixture() diff --git a/tests/report/test_report_transforms.py b/tests/report/test_report_transforms.py index 8fcbbf5..cf58cff 100644 --- a/tests/report/test_report_transforms.py +++ b/tests/report/test_report_transforms.py @@ -1,10 +1,5 @@ import unittest -from cloudexit.report.html_data import ( - transform_alt_tech_for_html, - transform_cost_inventory_for_html, - transform_risk_inventory_for_html, -) from cloudexit.report.json_data import ( transform_alt_tech_for_json, transform_resource_inventory_for_json, @@ -120,62 +115,6 @@ def build_alternative_technology_organizations(): ] -class HtmlTransformTests(unittest.TestCase): - def test_transform_cost_inventory_for_html_sorts_and_sums_costs(self): - months, cost_values, total_cost, currency_code, currency_symbol = ( - transform_cost_inventory_for_html( - [ - {"month": "2026-02-01", "cost": 11.25, "currency": "USD"}, - {"month": "2026-01-01", "cost": 14.75, "currency": "USD"}, - ] - ) - ) - - self.assertEqual(months, ["Jan", "Feb"]) - self.assertEqual(cost_values, [14.75, 11.25]) - self.assertEqual(total_cost, 26.0) - self.assertEqual(currency_code, "USD") - self.assertEqual(currency_symbol, "$") - - def test_transform_risk_inventory_for_html_counts_overall_and_resource_risks(self): - resource_inventory = { - "101": {"name": "EC2 Instance"}, - "202": {"name": "S3 Bucket"}, - } - - risks, severity_counts = transform_risk_inventory_for_html( - build_risk_data(), - build_risk_definitions(), - resource_inventory, - ) - - self.assertEqual([risk["severity"] for risk in risks], ["high", "medium"]) - self.assertEqual(risks[0]["impacted_resources_count"], 2) - self.assertCountEqual( - risks[0]["impacted_resources"], ["EC2 Instance", "S3 Bucket"] - ) - self.assertIsNone(risks[1]["impacted_resources_count"]) - self.assertEqual(severity_counts, {"high": 1, "medium": 1, "low": 0}) - - def test_transform_alt_tech_for_html_filters_by_strategy_and_status(self): - transformed = transform_alt_tech_for_html( - build_resource_inventory(), - build_alternatives(), - build_alternative_technologies(), - exit_strategy=1, - alternative_technology_organizations=build_alternative_technology_organizations(), - ) - - self.assertEqual(len(transformed), 2) - self.assertEqual(transformed[0]["product_name"], "OpenStack") - self.assertEqual(transformed[1]["product_name"], "MinIO") - self.assertTrue(transformed[0]["open_source"]) - self.assertFalse(transformed[1]["support_plan"]) - self.assertEqual(transformed[0]["organization_name"], "OpenInfra Foundation") - self.assertEqual(transformed[0]["organization_region"], "united-states") - self.assertEqual(transformed[1]["organization_region"], "european-union") - - class JsonTransformTests(unittest.TestCase): def test_transform_resource_inventory_for_json_maps_names_and_codes(self): transformed = transform_resource_inventory_for_json( diff --git a/tests/samples/__main__.py b/tests/samples/__main__.py index 1a7856c..c4fe0fd 100644 --- a/tests/samples/__main__.py +++ b/tests/samples/__main__.py @@ -101,9 +101,8 @@ def overview(scenarios, results, failures, sheets): link(case, name, title) for name, title in ( ("report.pdf", "Executive Summary PDF"), - ("index.html", "Executive Summary HTML"), + ("index.html", "HTML report"), ("egress.pdf", "Egress PDF"), - ("egress.html", "Egress HTML"), ) if name in result.files ] diff --git a/tests/samples/catalogue.py b/tests/samples/catalogue.py index 668a2a0..dc11903 100644 --- a/tests/samples/catalogue.py +++ b/tests/samples/catalogue.py @@ -109,6 +109,8 @@ "Microsoft.ContainerRegistry/registries": (0, 0), "Microsoft.DocumentDB/databaseAccounts": (2, 2), "Microsoft.Compute/virtualMachines": (5, 4), + # More than the page shows at first, so the rest is collapsed + "AWS.ec2.describe_instances.Reservations": (12, 10), } DEFAULT_ALTERNATIVES = (3, 3) @@ -196,12 +198,14 @@ def build_sample_catalogue(path, scenarios): conn.execute( "INSERT INTO alternativetechnology (id, product_name, " "product_description, product_url, open_source, support_plan, " - "status, organization_id) VALUES (?, ?, '', NULL, ?, ?, 't', 1)", + "status, organization_id) VALUES (?, ?, '', NULL, ?, ?, 't', ?)", ( technology, f"Sample Technology {technology}", "t" if position % 2 else "f", "t" if position < supported else "f", + # Some have no recorded vendor + 1 if technology % 4 else None, ), ) for strategy in (1, 2, 3): diff --git a/tests/samples/render.py b/tests/samples/render.py index afe9947..de5d4b5 100644 --- a/tests/samples/render.py +++ b/tests/samples/render.py @@ -19,19 +19,13 @@ perform_risk_assessment, ) from cloudexit.report.assets import copy_assets -from cloudexit.report.egress import generate_egress_html_report from tests.support.html import html_blocks -# What a rendered case holds. Both HTML reports have a text dump, so a change -# to either can be reviewed with a diff. +# What a rendered case holds. The HTML report and each PDF have a text dump, so +# a change to any of them can be reviewed with a diff. EXPECTED_FILES = { "always": ("report.pdf", "index.html", "texts/report.txt", "texts/index-html.txt"), - "egress": ( - "egress.pdf", - "egress.html", - "texts/egress.txt", - "texts/egress-html.txt", - ), + "egress": ("egress.pdf", "texts/egress.txt"), } ASSESSMENT_TYPE = 1 @@ -186,13 +180,8 @@ def _write_dumps(scenario, directory, result): text, pages = pdf_dump(directory / pdf) (texts / dump).write_text(text, encoding="utf-8") result.pages[pdf] = pages - for html, dump in ( - ("index.html", "index-html.txt"), - ("egress.html", "egress-html.txt"), - ): - if (directory / html).is_file(): - source = (directory / html).read_text(encoding="utf-8") - (texts / dump).write_text(html_blocks(source), encoding="utf-8") + source = (directory / "index.html").read_text(encoding="utf-8") + (texts / "index-html.txt").write_text(html_blocks(source), encoding="utf-8") def _render(scenario, directory, catalogue_path): @@ -221,13 +210,6 @@ def _render(scenario, directory, catalogue_path): if not risks["success"]: raise RenderError(f"risk assessment failed: {risks['logs']}") - if egress_json: - html = generate_egress_html_report( - report_path, egress_json, details, scenario.run_id - ) - if not html["success"]: - raise RenderError(f"egress.html failed: {html['logs']}") - report = generate_report( scenario.csp, details, diff --git a/tests/samples/rendered.py b/tests/samples/rendered.py new file mode 100644 index 0000000..6eae4c3 --- /dev/null +++ b/tests/samples/rendered.py @@ -0,0 +1,31 @@ +import atexit +import tempfile +from pathlib import Path + +from tests.samples.catalogue import build_sample_catalogue +from tests.samples.render import render_cases +from tests.samples.scenarios import load_scenarios + +_STATE = {} + + +def rendered(): + # Every sample case rendered once per test run, for all the tests that read + # a finished report folder; rendering is the slow part + if not _STATE: + tmp = tempfile.TemporaryDirectory() + atexit.register(tmp.cleanup) + root = Path(tmp.name) + scenarios = load_scenarios() + catalogue = root / "catalogue.db" + build_sample_catalogue(catalogue, scenarios.values()) + results, failures = render_cases( + scenarios.values(), root / "out", catalogue_path=catalogue + ) + _STATE.update( + scenarios=scenarios, + catalogue=catalogue, + results={result.case: result for result in results}, + failures=failures, + ) + return _STATE diff --git a/tests/samples/test_samples.py b/tests/samples/test_samples.py index fea1e96..8026ed3 100644 --- a/tests/samples/test_samples.py +++ b/tests/samples/test_samples.py @@ -15,13 +15,13 @@ from tests.report.test_no_credentials import find_leaks, folder_text from tests.samples import __main__ as samples_main from tests.samples.catalogue import build_sample_catalogue +from tests.samples.rendered import rendered from tests.samples.render import ( expected_files, pdf_dump, render_case, render_cases, ) -from tests.samples.scenarios import load_scenarios from tests.support.credentials import AWS_DETAILS, AZURE_DETAILS from tests.support.html import html_blocks, html_text from tests.support.report_terms import american_hits, find_terms, retired_term_hits @@ -31,9 +31,7 @@ ICON_MARKERS = {"azure": "Microsoft group", "aws": "Amazon.com"} EGRESS_FILES = ( "egress.pdf", - "egress.html", "texts/egress.txt", - "texts/egress-html.txt", ) STATE = {} @@ -44,22 +42,10 @@ def squashed(text): def setUpModule(): - tmp = tempfile.TemporaryDirectory() - STATE["tmp"] = tmp - root = Path(tmp.name) - STATE["scenarios"] = scenarios = load_scenarios() - STATE["catalogue"] = catalogue = root / "catalogue.db" - build_sample_catalogue(catalogue, scenarios.values()) - STATE["out"] = root / "out" - results, failures = render_cases( - scenarios.values(), STATE["out"], catalogue_path=catalogue - ) - STATE["results"] = {result.case: result for result in results} - STATE["failures"] = failures + STATE.update(rendered()) def tearDownModule(): - STATE["tmp"].cleanup() STATE.clear() @@ -111,15 +97,19 @@ def test_the_dumps_match_the_files(self): text, pages = pdf_dump(folder / pdf) self.assertEqual(self.text(case, f"texts/{dump}"), text) self.assertEqual(text.count("--- page "), pages) - for html, dump in ( - ("index.html", "index-html.txt"), - ("egress.html", "egress-html.txt"), - ): - if (folder / html).is_file(): - self.assertEqual( - self.text(case, f"texts/{dump}"), - html_blocks(self.text(case, html)), - ) + self.assertEqual( + self.text(case, "texts/index-html.txt"), + html_blocks(self.text(case, "index.html")), + ) + # One HTML report: there is no second page for the egress cases + self.assertFalse((folder / "egress.html").exists()) + + def test_the_html_dump_has_the_egress_section_for_the_egress_cases(self): + for case, scenario in self.cases(): + with self.subTest(case): + dump = self.text(case, "texts/index-html.txt") + has_section = "## Egress & Data Landscape" in dump + self.assertEqual(has_section, scenario.egress is not None) def test_rendering_a_case_twice_gives_the_same_texts(self): scenario = STATE["scenarios"]["aws_typical"] @@ -172,11 +162,9 @@ def test_the_term_wording_and_retired_term_guards(self): for text in metadata_texts(reader): self.assertEqual(find_terms(text), []) texts += page_texts(reader) - for html in ("index.html", "egress.html"): - if (folder / html).is_file(): - source = self.text(case, html) - texts.append(html_text(source)) - self.assertEqual(find_terms(source.split(" Date: Sun, 25 Oct 2026 13:07:09 +0100 Subject: [PATCH 54/61] keep line endings of the bundled driver.js files as they are --- .gitattributes | 5 +++++ tests/test_portability.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/.gitattributes b/.gitattributes index 770e167..16b426a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,7 @@ # The definition copy is compared by file hash, so it must not get CRLF line endings docs/api-v2/** text eol=lf + +# The bundled driver.js files are checked against a pinned hash and shipped +# unmodified, so no line ending conversion may touch them +cloudexit/assets/js/** -text +cloudexit/assets/css/driver.css -text diff --git a/tests/test_portability.py b/tests/test_portability.py index 3882407..966f8be 100644 --- a/tests/test_portability.py +++ b/tests/test_portability.py @@ -191,3 +191,11 @@ class LineEndingTests(unittest.TestCase): def test_the_definition_copy_is_checked_out_with_unix_line_endings(self): attributes = (ROOT / ".gitattributes").read_text(encoding="utf-8") self.assertIn("docs/api-v2/** text eol=lf", attributes) + + def test_the_bundled_scripts_are_never_converted(self): + attributes = (ROOT / ".gitattributes").read_text(encoding="utf-8") + for pattern in ( + "cloudexit/assets/js/** -text", + "cloudexit/assets/css/driver.css -text", + ): + self.assertIn(pattern, attributes) From a649387213224723736bbd86b660984512855ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Sun, 25 Oct 2026 13:48:28 +0100 Subject: [PATCH 55/61] drop the pdf closing page and state failed uploads and skipped state resources --- cloudexit/engine.py | 17 +- cloudexit/platform/upload.py | 8 +- cloudexit/report/context.py | 34 +- cloudexit/report/evidence.py | 35 ++ cloudexit/report/executive_summary.py | 3 - cloudexit/report/html_sections.py | 37 +-- cloudexit/report/notices.py | 29 +- cloudexit/report/sections.py | 91 +---- cloudexit/report/wording.py | 89 ++--- tests/cli/test_upload_modes.py | 2 +- tests/fixtures/samples/azure_uploaded.json | 2 +- tests/fixtures/samples/egress_unsized.json | 83 +++++ tests/fixtures/samples/empty_with_costs.json | 27 ++ tests/fixtures/samples/legacy_catalogue.json | 43 +++ .../tfstate/sample_aws_skipped.tfstate | 126 +++++++ tests/fixtures/samples/tfstate_skipped.json | 15 + tests/fixtures/samples/upload_failed.json | 52 +++ tests/report/test_basic_report.py | 314 ++++++++++++++---- tests/report/test_html_report.py | 79 ++++- tests/report/test_report_flow.py | 42 ++- tests/samples/render.py | 19 +- tests/samples/scenarios.py | 24 +- tests/samples/test_samples.py | 90 ++++- tests/samples/test_scenarios.py | 47 +++ tests/test_engine.py | 27 +- 25 files changed, 1061 insertions(+), 274 deletions(-) create mode 100644 tests/fixtures/samples/egress_unsized.json create mode 100644 tests/fixtures/samples/empty_with_costs.json create mode 100644 tests/fixtures/samples/legacy_catalogue.json create mode 100644 tests/fixtures/samples/tfstate/sample_aws_skipped.tfstate create mode 100644 tests/fixtures/samples/tfstate_skipped.json create mode 100644 tests/fixtures/samples/upload_failed.json diff --git a/cloudexit/engine.py b/cloudexit/engine.py index 5507910..15a4390 100644 --- a/cloudexit/engine.py +++ b/cloudexit/engine.py @@ -415,15 +415,19 @@ def perform_risk_assessment(exit_strategy: int, report_path: str) -> dict[str, A return {"success": False, "logs": str(e)} -def _load_tfstate_scope(raw_data_path: str) -> dict[str, Any] | None: - # A missing or unreadable manifest must not fail report generation. +def _load_tfstate_scope( + raw_data_path: str, +) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: + # A missing or unreadable manifest must not fail report generation. Older + # manifests have no coverage. manifest_path = os.path.join(raw_data_path, "tfstate_manifest.json") try: with open(manifest_path, "r", encoding="utf-8") as manifest_file: - return json.load(manifest_file).get("scope") + manifest = json.load(manifest_file) + return manifest.get("scope"), manifest.get("coverage") except (OSError, json.JSONDecodeError) as e: logger.warning("Could not read the tfstate manifest for report scope: %s", e) - return None + return None, None # Stage 8 @@ -477,10 +481,10 @@ def generate_report( reports = {} # One context feeds all three reports, so they say the same things - tfstate_scope = ( + tfstate_scope, tfstate_coverage = ( _load_tfstate_scope(raw_data_path) if provider_details.get("tfstatePath") - else None + else (None, None) ) provenance = collect_provenance(db_path, run_id=sync_id, upload=upload) context = build_context( @@ -496,6 +500,7 @@ def generate_report( alternative_technology_organizations=alternative_technology_organizations, exit_strategy=exit_strategy, tfstate_scope=tfstate_scope, + tfstate_coverage=tfstate_coverage, provenance=provenance, report_path=report_path, ) diff --git a/cloudexit/platform/upload.py b/cloudexit/platform/upload.py index 33ac4c1..c63ee36 100644 --- a/cloudexit/platform/upload.py +++ b/cloudexit/platform/upload.py @@ -250,7 +250,7 @@ def read_sync_files( return found, problems -REPORT_KEYS = ("status", "reason", "sync_id", "assessment_id", "url") +REPORT_KEYS = ("status", "reason", "sync_id", "assessment_id", "url", "spooled") def _record(result: UploadResult | None) -> dict[str, Any] | None: @@ -273,9 +273,9 @@ def _record(result: UploadResult | None) -> dict[str, Any] | None: return {key: value for key, value in info.items() if value} -def report_info(result: UploadResult | None) -> dict[str, str] | None: - # What the local report may show: the status and the link, nothing the - # platform computed. +def report_info(result: UploadResult | None) -> dict[str, Any] | None: + # What the local report may show: the status, the link and whether a failed + # run was kept, nothing the platform computed. record = _record(result) if record is None: return None diff --git a/cloudexit/report/context.py b/cloudexit/report/context.py index da43ca6..2de9e54 100644 --- a/cloudexit/report/context.py +++ b/cloudexit/report/context.py @@ -38,8 +38,10 @@ class Provenance: uploaded: bool = False platform_url: str | None = None assessment_id: str | None = None - # Why a requested upload did not happen, when it did not + # A requested upload that failed, why, and whether the run was kept + upload_failed: bool = False upload_reason: str | None = None + upload_spooled: bool = False @property def run_short_id(self): @@ -79,9 +81,11 @@ def collect_provenance(db_path, *, run_id=None, upload=None, collected_at=None): uploaded=upload.get("status") == "uploaded", platform_url=upload.get("url") if upload.get("status") == "uploaded" else None, assessment_id=upload.get("assessment_id"), + upload_failed=upload.get("status") == "failed", upload_reason=( - upload.get("reason") if upload.get("status") != "uploaded" else None + upload.get("reason") if upload.get("status") == "failed" else None ), + upload_spooled=bool(upload.get("spooled")) and upload.get("status") == "failed", ) @@ -147,6 +151,14 @@ class ScopeFacts: resource_group: str = "" file_name: str = "" locations: tuple = () + # State file instances left out: of other providers, of types not in the + # data catalogue + not_assessed_other: int = 0 + not_assessed_unmapped: int = 0 + + @property + def not_assessed(self): + return self.not_assessed_other + self.not_assessed_unmapped @dataclass @@ -316,7 +328,14 @@ def _alternative_entries( return entries -def scope_facts(provider_details, cloud_service_provider, tfstate_scope): +def _excluded(coverage, key): + value = (coverage or {}).get(key) + return value if isinstance(value, int) and value > 0 else 0 + + +def scope_facts( + provider_details, cloud_service_provider, tfstate_scope, tfstate_coverage=None +): provider = PROVIDER_NAMES.get(cloud_service_provider, "–") if provider_details.get("tfstatePath"): scope = tfstate_scope or { @@ -343,6 +362,12 @@ def scope_facts(provider_details, cloud_service_provider, tfstate_scope): tuple(rows), file_name=scope.get("file") or "", locations=locations, + not_assessed_other=_excluded( + tfstate_coverage, "instances_excluded_other_provider" + ), + not_assessed_unmapped=_excluded( + tfstate_coverage, "instances_excluded_unmapped" + ), ) if cloud_service_provider == 1: rows = ( @@ -379,6 +404,7 @@ def build_context( alternative_technology_organizations: list[dict[str, Any]], exit_strategy: int, tfstate_scope: dict[str, Any] | None, + tfstate_coverage: dict[str, Any] | None = None, provenance: Provenance, report_path: str, ) -> ReportContext: @@ -392,7 +418,7 @@ def build_context( provider=PROVIDER_NAMES.get(csp, "–"), exit_strategy=exit_strategy, strategy_code=STRATEGY_CODES.get(int(exit_strategy), ""), - scope=scope_facts(provider_details, csp, tfstate_scope), + scope=scope_facts(provider_details, csp, tfstate_scope, tfstate_coverage), provenance=provenance, resources=resources, costs=_cost_entries(cost_data, provenance.collected_at), diff --git a/cloudexit/report/evidence.py b/cloudexit/report/evidence.py index e0ea97e..7c81168 100644 --- a/cloudexit/report/evidence.py +++ b/cloudexit/report/evidence.py @@ -53,6 +53,22 @@ def build_document_table( return key_value_table(rows, context.appearance) +def _not_assessed_text(scope, b): + parts = [ + _counted(b, key, n) + for key, n in ( + ("ev_not_assessed_other", scope.not_assessed_other), + ("ev_not_assessed_unmapped", scope.not_assessed_unmapped), + ) + if n + ] + return ", ".join(parts) + + +def _counted(b, key, n): + return b[f"{key}_one"] if n == 1 else b[f"{key}_many"].format(n=n) + + def scope_rows(context, b): # (label, text, monospaced), the same for the PDF and the HTML report scope = context.scope @@ -91,6 +107,8 @@ def scope_rows(context, b): ): if key in values: rows.append((b[label], values[key] or b["ev_not_determinable"], False)) + if scope.not_assessed: + rows.append((b["ev_not_assessed"], _not_assessed_text(scope, b), False)) source = b["ev_source_tfstate" if scope.kind == "tfstate" else "ev_source_live"] rows.append( ( @@ -117,6 +135,19 @@ def link_markup(url, color): return f'{escape(url)}' +def platform_failure(provenance, b): + # What happened to an upload that failed, as it was true when the report was written + if provenance.upload_reason: + text = b["ev_platform_failed"].format( + reason=provenance.upload_reason.rstrip(". ") + ) + else: + text = b["ev_platform_failed_no_reason"] + if provenance.upload_spooled: + text = f"{text} {b['ev_platform_kept']}" + return text + + def build_provenance_table(context, b): provenance = context.provenance if provenance.catalogue_version and provenance.catalogue_sha256: @@ -137,7 +168,11 @@ def build_provenance_table(context, b): text = escape(b["ev_platform_uploaded"]) if provenance.platform_url: text = f"{text}: {link_markup(provenance.platform_url, context.appearance.heading2_color)}" + elif provenance.assessment_id: + text = f"{text} · {escape(b['fa_uploaded_id'].format(assessment_id=provenance.assessment_id))}" rows.append((b["ev_platform_label"], Paragraph(text, KV_VALUE))) + elif provenance.upload_failed: + rows.append((b["ev_platform_label"], platform_failure(provenance, b))) else: rows.append((b["ev_platform_label"], b["ev_platform_none"])) return key_value_table(rows, context.appearance) diff --git a/cloudexit/report/executive_summary.py b/cloudexit/report/executive_summary.py index eb1b4d5..b36e939 100644 --- a/cloudexit/report/executive_summary.py +++ b/cloudexit/report/executive_summary.py @@ -14,7 +14,6 @@ from .sections import ( alternative_technologies, costs_overview, - full_assessment, glossary, introduction, resource_inventory, @@ -45,8 +44,6 @@ def build_content(context, styles, b): content.append(Spacer(1, 18)) content += glossary(context, styles, b) content.append(chapter_break()) - content += full_assessment(context, styles, b) - content.append(chapter_break()) content += legal_notices(context, styles, b) return content diff --git a/cloudexit/report/html_sections.py b/cloudexit/report/html_sections.py index e41ef37..9cd6146 100644 --- a/cloudexit/report/html_sections.py +++ b/cloudexit/report/html_sections.py @@ -1,6 +1,6 @@ from markupsafe import Markup -from .evidence import scope_rows +from .evidence import platform_failure, scope_rows from .helpers import ( format_change, initials, @@ -11,12 +11,9 @@ strategy_name, ) from .html_egress import egress_page -from .notices import text_parts +from .notices import icon_notice, text_parts from .palette import SEVERITIES from .sections import ( - AWS_ICONS_URL, - AWS_TRADEMARK_URL, - AZURE_ICONS_URL, PRODUCT_SITES, REPOSITORY_URL, ) @@ -406,6 +403,15 @@ def _inventory(context, b): ] if scope.kind == "tfstate": caption_parts.append(b["html_inv_not_managed"]) + if scope.not_assessed: + key = ( + "html_inv_not_assessed_one" + if scope.not_assessed == 1 + else "html_inv_not_assessed_many" + ) + caption_parts.append( + b[key].format(n=scope.not_assessed, about=b["html_nav_about"]) + ) if scope.kind == "azure": empty = b["html_inv_empty_resource_group"].format( @@ -544,10 +550,8 @@ def _count_more(b, n): def _platform_cell(context, b): provenance = context.provenance if not provenance.uploaded: - if provenance.upload_reason: - return [ - b["html_platform_not_uploaded"].format(reason=provenance.upload_reason) - ] + if provenance.upload_failed: + return [platform_failure(provenance, b)] return [b["ev_platform_none"]] parts = [b["ev_platform_uploaded"]] if provenance.platform_url: @@ -667,20 +671,7 @@ def _about(context, egress, b): def _notices(context, egress, b): provenance = context.provenance - icons = None - if context.cloud_service_provider == 2: - icons = text_parts( - b["notices_icons_aws"], - { - "icons_url": (b["html_label_aws_icons"], AWS_ICONS_URL), - "guidelines_url": (b["html_label_aws_guidelines"], AWS_TRADEMARK_URL), - }, - ) - elif context.cloud_service_provider == 1: - icons = text_parts( - b["notices_icons_azure"], - {"icons_url": (b["html_label_azure_icons"], AZURE_ICONS_URL)}, - ) + icons = icon_notice(context.cloud_service_provider, b) if context.scope.kind == "tfstate": figures = b["html_figures_tfstate"] else: diff --git a/cloudexit/report/notices.py b/cloudexit/report/notices.py index 60c96d7..2f81551 100644 --- a/cloudexit/report/notices.py +++ b/cloudexit/report/notices.py @@ -37,14 +37,22 @@ def text_parts(template, links, values=None): return parts -def _icon_notice(provider, b): +def icon_notice(provider, b): + # The names are the links: the HTML shows the name, the PDF the name and, in + # brackets, the address if provider == 2: return text_parts( b["notices_icons_aws"], - {"icons_url": AWS_ICONS_URL, "guidelines_url": AWS_TRADEMARK_URL}, + { + "icons": (b["notices_label_aws_icons"], AWS_ICONS_URL), + "guidelines": (b["notices_label_aws_guidelines"], AWS_TRADEMARK_URL), + }, ) if provider == 1: - return text_parts(b["notices_icons_azure"], {"icons_url": AZURE_ICONS_URL}) + return text_parts( + b["notices_icons_azure"], + {"icons": (b["notices_label_azure_icons"], AZURE_ICONS_URL)}, + ) return None @@ -60,7 +68,7 @@ def notices_html(provider, generated_at, b=BLOCKS, egress=False): *([[b["notices_egress"]]] if egress else []), ] trademarks = [[b["notices_trademark_own"]], [b["notices_trademark_others"]]] - icons = _icon_notice(provider, b) + icons = icon_notice(provider, b) if icons: trademarks.append(icons) return { @@ -85,9 +93,20 @@ def notices_html(provider, generated_at, b=BLOCKS, egress=False): } +def _link_markup(label, url, color): + # A link that is not its own address prints the address too, for print + if label == url: + return link_markup(url, color) + return f"{escape(label)} ({link_markup(url, color)})" + + def parts_markup(parts, color): return "".join( - link_markup(part[1], color) if isinstance(part, tuple) else escape(part) + ( + _link_markup(part[0], part[1], color) + if isinstance(part, tuple) + else escape(part) + ) for part in parts ) diff --git a/cloudexit/report/sections.py b/cloudexit/report/sections.py index c9d5584..5df8c54 100644 --- a/cloudexit/report/sections.py +++ b/cloudexit/report/sections.py @@ -7,7 +7,6 @@ from reportlab.pdfbase.pdfmetrics import stringWidth from reportlab.platypus import ( Flowable, - KeepTogether, ListFlowable, ListItem, Paragraph, @@ -17,7 +16,6 @@ ) from .brief_summary import cost_finding -from .evidence import link_markup from .flow import section from .fonts import FONT, FONT_BOLD, FONT_MONO from .placeholder import LabelWithPlaceholder @@ -56,9 +54,6 @@ "RiskCode", fontName=FONT_MONO ) RISK_MUTED_RIGHT = text_style("RiskMutedRight", 8.5, 11, MUTED, alignment=TA_RIGHT) -COMMAND = text_style("CommandLine", 8.5, 12, INK).clone( - "CommandLine", fontName=FONT_MONO, leftIndent=8, spaceBefore=4, spaceAfter=8 -) GLOSSARY_TERM_WIDTH = 4.2 * cm RISK_BAR_MIN_SEGMENT = 62 ALT_EXAMPLES = 3 @@ -358,6 +353,11 @@ def resource_inventory(context, styles, b): ("NOSPLIT", (0, total_row - 1), (-1, total_row)), ], ) + caption = b["es_res_caption"] + skipped = context.scope.not_assessed + if skipped: + key = "res_not_assessed_one" if skipped == 1 else "res_not_assessed_many" + caption = f"{caption} {b[key].format(n=skipped)}" return section( head, [ @@ -371,7 +371,7 @@ def resource_inventory(context, styles, b): Spacer(1, 12), table, Spacer(1, 6), - Paragraph(b["es_res_caption"], ALT_CAPTION), + Paragraph(caption, ALT_CAPTION), ], ) @@ -576,11 +576,7 @@ def risk_inventory(context, styles, b): def _examples_cell(entry, b): cell = [] for alt in entry.alternatives[:ALT_EXAMPLES]: - vendor = ( - f' · {escape(alt.vendor)}' - if alt.vendor - else "" - ) + vendor = f' · {escape(alt.vendor or b["ev_dash"])}' cell.append(Paragraph(f"{escape(alt.name)}{vendor}", ALT_BODY)) flags = [ b[key] @@ -758,76 +754,3 @@ def glossary(context, styles, b): Spacer(1, 12), table, ] - - -# Closing page -def full_assessment(context, styles, b): - body = styles["ContentStyle"] - provenance = context.provenance - if provenance.uploaded: - content = [ - Paragraph(b["fa_heading_uploaded"], styles["Heading1"]), - Paragraph(escape(b["fa_uploaded"]), body), - ] - if provenance.platform_url: - link = link_markup( - provenance.platform_url, context.appearance.heading2_color - ) - content.append(Paragraph(b["fa_uploaded_link"].format(link=link), body)) - elif provenance.assessment_id: - content.append( - Paragraph( - escape( - b["fa_uploaded_id"].format( - assessment_id=provenance.assessment_id - ) - ), - body, - ) - ) - return content - - rows = [[_text(term, ALT_BODY_BOLD), _text(text)] for term, text in b["fa_items"]] - table = Table( - rows, - colWidths=[GLOSSARY_TERM_WIDTH, 16 * cm - GLOSSARY_TERM_WIDTH], - hAlign="LEFT", - ) - table.setStyle( - TableStyle( - [ - ( - "LINEABOVE", - (0, 0), - (-1, 0), - 1, - HexColor(context.appearance.header_row_color), - ), - ("LINEBELOW", (0, 0), (-1, -1), 0.4, HexColor(HAIRLINE)), - ("LEFTPADDING", (0, 0), (-1, -1), 5), - ("RIGHTPADDING", (0, 0), (-1, -1), 5), - ("TOPPADDING", (0, 0), (-1, -1), 5), - ("BOTTOMPADDING", (0, 0), (-1, -1), 5), - ("VALIGN", (0, 0), (-1, -1), "TOP"), - ] - ) - ) - color = context.appearance.heading2_color - sites = b["fa_sites"].format( - **{name: link_markup(url, color) for name, url in PRODUCT_SITES} - ) - return [ - Paragraph(b["fa_heading"], styles["Heading1"]), - Paragraph(escape(b["fa_lead"]), body), - Spacer(1, 6), - table, - Spacer(1, 6), - KeepTogether( - [ - Paragraph(b["fa_how_heading"], styles["Heading2"]), - Paragraph(escape(b["fa_how"]), body), - Paragraph(escape(b["fa_command"]), COMMAND), - Paragraph(sites, body), - ] - ), - ] diff --git a/cloudexit/report/wording.py b/cloudexit/report/wording.py index 86cc08f..d54befd 100644 --- a/cloudexit/report/wording.py +++ b/cloudexit/report/wording.py @@ -27,6 +27,25 @@ "ev_resource_groups": "Resource Groups", "ev_regions": "Regions", "ev_not_determinable": "Not determinable from the state", + "ev_not_assessed": "Not Assessed", + "ev_not_assessed_other_one": "1 resource of another provider", + "ev_not_assessed_other_many": "{n} resources of other providers", + "ev_not_assessed_unmapped_one": "1 resource of a type not in the data catalogue", + "ev_not_assessed_unmapped_many": ( + "{n} resources of types not in the data catalogue" + ), + "res_not_assessed_one": ( + "1 resource in the state file was not assessed (see page 2)." + ), + "res_not_assessed_many": ( + "{n} resources in the state file were not assessed (see page 2)." + ), + "html_inv_not_assessed_one": ( + "1 resource in the state file was not assessed (see {about})" + ), + "html_inv_not_assessed_many": ( + "{n} resources in the state file were not assessed (see {about})" + ), "ev_source_label": "Source", "ev_source_live": "Provider APIs", "ev_source_tfstate": "Terraform State", @@ -38,6 +57,9 @@ "ev_platform_label": "Platform", "ev_platform_none": "Self-collected data: no platform evaluated this report.", "ev_platform_uploaded": "Uploaded to the platform", + "ev_platform_failed": "Upload failed: {reason}.", + "ev_platform_failed_no_reason": "Upload failed.", + "ev_platform_kept": "The run was kept for a later upload.", "ev_stored_footnote": ( "This document was generated when run {run} completed and is stored " "unchanged. Later runs are reported in their own documents." @@ -335,41 +357,6 @@ "definition": "One completed assessment of the defined scope, identified by the run ID on page 2.", }, ], - "fa_heading": "What the Full Assessment Adds", - "fa_lead": ( - "This Executive Summary is the Basic assessment. It was collected and " - "evaluated locally and covers the resource inventory, " - "costs, exit risks and alternative technologies of the defined scope. " - "The full assessment on a platform adds:" - ), - "fa_items": [ - ( - "Exit Readiness Grade", - "a letter grade from A to F that shows how ready the defined scope is for an exit.", - ), - ( - "Vendor Lock-in Score", - "a rating of how easily each resource type can be replaced, in terms of skills, technology and operations.", - ), - ( - "Scored Alternatives", - "the alternative technologies for each resource type, rated and ranked.", - ), - ], - "fa_how_heading": "How to Get Them", - "fa_how": ( - "Connect cloudexit to a platform with an API key and run the " - "assessment again; the results are uploaded and evaluated there. For " - "example:" - ), - "fa_command": "cloudexit aws --host HOST --api-key-file KEY_FILE", - "fa_sites": "Platforms: {escapecloud} · {exitcloud}", - "fa_heading_uploaded": "The Full Assessment", - "fa_uploaded": ( - "This assessment was uploaded. The Exit Readiness Grade, the Vendor " - "Lock-in Score, scored alternatives and the Egress & Data Landscape " - "are available in its reports on the platform." - ), "fa_uploaded_link": "Open the reports: {link}", "fa_uploaded_id": "Assessment ID: {assessment_id}", "eg_heading_introduction": "Introduction", @@ -652,20 +639,20 @@ "affiliation." ), "notices_icons_aws": ( - "AWS service icons are taken from the AWS Architecture Icons " - "({icons_url}); their use is subject to the terms published there and " - "in the AWS Trademark Guidelines ({guidelines_url}). Amazon Web " - "Services and AWS are trademarks of Amazon.com, Inc. or its " - "affiliates. This report is not endorsed by or affiliated with Amazon " - "Web Services." + "AWS service icons are taken from the {icons}; their use is subject to " + "the terms published there and in the {guidelines}. Amazon Web Services " + "and AWS are trademarks of Amazon.com, Inc. or its affiliates. This " + "report is not endorsed by or affiliated with Amazon Web Services." ), "notices_icons_azure": ( - "Azure service icons are taken from the Azure Architecture Icons " - "({icons_url}); their use is subject to Microsoft's icon terms " - "published there. Microsoft and Azure are trademarks of the Microsoft " - "group of companies. This report is not endorsed by or affiliated with " - "Microsoft." - ), + "Azure service icons are taken from the {icons}; their use is subject " + "to Microsoft's icon terms published there. Microsoft and Azure are " + "trademarks of the Microsoft group of companies. This report is not " + "endorsed by or affiliated with Microsoft." + ), + "notices_label_aws_icons": "AWS Architecture Icons", + "notices_label_aws_guidelines": "AWS Trademark Guidelines", + "notices_label_azure_icons": "Azure Architecture Icons", "notices_software_heading": "Software", "notices_software": ( "cloudexit is free software, licensed under the GNU Affero General " @@ -752,7 +739,6 @@ "and operations, with the alternatives rated and ranked." ), "html_scores_on_platform": "For this run it is on the platform.", - "html_platform_not_uploaded": "Not uploaded: {reason}", "html_hint_title": "EscapeCloud Light & Enterprise", "html_hint_text": ( "EscapeCloud's scoring engine offers several scoring methods to assess " @@ -934,9 +920,6 @@ "html_notice_advice": ( "It is not legal, financial or investment advice, an audit or an exit plan." ), - "html_label_aws_icons": "AWS Architecture Icons", - "html_label_aws_guidelines": "AWS Trademark Guidelines", - "html_label_azure_icons": "Azure Architecture Icons", "html_label_repository": "github.com/escapecloud/cloudexit", "html_figures_heading": "Figures", "html_figures": ( @@ -1049,6 +1032,7 @@ "ev_catalogue_label", "ev_run_id", "ev_platform_label", + "ev_not_assessed", "cover_title_executive_summary", "cover_title_egress", "ev_egress_report", @@ -1143,9 +1127,6 @@ "es_heading_severity", "es_severity_th_meaning", "es_heading_glossary", - "fa_heading", - "fa_how_heading", - "fa_heading_uploaded", "html_download", "html_download_summary", "html_download_egress", diff --git a/tests/cli/test_upload_modes.py b/tests/cli/test_upload_modes.py index f0b3299..7cd86f2 100644 --- a/tests/cli/test_upload_modes.py +++ b/tests/cli/test_upload_modes.py @@ -91,7 +91,7 @@ def test_auto_with_a_platform_that_refuses_the_key_writes_the_report_first(self) self.assertEqual(ctx.exception.code, codes.UPLOAD_AUTH) self.assert_local_report(report) - self.assertIn("Not uploaded", self.html(report)) + self.assertIn("Upload failed", self.html(report)) def test_required_without_a_platform_fails_before_collecting_anything(self): report = self.report() diff --git a/tests/fixtures/samples/azure_uploaded.json b/tests/fixtures/samples/azure_uploaded.json index 6d65415..1131ce0 100644 --- a/tests/fixtures/samples/azure_uploaded.json +++ b/tests/fixtures/samples/azure_uploaded.json @@ -1,6 +1,6 @@ { "name": "Sample – Document Archive", - "purpose": "The same run, uploaded with a link: the uploaded closing page and the link on page 2 of both documents.", + "purpose": "Uploaded with a link: the link on page 2 of both PDFs and in the HTML.", "provider": "azure", "exit_strategy": 1, "scope": { diff --git a/tests/fixtures/samples/egress_unsized.json b/tests/fixtures/samples/egress_unsized.json new file mode 100644 index 0000000..7c3c17e --- /dev/null +++ b/tests/fixtures/samples/egress_unsized.json @@ -0,0 +1,83 @@ +{ + "name": "Sample – Archive Migration Staging", + "purpose": "Egress where no data store has a size: fee and totals without a figure, every type not sized, no free-tier headroom.", + "provider": "azure", + "exit_strategy": 1, + "scope": { + "tenantId": "00000000-0000-4000-8000-0000000000a1", + "subscriptionId": "00000000-0000-4000-8000-0000000000a2", + "resourceGroupName": "rg-sample-staging" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "region": "westeurope", + "count": 3 + }, + { + "type": "Microsoft.Sql/servers/databases", + "region": "westeurope", + "count": 2 + }, + { + "type": "Microsoft.DocumentDB/databaseAccounts", + "region": "westeurope", + "count": 1 + }, + { + "type": "Microsoft.RecoveryServices/vaults", + "region": "westeurope", + "count": 1 + } + ], + "costs": { + "currency": "EUR", + "months": { + "2026-06": 830.4, + "2026-07": 842.15, + "2026-08": 851.0, + "2026-09": 860.3, + "2026-10": 855.75, + "2026-11": 351.2 + } + }, + "egress": { + "archive_tiers": [ + "Archive" + ], + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "samplestagingblobs", + "size_gib": null, + "unknown": true + }, + { + "type": "Microsoft.Storage/storageAccounts", + "name": "samplestagingexports", + "size_gib": null, + "unknown": true + }, + { + "type": "Microsoft.Sql/servers/databases", + "name": "sample-staging-orders", + "size_gib": null, + "unknown": true + }, + { + "type": "Microsoft.DocumentDB/databaseAccounts", + "name": "sample-staging-search", + "size_gib": null, + "unknown": true + }, + { + "type": "Microsoft.RecoveryServices/vaults", + "name": "sample-staging-vault", + "size_gib": null + } + ] + }, + "upload": null, + "clock": "2026-11-12T09:30:00Z", + "run_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0014" +} diff --git a/tests/fixtures/samples/empty_with_costs.json b/tests/fixtures/samples/empty_with_costs.json new file mode 100644 index 0000000..cadea8b --- /dev/null +++ b/tests/fixtures/samples/empty_with_costs.json @@ -0,0 +1,27 @@ +{ + "name": "Sample – Decommissioned Test Environment", + "purpose": "An empty resource group that still shows six months of small costs: empty states next to real figures.", + "provider": "azure", + "exit_strategy": 1, + "scope": { + "tenantId": "00000000-0000-4000-8000-0000000000a1", + "subscriptionId": "00000000-0000-4000-8000-0000000000a2", + "resourceGroupName": "rg-sample-decommissioned" + }, + "resources": [], + "costs": { + "currency": "EUR", + "months": { + "2026-06": 38.2, + "2026-07": 31.75, + "2026-08": 24.4, + "2026-09": 12.9, + "2026-10": 6.35, + "2026-11": 1.8 + } + }, + "egress": null, + "upload": null, + "clock": "2026-11-12T09:30:00Z", + "run_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0015" +} diff --git a/tests/fixtures/samples/legacy_catalogue.json b/tests/fixtures/samples/legacy_catalogue.json new file mode 100644 index 0000000..2254651 --- /dev/null +++ b/tests/fixtures/samples/legacy_catalogue.json @@ -0,0 +1,43 @@ +{ + "name": "Sample – Internal Wiki", + "purpose": "A dataset without catalogue_meta: the catalogue version is unknown on page 2 and in About This Report.", + "provider": "aws", + "exit_strategy": 2, + "scope": { + "accountId": "123456789012", + "region": "eu-central-1" + }, + "resources": [ + { + "type": "AWS.ec2.describe_instances.Reservations", + "region": "eu-central-1", + "count": 2 + }, + { + "type": "AWS.s3.list_buckets.Buckets", + "region": "eu-central-1", + "count": 1 + }, + { + "type": "AWS.rds.describe_db_instances.DBInstances", + "region": "eu-central-1", + "count": 1 + } + ], + "costs": { + "currency": "USD", + "months": { + "2026-06": 96.4, + "2026-07": 98.1, + "2026-08": 97.25, + "2026-09": 99.6, + "2026-10": 98.8, + "2026-11": 40.3 + } + }, + "egress": null, + "upload": null, + "clock": "2026-11-12T09:30:00Z", + "run_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0016", + "catalogue": "legacy" +} diff --git a/tests/fixtures/samples/tfstate/sample_aws_skipped.tfstate b/tests/fixtures/samples/tfstate/sample_aws_skipped.tfstate new file mode 100644 index 0000000..2724066 --- /dev/null +++ b/tests/fixtures/samples/tfstate/sample_aws_skipped.tfstate @@ -0,0 +1,126 @@ +{ + "version": 4, + "terraform_version": "1.14.6", + "serial": 31, + "lineage": "00000000-0000-4000-8000-0000000000c3", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "aws_instance", + "name": "app", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "availability_zone": "eu-central-1a", + "arn": "arn:aws:ec2:eu-central-1:123456789012:instance/i-0123456789abcdef0" + } + }, + { + "schema_version": 0, + "attributes": { + "availability_zone": "eu-central-1b", + "arn": "arn:aws:ec2:eu-central-1:123456789012:instance/i-0123456789abcdef1" + } + } + ] + }, + { + "mode": "managed", + "type": "aws_s3_bucket", + "name": "assets", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "region": "eu-central-1", + "bucket": "sample-landing-assets", + "arn": "arn:aws:s3:::sample-landing-assets" + } + } + ] + }, + { + "mode": "managed", + "type": "aws_iam_role", + "name": "deploy", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "arn": "arn:aws:iam::123456789012:role/sample-deploy" + } + }, + { + "schema_version": 0, + "attributes": { + "arn": "arn:aws:iam::123456789012:role/sample-readonly" + } + } + ] + }, + { + "mode": "managed", + "type": "aws_cloudwatch_log_group", + "name": "app", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "arn": "arn:aws:logs:eu-central-1:123456789012:log-group:/sample/app" + } + } + ] + }, + { + "mode": "managed", + "type": "azurerm_resource_group", + "name": "shared", + "provider": "provider[\"registry.terraform.io/hashicorp/azurerm\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "location": "westeurope", + "name": "rg-sample-shared" + } + } + ] + }, + { + "mode": "managed", + "type": "azurerm_storage_account", + "name": "backup", + "provider": "provider[\"registry.terraform.io/hashicorp/azurerm\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "location": "westeurope", + "name": "samplebackupstore" + } + } + ] + }, + { + "mode": "managed", + "type": "google_storage_bucket", + "name": "exports", + "provider": "provider[\"registry.terraform.io/hashicorp/google\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "location": "EU", + "name": "sample-exports" + } + } + ] + } + ] +} diff --git a/tests/fixtures/samples/tfstate_skipped.json b/tests/fixtures/samples/tfstate_skipped.json new file mode 100644 index 0000000..c156841 --- /dev/null +++ b/tests/fixtures/samples/tfstate_skipped.json @@ -0,0 +1,15 @@ +{ + "name": "Sample – Multi-Cloud Landing Zone (Terraform)", + "purpose": "A state file with instances of another provider and of types not in the data catalogue: the Not Assessed row on page 2 and in About This Report.", + "provider": "aws", + "exit_strategy": 1, + "scope": { + "tfstate": "sample_aws_skipped.tfstate" + }, + "resources": [], + "costs": null, + "egress": null, + "upload": null, + "clock": "2026-11-12T09:30:00Z", + "run_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0017" +} diff --git a/tests/fixtures/samples/upload_failed.json b/tests/fixtures/samples/upload_failed.json new file mode 100644 index 0000000..5ebd294 --- /dev/null +++ b/tests/fixtures/samples/upload_failed.json @@ -0,0 +1,52 @@ +{ + "name": "Sample – Order Tracking Service", + "purpose": "A failed upload that was kept for a later upload: the wording on page 2 and in About This Report.", + "provider": "aws", + "exit_strategy": 1, + "scope": { + "accountId": "123456789012", + "region": "eu-central-1" + }, + "resources": [ + { + "type": "AWS.ec2.describe_instances.Reservations", + "region": "eu-central-1", + "count": 5 + }, + { + "type": "AWS.s3.list_buckets.Buckets", + "region": "eu-central-1", + "count": 3 + }, + { + "type": "AWS.rds.describe_db_instances.DBInstances", + "region": "eu-central-1", + "count": 1 + }, + { + "type": "AWS.lambda.list_functions.Functions", + "region": "eu-central-1", + "count": 6 + } + ], + "costs": { + "currency": "USD", + "months": { + "2026-06": 412.5, + "2026-07": 398.2, + "2026-08": 405.75, + "2026-09": 421.1, + "2026-10": 418.9, + "2026-11": 171.6 + } + }, + "egress": null, + "upload": { + "status": "failed", + "reason": "The platform did not answer in time", + "spooled": true, + "sync_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0013" + }, + "clock": "2026-11-12T09:30:00Z", + "run_id": "01939b8e-7c2a-7d41-9a51-3c8f0a6e0013" +} diff --git a/tests/report/test_basic_report.py b/tests/report/test_basic_report.py index 24606d8..64d3e4d 100644 --- a/tests/report/test_basic_report.py +++ b/tests/report/test_basic_report.py @@ -69,15 +69,13 @@ (0, "Risk Severity Levels"), (0, "Glossary"), ] -OFFLINE_OUTLINE = BASE_OUTLINE + [ - (0, "What the Full Assessment Adds"), - (1, "How to Get Them"), - (0, "Legal Notices"), -] -UPLOADED_OUTLINE = BASE_OUTLINE + [ - (0, "The Full Assessment"), - (0, "Legal Notices"), -] +OUTLINE = BASE_OUTLINE + [(0, "Legal Notices")] +CLOSING_PAGE_TEXTS = ( + "What the Full Assessment Adds", + "The Full Assessment", + "How to Get Them", + "HOST", +) SCORE_PATTERNS = ( r"/ 100", r"/ 5\b", @@ -87,6 +85,14 @@ "Lock-in Score", "Scored Alternatives", ) +# An edition name; the "Enterprise support" flag of an alternative is not one +PROMOTION_PATTERNS = ( + r"\bLight\b", + r"\bEnterprise\b(?!\s+support)", + "Exit Readiness Grade", + "Vendor Lock-in Score", +) +EXITCLOUD_URL = "https://app.exitcloud.io/assessments/a-1" TFSTATE_SCOPE = { "file": "infra.tfstate", "sha256": "ab" * 32, @@ -252,19 +258,125 @@ def test_a_tfstate_assessment_explains_the_missing_costs(self): self.assertIn("e1c2f0c0-0000-4000-8000-000000000000 (serial 7)", text) +class TfstateCoverageTests(BasicReportTestCase): + def coverage(self, other=0, unmapped=0): + return { + "instances_total": 9 + other + unmapped, + "instances_counted": 9, + "instances_excluded_other_provider": other, + "instances_excluded_unmapped": unmapped, + } + + def pages(self, key, coverage, **overrides): + _path, context, reader = self.render( + self.tfstate_fixture(), + key=key, + tfstate_scope=TFSTATE_SCOPE, + tfstate_coverage=coverage, + **overrides, + ) + return context, page_texts(reader) + + def test_both_kinds_are_named_with_the_total_in_the_inventory_caption(self): + context, pages = self.pages("covered", self.coverage(other=3, unmapped=2)) + + self.assertEqual(context.scope.not_assessed, 5) + self.assertIn( + "Not Assessed 3 resources of other providers, " + "2 resources of types not in the data catalogue", + pages[1], + ) + inventory = next(text for text in pages if "Share = resources" in text) + self.assertIn( + "5 resources in the state file were not assessed (see page 2)", inventory + ) + + def test_only_the_parts_that_are_not_zero_are_shown(self): + for key, coverage, expected, absent in ( + ( + "other", + self.coverage(other=4), + "4 resources of other providers", + "types not in", + ), + ( + "unmapped", + self.coverage(unmapped=1), + "1 resource of a type not in the data catalogue", + "other provider", + ), + ( + "one other", + self.coverage(other=1), + "1 resource of another provider", + "types not in", + ), + ): + with self.subTest(key): + _context, pages = self.pages(key, coverage) + row = pages[1].split("Not Assessed", 1)[1].split("Source", 1)[0] + + self.assertIn(expected, row) + self.assertNotIn(absent, row) + + def test_a_singular_total_says_was(self): + _context, pages = self.pages("one total", self.coverage(unmapped=1)) + + inventory = next(text for text in pages if "Share = resources" in text) + self.assertIn( + "1 resource in the state file was not assessed (see page 2)", inventory + ) + + def test_without_excluded_instances_there_is_no_row_and_no_caption(self): + for key, coverage in ( + ("zeros", self.coverage()), + ("older manifest", None), + ("empty", {}), + ( + "not numbers", + { + "instances_excluded_other_provider": "3", + "instances_excluded_unmapped": -2, + }, + ), + ): + with self.subTest(key): + context, pages = self.pages(key, coverage) + text = " ".join(pages) + + self.assertEqual(context.scope.not_assessed, 0) + self.assertNotIn("Not Assessed", text) + self.assertNotIn("were not assessed", text) + self.assertNotIn("was not assessed", text) + + def test_a_live_run_never_shows_the_row(self): + _path, context, reader = self.render( + key="live coverage", tfstate_coverage=self.coverage(other=3, unmapped=2) + ) + + self.assertEqual(context.scope.not_assessed, 0) + self.assertNotIn("Not Assessed", " ".join(page_texts(reader))) + + class StructureTests(BasicReportTestCase): def test_the_heading_outline_is_the_expected_list(self): for name, fixture, overrides in self.scenarios(): with self.subTest(name): _path, _context, reader = self.render(fixture, key=name, **overrides) - expected = UPLOADED_OUTLINE if "uploaded" in name else OFFLINE_OUTLINE - self.assertEqual(outline_entries(reader.outline), expected) + self.assertEqual(outline_entries(reader.outline), OUTLINE) + + def test_the_outline_is_the_same_for_an_upload_without_a_link(self): + _path, _context, reader = self.render( + key="no link", provenance=provenance(uploaded=True, assessment_id="a-77") + ) + + self.assertEqual(outline_entries(reader.outline), OUTLINE) def test_the_outline_does_not_depend_on_the_data(self): _path, _context, reader = self.render( build_empty_report_fixture(), provenance=provenance() ) - self.assertEqual(outline_entries(reader.outline), OFFLINE_OUTLINE) + self.assertEqual(outline_entries(reader.outline), OUTLINE) def test_each_chapter_starts_a_page_and_page_two_is_the_document_information(self): _path, _context, reader = self.render(key="default") @@ -281,7 +393,9 @@ def test_each_chapter_starts_a_page_and_page_two_is_the_document_information(sel numbers = [reader.get_destination_page_number(entry) for entry in chapters] # One chapter per page, except the severity levels and the glossary self.assertEqual(len(set(numbers)), len(numbers) - 1) - self.assertEqual(numbers[-4], numbers[-3]) + self.assertEqual(numbers[-3], numbers[-2]) + self.assertIn("Legal Notices", pages[numbers[-1]]) + self.assertEqual(numbers[-1], len(pages) - 1) def test_running_header_and_footer(self): path, context, reader = self.render(key="default") @@ -393,7 +507,7 @@ def test_names_from_the_data_are_escaped(self): class AlternativesTests(BasicReportTestCase): - def test_a_missing_vendor_is_left_out_and_alternatives_are_alphabetical(self): + def test_a_missing_vendor_shows_a_dash_and_alternatives_are_alphabetical(self): fixture = build_report_fixture() technologies = fixture["alternative_technologies"] technologies.append( @@ -416,7 +530,11 @@ def test_a_missing_vendor_is_left_out_and_alternatives_are_alphabetical(self): ) self.assertEqual(entry.alternatives[0].vendor, "") self.assertEqual(entry.alternatives[1].vendor, "OpenInfra Foundation") - self.assertNotIn("Unknown Organization", " ".join(page_texts(reader))) + text = " ".join(page_texts(reader)) + self.assertIn("Alpha · –", text) + self.assertIn("OpenStack · OpenInfra Foundation", text) + self.assertLess(text.index("Alpha · –"), text.index("OpenStack ·")) + self.assertNotIn("Unknown Organization", text) class ProvenanceTests(BasicReportTestCase): @@ -433,33 +551,71 @@ def test_an_offline_report_says_it_is_self_collected(self): self.assertIn(SYNC_ID, second) self.assertIn("2026-11-02 · (sha256 cccccccccccc)", second) self.assertIn("Self-collected data: no platform evaluated this report.", second) - self.assertIn("What the Full Assessment Adds", pages[-2]) self.assertEqual( link_targets(reader) - AWS_NOTICE_LINKS, - {"https://escapecloud.io", "https://exitcloud.io"}, + set(), ) - def test_an_uploaded_report_carries_the_link_instead_of_the_promotion(self): + def test_an_uploaded_report_carries_the_link_on_page_two_only(self): _path, _context, reader = self.render(key="uploaded", provenance=uploaded()) pages = page_texts(reader) self.assertIn("Uploaded to the platform", pages[1]) self.assertIn(URL, pages[1]) self.assertNotIn("Self-collected data", " ".join(pages)) - self.assertIn("The Full Assessment", pages[-2]) - self.assertIn(URL, pages[-2]) - self.assertNotIn("What the Full Assessment Adds", " ".join(pages)) + self.assertEqual(sum(URL in text for text in pages), 1) self.assertEqual(link_targets(reader) - AWS_NOTICE_LINKS, {URL}) - def test_an_upload_without_a_link_names_the_assessment(self): + def test_an_upload_without_a_link_names_the_assessment_on_page_two(self): _path, _context, reader = self.render( - provenance=( - uploaded(platform_url=None) - if False - else provenance(uploaded=True, platform_url=None, assessment_id="a-77") - ) + key="no link", provenance=provenance(uploaded=True, assessment_id="a-77") + ) + pages = page_texts(reader) + + self.assertIn("Uploaded to the platform", pages[1]) + self.assertIn("Assessment ID: a-77", pages[1]) + self.assertEqual(sum("a-77" in text for text in pages), 1) + self.assertEqual(link_targets(reader) - AWS_NOTICE_LINKS, set()) + + def test_a_failed_upload_says_what_happened(self): + for name, changes, expected in ( + ( + "kept", + {"upload_reason": "upload_unavailable", "upload_spooled": True}, + "Upload failed: upload_unavailable. The run was kept for a later upload.", + ), + ( + "lost", + {"upload_reason": "upload_rejected"}, + "Upload failed: upload_rejected.", + ), + ("no reason", {}, "Upload failed."), + ( + "no reason, kept", + {"upload_spooled": True}, + "Upload failed. The run was kept", + ), + ): + with self.subTest(name): + _path, _context, reader = self.render( + key=f"failed {name}", + provenance=provenance(upload_failed=True, **changes), + ) + pages = page_texts(reader) + + self.assertIn(expected, pages[1]) + self.assertNotIn("Self-collected data", " ".join(pages)) + self.assertEqual("kept for a later upload" in pages[1], "kept" in name) + self.assertEqual(link_targets(reader) - AWS_NOTICE_LINKS, set()) + + def test_a_reason_that_ends_in_a_full_stop_is_not_doubled(self): + _path, _context, reader = self.render( + key="failed stop", + provenance=provenance(upload_failed=True, upload_reason="No answer."), ) - self.assertIn("Assessment ID: a-77", page_texts(reader)[-2]) + + self.assertIn("Upload failed: No answer. ", page_texts(reader)[1] + " ") + self.assertNotIn("No answer..", page_texts(reader)[1]) def test_an_unknown_catalogue_version_is_said_so(self): _path, _context, reader = self.render( @@ -554,31 +710,71 @@ def test_the_json_report_has_a_provenance_object(self): class BasicOnlyTests(BasicReportTestCase): - def test_no_score_or_grade_appears_outside_the_closing_page(self): + def test_no_score_or_grade_appears_on_any_page(self): for name, fixture, overrides in self.scenarios(): with self.subTest(name): _path, _context, reader = self.render(fixture, key=name, **overrides) - body = " ".join(page_texts(reader)[:-2]) + body = " ".join(page_texts(reader)) for pattern in SCORE_PATTERNS: self.assertIsNone(re.search(pattern, body, re.IGNORECASE), pattern) - def test_the_closing_page_only_names_what_the_full_assessment_adds(self): - _path, _context, reader = self.render(key="offline", provenance=provenance()) - closing = page_texts(reader)[-2] + def test_no_closing_page_and_no_command_is_left(self): + for name, fixture, overrides in self.scenarios(): + with self.subTest(name): + _path, _context, reader = self.render(fixture, key=name, **overrides) + body = " ".join(page_texts(reader)) - for name in ( - "Exit Readiness Grade", - "Vendor Lock-in Score", - "Scored Alternatives", - ): - self.assertIn(name, closing) - # The Community Edition produces it itself, so the platforms do not add it - self.assertNotIn("Egress & Data Landscape", closing) - self.assertFalse( - any("Egress" in term for term, _text in BLOCKS["fa_items"]), - ) - self.assertNotRegex(closing, r"\d+ / (100|5)") + for text in CLOSING_PAGE_TEXTS: + self.assertNotIn(text, body) + self.assertNotIn("--api-key-file", body) + + def test_no_edition_or_grade_is_promoted_on_any_page(self): + for name, fixture, overrides in self.scenarios(): + with self.subTest(name): + _path, _context, reader = self.render(fixture, key=name, **overrides) + for number, text in enumerate(page_texts(reader), start=1): + for pattern in PROMOTION_PATTERNS: + self.assertIsNone(re.search(pattern, text), (number, pattern)) + + def test_the_enterprise_support_flag_does_not_trip_the_edition_check(self): + _path, _context, reader = self.render(key="default") + body = " ".join(page_texts(reader)) + + self.assertIn("Enterprise support", body) + self.assertIsNone(re.search(PROMOTION_PATTERNS[1], body)) + self.assertIsNotNone(re.search(PROMOTION_PATTERNS[1], "The Enterprise edition")) + self.assertIsNotNone(re.search(PROMOTION_PATTERNS[0], "A Light assessment")) + + def test_exitcloud_is_named_only_on_page_two_of_an_uploaded_report(self): + for name, fixture, overrides in self.scenarios(): + if "uploaded" in name: + overrides = { + **overrides, + "provenance": provenance( + uploaded=True, platform_url=EXITCLOUD_URL, assessment_id="a-1" + ), + } + with self.subTest(name): + _path, _context, reader = self.render(fixture, **overrides) + pages = [text.lower() for text in page_texts(reader)] + named = [ + n for n, text in enumerate(pages, start=1) if "exitcloud" in text + ] + + self.assertEqual(named, [2] if "uploaded" in name else []) + + def test_escapecloud_is_named_only_on_the_legal_notices_page(self): + for name, fixture, overrides in self.scenarios(): + with self.subTest(name): + _path, _context, reader = self.render(fixture, key=name, **overrides) + pages = [text.lower() for text in page_texts(reader)] + named = [ + n for n, text in enumerate(pages, start=1) if "escapecloud" in text + ] + + self.assertEqual(named, [len(pages)]) + self.assertIn("legal notices", pages[-1]) class TermGuardTests(BasicReportTestCase): @@ -623,19 +819,6 @@ def test_the_guard_finds_what_it_should(self): self.assertEqual(find_terms("an ordinary sentence about cloud exit"), []) self.assertEqual(find_terms("ICTs and DORAs"), []) - def test_the_platforms_are_named_only_on_the_last_two_pages(self): - _path, _context, reader = self.render(key="default") - pages = page_texts(reader) - - for text in pages[:-2]: - self.assertNotIn("escapecloud", text.lower()) - self.assertNotIn("exitcloud", text.lower()) - # The closing page keeps both platform URLs; the notices name EscapeCloud - self.assertIn("https://escapecloud.io", pages[-2]) - self.assertIn("https://exitcloud.io", pages[-2]) - self.assertIn("EscapeCloud", pages[-1]) - self.assertNotIn("exitcloud", pages[-1].lower()) - class LegalNoticesTests(BasicReportTestCase): ICON_MARKERS = {1: "Microsoft group", 2: "Amazon.com"} @@ -685,6 +868,21 @@ def test_the_icon_links_follow_the_provider(self): self.assertIn(AZURE_ICONS_URL, links) self.assertNotIn(AWS_ICONS_URL, links) + def test_the_icon_names_are_followed_by_their_addresses(self): + expected = { + 2: [ + f"AWS Architecture Icons ({AWS_ICONS_URL})", + f"AWS Trademark Guidelines ({AWS_TRADEMARK_URL})", + ], + 1: [f"Azure Architecture Icons ({AZURE_ICONS_URL})"], + } + for name, fixture, overrides in self.scenarios(): + with self.subTest(name): + context, _reader, pages = self.notices_page(name, fixture, **overrides) + last = pages[-1].replace(" ", "") + for text in expected[context.cloud_service_provider]: + self.assertIn(text.replace(" ", ""), last) + def test_the_cover_names_the_edition_and_no_classification(self): _context, _reader, pages = self.notices_page("offline", provenance=provenance()) cover = pages[0] diff --git a/tests/report/test_html_report.py b/tests/report/test_html_report.py index 128569d..de594af 100644 --- a/tests/report/test_html_report.py +++ b/tests/report/test_html_report.py @@ -11,7 +11,12 @@ from pypdf import PdfReader from cloudexit.engine import generate_report, perform_risk_assessment -from cloudexit.report.sections import AWS_ICONS_URL, AZURE_ICONS_URL, REPOSITORY_URL +from cloudexit.report.sections import ( + AWS_ICONS_URL, + AWS_TRADEMARK_URL, + AZURE_ICONS_URL, + REPOSITORY_URL, +) from cloudexit.report.wording import BLOCKS from cloudexit.resources import asset_path from tests.report.test_basic_report import page_texts @@ -227,9 +232,8 @@ def test_the_uploaded_chip_and_link_appear_only_on_uploaded_runs(self): chip = re.search(r']*>{name}', page + ) + ), + 1, + name, + ) + self.assertNotIn(f"({name})", text) + self.assertNotIn(f"( {name} )", text) + self.assertNotIn(f"{name} (", text) + for other in names.keys() - {scenario.provider}: + for name in names[other]: + self.assertNotIn(name, text) + def test_the_figures_notice_follows_the_state(self): def figures(case): page = self.page(case) diff --git a/tests/report/test_report_flow.py b/tests/report/test_report_flow.py index 1990737..6277a8c 100644 --- a/tests/report/test_report_flow.py +++ b/tests/report/test_report_flow.py @@ -5,6 +5,7 @@ import sqlite3 import tempfile import unittest +from dataclasses import replace from contextlib import nullcontext from pathlib import Path from unittest.mock import patch @@ -23,6 +24,7 @@ upload, write_upload_record, ) +from tests.support.html import html_text from tests.support.isolation import forget_cache, isolated_environ from tests.support.api import ( ASSESSMENT_ID, @@ -184,7 +186,7 @@ def test_an_offline_report_has_no_upload_trace(self): sorted(int(entry["id"]) for entry in document["data"]["risk_inventory"]), [2, 4], ) - self.assertNotIn("Not uploaded", self.html(report)) + self.assertNotIn("Upload failed", self.html(report)) self.assertTrue(Path(result["reports"]["PDF"]).is_file()) def generate(self, report, upload_info): @@ -207,10 +209,29 @@ def test_a_failed_upload_puts_a_banner_in_every_report(self): result = self.generate(report, info) self.assertTrue(result["success"], result.get("logs")) - self.assertIn("Not uploaded: upload_unavailable", self.html(report)) + self.assertIn("Upload failed: upload_unavailable.", self.html(report)) self.assertEqual(self.json_report(report)["meta"]["upload"], info) self.assertTrue(Path(result["reports"]["PDF"]).is_file()) + def test_a_failed_upload_that_was_kept_says_so_in_the_provenance(self): + report = self.report() + perform_risk_assessment(1, report) + info = { + "status": "failed", + "reason": REASON_UNAVAILABLE, + "sync_id": SYNC_ID, + "spooled": True, + } + + result = self.generate(report, info) + + self.assertTrue(result["success"], result.get("logs")) + self.assertIn( + "Upload failed: upload_unavailable. The run was kept for a later upload.", + html_text(self.html(report)), + ) + self.assertEqual(self.json_report(report)["meta"]["upload"], info) + def test_a_successful_upload_records_only_the_link(self): report = self.report() perform_risk_assessment(1, report) @@ -347,6 +368,17 @@ def test_report_info_and_the_record_file(self): self.assertIsNone(report_info(None)) self.assertIsNone(report_info(UploadResult(False, False))) + kept = replace(failed, spooled=True) + self.assertEqual( + report_info(kept), + { + "status": "failed", + "reason": REASON_REJECTED, + "sync_id": SYNC_ID, + "spooled": True, + }, + ) + detailed = UploadResult( True, True, @@ -547,7 +579,7 @@ def test_a_failed_upload_still_writes_the_report_and_exits_with_an_upload_code( self.assertEqual(ctx.exception.code, exit_code) self.assert_local_report(report) - self.assertIn(f"Not uploaded: {reason}", self.html(report)) + self.assertIn(f"Upload failed: {reason}.", self.html(report)) self.assertEqual( self.json_report(report)["meta"]["upload"]["reason"], reason ) @@ -650,7 +682,7 @@ def test_a_key_that_cannot_be_right_is_a_failed_upload_not_an_offline_run(self): mock_upload.assert_not_called() self.assertEqual(ctx.exception.code, codes.UPLOAD_AUTH) self.assert_local_report(report) - self.assertIn("Not uploaded: upload_auth", self.html(report)) + self.assertIn("Upload failed: upload_auth.", self.html(report)) # A configured platform keeps the requested depth: nothing forces Basic self.assertEqual(self.sync(report)["assessment_type"], 2) messages = [args[0] for args, _ in self.steps] @@ -691,7 +723,7 @@ def test_collection_problems_win_over_upload_codes(self): self.assertEqual(ctx.exception.code, codes.EGRESS) self.assert_local_report(report) - self.assertIn("Not uploaded", self.html(report)) + self.assertIn("Upload failed", self.html(report)) def test_a_sync_document_problem_wins_over_an_upload_code(self): report = self.report() diff --git a/tests/samples/render.py b/tests/samples/render.py index de5d4b5..84eaaaf 100644 --- a/tests/samples/render.py +++ b/tests/samples/render.py @@ -1,6 +1,7 @@ import logging import shutil import sqlite3 +import tempfile from contextlib import ExitStack from dataclasses import dataclass, field from datetime import datetime @@ -228,6 +229,19 @@ def _render(scenario, directory, catalogue_path): raise RenderError(f"egress.pdf failed: {report['egress_pdf_logs']}") +def _legacy_copy(catalogue_path, directory): + # The dataset of an older release: the same tables, no catalogue_meta + copy = Path(directory) / "legacy_catalogue.db" + shutil.copy(catalogue_path, copy) + conn = sqlite3.connect(copy) + try: + conn.execute("DROP TABLE IF EXISTS catalogue_meta") + conn.commit() + finally: + conn.close() + return copy + + def render_case(scenario, out_dir, *, catalogue_path): directory = Path(out_dir) / scenario.case if directory.exists(): @@ -237,8 +251,11 @@ def render_case(scenario, out_dir, *, catalogue_path): previous = store._active_override clock = frozen_datetime(scenario.clock) try: - store.set_active_catalogue(catalogue_path) with ExitStack() as stack: + if scenario.catalogue == "legacy": + scratch = stack.enter_context(tempfile.TemporaryDirectory()) + catalogue_path = _legacy_copy(catalogue_path, scratch) + store.set_active_catalogue(catalogue_path) # The warnings of the pipeline are about the sample data, not news stack.callback(logging.disable, logging.root.manager.disable) logging.disable(logging.WARNING) diff --git a/tests/samples/scenarios.py b/tests/samples/scenarios.py index 94a2f46..a531f25 100644 --- a/tests/samples/scenarios.py +++ b/tests/samples/scenarios.py @@ -23,11 +23,14 @@ "clock", "run_id", ) +OPTIONAL_FIELDS = ("catalogue",) +CATALOGUES = ("legacy",) SCOPE_FIELDS = { "aws": ("accountId", "region"), "azure": ("tenantId", "subscriptionId", "resourceGroupName"), } -UPLOAD_FIELDS = ("status", "url", "assessment_id", "sync_id", "reason") +UPLOAD_FIELDS = ("status", "url", "assessment_id", "sync_id", "reason", "spooled") +UPLOAD_STATUSES = ("uploaded", "failed") MONTH = re.compile(r"^\d{4}-(0[1-9]|1[0-2])$") CURRENCY = re.compile(r"^[A-Z]{3}$") UUID7 = re.compile( @@ -54,6 +57,8 @@ class Scenario: upload: dict | None clock: datetime run_id: str + # "legacy": a catalogue without catalogue_meta + catalogue: str | None = None @property def csp(self): @@ -218,10 +223,22 @@ def _upload(path, upload): if not isinstance(upload, dict): _fail(path, "upload", "expected an object or null") _only(path, "upload", upload, UPLOAD_FIELDS) - _text(path, "upload.status", upload.get("status")) + status = _text(path, "upload.status", upload.get("status")) + if status not in UPLOAD_STATUSES: + _fail(path, "upload.status", f"expected one of {list(UPLOAD_STATUSES)}") + if "reason" in upload: + _text(path, "upload.reason", upload["reason"]) + if "spooled" in upload and not isinstance(upload["spooled"], bool): + _fail(path, "upload.spooled", "expected true or false") return upload +def _catalogue(path, value): + if value is not None and value not in CATALOGUES: + _fail(path, "catalogue", f"expected one of {list(CATALOGUES)}, got {value!r}") + return value + + def _clock(path, value): text = _text(path, "clock", value) if not text.endswith("Z"): @@ -243,7 +260,7 @@ def load_scenario(path): for field in FIELDS: if field not in data: _fail(path, field, "missing") - _only(path, "(file)", data, FIELDS) + _only(path, "(file)", data, FIELDS + OPTIONAL_FIELDS) provider = data["provider"] if provider not in PROVIDER_IDS: @@ -280,6 +297,7 @@ def load_scenario(path): upload=_upload(path, data["upload"]), clock=_clock(path, data["clock"]), run_id=run_id, + catalogue=_catalogue(path, data.get("catalogue")), ) diff --git a/tests/samples/test_samples.py b/tests/samples/test_samples.py index 8026ed3..26524b6 100644 --- a/tests/samples/test_samples.py +++ b/tests/samples/test_samples.py @@ -224,35 +224,54 @@ def test_the_egress_notices_carry_the_egress_paragraph(self): class UploadTests(RenderedTestCase): - def test_uploaded_cases_show_their_link_or_assessment_id(self): + def test_uploaded_cases_show_their_link_or_assessment_id_on_page_two_only(self): linked = STATE["scenarios"]["azure_uploaded"].upload["url"] for pdf in ("report.pdf", "egress.pdf"): pages = self.pdf_pages("azure_uploaded", pdf) self.assertIn(linked, squashed(pages[1])) self.assertIn("Uploaded to the platform", pages[1]) - self.assertIn( - "The Full Assessment", self.pdf_pages("azure_uploaded", "report.pdf")[-2] - ) - self.assertIn( - linked, squashed(self.pdf_pages("azure_uploaded", "egress.pdf")[-2]) + if pdf == "report.pdf": + self.assertEqual(sum(linked in squashed(text) for text in pages), 1) + self.assertNotIn( + "The Full Assessment", + " ".join(self.pdf_pages("azure_uploaded", "report.pdf")), ) pages = self.pdf_pages("uploaded_no_link", "report.pdf") self.assertIn("Uploaded to the platform", pages[1]) - self.assertIn("Assessment ID: sample-assessment-0001", pages[-2]) + self.assertIn("Assessment ID: sample-assessment-0001", pages[1]) + self.assertEqual(sum("sample-assessment-0001" in text for text in pages), 1) self.assertNotIn("https://platform", " ".join(pages)) - def test_offline_cases_show_the_self_collected_line(self): + def test_offline_cases_show_the_self_collected_line_and_no_closing_page(self): for case, scenario in self.cases(): if scenario.upload is None: with self.subTest(case): pages = self.pdf_pages(case, "report.pdf") self.assertIn("Self-collected data", pages[1]) - self.assertIn("What the Full Assessment Adds", " ".join(pages)) + self.assertNotIn("What the Full Assessment Adds", " ".join(pages)) + self.assertIn("Legal Notices", pages[-1][:200]) if scenario.egress is not None: egress = self.pdf_pages(case, "egress.pdf") self.assertIn("Self-collected data", egress[1]) + def test_a_failed_upload_says_what_happened_in_both_pdfs_and_the_html(self): + wording = ( + "Upload failed: The platform did not answer in time. " + "The run was kept for a later upload." + ) + for pdf in ("report.pdf", "egress.pdf"): + if (self.folder("upload_failed") / pdf).is_file(): + self.assertIn(wording, self.pdf_pages("upload_failed", pdf)[1]) + pages = self.pdf_pages("upload_failed", "report.pdf") + self.assertNotIn("Self-collected data", " ".join(pages)) + self.assertIn(wording, html_text(self.text("upload_failed", "index.html"))) + chip = re.search( + r' Date: Sun, 25 Oct 2026 14:22:32 +0100 Subject: [PATCH 56/61] harden error output, spool keys, report files and catalogue --- README.md | 10 +- cloudexit/catalogue/db.py | 2 + cloudexit/catalogue/download.py | 4 - cloudexit/catalogue/store.py | 62 +++--- cloudexit/cli/app.py | 248 +++++++++++++++++------ cloudexit/cli/prompts.py | 8 +- cloudexit/engine.py | 7 +- cloudexit/platform/client.py | 9 +- cloudexit/platform/document.py | 5 +- cloudexit/platform/run_output.py | 24 ++- cloudexit/platform/shape.py | 13 ++ cloudexit/platform/spool.py | 36 +++- cloudexit/platform/status.py | 75 ++++--- cloudexit/platform/upload.py | 3 +- cloudexit/private_files.py | 35 ++++ cloudexit/providers/azure/cli_session.py | 22 +- cloudexit/report/assets.py | 13 +- cloudexit/report/helpers.py | 9 +- cloudexit/resources.py | 41 +++- cloudexit/runconfig/loader.py | 25 ++- cloudexit/terminal.py | 28 ++- tests/catalogue/test_catalogue.py | 159 +++++++++++---- tests/cli/test_report_folder.py | 138 +++++++++++++ tests/cli/test_status.py | 87 +++++++- tests/cli/test_upload_modes.py | 61 ++++++ tests/platform/test_spool.py | 132 ++++++++++++ tests/report/test_copy_assets.py | 6 +- tests/report/test_report_flow.py | 94 ++++++++- tests/test_asset_paths.py | 140 +++++++++++++ tests/test_layering.py | 3 +- 30 files changed, 1291 insertions(+), 208 deletions(-) create mode 100644 cloudexit/platform/shape.py create mode 100644 cloudexit/private_files.py create mode 100644 tests/cli/test_report_folder.py create mode 100644 tests/test_asset_paths.py diff --git a/README.md b/README.md index 5d0f857..19b2f83 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ cloudexit catalogue import FILE.db.gz # install a file on an air-gapped mach cloudexit aws --tfstate infra.tfstate --catalogue FILE.db.gz # use a file for one run, without installing it ``` -Set `CLOUDEXIT_CATALOGUE_URL` to download from a mirror instead of the default location. A `./datasets/data.db` from an earlier version is copied to the data directory once. +Set `CLOUDEXIT_CATALOGUE_URL` to download from a mirror instead of the default location. A `./datasets/data.db` from an earlier version is not picked up on its own; install it on purpose with `cloudexit catalogue import datasets/data.db`. A catalogue that holds a trigger or a view is refused, wherever it comes from. ## Sync document @@ -209,7 +209,7 @@ The local report is always the Basic report, computed on your machine, whether o | --- | --- | | 10 | The platform rejected the data, or it is beyond a limit the platform announces. Retrying the same data will not help. | | 11 | The API key is missing, mistyped, expired, revoked, not allowed to do this, or belongs to another region. | -| 12 | The platform is unreachable, busy or in maintenance. Running again is safe: the run keeps the same sync ID. | +| 12 | The platform is unreachable, busy or in maintenance. A spooled sync is delivered on the next run, and that run then makes a new assessment of its own: see [Retries and the spool](#retries-and-the-spool). | | 13 | Credits or a quota ran out. | | 14 | The assessment named by `--assessment-id` is missing, deleted, archived, or does not match the data. | | 15 | The platform does not accept this cloudexit version or the sync document version. | @@ -220,7 +220,7 @@ Before anything is sent the key is checked on your machine (prefix, length and c A failed upload is retried when it may pass: connection errors, timeouts, HTTP 408, 425, 429, 500, 502, 503 and 504, and any error the platform marks as retryable. The wait doubles from 1 second up to 60 seconds with random jitter, `Retry-After` is honoured, and every attempt carries the same idempotency key (the sync ID), so a repeat never stores or charges a second time. All attempts together stay within `--upload-budget` seconds (default 300, `CLOUDEXIT_UPLOAD_BUDGET`). Final failures (exit codes 10, 11, 13, 14 and 15) are not retried. -When the budget runs out on retryable failures the sync is saved to a local spool and the run exits 12 ("spooled; run `cloudexit spool flush`"). `--spool-on-error` also spools syncs the platform refused, for a later manual retry. The spool is `$XDG_STATE_HOME/cloudexit/spool//` (`~/.local/state/...`, or `CLOUDEXIT_SPOOL_DIR`): the sync document and a small file with the attempts and the last error, readable by you only, never holding the API key. The host id is a hash, so the host name is not in the path. +When the budget runs out on retryable failures the sync is saved to a local spool and the run exits 12 ("spooled; run `cloudexit spool flush`"). `--spool-on-error` also spools syncs the platform refused, for a later manual retry. The spool is `$XDG_STATE_HOME/cloudexit/spool//` (`~/.local/state/...`, or `CLOUDEXIT_SPOOL_DIR`): the sync document and a small file with the attempts and the last error, readable by you only, never holding the API key. The host id is a hash, so the host name is not in the path. Each spooled sync also records a fingerprint of the API key in use when it was spooled (a hash, never the key). ```bash cloudexit spool list # id, host id, age, attempts, last error @@ -229,7 +229,9 @@ cloudexit spool purge [--older-than 30d] [--yes] cloudexit upload raw_data/sync.json # upload sync documents (or folders of them), for example from an air-gapped machine ``` -Every run with a platform first delivers the spooled syncs of the same host (oldest first, within 60 seconds); `--no-flush` skips that, and a failing flush never blocks the new run. A spooled sync older than the platform's `max_collection_age_days` is reported as "will be rejected" and kept until you purge it. For an account key, `upload` needs `--assessment-id`, or `--name` and `--exit-strategy`, to say where the sync goes. +Every run with a platform first delivers the spooled syncs of the same host and the same API key (oldest first, within 60 seconds); `--no-flush` skips that, and a failing flush never blocks the new run. Syncs spooled with another API key are never delivered with this one: they stay in the spool, the run says so, and `cloudexit spool list` marks them (`cloudexit spool purge` removes them). A spooled sync with no recorded key is treated the same way. + +Running again after exit 12 does two things: it delivers the spooled sync under its own sync ID, which the platform stores and charges once however often it is sent, and it then makes a new assessment, which is a new sync with a new sync ID and is charged like any other. Use `--no-flush` to leave the spool alone, or `cloudexit spool flush` to deliver it without a new assessment. A spooled sync older than the platform's `max_collection_age_days` is reported as "will be rejected" and kept until you purge it. For an account key, `upload` needs `--assessment-id`, or `--name` and `--exit-strategy`, to say where the sync goes. Connections always verify the platform's certificate. For a private certificate authority use `--ca-bundle` or `CLOUDEXIT_CA_BUNDLE`; otherwise `REQUESTS_CA_BUNDLE` and `SSL_CERT_FILE` are honoured. There is no option to turn verification off. `HTTPS_PROXY` and `NO_PROXY` are honoured. Requests give up after 10 seconds to connect and 60 seconds to read (`--timeout` sets both). The platform's capabilities are cached for five minutes in `~/.cache/cloudexit/` (`$XDG_CACHE_HOME/cloudexit/`, or `%LOCALAPPDATA%\cloudexit\cache\` on Windows). diff --git a/cloudexit/catalogue/db.py b/cloudexit/catalogue/db.py index 550df27..4ce4ceb 100644 --- a/cloudexit/catalogue/db.py +++ b/cloudexit/catalogue/db.py @@ -39,6 +39,8 @@ def connect(db_path=None): db_path = db_path or active_catalogue_path() try: conn = sqlite3.connect(db_path, factory=_Connection) + # Nothing the file's own SQL defines is trusted to run + conn.execute("PRAGMA trusted_schema=OFF") return conn except sqlite3.Error as e: logger.debug(f"Error connecting to database: {e}") diff --git a/cloudexit/catalogue/download.py b/cloudexit/catalogue/download.py index b75609d..9238023 100644 --- a/cloudexit/catalogue/download.py +++ b/cloudexit/catalogue/download.py @@ -18,7 +18,6 @@ check_checksum, install_dataset, installed_catalogue, - migrate_legacy_dataset, ) logger = logging.getLogger(__name__) @@ -203,7 +202,6 @@ def ensure_catalogue( base_url: str, *, directory: Path | None = None, verify: str | bool = True ) -> EnsureResult: directory = directory or catalogue_dir() - migrated = migrate_legacy_dataset(directory) try: result = update_catalogue(base_url, directory=directory, verify=verify) except (CatalogueError, OSError) as exc: @@ -218,7 +216,6 @@ def ensure_catalogue( installed.version, installed.date, str(exc), - migrated, ) installed = installed_catalogue(directory) @@ -228,7 +225,6 @@ def ensure_catalogue( installed.version, installed.date, None, - migrated, ) diff --git a/cloudexit/catalogue/store.py b/cloudexit/catalogue/store.py index b9712eb..b259144 100644 --- a/cloudexit/catalogue/store.py +++ b/cloudexit/catalogue/store.py @@ -21,7 +21,6 @@ CATALOGUE_BASE_URL = "https://cloudexit-oss-data-eu.fsn1.your-objectstorage.com" SUPPORTED_SCHEMA = 1 REQUIRED_TABLES = ("resourcetype", "risk") -LEGACY_DATABASE = Path("datasets") / "data.db" PROVIDER_IDS = {"azure": 1, "aws": 2} GZIP_MAGIC = b"\x1f\x8b" # A dataset unpacks to a few times its download size. The limit is that ratio @@ -84,7 +83,6 @@ class EnsureResult: version: str | None date: str reason: str | None = None - migrated: bool = False def catalogue_dir( @@ -126,7 +124,10 @@ def catalogue_base_url(settings: Settings) -> str: def _open_readonly(path: Path) -> sqlite3.Connection: - return sqlite3.connect(f"{Path(path).resolve().as_uri()}?mode=ro", uri=True) + conn = sqlite3.connect(f"{Path(path).resolve().as_uri()}?mode=ro", uri=True) + # Nothing the file's own SQL defines is trusted to run + conn.execute("PRAGMA trusted_schema=OFF") + return conn def _table_names(conn: sqlite3.Connection) -> set[str]: @@ -134,6 +135,34 @@ def _table_names(conn: sqlite3.Connection) -> set[str]: return {row[0] for row in rows} +def _check_schema_objects(conn: sqlite3.Connection) -> None: + # A catalogue holds tables and indexes. A trigger or a view runs SQL of its + # own on the data a run adds to the same database, so none is accepted. + found = conn.execute( + "SELECT type, name FROM sqlite_master " + "WHERE type NOT IN ('table', 'index') ORDER BY type, name" + ).fetchall() + if found: + names = ", ".join(f"{kind} {name}" for kind, name in found[:3]) + more = f" and {len(found) - 3} more" if len(found) > 3 else "" + raise CatalogueError( + f"the catalogue holds {names}{more}, which a catalogue does not have; " + "it was not used" + ) + + +def validate_schema(path: str | Path) -> None: + # For a database that is about to be written to, or that has been + try: + conn = _open_readonly(Path(path)) + try: + _check_schema_objects(conn) + finally: + conn.close() + except sqlite3.DatabaseError as exc: + raise CatalogueError(f"not a valid catalogue database: {exc}") from exc + + def _read_meta(conn: sqlite3.Connection, tables: set[str]) -> CatalogueInfo | None: if "catalogue_meta" not in tables: return None @@ -215,6 +244,7 @@ def validate_database( raise CatalogueError( f"not a catalogue database: missing table(s) {', '.join(missing)}" ) + _check_schema_objects(conn) info = _read_meta(conn, tables) finally: conn.close() @@ -257,32 +287,6 @@ def installed_catalogue(directory: Path | None = None) -> Installed | None: return Installed(path, info, read_state(directory)) -def migrate_legacy_dataset( - directory: Path | None = None, legacy: Path | None = None -) -> bool: - directory = directory or catalogue_dir() - legacy = legacy or LEGACY_DATABASE - if installed_catalogue(directory) or not legacy.is_file(): - return False - try: - info = validate_database(legacy) - except CatalogueError: - return False - - directory.mkdir(parents=True, exist_ok=True) - shutil.copy2(legacy, directory / "data.db") - modified = datetime.fromtimestamp(legacy.stat().st_mtime, timezone.utc) - _write_state( - directory, - { - "version": info.version if info else None, - "sha256": None, - "downloaded_at": modified.isoformat(timespec="seconds"), - }, - ) - return True - - def _is_gzip(data: bytes) -> bool: return data[:2] == GZIP_MAGIC diff --git a/cloudexit/cli/app.py b/cloudexit/cli/app.py index 1c44cbf..46c2bd7 100644 --- a/cloudexit/cli/app.py +++ b/cloudexit/cli/app.py @@ -3,6 +3,7 @@ import contextlib import json import boto3 +import tempfile import time import sys import os @@ -11,7 +12,6 @@ from typing import Any from rich.console import Console from rich.logging import RichHandler -from rich.markup import escape from datetime import datetime from botocore.exceptions import NoCredentialsError, ProfileNotFound @@ -30,8 +30,12 @@ ) from cloudexit.egress.estimate import estimate_egress from cloudexit.platform.client import ApiError, check_key -from cloudexit.platform.run_output import build_run_json, exit_code_of -from cloudexit.platform.status import host_catalogue_lines, run_status +from cloudexit.platform.run_output import ( + build_run_json, + exit_code_of, + run_json_fallback, +) +from cloudexit.platform.status import StatusReport, host_catalogue_lines, run_status from cloudexit.platform.spool import ( AUTO_FLUSH_BUDGET, DEFAULT_MAX_AGE_DAYS, @@ -39,6 +43,7 @@ flush, format_age, host_id, + key_fingerprint, parse_age, ) from cloudexit.platform.upload import ( @@ -93,6 +98,7 @@ prompt_required_inputs, print_help_message, ) +from cloudexit.private_files import restrict_tree from cloudexit.terminal import ask, print_step from cloudexit.runconfig.loader import ( build_config, @@ -213,8 +219,11 @@ def _aws_provider_from_profile(profile: str) -> dict: raise ConfigError if not is_aws_profile_valid(profile): console.print( - f"[red]AWS profile '{profile}' is not configured. " - f"Use `aws configure --profile {profile}`.[/red]" + f"AWS profile '{profile}' is not configured. " + f"Use `aws configure --profile {profile}`.", + style="red", + markup=False, + emoji=False, ) raise ConfigError try: @@ -222,14 +231,20 @@ def _aws_provider_from_profile(profile: str) -> dict: credentials = session.get_credentials() if credentials is None: console.print( - f"[red]AWS profile '{profile}' has no valid credentials. " - f"Use `aws configure --profile {profile}`.[/red]" + f"AWS profile '{profile}' has no valid credentials. " + f"Use `aws configure --profile {profile}`.", + style="red", + markup=False, + emoji=False, ) raise ConfigError region = session.region_name or "us-east-1" except (NoCredentialsError, ProfileNotFound) as e: console.print( - f"[red]AWS profile error: {str(e)}. Use `aws configure` to set up a profile.[/red]" + f"AWS profile error: {str(e)}. Use `aws configure` to set up a profile.", + style="red", + markup=False, + emoji=False, ) raise ConfigError @@ -251,7 +266,9 @@ def _aws_provider_from_env() -> dict: try: validate_region(region) except ValueError as e: - console.print(f"[red]AWS_DEFAULT_REGION: {e}[/red]") + console.print( + f"AWS_DEFAULT_REGION: {e}", style="red", markup=False, emoji=False + ) raise ConfigError provider_details = { @@ -276,9 +293,19 @@ def _aws_provider_from_prompt() -> dict: validate_region(region) break except ValueError as e: - console.print(f"[red]{e} Please enter a valid AWS region.[/red]") + console.print( + f"{e} Please enter a valid AWS region.", + style="red", + markup=False, + emoji=False, + ) except Exception as e: - console.print(f"[red]Error during manual AWS configuration: {e}[/red]") + console.print( + f"Error during manual AWS configuration: {e}", + style="red", + markup=False, + emoji=False, + ) raise ConfigError return { @@ -368,7 +395,7 @@ def _require_azure_sdk() -> None: try: require_azure_sdk() except AzureSdkError as e: - console.print(f"[red]{e}[/red]") + console.print(f"{e}", style="red", markup=False, emoji=False) sys.exit(codes.CONFIG) @@ -471,7 +498,7 @@ def _azure_provider_from_cli() -> dict: raise except Exception as e: logger.error(f"Error during Azure CLI processing: {e}", exc_info=True) - console.print(f"[red]An error occurred: {e}[/red]") + console.print(f"An error occurred: {e}", style="red", markup=False, emoji=False) raise ConfigError return { @@ -517,7 +544,7 @@ def _azure_provider_from_prompt() -> dict: raise except Exception as e: logger.error(f"Error during manual Azure configuration: {e}", exc_info=True) - console.print(f"[red]An error occurred: {e}[/red]") + console.print(f"An error occurred: {e}", style="red", markup=False, emoji=False) raise ConfigError return { @@ -607,16 +634,15 @@ def handle_azure(args): def create_directory(base_path="reports"): - # Generate the main directory with a timestamp + # The timestamp keeps the name readable; the random suffix and the exclusive + # creation mean two runs started in the same second never share a folder. + # The folder is for its owner only. timestamp = datetime.now().strftime("%Y%m%d%H%M%S") - directory_path = os.path.join(base_path, timestamp) - - # Create the main directory - os.makedirs(directory_path, exist_ok=True) + os.makedirs(base_path, mode=0o700, exist_ok=True) + directory_path = tempfile.mkdtemp(prefix=f"{timestamp}-", dir=base_path) - # Create the raw_data subdirectory within the main directory raw_data_path = os.path.join(directory_path, "raw_data") - os.makedirs(raw_data_path, exist_ok=True) + os.mkdir(raw_data_path, 0o700) return directory_path, raw_data_path @@ -670,6 +696,14 @@ def _report_flush(result): status="warning", logs=", ".join(sorted(set(result.failed.values()))), ) + if result.other_key: + print_step( + f"{_count(len(result.other_key), 'spooled sync')} for another API key " + "left in the spool.", + status="warning", + logs="not delivered with this key; run again with that key, " + "or `cloudexit spool purge` removes them", + ) if result.pending: print_step( f"{_count(len(result.pending), 'spooled sync')} still waiting.", @@ -845,7 +879,7 @@ def run_assessment( print_step(f"Connecting to {provider_name}...", status="ok") else: print_step(f"Connecting to {provider_name}...", status="error") - console.print(f" ↳ {logs}", style="dim") + console.print(f" ↳ {logs}", style="dim", markup=False, emoji=False) logger.error(f"Credential verification failed: {logs}") sys.exit(codes.CREDENTIALS) console.print("-------------------------------------------") @@ -1062,7 +1096,7 @@ def run_assessment( logs=str(e), ) for problem in getattr(e, "problems", [])[:10]: - console.print(f" ↳ {problem}", style="dim") + console.print(f" ↳ {problem}", style="dim", markup=False, emoji=False) sync_failed = wanted RUN["sync_json"] = str(sync_output.path) if sync_output else None @@ -1103,7 +1137,10 @@ def run_assessment( ) if upload_result.request_id: console.print( - f" ↳ request id: {upload_result.request_id}", style="dim" + f" ↳ request id: {upload_result.request_id}", + style="dim", + markup=False, + emoji=False, ) if upload_result.spooled: print_step( @@ -1181,25 +1218,57 @@ def run_assessment( else "Uploaded to the platform." ), style="cyan", + markup=False, + emoji=False, ) charge = billing_line(upload_result.billing) if charge: console.print(charge, style="cyan") if sync_output: - console.print(f"Sync document: {sync_output.path}", style="cyan") + console.print( + f"Sync document: {sync_output.path}", + style="cyan", + markup=False, + emoji=False, + ) if sync_output.emitted: - console.print(f"Emitted copy: {sync_output.emitted}", style="cyan") + console.print( + f"Emitted copy: {sync_output.emitted}", + style="cyan", + markup=False, + emoji=False, + ) html_report_path = report_status.get("reports", {}).get("HTML") if html_report_path: - console.print(f"HTML Report: {html_report_path}", style="cyan") + console.print( + f"HTML Report: {html_report_path}", + style="cyan", + markup=False, + emoji=False, + ) pdf_report_path = report_status.get("reports", {}).get("PDF") if pdf_report_path: - console.print(f"PDF Report: {pdf_report_path}", style="cyan") + console.print( + f"PDF Report: {pdf_report_path}", + style="cyan", + markup=False, + emoji=False, + ) json_report_path = report_status.get("reports", {}).get("JSON") if json_report_path: - console.print(f"JSON Report: {json_report_path}", style="cyan") + console.print( + f"JSON Report: {json_report_path}", + style="cyan", + markup=False, + emoji=False, + ) if egress_pdf_path: - console.print(f"Egress PDF: {egress_pdf_path}", style="cyan") + console.print( + f"Egress PDF: {egress_pdf_path}", + style="cyan", + markup=False, + emoji=False, + ) console.print("-------------------------------------------") try: @@ -1228,7 +1297,7 @@ def run_assessment( sys.exit(codes.UPLOAD_REJECTED) except Exception as e: - console.print(f"[red]Unexpected error: {e}[/red]") + console.print(f"Unexpected error: {e}", style="red", markup=False, emoji=False) # Persist the full traceback so a one-line error is actionable. Falls # back to the cwd when the crash happened before the report dir existed. target_dir = report_path or os.getcwd() @@ -1236,7 +1305,12 @@ def run_assessment( log_file = os.path.join(target_dir, f"error-{int(time.time())}.log") with open(log_file, "w", encoding="utf-8") as fh: fh.write(traceback.format_exc()) - console.print(f"[yellow]Full traceback written to: {log_file}[/yellow]") + console.print( + f"Full traceback written to: {log_file}", + style="yellow", + markup=False, + emoji=False, + ) except OSError: console.print( "[yellow]Could not write a traceback file; see run.log.[/yellow]" @@ -1246,6 +1320,9 @@ def run_assessment( sys.exit(codes.UNEXPECTED) finally: close_run_log_handler() + if report_path: + # Everything the run wrote, however it was written, is for its owner + restrict_tree(report_path) def _catalogue_settings(args=None): @@ -1257,7 +1334,7 @@ def _catalogue_settings(args=None): settings, ) except SettingsError as e: - console.print(f"[red]{escape(str(e))}[/red]") + console.print(f"{str(e)}", style="red", markup=False, emoji=False) sys.exit(codes.CONFIG) @@ -1268,7 +1345,7 @@ def prepare_catalogue(args): try: path, info = use_catalogue_file(args.catalogue) except CatalogueError as e: - console.print(f"[red]{escape(str(e))}[/red]") + console.print(f"{str(e)}", style="red", markup=False, emoji=False) sys.exit(codes.CONFIG) set_active_catalogue(path) print_step( @@ -1284,15 +1361,13 @@ def prepare_catalogue(args): base_url, verify=(settings.ca_bundle.value if settings else None) or True ) except SettingsError as e: - console.print(f"[red]{escape(str(e))}[/red]") + console.print(f"{str(e)}", style="red", markup=False, emoji=False) sys.exit(codes.CONFIG) except CatalogueUnavailable as e: - console.print(f"[red]{escape(str(e))}[/red]") + console.print(f"{str(e)}", style="red", markup=False, emoji=False) sys.exit(codes.CONFIG) version = result.version or "unknown" - if result.migrated: - print_step("Catalogue copied from ./datasets to the data directory.", "ok") if result.status == "failed": print_step( f"Catalogue update failed, using {version} from {result.date}.", @@ -1327,7 +1402,7 @@ def handle_catalogue(args): info = import_catalogue(args.file, sha256=args.sha256) print(f"Catalogue {info.version if info else 'unknown'} installed.") except (CatalogueError, OSError) as e: - console.print(f"[red]{escape(str(e))}[/red]") + console.print(f"{str(e)}", style="red", markup=False, emoji=False) sys.exit(codes.CONFIG) @@ -1335,7 +1410,7 @@ def _settings_or_exit(args): try: return load_settings(flags_from_args(args)) except SettingsError as e: - console.print(f"[red]{escape(str(e))}[/red]") + console.print(f"{str(e)}", style="red", markup=False, emoji=False) sys.exit(codes.CONFIG) @@ -1354,19 +1429,21 @@ def handle_upload(args): try: check_key(settings.api_key.value) except ApiError as e: - console.print(f"[red]{escape(e.message)}[/red]") + console.print(f"{e.message}", style="red", markup=False, emoji=False) sys.exit(e.exit_code) files, problems = read_sync_files(args.paths) exit_code = codes.SUCCESS for path, message in problems: - console.print(f"[red]{escape(str(path))}: {escape(message)}[/red]") + console.print(f"{str(path)}: {message}", style="red", markup=False, emoji=False) exit_code = exit_code or codes.UPLOAD_REJECTED for path, document in files: try: validate_document(document) except SyncDocumentError as e: - console.print(f"[red]{escape(str(path))}: {escape(str(e))}[/red]") + console.print( + f"{str(path)}: {str(e)}", style="red", markup=False, emoji=False + ) exit_code = exit_code or codes.UPLOAD_REJECTED continue result = upload( @@ -1376,26 +1453,38 @@ def handle_upload(args): meta={"exit_strategy": args.exit_strategy}, spool_on_error=args.spool_on_error, ) - shown = escape(str(path)) + shown = str(path) if result.success: console.print( f"{shown}: {result.message}" + (f" ({result.url})" if result.url else ""), style="cyan", + markup=False, + emoji=False, ) charge = billing_line(result.billing) if charge: - console.print(f" {charge}", style="cyan") + console.print(f" {charge}", style="cyan", markup=False, emoji=False) for warning in result.warnings: - console.print(f" warning: {escape(warning)}", style="dim") + console.print( + f" warning: {warning}", style="dim", markup=False, emoji=False + ) continue exit_code = exit_code or result.exit_code console.print( - f"[red]{shown}: {escape(result.error_code or result.reason or '')}: " - f"{escape(result.message)}[/red]" + f"{shown}: {result.error_code or result.reason or ''}: " + f"{result.message}", + style="red", + markup=False, + emoji=False, ) if result.request_id: - console.print(f" request id: {result.request_id}", style="dim") + console.print( + f" request id: {result.request_id}", + style="dim", + markup=False, + emoji=False, + ) if result.spooled: console.print(" spooled; run `cloudexit spool flush`", style="dim") if exit_code: @@ -1410,7 +1499,7 @@ def handle_spool(args): try: only = host_id(flag_host) if flag_host else None except SettingsError as e: - console.print(f"[red]{escape(str(e))}[/red]") + console.print(f"{str(e)}", style="red", markup=False, emoji=False) sys.exit(codes.CONFIG) if command == "list": @@ -1420,6 +1509,7 @@ def handle_spool(args): return now = time.time() print(f"{'SYNC ID':<38} {'HOST':<17} {'AGE':<8} {'ATTEMPTS':<9} LAST ERROR") + key_id = key_fingerprint(settings.api_key.value) for entry in entries: print( f"{entry.sync_id:<38} {entry.host_id:<17} " @@ -1431,6 +1521,7 @@ def handle_spool(args): if now - entry.created > DEFAULT_MAX_AGE_DAYS * 86400 else "" ) + + ("" if entry.belongs_to(key_id) else " not for the key in use") ) return @@ -1440,10 +1531,10 @@ def handle_spool(args): check_key(settings.api_key.value) result = flush(settings, spool, host=flag_host) except ApiError as e: - console.print(f"[red]{escape(e.message)}[/red]") + console.print(f"{e.message}", style="red", markup=False, emoji=False) sys.exit(e.exit_code) except SettingsError as e: - console.print(f"[red]{escape(str(e))}[/red]") + console.print(f"{str(e)}", style="red", markup=False, emoji=False) sys.exit(codes.CONFIG) if result.empty: print("Nothing to deliver for this host.") @@ -1456,7 +1547,7 @@ def handle_spool(args): try: older_than = parse_age(args.older_than) if args.older_than else None except ValueError as e: - console.print(f"[red]{escape(str(e))}[/red]") + console.print(f"{str(e)}", style="red", markup=False, emoji=False) sys.exit(codes.CONFIG) now = time.time() due = [ @@ -1485,7 +1576,13 @@ def handle_spool(args): def handle_status(args): settings = _settings_or_exit(args) - report = run_status(settings) + try: + report = run_status(settings) + except Exception as e: + # Whatever the platform sent, the answer is still a report + logger.debug("The status check failed", exc_info=True) + report = StatusReport() + report.problem(codes.UNEXPECTED, f"the status check failed: {type(e).__name__}") if args.output == "json": print(json.dumps(report.as_json(), indent=2)) else: @@ -1503,7 +1600,7 @@ def handle_config(args): try: settings = load_settings(flags_from_args(args)) except SettingsError as e: - console.print(f"[red]{escape(str(e))}[/red]") + console.print(f"{str(e)}", style="red", markup=False, emoji=False) sys.exit(codes.CONFIG) for line in describe(settings): print(line) @@ -1543,7 +1640,7 @@ def _dispatch(args): try: args.settings = load_settings(flags_from_args(args)) except SettingsError as e: - console.print(f"[red]{escape(str(e))}[/red]") + console.print(f"{str(e)}", style="red", markup=False, emoji=False) sys.exit(codes.CONFIG) # Make sure a catalogue is available before any work starts @@ -1577,15 +1674,27 @@ def _absolute(path): def _emit_run_json(stream, exit_code): # Absolute, so a script can use them from any directory - summary = build_run_json( - exit_code=exit_code, - report_dir=_absolute(RUN.get("report_dir")), - sync_json=_absolute(RUN.get("sync_json")), - upload=RUN.get("upload"), - skipped_reason=RUN.get("skipped_reason"), - catalogue_version=RUN.get("catalogue_version"), - ) - stream.write(json.dumps(summary, indent=2) + "\n") + try: + summary = build_run_json( + exit_code=exit_code, + report_dir=_absolute(RUN.get("report_dir")), + sync_json=_absolute(RUN.get("sync_json")), + upload=RUN.get("upload"), + skipped_reason=RUN.get("skipped_reason"), + catalogue_version=RUN.get("catalogue_version"), + ) + text = json.dumps(summary, indent=2, allow_nan=False) + except Exception as e: + # One object on stdout whatever happened, so a script can always read it + logger.debug("The run summary could not be built", exc_info=True) + summary = run_json_fallback( + exit_code=exit_code, + report_dir=_absolute(RUN.get("report_dir")), + sync_json=_absolute(RUN.get("sync_json")), + error=f"the run summary could not be built: {type(e).__name__}", + ) + text = json.dumps(summary, indent=2) + stream.write(text + "\n") stream.flush() @@ -1603,7 +1712,12 @@ def _dispatch_json(args): exit_code = exit_code_of(e.code) raise except Exception as e: - console.print(f"[red]Unexpected error: {escape(str(e))}[/red]") + console.print( + f"Unexpected error: {str(e)}", + style="red", + markup=False, + emoji=False, + ) sys.exit(codes.UNEXPECTED) finally: _emit_run_json(stdout, exit_code) @@ -1645,5 +1759,5 @@ def run(): main() except Exception as e: # logger.error(f"An unexpected error occurred: {e}", exc_info=True) - console.print(f"[red]Unexpected error: {e}[/red]") + console.print(f"Unexpected error: {e}", style="red", markup=False, emoji=False) sys.exit(1) diff --git a/cloudexit/cli/prompts.py b/cloudexit/cli/prompts.py index 26c11e7..0aa04c1 100644 --- a/cloudexit/cli/prompts.py +++ b/cloudexit/cli/prompts.py @@ -20,7 +20,9 @@ def prompt_required_inputs() -> tuple[int, int]: break except ValueError as e: logger.debug(f"Invalid exit strategy input: {e}") - console.print(f"[red]{e} Please enter 1 or 3.[/red]") + console.print( + f"{e} Please enter 1 or 3.", style="red", markup=False, emoji=False + ) while True: try: @@ -33,7 +35,9 @@ def prompt_required_inputs() -> tuple[int, int]: break except ValueError as e: logger.debug(f"Invalid assessment type input: {e}") - console.print(f"[red]{e} Please enter 1 or 2.[/red]") + console.print( + f"{e} Please enter 1 or 2.", style="red", markup=False, emoji=False + ) return exit_strategy, assessment_type diff --git a/cloudexit/engine.py b/cloudexit/engine.py index 15a4390..0871ece 100644 --- a/cloudexit/engine.py +++ b/cloudexit/engine.py @@ -21,6 +21,7 @@ from cloudexit.providers.azure.inventory import build_azure_resource_inventory from cloudexit.providers.azure.sdk import azure_import from cloudexit.catalogue.db import connect, load_data +from cloudexit.catalogue.store import CatalogueError from cloudexit.tfstate import build_tfstate_resource_inventory from cloudexit.report.context import ( EGRESS_DOCUMENT_CODE, @@ -268,7 +269,11 @@ def create_resource_inventory( raw_data_path: str, ) -> dict[str, Any]: # Copy assets and datasets folders data - copy_assets(report_path, cloud_service_provider) + try: + copy_assets(report_path, cloud_service_provider) + except CatalogueError as e: + logger.error(f"The catalogue cannot be used: {str(e)}") + return {"success": False, "logs": str(e)} try: diff --git a/cloudexit/platform/client.py b/cloudexit/platform/client.py index 1cb21d1..81f64be 100644 --- a/cloudexit/platform/client.py +++ b/cloudexit/platform/client.py @@ -29,6 +29,7 @@ ) from cloudexit.version import __version__ +from cloudexit.platform.shape import as_dict, as_list from cloudexit.platform.document import ( MAX_BODY_BYTES, MAX_CATALOGUE_TYPE_IDS, @@ -341,7 +342,7 @@ def _one_line(text: Any) -> str: def client_policy(meta: Mapping[str, Any]) -> tuple[str, str]: # What the platform says about this version of cloudexit: accepted, # deprecated or unsupported, and why. - policy = (meta.get("clients") or {}).get(CLIENT_NAME) + policy = as_dict(meta.get("clients")).get(CLIENT_NAME) current = _version(__version__) if not isinstance(policy, dict) or not current: return "accepted", f"cloudexit {__version__} is accepted" @@ -349,7 +350,7 @@ def client_policy(meta: Mapping[str, Any]) -> tuple[str, str]: f"; upgrade to {policy['recommended']}" if policy.get("recommended") else "" ) minimum = _version(policy.get("min_supported")) - blocked = [_version(v) for v in policy.get("blocked") or []] + blocked = [_version(v) for v in as_list(policy.get("blocked"))] if current in blocked or (minimum and current < minimum): return ( "unsupported", @@ -769,7 +770,7 @@ def whoami(self) -> dict[str, Any]: def _check_meta( self, meta: Mapping[str, Any], document: Mapping[str, Any] ) -> list[dict[str, str | None]]: - versions = (meta.get("contract") or {}).get("schema_versions") + versions = as_dict(meta.get("contract")).get("schema_versions") schema_version = document.get("schema_version") if isinstance(versions, list) and schema_version not in versions: raise ApiError( @@ -889,7 +890,7 @@ def _push( "utf-8" ) problems = limit_problems( - outgoing, meta.get("limits") or {}, size=len(body), now=self._clock() + outgoing, as_dict(meta.get("limits")), size=len(body), now=self._clock() ) if problems: raise ApiError( diff --git a/cloudexit/platform/document.py b/cloudexit/platform/document.py index a57a34c..cb9fa50 100644 --- a/cloudexit/platform/document.py +++ b/cloudexit/platform/document.py @@ -23,6 +23,7 @@ catalogue_dir, catalogue_info, ) +from cloudexit.private_files import write_private from cloudexit.version import __version__ from cloudexit.catalogue.db import load_data @@ -770,13 +771,13 @@ def write_sync_document( digest = hashlib.sha256(data).hexdigest() path = Path(raw_data_path) / "sync.json" - path.write_bytes(data) + write_private(path, data) emitted = None if emit_dir: emitted = Path(emit_dir) / f"{document['sync']['id']}.json" emitted.parent.mkdir(parents=True, exist_ok=True) - emitted.write_bytes(data) + write_private(emitted, data) summary = summary_line(document, len(data), digest) logger.info(summary) diff --git a/cloudexit/platform/run_output.py b/cloudexit/platform/run_output.py index 6b0caae..72a1097 100644 --- a/cloudexit/platform/run_output.py +++ b/cloudexit/platform/run_output.py @@ -5,6 +5,7 @@ from cloudexit import codes from cloudexit.catalogue.store import compare_catalogue_versions +from cloudexit.platform.shape import as_dict, as_text from cloudexit.platform.upload import UploadResult @@ -23,10 +24,10 @@ def catalogue_outdated( # used; None when nothing tells. if result is None or not result.success: return None - codes_seen = {item.get("code") for item in result.warning_items} + codes_seen = {as_dict(item).get("code") for item in result.warning_items} if "collector_catalogue_outdated" in codes_seen: return True - host = ((result.catalogue or {}).get("host") or {}).get("version") + host = as_text(as_dict(as_dict(result.catalogue).get("host")).get("version")) standing = compare_catalogue_versions(local_version, host) if standing is None: return None @@ -81,6 +82,25 @@ def build_run_json( } +def run_json_fallback( + *, + exit_code: int, + report_dir: str | None, + sync_json: str | None, + error: str, +) -> dict[str, Any]: + # The same keys as the full summary, with what is known and the reason + return { + "exit_code": int(exit_code), + "report_dir": report_dir, + "sync_json": sync_json, + "upload": None, + "results": None, + "catalogue": {"version": None, "outdated": None}, + "error": error, + } + + def exit_code_of(value: Any) -> int: if value is None: return codes.SUCCESS diff --git a/cloudexit/platform/shape.py b/cloudexit/platform/shape.py new file mode 100644 index 0000000..26d218c --- /dev/null +++ b/cloudexit/platform/shape.py @@ -0,0 +1,13 @@ +from typing import Any + + +def as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + +def as_list(value: Any) -> list[Any]: + return value if isinstance(value, list) else [] + + +def as_text(value: Any) -> str | None: + return value if isinstance(value, str) and value else None diff --git a/cloudexit/platform/spool.py b/cloudexit/platform/spool.py index e582090..ebdc385 100644 --- a/cloudexit/platform/spool.py +++ b/cloudexit/platform/spool.py @@ -44,6 +44,15 @@ def host_id(host: str) -> str: return hashlib.sha256(f"{parts.scheme}://{netloc}".encode("utf-8")).hexdigest()[:16] +def key_fingerprint(key: str | None) -> str | None: + # Which API key spooled a sync, without keeping the key: a hash, never the key + if not key: + return None + return hashlib.sha256(b"cloudexit spool key\0" + key.encode("utf-8")).hexdigest()[ + :16 + ] + + def state_dir( environ: Mapping[str, str] | None = None, *, platform: str = sys.platform ) -> Path: @@ -90,6 +99,15 @@ class SpoolEntry: def created(self) -> float: return float(self.meta.get("created") or 0) + @property + def key_id(self) -> str | None: + value = self.meta.get("key_id") + return value if isinstance(value, str) and value else None + + def belongs_to(self, key_id: str | None) -> bool: + # An entry without a recorded key cannot be matched to any key + return key_id is not None and self.key_id == key_id + @dataclass class FlushResult: @@ -98,10 +116,18 @@ class FlushResult: rejected: list[str] = field(default_factory=list) failed: dict[str, str] = field(default_factory=dict) stopped: str | None = None + # Spooled with another API key: left where they are + other_key: list[str] = field(default_factory=list) @property def empty(self) -> bool: - return not (self.uploaded or self.pending or self.rejected or self.failed) + return not ( + self.uploaded + or self.pending + or self.rejected + or self.failed + or self.other_key + ) class Spool: @@ -140,6 +166,7 @@ def add( name: str | None, exit_strategy: int | None, assessment_id: str | None, + key_id: str | None = None, ) -> SpoolEntry: sync_id = ((document.get("sync") or {}).get("id")) or "" if not isinstance(sync_id, str) or not SYNC_ID.match(sync_id): @@ -164,6 +191,7 @@ def add( "name": name, "exit_strategy": exit_strategy, "assessment_id": assessment_id, + "key_id": key_id, } # The document only: never the key, never a response self._write( @@ -283,7 +311,11 @@ def flush( # for the whole flush; without it every entry gets the upload budget. host = host or settings.host.value result = FlushResult() - entries = spool.entries(host_id(host)) + key_id = key_fingerprint(settings.api_key.value) + found = spool.entries(host_id(host)) + # A sync goes out only with the key it was spooled with + entries = [entry for entry in found if entry.belongs_to(key_id)] + result.other_key = [entry.sync_id for entry in found if entry not in entries] if not entries: return result diff --git a/cloudexit/platform/status.py b/cloudexit/platform/status.py index 1fbacf7..0360e74 100644 --- a/cloudexit/platform/status.py +++ b/cloudexit/platform/status.py @@ -27,8 +27,15 @@ client_policy, resolve_ca_bundle, ) -from cloudexit.platform.spool import DEFAULT_MAX_AGE_DAYS, Spool, format_age, host_id +from cloudexit.platform.spool import ( + DEFAULT_MAX_AGE_DAYS, + Spool, + format_age, + host_id, + key_fingerprint, +) from cloudexit.platform.document import SCHEMA_VERSION +from cloudexit.platform.shape import as_dict, as_list, as_text STATUS_BUDGET = 15.0 KEY_EXPIRY_WARNING_DAYS = 14 @@ -316,18 +323,30 @@ def host_catalogue_lines( make_client = make_client or ApiClient.from_settings try: client = make_client(settings, budget=status_budget(settings)) - host = ((client.meta(fresh=True).get("catalogue") or {}).get("version")) or None + host = as_text(as_dict(client.meta(fresh=True).get("catalogue")).get("version")) except ApiError as error: return [f"platform: unavailable ({error.message})"] return [f"platform: {catalogue_standing(local, host)}"] +def _finite(value: Any) -> Any: + # JSON output has no NaN or infinity + if isinstance(value, bool) or not isinstance(value, (int, float)): + return None + return value if value == value and abs(value) != float("inf") else None + + def _expiry_lines(key: Mapping[str, Any], now: float, report: StatusReport) -> str: expires = key.get("expires_at") - if not isinstance(expires, (int, float)): + if isinstance(expires, bool) or not isinstance(expires, (int, float)): + return "unknown" + try: + when = datetime.fromtimestamp(expires, tz=timezone.utc).strftime("%Y-%m-%d") + days = (expires - now) / 86400 + int(days) + except (OverflowError, OSError, ValueError): + # A time no calendar holds, or not a number at all return "unknown" - when = datetime.fromtimestamp(expires, tz=timezone.utc).strftime("%Y-%m-%d") - days = (expires - now) / 86400 if days < KEY_EXPIRY_WARNING_DAYS: report.warnings.append( f"the API key expires in {max(0, int(days))} days ({when}); " @@ -422,12 +441,13 @@ def finish_spool(days: int) -> None: report.problem(error.exit_code, f"{error.code}: {error.message}") return report - host_info = meta.get("host") or {} - contract = meta.get("contract") or {} - catalogue = meta.get("catalogue") or {} - limits = meta.get("limits") or {} + host_info = as_dict(meta.get("host")) + contract = as_dict(meta.get("contract")) + catalogue = as_dict(meta.get("catalogue")) + limits = as_dict(meta.get("limits")) + catalogue_version = as_text(catalogue.get("version")) state, message = client_policy(meta) - versions = contract.get("schema_versions") or [] + versions = as_list(contract.get("schema_versions")) report.add( "Platform (GET /meta)", [ @@ -440,7 +460,7 @@ def finish_spool(days: int) -> None: ), f"schema: {', '.join(map(str, versions)) or 'unknown'}" + (f" (current {contract['current']})" if contract.get("current") else ""), - f"catalogue: {catalogue.get('version') or 'unknown'}", + f"catalogue: {catalogue_version or 'unknown'}", "limits: " + ", ".join(f"{k}={limits[k]}" for k in LIMITS_SHOWN if k in limits), f"client: {message}", @@ -451,7 +471,7 @@ def finish_spool(days: int) -> None: "region": host_info.get("region"), "app_version": host_info.get("app_version"), "schema_versions": versions, - "catalogue_version": catalogue.get("version"), + "catalogue_version": catalogue_version, "limits": {k: limits[k] for k in LIMITS_SHOWN if k in limits}, "client_policy": {"status": state, "message": message}, } @@ -474,8 +494,9 @@ def finish_spool(days: int) -> None: report.problem(error.exit_code, f"{error.code}: {error.message}") finish_spool(days) return report - key_info = whoami.get("key") or {} - tenant = whoami.get("tenant") or {} + key_info = as_dict(whoami.get("key")) + tenant = as_dict(whoami.get("tenant")) + scopes = [str(scope) for scope in as_list(key_info.get("scopes"))] expiry = _expiry_lines(key_info, now(), report) report.add( "Key (GET /whoami)", @@ -484,19 +505,19 @@ def finish_spool(days: int) -> None: f"key: {key_info.get('display')} \"{key_info.get('name')}\", " f"{key_info.get('kind')}", f"assessment: {key_info.get('assessment_id') or 'any (not bound)'}", - f"scopes: {', '.join(key_info.get('scopes') or []) or 'none'}", + f"scopes: {', '.join(scopes) or 'none'}", f"expires: {expiry}", ], ) - for item in whoami.get("warnings") or []: + for item in as_list(whoami.get("warnings")): if isinstance(item, dict) and item.get("message"): report.warnings.append(str(item["message"])) report.data["key"] = { "organisation": tenant.get("name"), "kind": key_info.get("kind"), "assessment_id": key_info.get("assessment_id"), - "scopes": key_info.get("scopes") or [], - "expires_at": key_info.get("expires_at"), + "scopes": scopes, + "expires_at": _finite(key_info.get("expires_at")), } finish_spool(days) @@ -504,13 +525,13 @@ def finish_spool(days: int) -> None: "Catalogue", [ f"installed: {local or 'none'}", - f"platform: {catalogue_standing(local, catalogue.get('version'))}", + f"platform: {catalogue_standing(local, catalogue_version)}", ], ) report.data["catalogue"] = { "local": local, - "platform": catalogue.get("version"), - "standing": compare_catalogue_versions(local, catalogue.get("version")), + "platform": catalogue_version, + "standing": compare_catalogue_versions(local, catalogue_version), } return report @@ -519,15 +540,19 @@ def _spool_lines( settings: Any, spool: Spool | None, days: int, now: float, report: StatusReport ) -> list[str]: spool = spool or Spool.from_settings(settings) - mine = spool.entries(host_id(settings.host.value)) - others = len(spool.entries()) - len(mine) + on_host = spool.entries(host_id(settings.host.value)) + key_id = key_fingerprint(settings.api_key.value) + mine = [e for e in on_host if e.belongs_to(key_id)] + other_keys = len(on_host) - len(mine) + others = len(spool.entries()) - len(on_host) stale = [e for e in mine if now - e.created > days * 86400] report.data["spool"] = { "pending": len(mine), "too_old": len(stale), "other_hosts": others, + "other_keys": other_keys, } - if not mine and not others: + if not on_host and not others: return ["empty"] lines = [] if mine: @@ -538,6 +563,8 @@ def _spool_lines( ) if stale: lines.append(f"{len(stale)} will be rejected: older than {days} days") + if other_keys: + lines.append(f"{other_keys} for another API key (not delivered with this one)") if others: lines.append(f"{others} for other hosts") return lines diff --git a/cloudexit/platform/upload.py b/cloudexit/platform/upload.py index c63ee36..fde9dd2 100644 --- a/cloudexit/platform/upload.py +++ b/cloudexit/platform/upload.py @@ -11,7 +11,7 @@ from cloudexit.settings import Settings, SettingsError, load_settings from cloudexit.platform.client import ApiClient, ApiError -from cloudexit.platform.spool import Spool +from cloudexit.platform.spool import Spool, key_fingerprint logger = logging.getLogger(__name__) @@ -119,6 +119,7 @@ def _spool( name=name, exit_strategy=meta.get("exit_strategy"), assessment_id=settings.assessment_id.value, + key_id=key_fingerprint(settings.api_key.value), ) return True except (OSError, ValueError, SettingsError) as exc: diff --git a/cloudexit/private_files.py b/cloudexit/private_files.py new file mode 100644 index 0000000..8e0b306 --- /dev/null +++ b/cloudexit/private_files.py @@ -0,0 +1,35 @@ +import os +import stat +import sys +from pathlib import Path + + +def write_private(path: str | Path, data: bytes) -> None: + # Readable by the owner only; a stricter umask is never loosened + descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + with os.fdopen(descriptor, "wb") as handle: + handle.write(data) + + +def restrict_tree(root: str | Path) -> None: + # Remove the group and other permissions of a folder and everything in it; + # whatever the owner has stays as it is + if sys.platform == "win32": + return + root = Path(root) + if not root.is_dir(): + return + for directory, folders, files in os.walk(root): + for name in [*folders, *files]: + _restrict(Path(directory) / name) + _restrict(root) + + +def _restrict(path: Path) -> None: + try: + if path.is_symlink(): + return + mode = stat.S_IMODE(path.stat().st_mode) + path.chmod(mode & 0o700) + except OSError: + pass diff --git a/cloudexit/providers/azure/cli_session.py b/cloudexit/providers/azure/cli_session.py index 2c03c54..d555f8e 100644 --- a/cloudexit/providers/azure/cli_session.py +++ b/cloudexit/providers/azure/cli_session.py @@ -51,7 +51,11 @@ def select_subscription(subscriptions: list[Any]) -> Any: # logger.info("Listing available subscriptions for selection.") console.print("Available Subscriptions:") for idx, sub in enumerate(subscriptions, start=1): - console.print(f"{idx}. {sub.display_name} ({sub.subscription_id})") + console.print( + f"{idx}. {sub.display_name} ({sub.subscription_id})", + markup=False, + emoji=False, + ) while True: try: selection = int(ask("Select a subscription by number: ").strip()) @@ -62,14 +66,19 @@ def select_subscription(subscriptions: list[Any]) -> Any: return selected_subscription except ValueError as e: logger.debug(f"Invalid subscription selection: {e}") - console.print(f"[red]{e} Please select a valid number.[/red]") + console.print( + f"{e} Please select a valid number.", + style="red", + markup=False, + emoji=False, + ) def select_resource_group(resource_groups: list[Any]) -> str: # logger.info("Listing available resource groups for selection.") console.print("Available Resource Groups:") for idx, rg in enumerate(resource_groups, start=1): - console.print(f"{idx}. {rg.name}") + console.print(f"{idx}. {rg.name}", markup=False, emoji=False) while True: try: selection = int(ask("Select a resource group by number: ").strip()) @@ -80,4 +89,9 @@ def select_resource_group(resource_groups: list[Any]) -> str: return selected_resource_group except ValueError as e: logger.debug(f"Invalid resource group selection: {e}") - console.print(f"[red]{e} Please select a valid number.[/red]") + console.print( + f"{e} Please select a valid number.", + style="red", + markup=False, + emoji=False, + ) diff --git a/cloudexit/report/assets.py b/cloudexit/report/assets.py index 784d0dd..e08be54 100644 --- a/cloudexit/report/assets.py +++ b/cloudexit/report/assets.py @@ -3,7 +3,11 @@ import shutil import logging -from cloudexit.catalogue.store import active_catalogue_path +from cloudexit.catalogue.store import ( + CatalogueError, + active_catalogue_path, + validate_schema, +) from cloudexit.resources import asset_path @@ -71,3 +75,10 @@ def copy_assets(report_path: str, cloud_service_provider: int) -> None: # Only copy if the destination doesn't already exist if not os.path.exists(db_dest_path): shutil.copyfile(db_src_path, db_dest_path) + # The run writes its results into this copy, so what it holds is checked + # here too: a copy with a trigger or a view is not kept + try: + validate_schema(db_dest_path) + except CatalogueError: + os.unlink(db_dest_path) + raise diff --git a/cloudexit/report/helpers.py b/cloudexit/report/helpers.py index 3536541..a7221ea 100644 --- a/cloudexit/report/helpers.py +++ b/cloudexit/report/helpers.py @@ -1,4 +1,6 @@ -import os +from pathlib import Path + +from cloudexit.resources import resolve_inside, safe_relative CURRENCY_SYMBOLS = {"USD": "$", "EUR": "€", "GBP": "£"} @@ -84,8 +86,9 @@ def initials(name): def report_icon(report_path, icon_rel): # The page only points at files in its own folder: an icon the report # folder does not hold is left out, and the page draws a placeholder - if icon_rel and os.path.isfile(os.path.join(report_path, REPORT_ASSETS, icon_rel)): - return f"{REPORT_ASSETS}/{icon_rel}" + found = resolve_inside(Path(report_path) / REPORT_ASSETS, icon_rel) + if found is not None and found.is_file(): + return f"{REPORT_ASSETS}/{safe_relative(icon_rel).as_posix()}" return None diff --git a/cloudexit/resources.py b/cloudexit/resources.py index 1398800..9dcdff1 100644 --- a/cloudexit/resources.py +++ b/cloudexit/resources.py @@ -12,14 +12,45 @@ def asset_path(*parts: str) -> Path: return root.joinpath("assets", *parts) +def safe_relative(relative: object) -> PurePosixPath | None: + # A path a catalogue names for an asset, as a relative path inside the + # assets folder, or None. One leading slash is how the catalogue writes its + # icons ("/icons/aws/..."): it means the assets folder, not the file system. + text = str(relative or "") + if text.startswith("/"): + text = text[1:] + if not text or text.startswith(("/", "\\")): + return None + # No backslash (UNC and Windows separators), no colon (drive letters and + # stream names) and no NUL; nothing that could leave the folder + if any(char in text for char in ("\\", ":", "\x00")): + return None + parts = PurePosixPath(text).parts + if not parts or ".." in parts: + return None + return PurePosixPath(*parts) + + +def resolve_inside(base: str | Path, relative: object) -> Path | None: + # base/relative when it stays inside base, whatever links lie on the way; + # nothing outside base is looked at + safe = safe_relative(relative) + if safe is None: + return None + root = Path(base) + candidate = root.joinpath(*safe.parts) + try: + inside = candidate.resolve().is_relative_to(root.resolve()) + except (OSError, RuntimeError, ValueError): + return None + return candidate if inside else None + + def find_asset(report_path: str | Path, relative: str) -> Path | None: # The report folder carries its own copy of the assets; the installed # package is the fallback. - parts = PurePosixPath(str(relative).lstrip("/")).parts - if not parts or ".." in parts: - return None for base in (Path(report_path) / "assets", asset_path()): - candidate = base.joinpath(*parts) - if candidate.is_file(): + candidate = resolve_inside(base, relative) + if candidate is not None and candidate.is_file(): return candidate return None diff --git a/cloudexit/runconfig/loader.py b/cloudexit/runconfig/loader.py index aab6ec1..887b704 100644 --- a/cloudexit/runconfig/loader.py +++ b/cloudexit/runconfig/loader.py @@ -22,7 +22,9 @@ def load_config(file_path: str) -> dict[str, Any] | None: return config except Exception as e: logger.error(f"Error loading config file: {e}", exc_info=True) - console.print(f"[red]Error loading config file: {e}[/red]") + console.print( + f"Error loading config file: {e}", style="red", markup=False, emoji=False + ) return None @@ -53,8 +55,11 @@ def require_env(var: str, description: str) -> str: value = os.environ.get(var, "").strip() if not value: console.print( - f"[red]--non-interactive requires {description}. " - f"Set the {var} environment variable.[/red]" + f"--non-interactive requires {description}. " + f"Set the {var} environment variable.", + style="red", + markup=False, + emoji=False, ) sys.exit(codes.CONFIG) return value @@ -66,9 +71,19 @@ def require_env_int(var: str, description: str, valid: set) -> int: try: value = int(raw) except ValueError: - console.print(f"[red]{var} must be an integer, got: {raw!r}[/red]") + console.print( + f"{var} must be an integer, got: {raw!r}", + style="red", + markup=False, + emoji=False, + ) sys.exit(codes.CONFIG) if value not in valid: - console.print(f"[red]{var} must be one of {sorted(valid)}, got: {value}[/red]") + console.print( + f"{var} must be one of {sorted(valid)}, got: {value}", + style="red", + markup=False, + emoji=False, + ) sys.exit(codes.CONFIG) return value diff --git a/cloudexit/terminal.py b/cloudexit/terminal.py index 51f263d..60cf2f3 100644 --- a/cloudexit/terminal.py +++ b/cloudexit/terminal.py @@ -2,6 +2,7 @@ import getpass import sys from rich.console import Console +from rich.markup import escape from rich.style import Style from time import sleep @@ -47,18 +48,33 @@ def print_step( # Handle the pending status with a spinner if status == "pending": with console.status( - f"{description:<50} [yellow]{status_map['pending']}[/yellow]", + f"{escape(description):<50} [yellow]{status_map['pending']}[/yellow]", spinner="dots", ): sleep(2) print_step(description, status="ok") elif status == "ok": - console.print(f"{description:<50} {status_map['ok']}", style=ok_style) + console.print( + f"{description:<50} {status_map['ok']}", + style=ok_style, + markup=False, + emoji=False, + ) elif status == "warning": - console.print(f"{description:<50} {status_map['warning']}", style=warning_style) + console.print( + f"{description:<50} {status_map['warning']}", + style=warning_style, + markup=False, + emoji=False, + ) if logs: - console.print(f" ↳ {logs}", style="dim") + console.print(f" ↳ {logs}", style="dim", markup=False, emoji=False) elif status == "error": - console.print(f"{description:<50} {status_map['error']}", style=error_style) + console.print( + f"{description:<50} {status_map['error']}", + style=error_style, + markup=False, + emoji=False, + ) if logs: - console.print(f" ↳ {logs}", style="dim") + console.print(f" ↳ {logs}", style="dim", markup=False, emoji=False) diff --git a/tests/catalogue/test_catalogue.py b/tests/catalogue/test_catalogue.py index 6b1c841..d7dadef 100644 --- a/tests/catalogue/test_catalogue.py +++ b/tests/catalogue/test_catalogue.py @@ -15,6 +15,9 @@ from cloudexit.cli import app from cloudexit.cli.parser import parse_arguments from cloudexit.catalogue import db as catalogue_db +from cloudexit.catalogue.db import connect +from cloudexit.engine import create_resource_inventory +from cloudexit.report.assets import copy_assets from tests.support.isolation import isolated_environ from tests.support.catalogue import ( build_catalogue, @@ -38,10 +41,10 @@ catalogue_info, import_catalogue, installed_catalogue, - migrate_legacy_dataset, read_state, set_active_catalogue, use_catalogue_file, + _open_readonly, ) from cloudexit.catalogue.download import ( ensure_catalogue, @@ -63,12 +66,6 @@ def setUp(self): self.tmp = Path(tmp.name) self.dir = self.tmp / "catalogue" self.addCleanup(set_active_catalogue, None) - patcher = patch( - "cloudexit.catalogue.store.LEGACY_DATABASE", - self.tmp / "no-legacy" / "data.db", - ) - patcher.start() - self.addCleanup(patcher.stop) def install(self, version=V1, **kwargs): gz = catalogue_gz(version=version, **kwargs) @@ -408,51 +405,145 @@ def test_an_invalid_file_is_an_error(self): use_catalogue_file(path) -class MigrationTests(CatalogueTestCase): +class LegacyDatasetTests(CatalogueTestCase): def legacy(self, **kwargs): path = self.tmp / "datasets" / "data.db" path.parent.mkdir() build_catalogue(path, **kwargs) return path - def test_copies_the_legacy_dataset_once(self): + def run_in_tmp(self, action): + previous = os.getcwd() + os.chdir(self.tmp) + try: + return action() + finally: + os.chdir(previous) + + def test_a_datasets_folder_is_never_installed_on_its_own(self): + self.legacy(version=V2) + + def ensure(): + with fake_http(requests.ConnectionError("offline")): + return ensure_catalogue(BASE, directory=self.dir) + + with self.assertRaises(CatalogueUnavailable): + self.run_in_tmp(ensure) + + self.assertFalse((self.dir / "data.db").exists()) + self.assertIsNone(installed_catalogue(self.dir)) + + def test_it_is_installed_when_it_is_imported_on_purpose(self): legacy = self.legacy(version=V2) - self.assertTrue(migrate_legacy_dataset(self.dir, legacy)) + info = import_catalogue(legacy, directory=self.dir) + self.assertEqual(info.version, V2) self.assertEqual(self.version(), V2) - self.assertEqual(read_state(self.dir)["version"], V2) - self.assertEqual((self.dir / "data.db").read_bytes(), legacy.read_bytes()) - self.assertFalse(migrate_legacy_dataset(self.dir, legacy)) - def test_does_not_overwrite_an_installed_dataset(self): - self.install(V3) - legacy = self.legacy(version=V1) - self.assertFalse(migrate_legacy_dataset(self.dir, legacy)) - self.assertEqual(self.version(), V3) +class HardeningTests(CatalogueTestCase): + def catalogue_with(self, sql, name="hostile.db"): + path = self.tmp / name + build_catalogue(path) + conn = sqlite3.connect(path) + conn.executescript(sql) + conn.commit() + conn.close() + return path - def test_an_old_or_invalid_legacy_file(self): - self.assertTrue(migrate_legacy_dataset(self.dir, self.legacy(meta=False))) - self.assertIsNone(self.version()) + TRIGGER = """ + CREATE TABLE resource_inventory (resource_type INTEGER, count INTEGER); + CREATE TRIGGER rewrite AFTER INSERT ON resource_inventory + BEGIN UPDATE resource_inventory SET count = 999999; END; + """ + VIEW = "CREATE VIEW everything AS SELECT * FROM resourcetype;" - other = self.tmp / "other" - bad = self.tmp / "bad.db" - bad.write_bytes(b"nope") - self.assertFalse(migrate_legacy_dataset(other, bad)) - self.assertFalse(migrate_legacy_dataset(other, self.tmp / "missing.db")) + def test_a_catalogue_with_a_trigger_or_a_view_is_refused_at_install(self): + for what, sql in (("trigger", self.TRIGGER), ("view", self.VIEW)): + with self.subTest(what): + path = self.catalogue_with(sql, f"{what}.db") + + with self.assertRaises(CatalogueError) as raised: + import_catalogue(path, directory=self.dir) + + self.assertIn(what, str(raised.exception)) + self.assertIn("it was not used", str(raised.exception)) + self.assertFalse((self.dir / "data.db").exists()) + + def test_a_gzipped_catalogue_with_a_trigger_is_refused_too(self): + path = self.catalogue_with(self.TRIGGER) + packed = self.tmp / "hostile.db.gz" + packed.write_bytes(gzip.compress(path.read_bytes())) + + with self.assertRaises(CatalogueError): + import_catalogue(packed, directory=self.dir) + with self.assertRaises(CatalogueError): + use_catalogue_file(packed) + self.assertIsNone(installed_catalogue(self.dir)) + + def test_a_catalogue_file_given_on_the_command_line_is_refused(self): + path = self.catalogue_with(self.VIEW) + + with self.assertRaises(CatalogueError): + use_catalogue_file(path) + + def test_extra_tables_and_indexes_are_still_accepted(self): + path = self.catalogue_with( + "CREATE TABLE extra (a TEXT, b TEXT);" + "CREATE INDEX extra_a ON extra (a);" + "CREATE INDEX resourcetype_code ON resourcetype (code);" + ) + + info = import_catalogue(path, directory=self.dir) + + self.assertEqual(info.version, "2026-11-02") + + def test_a_trigger_that_got_past_the_install_is_stopped_before_the_copy_is_used( + self, + ): + path = self.catalogue_with(self.TRIGGER) + set_active_catalogue(path) + report = self.tmp / "report" + (report / "data").mkdir(parents=True) + + with patch("cloudexit.report.assets.shutil.copytree"): + with self.assertRaises(CatalogueError): + copy_assets(str(report), 2) + + self.assertFalse((report / "data" / "assessment.db").exists()) + + def test_the_resource_inventory_stage_fails_without_running_the_triggers(self): + path = self.catalogue_with(self.TRIGGER) + set_active_catalogue(path) + report = self.tmp / "report" + raw = report / "raw_data" + raw.mkdir(parents=True) - def test_ensure_reports_the_migration(self): - legacy = self.legacy(version=V2) - _, entry = self.release(V2) with ( - patch("cloudexit.catalogue.store.LEGACY_DATABASE", legacy), - fake_http(self.manifest_reply(entry)), + patch("cloudexit.report.assets.shutil.copytree"), + patch("cloudexit.engine.build_aws_resource_inventory") as collected, ): - result = ensure_catalogue(BASE, directory=self.dir) + result = create_resource_inventory(2, {}, str(report), str(raw)) - self.assertTrue(result.migrated) - self.assertEqual(result.status, "current") + self.assertFalse(result["success"]) + self.assertIn("trigger", result["logs"]) + collected.assert_not_called() + self.assertFalse((report / "data" / "assessment.db").exists()) + + def test_the_connections_do_not_trust_the_schema(self): + path = self.tmp / "plain.db" + build_catalogue(path) + + with connect(path) as conn: + self.assertEqual(conn.execute("PRAGMA trusted_schema").fetchone()[0], 0) + read_only = _open_readonly(path) + try: + self.assertEqual( + read_only.execute("PRAGMA trusted_schema").fetchone()[0], 0 + ) + finally: + read_only.close() class EnsureCatalogueTests(CatalogueTestCase): diff --git a/tests/cli/test_report_folder.py b/tests/cli/test_report_folder.py new file mode 100644 index 0000000..9efadd8 --- /dev/null +++ b/tests/cli/test_report_folder.py @@ -0,0 +1,138 @@ +import os +import stat +import sys +import tempfile +import unittest +from datetime import datetime +from pathlib import Path +from unittest.mock import patch + +from cloudexit.cli import app +from cloudexit.private_files import restrict_tree, write_private + +MOMENT = datetime(2026, 11, 12, 9, 30, 0) +POSIX_ONLY = unittest.skipIf(sys.platform == "win32", "POSIX file modes") + + +def frozen(): + class FrozenDatetime(datetime): + @classmethod + def now(cls, tz=None): + return MOMENT + + return FrozenDatetime + + +def mode(path): + return stat.S_IMODE(os.stat(path).st_mode) + + +class FolderTestCase(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.base = Path(tmp.name) / "reports" + + +class UniqueFolderTests(FolderTestCase): + def test_two_runs_in_the_same_second_get_different_folders(self): + with patch("cloudexit.cli.app.datetime", frozen()): + first, first_raw = app.create_directory(str(self.base)) + second, second_raw = app.create_directory(str(self.base)) + + self.assertNotEqual(first, second) + self.assertNotEqual(first_raw, second_raw) + for folder, raw in ((first, first_raw), (second, second_raw)): + self.assertTrue(Path(raw).is_dir()) + self.assertEqual(Path(raw).parent, Path(folder)) + self.assertTrue(Path(folder).name.startswith("20261112093000-")) + self.assertEqual(len(list(self.base.iterdir())), 2) + + def test_a_folder_that_exists_is_never_reused(self): + seen = set() + with patch("cloudexit.cli.app.datetime", frozen()): + for _ in range(25): + folder, _raw = app.create_directory(str(self.base)) + self.assertNotIn(folder, seen) + seen.add(folder) + self.assertEqual(len(seen), 25) + + def test_what_an_earlier_run_left_behind_is_untouched(self): + old = self.base / "20261112093000-abcdefgh" + (old / "raw_data").mkdir(parents=True) + (old / "raw_data" / "sync.json").write_text("earlier") + + with patch("cloudexit.cli.app.datetime", frozen()): + folder, _raw = app.create_directory(str(self.base)) + + self.assertNotEqual(Path(folder), old) + self.assertEqual((old / "raw_data" / "sync.json").read_text(), "earlier") + self.assertEqual(list(Path(folder).glob("**/*.json")), []) + + +@POSIX_ONLY +class PrivateFolderTests(FolderTestCase): + def setUp(self): + super().setUp() + previous = os.umask(0) + self.addCleanup(os.umask, previous) + + def test_the_folder_and_its_raw_data_are_for_the_owner_only(self): + folder, raw = app.create_directory(str(self.base)) + + self.assertEqual(mode(folder), 0o700) + self.assertEqual(mode(raw), 0o700) + self.assertEqual(mode(self.base), 0o700) + + def test_a_private_file_is_not_readable_by_others_and_keeps_a_stricter_umask(self): + path = self.base.parent / "a.json" + write_private(path, b"{}") + self.assertEqual(mode(path), 0o600) + + os.umask(0o277) + stricter = self.base.parent / "b.json" + write_private(stricter, b"{}") + self.assertEqual(mode(stricter), 0o400) + + def test_restricting_a_tree_removes_group_and_other_and_never_loosens(self): + root = self.base / "run" + (root / "assets").mkdir(parents=True) + files = { + root / "index.html": 0o644, + root / "assets" / "logo.svg": 0o664, + root / "report.pdf": 0o600, + root / "read-only.json": 0o400, + } + for path, wanted in files.items(): + path.write_text("x") + path.chmod(wanted) + os.chmod(root, 0o755) + os.chmod(root / "assets", 0o775) + + restrict_tree(root) + + self.assertEqual(mode(root), 0o700) + self.assertEqual(mode(root / "assets"), 0o700) + self.assertEqual(mode(root / "index.html"), 0o600) + self.assertEqual(mode(root / "assets" / "logo.svg"), 0o600) + self.assertEqual(mode(root / "report.pdf"), 0o600) + self.assertEqual(mode(root / "read-only.json"), 0o400) + + def test_a_link_is_left_alone(self): + root = self.base / "run" + root.mkdir(parents=True) + outside = self.base.parent / "outside.txt" + outside.write_text("x") + outside.chmod(0o644) + (root / "link").symlink_to(outside) + + restrict_tree(root) + + self.assertEqual(mode(outside), 0o644) + + def test_a_missing_folder_is_not_an_error(self): + restrict_tree(self.base / "nothing") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cli/test_status.py b/tests/cli/test_status.py index 403a10d..dcf0e12 100644 --- a/tests/cli/test_status.py +++ b/tests/cli/test_status.py @@ -12,7 +12,7 @@ from cloudexit.cli import app from cloudexit.platform.client import ApiClient -from cloudexit.platform.spool import Spool +from cloudexit.platform.spool import Spool, key_fingerprint from cloudexit.platform.status import ( TlsResult, catalogue_standing, @@ -444,6 +444,7 @@ def test_the_spool_is_summarised(self): name=None, exit_strategy=None, assessment_id=None, + key_id=key_fingerprint(KEY), ) with fake_http(meta_reply(), whoami_reply()): @@ -558,6 +559,90 @@ def test_an_unreachable_platform_is_reported_not_hidden(self): self.assertIn("unavailable", lines[0]) +class ShapeTests(StatusTestCase): + def test_a_platform_answer_of_the_wrong_shape_never_crashes_status(self): + for field in ("host", "contract", "catalogue", "limits", "clients"): + for value in ("escapecloud", ["x"], 7, None, True): + with self.subTest(field=field, value=value): + report = self.status( + configured(), + [meta_reply(**{field: value}), whoami_reply()], + ) + + self.assertEqual(report.exit_code, codes.SUCCESS) + self.assertIn("Platform (GET /meta)", self.text(report)) + json.dumps(report.as_json(), allow_nan=False) + + def test_a_whoami_of_the_wrong_shape_never_crashes_status(self): + for field, value in ( + ("key", "esc_sk"), + ("tenant", ["Org"]), + ("warnings", "careful"), + ("key", {"scopes": "syncs:write", "expires_at": "soon"}), + ("key", {"scopes": 5, "expires_at": True}), + ): + with self.subTest(field=field, value=value): + report = self.status( + configured(), + [meta_reply(), whoami_reply(**{field: value})], + ) + + self.assertEqual(report.exit_code, codes.SUCCESS) + self.assertIn("Key (GET /whoami)", self.text(report)) + json.dumps(report.as_json(), allow_nan=False) + + def test_an_expiry_no_calendar_holds_is_unknown(self): + for expires in (1e20, -1e20, float("inf"), float("nan"), 2**80): + with self.subTest(expires=expires): + report = self.status( + configured(), + [meta_reply(), whoami_reply(key={"expires_at": expires})], + ) + + self.assertEqual(report.exit_code, codes.SUCCESS) + self.assertIn("expires: unknown", self.text(report)) + data = json.loads(json.dumps(report.as_json(), allow_nan=False)) + if expires != expires or abs(expires) == float("inf"): + self.assertIsNone(data["key"]["expires_at"]) + + def test_a_catalogue_of_the_wrong_shape_is_unknown_on_the_catalogue_command(self): + for catalogue in ("2026-11-02", ["2026-11-02"], {"version": 20261102}): + with self.subTest(catalogue=catalogue): + with ( + patch( + "cloudexit.platform.status.ApiClient.from_settings", + side_effect=self.make_client, + ), + fake_http(meta_reply(catalogue=catalogue)), + ): + lines = host_catalogue_lines(configured(), "2026-11-01") + + self.assertEqual(lines, ["platform: the platform reports none"]) + + def test_the_status_command_answers_even_when_the_check_itself_fails(self): + for output in ("text", "json"): + with self.subTest(output=output): + out = io.StringIO() + with ( + patch("sys.argv", ["cloudexit", "status", "--output", output]), + patch("cloudexit.cli.app.load_settings", return_value=configured()), + patch( + "cloudexit.cli.app.run_status", + side_effect=AttributeError("boom"), + ), + contextlib.redirect_stdout(out), + ): + with self.assertRaises(SystemExit) as raised: + app.main() + + self.assertEqual(raised.exception.code, codes.UNEXPECTED) + self.assertNotIn("boom", out.getvalue()) + if output == "json": + data = json.loads(out.getvalue()) + self.assertEqual(data["exit_code"], codes.UNEXPECTED) + self.assertIn("AttributeError", data["problems"][0]["message"]) + + class CommandTests(StatusTestCase): def run_command(self, argv, replies=None, settings=None): out = io.StringIO() diff --git a/tests/cli/test_upload_modes.py b/tests/cli/test_upload_modes.py index 7cd86f2..aa3d4f7 100644 --- a/tests/cli/test_upload_modes.py +++ b/tests/cli/test_upload_modes.py @@ -457,6 +457,34 @@ def host(version): self.assertIsNone(catalogue_outdated("2026-11-02", self.result())) self.assertIsNone(catalogue_outdated("2026-11-02", None)) + def test_a_platform_catalogue_of_the_wrong_shape_is_no_answer(self): + for catalogue in ( + {"host": "2026-11-02"}, + {"host": ["2026-11-02"]}, + {"host": {"version": 20261102}}, + {"host": {"version": ["x"]}}, + {"host": None}, + {}, + ): + with self.subTest(catalogue=catalogue): + result = self.result(catalogue=catalogue) + self.assertIsNone(catalogue_outdated("2026-11-02", result)) + warned = self.result(warning_items=("not a dict", OUTDATED)) + self.assertIs(catalogue_outdated("2026-11-02", warned), True) + + def test_a_summary_with_a_platform_answer_of_the_wrong_shape_is_complete(self): + summary = build_run_json( + exit_code=0, + report_dir=None, + sync_json=None, + upload=self.result(catalogue={"host": "escapecloud"}), + skipped_reason=None, + catalogue_version="2026-11-02", + ) + + self.assertEqual(summary["catalogue"]["outdated"], None) + self.assertEqual(summary["upload"]["status"], "uploaded") + def test_a_missing_upload_gives_a_complete_summary(self): summary = build_run_json( exit_code=2, @@ -506,6 +534,39 @@ def dispatch(args): self.assertIn("a step", err) self.assertNotIn("Preliminary Stage", out) + def test_a_platform_catalogue_of_the_wrong_shape_still_gives_one_summary(self): + def dispatch(args): + app.RUN["upload"] = UploadResult( + True, True, sync_id="s", catalogue={"host": "escapecloud"} + ) + app.RUN["catalogue_version"] = "2026-11-02" + + code, out, _ = self.run_json(dispatch) + + data = json.loads(out) + self.assertEqual(code, 0) + self.assertEqual(data["exit_code"], 0) + self.assertIsNone(data["catalogue"]["outdated"]) + self.assertNotIn("error", data) + + def test_a_summary_that_cannot_be_built_is_still_one_object_with_an_error(self): + def dispatch(args): + sys.exit(codes.UPLOAD_REJECTED) + + with patch( + "cloudexit.cli.app.build_run_json", side_effect=AttributeError("boom") + ): + code, out, _ = self.run_json(dispatch) + + data = json.loads(out) + self.assertEqual(code, codes.UPLOAD_REJECTED) + self.assertEqual(data["exit_code"], codes.UPLOAD_REJECTED) + self.assertIn("AttributeError", data["error"]) + self.assertNotIn("boom", out) + self.assertEqual(out.count("\n"), len(out.splitlines())) + for key in ("report_dir", "sync_json", "upload", "results", "catalogue"): + self.assertIn(key, data) + def test_a_clean_run_reports_exit_code_zero(self): code, out, _ = self.run_json(lambda args: None) self.assertEqual(code, 0) diff --git a/tests/platform/test_spool.py b/tests/platform/test_spool.py index 3935a34..968a0cb 100644 --- a/tests/platform/test_spool.py +++ b/tests/platform/test_spool.py @@ -21,6 +21,7 @@ flush, format_age, host_id, + key_fingerprint, parse_age, spool_root, state_dir, @@ -28,6 +29,7 @@ from cloudexit.platform.upload import UploadResult, read_sync_files, upload from tests.support.isolation import isolated_environ from tests.support.api import ( + EU_KEY, HOST, KEY, created_reply, @@ -112,6 +114,7 @@ def add(self, sync_id=OLD, *, created=None, age_days=0, error=None, **meta): name=meta.pop("name", "Spooled"), exit_strategy=meta.pop("exit_strategy", 1), assessment_id=meta.pop("assessment_id", None), + key_id=meta.pop("key_id", key_fingerprint(KEY)), ) self.now = NOW return entry @@ -245,6 +248,7 @@ def test_the_meta_holds_the_attempts_and_what_a_later_upload_needs(self): "name": "Prod", "exit_strategy": 3, "assessment_id": "a", + "key_id": key_fingerprint(KEY), }, ) @@ -536,6 +540,104 @@ def test_nothing_spooled_means_no_request(self): self.assertTrue(result.empty) self.assertEqual(fake.requests, []) + def flush_with(self, key, replies=()): + settings = self.settings( + api_key=Setting(key, "test"), upload_budget=Setting(5.0, "test") + ) + with fake_http(*replies) as fake: + result = flush(settings, self.spool, **self.options()) + return result, fake + + def test_a_sync_is_not_delivered_with_another_key_and_stays(self): + self.add(OLD) + + result, fake = self.flush_with(EU_KEY) + + self.assertEqual(fake.requests, []) + self.assertEqual(result.uploaded, []) + self.assertEqual(result.other_key, [OLD]) + self.assertFalse(result.empty) + self.assertEqual([e.sync_id for e in self.spool.entries()], [OLD]) + + def test_a_sync_is_delivered_once_with_the_key_that_spooled_it(self): + self.add(OLD) + self.flush_with(EU_KEY) + + result, fake = self.flush_with( + KEY, [meta_reply(), whoami_reply(), created_reply()] + ) + again, second = self.flush_with(KEY) + + self.assertEqual(result.uploaded, [OLD]) + self.assertEqual(result.other_key, []) + self.assertEqual(self.spool.entries(), []) + self.assertEqual(second.requests, []) + self.assertTrue(again.empty) + + def test_only_the_entries_of_the_key_in_use_are_delivered(self): + self.add(OLD, created=NOW - 300, key_id=key_fingerprint(EU_KEY)) + self.add(NEW, created=NOW - 10) + + result, fake = self.flush_with( + KEY, [meta_reply(), whoami_reply(), created_reply()] + ) + + self.assertEqual(self.keys(fake), [NEW]) + self.assertEqual(result.uploaded, [NEW]) + self.assertEqual(result.other_key, [OLD]) + self.assertEqual([e.sync_id for e in self.spool.entries()], [OLD]) + + def test_an_entry_without_a_recorded_key_is_never_delivered(self): + entry = self.add(OLD) + meta = json.loads(entry.meta_path.read_text(encoding="utf-8")) + del meta["key_id"] + entry.meta_path.write_text(json.dumps(meta), encoding="utf-8") + lost = self.add(NEW) + lost.meta_path.unlink() + + result, fake = self.flush_with(KEY) + + self.assertEqual(fake.requests, []) + self.assertEqual(sorted(result.other_key), sorted([OLD, NEW])) + self.assertEqual(len(self.spool.entries()), 2) + + def test_the_spool_never_holds_a_key(self): + self.add(OLD) + self.add(NEW, key_id=key_fingerprint(EU_KEY)) + + text = "".join( + path.read_text(encoding="utf-8") + for path in (self.tmp / "spool").rglob("*") + if path.is_file() + ) + + self.assertTrue(text) + for key in (KEY, EU_KEY): + self.assertNotIn(key, text) + self.assertNotIn(key.split("_sk_")[-1], text) + self.assertEqual(len(key_fingerprint(KEY)), 16) + self.assertNotEqual(key_fingerprint(KEY), key_fingerprint(EU_KEY)) + self.assertIsNone(key_fingerprint(None)) + self.assertIsNone(key_fingerprint("")) + + def test_a_failed_upload_spools_with_the_fingerprint_of_its_key(self): + settings = self.settings(upload_budget=Setting(0.0, "test")) + document = self.document(OLD) + + with fake_http(meta_reply(), whoami_reply(), unavailable()): + result = upload( + document, + settings, + name="Prod", + meta={"exit_strategy": 1}, + spool=self.spool, + **self.options(), + ) + + self.assertTrue(result.spooled) + entries = self.spool.entries(host_id(HOST)) + self.assertEqual([e.key_id for e in entries], [key_fingerprint(KEY)]) + def test_entries_of_another_host_are_left_alone(self): self.spool.add( "https://other.example.com", @@ -826,6 +928,15 @@ def test_what_was_delivered_and_what_waits_is_said(self): self.assertIn("1 spooled sync still waiting.", text) self.assertIn("maintenance", text) + def test_what_belongs_to_another_key_is_said_and_the_run_goes_on(self): + self.add(OLD, key_id=key_fingerprint(EU_KEY)) + order, steps = self.run_run(flush_result=FlushResult(other_key=[OLD])) + + text = " | ".join(str(a[0]) + " " + str(k.get("logs", "")) for a, k in steps) + self.assertIn("1 spooled sync for another API key left in the spool.", text) + self.assertIn("not delivered with this key", text) + self.assertIn("upload", [name for name, _ in order]) + def test_a_failing_flush_never_blocks_the_new_run(self): self.add(OLD) for error in ( @@ -1144,6 +1255,27 @@ def test_list_shows_id_host_age_attempts_and_last_error(self): lines[1].split(), [OLD, host_id(HOST), "2d", "4", "maintenance"] ) + def test_list_marks_what_belongs_to_another_key(self): + self.add(OLD) + self.add(NEW, key_id=key_fingerprint(EU_KEY)) + with patch("time.time", return_value=NOW): + _, out, _ = self.run_command(app.handle_spool, self.spool_args("list")) + + lines = {line.split()[0]: line for line in out.strip().splitlines()[1:]} + self.assertNotIn("not for the key in use", lines[OLD]) + self.assertIn("not for the key in use", lines[NEW]) + + def test_flush_leaves_the_entries_of_another_key_and_says_so(self): + self.add(OLD, key_id=key_fingerprint(EU_KEY)) + code, out, fake = self.run_command( + app.handle_spool, self.spool_args("flush"), [] + ) + + self.assertEqual(code, 0) + self.assertIn("1 spooled sync for another API key left in the spool.", out) + self.assertEqual([r for r in fake.requests if r.method == "POST"], []) + self.assertEqual(len(self.spool.entries()), 1) + def test_list_marks_what_the_platform_will_reject(self): self.add(OLD, created=NOW - 31 * DAY) with patch("time.time", return_value=NOW): diff --git a/tests/report/test_copy_assets.py b/tests/report/test_copy_assets.py index c7911e9..830e21e 100644 --- a/tests/report/test_copy_assets.py +++ b/tests/report/test_copy_assets.py @@ -1,5 +1,6 @@ import os import shutil +import sqlite3 import tempfile import unittest from contextlib import contextmanager @@ -21,7 +22,10 @@ def staged(cloud_service_provider): # on shutil itself: copytree calls copyfile internally, so patching it # globally would break the asset copy this module is testing. fake_shutil = mock.MagicMock(wraps=shutil) - fake_shutil.copyfile = mock.MagicMock() + # What the real copy leaves behind is a database; an empty one will do + fake_shutil.copyfile = mock.MagicMock( + side_effect=lambda source, target: sqlite3.connect(target).close() + ) with tempfile.TemporaryDirectory() as report_dir: with mock.patch("cloudexit.report.assets.shutil", fake_shutil): diff --git a/tests/report/test_report_flow.py b/tests/report/test_report_flow.py index 6277a8c..3e799a6 100644 --- a/tests/report/test_report_flow.py +++ b/tests/report/test_report_flow.py @@ -1,8 +1,10 @@ import copy import inspect +import io import json import os import sqlite3 +import sys import tempfile import unittest from dataclasses import replace @@ -11,6 +13,7 @@ from unittest.mock import patch import requests +from rich.console import Console from cloudexit.cli import app from cloudexit.engine import generate_report, perform_risk_assessment @@ -31,6 +34,7 @@ KEY, URL, corrupt, + created_reply, error_body, meta_reply, minimal_document, @@ -419,9 +423,11 @@ def run_assessment( replies=None, egress_ok=None, assessment_type=2, + real_terminal=False, **kwargs, ): self.steps = [] + self.output = io.StringIO() raw_data = os.path.join(report, "raw_data") console_patch = patch("cloudexit.cli.app.console.print") patches = [ @@ -443,12 +449,22 @@ def run_assessment( return_value={"success": True, "logs": ""}, ), patch("cloudexit.cli.app.add_run_log_handler"), - patch( - "cloudexit.cli.app.print_step", - side_effect=lambda *a, **k: self.steps.append((a, k)), - ), - console_patch, ] + if real_terminal: + # The real printing, into a buffer: what the user would read + shown = Console(file=self.output, width=200, color_system=None) + patches += [ + patch("cloudexit.cli.app.console", shown), + patch("cloudexit.terminal.console", shown), + ] + else: + patches += [ + patch( + "cloudexit.cli.app.print_step", + side_effect=lambda *a, **k: self.steps.append((a, k)), + ), + console_patch, + ] if egress_ok is not None: patches += [ patch( @@ -559,6 +575,74 @@ def test_connected_runs_keep_the_requested_depth_in_the_document(self): messages = [args[0] for args, _ in self.steps] self.assertEqual([m for m in messages if m.startswith("Forcing Basic")], []) + def test_platform_text_with_brackets_is_shown_literally_and_the_run_goes_on(self): + message = "rejected [/bold] [red]x[/red] :warning: [1, 2]" + report = self.report() + replies = [ + meta_reply(), + whoami_reply(), + reply(422, error_body("validation_failed", message)), + ] + + with self.assertRaises(SystemExit) as ctx: + self.run_assessment( + report, settings=CONFIGURED, replies=replies, real_terminal=True + ) + + self.assertEqual(ctx.exception.code, codes.UPLOAD_REJECTED) + self.assertIn(message, self.output.getvalue()) + self.assertNotIn("Unexpected error", self.output.getvalue()) + self.assert_local_report(report) + + def test_platform_capabilities_of_the_wrong_shape_do_not_stop_an_upload(self): + report = self.report() + replies = [ + meta_reply(contract="v2", clients=["x"], limits="none", host=7), + whoami_reply(), + created_reply(), + ] + + self.run_assessment(report, settings=CONFIGURED, replies=replies) + + self.assert_local_report(report) + record = json.loads(self.raw(report, "upload.json").read_text()) + self.assertEqual(record["status"], "uploaded") + + @unittest.skipIf(sys.platform == "win32", "POSIX file modes") + def test_a_finished_run_leaves_nothing_readable_by_others(self): + for replies, settings in ((None, None), (success_replies(), CONFIGURED)): + with self.subTest(uploaded=settings is not None): + report = self.report() + for path in Path(report).rglob("*"): + path.chmod(0o777 if path.is_dir() else 0o666) + + self.run_assessment(report, settings=settings, replies=replies) + + self.assert_local_report(report) + loose = [ + str(path.relative_to(report)) + for path in [Path(report), *Path(report).rglob("*")] + if not path.is_symlink() and path.stat().st_mode & 0o077 + ] + self.assertEqual(loose, []) + self.assertEqual( + self.raw(report, "sync.json").stat().st_mode & 0o777, 0o600 + ) + + def test_an_unexpected_error_with_brackets_is_shown_literally(self): + report = self.report() + with ( + patch( + "cloudexit.cli.app.prepare_sync_document", + side_effect=RuntimeError("bad [/bold] text"), + ), + self.assertRaises(SystemExit) as ctx, + ): + self.run_assessment(report, real_terminal=True) + + self.assertEqual(ctx.exception.code, codes.UNEXPECTED) + self.assertIn("Unexpected error: bad [/bold] text", self.output.getvalue()) + def test_a_failed_upload_still_writes_the_report_and_exits_with_an_upload_code( self, ): diff --git a/tests/test_asset_paths.py b/tests/test_asset_paths.py new file mode 100644 index 0000000..2cbc33a --- /dev/null +++ b/tests/test_asset_paths.py @@ -0,0 +1,140 @@ +import os +import sys +import tempfile +import unittest +from pathlib import Path, PurePosixPath +from unittest.mock import patch + +from cloudexit.report.helpers import report_icon +from cloudexit.resources import find_asset, resolve_inside, safe_relative + +REFUSED = ( + "", + "/", + "..", + "../x.png", + "icons/../../x.png", + "/../x.png", + "//host/share/x.png", + "///x.png", + "\\\\host\\share\\x.png", + "\\\\host\\share\\x.png".replace("\\", "/"), + "C:\\x.png", + "C:/x.png", + "c:x.png", + "icons\\aws\\EC2.png", + "/icons\\aws.png", + "x.png:stream", + "x\x00.png", +) + + +class AssetPathTestCase(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.tmp = Path(tmp.name) + self.report = self.tmp / "report" + self.assets = self.report / "assets" + (self.assets / "icons" / "aws").mkdir(parents=True) + (self.assets / "icons" / "aws" / "EC2.png").write_bytes(b"png") + self.outside = self.tmp / "outside.png" + self.outside.write_bytes(b"secret") + + +class SafeRelativeTests(unittest.TestCase): + def test_a_normal_relative_path_is_kept(self): + self.assertEqual( + safe_relative("icons/aws/EC2.png"), PurePosixPath("icons/aws/EC2.png") + ) + + def test_the_catalogue_form_with_one_leading_slash_means_the_assets_folder(self): + self.assertEqual( + safe_relative("/icons/aws/EC2.png"), PurePosixPath("icons/aws/EC2.png") + ) + + def test_everything_that_could_leave_the_folder_is_refused(self): + for relative in REFUSED: + with self.subTest(relative=relative): + self.assertIsNone(safe_relative(relative)) + self.assertIsNone(safe_relative(None)) + + +class FindAssetTests(AssetPathTestCase): + def test_a_normal_icon_still_resolves(self): + found = find_asset(self.report, "/icons/aws/EC2.png") + + self.assertEqual(found, self.assets / "icons" / "aws" / "EC2.png") + + def test_refused_paths_find_nothing_and_touch_no_file(self): + with ( + patch.object(Path, "is_file", side_effect=AssertionError("looked")), + patch.object(Path, "resolve", side_effect=AssertionError("looked")), + patch.object(os.path, "isfile", side_effect=AssertionError("looked")), + patch.object(os, "stat", side_effect=AssertionError("looked")), + ): + for relative in REFUSED: + with self.subTest(relative=relative): + self.assertIsNone(find_asset(self.report, relative)) + + def test_an_absolute_path_is_read_inside_the_assets_only(self): + looked = [] + real = Path.is_file + + def spy(path): + looked.append(path) + return real(path) + + with patch.object(Path, "is_file", spy): + found = find_asset(self.report, str(self.outside)) + + self.assertIsNone(found) + for path in looked: + self.assertNotEqual(path, self.outside) + + @unittest.skipIf(sys.platform == "win32", "needs symbolic links") + def test_a_link_out_of_the_assets_is_not_followed(self): + (self.assets / "icons" / "link.png").symlink_to(self.outside) + + self.assertIsNone(find_asset(self.report, "icons/link.png")) + self.assertIsNone(resolve_inside(self.assets, "icons/link.png")) + + +class ReportIconTests(AssetPathTestCase): + def test_a_normal_icon_points_into_the_folder(self): + self.assertEqual( + report_icon(self.report, "icons/aws/EC2.png"), "assets/icons/aws/EC2.png" + ) + self.assertEqual( + report_icon(self.report, "/icons/aws/EC2.png"), "assets/icons/aws/EC2.png" + ) + + def test_a_missing_icon_is_none(self): + self.assertIsNone(report_icon(self.report, "icons/aws/missing.png")) + self.assertIsNone(report_icon(self.report, None)) + self.assertIsNone(report_icon(self.report, "")) + + def test_refused_paths_give_the_placeholder_and_touch_no_file(self): + with ( + patch.object(Path, "is_file", side_effect=AssertionError("looked")), + patch.object(Path, "resolve", side_effect=AssertionError("looked")), + patch.object(os.path, "isfile", side_effect=AssertionError("looked")), + patch.object(os, "stat", side_effect=AssertionError("looked")), + ): + for relative in REFUSED: + with self.subTest(relative=relative): + self.assertIsNone(report_icon(self.report, relative)) + + def test_a_file_outside_the_folder_is_never_pointed_at(self): + for relative in ( + str(self.outside), + "../outside.png", + "..\\outside.png", + f"/{self.outside}", + ): + with self.subTest(relative=relative): + self.assertIsNone(report_icon(self.report, relative)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_layering.py b/tests/test_layering.py index dd428d4..4ec827c 100644 --- a/tests/test_layering.py +++ b/tests/test_layering.py @@ -12,6 +12,7 @@ "codes": set(), "version": set(), "resources": set(), + "private_files": set(), "terminal": {"codes"}, "settings": set(), "catalogue": {"settings"}, @@ -20,7 +21,7 @@ "providers": {"catalogue", "egress", "terminal"}, # egress.estimate reaches the providers inside a function, on purpose "egress": {"catalogue", "providers"}, - "platform": {"catalogue", "settings", "version", "codes"}, + "platform": {"catalogue", "settings", "version", "codes", "private_files"}, "report": {"catalogue", "egress", "providers", "resources", "version", "codes"}, "engine": {"catalogue", "providers", "report", "tfstate", "terminal", "codes"}, } From a9fd198c8319ebf40e16e4efbeecc4177e3824a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Sun, 25 Oct 2026 14:35:46 +0100 Subject: [PATCH 57/61] write the report logo path with forward slashes --- cloudexit/report/cover.py | 4 ++-- tests/test_asset_paths.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/cloudexit/report/cover.py b/cloudexit/report/cover.py index 6e9c0a6..74ab6d4 100644 --- a/cloudexit/report/cover.py +++ b/cloudexit/report/cover.py @@ -1,5 +1,4 @@ import logging -import os from dataclasses import dataclass from xml.sax.saxutils import escape @@ -29,7 +28,8 @@ TITLE_TOP = 8.1 * cm FOOTER_Y = 1.5 * cm ROW_SPACING = 14 -LOGO_FILE = os.path.join("img", "logo", "report_logo.png") +# Written with forward slashes on every system: asset paths are not file system paths +LOGO_FILE = "img/logo/report_logo.png" TITLE_STYLE = ParagraphStyle( "CoverTitle", diff --git a/tests/test_asset_paths.py b/tests/test_asset_paths.py index 2cbc33a..1d17b13 100644 --- a/tests/test_asset_paths.py +++ b/tests/test_asset_paths.py @@ -5,6 +5,7 @@ from pathlib import Path, PurePosixPath from unittest.mock import patch +from cloudexit.report.cover import LOGO_FILE, logo_reader from cloudexit.report.helpers import report_icon from cloudexit.resources import find_asset, resolve_inside, safe_relative @@ -61,6 +62,12 @@ def test_everything_that_could_leave_the_folder_is_refused(self): class FindAssetTests(AssetPathTestCase): + def test_the_report_logo_is_found_by_its_own_name_on_every_system(self): + self.assertNotIn("\\", LOGO_FILE) + self.assertEqual(safe_relative(LOGO_FILE), PurePosixPath(LOGO_FILE)) + self.assertIsNotNone(find_asset(self.report, LOGO_FILE)) + self.assertIsNotNone(logo_reader(self.report)) + def test_a_normal_icon_still_resolves(self): found = find_asset(self.report, "/icons/aws/EC2.png") From 36bb64409f9426e1928ab42349b2ea78854b57ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Sun, 25 Oct 2026 15:53:47 +0100 Subject: [PATCH 58/61] remove the legacy settings sources, the output promotion and the credentials in -vv logs --- cloudexit/cli/app.py | 91 ++++++++----- cloudexit/cli/parser.py | 2 +- cloudexit/cli/prompts.py | 7 +- cloudexit/log_redaction.py | 51 +++++++ cloudexit/report/egress.py | 49 +++---- cloudexit/report/egress_report.py | 3 - cloudexit/report/egress_sections.py | 41 +----- cloudexit/report/html_egress.py | 34 +++-- cloudexit/report/sections.py | 1 - cloudexit/report/wording.py | 15 --- cloudexit/settings.py | 76 ++++------- cloudexit/terminal.py | 10 +- config.example.toml | 2 - tests/cli/test_prompts.py | 16 +++ tests/cli/test_stub_platform.py | 4 +- tests/cli/test_upload_modes.py | 121 ++++++++++++++++- tests/report/test_basic_report.py | 3 +- tests/report/test_egress.py | 56 +++++++- tests/report/test_egress_report.py | 32 +++-- tests/report/test_report_flow.py | 6 +- tests/samples/test_samples.py | 30 ++++- tests/support/egress.py | 2 +- tests/test_layering.py | 1 + tests/test_logging.py | 144 +++++++++++++++++--- tests/test_settings.py | 202 +++++++++++++++++----------- 25 files changed, 684 insertions(+), 315 deletions(-) create mode 100644 cloudexit/log_redaction.py diff --git a/cloudexit/cli/app.py b/cloudexit/cli/app.py index 46c2bd7..d6944c6 100644 --- a/cloudexit/cli/app.py +++ b/cloudexit/cli/app.py @@ -10,6 +10,7 @@ import traceback from dataclasses import replace from typing import Any +from urllib.parse import urlsplit from rich.console import Console from rich.logging import RichHandler from datetime import datetime @@ -98,6 +99,7 @@ prompt_required_inputs, print_help_message, ) +from cloudexit.log_redaction import SecretRedactingFilter from cloudexit.private_files import restrict_tree from cloudexit.terminal import ask, print_step from cloudexit.runconfig.loader import ( @@ -130,9 +132,14 @@ # Loggers with no diagnostic value ever — pinned to WARNING regardless of verbosity, # so they never flood the -vv console or run.log. MSAL (behind the Azure -# credentials) writes the client ID in its debug output, and run.log is a file -# users share. -_ALWAYS_QUIET = ("PIL", "msal") +# credentials) writes the client ID in its debug output, botocore's signing and +# credential-resolution internals carry the credentials themselves, and run.log +# is a file users share. +_ALWAYS_QUIET = ("PIL", "msal", "botocore.auth", "botocore.credentials") + +# A last line of defence for the records that do stay on: both handlers drop +# credentials out of whatever reaches them. +_redacting_filter = SecretRedactingFilter() def configure_logging(verbose: int = 0) -> None: @@ -156,6 +163,7 @@ def configure_logging(verbose: int = 0) -> None: console=console, show_path=False, rich_tracebacks=True ) console_handler.setLevel(console_level) + console_handler.addFilter(_redacting_filter) root.addHandler(console_handler) third_party_level = logging.DEBUG if verbose >= 2 else logging.WARNING @@ -191,6 +199,7 @@ def add_run_log_handler(report_path: str) -> None: file_handler.setFormatter( logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") ) + file_handler.addFilter(_redacting_filter) logging.getLogger().addHandler(file_handler) _run_log_handler = file_handler except OSError as exc: @@ -364,13 +373,19 @@ def handle_aws(args): else: provider_details = _aws_provider_from_env() elif tfstate_path: - exit_strategy, assessment_type = prompt_required_inputs() + exit_strategy, assessment_type = prompt_required_inputs( + _asks_assessment_type(args) + ) provider_details = {"tfstatePath": tfstate_path} elif args.profile: provider_details = _aws_provider_from_profile(args.profile) - exit_strategy, assessment_type = prompt_required_inputs() + exit_strategy, assessment_type = prompt_required_inputs( + _asks_assessment_type(args) + ) else: - exit_strategy, assessment_type = prompt_required_inputs() + exit_strategy, assessment_type = prompt_required_inputs( + _asks_assessment_type(args) + ) provider_details = _aws_provider_from_prompt() config = build_config( @@ -606,13 +621,19 @@ def handle_azure(args): else: provider_details = _azure_provider_noninteractive(args) elif tfstate_path: - exit_strategy, assessment_type = prompt_required_inputs() + exit_strategy, assessment_type = prompt_required_inputs( + _asks_assessment_type(args) + ) provider_details = {"tfstatePath": tfstate_path} elif args.cli: provider_details = _azure_provider_from_cli() - exit_strategy, assessment_type = prompt_required_inputs() + exit_strategy, assessment_type = prompt_required_inputs( + _asks_assessment_type(args) + ) else: - exit_strategy, assessment_type = prompt_required_inputs() + exit_strategy, assessment_type = prompt_required_inputs( + _asks_assessment_type(args) + ) provider_details = _azure_provider_from_prompt() config = build_config( @@ -658,6 +679,26 @@ def _missing_platform(host, key): return "No host and no API key are configured." +def _host_display(host): + # The origin only: a path, a query or credentials have no place in a status + # line the user reads on every run + parts = urlsplit(host or "") + if not parts.hostname: + return host + origin = parts.hostname if not parts.port else f"{parts.hostname}:{parts.port}" + return f"{parts.scheme}://{origin}" if parts.scheme else origin + + +def _asks_assessment_type(args): + # Standard needs an upload, so the question is only real when one will happen + settings = getattr(args, "settings", None) + if settings is None or getattr(args, "dry_run", False): + return False + if settings.upload.value == "off": + return False + return bool(settings.host.value and settings.api_key.value) + + def _upload_mode(settings, dry_run): mode = settings.upload.value if settings else "auto" if not dry_run: @@ -782,7 +823,7 @@ def run_assessment( ) sys.exit(codes.CONFIG) - # Detect ExitCloud Integration + # Detect a configured platform mode = _upload_mode(settings, dry_run) host = settings.host.value if settings else None key = settings.api_key.value if settings else None @@ -813,9 +854,7 @@ def run_assessment( print_step("Upload is off – nothing is sent to a platform.", status="ok") # Without an upload only the Basic assessment is possible if config["assessmentType"] != 1: - print_step( - "Forcing Basic Assessment: uploads are off.", status="warning" - ) + print_step("Basic assessment: Standard needs an upload.", status="ok") config["assessmentType"] = 1 elif configured: # A key that cannot be right is a failed upload, found before the @@ -823,24 +862,24 @@ def run_assessment( # the local report. try: check_key(settings.api_key.value) - print_step("ExitCloud integration configured.", status="ok") + print_step(f"Platform configured: {_host_display(host)}.", status="ok") if flush_spool: _flush_earlier_syncs(settings) except ApiError as error: signin_failure = error_failure(error) print_step( - "ExitCloud integration configured, but the API key is not usable.", + "Platform configured, but the API key is not usable.", status="warning", logs=f"{error.message}. The report is still written locally; the upload counts as failed.", ) else: - print_step("ExitCloud integration not configured.", status="warning") + print_step( + "Offline run: no platform configured; the report is written locally.", + status="ok", + ) # Without a platform only the Basic assessment is possible if config["assessmentType"] != 1: - print_step( - "Forcing Basic Assessment: no platform is configured.", - status="warning", - ) + print_step("Basic assessment: Standard needs an upload.", status="ok") config["assessmentType"] = 1 RUN["skipped_reason"] = ( "dry_run" if dry_run else "off" if mode == "off" else "offline" @@ -1196,18 +1235,6 @@ def run_assessment( egress_failed = True # Output the report path after the separator - if not (dry_run or non_interactive): - console.print("-------------------------------------------") - console.print( - "Need to see how a regulatory-aligned report looks (DORA / FINMA / UK PRA evidence support)?" - ) - console.print( - "We can generate one from your --dry-run output and share a sample with you -- or forward this to your risk or compliance team." - ) - console.print( - "Point-in-time evidence only; not a compliance certification or a complete exit strategy." - ) - console.print("Contact: request_report@escapecloud.io") console.print("-------------------------------------------") console.print("Outputs:", style="bold") if upload_result and upload_result.success: diff --git a/cloudexit/cli/parser.py b/cloudexit/cli/parser.py index 03a5459..96b6b74 100644 --- a/cloudexit/cli/parser.py +++ b/cloudexit/cli/parser.py @@ -58,7 +58,7 @@ def parse_arguments(): help="Increase log verbosity (-v for INFO, -vv for DEBUG + third-party).", ) - # Platform options: flags > CLOUDEXIT_* environment > config file > legacy + # Platform options: flags > CLOUDEXIT_* environment > config file platform = argparse.ArgumentParser(add_help=False) platform.add_argument( "--host", type=str, help="Platform host (https://, or http:// for localhost)." diff --git a/cloudexit/cli/prompts.py b/cloudexit/cli/prompts.py index 0aa04c1..35f1079 100644 --- a/cloudexit/cli/prompts.py +++ b/cloudexit/cli/prompts.py @@ -6,7 +6,7 @@ logger = logging.getLogger(__name__) -def prompt_required_inputs() -> tuple[int, int]: +def prompt_required_inputs(ask_assessment_type: bool = True) -> tuple[int, int]: while True: try: exit_strategy = int( @@ -24,6 +24,11 @@ def prompt_required_inputs() -> tuple[int, int]: f"{e} Please enter 1 or 3.", style="red", markup=False, emoji=False ) + # Standard only exists on a platform, so without an upload there is + # nothing to choose + if not ask_assessment_type: + return exit_strategy, 1 + while True: try: assessment_type = int( diff --git a/cloudexit/log_redaction.py b/cloudexit/log_redaction.py new file mode 100644 index 0000000..8ea184f --- /dev/null +++ b/cloudexit/log_redaction.py @@ -0,0 +1,51 @@ +import logging +import re + +# botocore builds its request signature from the caller's credentials and logs +# the material at DEBUG, so -vv would otherwise copy the access key ID, the +# session token and the signature into the console and into run.log, which is a +# file users attach to bug reports. Redacting the values keeps the surrounding +# request detail readable. +REDACTED = "REDACTED" + +_SECRET_KEYS = ( + "authorization", + "x-amz-security-token", + "aws_secret_access_key", + "aws_session_token", + "client_secret", +) + +_RULES = [ + ( + re.compile( + rf"(?i)({re.escape(key)}['\"]?\s*[:=]\s*)(b?['\"][^'\"]*['\"]|[^,\r\n]*)" + ), + rf"\1{REDACTED}", + ) + for key in _SECRET_KEYS +] + [ + (re.compile(r"(?i)(credential=)[^/\s,]+(/)"), rf"\1{REDACTED}\2"), + (re.compile(r"(?i)(signature\s*[=:]\s*)[^\s,&'\"]+"), rf"\1{REDACTED}"), +] + + +def redact(text: str) -> str: + for pattern, replacement in _RULES: + text = pattern.sub(replacement, text) + return text + + +class SecretRedactingFilter(logging.Filter): + def filter(self, record: logging.LogRecord) -> bool: + # Redact the formatted text, because the secret often sits in an object + # passed as a %s argument rather than in the format string itself. + try: + message = record.getMessage() + except Exception: + return True + cleaned = redact(message) + if cleaned != message: + record.msg = cleaned + record.args = () + return True diff --git a/cloudexit/report/egress.py b/cloudexit/report/egress.py index 099eacd..0c2bf12 100644 --- a/cloudexit/report/egress.py +++ b/cloudexit/report/egress.py @@ -16,8 +16,6 @@ from cloudexit.providers.aws.egress import ARCHIVE_TIERS as AWS_ARCHIVE_TIERS from cloudexit.providers.azure.egress import ARCHIVE_TIERS as AZURE_ARCHIVE_TIERS from cloudexit.report.context import PROVIDER_NAMES -from cloudexit.report.notices import text_parts -from cloudexit.report.sections import BOOK_DEMO_URL, PRODUCT_SITES from cloudexit.report.wording import BLOCKS logger = logging.getLogger(__name__) @@ -199,9 +197,11 @@ def free_tier_headroom( def _summarize_totals( totals: dict[str, Any], + rows: list[dict[str, Any]], total_fee: float, prices: list[dict[str, Any]], cloud_service_provider: int, + b: dict[str, Any] = BLOCKS, ) -> dict[str, Any]: free_tier_limit = free_tier_limit_display(prices, cloud_service_provider) free_tier = ( @@ -210,9 +210,16 @@ def _summarize_totals( and free_tier_limit is not None ) unknown_count = totals["resources_with_unknown_size"] + # Nothing was measured, so there is no lower bound to show: "0.0 GiB+" would + # claim one of zero. A resource counts as sized when it has a size, the same + # way the "n of m sized" strip counts it. + nothing_sized = bool(rows) and all(row["size_bytes"] is None for row in rows) total_fee_display = format_fee(total_fee) total_data_display = format_gib(totals["known_size_bytes"]) - if unknown_count > 0: + if nothing_sized: + total_fee_display = b["ev_dash"] + total_data_display = b["ev_dash"] + elif unknown_count > 0: total_data_display = f"{total_data_display}+" if not free_tier: total_fee_display = f"{total_fee_display}+" @@ -220,6 +227,7 @@ def _summarize_totals( "free_tier": free_tier, "free_tier_limit": free_tier_limit, "unknown_count": unknown_count, + "nothing_sized": nothing_sized, "total_fee_display": total_fee_display, "total_data_display": total_data_display, } @@ -330,7 +338,7 @@ def load_egress_estimate(report_path: str, json_path: str) -> dict[str, Any]: "total_fee": total_fee, "type_groups": type_groups, "summary": _summarize_totals( - totals, total_fee, pricing, meta["cloud_service_provider"] + totals, rows, total_fee, pricing, meta["cloud_service_provider"] ), } @@ -339,9 +347,12 @@ def _share(part, whole): return part / whole * 100 if whole else 0 -def _plus(text, estimate): +def _plus(text, estimate, b=BLOCKS): # Resources without a size are left out, so the figure is a lower bound - return f"{text}+" if estimate["summary"]["unknown_count"] > 0 else text + summary = estimate["summary"] + if summary["nothing_sized"]: + return b["ev_dash"] + return f"{text}+" if summary["unknown_count"] > 0 else text def introduction_facts(scope, b=BLOCKS): @@ -540,6 +551,8 @@ def cost_facts(estimate, b=BLOCKS): if summary["free_tier"]: volume_stat = (b["eg_st_allowance"], summary["free_tier_limit"], False) + elif summary["nothing_sized"]: + volume_stat = (b["eg_st_billable"], b["ev_dash"], False) else: volume_stat = (b["eg_st_billable"], format_gib(billable), False) return { @@ -685,27 +698,3 @@ def pricing_facts(estimate, b=BLOCKS): ) ], } - - -def next_steps_facts( - executive_id, uploaded=False, platform_url=None, assessment_id=None, b=BLOCKS -): - # Paragraphs as text and (text, url) links, like the notices - sites = dict(PRODUCT_SITES) - paragraphs = [text_parts(b["eg_next_lead"], {}, {"executive_id": executive_id})] - if uploaded: - paragraphs.append([b["eg_next_uploaded"]]) - if platform_url: - paragraphs.append(text_parts(b["fa_uploaded_link"], {"link": platform_url})) - elif assessment_id: - paragraphs.append( - text_parts(b["fa_uploaded_id"], {}, {"assessment_id": assessment_id}) - ) - else: - paragraphs.append( - text_parts( - b["eg_next_platforms"], - {"exitcloud_url": sites["exitcloud"], "book_demo_url": BOOK_DEMO_URL}, - ) - ) - return {"heading": b["eg_heading_next_steps"], "paragraphs": paragraphs} diff --git a/cloudexit/report/egress_report.py b/cloudexit/report/egress_report.py index 8e108f6..a72abf0 100644 --- a/cloudexit/report/egress_report.py +++ b/cloudexit/report/egress_report.py @@ -15,7 +15,6 @@ data_landscape, estimated_costs, introduction, - next_steps, ) from .evidence import build_document_information_page from .flow import chapter_break, section_break @@ -41,8 +40,6 @@ def build_egress_content(context, estimate, styles, b): content += estimated_costs(context, estimate, styles, b) content.append(chapter_break()) content += appendix(context, estimate, styles, b) - content.append(Spacer(1, 12)) - content += next_steps(context, styles, b) content.append(chapter_break()) content += legal_notices(context, styles, b, egress=True) return content diff --git a/cloudexit/report/egress_sections.py b/cloudexit/report/egress_sections.py index 553a46a..9a01519 100644 --- a/cloudexit/report/egress_sections.py +++ b/cloudexit/report/egress_sections.py @@ -12,13 +12,11 @@ cost_facts, introduction_facts, landscape_facts, - next_steps_facts, pricing_facts, ) from .evidence import link_markup from .flow import section -from .notices import parts_markup -from .palette import HAIRLINE, HOTSPOT_COUNT, INK, MUTED, PROVENANCE_BACKGROUND, RULE +from .palette import HAIRLINE, HOTSPOT_COUNT, INK, MUTED from .placeholder import LabelWithPlaceholder from .sections import GLOSSARY_TERM_WIDTH from .table_style import data_table, header_cell, share_bar, summary_strip, text_style @@ -285,40 +283,3 @@ def appendix(context, estimate, styles, b): _glossary_table(facts["glossary"], context.appearance), ] return content - - -def next_steps(context, styles, b): - provenance = context.provenance - facts = next_steps_facts( - context.document_id, - provenance.uploaded, - provenance.platform_url, - provenance.assessment_id, - b, - ) - color = context.appearance.heading2_color - box = Table( - [ - [ - [ - Paragraph(parts_markup(parts, color), styles["ContentStyle"]) - for parts in facts["paragraphs"] - ] - ] - ], - colWidths=[16 * cm], - hAlign="LEFT", - ) - box.setStyle( - TableStyle( - [ - ("BACKGROUND", (0, 0), (-1, -1), HexColor(PROVENANCE_BACKGROUND)), - ("BOX", (0, 0), (-1, -1), 0.6, HexColor(RULE)), - ("LEFTPADDING", (0, 0), (-1, -1), 10), - ("RIGHTPADDING", (0, 0), (-1, -1), 10), - ("TOPPADDING", (0, 0), (-1, -1), 4), - ("BOTTOMPADDING", (0, 0), (-1, -1), 4), - ] - ) - ) - return [_heading(facts, styles), Spacer(1, 4), box] diff --git a/cloudexit/report/html_egress.py b/cloudexit/report/html_egress.py index fa19b09..5048995 100644 --- a/cloudexit/report/html_egress.py +++ b/cloudexit/report/html_egress.py @@ -40,8 +40,13 @@ def _kpis(estimate, sized, unsized, groups, b): summary = estimate["summary"] csp = estimate["meta"]["cloud_service_provider"] total = estimate["totals"]["known_size_bytes"] - headroom = free_tier_headroom(estimate["pricing"], csp, total) + nothing_sized = summary["nothing_sized"] + # Nothing was measured: no volume to show, and no headroom to claim against + headroom = ( + None if nothing_sized else free_tier_headroom(estimate["pricing"], csp, total) + ) plus = "+" if summary["unknown_count"] > 0 else "" + volume = b["ev_dash"] if nothing_sized else gib_number(total) kpis = [ { @@ -55,8 +60,8 @@ def _kpis(estimate, sized, unsized, groups, b): }, { "label": b["eg_st_volume"], - "figure": gib_number(total), - "unit": f"GiB{plus}", + "figure": volume, + "unit": "" if nothing_sized else f"GiB{plus}", "caption": ( b["html_eg_types_sized"].format(sized=len(sized), types=len(groups)) if groups @@ -83,9 +88,11 @@ def _kpis(estimate, sized, unsized, groups, b): kpis.append( { "label": b["eg_st_billable"], - "figure": gib_number(total), - "unit": "GiB", - "caption": b["html_eg_billable_caption"], + "figure": volume, + "unit": "" if nothing_sized else "GiB", + # With nothing sized the prices were never applied, so the + # caption would claim something the run did not establish + "caption": "" if nothing_sized else b["html_eg_billable_caption"], } ) kpis.append( @@ -142,7 +149,8 @@ def _volume(sized, unsized, total, b): def _table(context, estimate, sized, unsized, total, b): - plus = "+" if estimate["summary"]["unknown_count"] > 0 else "" + summary = estimate["summary"] + plus = "+" if summary["unknown_count"] > 0 else "" def row(group): icon = group["icon"].lstrip("/") if group["icon"] else "" @@ -177,8 +185,16 @@ def row(group): "not_sized": b["html_eg_not_sized"], "total": ( b["eg_total"], - f"{gib_number(total, 2)}{plus}", - fee_number(estimate["total_fee"]), + ( + b["ev_dash"] + if summary["nothing_sized"] + else f"{gib_number(total, 2)}{plus}" + ), + ( + b["ev_dash"] + if summary["nothing_sized"] + else fee_number(estimate["total_fee"]) + ), ), } diff --git a/cloudexit/report/sections.py b/cloudexit/report/sections.py index 5df8c54..56c155d 100644 --- a/cloudexit/report/sections.py +++ b/cloudexit/report/sections.py @@ -61,7 +61,6 @@ ("escapecloud", "https://escapecloud.io"), ("exitcloud", "https://exitcloud.io"), ) -BOOK_DEMO_URL = "https://escapecloud.io/book-demo/" AWS_ICONS_URL = "https://aws.amazon.com/architecture/icons/" AWS_TRADEMARK_URL = "https://aws.amazon.com/trademark-guidelines/" AZURE_ICONS_URL = "https://learn.microsoft.com/en-us/azure/architecture/icons/" diff --git a/cloudexit/report/wording.py b/cloudexit/report/wording.py index d54befd..d9db971 100644 --- a/cloudexit/report/wording.py +++ b/cloudexit/report/wording.py @@ -357,7 +357,6 @@ "definition": "One completed assessment of the defined scope, identified by the run ID on page 2.", }, ], - "fa_uploaded_link": "Open the reports: {link}", "fa_uploaded_id": "Assessment ID: {assessment_id}", "eg_heading_introduction": "Introduction", "eg_heading_purpose": "Purpose", @@ -576,19 +575,6 @@ "definition": "Fees a provider charges for moving data or workloads to another provider, including data egress.", }, ], - "eg_heading_next_steps": "Next Steps", - "eg_next_lead": ( - "Use this estimate together with the Executive Summary of the same " - "run ({executive_id}) when planning an exit." - ), - "eg_next_platforms": ( - "Connected ({exitcloud_url}) and Enterprise ({book_demo_url}) combine " - "this estimate with the Exit Readiness Report and version history." - ), - "eg_next_uploaded": ( - "This run was uploaded. The platform combines this estimate with the " - "Exit Readiness Report and version history." - ), "notices_egress": ( "Egress figures are estimates based on the provider's public list " "prices at the time of the run. Actual invoices can differ: " @@ -1046,7 +1032,6 @@ "eg_heading_costs", "eg_heading_pricing", "eg_heading_glossary", - "eg_heading_next_steps", "eg_th_resource_type", "eg_th_measured", "eg_th_resources", diff --git a/cloudexit/settings.py b/cloudexit/settings.py index 7af05e7..d1a0d99 100644 --- a/cloudexit/settings.py +++ b/cloudexit/settings.py @@ -12,11 +12,6 @@ logger = logging.getLogger(__name__) -try: - import config as legacy_config -except ModuleNotFoundError: - legacy_config = None - DEFAULT_TIMEOUT = 10.0 DEFAULT_UPLOAD_BUDGET = 300.0 LOCAL_HTTP_HOSTS = ("localhost", "127.0.0.1") @@ -51,6 +46,16 @@ FILE_KEYS = ("host", "api_key", "api_key_file", "assessment_id", "ca_bundle", "upload") PATH_KEYS = ("api_key_file", "ca_bundle", "spool_dir") +LEGACY_ENV_WARNING = ( + "HOST and KEY are no longer read (removed in 2.0). Use CLOUDEXIT_HOST and " + "CLOUDEXIT_API_KEY_FILE (or CLOUDEXIT_API_KEY)." +) +LEGACY_CONFIG_WARNING = ( + "config.py is no longer read (removed in 2.0). Move HOST and KEY to the " + "config file (see `cloudexit config path`) or to CLOUDEXIT_* environment " + "variables." +) + TRUE_WORDS = ("1", "true", "yes", "on") FALSE_WORDS = ("0", "false", "no", "off") @@ -196,28 +201,6 @@ def _env_layer(environ: Mapping[str, str]) -> dict[str, Setting]: return layer -def _legacy_env_layer(environ: Mapping[str, str]) -> dict[str, Setting]: - host = environ.get("HOST", "").strip() - key = environ.get("KEY", "").strip() - if host and key: - return { - "host": Setting(host, "legacy env HOST"), - "api_key": Setting(key, "legacy env KEY"), - } - if host or key: - logger.debug("Ignoring legacy HOST/KEY: they are only used when both are set.") - return {} - - -def _legacy_config_layer() -> dict[str, Setting]: - layer = {} - for name, attr in (("host", "HOST"), ("api_key", "KEY")): - value = getattr(legacy_config, attr, "") - if isinstance(value, str) and value.strip(): - layer[name] = Setting(value.strip(), "legacy config.py") - return layer - - def _read_config_file(path: Path) -> dict[str, Any] | None: if not path.is_file(): return None @@ -364,22 +347,20 @@ def _check_ca_bundle(setting: Setting) -> Setting: return setting -def _warn_deprecated(settings: Settings) -> None: - sources = { - setting.source - for setting in (settings.host, settings.api_key) - if setting.source.startswith("legacy") - } - if "legacy env HOST" in sources or "legacy env KEY" in sources: - logger.warning( - "The HOST and KEY environment variables are deprecated; use " - "CLOUDEXIT_HOST and CLOUDEXIT_API_KEY_FILE (or CLOUDEXIT_API_KEY)." - ) - if "legacy config.py" in sources: - logger.warning( - "config.py is deprecated; move HOST and KEY to the config file " - "(see `cloudexit config path`) or to CLOUDEXIT_* environment variables." - ) +def _warn_legacy_sources(settings: Settings, environ: Mapping[str, str]) -> None: + # Only for users still relying on the sources 2.0 dropped, so it stays quiet + # once anything else supplies a host or a key. Presence is all that is + # checked: config.py is never imported, opened or parsed, and the values of + # HOST and KEY are never logged or used. + if settings.host.value or settings.api_key.value: + return + notes = [] + if environ.get("HOST", "").strip() and environ.get("KEY", "").strip(): + notes.append(LEGACY_ENV_WARNING) + if Path("config.py").is_file(): + notes.append(LEGACY_CONFIG_WARNING) + if notes: + logger.warning(" ".join(notes)) def load_settings( @@ -404,12 +385,7 @@ def load_settings( else: file_layers = _file_layers(data, path, profile.value) - layers = [ - *top, - *file_layers, - _legacy_env_layer(environ), - _legacy_config_layer(), - ] + layers = [*top, *file_layers] api_key, api_key_file = _resolve_key(layers) host = _first(layers, "host") @@ -441,7 +417,7 @@ def load_settings( config_path=path, config_found=data is not None, ) - _warn_deprecated(settings) + _warn_legacy_sources(settings, environ) return settings diff --git a/cloudexit/terminal.py b/cloudexit/terminal.py index 60cf2f3..101b84e 100644 --- a/cloudexit/terminal.py +++ b/cloudexit/terminal.py @@ -2,9 +2,7 @@ import getpass import sys from rich.console import Console -from rich.markup import escape from rich.style import Style -from time import sleep from cloudexit import codes @@ -45,14 +43,8 @@ def print_step( "pending": "[ ... ]", } - # Handle the pending status with a spinner if status == "pending": - with console.status( - f"{escape(description):<50} [yellow]{status_map['pending']}[/yellow]", - spinner="dots", - ): - sleep(2) - print_step(description, status="ok") + print_step(description, status="ok") elif status == "ok": console.print( f"{description:<50} {status_map['ok']}", diff --git a/config.example.toml b/config.example.toml index 6956c69..d9c1aef 100644 --- a/config.example.toml +++ b/config.example.toml @@ -9,8 +9,6 @@ # 1. command-line flags (--host, --api-key-file, --config-profile, ...) # 2. CLOUDEXIT_* environment variables # 3. this file ([profiles.] first, then [default]) -# 4. deprecated: the HOST and KEY environment variables (only when both are -# set) and config.py # Run `cloudexit config show` to see each value and where it came from. [default] diff --git a/tests/cli/test_prompts.py b/tests/cli/test_prompts.py index 62c293d..cd7b935 100644 --- a/tests/cli/test_prompts.py +++ b/tests/cli/test_prompts.py @@ -212,3 +212,19 @@ def test_the_stdin_helper_works_with_an_in_memory_stream(self): if __name__ == "__main__": unittest.main() + + +class AssessmentTypePromptTests(unittest.TestCase): + def test_the_type_is_asked_when_an_upload_will_happen(self): + with patch("cloudexit.cli.prompts.ask", side_effect=["1", "2"]) as asked: + self.assertEqual(prompt_required_inputs(True), (1, 2)) + + self.assertEqual(len(asked.call_args_list), 2) + self.assertIn("Assessment Type", str(asked.call_args_list)) + + def test_without_an_upload_the_type_is_basic_and_never_asked(self): + with patch("cloudexit.cli.prompts.ask", side_effect=["3"]) as asked: + self.assertEqual(prompt_required_inputs(False), (3, 1)) + + self.assertEqual(len(asked.call_args_list), 1) + self.assertNotIn("Assessment Type", str(asked.call_args_list)) diff --git a/tests/cli/test_stub_platform.py b/tests/cli/test_stub_platform.py index 5751941..8bf096a 100644 --- a/tests/cli/test_stub_platform.py +++ b/tests/cli/test_stub_platform.py @@ -193,7 +193,7 @@ def test_off_sends_nothing_even_with_a_key(self): self.assertEqual(platform.requests, []) self.assertEqual(summary["upload"]["status"], "skipped") self.assertEqual(summary["upload"]["reason"], "off") - self.assertIn("Forcing Basic Assessment: uploads are off.", result.stderr) + self.assertIn("Basic assessment: Standard needs an upload.", result.stderr) self.assertEqual(len(self.reports()), 1) def test_the_mode_can_come_from_the_environment(self): @@ -222,7 +222,7 @@ def test_auto_without_a_platform_runs_offline(self): self.assertEqual(summary["upload"]["status"], "skipped") self.assertEqual(summary["upload"]["reason"], "offline") self.assertIn( - "Forcing Basic Assessment: no platform is configured.", + "Basic assessment: Standard needs an upload.", " ".join(result.stderr.split()), ) diff --git a/tests/cli/test_upload_modes.py b/tests/cli/test_upload_modes.py index aa3d4f7..dacf7dd 100644 --- a/tests/cli/test_upload_modes.py +++ b/tests/cli/test_upload_modes.py @@ -2,6 +2,7 @@ import json import sys import unittest +from argparse import Namespace from contextlib import redirect_stderr from pathlib import Path from unittest.mock import patch @@ -24,7 +25,8 @@ ) from tests.report import test_report_flow as flow from cloudexit import codes -from cloudexit.settings import Setting, Settings +from cloudexit.settings import Setting, Settings, load_settings +from tests.support.isolation import isolated_environ OUTDATED = { "code": "collector_catalogue_outdated", @@ -51,7 +53,7 @@ def messages(self): return [args[0] for args, _ in self.steps] def forced(self): - return [m for m in self.messages() if m.startswith("Forcing Basic")] + return [m for m in self.messages() if m.startswith("Basic assessment")] def summary(self, exit_code=0): out = io.StringIO() @@ -67,9 +69,28 @@ def test_auto_without_a_platform_runs_offline_and_succeeds(self): mock_upload.assert_not_called() self.assert_local_report(report) - self.assertEqual( - self.forced(), ["Forcing Basic Assessment: no platform is configured."] - ) + self.assertEqual(self.forced(), ["Basic assessment: Standard needs an upload."]) + + def test_the_legacy_host_and_key_environment_variables_upload_nothing(self): + report = self.report() + environ = { + **isolated_environ(self.tmp), + "HOST": "platform.test", + "KEY": "fixture-key-0123456789", + } + with self.assertLogs("cloudexit.settings", level="WARNING") as logs: + settings = load_settings(None, environ=environ) + + self.assertIsNone(settings.host.value) + self.assertIsNone(settings.api_key.value) + self.assertNotIn("fixture-key-0123456789", logs.output[0]) + + with patch("cloudexit.cli.app.upload") as mock_upload: + self.run_assessment(report, settings=settings, assessment_type=2) + + mock_upload.assert_not_called() + self.assert_local_report(report) + self.assertEqual(self.sync(report)["assessment_type"], 1) def test_auto_with_a_platform_uploads(self): report = self.report() @@ -129,7 +150,7 @@ def test_off_with_a_platform_makes_no_request_and_forces_basic(self): mock_upload.assert_not_called() self.assert_local_report(report) self.assertEqual(self.sync(report)["assessment_type"], 1) - self.assertEqual(self.forced(), ["Forcing Basic Assessment: uploads are off."]) + self.assertEqual(self.forced(), ["Basic assessment: Standard needs an upload."]) self.assertFalse(self.raw(report, "upload.json").exists()) def test_off_does_not_even_check_a_key_that_cannot_be_right(self): @@ -586,3 +607,91 @@ def dispatch(args): if __name__ == "__main__": unittest.main() + + +class TerminalOutputTests(ModeTestCase): + # Nothing in the terminal sells an edition, a contact or a framework + MARKETING = ( + "regulatory-aligned", + "DORA", + "FINMA", + "PRA", + "request_report", + "ExitCloud", + "Light", + "Enterprise", + ) + + def steps_starting(self, prefix): + return [(a, k) for a, k in self.steps if a and a[0].startswith(prefix)] + + def test_an_offline_run_says_what_it_did_and_sells_nothing(self): + report = self.report() + self.run_assessment(report, settings=Settings(), real_terminal=True) + text = self.output.getvalue() + + self.assertIn("Offline run: no platform configured", text) + self.assertIn("Outputs:", text) + for banned in self.MARKETING: + with self.subTest(banned): + self.assertNotIn(banned, text) + + def test_an_offline_run_is_not_a_warning(self): + report = self.report() + self.run_assessment(report, settings=Settings(), assessment_type=1) + + offline = self.steps_starting("Offline run:") + self.assertEqual(len(offline), 1) + self.assertEqual(offline[0][1]["status"], "ok") + self.assertEqual( + [k for _a, k in self.steps if k.get("status") == "warning"], [] + ) + + def test_an_explicit_standard_type_offline_is_reduced_without_a_warning(self): + report = self.report() + self.run_assessment(report, settings=Settings(), assessment_type=2) + + reduced = self.steps_starting("Basic assessment") + self.assertEqual(len(reduced), 1) + self.assertEqual( + reduced[0][0][0], "Basic assessment: Standard needs an upload." + ) + self.assertEqual(reduced[0][1]["status"], "ok") + + def test_a_configured_run_names_the_platform_by_host(self): + report = self.report() + self.run_assessment(report, settings=flow.CONFIGURED, replies=success_replies()) + + self.assertEqual( + [a[0] for a, _k in self.steps_starting("Platform configured")], + ["Platform configured: https://platform.test."], + ) + + def test_the_host_line_carries_no_path_query_or_credentials(self): + for raw, shown in ( + ("https://platform.test", "https://platform.test"), + ("https://platform.test/api/v2?token=abc", "https://platform.test"), + ("https://user:pw@platform.test:8443/x", "https://platform.test:8443"), + ("http://127.0.0.1:8000/path", "http://127.0.0.1:8000"), + ): + with self.subTest(raw): + self.assertEqual(app._host_display(raw), shown) + + +class AssessmentTypeQuestionTests(unittest.TestCase): + def args(self, settings, dry_run=False): + return Namespace(settings=settings, dry_run=dry_run) + + def test_a_configured_platform_that_will_upload_gets_the_question(self): + self.assertTrue(app._asks_assessment_type(self.args(flow.CONFIGURED))) + + def test_an_offline_run_does_not_get_the_question(self): + for settings in (None, Settings(), Settings(host=flow.CONFIGURED.host)): + with self.subTest(settings=settings): + self.assertFalse(app._asks_assessment_type(self.args(settings))) + + def test_upload_off_or_a_dry_run_does_not_get_the_question(self): + self.assertFalse(app._asks_assessment_type(self.args(with_mode("off")))) + self.assertFalse( + app._asks_assessment_type(self.args(flow.CONFIGURED, dry_run=True)) + ) diff --git a/tests/report/test_basic_report.py b/tests/report/test_basic_report.py index 64d3e4d..b56f3c9 100644 --- a/tests/report/test_basic_report.py +++ b/tests/report/test_basic_report.py @@ -998,7 +998,6 @@ def test_the_egress_labels_are_in_the_title_case_list(self): "eg_heading_landscape", "eg_heading_costs", "eg_heading_pricing", - "eg_heading_next_steps", "eg_th_size_gib", "eg_st_largest", ): @@ -1048,7 +1047,7 @@ def test_no_wording_carries_regulatory_words_or_scores(self): for key, text in self.strings(): # The pages that point to the platforms name what they add, and the # Scores row of the HTML report names the results they compute - if key.startswith(("fa_", "eg_next_", "html_scores_", "html_tour_")): + if key.startswith(("fa_", "html_scores_", "html_tour_")): continue with self.subTest(key): self.assertEqual(find_terms(text), []) diff --git a/tests/report/test_egress.py b/tests/report/test_egress.py index 3c9b3d6..ecd11ae 100644 --- a/tests/report/test_egress.py +++ b/tests/report/test_egress.py @@ -6,11 +6,13 @@ from unittest.mock import patch -from cloudexit.egress.model import GIB, write_egress_inventory +from cloudexit.egress.model import GIB, compute_totals, write_egress_inventory from cloudexit.report.helpers import format_gib, gib_number, percent from cloudexit.catalogue.store import active_catalogue_path +from cloudexit.report.wording import BLOCKS from cloudexit.report.egress import ( _build_type_groups, + _summarize_totals, _resolve_icon, build_fee_estimate, calculate_tiered_cost, @@ -457,3 +459,55 @@ def test_no_free_tier_means_no_headroom(self): if __name__ == "__main__": unittest.main() + + +class SummaryDisplayTests(unittest.TestCase): + DASH = BLOCKS["ev_dash"] + + def rows(self, *sizes, flagged=True): + return [ + { + "size_bytes": size, + "size_unknown": size is None and flagged, + "tier_bytes": None, + } + for size in sizes + ] + + def summary(self, rows, total_fee=0.0): + return _summarize_totals(compute_totals(rows, set()), rows, total_fee, [], 1) + + def test_nothing_sized_shows_a_dash_rather_than_a_zero_with_a_plus(self): + summary = self.summary(self.rows(None, None, None)) + + self.assertTrue(summary["nothing_sized"]) + self.assertEqual(summary["total_data_display"], self.DASH) + self.assertEqual(summary["total_fee_display"], self.DASH) + + def test_a_row_with_no_size_and_no_unknown_flag_is_still_not_sized(self): + # One resource type of the egress_unsized sample is shaped this way + rows = self.rows(None, None) + self.rows(None, flagged=False) + summary = self.summary(rows) + + self.assertTrue(summary["nothing_sized"]) + self.assertEqual(summary["total_data_display"], self.DASH) + + def test_partial_sizing_keeps_the_lower_bound_plus(self): + summary = self.summary(self.rows(10 * GIB, None), total_fee=1.5) + + self.assertFalse(summary["nothing_sized"]) + self.assertEqual(summary["total_data_display"], "10.0 GiB+") + self.assertEqual(summary["total_fee_display"], "$1.50+") + + def test_sized_at_exactly_zero_bytes_is_a_figure_not_a_dash(self): + summary = self.summary(self.rows(0)) + + self.assertFalse(summary["nothing_sized"]) + self.assertEqual(summary["total_data_display"], "0.0 GiB") + self.assertEqual(summary["total_fee_display"], "$0.00") + + def test_without_any_data_bearing_resource_nothing_is_claimed_either_way(self): + summary = self.summary([]) + + self.assertFalse(summary["nothing_sized"]) + self.assertEqual(summary["total_data_display"], "0.0 GiB") diff --git a/tests/report/test_egress_report.py b/tests/report/test_egress_report.py index 85e1087..884edf4 100644 --- a/tests/report/test_egress_report.py +++ b/tests/report/test_egress_report.py @@ -53,7 +53,6 @@ (0, "Estimated Costs"), (0, "Appendix – Egress Prices"), (1, "Glossary"), - (0, "Next Steps"), (0, "Legal Notices"), ] MULTI_TIB = [ @@ -326,15 +325,28 @@ def test_the_appendix_names_the_prices_the_sources_and_the_units(self): ): self.assertIn(expected, text) - def test_next_steps_name_the_companion_and_the_platforms(self): - text = self.text(MULTI_TIB, prov=provenance()) - self.assertIn(f"Executive Summary of the same run ({ES_ID})", text) - self.assertIn("Connected (https://exitcloud.io)", text) - self.assertIn("Enterprise (https://escapecloud.io/book-demo/)", text) - - text = self.text(MULTI_TIB, prov=uploaded()) - self.assertIn(f"Open the reports: {URL}", text) - self.assertNotIn("Connected (https://exitcloud.io)", text) + def test_nothing_promotes_a_platform_or_an_edition(self): + for name, prov in (("offline", provenance()), ("uploaded", uploaded())): + with self.subTest(name): + text = self.text(MULTI_TIB, prov=prov) + for banned in ( + "Next Steps", + "exitcloud.io", + "book-demo", + "book a demo", + "Connected", + "Enterprise", + ): + self.assertNotIn(banned, text) + + def test_page_two_still_carries_the_companion_and_the_platform_link(self): + _path, _context, reader = self.render(MULTI_TIB, prov=provenance()) + self.assertIn(f"Companion Document {ES_ID}", page_texts(reader)[1]) + + _path, _context, reader = self.render(MULTI_TIB, prov=uploaded()) + page_two = page_texts(reader)[1] + self.assertIn(f"Companion Document {ES_ID}", page_two) + self.assertIn(URL, page_two) def test_no_date_is_written_month_first(self): for name, provider, prov in self.scenarios(): diff --git a/tests/report/test_report_flow.py b/tests/report/test_report_flow.py index 3e799a6..32ea635 100644 --- a/tests/report/test_report_flow.py +++ b/tests/report/test_report_flow.py @@ -526,7 +526,7 @@ def test_an_offline_run_forces_basic_and_never_uploads(self): LOCAL_RISKS, ) messages = [args[0] for args, _ in self.steps] - self.assertIn("Forcing Basic Assessment: no platform is configured.", messages) + self.assertIn("Basic assessment: Standard needs an upload.", messages) def test_the_local_results_are_computed_when_connected_too(self): report = self.report() @@ -573,7 +573,7 @@ def test_connected_runs_keep_the_requested_depth_in_the_document(self): # The local report is always the Basic report self.assertEqual(self.json_report(report)["meta"]["assessment_type"], 1) messages = [args[0] for args, _ in self.steps] - self.assertEqual([m for m in messages if m.startswith("Forcing Basic")], []) + self.assertEqual([m for m in messages if m.startswith("Basic assessment")], []) def test_platform_text_with_brackets_is_shown_literally_and_the_run_goes_on(self): message = "rejected [/bold] [red]x[/red] :warning: [1, 2]" @@ -770,7 +770,7 @@ def test_a_key_that_cannot_be_right_is_a_failed_upload_not_an_offline_run(self): # A configured platform keeps the requested depth: nothing forces Basic self.assertEqual(self.sync(report)["assessment_type"], 2) messages = [args[0] for args, _ in self.steps] - self.assertEqual([m for m in messages if m.startswith("Forcing Basic")], []) + self.assertEqual([m for m in messages if m.startswith("Basic assessment")], []) def test_basic_is_forced_only_without_platform_configuration(self): for settings in (None, Settings()): diff --git a/tests/samples/test_samples.py b/tests/samples/test_samples.py index 26524b6..a8f459f 100644 --- a/tests/samples/test_samples.py +++ b/tests/samples/test_samples.py @@ -342,17 +342,41 @@ def test_a_state_file_has_no_costs_and_an_unknown_region(self): def test_unsized_egress_is_not_sized_on_every_type(self): text = " ".join(self.pdf_pages("egress_unsized", "egress.pdf")) dump = self.text("egress_unsized", "texts/index-html.txt") + dash = BLOCKS["ev_dash"] self.assertIn("None of the 5 data-bearing resources could be sized.", text) self.assertIn("RESOURCES SIZED 0 of 5", text) self.assertIn("Not sized: Storage Account (2 resources)", text) self.assertIn("SQL Database (1 resource)", text) - # A fee of zero with a +: a lower bound, never a figure - self.assertIn("$0.00+", text) - self.assertIn("0.0 GiB+", text) + # Nothing was measured, so no figure may claim a lower bound of zero + for strip in ("DATA VOLUME", "ESTIMATED FEE (USD)", "BILLABLE VOLUME"): + self.assertIn(f"{strip} {dash}", text) + for kpi in ("Estimated Transfer Fee", "Data Volume", "Billable Volume"): + self.assertIn(f"{kpi} {dash}", dump) + self.assertIn(f"Total {dash} {dash}", dump) + for banned in ("0.0 GiB+", "$0.00+", "0.00+"): + self.assertNotIn(banned, text) + self.assertNotIn(banned, dump) self.assertNotIn("monthly free tier", text) + self.assertNotIn("Free-Tier Headroom", dump) + self.assertNotIn("no free tier in the prices used", dump) self.assertIn("not sized", dump.lower()) + def test_no_egress_pdf_promotes_a_platform_or_an_edition(self): + for case, scenario in self.cases(): + if scenario.egress is None: + continue + with self.subTest(case): + text = " ".join(self.pdf_pages(case, "egress.pdf")) + for banned in ( + "Next Steps", + "exitcloud.io", + "book-demo", + "book a demo", + "Connected", + ): + self.assertNotIn(banned, text) + def test_an_empty_scope_with_costs_has_empty_states_next_to_real_figures(self): text = " ".join(self.pdf_pages("empty_with_costs", "report.pdf")) dump = self.text("empty_with_costs", "texts/index-html.txt") diff --git a/tests/support/egress.py b/tests/support/egress.py index bb2e327..cf334eb 100644 --- a/tests/support/egress.py +++ b/tests/support/egress.py @@ -43,5 +43,5 @@ def empty_estimate(csp=2): "pricing": pricing, "total_fee": 0.0, "type_groups": [], - "summary": _summarize_totals(totals, 0.0, pricing, csp), + "summary": _summarize_totals(totals, [], 0.0, pricing, csp), } diff --git a/tests/test_layering.py b/tests/test_layering.py index 4ec827c..6199e14 100644 --- a/tests/test_layering.py +++ b/tests/test_layering.py @@ -13,6 +13,7 @@ "version": set(), "resources": set(), "private_files": set(), + "log_redaction": set(), "terminal": {"codes"}, "settings": set(), "catalogue": {"settings"}, diff --git a/tests/test_logging.py b/tests/test_logging.py index cb6dc9f..ea8f6cc 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -3,13 +3,66 @@ import logging import tempfile import unittest +from io import StringIO from pathlib import Path +from unittest.mock import patch + +from rich.console import Console from cloudexit.cli import app +from cloudexit.log_redaction import redact from tests.support.paths import ROOT PACKAGE = ROOT / "cloudexit" # Loggers of other libraries that the package sets a level on +THIRD_PARTY = ( + "botocore", + "botocore.auth", + "botocore.credentials", + "botocore.endpoint", + "boto3", + "azure", + "urllib3", + "msal", + "PIL", +) + +ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE" +SIGNATURE = "deadbeefcafe0123456789abcdef0123456789abcdef0123456789abcdef0123" +TOKEN = "FwoGZXIvYXdzEExAMPLESESSIONTOKEN" +SECRET = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + + +class PreparedRequest: + # botocore reaches the logger with the request object as a %s argument, so + # the secret is not in the format string + def __repr__(self): + return ( + "" + ) + + +class LoggingStateMixin: + def setUp(self): + root = logging.getLogger() + saved = (root.level, list(root.handlers)) + levels = {name: logging.getLogger(name).level for name in THIRD_PARTY} + + def restore(): + root.setLevel(saved[0]) + root.handlers[:] = saved[1] + for name, level in levels.items(): + logging.getLogger(name).setLevel(level) + + self.addCleanup(restore) + + def level(self, name): + return logging.getLogger(name).getEffectiveLevel() def module_loggers(path): @@ -61,25 +114,7 @@ def test_the_run_log_names_the_module_that_wrote_each_line(self): self.assertNotIn("core.engine", text) -class ThirdPartyLevelTests(unittest.TestCase): - NAMES = ("botocore", "boto3", "azure", "urllib3", "msal", "PIL") - - def setUp(self): - root = logging.getLogger() - saved = (root.level, list(root.handlers)) - levels = {name: logging.getLogger(name).level for name in self.NAMES} - - def restore(): - root.setLevel(saved[0]) - root.handlers[:] = saved[1] - for name, level in levels.items(): - logging.getLogger(name).setLevel(level) - - self.addCleanup(restore) - - def level(self, name): - return logging.getLogger(name).getEffectiveLevel() - +class ThirdPartyLevelTests(LoggingStateMixin, unittest.TestCase): def test_msal_is_never_below_warning_because_it_logs_the_client_id(self): for verbose in (0, 1, 2, 3): with self.subTest(verbose=verbose): @@ -106,6 +141,77 @@ def test_the_other_libraries_keep_their_levels(self): self.assertEqual(self.level(name), logging.DEBUG, name) self.assertEqual(self.level("PIL"), logging.WARNING) + def test_botocore_signing_and_credentials_are_never_below_warning(self): + for verbose in (0, 1, 2): + with self.subTest(verbose=verbose): + app.configure_logging(verbose) + self.assertEqual(self.level("botocore.auth"), logging.WARNING) + self.assertEqual(self.level("botocore.credentials"), logging.WARNING) + + def test_the_endpoint_logger_still_shows_requests_at_the_deepest_verbosity(self): + app.configure_logging(2) + self.assertEqual(self.level("botocore.endpoint"), logging.DEBUG) + + +class CredentialRedactionTests(LoggingStateMixin, unittest.TestCase): + def emit(self): + buffer = StringIO() + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + with patch.object(app, "console", Console(file=buffer, width=2000)): + app.configure_logging(2) + app.add_run_log_handler(tmp.name) + try: + endpoint = logging.getLogger("botocore.endpoint") + endpoint.debug("Sending http request: %s", PreparedRequest()) + endpoint.debug( + "Making request with headers Authorization: AWS4-HMAC-SHA256 " + f"Credential={ACCESS_KEY}/20261025/eu-west-1/ec2/aws4_request, " + f"SignedHeaders=host;x-amz-date, Signature={SIGNATURE}" + ) + logging.getLogger("botocore.auth").warning( + "CanonicalRequest:\nPOST\n/\n\nx-amz-security-token:%s\n", TOKEN + ) + logging.getLogger("botocore.credentials").warning( + "Found credentials: aws_secret_access_key=%s, " + "aws_session_token=%s", + SECRET, + TOKEN, + ) + finally: + app.close_run_log_handler() + run_log = (Path(tmp.name) / "run.log").read_text(encoding="utf-8") + # The console wraps long lines, so compare without whitespace + return "".join(buffer.getvalue().split()), run_log + + def test_no_credential_reaches_the_console_or_the_run_log(self): + console_text, run_log = self.emit() + for name, text in (("console", console_text), ("run.log", run_log)): + for secret in (ACCESS_KEY, SIGNATURE, TOKEN, SECRET): + with self.subTest(target=name, secret=secret[:8]): + self.assertNotIn(secret, text) + with self.subTest(target=name): + self.assertIn("REDACTED", text) + + def test_the_request_detail_around_the_secret_is_kept(self): + _, run_log = self.emit() + self.assertIn("ec2.eu-west-1.amazonaws.com", run_log) + self.assertIn("botocore.endpoint - DEBUG", run_log) + self.assertIn("'Authorization': REDACTED", run_log) + + def test_a_credential_scope_outside_a_header_keeps_its_shape(self): + # A presigned URL carries the key ID in the query string, where the + # scope that follows it is worth reading + cleaned = redact( + "https://s3.eu-west-1.amazonaws.com/bucket/key?" + f"X-Amz-Credential={ACCESS_KEY}/20261025/eu-west-1/s3/aws4_request" + f"&X-Amz-Signature={SIGNATURE}" + ) + self.assertIn("Credential=REDACTED/20261025/eu-west-1/s3/aws4_request", cleaned) + self.assertIn("X-Amz-Signature=REDACTED", cleaned) + self.assertNotIn(ACCESS_KEY, cleaned) + self.assertNotIn(SIGNATURE, cleaned) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_settings.py b/tests/test_settings.py index e43a40b..4e4c41d 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,8 +1,10 @@ +import ast import contextlib import io import os +import subprocess +import sys import tempfile -import types import unittest from argparse import Namespace from pathlib import Path @@ -25,6 +27,8 @@ LOGGER = "cloudexit.settings" KEY = "sk_live_0123456789abcdef" +LEGACY_HOST = "platform.test" +LEGACY_KEY = "fixture-key-0123456789" class SettingsTestCase(unittest.TestCase): @@ -32,9 +36,6 @@ def setUp(self): tmp = tempfile.TemporaryDirectory() self.addCleanup(tmp.cleanup) self.tmp = Path(tmp.name) - patcher = patch("cloudexit.settings.legacy_config", None) - patcher.start() - self.addCleanup(patcher.stop) @property def config_path(self): @@ -56,10 +57,16 @@ def load(self, flags=None, env=None): environ = {**isolated_environ(self.tmp), **(env or {})} return load_settings(flags, environ=environ) - def legacy(self, **values): - return patch( - "cloudexit.settings.legacy_config", types.SimpleNamespace(**values) + @contextlib.contextmanager + def directory_with_config_py(self): + # A real config.py next to the run, to prove it is never read + legacy = self.tmp / "legacy" + legacy.mkdir(exist_ok=True) + (legacy / "config.py").write_text( + f"HOST = 'old.test'\nKEY = '{LEGACY_KEY}'\n", encoding="utf-8" ) + with contextlib.chdir(legacy): + yield legacy class ConfigFilePathTests(unittest.TestCase): @@ -119,36 +126,18 @@ def test_nothing_configured_is_all_unset(self): self.assertEqual(settings.timeout.source, "default") self.assertFalse(settings.config_found) - def test_host_flag_over_env_over_file_over_legacy(self): + def test_host_flag_over_env_over_file(self): self.write_config('[default]\nhost = "file.test"\n') - env = { - "CLOUDEXIT_HOST": "env.test", - "HOST": "legacy-env.test", - "KEY": "legacy-key", - } - with self.legacy(HOST="legacy-config.test", KEY="legacy-key"): - steps = [ - ( - self.load({"host": "flag.test"}, env), - "https://flag.test", - "flag --host", - ), - (self.load(None, env), "https://env.test", "env CLOUDEXIT_HOST"), - ( - self.load(None, {"HOST": "legacy-env.test", "KEY": "k"}), - "https://file.test", - "config file [default]", - ), - ] - self.config_path.unlink() - steps += [ - ( - self.load(None, {"HOST": "legacy-env.test", "KEY": "k"}), - "https://legacy-env.test", - "legacy env HOST", - ), - (self.load(), "https://legacy-config.test", "legacy config.py"), - ] + env = {"CLOUDEXIT_HOST": "env.test"} + steps = [ + ( + self.load({"host": "flag.test"}, env), + "https://flag.test", + "flag --host", + ), + (self.load(None, env), "https://env.test", "env CLOUDEXIT_HOST"), + (self.load(), "https://file.test", "config file [default]"), + ] for settings, value, source in steps: self.assertEqual(settings.host.value, value) @@ -401,60 +390,123 @@ def test_private_key_file_does_not_warn(self): class LegacySourceTests(SettingsTestCase): - def test_host_and_key_env_vars_are_used_together_with_a_warning(self): - env = {"HOST": "legacy.test", "KEY": "legacy-key-0123456"} + def test_host_and_key_env_vars_are_no_longer_read(self): + env = {"HOST": LEGACY_HOST, "KEY": LEGACY_KEY} with self.assertLogs(LOGGER, level="WARNING") as logs: settings = self.load(None, env) - self.assertEqual(settings.host.value, "https://legacy.test") - self.assertEqual(settings.host.source, "legacy env HOST") - self.assertEqual(settings.api_key.value, "legacy-key-0123456") - self.assertEqual(settings.api_key.source, "legacy env KEY") + self.assertIsNone(settings.host.value) + self.assertIsNone(settings.api_key.value) self.assertEqual(len(logs.output), 1) - self.assertIn("deprecated", logs.output[0]) - self.assertNotIn("legacy-key-0123456", logs.output[0]) + self.assertIn("HOST and KEY are no longer read", logs.output[0]) + self.assertNotIn(LEGACY_HOST, logs.output[0]) + self.assertNotIn(LEGACY_KEY, logs.output[0]) - def test_host_alone_or_key_alone_is_ignored(self): - for env in ({"HOST": "legacy.test"}, {"KEY": "legacy-key-0123456"}): + def test_host_alone_or_key_alone_does_not_warn(self): + for env in ({"HOST": LEGACY_HOST}, {"KEY": LEGACY_KEY}): with self.assertNoLogs(LOGGER, level="WARNING"): settings = self.load(None, env) self.assertIsNone(settings.host.value) self.assertIsNone(settings.api_key.value) - def test_config_py_is_used_with_a_warning(self): - with ( - self.legacy(HOST="old.test", KEY="old-key-0123456789"), - self.assertLogs(LOGGER, level="WARNING") as logs, - ): - settings = self.load() + def test_cloudexit_env_vars_are_used_and_do_not_warn(self): + env = { + "HOST": LEGACY_HOST, + "KEY": LEGACY_KEY, + "CLOUDEXIT_HOST": "env.test", + "CLOUDEXIT_API_KEY": "inline-key-123456", + } + with self.assertNoLogs(LOGGER, level="WARNING"): + settings = self.load(None, env) - self.assertEqual(settings.host.value, "https://old.test") - self.assertEqual(settings.host.source, "legacy config.py") - self.assertEqual(settings.api_key.source, "legacy config.py") - self.assertEqual(len(logs.output), 1) - self.assertIn("config.py is deprecated", logs.output[0]) - self.assertNotIn("old-key-0123456789", logs.output[0]) + self.assertEqual(settings.host.value, "https://env.test") + self.assertEqual(settings.host.source, "env CLOUDEXIT_HOST") + self.assertEqual(settings.api_key.source, "env CLOUDEXIT_API_KEY") - def test_env_pair_beats_config_py(self): - env = {"HOST": "legacy.test", "KEY": "legacy-key-0123456"} - with self.legacy(HOST="old.test", KEY="old-key"): - with self.assertLogs(LOGGER, level="WARNING"): - settings = self.load(None, env) - self.assertEqual(settings.host.source, "legacy env HOST") + def test_a_config_py_in_the_working_directory_is_no_longer_read(self): + with self.directory_with_config_py(): + with self.assertLogs(LOGGER, level="WARNING") as logs: + settings = self.load() - def test_no_warning_when_a_higher_source_covers_everything(self): - with self.legacy(HOST="old.test", KEY="old-key-0123456789"): + self.assertIsNone(settings.host.value) + self.assertIsNone(settings.api_key.value) + self.assertEqual(len(logs.output), 1) + self.assertIn("config.py is no longer read", logs.output[0]) + self.assertNotIn(LEGACY_KEY, logs.output[0]) + + def test_a_host_from_another_source_silences_the_config_py_warning(self): + with self.directory_with_config_py(): with self.assertNoLogs(LOGGER, level="WARNING"): - settings = self.load( - {"host": "flag.test"}, {"CLOUDEXIT_API_KEY": "inline-key-123456"} - ) - self.assertEqual(settings.host.source, "flag --host") + settings = self.load(None, {"CLOUDEXIT_HOST": "env.test"}) + self.assertEqual(settings.host.source, "env CLOUDEXIT_HOST") - def test_an_empty_config_py_or_a_namespace_package_is_ignored(self): - for legacy in (types.SimpleNamespace(), types.SimpleNamespace(HOST="", KEY="")): - with self.legacy(**vars(legacy)): - settings = self.load() - self.assertIsNone(settings.host.value) + def test_both_legacy_sources_give_a_single_warning(self): + env = {"HOST": LEGACY_HOST, "KEY": LEGACY_KEY} + with self.directory_with_config_py(): + with self.assertLogs(LOGGER, level="WARNING") as logs: + self.load(None, env) + + self.assertEqual(len(logs.output), 1) + self.assertIn("HOST and KEY are no longer read", logs.output[0]) + self.assertIn("config.py is no longer read", logs.output[0]) + + def test_no_module_in_the_package_imports_config(self): + offenders = [] + for path in Path("cloudexit").rglob("*.py"): + tree = ast.parse(path.read_text(encoding="utf-8")) + for node in ast.walk(tree): + names = [] + if isinstance(node, ast.Import): + names = [alias.name for alias in node.names] + elif isinstance(node, ast.ImportFrom) and node.level == 0: + names = [node.module or ""] + if any(name.split(".")[0] == "config" for name in names): + offenders.append(str(path)) + self.assertEqual(offenders, []) + + +class LegacyConfigPySubprocessTests(unittest.TestCase): + # `python -m cloudexit` puts the current directory first on sys.path, so a + # config.py there used to be executed at start-up + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.tmp = Path(tmp.name) + self.marker = self.tmp / "config-py-was-executed" + (self.tmp / "config.py").write_text( + "from pathlib import Path\n" + f"Path({str(self.marker)!r}).write_text('executed')\n" + "HOST = 'old.test'\n" + f"KEY = {LEGACY_KEY!r}\n", + encoding="utf-8", + ) + + def run_cloudexit(self, *args): + root = Path(__file__).resolve().parent.parent + env = {**os.environ, "PYTHONPATH": str(root)} + for name in ("HOST", "KEY", "CLOUDEXIT_HOST", "CLOUDEXIT_API_KEY"): + env.pop(name, None) + return subprocess.run( + [sys.executable, "-m", "cloudexit", *args], + cwd=self.tmp, + env=env, + capture_output=True, + text=True, + ) + + def test_version_does_not_execute_config_py(self): + result = self.run_cloudexit("--version") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse(self.marker.exists()) + + def test_config_show_lists_no_legacy_source(self): + result = self.run_cloudexit("config", "show") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse(self.marker.exists()) + out = result.stdout + result.stderr + self.assertNotIn("legacy", out) + self.assertNotIn("old.test", out) + self.assertNotIn(LEGACY_KEY, out) class DescribeTests(SettingsTestCase): From 3716c2d5a898b60d80f3e69e6eff52c2cb37e7ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Sun, 25 Oct 2026 16:05:25 +0100 Subject: [PATCH 59/61] offer the hybrid cloud exit strategy in the prompt and in ESC_EXIT_STRATEGY --- cloudexit/cli/app.py | 4 ++-- cloudexit/cli/parser.py | 4 ++-- cloudexit/cli/prompts.py | 9 ++++++--- tests/cli/test_app.py | 34 ++++++++++++++++++++++++++++++---- tests/cli/test_prompts.py | 27 +++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 11 deletions(-) diff --git a/cloudexit/cli/app.py b/cloudexit/cli/app.py index d6944c6..7d6f829 100644 --- a/cloudexit/cli/app.py +++ b/cloudexit/cli/app.py @@ -361,7 +361,7 @@ def handle_aws(args): if args.non_interactive: exit_strategy = require_env_int( - "ESC_EXIT_STRATEGY", "exit strategy (1 or 3)", {1, 3} + "ESC_EXIT_STRATEGY", "exit strategy (1, 2 or 3)", {1, 2, 3} ) assessment_type = require_env_int( "ESC_ASSESSMENT_TYPE", "assessment type (1 or 2)", {1, 2} @@ -611,7 +611,7 @@ def handle_azure(args): if args.non_interactive: exit_strategy = require_env_int( - "ESC_EXIT_STRATEGY", "exit strategy (1 or 3)", {1, 3} + "ESC_EXIT_STRATEGY", "exit strategy (1, 2 or 3)", {1, 2, 3} ) assessment_type = require_env_int( "ESC_ASSESSMENT_TYPE", "assessment type (1 or 2)", {1, 2} diff --git a/cloudexit/cli/parser.py b/cloudexit/cli/parser.py index 96b6b74..6a31c7c 100644 --- a/cloudexit/cli/parser.py +++ b/cloudexit/cli/parser.py @@ -186,7 +186,7 @@ def parse_arguments(): action="store_true", help=( "Run without prompts; read all inputs from environment variables " - "(for CI use), for example ESC_EXIT_STRATEGY (1 or 3) and " + "(for CI use), for example ESC_EXIT_STRATEGY (1, 2 or 3) and " "ESC_ASSESSMENT_TYPE (1 or 2)." ), ) @@ -246,7 +246,7 @@ def parse_arguments(): action="store_true", help=( "Run without prompts; read all inputs from environment variables " - "(for CI use), for example ESC_EXIT_STRATEGY (1 or 3) and " + "(for CI use), for example ESC_EXIT_STRATEGY (1, 2 or 3) and " "ESC_ASSESSMENT_TYPE (1 or 2)." ), ) diff --git a/cloudexit/cli/prompts.py b/cloudexit/cli/prompts.py index 35f1079..8807ff5 100644 --- a/cloudexit/cli/prompts.py +++ b/cloudexit/cli/prompts.py @@ -11,17 +11,20 @@ def prompt_required_inputs(ask_assessment_type: bool = True) -> tuple[int, int]: try: exit_strategy = int( ask( - "Enter Exit Strategy (1 for 'Repatriation to On-Premises', 3 for 'Migration to Alternate Cloud'): " + "Enter Exit Strategy (1 for 'Repatriation to On-Premises', 2 for 'Hybrid Cloud Adoption', 3 for 'Migration to Alternate Cloud'): " ).strip() ) - if exit_strategy not in [1, 3]: + if exit_strategy not in [1, 2, 3]: raise ValueError("Invalid exit strategy.") # logger.info(f"Exit Strategy selected: {exit_strategy}") break except ValueError as e: logger.debug(f"Invalid exit strategy input: {e}") console.print( - f"{e} Please enter 1 or 3.", style="red", markup=False, emoji=False + f"{e} Please enter 1, 2 or 3.", + style="red", + markup=False, + emoji=False, ) # Standard only exists on a platform, so without an upload there is diff --git a/tests/cli/test_app.py b/tests/cli/test_app.py index 0af07c8..5b12a55 100644 --- a/tests/cli/test_app.py +++ b/tests/cli/test_app.py @@ -510,14 +510,28 @@ def test_missing_exit_strategy_exits_config(self): self.assertEqual(ctx.exception.code, codes.CONFIG) def test_invalid_exit_strategy_exits_config(self): - env = {**self._BASE_ENV, "ESC_EXIT_STRATEGY": "9"} + for value in ("4", "9"): + with self.subTest(value=value): + env = {**self._BASE_ENV, "ESC_EXIT_STRATEGY": value} + with ( + patch.dict(os.environ, env, clear=False), + patch("cloudexit.cli.app.console.print"), + ): + with self.assertRaises(SystemExit) as ctx: + app.handle_aws(_ni_aws_args()) + self.assertEqual(ctx.exception.code, codes.CONFIG) + + def test_the_hybrid_exit_strategy_reaches_the_assessment(self): + env = {**self._BASE_ENV, "ESC_EXIT_STRATEGY": "2"} with ( patch.dict(os.environ, env, clear=False), + patch("cloudexit.cli.app.validate_region"), + patch("cloudexit.cli.app.run_assessment") as mock_run, patch("cloudexit.cli.app.console.print"), ): - with self.assertRaises(SystemExit) as ctx: - app.handle_aws(_ni_aws_args()) - self.assertEqual(ctx.exception.code, codes.CONFIG) + app.handle_aws(_ni_aws_args()) + + self.assertEqual(mock_run.call_args[0][0]["exitStrategy"], 2) def test_missing_aws_region_exits_config(self): env = {k: v for k, v in self._BASE_ENV.items() if k != "AWS_DEFAULT_REGION"} @@ -560,6 +574,18 @@ def test_builds_config_from_env_and_calls_run_assessment(self): self.assertEqual(config_arg["providerDetails"]["subscriptionId"], "sub-id-789") self.assertEqual(config_arg["providerDetails"]["resourceGroupName"], "my-rg") + def test_the_hybrid_exit_strategy_reaches_the_assessment(self): + env = {**self._BASE_ENV, "ESC_EXIT_STRATEGY": "2"} + with ( + patch.dict(os.environ, env, clear=False), + patch("azure.identity.ClientSecretCredential", return_value=MagicMock()), + patch("cloudexit.cli.app.run_assessment") as mock_run, + patch("cloudexit.cli.app.console.print"), + ): + app.handle_azure(_ni_azure_args()) + + self.assertEqual(mock_run.call_args[0][0]["exitStrategy"], 2) + def test_uses_default_credential_when_client_secret_missing(self): env = {k: v for k, v in self._BASE_ENV.items() if k != "AZURE_CLIENT_SECRET"} mock_credential = MagicMock() diff --git a/tests/cli/test_prompts.py b/tests/cli/test_prompts.py index cd7b935..bfde679 100644 --- a/tests/cli/test_prompts.py +++ b/tests/cli/test_prompts.py @@ -228,3 +228,30 @@ def test_without_an_upload_the_type_is_basic_and_never_asked(self): self.assertEqual(len(asked.call_args_list), 1) self.assertNotIn("Assessment Type", str(asked.call_args_list)) + + +class ExitStrategyPromptTests(unittest.TestCase): + def test_the_hybrid_strategy_can_be_chosen(self): + with patch("cloudexit.cli.prompts.ask", side_effect=["2", "2"]): + self.assertEqual(prompt_required_inputs(True), (2, 2)) + + def test_the_prompt_names_all_three_strategies(self): + with patch("cloudexit.cli.prompts.ask", side_effect=["1"]) as asked: + prompt_required_inputs(False) + + question = asked.call_args_list[0][0][0] + for label in ( + "1 for 'Repatriation to On-Premises'", + "2 for 'Hybrid Cloud Adoption'", + "3 for 'Migration to Alternate Cloud'", + ): + self.assertIn(label, question) + + def test_an_out_of_range_strategy_is_refused_and_asked_again(self): + with ( + patch("cloudexit.cli.prompts.ask", side_effect=["4", "2"]), + patch("cloudexit.cli.prompts.console.print") as printed, + ): + self.assertEqual(prompt_required_inputs(False), (2, 1)) + + self.assertIn("Please enter 1, 2 or 3.", str(printed.call_args_list)) From c296039e64dc642ff39abee07da1e2f98fbf4d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Sun, 25 Oct 2026 16:49:44 +0100 Subject: [PATCH 60/61] move the platform reference to the docs site and add the 2.0 docs --- CHANGELOG.md | 146 ++++++++++++++++++++ README.md | 187 ++++++-------------------- config/aws_example.json | 2 +- config/azure_example.json | 4 +- docs/images/AWS_Config.png | Bin 811241 -> 0 bytes docs/images/AWS_Profile.png | Bin 789478 -> 0 bytes docs/images/Azure_CLI.png | Bin 663319 -> 0 bytes docs/images/Azure_Config.png | Bin 818154 -> 0 bytes docs/images/Main.png | Bin 617904 -> 0 bytes docs/images/Report.png | Bin 894468 -> 0 bytes docs/migration-2.0.md | 254 +++++++++++++++++++++++++++++++++++ publiccode.yml | 20 ++- tests/test_docs.py | 206 ++++++++++++++++++++++++++++ 13 files changed, 666 insertions(+), 153 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 docs/images/AWS_Config.png delete mode 100644 docs/images/AWS_Profile.png delete mode 100644 docs/images/Azure_CLI.png delete mode 100644 docs/images/Azure_Config.png delete mode 100644 docs/images/Main.png delete mode 100644 docs/images/Report.png create mode 100644 docs/migration-2.0.md create mode 100644 tests/test_docs.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cd3af69 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,146 @@ +# Changelog + +All notable changes to cloudexit are recorded here. The format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and cloudexit uses +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +The public contract is the `cloudexit` command, `python -m cloudexit` and +`cloudexit.cli:main`. Everything under `cloudexit.*` is internal and may move in +any 2.x release. + +## [2.0.0] - unreleased + +Upgrading from 1.x? Follow the [migration guide](./docs/migration-2.0.md). + +### Breaking + +- `config.py` is no longer read. Settings come from flags, `CLOUDEXIT_*` + environment variables or `~/.config/cloudexit/config.toml`. Under + `python -m cloudexit` the current directory is first on `sys.path`, so a + `config.py` there used to be imported and executed at start-up; it no longer + is. When one is present and nothing else supplies a host or a key, the run + logs a single warning saying where to move the values. The file is never + imported, opened or parsed. +- The `HOST` and `KEY` environment variables are no longer read. Use + `CLOUDEXIT_HOST` and `CLOUDEXIT_API_KEY_FILE` (or `CLOUDEXIT_API_KEY`). The + same single warning covers them; their values are never read or logged. +- A run that relied on either source now runs offline, or fails with a + configuration error under `--upload required`. +- Platform uploads use API v2. A 1.x platform key does not work; create a new + assessment key on the assessment page. +- The sync document is `raw_data/sync.json` in the shape defined by + [`docs/api-v2/`](./docs/api-v2/), replacing 1.x `payload.json`. +- Exit codes 10–15 are reserved for upload outcomes and 130 for Ctrl+C, so a + pipeline that treated any non-zero exit as a collection failure needs + updating. See + [Exit codes](https://cloudexit.escapecloud.io/reference/exit-codes.html). +- The catalogue dataset lives in the user data directory and updates itself. A + `./datasets/data.db` from 1.x is no longer picked up implicitly; install it + with `cloudexit catalogue import datasets/data.db`. +- The package is `cloudexit/`. 1.x `core.*` and `utils.*` imports are gone. +- There is no `egress.html`. The egress section is part of `index.html`, with + `egress.pdf` as its printable form. + +### Added + +- `--upload auto|required|off` (also `CLOUDEXIT_UPLOAD` and `upload` in the + config file), so a pipeline can insist that an upload happens rather than + falling back to an offline run. +- A config file with profiles: `[default]` and `[profiles.]`, selected + with `--config-profile` or `CLOUDEXIT_PROFILE`. Template: + [`config.example.toml`](./config.example.toml). +- Key files: `--api-key-file`, `CLOUDEXIT_API_KEY_FILE` and `api_key_file`, with + a warning when the file is readable by group or others. +- `cloudexit status` — the resolved settings with their sources, the TLS chain, + the key (kind, organisation, assessment, scopes, expiry) and the platform's + own `/meta`, plus the spool and a catalogue comparison. `--output json` prints + the same as one JSON object. +- `cloudexit config path` and `cloudexit config show` — where settings are + looked up, and every resolved value with its source. Keys are masked. +- `cloudexit catalogue status|update|import` — inspect, download or install the + catalogue dataset, including on an air-gapped machine (`--sha256` verifies the + file). `--catalogue FILE` uses a file for one run without installing it, and + `CLOUDEXIT_CATALOGUE_URL` points the download at a mirror. +- `cloudexit upload` — upload sync documents, or folders of them, for example + from an air-gapped machine. With an account key it takes `--assessment-id`, or + `--name` and `--exit-strategy`. +- A spool for uploads that could not be delivered, with + `cloudexit spool list|flush|purge`. Retries honour `Retry-After`, carry one + idempotency key per sync so a repeat is never charged twice, and stay within + `--upload-budget` seconds. `--no-flush` leaves earlier syncs alone and + `--spool-on-error` also keeps syncs the platform refused. +- `--emit-payload DIR` — write a second copy of the sync document for review or + for a later upload. +- `--strict` — fail the run when the platform reports warnings, after the report + has been written. +- `--output json` — one JSON object on stdout at the end of a run, with the step + output on stderr. +- `--egress-names plain|hashed` — send egress resource names as they are, or as + HMAC-SHA256 hashes with a per-scope salt that stays on your machine. +- `--ca-bundle` and `CLOUDEXIT_CA_BUNDLE` for private certificate authorities; + `REQUESTS_CA_BUNDLE`, `SSL_CERT_FILE`, `HTTPS_PROXY` and `NO_PROXY` are + honoured. Certificate verification cannot be turned off. +- `--timeout` and `--upload-budget` for slow links. +- Exit strategy 2, Hybrid Cloud Adoption, can be chosen in the interactive + prompt and with `ESC_EXIT_STRATEGY`, matching the JSON config, the catalogue + and the reports. +- This changelog, and the [2.0 migration guide](./docs/migration-2.0.md). + +### Changed + +- `index.html` is one self-contained file. Fonts, icons and styles are bundled + and nothing is fetched from the network, so the report opens on an air-gapped + machine and can be attached to an email. +- `report.pdf` is the Basic Executive Summary, set in IBM Plex, opening with a + cover and a document information page that record the cloudexit version, the + run ID, the catalogue version and hash, and either the platform link or the + statement that the data was self-collected. The same facts are in the PDF + metadata and in a `provenance` object in the JSON report. Both PDFs end with + the Legal Notices. +- The local report is always the Basic report, computed on your machine. What a + platform computes stays on the platform; after an upload the local report + records only the link. +- A failed upload still writes the report, with a short banner, and the run + exits with the upload code once everything else has finished. Collection + failures take precedence over upload codes. +- Platform messages name the host rather than a product, an offline run is + reported as a normal mode instead of a warning, and the assessment type is + only asked when an upload will happen. +- `-vv` no longer writes request signing details or credentials to the console + or to `run.log`: botocore's signing and credential loggers stay quiet, and a + filter on both handlers redacts `Authorization` values, credentials, session + tokens and signatures. +- Egress figures that leave unsized resources out are marked with a `+`, and an + estimate where nothing could be sized shows `–` rather than a zero. +- A half-configured platform (a host without a key, or the reverse) is a + configuration error instead of a silent offline run. +- Loggers are named after their module, so `run.log` says which part of the CLI + wrote each line. +- Dependencies refreshed; `requires-python` is `>=3.12`. +- Installation is from a release tag with `pip install .`. cloudexit is not + published on PyPI. +- Data Landscape & Egress Estimation and the Infrastructure-as-Code State Scan + are no longer marked alpha. +- The platform, catalogue, upload and report reference moved from the README to + the [documentation site](https://cloudexit.escapecloud.io), so it is written + once. The README keeps a short pointer and a links table. + +### Removed + +- `config.py` and the `HOST` / `KEY` environment variables as settings sources + (see Breaking). +- The end-of-run promotional block in the terminal, and the "Next Steps" page of + the egress PDF. The terminal states what happened and where the outputs are. +- The two-second artificial pause behind the progress spinner. +- `egress.html` as a separate file, and the shared icon assets that were fetched + per run. + +### Deprecated + +- Boolean `upload` values in the config file and the environment. `true` and + `false` still map to `auto` and `off`, but write the mode instead. + +## Earlier releases + +1.x release notes are on the +[releases page](https://github.com/escapecloud/cloudexit/releases). diff --git a/README.md b/README.md index 19b2f83..42eed00 100644 --- a/README.md +++ b/README.md @@ -38,23 +38,25 @@ It is the open-source foundation of the [EscapeCloud](https://escapecloud.io) ec **Prerequisites:** Python 3.12+, and either an AWS CLI profile or an Azure CLI session. ```bash -git clone git@github.com:escapecloud/cloudexit.git +git clone --branch v2.0.0 https://github.com/escapecloud/cloudexit.git cd cloudexit -python -m venv .venv && source .venv/bin/activate -pip install -r requirements.txt +python3 -m venv .venv && source .venv/bin/activate +pip install . ``` +That installs the `cloudexit` command. To run from the checkout instead, use `pip install -r requirements.txt` and `python3 main.py`. cloudexit is not published on PyPI. + Upgrading from 1.x? Install into a new virtual environment, not over the old one. Run your first assessment: ```bash # AWS — using your existing CLI profile -python main.py aws --profile default +python3 main.py aws --profile default # Azure — using your Azure CLI session az login -python main.py azure --cli +python3 main.py azure --cli ``` Reports are written locally as HTML, PDF, and JSON. Full credential and permissions setup is covered in the [documentation](https://cloudexit.escapecloud.io). @@ -68,7 +70,7 @@ cloudexit produces a structured exit readiness report covering: - **Risk Assessment** — rule-based evaluation of exit feasibility across each identified service - **Alternative Technologies** — open-source and portable replacements for each identified cloud service, with additional details such as company ownership and location -![Report](./docs/images/Report.png) +The [documentation](https://cloudexit.escapecloud.io/reports/overview.html) shows an example report. ## Cloud Providers @@ -89,170 +91,61 @@ The CLI supports multiple input modes for both AWS and Azure. | Mode | Command | |---|---| -| Interactive (manual credentials) | `python main.py aws` | -| AWS CLI profile | `python main.py aws --profile PROFILE` | -| JSON config file | `python main.py aws --config config.json` | -| Terraform/OpenTofu state file | `python main.py aws --tfstate infra.tfstate` | -| Non-interactive (env vars, CI) | `python main.py aws --non-interactive` | +| Interactive (manual credentials) | `python3 main.py aws` | +| AWS CLI profile | `python3 main.py aws --profile PROFILE` | +| JSON config file | `python3 main.py aws --config config.json` | +| Terraform/OpenTofu state file | `python3 main.py aws --tfstate infra.tfstate` | +| Non-interactive (env vars, CI) | `python3 main.py aws --non-interactive` | **Azure** | Mode | Command | |---|---| -| Interactive (service principal) | `python main.py azure` | -| Azure CLI session | `python main.py azure --cli` | -| JSON config file | `python main.py azure --config config.json` | -| Terraform/OpenTofu state file | `python main.py azure --tfstate infra.tfstate` | -| Non-interactive (env vars, CI) | `python main.py azure --non-interactive` | +| Interactive (service principal) | `python3 main.py azure` | +| Azure CLI session | `python3 main.py azure --cli` | +| JSON config file | `python3 main.py azure --config config.json` | +| Terraform/OpenTofu state file | `python3 main.py azure --tfstate infra.tfstate` | +| Non-interactive (env vars, CI) | `python3 main.py azure --non-interactive` | See the [configuration reference](https://cloudexit.escapecloud.io/config/config-schema.html) for required permissions and config file format. -Want to see how a regulatory-aligned report looks (DORA / FINMA / UK PRA)? Run with `--dry-run` and send the output `raw_data/sync.json` to request_report@escapecloud.io — we'll generate a sample you can share with your risk or compliance team. - -## Platform settings - -Connecting to a platform is optional. Without a host and an API key cloudexit runs fully offline. Each value is resolved separately, highest precedence first: - -1. **Flags**: `--host`, `--api-key-file`, `--config-profile`, `--assessment-id`, `--ca-bundle`, `--timeout`, `--upload` -2. **Environment**: `CLOUDEXIT_HOST`, `CLOUDEXIT_API_KEY_FILE`, `CLOUDEXIT_API_KEY`, `CLOUDEXIT_PROFILE`, `CLOUDEXIT_ASSESSMENT_ID`, `CLOUDEXIT_UPLOAD`, `CLOUDEXIT_CA_BUNDLE`, `CLOUDEXIT_TIMEOUT` -3. **Config file**: `~/.config/cloudexit/config.toml` (`$XDG_CONFIG_HOME/cloudexit/config.toml`, or `%APPDATA%\cloudexit\config.toml` on Windows), with a `[default]` table and optional `[profiles.]` tables. See [`config.example.toml`](./config.example.toml). -4. **Deprecated**: the `HOST` and `KEY` environment variables (only when both are set) and `config.py`. Both still work and print a warning. - -Prefer a key file (`--api-key-file`, `CLOUDEXIT_API_KEY_FILE` or `api_key_file`) over a key in plain text, and keep it readable by you only. The CLI warns when the config file or a key file is readable by group or others. `http://` hosts are rejected, except `localhost` and `127.0.0.1`. - -```bash -cloudexit config path # where the config file is looked up -cloudexit config show # every resolved value and its source (keys are masked) -cloudexit status # check the settings, the TLS chain, the key and the platform -``` - -### Upload modes - -`--upload` (also `CLOUDEXIT_UPLOAD` and `upload` in the config file) decides what a run does with a configured platform: - -| Mode | What happens | -| --- | --- | -| `auto` (default) | Uploads when a host and an API key are configured, and runs offline when neither is. If the upload fails, the report is still written and the run exits with the upload code. A host without a key, or a key without a host, is a configuration error (exit 2), never a quiet offline run. | -| `required` | Exits 2 before anything is collected when no host and key are configured. | -| `off` | Never contacts the platform, even with a key configured. The run is the local Basic report ("Forcing Basic Assessment: uploads are off."). | - -`--dry-run` is `--upload=off` plus the sync document in `raw_data/sync.json`; it keeps the assessment depth you asked for, so the document can be uploaded later with `cloudexit upload`. It cannot be combined with `--upload=auto` or `--upload=required` on the command line. "Forcing Basic Assessment" appears only with `--upload=off` or when nothing is configured; a configured platform keeps the depth you asked for. +## Connecting to a platform -`cloudexit status` shows where each setting comes from (keys masked), checks the TLS connection (which CA bundle was used and, on failure, which certificate was not trusted), and asks the platform for its `/meta` (versions, limits, catalogue version, whether this cloudexit version is accepted) and `/whoami` (organisation, key kind, assessment, scopes, expiry, with a warning from 14 days before it). It also lists the spooled syncs and compares the installed catalogue with the platform's. `--output json` prints the same as one JSON object. +Connecting to a platform is optional; without a host and an API key cloudexit runs fully offline. Create an **assessment key** on the assessment's page, then set `CLOUDEXIT_HOST` and `CLOUDEXIT_API_KEY_FILE` — a key file readable by you only, rather than a key in plain text. In CI add `--upload required`, so a missing key fails the job before anything is collected instead of quietly producing an offline report. `cloudexit status` checks the settings, the TLS chain and the key; `cloudexit config show` says where each setting came from. -| Exit code | Meaning | -| --- | --- | -| 0 | Everything checked is fine, or no platform is configured. | -| 11 | A host without a key (or the reverse), or a key the platform does not accept. | -| 12 | The TLS check failed or the platform is unreachable. | -| 15 | The platform does not accept this cloudexit version or the sync document version. | - -## Catalogue dataset - -cloudexit assesses against a catalogue dataset that it keeps up to date. It is stored in your data directory (`~/.local/share/cloudexit/catalogue/`, `$XDG_DATA_HOME/cloudexit/catalogue/`, or `%LOCALAPPDATA%\cloudexit\catalogue\` on Windows) together with the previous version. Each run checks for a newer catalogue first. If that check fails, the run continues with the installed catalogue and says so; without any catalogue it stops. - -```bash -cloudexit catalogue status # installed and latest catalogue (and the platform's, when a key is configured) -cloudexit catalogue update # download the latest (or --version V) -cloudexit catalogue import FILE.db.gz # install a file on an air-gapped machine (--sha256 to verify) -cloudexit aws --tfstate infra.tfstate --catalogue FILE.db.gz # use a file for one run, without installing it -``` - -Set `CLOUDEXIT_CATALOGUE_URL` to download from a mirror instead of the default location. A `./datasets/data.db` from an earlier version is not picked up on its own; install it on purpose with `cloudexit catalogue import datasets/data.db`. A catalogue that holds a trigger or a view is refused, wherever it comes from. - -## Sync document - -Every run writes `raw_data/sync.json` next to the report: the run's result in the shape defined by the API v2 definition in [`docs/api-v2/`](./docs/api-v2/) (a vendored copy; do not edit it here). It holds the scope (an AWS account and region, an Azure subscription, or a Terraform state), the resource inventory, the monthly costs and, when `--egress` ran, the egress estimate. It never contains credentials and is built without a network connection. - -```bash -cloudexit aws --config config.json --emit-payload out/ # also write out/.json -cloudexit aws --config config.json --egress --egress-names hashed # send egress names as salted hashes -``` +> **Changed in 2.0:** `config.py` and the `HOST` / `KEY` environment variables are no longer read. See [Migrating from 1.x](./docs/migration-2.0.md). -With `--egress-names hashed` each egress resource name is replaced by an HMAC-SHA256 with a random per-scope salt that stays on your machine (`~/.local/share/cloudexit/salts/`), so the same name hashes the same way in later runs of the same scope. +## Documentation -## The report - -Every run writes the same three files: `report.pdf`, `index.html` and `raw_data/assessment_result.json`. The PDF is the Basic Executive Summary, set in IBM Plex (SIL Open Font License, in `cloudexit/assets/fonts/`). It opens with a cover and a document information page that record where the data came from: the cloudexit version, the run ID, the data catalogue version and hash, and either a link to the platform's reports or the statement that the data was self-collected. The same facts are in the PDF metadata and in a `provenance` object in the JSON report. The last page says what the full assessment on a platform adds. - -## Reports and uploads - -The local report is always the Basic report, computed on your machine, whether or not a platform is configured. Anything the platform computes stays on the platform: after an upload the local report only records the link (also stored in `raw_data/upload.json`). If an upload fails, the report is still written, with a short "Not uploaded: " banner, and the run exits with an upload code after everything else has finished. Problems in the collection stages take precedence over upload codes. - -`--strict` also fails the run when the platform reports warnings (an outdated catalogue, unknown resource types, a downgraded assessment tier, a deprecated cloudexit version, and so on). The report is written first, then the run exits 10; an upload failure keeps its own code. - -`--output json` makes stdout carry exactly one JSON object when the run ends, and sends the banner and the step output to stderr: - -```json -{ - "exit_code": 0, - "report_dir": "/work/reports/20261102_101500", - "sync_json": "/work/reports/20261102_101500/raw_data/sync.json", - "upload": { - "status": "uploaded", - "reason": null, - "assessment_id": "…", - "sync_id": "…", - "platform_sync_id": "…", - "url": "https://…", - "warnings": [{"code": "collector_catalogue_outdated", "message": "…"}], - "error": null - }, - "results": {"…": "what the platform returned, never shown in the local report"}, - "catalogue": {"version": "2026-11-02", "outdated": false} -} -``` - -`upload.status` is `uploaded`, `spooled`, `skipped` (`reason` is `offline`, `off` or `dry_run`) or `failed`; `error` holds `code`, `message` and `request_id`. `results` is `null` unless the platform answered. The summary never contains the API key. - -| Exit code | Meaning | -| --- | --- | -| 10 | The platform rejected the data, or it is beyond a limit the platform announces. Retrying the same data will not help. | -| 11 | The API key is missing, mistyped, expired, revoked, not allowed to do this, or belongs to another region. | -| 12 | The platform is unreachable, busy or in maintenance. A spooled sync is delivered on the next run, and that run then makes a new assessment of its own: see [Retries and the spool](#retries-and-the-spool). | -| 13 | Credits or a quota ran out. | -| 14 | The assessment named by `--assessment-id` is missing, deleted, archived, or does not match the data. | -| 15 | The platform does not accept this cloudexit version or the sync document version. | - -Before anything is sent the key is checked on your machine (prefix, length and checksum), and the platform's own description of itself (`/meta`, which needs no key) must match the key: a regional ExitCloud key is never sent to a platform that reports another region or product. Host names, addresses and ports do not matter. The key never appears in output or logs. On failure the run prints the platform's error code, its message and a request ID to quote when asking for help. - -### Retries and the spool - -A failed upload is retried when it may pass: connection errors, timeouts, HTTP 408, 425, 429, 500, 502, 503 and 504, and any error the platform marks as retryable. The wait doubles from 1 second up to 60 seconds with random jitter, `Retry-After` is honoured, and every attempt carries the same idempotency key (the sync ID), so a repeat never stores or charges a second time. All attempts together stay within `--upload-budget` seconds (default 300, `CLOUDEXIT_UPLOAD_BUDGET`). Final failures (exit codes 10, 11, 13, 14 and 15) are not retried. - -When the budget runs out on retryable failures the sync is saved to a local spool and the run exits 12 ("spooled; run `cloudexit spool flush`"). `--spool-on-error` also spools syncs the platform refused, for a later manual retry. The spool is `$XDG_STATE_HOME/cloudexit/spool//` (`~/.local/state/...`, or `CLOUDEXIT_SPOOL_DIR`): the sync document and a small file with the attempts and the last error, readable by you only, never holding the API key. The host id is a hash, so the host name is not in the path. Each spooled sync also records a fingerprint of the API key in use when it was spooled (a hash, never the key). - -```bash -cloudexit spool list # id, host id, age, attempts, last error -cloudexit spool flush [--host URL] # upload the spooled syncs, oldest first -cloudexit spool purge [--older-than 30d] [--yes] -cloudexit upload raw_data/sync.json # upload sync documents (or folders of them), for example from an air-gapped machine -``` - -Every run with a platform first delivers the spooled syncs of the same host and the same API key (oldest first, within 60 seconds); `--no-flush` skips that, and a failing flush never blocks the new run. Syncs spooled with another API key are never delivered with this one: they stay in the spool, the run says so, and `cloudexit spool list` marks them (`cloudexit spool purge` removes them). A spooled sync with no recorded key is treated the same way. - -Running again after exit 12 does two things: it delivers the spooled sync under its own sync ID, which the platform stores and charges once however often it is sent, and it then makes a new assessment, which is a new sync with a new sync ID and is charged like any other. Use `--no-flush` to leave the spool alone, or `cloudexit spool flush` to deliver it without a new assessment. A spooled sync older than the platform's `max_collection_age_days` is reported as "will be rejected" and kept until you purge it. For an account key, `upload` needs `--assessment-id`, or `--name` and `--exit-strategy`, to say where the sync goes. - -Connections always verify the platform's certificate. For a private certificate authority use `--ca-bundle` or `CLOUDEXIT_CA_BUNDLE`; otherwise `REQUESTS_CA_BUNDLE` and `SSL_CERT_FILE` are honoured. There is no option to turn verification off. `HTTPS_PROXY` and `NO_PROXY` are honoured. Requests give up after 10 seconds to connect and 60 seconds to read (`--timeout` sets both). The platform's capabilities are cached for five minutes in `~/.cache/cloudexit/` (`$XDG_CACHE_HOME/cloudexit/`, or `%LOCALAPPDATA%\cloudexit\cache\` on Windows). +| Topic | Page | +|---|---| +| Settings, precedence, config file, key files | [Platform settings →](https://cloudexit.escapecloud.io/config/platform-settings.html) | +| Keys, upload modes, the spool, what gets uploaded | [Connected Mode →](https://cloudexit.escapecloud.io/connected-mode/upload-modes.html) | +| Running in CI/CD | [Non-Interactive Mode →](https://cloudexit.escapecloud.io/non-interactive/overview.html) | +| The catalogue dataset | [Catalogue →](https://cloudexit.escapecloud.io/catalogue/overview.html) | +| Exit codes | [Exit codes →](https://cloudexit.escapecloud.io/reference/exit-codes.html) | +| Troubleshooting | [Troubleshooting →](https://cloudexit.escapecloud.io/troubleshooting/common-errors.html) | +| Migrating from 1.x | [docs/migration-2.0.md](./docs/migration-2.0.md) | +| Release history | [CHANGELOG.md](./CHANGELOG.md) | -## Data Landscape & Egress Estimation (alpha) +## Data Landscape & Egress Estimation Add `--egress` to any assessment to measure how much data lives in the assessed scope and estimate the one-time internet egress fee for moving it out — based on the provider's tiered list prices, with no additional permissions required. ```bash -python main.py azure --cli --egress -python main.py aws --profile PROFILE --egress +python3 main.py azure --cli --egress +python3 main.py aws --profile PROFILE --egress ``` See the [egress reference](https://cloudexit.escapecloud.io/egress/overview.html) for details. -## Infrastructure-as-Code State Scan (alpha) +## Infrastructure-as-Code State Scan Instead of connecting to a cloud account, cloudexit can build the assessment from a local **Terraform / OpenTofu state file** — no credentials, no API calls, nothing leaves your machine. ```bash -python main.py aws --tfstate infra.tfstate -python main.py azure --tfstate infra.tfstate +python3 main.py aws --tfstate infra.tfstate +python3 main.py azure --tfstate infra.tfstate ``` If your state lives in a remote backend (S3, Azure Storage, Terraform Cloud, …), export it first: @@ -281,6 +174,8 @@ cloudexit runs headlessly in CI pipelines via `--non-interactive` and environmen [View on GitHub Marketplace →](https://github.com/marketplace/actions/github-action-for-cloudexit) +The Action is at `v1`; a 2.0 version is coming. Until then, pass the 2.0 flags to the CLI directly, as below. + In a pipeline that must upload, add `--upload=required` so a missing key fails the job before anything is collected, `--strict` to fail on platform warnings, and `--output json` to read the result from stdout. ## Ecosystem @@ -308,4 +203,6 @@ The public contract is the `cloudexit` command, `python -m cloudexit` and `cloud © 2024–2026 EscapeCloud. cloudexit is licensed under the **GNU Affero General Public License v3 (AGPL-3.0)**. See [LICENSE](./LICENSE) for details. +A commercial licence is available through the EscapeCloud enterprise package for organizations that cannot use AGPL-licensed software; see [escapecloud.io](https://escapecloud.io). + EscapeCloud, cloudexit and the EscapeCloud logo are trademarks. The AGPL-3.0 licence does not grant any right to use them; modified versions must not use them in a way that suggests they are official. diff --git a/config/aws_example.json b/config/aws_example.json index 1647581..01df95c 100644 --- a/config/aws_example.json +++ b/config/aws_example.json @@ -3,7 +3,7 @@ "exitStrategy": 3, "assessmentType": 1, "providerDetails":{ - "accessKey":"AKIAXASFHMTLKD6YQLHA", + "accessKey":"AKIAIOSFODNN7EXAMPLE", "secretKey":"", "region":"eu-central-1" } diff --git a/config/azure_example.json b/config/azure_example.json index 6c72a09..7081b78 100644 --- a/config/azure_example.json +++ b/config/azure_example.json @@ -3,9 +3,9 @@ "exitStrategy": 3, "assessmentType": 1, "providerDetails":{ - "clientId":"57344955-1579-4058-8604-5bb8724002de", + "clientId":"00000000-0000-0000-0000-000000000000", "clientSecret":"", - "tenantId":"38997009-9dad-42b2-b187-53f1cb71560e", + "tenantId":"00000000-0000-0000-0000-000000000000", "subscriptionId":"", "resourceGroupName":"" } diff --git a/docs/images/AWS_Config.png b/docs/images/AWS_Config.png deleted file mode 100644 index ef10ed9a72570414ebcecaf1e09da6d8f81a1e08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 811241 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 -GI} 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∨VbpVVGShrUJGfqP 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

-eR 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 z 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

r5jqDjPalv2~x4 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Π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 diff --git a/docs/images/AWS_Profile.png b/docs/images/AWS_Profile.png deleted file mode 100644 index 59a4e100497ab1ba1fe62c896638bb7a6719398b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 789478 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 z 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

hd5bB`kB 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

C#ZA7!8uHI?>g^ytX=`P?2fej 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 z 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