diff --git a/DEVELOPER.md b/DEVELOPER.md index a5a2a07a7..f410d7d2d 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -16,13 +16,6 @@ They are exposed here: * Locally: http://127.0.0.1:8099/metrics * Datadog: https://app.datadoghq.com/metric/summary?filter=kubehound.janusgraph - -## MongoDB debug interface - -A mongo express is deployed and allows you to browse the MongoDB. Thi service is accessible (the logs for this docker are not pushed to dd): -* http://127.0.0.1:8081 - - ## Advanced command In case of conflict/error, or just if you want to free some of your RAM, you can use `make system-test-clean` to destroy the backend stack dedicated to the system-test. \ No newline at end of file diff --git a/Makefile b/Makefile index e0ac3db30..646a7ff17 100644 --- a/Makefile +++ b/Makefile @@ -37,9 +37,9 @@ endif ifeq (,$(filter $(SYSTEM_TEST_CMD),$(MAKECMDGOALS))) ifeq (${KUBEHOUND_ENV}, release) - DOCKER_COMPOSE_FILE_PATH += -f deployments/kubehound/docker-compose.release.yaml + DOCKER_COMPOSE_FILE_PATH += -f deployments/kubehound/docker-compose.release.yaml -f deployments/kubehound/docker-compose.ui.yaml else ifeq (${KUBEHOUND_ENV}, dev) - DOCKER_COMPOSE_FILE_PATH += -f deployments/kubehound/docker-compose.dev.yaml + DOCKER_COMPOSE_FILE_PATH += -f deployments/kubehound/docker-compose.dev.yaml -f deployments/kubehound/docker-compose.ui.yaml endif # No API key is being set @@ -87,7 +87,7 @@ endif all: build .PHONY: generate -generate: ## Generate code the application +generate: ## Generate code for the application go generate $(BUILD_FLAGS) ./... .PHONY: build diff --git a/README.md b/README.md index 99d2949f2..862dfdfcd 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,9 @@ A Kubernetes attack graph tool allowing automated calculation of attack paths be - [Infrastructure Setup](#infrastructure-setup) - [Running Kubehound](#running-kubehound) - [Using KubeHound Data](#using-kubehound-data) + - [Example queries](#example-queries) + - [Query data from your scripts](#query-data-from-your-scripts) + - [Python](#python) - [Development](#development) - [Build](#build) - [Release build](#release-build) @@ -133,7 +136,7 @@ Edit the variables (datadog env `DD_*` related and `KUBEHOUND_ENV`): * `DD_API_KEY`: api key you created from https://app.datadoghq.com/ website Note: -* `KUBEHOUND_ENV=dev` will build the images locally (and provide some local debugging containers e.g `mongo-express`) +* `KUBEHOUND_ENV=dev` will build the images locally * `KUBEHOUND_ENV=release` will use prebuilt images from ghcr.io ### Running Kubehound @@ -175,11 +178,12 @@ make help ## Using KubeHound Data -To query the KubeHound graph data requires using the [Gremlin](https://tinkerpop.apache.org/gremlin.html) query language via an API call or dedicated graph query UI. A number of graph query UIs are availble, but we recommend [gdotv](https://gdotv.com/). To access the KubeHound graph using `gdotv`: +To query the KubeHound graph data requires using the [Gremlin](https://tinkerpop.apache.org/gremlin.html) query language via an API call or dedicated graph query UI. A number of fully featured graph query UIs are available (both commercial and open source), but we provide an accompanying Jupyter notebook based on the [AWS Graph Notebook](https://github.com/aws/graph-notebook),to quickly showcase the capabilities of Kubehound. To access the UI: -+ Download and install the application from https://gdotv.com/ -+ Create a connection to the local janusgraph instance by following the steps here https://docs.gdotv.com/connection-management/ and using `hostname=localhost` -+ Navigate to the query editor and enter a sample query e.g `g.V().count()`. See detailed instructions here: https://docs.gdotv.com/query-editor/#run-your-query ++ Visit [http://localhost:8888/notebooks/Kubehound.ipynb](http://localhost:8888/notebooks/Kubehound.ipynb) in your browser ++ Use the default password `admin` to login (note: this can be changed via the [Dockerfile](./deployments/kubehound/notebook/Dockerfile) or by setting the `NOTEBOOK_PASSWORD` environment variable in the [.env](./deployments/kubehound/.env.tpl) file) ++ Follow the initial setup instructions in the notebook to connect to the Kubehound graph and configure the rendering ++ Start running the queries and exploring the graph! ### Example queries diff --git a/deployments/kubehound/docker-compose.datadog.yaml b/deployments/kubehound/docker-compose.datadog.yaml index c5e078d13..af4e85327 100644 --- a/deployments/kubehound/docker-compose.datadog.yaml +++ b/deployments/kubehound/docker-compose.datadog.yaml @@ -20,8 +20,6 @@ services: - DD_DOGSTATSD_NON_LOCAL_TRAFFIC=true - DD_CONTAINER_LABELS_AS_TAGS={"com.docker.compose.service":"+app"} - DD_CONTAINER_INCLUDE=name:kubehound-* - # https://github.com/DataDog/datadog-agent/issues/6599 - - DD_CONTAINER_EXCLUDE=name:kubehound-dev-mongo-express volumes: - /var/run/docker.sock:/var/run/docker.sock - /proc/:/host/proc/:ro diff --git a/deployments/kubehound/docker-compose.dev.yaml b/deployments/kubehound/docker-compose.dev.yaml index 0a9419c10..591f6fdc4 100644 --- a/deployments/kubehound/docker-compose.dev.yaml +++ b/deployments/kubehound/docker-compose.dev.yaml @@ -15,20 +15,6 @@ services: volumes: - kubegraph_data:/var/lib/janusgraph - mongo-express: - image: mongo-express:1.0.0-alpha - profiles: ["infra"] - container_name: ${COMPOSE_PROJECT_NAME}-mongo-express - restart: unless-stopped - depends_on: - - mongodb - ports: - - "127.0.0.1:8081:8081" - networks: - - kubenet - environment: - - ME_CONFIG_MONGODB_SERVER=mongodb - volumes: mongodb_data: kubegraph_data: diff --git a/deployments/kubehound/docker-compose.ui.yaml b/deployments/kubehound/docker-compose.ui.yaml new file mode 100644 index 000000000..399211f4d --- /dev/null +++ b/deployments/kubehound/docker-compose.ui.yaml @@ -0,0 +1,15 @@ +version: "3.8" +services: + notebook: + build: ./notebook/ + restart: unless-stopped + container_name: ${COMPOSE_PROJECT_NAME}-notebook + ports: + - "127.0.0.1:8888:8888" + networks: + - kubenet + volumes: + - ./notebook/shared:/root/notebooks/shared + +networks: + kubenet: \ No newline at end of file diff --git a/deployments/kubehound/kubegraph/Dockerfile b/deployments/kubehound/kubegraph/Dockerfile index 3064b7159..993c7a4c0 100644 --- a/deployments/kubehound/kubegraph/Dockerfile +++ b/deployments/kubehound/kubegraph/Dockerfile @@ -7,7 +7,7 @@ COPY dsl/kubehound/pom.xml /home/app RUN mvn -f /home/app/pom.xml clean install # Now build our janusgraph wrapper container with KubeHound customizations -FROM janusgraph/janusgraph:1.0.0-rc2 +FROM janusgraph/janusgraph:1.0.0 LABEL org.opencontainers.image.source="https://github.com/DataDog/kubehound/" # Add our initialization script for the database schema to the startup directory diff --git a/deployments/kubehound/kubegraph/kubehound-db-init.groovy b/deployments/kubehound/kubegraph/kubehound-db-init.groovy index 229427b3c..a4753ed15 100644 --- a/deployments/kubehound/kubegraph/kubehound-db-init.groovy +++ b/deployments/kubehound/kubegraph/kubehound-db-init.groovy @@ -151,15 +151,13 @@ roleBinding = mgmt.makePropertyKey('roleBinding').dataType(String.class).cardina // Define properties for each vertex -mgmt.addProperties(container, cls, cluster, runID, storeID, app, team, service, isNamespaced, namespace, name, image, privileged, privesc, hostPid, - hostIpc, hostNetwork, runAsUser, podName, nodeName, compromised, command, args, capabilities, ports); +mgmt.addProperties(container, cls, cluster, runID, storeID, app, team, service, isNamespaced, namespace, name, image, privileged, privesc, hostPid, hostIpc, hostNetwork, runAsUser, podName, nodeName, compromised, command, args, capabilities, ports); mgmt.addProperties(identity, cls, cluster, runID, storeID, app, team, service, name, isNamespaced, namespace, type, critical); mgmt.addProperties(node, cls, cluster, runID, storeID, app, team, service, name, isNamespaced, namespace, compromised, critical); mgmt.addProperties(pod, cls, cluster, runID, storeID, app, team, service, name, isNamespaced, namespace, sharedPs, serviceAccount, nodeName, compromised, critical); mgmt.addProperties(permissionSet, cls, cluster, runID, storeID, app, team, service, name, isNamespaced, namespace, role, roleBinding, rules, critical); mgmt.addProperties(volume, cls, cluster, runID, storeID, app, team, service, name, isNamespaced, namespace, type, sourcePath, mountPath, readonly); -mgmt.addProperties(endpoint, cls, cluster, runID, storeID, app, team, service, name, isNamespaced, namespace, serviceEndpoint, serviceDns, addressType, - addresses, port, portName, protocol, exposure, compromised); +mgmt.addProperties(endpoint, cls, cluster, runID, storeID, app, team, service, name, isNamespaced, namespace, serviceEndpoint, serviceDns, addressType, addresses, port, portName, protocol, exposure, compromised); // Create the indexes on vertex properties diff --git a/deployments/kubehound/notebook/BlueTeam.ipynb b/deployments/kubehound/notebook/BlueTeam.ipynb new file mode 100644 index 000000000..e7e6f9d7c --- /dev/null +++ b/deployments/kubehound/notebook/BlueTeam.ipynb @@ -0,0 +1,318 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Blue Team KubeHound Workflow\n", + "\n", + "A step by step example workflow of how to use KubeHound for an incident response scenario." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initial Setup\n", + "\n", + "Connect to the kubegraph server by running the cell below" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%graph_notebook_config\n", + "{\n", + " \"host\": \"host.docker.internal\",\n", + " \"port\": 8182,\n", + " \"ssl\": false,\n", + " \"gremlin\": {\n", + " \"traversal_source\": \"g\",\n", + " \"username\": \"\",\n", + " \"password\": \"\",\n", + " \"message_serializer\": \"graphsonv3\"\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now set the appearance customizations for the notebook. You can see a guide on possible options [here](https://github.com/aws/graph-notebook/blob/623d43827f798c33125219e8f45ad1b6e5b29513/src/graph_notebook/notebooks/01-Neptune-Database/02-Visualization/Grouping-and-Appearance-Customization-Gremlin.ipynb#L680)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%graph_notebook_vis_options\n", + "{\n", + " \"edges\": {\n", + " \"smooth\": {\n", + " \"enabled\": true,\n", + " \"type\": \"dynamic\"\n", + " },\n", + " \"arrows\": {\n", + " \"to\": {\n", + " \"enabled\": true,\n", + " \"type\": \"arrow\"\n", + " }\n", + " }\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Workflow" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Compromised Credentials\n", + "\n", + "Let us consider a scenario where a user's credentials have been compromised. We can use KubeHound to identify the resources that the user has access to, whether any lead to critical assets and what attacks might have been leveraged.\n", + "\n", + "First let's see whether there are any critical paths accessible. Because Kubernetes delegates the management of users' group memberships to third party components (e.g identity providers), we need to check paths from both the user and any groups they are a member of. \n", + "\n", + "**NOTE** the mapping of users to groups must be done prior to this step as it falls outside the scope of KubeHound and is specific to the identity provider." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin\n", + "kh.identities().\n", + " or(\n", + " has(\"type\", \"Group\").has(\"name\", within(\"dept-sales\", \"k8s-users\")),\n", + " has(\"type\", \"User\").has(\"name\", \"bits.barkley@datadoghq.com\")).\n", + " hasCriticalPath().\n", + " values(\"name\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's examine the possible attack paths that could be taken by an attacker who has access to the compromised credentials." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "kh.identities().\n", + " or(\n", + " has(\"type\", \"Group\").has(\"name\", within(\"dept-sales\", \"k8s-users\")),\n", + " has(\"type\", \"User\").has(\"name\", \"bits.barkley@datadoghq.com\")).\n", + " criticalPaths().\n", + " by(elementMap()).\n", + " limit(100) // Limit the number of results for large clusters\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Skip to the [next section](#advanced-workflows) to for more in-depth workflows to surface potential detection sources and eliminate attacks to narrow down the scope." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Compromised Container\n", + "\n", + "Consider the scenario where a container has been compromised via a malicious dependency found within a known set of images. We can use KubeHound to identify the resources that the container has access to, whether any lead to critical assets and what attacks might have been leveraged.\n", + "\n", + "First let's see whether there are any critical paths accessible" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin\n", + "kh.containers().\n", + " or(\n", + " has(\"image\", TextP.containing(\"nginx\")), // Replace with your image name\n", + " has(\"image\", TextP.containing(\"cilium\"))). // Replace with your image name\n", + " hasCriticalPath().\n", + " values(\"name\").\n", + " dedup()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's examine the possible attack paths that could be taken by the attacker." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "kh.containers().\n", + " or(\n", + " has(\"image\", TextP.containing(\"nginx\")), // Replace with your image name\n", + " has(\"image\", TextP.containing(\"cilium\"))). // Replace with your image name\n", + " criticalPaths().\n", + " by(elementMap()).\n", + " limit(100) // Limit the number of results for large clusters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Advanced Workflows" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In a real-world deployment the above queries may throw up too many results to be actionable. In such cases we can use KubeHound to narrow down the scope of the investigation.\n", + "\n", + "### Focus on container escapes\n", + "\n", + "For example in the compromised container case we may wish to understand the easiest attack path that the attacker could have taken and focus our detections efforts there. Let's first look for any potential container escapes from the compromised container. These provide easy privilege escalation for an attacker but could also provide detection opportunities for us. The query below provides a list of container escapes possible from the matching images." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin\n", + "kh.containers().\n", + " where(out().hasLabel(\"Node\")).\n", + " or(\n", + " has(\"image\", TextP.containing(\"nginx\")), // Replace with your image name\n", + " has(\"image\", TextP.containing(\"cilium\"))). // Replace with your image name\n", + "\tproject('image',\"escapes\").\n", + "\tby(values(\"image\")).\n", + "\tby(outE().where(inV().hasLabel(\"Node\")).label().fold()).\n", + "\tdedup()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Shortest attack paths\n", + "\n", + "Attackers are incentivized to take the shortest path to their target. We can use KubeHound to identify the shortest attack paths from the compromised container to the critical assets. This can help us focus our detection efforts on the most likely attack paths.\n", + "\n", + "First we calculate the length of the shortest attack path from our target container to a critical asset:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin \n", + "kh.containers().\n", + " or(\n", + " has(\"image\", TextP.containing(\"nginx\")), // Replace with your image name\n", + " has(\"image\", TextP.containing(\"cilium\"))). // Replace with your image name\n", + " minHopsToCritical()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then we can find the unique attack paths of that length:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "kh.containers().\n", + " or(\n", + " has(\"image\", TextP.containing(\"nginx\")), // Replace with your image name\n", + " has(\"image\", TextP.containing(\"cilium\"))). // Replace with your image name\n", + " \n", + " repeat(\n", + " outE().inV().simplePath()).\n", + " emit().\n", + " until(\n", + " has(\"critical\", true).\n", + " or().\n", + " loops().\n", + " is(4)). // Use result from previous cell\n", + " has(\"critical\", true).\n", + " dedup().\n", + " path().\n", + " by(elementMap())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Blast radius evaluation\n", + "\n", + "It may be the case that the compromised container does not have a path to a critical asset (at least within the KubeHound model). In this case in can be useful to understand the blast radius of the compromised container. We can use KubeHound to identify all the resources that an attacker could have accessed from a compromised container." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d name -g class -le 50 -p inv,oute\n", + "kh.containers().\n", + " or(\n", + " has(\"image\", TextP.containing(\"nginx\")),\n", + " has(\"image\", TextP.containing(\"cilium\"))).\n", + " \trepeat(\n", + " outE().inV().simplePath()).\n", + " times(5). // Increase to expand the potential blast radius, but graph size will increase exponentially!\n", + " emit().\n", + " path().\n", + " by(elementMap()).\n", + " limit(100) // Limit the number of results for large clusters" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/deployments/kubehound/notebook/Dockerfile b/deployments/kubehound/notebook/Dockerfile new file mode 100644 index 000000000..1f575f40f --- /dev/null +++ b/deployments/kubehound/notebook/Dockerfile @@ -0,0 +1,78 @@ +# This Dockerfile is a tailored version of https://github.com/aws/graph-notebook under APACHE 2 LICENCE + +FROM amazonlinux:2 + +# Notebook Port +EXPOSE 8888 +# Lab Port +EXPOSE 8889 +USER root + +ENV pipargs="" +ENV WORKING_DIR="/root" +ENV NOTEBOOK_DIR="${WORKING_DIR}/notebooks" +ENV NODE_VERSION=14.x +ENV GRAPH_NOTEBOOK_AUTH_MODE="DEFAULT" +ENV GRAPH_NOTEBOOK_HOST="localhost" +ENV GRAPH_NOTEBOOK_PORT="8182" +ENV NOTEBOOK_PORT="8888" +ENV LAB_PORT="8889" +ENV GRAPH_NOTEBOOK_SSL="True" +ENV NOTEBOOK_PASSWORD="admin" + +# "when the SIGTERM signal is sent to the docker process, it immediately quits and all established connections are closed" +# "graceful stop is triggered when the SIGUSR1 signal is sent to the docker process" +STOPSIGNAL SIGUSR1 + + +RUN mkdir -p "${WORKING_DIR}" && \ + mkdir -p "${NOTEBOOK_DIR}" && \ + # Yum Update and install dependencies + yum update -y && \ + yum install tar gzip git amazon-linux-extras which -y && \ + # Install NPM/Node + curl --silent --location https://rpm.nodesource.com/setup_${NODE_VERSION} | bash - && \ + yum install nodejs -y && \ + npm install -g opencollective && \ + # Install Python 3.8 + amazon-linux-extras install python3.8 -y && \ + update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 1 && \ + echo 'Using python version:' && \ + python3 --version && \ + python3 -m ensurepip --upgrade && \ + python3 -m venv /tmp/venv && \ + source /tmp/venv/bin/activate && \ + cd "${WORKING_DIR}" && \ + # Clone the repo and install python dependencies + git clone https://github.com/aws/graph-notebook && \ + cd "${WORKING_DIR}/graph-notebook" && \ + pip3 install --upgrade pip setuptools wheel && \ + pip3 install twine==3.7.1 && \ + pip3 install -r requirements.txt && \ + pip3 install "jupyterlab>=3,<4" && \ + # Build the package + python3 setup.py sdist bdist_wheel && \ + # install the copied repo + pip3 install . && \ + # copy premade starter notebooks + cd "${WORKING_DIR}/graph-notebook" && \ + jupyter nbextension enable --py --sys-prefix graph_notebook.widgets && \ + # This allows for the `.ipython` to be set + python -m graph_notebook.start_jupyterlab --jupyter-dir "${NOTEBOOK_DIR}" && \ + # Cleanup + yum clean all && \ + yum remove wget tar git -y && \ + rm -rf /var/cache/yum && \ + rm -rf "${WORKING_DIR}/graph-notebook" && \ + rm -rf /root/.cache && \ + rm -rf /root/.npm/_cacache && \ + rm -rf /usr/share + +ADD "KubeHound.ipynb" "${NOTEBOOK_DIR}/KubeHound.ipynb" +ADD "RedTeam.ipynb" "${NOTEBOOK_DIR}/RedTeam.ipynb" +ADD "BlueTeam.ipynb" "${NOTEBOOK_DIR}/BlueTeam.ipynb" +ADD "SecurityPosture.ipynb" "${NOTEBOOK_DIR}/SecurityPosture.ipynb" +ADD ./service.sh /usr/bin/service.sh +RUN chmod +x /usr/bin/service.sh + +ENTRYPOINT [ "bash","-c","service.sh" ] diff --git a/deployments/kubehound/notebook/KubeHound.ipynb b/deployments/kubehound/notebook/KubeHound.ipynb new file mode 100644 index 000000000..a7ccc43e7 --- /dev/null +++ b/deployments/kubehound/notebook/KubeHound.ipynb @@ -0,0 +1,170 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Example KubeHound queries \n", + "\n", + "This file contains examples queries for kubehound. This file should be mostly read only. No modification will be ported to the original file, it will be kept only for the duration of the docker container.\n", + "\n", + "Note: You may need to adjust the value on the cell below and execute (ctrl + enter) before running any other cells." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initial Setup\n", + "\n", + "Connect to the kubegraph server by running the cell below" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%graph_notebook_config\n", + "{\n", + " \"host\": \"host.docker.internal\",\n", + " \"port\": 8182,\n", + " \"ssl\": false,\n", + " \"gremlin\": {\n", + " \"traversal_source\": \"g\",\n", + " \"username\": \"\",\n", + " \"password\": \"\",\n", + " \"message_serializer\": \"graphsonv3\"\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now set the appearance customizations for the notebook. You can see a guide on possible options [here](https://github.com/aws/graph-notebook/blob/623d43827f798c33125219e8f45ad1b6e5b29513/src/graph_notebook/notebooks/01-Neptune-Database/02-Visualization/Grouping-and-Appearance-Customization-Gremlin.ipynb#L680)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%graph_notebook_vis_options\n", + "{\n", + " \"edges\": {\n", + " \"smooth\": {\n", + " \"enabled\": true,\n", + " \"type\": \"dynamic\"\n", + " },\n", + " \"arrows\": {\n", + " \"to\": {\n", + " \"enabled\": true,\n", + " \"type\": \"arrow\"\n", + " }\n", + " }\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Critical Path Queries" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### All critical paths from external services\n", + "\n", + "Critical paths from external services can highlight most likely paths an attacker can take following the compromise of an external service. In a well-configured cluster, such paths should be limited to very few (if any) services.\n", + "\n", + "**NOTE** an `ENDPOINT_EXPLOIT` edge does not signal that the endpoint is *necessarily* exploitable but serves as a useful starting point for path traversal queries\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "kh.services().\n", + " criticalPaths().\n", + " by(elementMap()).\n", + " limit(100) // Limit the number of results for large clusters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### All critical paths from groups\n", + "\n", + "Critical paths from groups can highlight overprivileged groups and help quickly assess the impact of compromised credentials." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "kh.groups().\n", + " criticalPaths().\n", + " by(elementMap()).\n", + " limit(100) // Limit the number of results for large clusters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Complex critical path queries\n", + "The [KubeHound DSL](https://kubehound.io/queries/dsl/) is fully compatible with the Gremlin language, so you can use any gremlin function in your queries to add additional filters if needed. The example below attempts to filter for Kubernetes services exposed on port 443 attached to `elasticsearch` containers, and limits attack paths to 6 hops or less." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "kh.services().\n", + " has(\"port\", 443). // Look for exposed port on 443 only\n", + " not(has(\"namespace\", within(\"system\", \"kube\"))). // Exclude endpoints from the 'kube' and 'system' namespaces\n", + " where(__.out().hasLabel(\"Container\").has(\"image\", TextP.containing(\"elasticsearch\"))). // Only accept endpoints attached to elasticsearch containers\n", + " criticalPaths(6). // Limit to critical paths of 6 hops or less\n", + " by(elementMap()).\n", + " limit(100) // Limit the number of results for large clusters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "See the [documentation](https://kubehound.io/queries/dsl/#criticalpaths-step) for further examples of critical path analysis" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/deployments/kubehound/notebook/RedTeam.ipynb b/deployments/kubehound/notebook/RedTeam.ipynb new file mode 100644 index 000000000..d9ad180a6 --- /dev/null +++ b/deployments/kubehound/notebook/RedTeam.ipynb @@ -0,0 +1,209 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Red Team KubeHound Workflow\n", + "\n", + "A step by step example workflow of how to use KubeHound for Red Team operations." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initial Setup\n", + "\n", + "Connect to the kubegraph server by running the cell below" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%graph_notebook_config\n", + "{\n", + " \"host\": \"host.docker.internal\",\n", + " \"port\": 8182,\n", + " \"ssl\": false,\n", + " \"gremlin\": {\n", + " \"traversal_source\": \"g\",\n", + " \"username\": \"\",\n", + " \"password\": \"\",\n", + " \"message_serializer\": \"graphsonv3\"\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now set the appearance customizations for the notebook. You can see a guide on possible options [here](https://github.com/aws/graph-notebook/blob/623d43827f798c33125219e8f45ad1b6e5b29513/src/graph_notebook/notebooks/01-Neptune-Database/02-Visualization/Grouping-and-Appearance-Customization-Gremlin.ipynb#L680)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%graph_notebook_vis_options\n", + "{\n", + " \"edges\": {\n", + " \"smooth\": {\n", + " \"enabled\": true,\n", + " \"type\": \"dynamic\"\n", + " },\n", + " \"arrows\": {\n", + " \"to\": {\n", + " \"enabled\": true,\n", + " \"type\": \"arrow\"\n", + " }\n", + " }\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Worfklow" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Initial Recon\n", + "\n", + "Look for exposed endpoints attached to containers and return the port details and image. This enables us to match against any exploits in our catalogue" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin\n", + "kh.endpoints().\n", + "\twhere(out().hasLabel(\"Container\")).\n", + "\tproject('port',\"portName\", 'image').\n", + "\tby(values(\"port\")).\n", + "\tby(values(\"portName\")).\n", + "\tby(out().hasLabel(\"Container\").values(\"image\")).\n", + "\tdedup()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's focus on those with a critical attack path" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin\n", + "kh.endpoints().\n", + "\thasCriticalPath().\n", + "\twhere(out().hasLabel(\"Container\")).\n", + "\tproject('port',\"portName\", 'image').\n", + "\tby(values(\"port\")).\n", + "\tby(values(\"portName\")).\n", + "\tby(out().hasLabel(\"Container\").values(\"image\")).\n", + "\tdedup()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Attack Path Analysis\n", + "\n", + "Let's pick a promising target we have an exploit for and look at possible attack paths" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "kh.endpoints().\n", + "\thas(\"portName\", \"elasticsearch\").\t// Change the value here for those found\n", + "\tnot(has(\"protocol\", \"UDP\")). // Exclude or change based on requirements\n", + "\tcriticalPaths().\n", + "\tby(elementMap()).\n", + "\tlimit(100)\t// Limit the number of results for large clusters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now to refine our analysis, let's discount a few of the noisier attacks and focus on shorter attack paths. See [reference documentation](https://kubehound.io/reference/attacks/) for detaills of these attacks." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "kh.endpoints().\n", + "\thas(\"portName\", \"elasticsearch\"). // Change the value here for those found\n", + "\tcriticalPathsFilter(6, \"TOKEN_BRUTEFORCE\", \"POD_EXEC\", \"POD_CREATE\").\n", + "\tby(elementMap()).\n", + " limit(100)\t// Limit the number of results for large clusters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's explore a couple of interesting endpoints which do not have a critical path BUT get us to a Node which could contain useful resources outside the scope of KubeHound e.g AWS credentials, SSH keys, etc" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d name -g class -le 50 -p inv,oute\n", + "kh.endpoints().\n", + "\thas(\"portName\", within(\"jmx\", \"ssh\", \"log4j\")). // Change the values here for those found\n", + "\trepeat(\n", + "\t\toutE().inV().\n", + "\t\tsimplePath()).\n", + "\tuntil(\n", + "\t\thasLabel(\"Node\").\n", + "\t\tor().\n", + "\t\tloops().is(5)).\n", + "\thasLabel(\"Node\").\n", + "\tpath().\n", + "\tby(elementMap()).\n", + " limit(100)\t// Limit the number of results for large clusters" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/deployments/kubehound/notebook/SecurityPosture.ipynb b/deployments/kubehound/notebook/SecurityPosture.ipynb new file mode 100644 index 000000000..dcee46c02 --- /dev/null +++ b/deployments/kubehound/notebook/SecurityPosture.ipynb @@ -0,0 +1,197 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Security Posture Workflow\n", + "\n", + "A step by step example workflow to measure the security posture of a Kubernetes cluster using KubeHound." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initial Setup\n", + "\n", + "Connect to the kubegraph server by running the cell below" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%graph_notebook_config\n", + "{\n", + " \"host\": \"host.docker.internal\",\n", + " \"port\": 8182,\n", + " \"ssl\": false,\n", + " \"gremlin\": {\n", + " \"traversal_source\": \"g\",\n", + " \"username\": \"\",\n", + " \"password\": \"\",\n", + " \"message_serializer\": \"graphsonv3\"\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Workflow\n", + "\n", + "### High Level Metrics\n", + "\n", + "Let us get a high-level view of the security posture of the cluster. These metrics are not very nuanced but provide a top-level view of cluster security and easy tracking of improvements over time.\n", + "\n", + "First let's look at the shortest path from external service to a critical asset" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin\n", + "kh.services().minHopsToCritical()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next let's see the total number of attacks paths originating from external services" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin\n", + "kh.services().criticalPaths().count()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exposed asset analyis\n", + "\n", + "The most likely entry points for an attacker into a Kubernetes cluster are:\n", + "+ Exposed services via 0day, n-day, or misconfigurations\n", + "+ Leaked credentials\n", + "+ Supply chain attacks leading to execution within a container\n", + "\n", + "We can use KubeHound to evaluate the percentage of each of these entry points that can lead to a critical asset. First services:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin\n", + "kh.V().\n", + " hasLabel(\"Endpoint\").\n", + " has(\"exposure\", gte(2)). // https://kubehound.io/queries/dsl/#endpoint-exposure\n", + " count().\n", + " aggregate(\"t\").\n", + " V().\n", + " hasLabel(\"Endpoint\").\n", + " has(\"exposure\", gte(2)). // https://kubehound.io/queries/dsl/#endpoint-exposure\n", + " hasCriticalPath().\n", + " count().\n", + " as(\"e\").\n", + " math(\"100 * e/t\").by().by(unfold())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next credentials:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin\n", + "kh.V().\n", + " hasLabel(\"Identity\").\n", + " has(\"critical\", false).\n", + " count().\n", + " aggregate(\"t\").\n", + " V().\n", + " hasLabel(\"Identity\").\n", + " has(\"critical\", false).\n", + " hasCriticalPath().\n", + " count().\n", + " as(\"e\").\n", + " math(\"100 * e/t\").by().by(unfold())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally containers:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin\n", + "kh.V().\n", + " hasLabel(\"Container\").\n", + " count().\n", + " aggregate(\"t\").\n", + " V().\n", + " hasLabel(\"Container\").\n", + " hasCriticalPath().\n", + " count().\n", + " as(\"e\").\n", + " math(\"100 * e/t\").by().by(unfold())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Threat Modelling\n", + "\n", + "KubeHound can provide a high level overview of attack paths grouped by frequency in any given cluster via the DSL" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin\n", + "kh.services().criticalPathsFreq()" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/deployments/kubehound/notebook/service.sh b/deployments/kubehound/notebook/service.sh new file mode 100755 index 000000000..38cf63659 --- /dev/null +++ b/deployments/kubehound/notebook/service.sh @@ -0,0 +1,25 @@ +# This Dockerfile is a tailored version of https://github.com/aws/graph-notebook under APACHE 2 LICENCE + +source /tmp/venv/bin/activate +cd "${WORKING_DIR}" + +python3 -m graph_notebook.configuration.generate_config \ + --host "${GRAPH_NOTEBOOK_HOST}" \ + --port "${GRAPH_NOTEBOOK_PORT}" \ + --auth_mode "${GRAPH_NOTEBOOK_AUTH_MODE}" + +##### Running The Notebook Service ##### +mkdir ~/.jupyter +if [ ! ${NOTEBOOK_PASSWORD} ]; + then + echo "No password set for notebook" + exit 1 +else + echo "c.NotebookApp.password='$(python -c "from notebook.auth import passwd; print(passwd('${NOTEBOOK_PASSWORD}'))")'" >> ~/.jupyter/jupyter_notebook_config.py +fi +echo "c.NotebookApp.allow_remote_access = True" >> ~/.jupyter/jupyter_notebook_config.py +echo "c.InteractiveShellApp.extensions = ['graph_notebook.magics']" >> ~/.jupyter/jupyter_notebook_config.py + +nohup jupyter notebook --ip='*' --port ${NOTEBOOK_PORT} "${WORKING_DIR}/notebooks" --allow-root > jupyterserver.log & +nohup jupyter lab --ip='*' --port ${LAB_PORT} "${WORKING_DIR}/notebooks" --allow-root > jupyterlab.log & +tail -f /dev/null \ No newline at end of file diff --git a/deployments/kubehound/notebook/shared/shared.ipynb b/deployments/kubehound/notebook/shared/shared.ipynb new file mode 100644 index 000000000..74dda15b2 --- /dev/null +++ b/deployments/kubehound/notebook/shared/shared.ipynb @@ -0,0 +1,116 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# User defined queries \n", + "\n", + "This notebook (and the ones next to it in this folder) will be in sync between the docker container and the hosts.\n", + "You can use this to experiment and save your queries in git, if needed.\n", + "This file is persistant.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initial Setup\n", + "\n", + "Connect to the kubegraph server by running the cell below" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%graph_notebook_config\n", + "{\n", + " \"host\": \"host.docker.internal\",\n", + " \"port\": 8182,\n", + " \"ssl\": false,\n", + " \"gremlin\": {\n", + " \"traversal_source\": \"g\",\n", + " \"username\": \"\",\n", + " \"password\": \"\",\n", + " \"message_serializer\": \"graphsonv3\"\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now set the appearance customizations for the notebook. You can see a guide on possible options [here](https://github.com/aws/graph-notebook/blob/623d43827f798c33125219e8f45ad1b6e5b29513/src/graph_notebook/notebooks/01-Neptune-Database/02-Visualization/Grouping-and-Appearance-Customization-Gremlin.ipynb#L680)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%graph_notebook_vis_options\n", + "{\n", + " \"edges\": {\n", + " \"smooth\": {\n", + " \"enabled\": true,\n", + " \"type\": \"dynamic\"\n", + " },\n", + " \"arrows\": {\n", + " \"to\": {\n", + " \"enabled\": true,\n", + " \"type\": \"arrow\"\n", + " }\n", + " }\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Queries\n", + "\n", + "Execute queries in the cell below or add additional cells as required. See the [query library](https://kubehound.io/queries/dsl/) for documentation and examples." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g class -le 50 -p inv,oute\n", + "kh.V().count()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.16" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/Architecture.excalidraw b/docs/Architecture.excalidraw index 322ec4398..56dacaebd 100644 --- a/docs/Architecture.excalidraw +++ b/docs/Architecture.excalidraw @@ -1052,8 +1052,8 @@ }, { "type": "text", - "version": 117, - "versionNonce": 1980260479, + "version": 119, + "versionNonce": 1055379226, "isDeleted": false, "id": "sJO_ZmMepiyi06AHrKkQ9", "fillStyle": "hachure", @@ -1082,7 +1082,7 @@ "type": "arrow" } ], - "updated": 1696427296401, + "updated": 1707755580528, "link": null, "locked": false, "fontSize": 28, @@ -1235,8 +1235,8 @@ }, { "type": "text", - "version": 187, - "versionNonce": 361442033, + "version": 189, + "versionNonce": 1603520390, "isDeleted": false, "id": "MhOA3DvDyl9_PMA_eGylg", "fillStyle": "hachure", @@ -1256,7 +1256,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296402, + "updated": 1707755580528, "link": null, "locked": false, "fontSize": 28, @@ -1412,8 +1412,8 @@ }, { "type": "text", - "version": 102, - "versionNonce": 1011218591, + "version": 104, + "versionNonce": 1779620826, "isDeleted": false, "id": "GTnv7qVfRupMg-qr0ON_9", "fillStyle": "hachure", @@ -1433,7 +1433,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296402, + "updated": 1707755580528, "link": null, "locked": false, "fontSize": 28, @@ -2019,8 +2019,8 @@ }, { "type": "text", - "version": 131, - "versionNonce": 229287121, + "version": 133, + "versionNonce": 436004550, "isDeleted": false, "id": "uJatiRDv4sNvMEWLmFCZs", "fillStyle": "hachure", @@ -2040,7 +2040,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296402, + "updated": 1707755580528, "link": null, "locked": false, "fontSize": 28, @@ -2181,8 +2181,8 @@ }, { "type": "text", - "version": 119, - "versionNonce": 1935379647, + "version": 121, + "versionNonce": 1035882650, "isDeleted": false, "id": "qsY95SxOH_-1MsBxffwR8", "fillStyle": "hachure", @@ -2202,7 +2202,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296402, + "updated": 1707755580528, "link": null, "locked": false, "fontSize": 28, @@ -2365,8 +2365,8 @@ }, { "type": "text", - "version": 326, - "versionNonce": 729801393, + "version": 328, + "versionNonce": 1082138118, "isDeleted": false, "id": "xj-u_gjSnmn3GDxyNR2d8", "fillStyle": "hachure", @@ -2386,7 +2386,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296402, + "updated": 1707755580528, "link": null, "locked": false, "fontSize": 28, @@ -2401,8 +2401,8 @@ }, { "type": "text", - "version": 299, - "versionNonce": 921279711, + "version": 301, + "versionNonce": 496840026, "isDeleted": false, "id": "5RMFef4-FX4TQRA_fiGXq", "fillStyle": "hachure", @@ -2422,7 +2422,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296403, + "updated": 1707755580529, "link": null, "locked": false, "fontSize": 28, @@ -2499,8 +2499,8 @@ }, { "type": "text", - "version": 316, - "versionNonce": 1001546897, + "version": 318, + "versionNonce": 2014997830, "isDeleted": false, "id": "Q8HFSir8MT7mNfc1xSlP0", "fillStyle": "hachure", @@ -2520,7 +2520,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296403, + "updated": 1707755580529, "link": null, "locked": false, "fontSize": 28, @@ -2597,8 +2597,8 @@ }, { "type": "text", - "version": 330, - "versionNonce": 1518381311, + "version": 332, + "versionNonce": 2130866714, "isDeleted": false, "id": "Xx8EMOQ-0nia17TSOBruD", "fillStyle": "hachure", @@ -2618,7 +2618,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296403, + "updated": 1707755580529, "link": null, "locked": false, "fontSize": 28, @@ -2695,8 +2695,8 @@ }, { "type": "text", - "version": 350, - "versionNonce": 167357041, + "version": 352, + "versionNonce": 1042863238, "isDeleted": false, "id": "OYk4eZYTSZz7Z8N1q51Im", "fillStyle": "hachure", @@ -2716,7 +2716,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296403, + "updated": 1707755580529, "link": null, "locked": false, "fontSize": 28, @@ -2793,8 +2793,8 @@ }, { "type": "text", - "version": 337, - "versionNonce": 1805725983, + "version": 339, + "versionNonce": 887168730, "isDeleted": false, "id": "THQTcyKGefUDkWgebQCI_", "fillStyle": "hachure", @@ -2814,7 +2814,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296403, + "updated": 1707755580529, "link": null, "locked": false, "fontSize": 28, @@ -2891,8 +2891,8 @@ }, { "type": "text", - "version": 329, - "versionNonce": 2132620369, + "version": 331, + "versionNonce": 1550341062, "isDeleted": false, "id": "eRAlYYwrSvaomNqigGNo_", "fillStyle": "hachure", @@ -2912,7 +2912,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296403, + "updated": 1707755580529, "link": null, "locked": false, "fontSize": 28, @@ -2989,8 +2989,8 @@ }, { "type": "text", - "version": 350, - "versionNonce": 1941681471, + "version": 352, + "versionNonce": 1003238298, "isDeleted": false, "id": "ae6RAopSKrGrhlpEiBhp9", "fillStyle": "hachure", @@ -3010,7 +3010,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296403, + "updated": 1707755580529, "link": null, "locked": false, "fontSize": 28, @@ -3133,8 +3133,8 @@ }, { "type": "text", - "version": 401, - "versionNonce": 2129953329, + "version": 403, + "versionNonce": 1559620358, "isDeleted": false, "id": "2Qvkr2NhtfGOcw3dpgKc-", "fillStyle": "hachure", @@ -3154,7 +3154,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296403, + "updated": 1707755580529, "link": null, "locked": false, "fontSize": 28, @@ -3169,8 +3169,8 @@ }, { "type": "text", - "version": 367, - "versionNonce": 2050012511, + "version": 369, + "versionNonce": 100053082, "isDeleted": false, "id": "uIC8FEvSKMfK2mGhnRdDZ", "fillStyle": "hachure", @@ -3190,7 +3190,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296403, + "updated": 1707755580529, "link": null, "locked": false, "fontSize": 28, @@ -3267,8 +3267,8 @@ }, { "type": "text", - "version": 402, - "versionNonce": 107418641, + "version": 404, + "versionNonce": 2119080518, "isDeleted": false, "id": "X1YLHU7ohQU4sOcnHA0qv", "fillStyle": "hachure", @@ -3288,7 +3288,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296404, + "updated": 1707755580530, "link": null, "locked": false, "fontSize": 28, @@ -3365,8 +3365,8 @@ }, { "type": "text", - "version": 517, - "versionNonce": 1179693439, + "version": 519, + "versionNonce": 1194562842, "isDeleted": false, "id": "jeCM6c0i4Y4GPlsfWd4VP", "fillStyle": "hachure", @@ -3386,7 +3386,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296404, + "updated": 1707755580530, "link": null, "locked": false, "fontSize": 28, @@ -3463,8 +3463,8 @@ }, { "type": "text", - "version": 404, - "versionNonce": 158099953, + "version": 406, + "versionNonce": 659513734, "isDeleted": false, "id": "vy6xQkiQNebtwi6MR1KA6", "fillStyle": "hachure", @@ -3489,7 +3489,7 @@ "type": "arrow" } ], - "updated": 1696427296404, + "updated": 1707755580530, "link": null, "locked": false, "fontSize": 28, @@ -3566,8 +3566,8 @@ }, { "type": "text", - "version": 401, - "versionNonce": 1633429919, + "version": 403, + "versionNonce": 1089855962, "isDeleted": false, "id": "9o8Nx_3Aqci2cLWRVw73U", "fillStyle": "hachure", @@ -3587,7 +3587,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296404, + "updated": 1707755580530, "link": null, "locked": false, "fontSize": 28, @@ -3664,8 +3664,8 @@ }, { "type": "text", - "version": 420, - "versionNonce": 286000081, + "version": 422, + "versionNonce": 1530216646, "isDeleted": false, "id": "62qOOnavk9Qc0c1Qulbt9", "fillStyle": "hachure", @@ -3685,7 +3685,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296404, + "updated": 1707755580530, "link": null, "locked": false, "fontSize": 28, @@ -3762,8 +3762,8 @@ }, { "type": "text", - "version": 416, - "versionNonce": 1485020607, + "version": 418, + "versionNonce": 240162458, "isDeleted": false, "id": "J24wke_9NjpUX88qpjt2a", "fillStyle": "hachure", @@ -3783,7 +3783,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296404, + "updated": 1707755580531, "link": null, "locked": false, "fontSize": 28, @@ -4775,8 +4775,8 @@ }, { "type": "text", - "version": 107, - "versionNonce": 425550257, + "version": 109, + "versionNonce": 765278214, "isDeleted": false, "id": "HapGfrQFrDl5KE-Oby23Z", "fillStyle": "hachure", @@ -4801,7 +4801,7 @@ "type": "arrow" } ], - "updated": 1696427296404, + "updated": 1707755580531, "link": null, "locked": false, "fontSize": 28, @@ -4816,8 +4816,8 @@ }, { "type": "text", - "version": 152, - "versionNonce": 1055362527, + "version": 154, + "versionNonce": 1878438746, "isDeleted": false, "id": "Yq7rXU67DrJQRiPk7DO4o", "fillStyle": "hachure", @@ -4837,7 +4837,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296404, + "updated": 1707755580531, "link": null, "locked": false, "fontSize": 28, @@ -4896,8 +4896,8 @@ }, { "type": "text", - "version": 102, - "versionNonce": 797442961, + "version": 104, + "versionNonce": 914638662, "isDeleted": false, "id": "cLd1O2Lwxzt4GEBAr_K4R", "fillStyle": "hachure", @@ -4917,7 +4917,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296404, + "updated": 1707755580531, "link": null, "locked": false, "fontSize": 28, @@ -4976,8 +4976,8 @@ }, { "type": "text", - "version": 155, - "versionNonce": 1872263679, + "version": 157, + "versionNonce": 406534170, "isDeleted": false, "id": "7W32qHrGktfKhsDLb5QvA", "fillStyle": "hachure", @@ -4997,7 +4997,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296404, + "updated": 1707755580531, "link": null, "locked": false, "fontSize": 28, @@ -5056,8 +5056,8 @@ }, { "type": "text", - "version": 153, - "versionNonce": 1338206577, + "version": 155, + "versionNonce": 105344646, "isDeleted": false, "id": "dni7LEI81rGkEtw9q3eZN", "fillStyle": "hachure", @@ -5077,7 +5077,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296404, + "updated": 1707755580531, "link": null, "locked": false, "fontSize": 28, @@ -5136,8 +5136,8 @@ }, { "type": "text", - "version": 147, - "versionNonce": 261546527, + "version": 149, + "versionNonce": 1997612250, "isDeleted": false, "id": "rvw_FkiMnZ6xfJUCssEnW", "fillStyle": "hachure", @@ -5157,7 +5157,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296405, + "updated": 1707755580531, "link": null, "locked": false, "fontSize": 28, @@ -5230,8 +5230,8 @@ }, { "type": "text", - "version": 116, - "versionNonce": 56038225, + "version": 118, + "versionNonce": 430157254, "isDeleted": false, "id": "v63LJ3Vzev4f3cKRNbejM", "fillStyle": "hachure", @@ -5251,7 +5251,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296405, + "updated": 1707755580531, "link": null, "locked": false, "fontSize": 28, @@ -5295,8 +5295,8 @@ }, { "type": "text", - "version": 126, - "versionNonce": 1339246143, + "version": 128, + "versionNonce": 101415322, "isDeleted": false, "id": "YeBADWHp2vZRmKb-TD44J", "fillStyle": "hachure", @@ -5316,7 +5316,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296405, + "updated": 1707755580531, "link": null, "locked": false, "fontSize": 28, @@ -5417,8 +5417,8 @@ }, { "type": "text", - "version": 461, - "versionNonce": 1050313009, + "version": 463, + "versionNonce": 1303774470, "isDeleted": false, "id": "0nXep7d3EFLCFaHLDUSx_", "fillStyle": "hachure", @@ -5438,7 +5438,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296405, + "updated": 1707755580532, "link": null, "locked": false, "fontSize": 20, @@ -7157,8 +7157,8 @@ }, { "type": "text", - "version": 155, - "versionNonce": 1865295455, + "version": 157, + "versionNonce": 341653082, "isDeleted": false, "id": "6Su8vpgYUqsmtknzq5VDS", "fillStyle": "hachure", @@ -7178,7 +7178,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296405, + "updated": 1707755580532, "link": null, "locked": false, "fontSize": 20, @@ -8897,8 +8897,8 @@ }, { "type": "text", - "version": 210, - "versionNonce": 2129998609, + "version": 212, + "versionNonce": 1408988230, "isDeleted": false, "id": "zZPs1bg4kgerfBJZlmBts", "fillStyle": "hachure", @@ -8918,7 +8918,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296405, + "updated": 1707755580532, "link": null, "locked": false, "fontSize": 20, @@ -10637,8 +10637,8 @@ }, { "type": "text", - "version": 318, - "versionNonce": 2088239743, + "version": 320, + "versionNonce": 316234522, "isDeleted": false, "id": "h749tTzy4MnTdUx-ebPlM", "fillStyle": "hachure", @@ -10658,7 +10658,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296406, + "updated": 1707755580533, "link": null, "locked": false, "fontSize": 20, @@ -12377,8 +12377,8 @@ }, { "type": "text", - "version": 332, - "versionNonce": 685680881, + "version": 334, + "versionNonce": 642841478, "isDeleted": false, "id": "_IkzPk-IXZOv1AkhgTZlJ", "fillStyle": "hachure", @@ -12398,7 +12398,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296406, + "updated": 1707755580533, "link": null, "locked": false, "fontSize": 20, @@ -12451,8 +12451,8 @@ }, { "type": "text", - "version": 126, - "versionNonce": 1483823775, + "version": 128, + "versionNonce": 1609754586, "isDeleted": false, "id": "fhtnQZ7xkE4vZzHVHaQXd", "fillStyle": "hachure", @@ -12472,7 +12472,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296406, + "updated": 1707755580533, "link": null, "locked": false, "fontSize": 20, @@ -12487,8 +12487,8 @@ }, { "type": "text", - "version": 198, - "versionNonce": 635331281, + "version": 200, + "versionNonce": 2062785222, "isDeleted": false, "id": "_awvnh49jf-mf_Dz4IlKh", "fillStyle": "hachure", @@ -12508,7 +12508,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296406, + "updated": 1707755580533, "link": null, "locked": false, "fontSize": 20, @@ -12961,8 +12961,8 @@ }, { "type": "text", - "version": 56, - "versionNonce": 2009771711, + "version": 58, + "versionNonce": 331144346, "isDeleted": false, "id": "-GHqS8t7mzHIfqTsZnevk", "fillStyle": "hachure", @@ -12987,7 +12987,7 @@ "type": "arrow" } ], - "updated": 1696427296406, + "updated": 1707755580533, "link": null, "locked": false, "fontSize": 20, @@ -13411,8 +13411,8 @@ }, { "type": "text", - "version": 128, - "versionNonce": 1116463281, + "version": 130, + "versionNonce": 947933702, "isDeleted": false, "id": "CvDdQVlqAFXOuaA4tKvyT", "fillStyle": "hachure", @@ -13432,7 +13432,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296406, + "updated": 1707755580533, "link": null, "locked": false, "fontSize": 20, @@ -13447,8 +13447,8 @@ }, { "type": "text", - "version": 147, - "versionNonce": 1696890591, + "version": 149, + "versionNonce": 1273641306, "isDeleted": false, "id": "XbeLWyVem7v6CSSyR3WKn", "fillStyle": "hachure", @@ -13468,7 +13468,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296406, + "updated": 1707755580533, "link": null, "locked": false, "fontSize": 20, @@ -13529,8 +13529,8 @@ }, { "type": "text", - "version": 90, - "versionNonce": 1284416145, + "version": 92, + "versionNonce": 1777606982, "isDeleted": false, "id": "EO8bRIiEmk7DWH7DzMvej", "fillStyle": "hachure", @@ -13550,7 +13550,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296406, + "updated": 1707755580534, "link": null, "locked": false, "fontSize": 20, @@ -14174,8 +14174,8 @@ }, { "type": "text", - "version": 101, - "versionNonce": 51333887, + "version": 103, + "versionNonce": 2009988634, "isDeleted": false, "id": "uAQf51mOXJz4uyUjWoj55", "fillStyle": "hachure", @@ -14195,7 +14195,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296406, + "updated": 1707755580534, "link": null, "locked": false, "fontSize": 20, @@ -14239,8 +14239,8 @@ }, { "type": "text", - "version": 58, - "versionNonce": 1889413233, + "version": 60, + "versionNonce": 850135174, "isDeleted": false, "id": "zEuehdc3bMVftmIh2a_Lj", "fillStyle": "hachure", @@ -14260,7 +14260,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296407, + "updated": 1707755580534, "link": null, "locked": false, "fontSize": 20, @@ -14304,8 +14304,8 @@ }, { "type": "text", - "version": 42, - "versionNonce": 235639583, + "version": 44, + "versionNonce": 609954522, "isDeleted": false, "id": "-NowFvWoyGBcG4bE3Eoe3", "fillStyle": "hachure", @@ -14325,7 +14325,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296407, + "updated": 1707755580534, "link": null, "locked": false, "fontSize": 20, @@ -14374,8 +14374,8 @@ }, { "type": "text", - "version": 108, - "versionNonce": 1484911185, + "version": 110, + "versionNonce": 1113917382, "isDeleted": false, "id": "KZiBuznSWGBjrOkeluaJh", "fillStyle": "hachure", @@ -14395,7 +14395,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296407, + "updated": 1707755580534, "link": null, "locked": false, "fontSize": 20, @@ -14444,8 +14444,8 @@ }, { "type": "text", - "version": 102, - "versionNonce": 1586091839, + "version": 104, + "versionNonce": 1210367898, "isDeleted": false, "id": "Cz3o8zCBmAKr3xDGJhqB2", "fillStyle": "hachure", @@ -14465,7 +14465,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296407, + "updated": 1707755580534, "link": null, "locked": false, "fontSize": 20, @@ -16133,8 +16133,8 @@ }, { "type": "text", - "version": 83, - "versionNonce": 152141873, + "version": 85, + "versionNonce": 1228676870, "isDeleted": false, "id": "TUqxZ7SnpcRy9MvMax_BO", "fillStyle": "hachure", @@ -16154,7 +16154,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296407, + "updated": 1707755580534, "link": null, "locked": false, "fontSize": 20, @@ -16169,8 +16169,8 @@ }, { "type": "text", - "version": 117, - "versionNonce": 1793759071, + "version": 119, + "versionNonce": 257801306, "isDeleted": false, "id": "6xdRAgKtKnk3_eGkmo0jO", "fillStyle": "hachure", @@ -16190,7 +16190,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296407, + "updated": 1707755580534, "link": null, "locked": false, "fontSize": 20, @@ -16763,8 +16763,8 @@ }, { "type": "text", - "version": 270, - "versionNonce": 3428881, + "version": 272, + "versionNonce": 1806585414, "isDeleted": false, "id": "Howa8TQy9WlNeQY7IFhdI", "fillStyle": "hachure", @@ -16784,7 +16784,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296407, + "updated": 1707755580534, "link": null, "locked": false, "fontSize": 28, @@ -16799,8 +16799,8 @@ }, { "type": "text", - "version": 208, - "versionNonce": 159273855, + "version": 210, + "versionNonce": 668541210, "isDeleted": false, "id": "-kMNckUw3lQqSqTjAISxd", "fillStyle": "hachure", @@ -16820,7 +16820,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296407, + "updated": 1707755580535, "link": null, "locked": false, "fontSize": 28, @@ -16931,8 +16931,8 @@ }, { "type": "text", - "version": 241, - "versionNonce": 316794865, + "version": 243, + "versionNonce": 915969414, "isDeleted": false, "id": "YPYEjVuQFyVe2PI-WkEpo", "fillStyle": "hachure", @@ -16957,7 +16957,7 @@ "type": "arrow" } ], - "updated": 1696427296407, + "updated": 1707755580535, "link": null, "locked": false, "fontSize": 28, @@ -17020,8 +17020,8 @@ }, { "type": "text", - "version": 168, - "versionNonce": 1921354655, + "version": 170, + "versionNonce": 218754522, "isDeleted": false, "id": "xBqw9q_jDO4kcgiPZ-6nK", "fillStyle": "hachure", @@ -17050,7 +17050,7 @@ "type": "arrow" } ], - "updated": 1696427296407, + "updated": 1707755580535, "link": null, "locked": false, "fontSize": 28, @@ -17117,8 +17117,8 @@ }, { "type": "text", - "version": 172, - "versionNonce": 186761681, + "version": 174, + "versionNonce": 322434246, "isDeleted": false, "id": "aC1hBzszkK5enBuqWlkCK", "fillStyle": "hachure", @@ -17147,7 +17147,7 @@ "type": "arrow" } ], - "updated": 1696427296407, + "updated": 1707755580535, "link": null, "locked": false, "fontSize": 28, @@ -17210,8 +17210,8 @@ }, { "type": "text", - "version": 131, - "versionNonce": 36749247, + "version": 133, + "versionNonce": 1409491610, "isDeleted": false, "id": "2SRBMgGBS9HO5oBhmR1Pm", "fillStyle": "hachure", @@ -17240,7 +17240,7 @@ "type": "arrow" } ], - "updated": 1696427296408, + "updated": 1707755580535, "link": null, "locked": false, "fontSize": 28, @@ -17651,8 +17651,8 @@ }, { "type": "text", - "version": 193, - "versionNonce": 101408689, + "version": 195, + "versionNonce": 992570374, "isDeleted": false, "id": "AjyE3hskerq5S0alAsJLS", "fillStyle": "hachure", @@ -17672,7 +17672,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296408, + "updated": 1707755580535, "link": null, "locked": false, "fontSize": 28, @@ -17787,8 +17787,8 @@ }, { "type": "text", - "version": 215, - "versionNonce": 983592927, + "version": 217, + "versionNonce": 2004336474, "isDeleted": false, "id": "2ImcFmDw5TePp9f6hhi4A", "fillStyle": "hachure", @@ -17821,7 +17821,7 @@ "type": "arrow" } ], - "updated": 1696427296408, + "updated": 1707755580535, "link": null, "locked": false, "fontSize": 28, @@ -18237,8 +18237,8 @@ }, { "type": "text", - "version": 353, - "versionNonce": 298205585, + "version": 355, + "versionNonce": 745143110, "isDeleted": false, "id": "ri5yBasROMNLv3BqfTkFY", "fillStyle": "hachure", @@ -18258,7 +18258,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296408, + "updated": 1707755580535, "link": null, "locked": false, "fontSize": 28, @@ -18321,8 +18321,8 @@ }, { "type": "text", - "version": 92, - "versionNonce": 98900991, + "version": 94, + "versionNonce": 599442458, "isDeleted": false, "id": "ZIXo5j24FVchmuML-JqrO", "fillStyle": "hachure", @@ -18342,7 +18342,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296408, + "updated": 1707755580536, "link": null, "locked": false, "fontSize": 28, @@ -19991,8 +19991,8 @@ }, { "type": "text", - "version": 431, - "versionNonce": 193587057, + "version": 433, + "versionNonce": 492216966, "isDeleted": false, "id": "XTD_gXU_gSq9V87gFisld", "fillStyle": "hachure", @@ -20012,7 +20012,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296408, + "updated": 1707755580536, "link": null, "locked": false, "fontSize": 28, @@ -20118,8 +20118,8 @@ }, { "type": "text", - "version": 450, - "versionNonce": 939475999, + "version": 452, + "versionNonce": 46084314, "isDeleted": false, "id": "z34KgsXM2TscgIAB9ooPf", "fillStyle": "hachure", @@ -20139,7 +20139,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296408, + "updated": 1707755580536, "link": null, "locked": false, "fontSize": 28, @@ -20604,8 +20604,8 @@ }, { "type": "text", - "version": 359, - "versionNonce": 1033752913, + "version": 361, + "versionNonce": 1890345414, "isDeleted": false, "id": "N9DAmwkBPCqomDAqz73Da", "fillStyle": "hachure", @@ -20625,7 +20625,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296408, + "updated": 1707755580536, "link": null, "locked": false, "fontSize": 28, @@ -20640,8 +20640,8 @@ }, { "type": "text", - "version": 282, - "versionNonce": 1356818495, + "version": 284, + "versionNonce": 135792026, "isDeleted": false, "id": "nac6VXoUQeADi7V2tIAyL", "fillStyle": "hachure", @@ -20661,7 +20661,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296408, + "updated": 1707755580536, "link": null, "locked": false, "fontSize": 28, @@ -20930,8 +20930,8 @@ }, { "type": "text", - "version": 225, - "versionNonce": 1029275441, + "version": 227, + "versionNonce": 696793350, "isDeleted": false, "id": "x4RtzbNrw0yT8GxZc_4P0", "fillStyle": "hachure", @@ -20951,7 +20951,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296408, + "updated": 1707755580536, "link": null, "locked": false, "fontSize": 28, @@ -21380,8 +21380,8 @@ }, { "type": "text", - "version": 398, - "versionNonce": 2120616031, + "version": 400, + "versionNonce": 1022902874, "isDeleted": false, "id": "xwwPm3GU_sRzaHk1Dnh99", "fillStyle": "hachure", @@ -21406,7 +21406,7 @@ "type": "arrow" } ], - "updated": 1696427296409, + "updated": 1707755580536, "link": null, "locked": false, "fontSize": 28, @@ -21421,8 +21421,8 @@ }, { "type": "text", - "version": 354, - "versionNonce": 153383185, + "version": 356, + "versionNonce": 1600596038, "isDeleted": false, "id": "F5igZKg0oi_RRfvl45Vf8", "fillStyle": "hachure", @@ -21442,7 +21442,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296409, + "updated": 1707755580537, "link": null, "locked": false, "fontSize": 28, @@ -21590,8 +21590,8 @@ }, { "type": "text", - "version": 328, - "versionNonce": 1583588479, + "version": 330, + "versionNonce": 204662554, "isDeleted": false, "id": "Okv5hntUC3DyIfWDsFiqX", "fillStyle": "hachure", @@ -21611,7 +21611,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296409, + "updated": 1707755580537, "link": null, "locked": false, "fontSize": 28, @@ -22044,8 +22044,8 @@ }, { "type": "text", - "version": 565, - "versionNonce": 1719019249, + "version": 567, + "versionNonce": 841363334, "isDeleted": false, "id": "E1sVYxk44xwhXS_sP4oVn", "fillStyle": "hachure", @@ -22065,7 +22065,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296409, + "updated": 1707755580537, "link": null, "locked": false, "fontSize": 28, @@ -22213,8 +22213,8 @@ }, { "type": "text", - "version": 535, - "versionNonce": 10009759, + "version": 537, + "versionNonce": 238744538, "isDeleted": false, "id": "JGiYtNmfw8M5Qu-vkVmHh", "fillStyle": "hachure", @@ -22234,7 +22234,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296409, + "updated": 1707755580537, "link": null, "locked": false, "fontSize": 28, @@ -23117,8 +23117,8 @@ }, { "type": "text", - "version": 525, - "versionNonce": 2144739537, + "version": 527, + "versionNonce": 1040338630, "isDeleted": false, "id": "hGRMhsF2Lt6tSi54-rnM5", "fillStyle": "hachure", @@ -23138,7 +23138,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296409, + "updated": 1707755580537, "link": null, "locked": false, "fontSize": 28, @@ -23153,8 +23153,8 @@ }, { "type": "text", - "version": 542, - "versionNonce": 683004095, + "version": 544, + "versionNonce": 1428383898, "isDeleted": false, "id": "Z87U81mHKsNQZGda8v-5z", "fillStyle": "hachure", @@ -23174,7 +23174,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296409, + "updated": 1707755580537, "link": null, "locked": false, "fontSize": 28, @@ -23237,8 +23237,8 @@ }, { "type": "text", - "version": 324, - "versionNonce": 1121705649, + "version": 326, + "versionNonce": 261654022, "isDeleted": false, "id": "75MkHAs2PaKs_YExiQaNq", "fillStyle": "hachure", @@ -23258,7 +23258,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296409, + "updated": 1707755580537, "link": null, "locked": false, "fontSize": 28, @@ -23273,8 +23273,8 @@ }, { "type": "text", - "version": 519, - "versionNonce": 1348165855, + "version": 521, + "versionNonce": 949962074, "isDeleted": false, "id": "eXXqVLxMZ-SINaZWEK6vz", "fillStyle": "hachure", @@ -23294,7 +23294,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296409, + "updated": 1707755580537, "link": null, "locked": false, "fontSize": 28, @@ -23592,8 +23592,8 @@ }, { "type": "text", - "version": 472, - "versionNonce": 2117001361, + "version": 474, + "versionNonce": 400938310, "isDeleted": false, "id": "eJdETe_wMa0w2MxgbDDoe", "fillStyle": "hachure", @@ -23613,7 +23613,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296409, + "updated": 1707755580537, "link": null, "locked": false, "fontSize": 28, @@ -23911,8 +23911,8 @@ }, { "type": "text", - "version": 530, - "versionNonce": 1763306751, + "version": 532, + "versionNonce": 570526234, "isDeleted": false, "id": "l_FDOh7PwBZ_RnXBuq1rf", "fillStyle": "hachure", @@ -23932,7 +23932,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296409, + "updated": 1707755580537, "link": null, "locked": false, "fontSize": 28, @@ -24230,8 +24230,8 @@ }, { "type": "text", - "version": 541, - "versionNonce": 1065789041, + "version": 543, + "versionNonce": 541539462, "isDeleted": false, "id": "jU6YsQJXfXX8LMOST6zpc", "fillStyle": "hachure", @@ -24251,7 +24251,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296410, + "updated": 1707755580538, "link": null, "locked": false, "fontSize": 28, @@ -24549,8 +24549,8 @@ }, { "type": "text", - "version": 599, - "versionNonce": 1584526623, + "version": 601, + "versionNonce": 1480406746, "isDeleted": false, "id": "JofDC9OYeEb-ry7vn6txt", "fillStyle": "hachure", @@ -24570,7 +24570,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296410, + "updated": 1707755580538, "link": null, "locked": false, "fontSize": 28, @@ -25273,8 +25273,8 @@ }, { "type": "text", - "version": 346, - "versionNonce": 1907011665, + "version": 348, + "versionNonce": 1048165318, "isDeleted": false, "id": "DgCGpzLU9rBvN5xs2dtna", "fillStyle": "hachure", @@ -25294,7 +25294,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296410, + "updated": 1707755580538, "link": null, "locked": false, "fontSize": 28, @@ -25837,8 +25837,8 @@ }, { "type": "text", - "version": 394, - "versionNonce": 1473509695, + "version": 396, + "versionNonce": 1273318298, "isDeleted": false, "id": "opI1A3GHk-glrkVHWtnzm", "fillStyle": "hachure", @@ -25858,7 +25858,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296410, + "updated": 1707755580538, "link": null, "locked": false, "fontSize": 19.993669122032355, @@ -25963,8 +25963,8 @@ }, { "type": "text", - "version": 428, - "versionNonce": 980581937, + "version": 430, + "versionNonce": 1218073350, "isDeleted": false, "id": "YeZy94byHPYDUDPEghHwz", "fillStyle": "hachure", @@ -25984,7 +25984,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296411, + "updated": 1707755580538, "link": null, "locked": false, "fontSize": 19.993669122032355, @@ -26037,8 +26037,8 @@ }, { "type": "text", - "version": 459, - "versionNonce": 983595359, + "version": 461, + "versionNonce": 1663879258, "isDeleted": false, "id": "oywRGTc7Qk21fMO5m7ipR", "fillStyle": "hachure", @@ -26058,7 +26058,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296411, + "updated": 1707755580538, "link": null, "locked": false, "fontSize": 19.99366912203235, @@ -26163,8 +26163,8 @@ }, { "type": "text", - "version": 491, - "versionNonce": 415600657, + "version": 493, + "versionNonce": 1227227718, "isDeleted": false, "id": "aKLeLpuGjjphQKAjHsJjd", "fillStyle": "hachure", @@ -26184,7 +26184,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296411, + "updated": 1707755580538, "link": null, "locked": false, "fontSize": 19.993669122032355, @@ -26453,8 +26453,8 @@ }, { "type": "text", - "version": 411, - "versionNonce": 1814558079, + "version": 413, + "versionNonce": 1172745498, "isDeleted": false, "id": "-LC6sWVh5jazUS4DtR93c", "fillStyle": "hachure", @@ -26474,7 +26474,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296411, + "updated": 1707755580538, "link": null, "locked": false, "fontSize": 19.993669122032355, @@ -26541,8 +26541,8 @@ }, { "type": "text", - "version": 346, - "versionNonce": 1117480433, + "version": 348, + "versionNonce": 1928972678, "isDeleted": false, "id": "2Y2E7wJPH9_gmr4i3S7pg", "fillStyle": "hachure", @@ -26562,7 +26562,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296411, + "updated": 1707755580539, "link": null, "locked": false, "fontSize": 19.993669122032355, @@ -26675,8 +26675,8 @@ }, { "type": "text", - "version": 502, - "versionNonce": 1403710879, + "version": 504, + "versionNonce": 696646106, "isDeleted": false, "id": "wR0GHC010wmNgfVtTA0EH", "fillStyle": "hachure", @@ -26701,7 +26701,7 @@ "type": "arrow" } ], - "updated": 1696427296411, + "updated": 1707755580539, "link": null, "locked": false, "fontSize": 19.993669122032355, @@ -26818,8 +26818,8 @@ }, { "type": "text", - "version": 798, - "versionNonce": 1123778513, + "version": 800, + "versionNonce": 357738694, "isDeleted": false, "id": "ZR7FlVxJKJhypbs2k28RF", "fillStyle": "hachure", @@ -26856,7 +26856,7 @@ "type": "arrow" } ], - "updated": 1696427296412, + "updated": 1707755580539, "link": null, "locked": false, "fontSize": 19.993669122032355, @@ -27021,8 +27021,8 @@ }, { "type": "text", - "version": 490, - "versionNonce": 475652543, + "version": 492, + "versionNonce": 488484506, "isDeleted": false, "id": "KrxqLrVYs9q4JJfaCjd4e", "fillStyle": "hachure", @@ -27051,7 +27051,7 @@ "type": "arrow" } ], - "updated": 1696427296412, + "updated": 1707755580539, "link": null, "locked": false, "fontSize": 20, @@ -27095,8 +27095,8 @@ }, { "type": "text", - "version": 258, - "versionNonce": 1476222385, + "version": 260, + "versionNonce": 265134086, "isDeleted": false, "id": "h3exS8HSaQTUutxxP5KUd", "fillStyle": "hachure", @@ -27116,7 +27116,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296412, + "updated": 1707755580539, "link": null, "locked": false, "fontSize": 20, @@ -27165,8 +27165,8 @@ }, { "type": "text", - "version": 286, - "versionNonce": 928338399, + "version": 288, + "versionNonce": 1432406874, "isDeleted": false, "id": "sZwffdBtAx5k-pfeeH9pz", "fillStyle": "hachure", @@ -27186,7 +27186,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296412, + "updated": 1707755580539, "link": null, "locked": false, "fontSize": 20, @@ -27201,8 +27201,8 @@ }, { "type": "text", - "version": 219, - "versionNonce": 281575313, + "version": 221, + "versionNonce": 1181200198, "isDeleted": false, "id": "8VlmUF_Qt4ApUNPz0uyu9", "fillStyle": "hachure", @@ -27222,7 +27222,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296412, + "updated": 1707755580539, "link": null, "locked": false, "fontSize": 20, @@ -27520,8 +27520,8 @@ }, { "type": "text", - "version": 369, - "versionNonce": 497925631, + "version": 371, + "versionNonce": 2023903258, "isDeleted": false, "id": "uCzkLthQbcSGl2d6NC343", "fillStyle": "hachure", @@ -27541,7 +27541,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296412, + "updated": 1707755580539, "link": null, "locked": false, "fontSize": 20, @@ -27556,8 +27556,8 @@ }, { "type": "text", - "version": 415, - "versionNonce": 731145585, + "version": 417, + "versionNonce": 360568454, "isDeleted": false, "id": "DuEE3eExprGc4nGfb45In", "fillStyle": "hachure", @@ -27577,7 +27577,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296412, + "updated": 1707755580539, "link": null, "locked": false, "fontSize": 20, @@ -27621,8 +27621,8 @@ }, { "type": "text", - "version": 486, - "versionNonce": 1382262303, + "version": 488, + "versionNonce": 1792359642, "isDeleted": false, "id": "tUM3WeCUqPHDKsr80vNyx", "fillStyle": "hachure", @@ -27642,7 +27642,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296412, + "updated": 1707755580539, "link": null, "locked": false, "fontSize": 20, @@ -27785,8 +27785,8 @@ }, { "type": "text", - "version": 401, - "versionNonce": 866684753, + "version": 403, + "versionNonce": 1171068358, "isDeleted": false, "id": "_m86-4rF1_MnbU0EYK2qV", "fillStyle": "hachure", @@ -27806,7 +27806,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296412, + "updated": 1707755580540, "link": null, "locked": false, "fontSize": 20, @@ -29123,8 +29123,8 @@ }, { "type": "text", - "version": 337, - "versionNonce": 610765375, + "version": 339, + "versionNonce": 428642714, "isDeleted": false, "id": "YZ9v3JHi75hcJXtdWoC80", "fillStyle": "hachure", @@ -29144,7 +29144,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296412, + "updated": 1707755580540, "link": null, "locked": false, "fontSize": 20, @@ -30142,8 +30142,8 @@ }, { "type": "text", - "version": 477, - "versionNonce": 1599896881, + "version": 479, + "versionNonce": 7499014, "isDeleted": false, "id": "K63fthKq1zDCMVb_JfTxI", "fillStyle": "hachure", @@ -30163,7 +30163,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296413, + "updated": 1707755580540, "link": null, "locked": false, "fontSize": 20, @@ -30207,8 +30207,8 @@ }, { "type": "text", - "version": 483, - "versionNonce": 815393375, + "version": 485, + "versionNonce": 1207651930, "isDeleted": false, "id": "SZx434IEAyN8u9Iok9wKs", "fillStyle": "hachure", @@ -30228,7 +30228,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296413, + "updated": 1707755580540, "link": null, "locked": false, "fontSize": 20, @@ -30272,8 +30272,8 @@ }, { "type": "text", - "version": 476, - "versionNonce": 773304081, + "version": 478, + "versionNonce": 1122688070, "isDeleted": false, "id": "PXg6Txuhjr9XnACRPA3o1", "fillStyle": "hachure", @@ -30293,7 +30293,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296413, + "updated": 1707755580540, "link": null, "locked": false, "fontSize": 20, @@ -30337,8 +30337,8 @@ }, { "type": "text", - "version": 158, - "versionNonce": 600524415, + "version": 160, + "versionNonce": 1525969690, "isDeleted": false, "id": "PsCFAZngKhjlM8aBiROp_", "fillStyle": "hachure", @@ -30358,7 +30358,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296413, + "updated": 1707755580540, "link": null, "locked": false, "fontSize": 20, @@ -30545,8 +30545,8 @@ }, { "type": "text", - "version": 434, - "versionNonce": 1179755761, + "version": 436, + "versionNonce": 1393779590, "isDeleted": false, "id": "I3IyBpdIHc46DQR-k_9ug", "fillStyle": "hachure", @@ -30566,7 +30566,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296413, + "updated": 1707755580540, "link": null, "locked": false, "fontSize": 20, @@ -30914,8 +30914,8 @@ }, { "type": "text", - "version": 404, - "versionNonce": 1018961567, + "version": 406, + "versionNonce": 619380698, "isDeleted": false, "id": "cwQfv7ctKwc9rzmOEdeLR", "fillStyle": "hachure", @@ -30935,7 +30935,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296413, + "updated": 1707755580540, "link": null, "locked": false, "fontSize": 20, @@ -31123,8 +31123,8 @@ }, { "type": "text", - "version": 360, - "versionNonce": 328326865, + "version": 362, + "versionNonce": 858325702, "isDeleted": false, "id": "rCH1-6E8SS-myByDCU3hi", "fillStyle": "hachure", @@ -31144,7 +31144,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296413, + "updated": 1707755580540, "link": null, "locked": false, "fontSize": 20, @@ -31159,8 +31159,8 @@ }, { "type": "text", - "version": 281, - "versionNonce": 236778175, + "version": 283, + "versionNonce": 837940378, "isDeleted": false, "id": "sIQsIbPKVe0sTNSdQqzJp", "fillStyle": "hachure", @@ -31180,7 +31180,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296413, + "updated": 1707755580540, "link": null, "locked": false, "fontSize": 20, @@ -32726,8 +32726,8 @@ }, { "type": "text", - "version": 81, - "versionNonce": 611310769, + "version": 83, + "versionNonce": 365476358, "isDeleted": false, "id": "IZmYbAvhwLOI-Fvyn9Eea", "fillStyle": "hachure", @@ -32747,7 +32747,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296413, + "updated": 1707755580540, "link": null, "locked": false, "fontSize": 20, @@ -32939,8 +32939,8 @@ }, { "type": "text", - "version": 169, - "versionNonce": 9323231, + "version": 171, + "versionNonce": 331108698, "isDeleted": false, "id": "60rTmjiMEDvJ_9hKBmY8v", "fillStyle": "hachure", @@ -32960,7 +32960,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296413, + "updated": 1707755580540, "link": null, "locked": false, "fontSize": 20, @@ -33193,8 +33193,8 @@ }, { "type": "text", - "version": 634, - "versionNonce": 1217601169, + "version": 636, + "versionNonce": 1374652742, "isDeleted": false, "id": "c6ZdG-H-hh_aHr6l1y-JA", "fillStyle": "hachure", @@ -33214,7 +33214,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296413, + "updated": 1707755580541, "link": null, "locked": false, "fontSize": 28, @@ -33356,8 +33356,8 @@ }, { "type": "text", - "version": 520, - "versionNonce": 346066687, + "version": 522, + "versionNonce": 765269530, "isDeleted": false, "id": "nC5afePC_WoGOZEuB2_DM", "fillStyle": "hachure", @@ -33382,7 +33382,7 @@ "type": "arrow" } ], - "updated": 1696427296414, + "updated": 1707755580541, "link": null, "locked": false, "fontSize": 20, @@ -33684,8 +33684,8 @@ }, { "type": "text", - "version": 330, - "versionNonce": 1857234033, + "version": 332, + "versionNonce": 1459253382, "isDeleted": false, "id": "944be9DVdoDP1qmUSQ59m", "fillStyle": "hachure", @@ -33705,7 +33705,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296414, + "updated": 1707755580541, "link": null, "locked": false, "fontSize": 20, @@ -34115,8 +34115,8 @@ }, { "type": "text", - "version": 51, - "versionNonce": 1691637535, + "version": 53, + "versionNonce": 8864474, "isDeleted": false, "id": "WG4oyhU-1p0gKRoDxmUKT", "fillStyle": "hachure", @@ -34136,7 +34136,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296414, + "updated": 1707755580541, "link": null, "locked": false, "fontSize": 28, @@ -34151,8 +34151,8 @@ }, { "type": "text", - "version": 154, - "versionNonce": 1117220433, + "version": 156, + "versionNonce": 547778502, "isDeleted": false, "id": "NWyEiRRktVlteGkjN1DUO", "fillStyle": "hachure", @@ -34172,7 +34172,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296414, + "updated": 1707755580541, "link": null, "locked": false, "fontSize": 20, @@ -34187,8 +34187,8 @@ }, { "type": "text", - "version": 96, - "versionNonce": 1738152767, + "version": 98, + "versionNonce": 1997395866, "isDeleted": false, "id": "PWAFoR0ONAwARI9lv6fZi", "fillStyle": "hachure", @@ -34208,7 +34208,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296414, + "updated": 1707755580541, "link": null, "locked": false, "fontSize": 28, @@ -34223,8 +34223,8 @@ }, { "type": "text", - "version": 292, - "versionNonce": 186088497, + "version": 294, + "versionNonce": 722503430, "isDeleted": false, "id": "naYXCEyAtglpnFE1iFeW8", "fillStyle": "hachure", @@ -34244,7 +34244,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296414, + "updated": 1707755580541, "link": null, "locked": false, "fontSize": 20, @@ -34516,8 +34516,8 @@ }, { "type": "text", - "version": 93, - "versionNonce": 1901222751, + "version": 95, + "versionNonce": 828626010, "isDeleted": false, "id": "-jHwBLdCc-wyPpCHePRzN", "fillStyle": "hachure", @@ -34537,7 +34537,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296414, + "updated": 1707755580541, "link": null, "locked": false, "fontSize": 28, @@ -34552,8 +34552,8 @@ }, { "type": "text", - "version": 189, - "versionNonce": 1209716241, + "version": 191, + "versionNonce": 1723184710, "isDeleted": false, "id": "teLU9J9uK3obJhfHwG_0O", "fillStyle": "hachure", @@ -34573,7 +34573,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296414, + "updated": 1707755580541, "link": null, "locked": false, "fontSize": 20, @@ -34674,8 +34674,8 @@ }, { "type": "text", - "version": 74, - "versionNonce": 1984796543, + "version": 76, + "versionNonce": 1364998426, "isDeleted": false, "id": "xZ9iEMva-bcaVH1BW0yM_", "fillStyle": "hachure", @@ -34700,7 +34700,7 @@ "type": "arrow" } ], - "updated": 1696427296414, + "updated": 1707755580542, "link": null, "locked": false, "fontSize": 20, @@ -34831,8 +34831,8 @@ }, { "type": "text", - "version": 120, - "versionNonce": 278455281, + "version": 122, + "versionNonce": 745733510, "isDeleted": false, "id": "5rFVQmM8KYHOhTyQTnUOj", "fillStyle": "hachure", @@ -34857,7 +34857,7 @@ "type": "arrow" } ], - "updated": 1696427296415, + "updated": 1707755580542, "link": null, "locked": false, "fontSize": 20, @@ -34976,8 +34976,8 @@ }, { "type": "text", - "version": 65, - "versionNonce": 214716319, + "version": 67, + "versionNonce": 1181353434, "isDeleted": false, "id": "xp5RuGA6ZiZFGhVA4aOdw", "fillStyle": "hachure", @@ -34997,7 +34997,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296415, + "updated": 1707755580542, "link": null, "locked": false, "fontSize": 28, @@ -35185,8 +35185,8 @@ }, { "type": "text", - "version": 117, - "versionNonce": 815349201, + "version": 119, + "versionNonce": 830823622, "isDeleted": false, "id": "BK9llMbWheyXF5BFMMfI-", "fillStyle": "hachure", @@ -35215,7 +35215,7 @@ "type": "arrow" } ], - "updated": 1696427296415, + "updated": 1707755580542, "link": null, "locked": false, "fontSize": 28, @@ -35230,8 +35230,8 @@ }, { "type": "text", - "version": 170, - "versionNonce": 788464575, + "version": 172, + "versionNonce": 429931162, "isDeleted": false, "id": "u3V4iaPha06p_gH0O3qcZ", "fillStyle": "hachure", @@ -35251,7 +35251,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296415, + "updated": 1707755580542, "link": null, "locked": false, "fontSize": 20, @@ -35535,8 +35535,8 @@ }, { "type": "text", - "version": 253, - "versionNonce": 120806321, + "version": 255, + "versionNonce": 2072630278, "isDeleted": false, "id": "GT3BpJMY46LwoRlz2Q4pa", "fillStyle": "hachure", @@ -35565,7 +35565,7 @@ "type": "arrow" } ], - "updated": 1696427296415, + "updated": 1707755580542, "link": null, "locked": false, "fontSize": 28, @@ -36081,8 +36081,8 @@ }, { "type": "text", - "version": 280, - "versionNonce": 1023816671, + "version": 282, + "versionNonce": 967956314, "isDeleted": false, "id": "vjU5mGB6m4JGkVNpNNuvJ", "fillStyle": "hachure", @@ -36111,7 +36111,7 @@ "type": "arrow" } ], - "updated": 1696427296415, + "updated": 1707755580542, "link": null, "locked": false, "fontSize": 28, @@ -36458,8 +36458,8 @@ }, { "type": "text", - "version": 72, - "versionNonce": 613334417, + "version": 74, + "versionNonce": 1417503558, "isDeleted": false, "id": "TQdPnFA8cvQ3ceam-sLvc", "fillStyle": "hachure", @@ -36479,7 +36479,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296415, + "updated": 1707755580542, "link": null, "locked": false, "fontSize": 20, @@ -36494,8 +36494,8 @@ }, { "type": "text", - "version": 69, - "versionNonce": 1056071679, + "version": 71, + "versionNonce": 1190255642, "isDeleted": false, "id": "uJ9sLSzwi234RYNdJF4C_", "fillStyle": "hachure", @@ -36515,7 +36515,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296415, + "updated": 1707755580542, "link": null, "locked": false, "fontSize": 20, @@ -36530,8 +36530,8 @@ }, { "type": "text", - "version": 69, - "versionNonce": 669180785, + "version": 71, + "versionNonce": 1052576390, "isDeleted": false, "id": "AQJC6-WKMIMjExjCql-tv", "fillStyle": "hachure", @@ -36551,7 +36551,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296415, + "updated": 1707755580542, "link": null, "locked": false, "fontSize": 20, @@ -36566,8 +36566,8 @@ }, { "type": "text", - "version": 67, - "versionNonce": 1724123167, + "version": 69, + "versionNonce": 1599293658, "isDeleted": false, "id": "oIgrnnwsPHeKvY9Znhkhk", "fillStyle": "hachure", @@ -36587,7 +36587,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296415, + "updated": 1707755580542, "link": null, "locked": false, "fontSize": 20, @@ -36602,8 +36602,8 @@ }, { "type": "text", - "version": 202, - "versionNonce": 1568099665, + "version": 204, + "versionNonce": 1761987014, "isDeleted": false, "id": "GiECrqsWtMc7YBX7TQXj9", "fillStyle": "hachure", @@ -36623,7 +36623,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296415, + "updated": 1707755580542, "link": null, "locked": false, "fontSize": 20, @@ -36638,8 +36638,8 @@ }, { "type": "text", - "version": 106, - "versionNonce": 1382788159, + "version": 108, + "versionNonce": 1785273754, "isDeleted": false, "id": "l82lHezNr7rBdUPYIY6lC", "fillStyle": "hachure", @@ -36659,7 +36659,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296416, + "updated": 1707755580543, "link": null, "locked": false, "fontSize": 20, @@ -36674,8 +36674,8 @@ }, { "type": "text", - "version": 102, - "versionNonce": 480475953, + "version": 104, + "versionNonce": 578068742, "isDeleted": false, "id": "_e0jvtDX1g5qBGA_m6Tdt", "fillStyle": "hachure", @@ -36695,7 +36695,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296416, + "updated": 1707755580543, "link": null, "locked": false, "fontSize": 20, @@ -36710,8 +36710,8 @@ }, { "type": "text", - "version": 99, - "versionNonce": 231328863, + "version": 101, + "versionNonce": 1701206618, "isDeleted": false, "id": "7Yy76wraDR8OeKf7GUId0", "fillStyle": "hachure", @@ -36731,7 +36731,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296416, + "updated": 1707755580543, "link": null, "locked": false, "fontSize": 20, @@ -36746,8 +36746,8 @@ }, { "type": "text", - "version": 197, - "versionNonce": 1708059921, + "version": 199, + "versionNonce": 1317441606, "isDeleted": false, "id": "Sk0DFnPwXzqPssasM_enB", "fillStyle": "hachure", @@ -36767,7 +36767,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296416, + "updated": 1707755580543, "link": null, "locked": false, "fontSize": 20, @@ -36782,8 +36782,8 @@ }, { "type": "text", - "version": 101, - "versionNonce": 1420748927, + "version": 103, + "versionNonce": 790495002, "isDeleted": false, "id": "tGgqSQu5udJDTSdL421b0", "fillStyle": "hachure", @@ -36803,7 +36803,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296416, + "updated": 1707755580543, "link": null, "locked": false, "fontSize": 20, @@ -36818,8 +36818,8 @@ }, { "type": "text", - "version": 100, - "versionNonce": 1081156337, + "version": 102, + "versionNonce": 1494783878, "isDeleted": false, "id": "Tn6ktXLiaZCstCZsdtbRR", "fillStyle": "hachure", @@ -36839,7 +36839,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296416, + "updated": 1707755580543, "link": null, "locked": false, "fontSize": 20, @@ -36854,8 +36854,8 @@ }, { "type": "text", - "version": 94, - "versionNonce": 349929631, + "version": 96, + "versionNonce": 1409485786, "isDeleted": false, "id": "OdmD68wjP_SeEpVxuZdB1", "fillStyle": "hachure", @@ -36875,7 +36875,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296416, + "updated": 1707755580543, "link": null, "locked": false, "fontSize": 20, @@ -37046,8 +37046,8 @@ }, { "type": "text", - "version": 323, - "versionNonce": 1494326481, + "version": 325, + "versionNonce": 711440070, "isDeleted": false, "id": "v4V7UB5vr1cfdDv8LMUdW", "fillStyle": "hachure", @@ -37067,7 +37067,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296416, + "updated": 1707755580543, "link": null, "locked": false, "fontSize": 20.625669225575916, @@ -37120,8 +37120,8 @@ }, { "type": "text", - "version": 612, - "versionNonce": 805311679, + "version": 614, + "versionNonce": 1512603802, "isDeleted": false, "id": "aEjjK2Bpe7tF3MVW-245J", "fillStyle": "hachure", @@ -37146,7 +37146,7 @@ "type": "arrow" } ], - "updated": 1696427296416, + "updated": 1707755580543, "link": null, "locked": false, "fontSize": 20, @@ -37466,8 +37466,8 @@ }, { "type": "text", - "version": 204, - "versionNonce": 1598544561, + "version": 206, + "versionNonce": 454094342, "isDeleted": false, "id": "Pqjfq6ESjYNJY2LwSjUUx", "fillStyle": "hachure", @@ -37487,7 +37487,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296417, + "updated": 1707755580543, "link": null, "locked": false, "fontSize": 28, @@ -37502,8 +37502,8 @@ }, { "type": "text", - "version": 309, - "versionNonce": 2109547743, + "version": 311, + "versionNonce": 222387546, "isDeleted": false, "id": "GU8ODILn2LtYkITW8XY3w", "fillStyle": "hachure", @@ -37523,7 +37523,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296417, + "updated": 1707755580544, "link": null, "locked": false, "fontSize": 20, @@ -37538,8 +37538,8 @@ }, { "type": "text", - "version": 501, - "versionNonce": 599481489, + "version": 503, + "versionNonce": 1745960262, "isDeleted": false, "id": "bKJ-SIITDhp53QZH3jU1d", "fillStyle": "hachure", @@ -37559,7 +37559,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296417, + "updated": 1707755580544, "link": null, "locked": false, "fontSize": 28, @@ -37857,8 +37857,8 @@ }, { "type": "text", - "version": 580, - "versionNonce": 228798719, + "version": 582, + "versionNonce": 1252041242, "isDeleted": false, "id": "fRZUAtibbT04Fhhlgr0qA", "fillStyle": "hachure", @@ -37878,7 +37878,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1696427296417, + "updated": 1707755580544, "link": null, "locked": false, "fontSize": 28, diff --git a/docs/architecture.md b/docs/architecture.md index f8b58eda8..9b0d37373 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -6,7 +6,7 @@ KubeHound works in 3 steps: 2. Compute attack paths 3. Write the results to a local graph database (JanusGraph) -After the initial ingestion is done, you use a compatible client (such as [gdotv](https://gdotv.com/)) to visualize and query attack paths in your cluster. +After the initial ingestion is done, you use a compatible client or the provided [Jupyter Notebook](../../deployments/kubehound/notebook/KubeHound.ipynb) to visualize and query attack paths in your cluster. [![KubeHound architecture (click to enlarge)](./images/kubehound-high-level.png)](./images/kubehound-high-level.png) diff --git a/docs/images/example-graph.png b/docs/images/example-graph.png index 3e016233f..7057b6fe0 100644 Binary files a/docs/images/example-graph.png and b/docs/images/example-graph.png differ diff --git a/docs/index.md b/docs/index.md index 194209236..37956d8f1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,7 +19,7 @@ After it has ingested data from your cluster, it can easily answer advanced ques - What is the shortest exploitable path between a publicly-exposed service and a cluster administrator role? - Is there an attack path from a specific container to a node in the cluster? -KubeHound was built with efficiency in mind and can consequently handle very large clusters. Ingestion and computation of attack paths typically takes 1 minute for a cluster with 1'000 running pods, 15 minutes for 10'000 pods, and 25 minutes for 25'000 pods. +KubeHound was built with efficiency in mind and can consequently handle very large clusters. Ingestion and computation of attack paths typically takes a few seconds for a cluster with 1'000 running pods, 2 minutes for 10'000 pods, and 5 minutes for 25'000 pods. Next steps: diff --git a/docs/user-guide/getting-started.md b/docs/user-guide/getting-started.md index eb264130f..0b5ed200e 100644 --- a/docs/user-guide/getting-started.md +++ b/docs/user-guide/getting-started.md @@ -77,13 +77,7 @@ INFO[0039] Attack graph generation complete in 39.108174109s component=kubehoun ## Access the KubeHound data At this point, the KubeHound data has been ingested in KubeHound's [graph database](../architecture.md). -You can use any client that supports accessing JanusGraph - we recommend using [gdotv](https://gdotv.com/): - -- Download and install gdotv from the [official website](https://gdotv.com/) -- Create a connection to the local KubeHound JanusGraph instance - 1. Click on the `New database connection` button - 2. Enter `localhost` as an hostname, and click on the `Test connection` button - 3. Once the connection is successful, click `Submit` - you're good to go! +You can use any client that supports accessing JanusGraph - a comprehensive list is available on the [JanusGraph home page](https://janusgraph.org/). We also provide a showcase [Jupyter Notebook](../../deployments/kubehound/notebook/KubeHound.ipynb) to get you started. This is accessible on [http://locahost:8888](http://locahost:8888) after starting KubeHound backend. The default password is `admin` but you can change this by setting the `NOTEBOOK_PASSWORD` environment variable in your `.env file`. ## Visualize and query the KubeHound data diff --git a/scripts/kubehound.bat b/scripts/kubehound.bat index 03e2993ab..a88091bc7 100644 --- a/scripts/kubehound.bat +++ b/scripts/kubehound.bat @@ -6,6 +6,8 @@ set KUBEHOUND_ENV=release set DOCKER_CMD=docker set DOCKER_COMPOSE_FILE_PATH=-f deployments\kubehound\docker-compose.yaml set DOCKER_COMPOSE_FILE_PATH=%DOCKER_COMPOSE_FILE_PATH% -f deployments\kubehound\docker-compose.release.yaml +set DOCKER_COMPOSE_FILE_PATH=%DOCKER_COMPOSE_FILE_PATH% -f deployments/kubehound/docker-compose.ui.yaml + if not "%DD_API_KEY%"=="" ( set DOCKER_COMPOSE_FILE_PATH=%DOCKER_COMPOSE_FILE_PATH% -f deployments\kubehound\docker-compose.datadog.yaml ) diff --git a/scripts/kubehound.sh b/scripts/kubehound.sh index 7805c4608..73bcb97d4 100755 --- a/scripts/kubehound.sh +++ b/scripts/kubehound.sh @@ -9,6 +9,8 @@ KUBEHOUND_ENV="release" # Pull in the requisite compose files for the current setup DOCKER_COMPOSE_FILE_PATH="-f deployments/kubehound/docker-compose.yaml" DOCKER_COMPOSE_FILE_PATH+=" -f deployments/kubehound/docker-compose.release.yaml" +DOCKER_COMPOSE_FILE_PATH+=" -f deployments/kubehound/docker-compose.ui.yaml" + if [ -n "${DD_API_KEY}" ]; then DOCKER_COMPOSE_FILE_PATH+=" -f deployments/kubehound/docker-compose.datadog.yaml" fi