diff --git a/deployments/kubehound/notebook/BlueTeam.ipynb b/deployments/kubehound/notebook/BlueTeam.ipynb index 595627945..0bd5219d1 100644 --- a/deployments/kubehound/notebook/BlueTeam.ipynb +++ b/deployments/kubehound/notebook/BlueTeam.ipynb @@ -28,6 +28,7 @@ }, "outputs": [], "source": [ + "%%capture \"Remove this line to see debug information\"\n", "%%graph_notebook_vis_options\n", "{\n", " \"edges\": {\n", @@ -72,12 +73,13 @@ "outputs": [], "source": [ "%%gremlin\n", - "kh.identities().\n", - " or(\n", + "kh.identities()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get())\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\")" + " has(\"type\", \"User\").has(\"name\", \"bits.barkley@datadoghq.com\"))\n", + " .hasCriticalPath()\n", + " .values(\"name\")" ] }, { @@ -95,13 +97,14 @@ "source": [ "\n", "%%gremlin -d class -g critical -le 50 -p inv,oute\n", - "kh.identities().\n", - " or(\n", + "kh.identities()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get())\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" + " 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" ] }, { @@ -129,13 +132,14 @@ "outputs": [], "source": [ "%%gremlin\n", - "kh.containers().\n", - " or(\n", + "kh.containers()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get())\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()" + " has(\"image\", TextP.containing(\"cilium\"))) // Replace with your image name\n", + " .hasCriticalPath()\n", + " .values(\"name\")\n", + " .dedup()" ] }, { @@ -152,13 +156,14 @@ "outputs": [], "source": [ "%%gremlin -d class -g critical -le 50 -p inv,oute\n", - "kh.containers().\n", - " or(\n", + "kh.containers()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get())\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" + " 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" ] }, { @@ -186,15 +191,17 @@ "outputs": [], "source": [ "%%gremlin\n", - "kh.containers().\n", - " where(out().hasLabel(\"Node\")).\n", - " or(\n", + "kh.containers()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get())\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()" + " has(\"image\", TextP.containing(\"cilium\")) // Replace with your image name\n", + " ) \n", + " .project('image',\"escapes\")\n", + " .by(values(\"image\"))\n", + " .by(outE().where(inV().hasLabel(\"Node\")).label().fold())\n", + " .dedup()" ] }, { @@ -215,11 +222,12 @@ "outputs": [], "source": [ "%%gremlin \n", - "kh.containers().\n", - " or(\n", + "kh.containers()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get())\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()" + " has(\"image\", TextP.containing(\"cilium\"))) // Replace with your image name\n", + " .minHopsToCritical()" ] }, { @@ -236,23 +244,21 @@ "outputs": [], "source": [ "%%gremlin -d class -g critical -le 50 -p inv,oute\n", - "kh.containers().\n", - " or(\n", + "kh.containers()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get())\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())" + " has(\"image\", TextP.containing(\"cilium\"))) // Replace with your image name\n", + " .repeat(\n", + " outE().inV().simplePath())\n", + " .emit()\n", + " .until(\n", + " has(\"critical\", true)\n", + " .or().loops().is(4))\n", + " .has(\"critical\", true)\n", + " .dedup()\n", + " .path()\n", + " .by(elementMap())" ] }, { @@ -271,17 +277,18 @@ "outputs": [], "source": [ "%%gremlin -d name -g class -le 50 -p inv,oute\n", - "kh.containers().\n", - " or(\n", + "kh.containers()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get())\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" + " has(\"image\", TextP.containing(\"cilium\")))\n", + " \t.repeat(\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" ] } ], diff --git a/deployments/kubehound/notebook/InitialSetup.ipynb b/deployments/kubehound/notebook/InitialSetup.ipynb new file mode 100644 index 000000000..1a7483d2c --- /dev/null +++ b/deployments/kubehound/notebook/InitialSetup.ipynb @@ -0,0 +1,150 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "execution": { + "iopub.execute_input": "2024-07-24T14:46:25.538529Z", + "iopub.status.busy": "2024-07-24T14:46:25.538280Z", + "iopub.status.idle": "2024-07-24T14:46:25.545724Z", + "shell.execute_reply": "2024-07-24T14:46:25.545041Z", + "shell.execute_reply.started": "2024-07-24T14:46:25.538501Z" + }, + "frozen": false, + "init_cell": true, + "tags": [ + "safe_output" + ] + }, + "source": [ + "# Autoloading\n", + "\n", + "Loading graph visualisation settings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "init_cell": true + }, + "outputs": [], + "source": [ + "%%capture \"Remove this line to see debug information\"\n", + "%%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": { + "frozen": false, + "tags": [ + "unsafe_output" + ] + }, + "source": [ + "# Initial Setup\n", + "\n", + "## Get a view of all Ingested Cluster\n", + "\n", + "Retrieve all the current cluster ingested in KubeHound with the associated runID with the number of nodes. This numbers can be used to get a clue of the size of the cluster and also identify if an ingestion did not complete." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": { + "iopub.execute_input": "2024-07-24T14:46:28.273187Z", + "iopub.status.busy": "2024-07-24T14:46:28.272852Z", + "iopub.status.idle": "2024-07-24T14:46:28.625859Z", + "shell.execute_reply": "2024-07-24T14:46:28.625126Z", + "shell.execute_reply.started": "2024-07-24T14:46:28.273156Z" + }, + "frozen": false, + "init_cell": true, + "tags": [ + "safe_output" + ] + }, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.nodes()\n", + " .groupCount()\n", + " .by(project('cluster','runID')\n", + " .by('cluster').by('runID'))\n", + " .unfold()\n", + " .limit(1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "frozen": false, + "tags": [ + "unsafe_output" + ] + }, + "source": [ + "## Setting your run_id/cluster\n", + "\n", + "Set which runID you want to use. The variable are being shared with all users of the instance, so we advise to make a uniq string for your user `runID_yourid` to avoid any conflict." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "frozen": false, + "tags": [ + "safe_output" + ] + }, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "graph.variables()\n", + " .set('runID_yourid','01htdgjj34mcmrrksw4bjy2e94')" + ] + } + ], + "metadata": { + "celltoolbar": "Initialization Cell", + "kernelspec": { + "display_name": "Python 3 (default)", + "language": "python", + "name": "ipykernel-default" + }, + "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.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/deployments/kubehound/notebook/KindCluster_Demo.ipynb b/deployments/kubehound/notebook/KindCluster_Demo.ipynb index 0ffe2bf52..7a39dce84 100644 --- a/deployments/kubehound/notebook/KindCluster_Demo.ipynb +++ b/deployments/kubehound/notebook/KindCluster_Demo.ipynb @@ -31,6 +31,7 @@ }, "outputs": [], "source": [ + "%%capture \"Remove this line to see debug information\"\n", "%%graph_notebook_vis_options\n", "{\n", " \"edges\": {\n", @@ -172,18 +173,19 @@ "outputs": [], "source": [ "%%gremlin -d label -g class -le 50 -p inv,oute\n", - "kh.endpoints().not(has(\"serviceEndpoint\",\"kube-dns\")).\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" + "kh.endpoints().not(has(\"serviceEndpoint\",\"kube-dns\"))\n", + "\t.repeat(\n", + "\t\toutE().inV().simplePath()\n", + "\t)\n", + "\t.until(\n", + "\t\thasLabel(\"Node\")\n", + "\t\t.or()\n", + "\t\t.loops().is(5)\n", + "\t)\n", + "\t.hasLabel(\"Node\")\n", + "\t.path()\n", + "\t.by(elementMap())\n", + "\t.limit(100)\t// Limit the number of results for large clusters" ] }, { diff --git a/deployments/kubehound/notebook/KubeHound.ipynb b/deployments/kubehound/notebook/KubeHound.ipynb index 426f24119..8eff02f6b 100644 --- a/deployments/kubehound/notebook/KubeHound.ipynb +++ b/deployments/kubehound/notebook/KubeHound.ipynb @@ -31,6 +31,7 @@ }, "outputs": [], "source": [ + "%%capture \"Remove this line to see debug information\"\n", "%%graph_notebook_vis_options\n", "{\n", " \"edges\": {\n", @@ -74,10 +75,10 @@ "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" + "kh.services()\n", + " .criticalPaths()\n", + " .by(elementMap())\n", + " .limit(100) // Limit the number of results for large clusters" ] }, { @@ -96,10 +97,10 @@ "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" + "kh.groups()\n", + " .criticalPaths()\n", + " .by(elementMap())\n", + " .limit(100) // Limit the number of results for large clusters" ] }, { @@ -117,13 +118,19 @@ "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" + "kh.services()\n", + " .has(\"port\", 443) // Look for exposed port on 443 only\n", + " .not(\n", + " // Exclude endpoints from the 'kube' and 'system' namespaces\n", + " has(\"namespace\", within(\"system\", \"kube\")) \n", + " ) \n", + " .where(\n", + " // Only accept endpoints attached to elasticsearch containers\n", + " __.out().hasLabel(\"Container\").has(\"image\", TextP.containing(\"elasticsearch\"))\n", + " ) \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" ] }, { diff --git a/deployments/kubehound/notebook/KubehoundDSL_101.ipynb b/deployments/kubehound/notebook/KubehoundDSL_101.ipynb index a793feea6..6241130c3 100644 --- a/deployments/kubehound/notebook/KubehoundDSL_101.ipynb +++ b/deployments/kubehound/notebook/KubehoundDSL_101.ipynb @@ -85,26 +85,11 @@ }, { "cell_type": "code", - "execution_count": 99, + "execution_count": null, "metadata": { "scrolled": true }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "af38af8aca84445799abf1e6e2615932", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin\n", "kh // traversal source (KubeHound DSL) \n", @@ -121,26 +106,11 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "metadata": { "scrolled": true }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ffd7abc18091475dac17510c4c18141b", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Force(network=<…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin -d class -g critical -le 50 -p inv,oute\n", "kh // traversal source (KubeHound DSL) \n", @@ -171,24 +141,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d9eb8ab0f60d446ca4361b6a4bcd562e", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin\n", "kh // traversal source (KubeHound DSL) \n", @@ -206,24 +161,9 @@ }, { "cell_type": "code", - "execution_count": 196, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "4fab2a36759341aba99969f53a2cc37e", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin\n", "kh // traversal source (KubeHound DSL) \n", @@ -241,24 +181,9 @@ }, { "cell_type": "code", - "execution_count": 111, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "cb9e353e63324392b52921f3a7b00900", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin\n", "kh // traversal source (KubeHound DSL) \n", @@ -275,24 +200,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "01f75acb9b4e49a2aaed3a615ea25540", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin\n", "kh // traversal source (KubeHound DSL) \n", @@ -309,24 +219,9 @@ }, { "cell_type": "code", - "execution_count": 115, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "a462f792e47c4da38f478fab4c47536f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin\n", "kh // traversal source (KubeHound DSL) \n", @@ -343,24 +238,9 @@ }, { "cell_type": "code", - "execution_count": 122, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "150582d797e74b6ca48b72d0739162f2", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin\n", "kh // traversal source (KubeHound DSL) \n", @@ -377,24 +257,9 @@ }, { "cell_type": "code", - "execution_count": 108, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "cccea3949de545dba5dbdb984e70ea22", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin\n", "kh // traversal source (KubeHound DSL) \n", @@ -411,24 +276,9 @@ }, { "cell_type": "code", - "execution_count": 109, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "f3e6bf3ea42941bc98ef706503292dbc", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin\n", "kh // traversal source (KubeHound DSL) \n", @@ -481,26 +331,11 @@ }, { "cell_type": "code", - "execution_count": 135, + "execution_count": null, "metadata": { "scrolled": true }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "52895e3dcbb5480c9e8487fbb38a2977", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin\n", "kh // traversal source (KubeHound DSL) \n", @@ -518,26 +353,11 @@ }, { "cell_type": "code", - "execution_count": 241, + "execution_count": null, "metadata": { "scrolled": true }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "c05762ab69524821b0b0bc8c7542c7b0", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin\n", "kh // traversal source (KubeHound DSL) \n", @@ -555,24 +375,9 @@ }, { "cell_type": "code", - "execution_count": 237, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5d2b5f19e3f3439a95489f4a5e5779dd", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin\n", "kh.containers().limit(1)\n", @@ -588,24 +393,9 @@ }, { "cell_type": "code", - "execution_count": 169, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "538582143263460db74405027174f698", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Force(network=<…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin -d class -g critical -le 50 -p inv,oute\n", "kh.containers()\n", @@ -639,24 +429,9 @@ }, { "cell_type": "code", - "execution_count": 275, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0243aee5d63e4081803bb8547c584240", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin -d class -g critical -le 50 -p inv,oute\n", "kh.pods() // get all the pods\n", @@ -674,24 +449,9 @@ }, { "cell_type": "code", - "execution_count": 277, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "370cdeddf59844aa8ac4fcc8edaab116", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin -d class -g critical -le 50 -p inv,oute\n", "kh.pods() // get all the pods\n", @@ -710,24 +470,9 @@ }, { "cell_type": "code", - "execution_count": 278, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "6900c76ad3ab4fdf9bab8218d2a02a05", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Force(network=<…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin -d name -g class -le 50 -p inv,oute\n", "kh.containers() // get all containers\n", @@ -749,26 +494,11 @@ }, { "cell_type": "code", - "execution_count": 309, + "execution_count": null, "metadata": { "scrolled": true }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "1b2e39284f9d4a7a8244d25b7480be5f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin -d class -g critical -le 50 -p inv,oute\n", "kh.containers() // get all the containers\n", @@ -789,24 +519,9 @@ }, { "cell_type": "code", - "execution_count": 316, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "566427195a64442c82c65578f9bc9616", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin -d class -g critical -le 50 -p inv,oute\n", "kh.endpoints()\n", @@ -823,24 +538,9 @@ }, { "cell_type": "code", - "execution_count": 320, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "de7c90d9cb514741b6f049cbdfde969e", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin -d class -g critical -le 50 -p inv,oute\n", "kh.endpoints()\n", @@ -862,24 +562,9 @@ }, { "cell_type": "code", - "execution_count": 326, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "6326b6f24bfe44e681a071fc7fcdb84e", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin -d class -g critical -le 50 -p inv,oute\n", "kh.V() // get all the vertices\n", @@ -899,24 +584,9 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ba3ef93cd11d47adbf2a569ac2cba431", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin -d class -g critical -le 50 -p inv,oute\n", "kh.permissions() // get the permissionsets\n", @@ -932,24 +602,9 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "1b3823fb11f74733a06eacadc6a12351", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin -d class -g critical -le 50 -p inv,oute\n", "kh.permissions() // get the permissionsets\n", @@ -978,24 +633,9 @@ }, { "cell_type": "code", - "execution_count": 109, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2b1d8bcea5974523ba7848afe09fc6f5", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Force(network=<…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin -d class -g critical -le 50 -p inv,oute\n", "\n", @@ -1011,24 +651,9 @@ }, { "cell_type": "code", - "execution_count": 112, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "a576160c3cc242f494f6dbf1cb2b083f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Force(network=<…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin -d class -g critical -le 50 -p inv,oute\n", "\n", @@ -1051,24 +676,9 @@ }, { "cell_type": "code", - "execution_count": 122, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9e04020ed0c241d4ac50f6f58fc87130", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Force(network=<…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin -d class -g critical -le 50 -p inv,oute\n", "\n", @@ -1091,24 +701,9 @@ }, { "cell_type": "code", - "execution_count": 127, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b0c604d75798477e87717d5311d900f5", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Force(network=<…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin -d class -g critical -le 50 -p inv,oute\n", "\n", @@ -1128,24 +723,9 @@ }, { "cell_type": "code", - "execution_count": 124, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "540a96ed705f4c47bca7ad781aa1971b", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Tab(children=(Output(layout=Layout(max_height='600px', max_width='940px', overflow='scroll')), Output(layout=L…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%%gremlin -d class -g critical -le 50 -p inv,oute\n", "\n", diff --git a/deployments/kubehound/notebook/LowHangingFruit-ContainerEscape.ipynb b/deployments/kubehound/notebook/LowHangingFruit-ContainerEscape.ipynb new file mode 100644 index 000000000..8b1736536 --- /dev/null +++ b/deployments/kubehound/notebook/LowHangingFruit-ContainerEscape.ipynb @@ -0,0 +1,550 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "execution": { + "iopub.execute_input": "2024-07-24T14:46:25.538529Z", + "iopub.status.busy": "2024-07-24T14:46:25.538280Z", + "iopub.status.idle": "2024-07-24T14:46:25.545724Z", + "shell.execute_reply": "2024-07-24T14:46:25.545041Z", + "shell.execute_reply.started": "2024-07-24T14:46:25.538501Z" + }, + "frozen": false, + "init_cell": true, + "tags": [ + "safe_output" + ] + }, + "source": [ + "# Autoloading\n", + "\n", + "Loading graph visualisation settings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "init_cell": true + }, + "outputs": [], + "source": [ + "%%capture \"Remove this line to see debug information\"\n", + "%%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": { + "frozen": false, + "tags": [ + "unsafe_output" + ] + }, + "source": [ + "# Initial Setup\n", + "\n", + "## Get a view of all Ingested Cluster\n", + "\n", + "Retrieve all the current cluster ingested in KubeHound with the associated runID with the number of nodes. This numbers can be used to get a clue of the size of the cluster and also identify if an ingestion did not complete." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": { + "iopub.execute_input": "2024-07-24T14:46:28.273187Z", + "iopub.status.busy": "2024-07-24T14:46:28.272852Z", + "iopub.status.idle": "2024-07-24T14:46:28.625859Z", + "shell.execute_reply": "2024-07-24T14:46:28.625126Z", + "shell.execute_reply.started": "2024-07-24T14:46:28.273156Z" + }, + "frozen": false, + "tags": [ + "safe_output" + ] + }, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.nodes()\n", + " .groupCount()\n", + " .by(project('cluster','runID')\n", + " .by('cluster').by('runID'))\n", + " .unfold()\n", + " .limit(1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "frozen": false, + "tags": [ + "unsafe_output" + ] + }, + "source": [ + "## Setting your run_id/cluster\n", + "\n", + "Set which runID you want to use. The variable are being shared with all users of the instance, so we advise to make a uniq string for your user `runID_yourid` to avoid any conflict." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "frozen": false, + "tags": [ + "safe_output" + ] + }, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "graph.variables()\n", + " .set('runID_yourid','01htdgjj34mcmrrksw4bjy2e94')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "frozen": false, + "tags": [ + "unsafe_output" + ] + }, + "source": [ + "# Container escapes\n", + "\n", + "List all containers which are vulnerable to container escape to the node. \n", + "\n", + "## Identify the vulnerable containers\n", + "\n", + "The goal of this list is to identify images vulnerable to container escape. It will list all the containers and remove duplicate entry that share the same `namespace`, `app`, `team` and `image` labels." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "frozen": false, + "tags": [ + "safe_output" + ] + }, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.containers()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get().trim())\n", + " .where(\n", + " repeat(\n", + " outE().inV().simplePath() // Building the path from one vertex to another\n", + " ).until(\n", + " has(label, \"Node\") // Stop when meeting a critical asset\n", + " .or().loops().is(10) // Stop after X iteration\n", + " ).has(label, \"Node\") // Keep only path ending with a critical asset\n", + " .limit(1)\n", + " )\n", + " .dedup().by(\"image\")\n", + " .valueMap(\"namespace\",\"app\",\"team\",\"image\")\n", + " .limit(1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "frozen": false, + "tags": [ + "unsafe_output" + ] + }, + "source": [ + "If the list above is still too big to handle you can start with a more narrow view. The following list give a more abstract view to get deduplicated list of vulnerable `app`/`namespace`.\n", + "\n", + "If the k8s label `app` is not set properly, scope it by `namespace`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "frozen": false, + "tags": [ + "safe_output" + ] + }, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.containers()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get().trim())\n", + " .where(\n", + " repeat(\n", + " outE().inV().simplePath() // Building the path from one vertex to another\n", + " ).until(\n", + " has(label, \"Node\") // Stop when meeting a critical asset\n", + " .or().loops().is(10) // Stop after X iteration\n", + " ).has(label, \"Node\") // Keep only path ending with a critical asset\n", + " .limit(1)\n", + " )\n", + " .dedup()\n", + " .by(\"namespace\")\n", + " .by(\"app\")\n", + " .valueMap(\"namespace\",\"app\")\n", + " .limit(1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The goal here is to extract a list of apps for which you accept the risk for XYZ reason, to ignore them in queries. You can set this exclude list of `app` or `namespace` using gremlin variables in the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "graph.variables()\n", + " .set('containerEscape_whiteListedApp_yourid',['WHITELISTED_APP1', \"WHITELISTED_APP2\"])\n", + "\n", + "graph.variables()\n", + " .set('containerEscape_whiteListedNamespace_yourid',['NAMESPACE1', \"NAMESPACE2\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To filter them out, add the following `.not(has(...whiteListedApp...).or(...whiteListedNamespace...)` block at the start of the Gremlin queries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.containers()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get().trim())\n", + " .not(\n", + " has(\"app\", within(graph.variables().get('containerEscape_whiteListedApp_yourid').get()))\n", + " .or().has(\"namespace\", within(graph.variables().get('containerEscape_whiteListedNamespace_yourid').get()))\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "frozen": false, + "tags": [ + "unsafe_output" + ] + }, + "source": [ + "## Manual investigation for each app/namespace\n", + "\n", + "From the above list, you can manually investigate each vulnerable `app`/`namespace`. To proceed with the investigation, just copy/paste the name of the vulnerable app (replace `VULNERABLE_APP` by the targetted app)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "graph.variables()\n", + " .set('containerEscape_vulnApp_yourid','VULNERABLE_APP')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Listing all attack paths from a particular app\n", + "\n", + "The following gremlin request will **list all container escapes for the selected app**. We add a limit(1000) to avoid having huge graph." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "frozen": false, + "scrolled": true, + "tags": [ + "safe_output" + ] + }, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.containers()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get().trim())\n", + " .has(\"app\",graph.variables().get('containerEscape_vulnApp_yourid').get().trim())\n", + " .repeat(\n", + " outE().inV().simplePath() // Building the path from one vertex to another\n", + " ).until(\n", + " has(label, \"Node\") // Stop when meeting a critical asset\n", + " .or().loops().is(10) // Stop after X iteration\n", + " ).has(label, \"Node\") // Keep only path ending with a critical asset\n", + " .path().by(elementMap())\n", + " .limit(1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "frozen": false, + "tags": [ + "unsafe_output" + ] + }, + "source": [ + "The last view can already be quite overwhelming, even if it might not be an exhaustive view (as we capped the result with `limit(1000)`). Increasing the limit will not solve the issue as it will become humanly unreadable. \n", + "\n", + "### Listing all attack path deduplicated by app from a particular app \n", + "\n", + "One way to solve it is to generate an **overall view to understand the attack path**. This view will strip any specific information (image, ids, ...) and keep only 3 labels:\n", + "* the `app` label which specify what is associated application\n", + "* the `class` of the object (node, pod, role, ...) \n", + "* if the resource is `critical`. \n", + "\n", + "For instance, this will remove any replicatset duplication." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "frozen": false, + "tags": [ + "safe_output" + ] + }, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.containers()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get().trim())\n", + " .has(\"app\",graph.variables().get('containerEscape_vulnApp_yourid').get().trim())\n", + " .repeat(\n", + " outE().inV().simplePath() // Building the path from one vertex to another\n", + " ).until(\n", + " has(label, \"Node\") // Stop when meeting a critical asset\n", + " .or().loops().is(10) // Stop after X iteration\n", + " ).has(label, \"Node\") // Keep only path ending with a critical asset\n", + " .path()\n", + " .by(valueMap(\"app\", \"class\",\"critical\").with(WithOptions.tokens,WithOptions.labels))\n", + " .dedup()\n", + " .limit(1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "frozen": false, + "tags": [ + "unsafe_output" + ] + }, + "source": [ + "### Listing all attack path deduplicated by label/type from a particular app \n", + "\n", + "Sometimes, the previous view is still too big and return too many elements to be easily processable. So, to get an even widder picture, we can deduplicate the attack paths by k8s resource type only. This show the interaction from one type (endpoints/containers/nodes/...) to try to understand the bigger picture." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "frozen": false, + "tags": [ + "safe_output" + ] + }, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.containers()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get().trim())\n", + " .has(\"app\",graph.variables().get('containerEscape_vulnApp_yourid').get().trim())\n", + " .repeat(\n", + " outE().inV().simplePath() // Building the path from one vertex to another\n", + " ).until(\n", + " has(label, \"Node\") // Stop when meeting a critical asset\n", + " .or().loops().is(10) // Stop after X iteration\n", + " ).has(label, \"Node\") // Keep only path ending with a critical asset\n", + " .path().by(label())\n", + " .dedup()\n", + " .limit(1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Global view using the whitelisted approach\n", + "\n", + "We are reusing the same queries as previously but instead of iterating over each app, we take the problem more globaly. This approach can be quicker but needs to have a smaller or secure cluster.\n", + "\n", + "### Listing all attack paths (except the whitelisted one)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.containers()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get().trim())\n", + " .not(\n", + " has(\"app\", within(graph.variables().get('containerEscape_whiteListedApp_yourid').get()))\n", + " .or().has(\"namespace\", within(graph.variables().get('containerEscape_whiteListedNamespace_yourid').get()))\n", + " )\n", + " .repeat(\n", + " outE().inV().simplePath() // Building the path from one vertex to another\n", + " ).until(\n", + " has(label, \"Node\") // Stop when meeting a critical asset\n", + " .or().loops().is(10) // Stop after X iteration\n", + " ).has(label, \"Node\") // Keep only path ending with a critical asset\n", + " .path().by(elementMap())\n", + " .limit(1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Listing all attack path deduplicated by app (except the whitelisted one)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.containers()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get().trim())\n", + " .not(\n", + " has(\"app\", within(graph.variables().get('containerEscape_whiteListedApp_yourid').get()))\n", + " .or().has(\"namespace\", within(graph.variables().get('containerEscape_whiteListedNamespace_yourid').get()))\n", + " )\n", + " .repeat(\n", + " outE().inV().simplePath() // Building the path from one vertex to another\n", + " ).until(\n", + " has(label, \"Node\") // Stop when meeting a critical asset\n", + " .or().loops().is(10) // Stop after X iteration\n", + " ).has(label, \"Node\") // Keep only path ending with a critical asset\n", + " .path()\n", + " .by(valueMap(\"app\", \"class\",\"critical\").with(WithOptions.tokens,WithOptions.labels))\n", + " .dedup()\n", + " .limit(1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Listing all attack path deduplicated by label/type (except the whitelisted one)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.containers()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get().trim())\n", + " .not(\n", + " has(\"app\", within(graph.variables().get('containerEscape_whiteListedApp_yourid').get()))\n", + " .or().has(\"namespace\", within(graph.variables().get('containerEscape_whiteListedNamespace_yourid').get()))\n", + " )\n", + " .repeat(\n", + " outE().inV().simplePath() // Building the path from one vertex to another\n", + " ).until(\n", + " has(label, \"Node\") // Stop when meeting a critical asset\n", + " .or().loops().is(10) // Stop after X iteration\n", + " ).has(label, \"Node\") // Keep only path ending with a critical asset\n", + " .path().by(label())\n", + " .dedup()\n", + " .limit(1000)" + ] + } + ], + "metadata": { + "celltoolbar": "Initialization Cell", + "dd-sharing": { + "allowed_groups": [ + "team-ase", + "subproduct-secopsengineering", + "team-aso", + "" + ], + "allowed_users": [ + "" + ], + "retention_period": "90" + }, + "kernelspec": { + "display_name": "Python 3 (default)", + "language": "python", + "name": "ipykernel-default" + }, + "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.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/deployments/kubehound/notebook/LowHangingFruit-Endpoints.ipynb b/deployments/kubehound/notebook/LowHangingFruit-Endpoints.ipynb new file mode 100644 index 000000000..309eb985b --- /dev/null +++ b/deployments/kubehound/notebook/LowHangingFruit-Endpoints.ipynb @@ -0,0 +1,490 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "execution": { + "iopub.execute_input": "2024-07-24T14:46:25.538529Z", + "iopub.status.busy": "2024-07-24T14:46:25.538280Z", + "iopub.status.idle": "2024-07-24T14:46:25.545724Z", + "shell.execute_reply": "2024-07-24T14:46:25.545041Z", + "shell.execute_reply.started": "2024-07-24T14:46:25.538501Z" + }, + "frozen": false, + "init_cell": true, + "tags": [ + "safe_output" + ] + }, + "source": [ + "# Autoloading\n", + "\n", + "Loading graph visualisation settings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "init_cell": true + }, + "outputs": [], + "source": [ + "%%capture \"Remove this line to see debug information\"\n", + "%%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": { + "frozen": false, + "tags": [ + "unsafe_output" + ] + }, + "source": [ + "# Initial Setup\n", + "\n", + "## Get a view of all Ingested Cluster\n", + "\n", + "Retrieve all the current cluster ingested in KubeHound with the associated runID with the number of nodes. This numbers can be used to get a clue of the size of the cluster and also identify if an ingestion did not complete." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": { + "iopub.execute_input": "2024-07-24T14:46:28.273187Z", + "iopub.status.busy": "2024-07-24T14:46:28.272852Z", + "iopub.status.idle": "2024-07-24T14:46:28.625859Z", + "shell.execute_reply": "2024-07-24T14:46:28.625126Z", + "shell.execute_reply.started": "2024-07-24T14:46:28.273156Z" + }, + "frozen": false, + "tags": [ + "safe_output" + ] + }, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.nodes()\n", + " .groupCount()\n", + " .by(project('cluster','runID')\n", + " .by('cluster').by('runID'))\n", + " .unfold()\n", + " .limit(1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "frozen": false, + "tags": [ + "unsafe_output" + ] + }, + "source": [ + "## Setting your run_id/cluster\n", + "\n", + "Set which runID you want to use. The variable are being shared with all users of the instance, so we advise to make a uniq string for your user `runID_yourid` to avoid any conflict." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "frozen": false, + "tags": [ + "safe_output" + ] + }, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "graph.variables()\n", + " .set('runID_yourid','01htdgjj34mcmrrksw4bjy2e94')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "frozen": false, + "tags": [ + "unsafe_output" + ] + }, + "source": [ + "# Endpoints\n", + "\n", + "Identify attack path from endpoints. Get a view of all endpoints leading to a critical path (full take over on the cluster).\n", + "\n", + "## Identify the vulnerable app/namespace\n", + "\n", + "The goal of this list is to identify endpoints leading to a critical path. The list here is exhaustive _by port_ which means we will deduplicate the result by the k8s label `app` or `namespace`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "frozen": false, + "tags": [ + "safe_output" + ] + }, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.endpoints()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get().trim())\n", + " .hasCriticalPath()\n", + " .dedup()\n", + " .by(\"namespace\")\n", + " .by(\"port\")\n", + " .valueMap(\"namespace\",\"app\",\"team\",\"portName\",\"port\",\"serviceDns\",\"exposure\")\n", + " .limit(100)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "frozen": false, + "tags": [ + "unsafe_output" + ] + }, + "source": [ + "If the list above is still too big to handle you can start with a more narrow view. The following list give a more abstract view to get deduplicated list of vulnerable `app`/`namespace`.\n", + "\n", + "If the k8s label `app` is not set properly, scope it by `namespace`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "frozen": false, + "tags": [ + "safe_output" + ] + }, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.endpoints()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get().trim())\n", + " .hasCriticalPath()\n", + " .dedup()\n", + " .by(\"namespace\")\n", + " .by(\"app\")\n", + " .valueMap(\"namespace\",\"app\")\n", + " .limit(100)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The goal here is to extract a list of apps for which you accept the risk for XYZ reason, to ignore them in queries. You can set this exclude list of `app` or `namespace` using gremlin variables in the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "graph.variables()\n", + " .set('endpoits_whiteListedApp_yourid',['WHITELISTED_APP1', \"WHITELISTED_APP2\"])\n", + "\n", + "graph.variables()\n", + " .set('endpoits_whiteListedNamespace_yourid',['NAMESPACE1', \"NAMESPACE2\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "frozen": false, + "tags": [ + "unsafe_output" + ] + }, + "source": [ + "## Manual investigation for each app/namespace\n", + "\n", + "From the above list, you can iterate manual investigation by scoping by each vulnerable `app`/`namespace`. To proceed with the investigation, just copy/paste the name of the vulnerable app (replace `VULNERABLE_APP` by the targetted app)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "graph.variables()\n", + " .set('endpoint_vulnApp_yourid','VULNERABLE_APP')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Listing all attack paths from a particular app\n", + "\n", + "The following gremlin request will **list all attack paths from the selected app**. We add a limit(1000) to avoid having huge graph." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "frozen": false, + "scrolled": true, + "tags": [ + "safe_output" + ] + }, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.endpoints()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get().trim())\n", + " .has(\"app\",graph.variables().get('endpoint_vulnApp_yourid').get().trim())\n", + " .criticalPaths()\n", + " .by(elementMap())\n", + " .limit(1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "frozen": false, + "tags": [ + "unsafe_output" + ] + }, + "source": [ + "The last view can already be quite overwhelming, even if it might not be an exhaustive view (as we capped the result with `limit(1000)`). Increasing the limit will not solve the issue as it will become humanly unreadable.\n", + "\n", + "### Listing all attack path deduplicated by app from a particular app \n", + "\n", + "One way to solve it is to generate an **overall view to understand the attack path**. This view will strip any specific information (image, ids, ...) and keep only 3 labels:\n", + "* the `app` label which specify what is associated application\n", + "* the `class` of the object (node, pod, role, ...) \n", + "* if the resource is `critical`. \n", + "\n", + "For instance, this will remove any replicatset duplication." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "frozen": false, + "tags": [ + "safe_output" + ] + }, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.endpoints()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get().trim())\n", + " .has(\"app\",graph.variables().get('endpoint_vulnApp_yourid').get().trim())\n", + " .criticalPaths()\n", + " .by(valueMap(\"app\", \"class\",\"critical\").with(WithOptions.tokens,WithOptions.labels))\n", + " .limit(10000)\n", + " .dedup()\n", + " .limit(1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "frozen": false, + "tags": [ + "unsafe_output" + ] + }, + "source": [ + "### Listing all attack path deduplicated by label/type from a particular app \n", + "\n", + "Sometimes, the previous view is still too big and return too many elements to be easily processable. So, to get an even widder picture, we can deduplicate the attack paths by k8s resource type only. This show the interaction from one type (endpoints/containers/nodes/...) to try to understand the bigger picture." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "frozen": false, + "tags": [ + "safe_output" + ] + }, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.endpoints()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get().trim())\n", + " .has(\"app\",graph.variables().get('endpoint_vulnApp_yourid').get().trim())\n", + " .criticalPaths()\n", + " .by(label())\n", + " .dedup()\n", + " .limit(1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Global view using the whitelisted approach\n", + "\n", + "We are reusing the same queries as previously but instead of iterating over each app, we take the problem more globaly. This approach can be quicker but needs to have a smaller or secure cluster.\n", + "\n", + "### Listing all attack paths (except the whitelisted one)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.endpoints()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get().trim())\n", + " .not(\n", + " has(\"app\", within(graph.variables().get('endpoits_whiteListedApp_yourid').get()))\n", + " .or().has(\"namespace\", within(graph.variables().get('endpoits_whiteListedNamespace_yourid').get()))\n", + " )\n", + " .criticalPaths()\n", + " .by(elementMap())\n", + " .limit(1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To filter them out, add the following `.not(has(...whiteListedApp...).or(...whiteListedNamespace...)` block at the start of the Gremlin queries" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Listing all attack path deduplicated by app (except the whitelisted one)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.endpoints()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get().trim())\n", + " .not(\n", + " has(\"app\", within(graph.variables().get('endpoits_whiteListedApp_yourid').get()))\n", + " .or().has(\"namespace\", within(graph.variables().get('endpoits_whiteListedNamespace_yourid').get()))\n", + " )\n", + " .criticalPaths()\n", + " .by(valueMap(\"app\", \"class\",\"critical\").with(WithOptions.tokens,WithOptions.labels))\n", + " .limit(10000)\n", + " .dedup()\n", + " .limit(1000)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Listing all attack path deduplicated by label/type (except the whitelisted one)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%gremlin -d class -g critical -le 50 -p inv,oute\n", + "\n", + "kh.endpoints()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get().trim())\n", + " .not(\n", + " has(\"app\", within(graph.variables().get('endpoits_whiteListedApp_yourid').get()))\n", + " .or().has(\"namespace\", within(graph.variables().get('endpoits_whiteListedNamespace_yourid').get()))\n", + " )\n", + " .criticalPaths()\n", + " .by(label())\n", + " .dedup()\n", + " .limit(1000)" + ] + } + ], + "metadata": { + "celltoolbar": "Initialization Cell", + "dd-sharing": { + "allowed_groups": [ + "team-ase", + "subproduct-secopsengineering", + "team-aso", + "" + ], + "allowed_users": [ + "" + ], + "retention_period": "90" + }, + "kernelspec": { + "display_name": "Python 3 (default)", + "language": "python", + "name": "ipykernel-default" + }, + "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.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/deployments/kubehound/notebook/RedTeam.ipynb b/deployments/kubehound/notebook/RedTeam.ipynb index e9f77a732..ca78faf18 100644 --- a/deployments/kubehound/notebook/RedTeam.ipynb +++ b/deployments/kubehound/notebook/RedTeam.ipynb @@ -28,6 +28,7 @@ }, "outputs": [], "source": [ + "%%capture \"Remove this line to see debug information\"\n", "%%graph_notebook_vis_options\n", "{\n", " \"edges\": {\n", @@ -68,12 +69,13 @@ "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", + "kh.endpoints()\n", + "\t.has(\"runID\", graph.variables().get('runID_yourid').get())\n", + "\t.where(out().hasLabel(\"Container\"))\n", + "\t.project('port',\"portName\", 'image')\n", + "\t.by(values(\"port\"))\n", + "\t.by(values(\"portName\"))\n", + "\t.by(out().hasLabel(\"Container\").values(\"image\"))\n", "\tdedup()" ] }, @@ -91,14 +93,15 @@ "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()" + "kh.endpoints()\n", + "\t.has(\"runID\", graph.variables().get('runID_yourid').get())\n", + "\t.hasCriticalPath()\n", + "\t.where(out().hasLabel(\"Container\"))\n", + "\t.project('port',\"portName\", 'image')\n", + "\t.by(values(\"port\"))\n", + "\t.by(values(\"portName\"))\n", + "\t.by(out().hasLabel(\"Container\").values(\"image\"))\n", + "\t.dedup()" ] }, { @@ -117,12 +120,13 @@ "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" + "kh.endpoints()\n", + "\t.has(\"runID\", graph.variables().get('runID_yourid').get())\n", + "\t.has(\"portName\", \"elasticsearch\")\t// Change the value here for those found\n", + "\t.not(has(\"protocol\", \"UDP\")) \t\t// Exclude or change based on requirements\n", + "\t.criticalPaths()\n", + "\t.by(elementMap())\n", + "\t.limit(100)\t\t\t\t\t\t\t// Limit the number of results for large clusters" ] }, { @@ -139,11 +143,12 @@ "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" + "kh.endpoints()\n", + "\t.has(\"runID\", graph.variables().get('runID_yourid').get())\n", + "\t.has(\"portName\", \"elasticsearch\") // Change the value here for those found\n", + "\t.criticalPathsFilter(6, \"TOKEN_BRUTEFORCE\", \"POD_EXEC\", \"POD_CREATE\")\n", + "\t.by(elementMap())\n", + " .limit(100)\t// Limit the number of results for large clusters" ] }, { @@ -160,19 +165,20 @@ "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" + "kh.endpoints()\n", + "\t.has(\"runID\", graph.variables().get('runID_yourid').get())\n", + "\t.has(\"portName\", within(\"jmx\", \"ssh\", \"log4j\")) // Change the values here for those found\n", + "\t.repeat(\n", + "\t\toutE().inV()\n", + "\t\t.simplePath())\n", + "\t.until(\n", + "\t\thasLabel(\"Node\")\n", + "\t\t.or()\n", + "\t\t.loops().is(5))\n", + "\t.hasLabel(\"Node\")\n", + "\t.path()\n", + "\t.by(elementMap())\n", + " .limit(100)\t// Limit the number of results for large clusters" ] } ], diff --git a/deployments/kubehound/notebook/SecurityPosture.ipynb b/deployments/kubehound/notebook/SecurityPosture.ipynb index 9054e24f4..1747b427d 100644 --- a/deployments/kubehound/notebook/SecurityPosture.ipynb +++ b/deployments/kubehound/notebook/SecurityPosture.ipynb @@ -15,27 +15,7 @@ "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\": \"kubegraph\",\n", - " \"port\": 8182,\n", - " \"ssl\": false,\n", - " \"gremlin\": {\n", - " \"traversal_source\": \"g\",\n", - " \"username\": \"\",\n", - " \"password\": \"\",\n", - " \"message_serializer\": \"graphsonv3\"\n", - " }\n", - "}" + "Connection is being initated directly from the docker using the env vars `GRAPH_NOTEBOOK_HOST` and `GRAPH_NOTEBOOK_PORT`. To overwrite it you can use the magic `%%graph_notebook_config` [details here](https://github.com/aws/graph-notebook/tree/main/additional-databases/gremlin-server#connecting-to-a-local-gremlin-server-from-jupyter)." ] }, { @@ -57,8 +37,11 @@ "metadata": {}, "outputs": [], "source": [ + "%%capture \"Remove this line to see debug information\"\n", "%%gremlin\n", - "kh.services().minHopsToCritical()" + "kh.services()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get())\n", + " .minHopsToCritical()" ] }, { @@ -75,7 +58,9 @@ "outputs": [], "source": [ "%%gremlin\n", - "kh.services().criticalPaths().count()" + "kh.services()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get())\n", + " .criticalPaths().count()" ] }, { @@ -99,18 +84,20 @@ "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())" + "kh.V()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get())\n", + " .hasLabel(\"Endpoint\")\n", + " .has(\"exposure\", gte(2)) // https://kubehound.io/queries/dsl/#endpoint-exposure\n", + " .count()\n", + " .aggregate(\"t\")\n", + " .V()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get())\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())" ] }, { @@ -127,18 +114,20 @@ "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())" + "kh.V()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get())\n", + " .hasLabel(\"Identity\")\n", + " .has(\"critical\", false)\n", + " .count()\n", + " .aggregate(\"t\")\n", + " .V()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get())\n", + " .hasLabel(\"Identity\")\n", + " .has(\"critical\", false)\n", + " .hasCriticalPath()\n", + " .count()\n", + " .as(\"e\")\n", + " .math(\"100 * e/t\").by().by(unfold())" ] }, { @@ -155,16 +144,18 @@ "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())" + "kh.V()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get())\n", + " .hasLabel(\"Container\")\n", + " .count()\n", + " .aggregate(\"t\")\n", + " .V()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get())\n", + " .hasLabel(\"Container\")\n", + " .hasCriticalPath()\n", + " .count()\n", + " .as(\"e\")\n", + " .math(\"100 * e/t\").by().by(unfold())" ] }, { @@ -183,7 +174,9 @@ "outputs": [], "source": [ "%%gremlin\n", - "kh.services().criticalPathsFreq()" + "kh.services()\n", + " .has(\"runID\", graph.variables().get('runID_yourid').get())\n", + " .criticalPathsFreq()" ] } ],