From 767d4e28d8c4865ea474364c6d5141e331a7c719 Mon Sep 17 00:00:00 2001 From: CloudOne Antimalware Bot <132491272+su-amaas@users.noreply.github.com> Date: Wed, 3 Apr 2024 18:37:42 -0700 Subject: [PATCH] update to latest version: v1.1.0 (#16) --- .github/workflows/publish-to-pypi.yml | 5 +- .github/workflows/unit-test.yml | 26 + .gitignore | 13 + CHANGELOG.md | 21 +- tools/Makefile => Makefile | 18 +- NOTICE | 2 +- Pipfile | 3 +- Pipfile.lock | 721 ++++++++++++++------------ README.md | 65 +-- VERSION | 1 + amaas/_version.py | 1 - amaas/grpc/__init__.py | 76 ++- amaas/grpc/aio/__init__.py | 70 ++- amaas/grpc/util.py | 2 +- docs/sdk.md | 9 - examples/README.md | 12 +- examples/client.py | 9 +- examples/client_aio.py | 9 +- {amaas/protos => protos}/scan.proto | 7 +- setup.py | 57 +- tests/__init__.py | 2 + tests/fake_server_cert.pem | 21 + tests/mock_server.py | 86 +++ tests/test_aio_client_sdk.py | 170 ++++++ tests/test_client_sdk.py | 333 ++++++++++++ tests/test_util.py | 100 ++++ tools/Makefile.dev | 22 - tox.ini | 6 +- 28 files changed, 1338 insertions(+), 529 deletions(-) create mode 100644 .github/workflows/unit-test.yml create mode 100644 .gitignore rename tools/Makefile => Makefile (53%) create mode 100644 VERSION delete mode 100644 amaas/_version.py delete mode 100644 docs/sdk.md rename {amaas/protos => protos}/scan.proto (81%) create mode 100644 tests/__init__.py create mode 100644 tests/fake_server_cert.pem create mode 100644 tests/mock_server.py create mode 100644 tests/test_aio_client_sdk.py create mode 100644 tests/test_client_sdk.py create mode 100644 tests/test_util.py delete mode 100644 tools/Makefile.dev diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index baf392f..ba91532 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -9,7 +9,8 @@ name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI on: - push: + release: + types: [published] permissions: contents: read @@ -34,7 +35,7 @@ jobs: - name: Build a binary wheel and a source tarball run: >- - make -f ./tools/Makefile build + make build - name: tox run: >- diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml new file mode 100644 index 0000000..a22dab8 --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,26 @@ +name: unittest +on: + pull_request: + branches: + - "main" + +jobs: + unit-test: + runs-on: ubuntu-latest + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v3 + + # Set up python 3.9 + - name: Prepare python env + uses: actions/setup-python@v4 + with: + python-version: "3.9" + + # Install pipenv + - name: Install pipenv + run: pip install pipenv + + # Pack and publish Python SDK + - name: Exec py client unit test + run: make test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e81f084 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# general things to ignore +build/ +dist/ +*.egg-info/ +*.egg +*.py[cod] +__pycache__/ +*.so +*~ + +# due to using tox and pytest +.tox +.cache diff --git a/CHANGELOG.md b/CHANGELOG.md index 1269f1f..ab1fdc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,33 @@ # CHANGELOG +## 1.1.0 - 2024-04-03 + +* Update protos +* Enable PML (Predictive Machine Learning) detection and smart feedback +* Enable bulk mode +* Enable India region +* Support for scanning large files (over 2GB) + +## 1.0.5 - 2023-12-28 + +* fix linting issues + ## 1.0.4 - 2023-05-18 + * set default timeout_in_seconds to 180 seconds ## 1.0.3 - 2023-05-10 + * Change LICENSE ## 1.0.2 - 2023-05-10 + * Change README.md ## 1.0.1 - 2023-05-04 + * Add scan_buffer() function - + ## 1.0.0 - 2023-05-01 -* Initial release \ No newline at end of file + +* Initial release diff --git a/tools/Makefile b/Makefile similarity index 53% rename from tools/Makefile rename to Makefile index 2cb0753..15bb324 100644 --- a/tools/Makefile +++ b/Makefile @@ -1,22 +1,24 @@ -# ROOT_DIR := $(abspath ../../) -# include $(ROOT_DIR)/build-scripts/variables.mk - PIPY_URL ?= https://upload.pypi.org/legacy/ TOKEN ?= +VERSION := $(shell cat VERSION | tr -d '\n') -build: clean +proto: pipenv sync --dev - pipenv run python -m grpc_tools.protoc -Iamaas/grpc/protos=./amaas/protos \ + pipenv run python -m grpc_tools.protoc -Iamaas/grpc/protos=./protos \ --python_out=. \ --pyi_out=. \ --grpc_python_out=. \ - ./amaas/protos/scan.proto + ./protos/scan.proto + +build: proto pipenv run pipenv-setup sync pipenv run python setup.py sdist bdist_wheel +test: proto + pipenv run pytest tests + upload: - #pipenv run twine upload ./dist/*.whl pipenv run twine upload --repository-url $(PIPY_URL) -u __token__ -p $(TOKEN) ./dist/*.whl --skip-existing clean: - @rm -rf dist build amaas/*.egg-info amaas/grpc/protos/ + @rm -rf dist build *.egg-info amaas/grpc/protos/ diff --git a/NOTICE b/NOTICE index 7397a38..c956282 100644 --- a/NOTICE +++ b/NOTICE @@ -1,2 +1,2 @@ Cloud One VSAPI Python Client -Copyright 2023 Trend Micro Inc. \ No newline at end of file +Copyright 2023 Trend Micro Inc. diff --git a/Pipfile b/Pipfile index 39c7de1..77d7cdb 100644 --- a/Pipfile +++ b/Pipfile @@ -14,7 +14,8 @@ pytest = "~=7.1" twine = "~=4.0" pipenv-setup = "~=3.2" vistir = "==0.6.1" -autopep8 = "~=2.0" +pytest-mock = "~=3.11" +pytest-asyncio = "~=0.21" [requires] python_version = "3" diff --git a/Pipfile.lock b/Pipfile.lock index 4e8bf6b..772422e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "7bb5d7e6291e9b77f2ccec68b4f630fddcda5763fb6e333e36484cf7706a6e96" + "sha256": "7865f24b8f22bb49581ac893044e19172af9861419245b8044dd683801bb4c3d" }, "pipfile-spec": 6, "requires": { @@ -18,73 +18,80 @@ "default": { "grpcio": { "hashes": [ - "sha256:06e84ad9ae7668a109e970c7411e7992751a116494cba7c4fb877656527f9a57", - "sha256:0ff789ae7d8ddd76d2ac02e7d13bfef6fc4928ac01e1dcaa182be51b6bcc0aaa", - "sha256:10954662f77dc36c9a1fb5cc4a537f746580d6b5734803be1e587252682cda8d", - "sha256:139f66656a762572ae718fa0d1f2dce47c05e9fbf7a16acd704c354405b97df9", - "sha256:1c31e52a04e62c8577a7bf772b3e7bed4df9c9e0dd90f92b6ffa07c16cab63c9", - "sha256:33971197c47965cc1d97d78d842163c283e998223b151bab0499b951fd2c0b12", - "sha256:345356b307cce5d14355e8e055b4ca5f99bc857c33a3dc1ddbc544fca9cd0475", - "sha256:373b48f210f43327a41e397391715cd11cfce9ded2fe76a5068f9bacf91cc226", - "sha256:3ccb621749a81dc7755243665a70ce45536ec413ef5818e013fe8dfbf5aa497b", - "sha256:42a3bbb2bc07aef72a7d97e71aabecaf3e4eb616d39e5211e2cfe3689de860ca", - "sha256:42e63904ee37ae46aa23de50dac8b145b3596f43598fa33fe1098ab2cbda6ff5", - "sha256:4eb37dd8dd1aa40d601212afa27ca5be255ba792e2e0b24d67b8af5e012cdb7d", - "sha256:51173e8fa6d9a2d85c14426bdee5f5c4a0654fd5fddcc21fe9d09ab0f6eb8b35", - "sha256:5144feb20fe76e73e60c7d73ec3bf54f320247d1ebe737d10672480371878b48", - "sha256:5344be476ac37eb9c9ad09c22f4ea193c1316bf074f1daf85bddb1b31fda5116", - "sha256:6108e5933eb8c22cd3646e72d5b54772c29f57482fd4c41a0640aab99eb5071d", - "sha256:6a007a541dff984264981fbafeb052bfe361db63578948d857907df9488d8774", - "sha256:6ee26e9dfb3996aff7c870f09dc7ad44a5f6732b8bdb5a5f9905737ac6fd4ef1", - "sha256:750de923b456ca8c0f1354d6befca45d1f3b3a789e76efc16741bd4132752d95", - "sha256:7c5ede2e2558f088c49a1ddda19080e4c23fb5d171de80a726b61b567e3766ed", - "sha256:830215173ad45d670140ff99aac3b461f9be9a6b11bee1a17265aaaa746a641a", - "sha256:8391cea5ce72f4a12368afd17799474015d5d3dc00c936a907eb7c7eaaea98a5", - "sha256:8940d6de7068af018dfa9a959a3510e9b7b543f4c405e88463a1cbaa3b2b379a", - "sha256:89a49cc5ad08a38b6141af17e00d1dd482dc927c7605bc77af457b5a0fca807c", - "sha256:900bc0096c2ca2d53f2e5cebf98293a7c32f532c4aeb926345e9747452233950", - "sha256:97e0efaebbfd222bcaac2f1735c010c1d3b167112d9d237daebbeedaaccf3d1d", - "sha256:9e04d4e4cfafa7c5264e535b5d28e786f0571bea609c3f0aaab13e891e933e9c", - "sha256:a4c60abd950d6de3e4f1ddbc318075654d275c29c846ab6a043d6ed2c52e4c8c", - "sha256:a6ff459dac39541e6a2763a4439c4ca6bc9ecb4acc05a99b79246751f9894756", - "sha256:a72797549935c9e0b9bc1def1768c8b5a709538fa6ab0678e671aec47ebfd55e", - "sha256:af4063ef2b11b96d949dccbc5a987272f38d55c23c4c01841ea65a517906397f", - "sha256:b975b85d1d5efc36cf8b237c5f3849b64d1ba33d6282f5e991f28751317504a1", - "sha256:bf0b9959e673505ee5869950642428046edb91f99942607c2ecf635f8a4b31c9", - "sha256:c0c85c5cbe8b30a32fa6d802588d55ffabf720e985abe9590c7c886919d875d4", - "sha256:c3f3237a57e42f79f1e560726576aedb3a7ef931f4e3accb84ebf6acc485d316", - "sha256:c3fa3ab0fb200a2c66493828ed06ccd1a94b12eddbfb985e7fd3e5723ff156c6", - "sha256:c435f5ce1705de48e08fcbcfaf8aee660d199c90536e3e06f2016af7d6a938dd", - "sha256:c90da4b124647547a68cf2f197174ada30c7bb9523cb976665dfd26a9963d328", - "sha256:cbdf2c498e077282cd427cfd88bdce4668019791deef0be8155385ab2ba7837f", - "sha256:d1fbad1f9077372b6587ec589c1fc120b417b6c8ad72d3e3cc86bbbd0a3cee93", - "sha256:d39f5d4af48c138cb146763eda14eb7d8b3ccbbec9fe86fb724cd16e0e914c64", - "sha256:ddb4a6061933bd9332b74eac0da25f17f32afa7145a33a0f9711ad74f924b1b8", - "sha256:ded637176addc1d3eef35331c39acc598bac550d213f0a1bedabfceaa2244c87", - "sha256:f20fd21f7538f8107451156dd1fe203300b79a9ddceba1ee0ac8132521a008ed", - "sha256:fda2783c12f553cdca11c08e5af6eecbd717280dc8fbe28a110897af1c15a88c" + "sha256:073f959c6f570797272f4ee9464a9997eaf1e98c27cb680225b82b53390d61e6", + "sha256:0fd3b3968ffe7643144580f260f04d39d869fcc2cddb745deef078b09fd2b328", + "sha256:1434ca77d6fed4ea312901122dc8da6c4389738bf5788f43efb19a838ac03ead", + "sha256:1c30bb23a41df95109db130a6cc1b974844300ae2e5d68dd4947aacba5985aa5", + "sha256:20e7a4f7ded59097c84059d28230907cd97130fa74f4a8bfd1d8e5ba18c81491", + "sha256:2199165a1affb666aa24adf0c97436686d0a61bc5fc113c037701fb7c7fceb96", + "sha256:297eef542156d6b15174a1231c2493ea9ea54af8d016b8ca7d5d9cc65cfcc444", + "sha256:2aef56e85901c2397bd557c5ba514f84de1f0ae5dd132f5d5fed042858115951", + "sha256:30943b9530fe3620e3b195c03130396cd0ee3a0d10a66c1bee715d1819001eaf", + "sha256:3b36a2c6d4920ba88fa98075fdd58ff94ebeb8acc1215ae07d01a418af4c0253", + "sha256:428d699c8553c27e98f4d29fdc0f0edc50e9a8a7590bfd294d2edb0da7be3629", + "sha256:43e636dc2ce9ece583b3e2ca41df5c983f4302eabc6d5f9cd04f0562ee8ec1ae", + "sha256:452ca5b4afed30e7274445dd9b441a35ece656ec1600b77fff8c216fdf07df43", + "sha256:467a7d31554892eed2aa6c2d47ded1079fc40ea0b9601d9f79204afa8902274b", + "sha256:4b44d7e39964e808b071714666a812049765b26b3ea48c4434a3b317bac82f14", + "sha256:4c86343cf9ff7b2514dd229bdd88ebba760bd8973dac192ae687ff75e39ebfab", + "sha256:5208a57eae445ae84a219dfd8b56e04313445d146873117b5fa75f3245bc1390", + "sha256:5ff21e000ff2f658430bde5288cb1ac440ff15c0d7d18b5fb222f941b46cb0d2", + "sha256:675997222f2e2f22928fbba640824aebd43791116034f62006e19730715166c0", + "sha256:676e4a44e740deaba0f4d95ba1d8c5c89a2fcc43d02c39f69450b1fa19d39590", + "sha256:6e306b97966369b889985a562ede9d99180def39ad42c8014628dd3cc343f508", + "sha256:6fd9584bf1bccdfff1512719316efa77be235469e1e3295dce64538c4773840b", + "sha256:705a68a973c4c76db5d369ed573fec3367d7d196673fa86614b33d8c8e9ebb08", + "sha256:74d7d9fa97809c5b892449b28a65ec2bfa458a4735ddad46074f9f7d9550ad13", + "sha256:77c8a317f0fd5a0a2be8ed5cbe5341537d5c00bb79b3bb27ba7c5378ba77dbca", + "sha256:79a050889eb8d57a93ed21d9585bb63fca881666fc709f5d9f7f9372f5e7fd03", + "sha256:7db16dd4ea1b05ada504f08d0dca1cd9b926bed3770f50e715d087c6f00ad748", + "sha256:83f2292ae292ed5a47cdcb9821039ca8e88902923198f2193f13959360c01860", + "sha256:87c9224acba0ad8bacddf427a1c2772e17ce50b3042a789547af27099c5f751d", + "sha256:8a97a681e82bc11a42d4372fe57898d270a2707f36c45c6676e49ce0d5c41353", + "sha256:9073513ec380434eb8d21970e1ab3161041de121f4018bbed3146839451a6d8e", + "sha256:90bdd76b3f04bdb21de5398b8a7c629676c81dfac290f5f19883857e9371d28c", + "sha256:91229d7203f1ef0ab420c9b53fe2ca5c1fbeb34f69b3bc1b5089466237a4a134", + "sha256:92f88ca1b956eb8427a11bb8b4a0c0b2b03377235fc5102cb05e533b8693a415", + "sha256:95ae3e8e2c1b9bf671817f86f155c5da7d49a2289c5cf27a319458c3e025c320", + "sha256:9e30be89a75ee66aec7f9e60086fadb37ff8c0ba49a022887c28c134341f7179", + "sha256:a48edde788b99214613e440fce495bbe2b1e142a7f214cce9e0832146c41e324", + "sha256:a7152fa6e597c20cb97923407cf0934e14224af42c2b8d915f48bc3ad2d9ac18", + "sha256:a9c7b71211f066908e518a2ef7a5e211670761651039f0d6a80d8d40054047df", + "sha256:b0571a5aef36ba9177e262dc88a9240c866d903a62799e44fd4aae3f9a2ec17e", + "sha256:b0fb2d4801546598ac5cd18e3ec79c1a9af8b8f2a86283c55a5337c5aeca4b1b", + "sha256:b10241250cb77657ab315270b064a6c7f1add58af94befa20687e7c8d8603ae6", + "sha256:b87efe4a380887425bb15f220079aa8336276398dc33fce38c64d278164f963d", + "sha256:b98f43fcdb16172dec5f4b49f2fece4b16a99fd284d81c6bbac1b3b69fcbe0ff", + "sha256:c193109ca4070cdcaa6eff00fdb5a56233dc7610216d58fb81638f89f02e4968", + "sha256:c826f93050c73e7769806f92e601e0efdb83ec8d7c76ddf45d514fee54e8e619", + "sha256:d020cfa595d1f8f5c6b343530cd3ca16ae5aefdd1e832b777f9f0eb105f5b139", + "sha256:d6a478581b1a1a8fdf3318ecb5f4d0cda41cacdffe2b527c23707c9c1b8fdb55", + "sha256:de2ad69c9a094bf37c1102b5744c9aec6cf74d2b635558b779085d0263166454", + "sha256:e278eafb406f7e1b1b637c2cf51d3ad45883bb5bd1ca56bc05e4fc135dfdaa65", + "sha256:e381fe0c2aa6c03b056ad8f52f8efca7be29fb4d9ae2f8873520843b6039612a", + "sha256:e61e76020e0c332a98290323ecfec721c9544f5b739fab925b6e8cbe1944cf19", + "sha256:f897c3b127532e6befdcf961c415c97f320d45614daf84deba0a54e64ea2457b", + "sha256:fb464479934778d7cc5baf463d959d361954d6533ad34c3a4f1d267e86ee25fd" ], "index": "pypi", - "version": "==1.56.2" + "version": "==1.60.0" }, "protobuf": { "hashes": [ - "sha256:0a5759f5696895de8cc913f084e27fd4125e8fb0914bb729a17816a33819f474", - "sha256:351cc90f7d10839c480aeb9b870a211e322bf05f6ab3f55fcb2f51331f80a7d2", - "sha256:5fea3c64d41ea5ecf5697b83e41d09b9589e6f20b677ab3c48e5f242d9b7897b", - "sha256:6dd9b9940e3f17077e820b75851126615ee38643c2c5332aa7a359988820c720", - "sha256:7b19b6266d92ca6a2a87effa88ecc4af73ebc5cfde194dc737cf8ef23a9a3b12", - "sha256:8547bf44fe8cec3c69e3042f5c4fb3e36eb2a7a013bb0a44c018fc1e427aafbd", - "sha256:9053df6df8e5a76c84339ee4a9f5a2661ceee4a0dab019e8663c50ba324208b0", - "sha256:c3e0939433c40796ca4cfc0fac08af50b00eb66a40bbbc5dee711998fb0bbc1e", - "sha256:ccd9430c0719dce806b93f89c91de7977304729e55377f872a92465d548329a9", - "sha256:e1c915778d8ced71e26fcf43c0866d7499891bca14c4368448a82edc61fdbc70", - "sha256:e9d0be5bf34b275b9f87ba7407796556abeeba635455d036c7351f7c183ef8ff", - "sha256:effeac51ab79332d44fba74660d40ae79985901ac21bca408f8dc335a81aa597", - "sha256:fee88269a090ada09ca63551bf2f573eb2424035bcf2cb1b121895b01a46594a" + "sha256:0bf384e75b92c42830c0a679b0cd4d6e2b36ae0cf3dbb1e1dfdda48a244f4bcd", + "sha256:0f881b589ff449bf0b931a711926e9ddaad3b35089cc039ce1af50b21a4ae8cb", + "sha256:1484f9e692091450e7edf418c939e15bfc8fc68856e36ce399aed6889dae8bb0", + "sha256:193f50a6ab78a970c9b4f148e7c750cfde64f59815e86f686c22e26b4fe01ce7", + "sha256:3497c1af9f2526962f09329fd61a36566305e6c72da2590ae0d7d1322818843b", + "sha256:57d65074b4f5baa4ab5da1605c02be90ac20c8b40fb137d6a8df9f416b0d0ce2", + "sha256:8bdbeaddaac52d15c6dce38c71b03038ef7772b977847eb6d374fc86636fa510", + "sha256:a19731d5e83ae4737bb2a089605e636077ac001d18781b3cf489b9546c7c80d6", + "sha256:abc0525ae2689a8000837729eef7883b9391cd6aa7950249dcf5a4ede230d5dd", + "sha256:becc576b7e6b553d22cbdf418686ee4daa443d7217999125c045ad56322dda10", + "sha256:ca37bf6a6d0046272c152eea90d2e4ef34593aaa32e8873fc14c16440f22d4b7" ], "index": "pypi", - "version": "==4.23.4" + "version": "==4.25.1" } }, "develop": { @@ -96,22 +103,6 @@ "markers": "python_version >= '3.7'", "version": "==23.1.0" }, - "autopep8": { - "hashes": [ - "sha256:86e9303b5e5c8160872b2f5ef611161b2893e9bfe8ccc7e2f76385947d57a2f1", - "sha256:f9849cdd62108cb739dbcdbfb7fdcc9a30d1b63c4cc3e1c1f893b5360941b61c" - ], - "index": "pypi", - "version": "==2.0.2" - }, - "bleach": { - "hashes": [ - "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414", - "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4" - ], - "markers": "python_version >= '3.7'", - "version": "==6.0.0" - }, "cached-property": { "hashes": [ "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130", @@ -121,17 +112,18 @@ }, "cerberus": { "hashes": [ - "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c" + "sha256:7649a5815024d18eb7c6aa5e7a95355c649a53aacfc9b050e9d0bf6bfa2af372", + "sha256:81011e10266ef71b6ec6d50e60171258a5b134d69f8fb387d16e4936d0d47642" ], - "version": "==1.3.4" + "version": "==1.3.5" }, "certifi": { "hashes": [ - "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", - "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" + "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", + "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" ], "markers": "python_version >= '3.6'", - "version": "==2023.7.22" + "version": "==2023.11.17" }, "chardet": { "hashes": [ @@ -143,84 +135,99 @@ }, "charset-normalizer": { "hashes": [ - "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96", - "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c", - "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710", - "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706", - "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020", - "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252", - "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad", - "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329", - "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a", - "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f", - "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6", - "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4", - "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a", - "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46", - "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2", - "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23", - "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace", - "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd", - "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982", - "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10", - "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2", - "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea", - "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09", - "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5", - "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149", - "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489", - "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9", - "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80", - "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592", - "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3", - "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6", - "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed", - "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c", - "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200", - "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a", - "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e", - "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d", - "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6", - "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623", - "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669", - "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3", - "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa", - "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9", - "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2", - "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f", - "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1", - "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4", - "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a", - "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8", - "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3", - "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029", - "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f", - "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959", - "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22", - "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7", - "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952", - "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346", - "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e", - "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d", - "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299", - "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd", - "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a", - "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3", - "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037", - "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94", - "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c", - "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858", - "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a", - "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449", - "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c", - "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918", - "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1", - "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c", - "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac", - "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa" + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.2.0" + "version": "==3.3.2" }, "colorama": { "hashes": [ @@ -232,10 +239,10 @@ }, "distlib": { "hashes": [ - "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057", - "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8" + "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", + "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64" ], - "version": "==0.3.7" + "version": "==0.3.8" }, "docutils": { "hashes": [ @@ -245,123 +252,149 @@ "markers": "python_version >= '3.7'", "version": "==0.20.1" }, + "exceptiongroup": { + "hashes": [ + "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", + "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68" + ], + "markers": "python_version < '3.11'", + "version": "==1.2.0" + }, "grpcio": { "hashes": [ - "sha256:06e84ad9ae7668a109e970c7411e7992751a116494cba7c4fb877656527f9a57", - "sha256:0ff789ae7d8ddd76d2ac02e7d13bfef6fc4928ac01e1dcaa182be51b6bcc0aaa", - "sha256:10954662f77dc36c9a1fb5cc4a537f746580d6b5734803be1e587252682cda8d", - "sha256:139f66656a762572ae718fa0d1f2dce47c05e9fbf7a16acd704c354405b97df9", - "sha256:1c31e52a04e62c8577a7bf772b3e7bed4df9c9e0dd90f92b6ffa07c16cab63c9", - "sha256:33971197c47965cc1d97d78d842163c283e998223b151bab0499b951fd2c0b12", - "sha256:345356b307cce5d14355e8e055b4ca5f99bc857c33a3dc1ddbc544fca9cd0475", - "sha256:373b48f210f43327a41e397391715cd11cfce9ded2fe76a5068f9bacf91cc226", - "sha256:3ccb621749a81dc7755243665a70ce45536ec413ef5818e013fe8dfbf5aa497b", - "sha256:42a3bbb2bc07aef72a7d97e71aabecaf3e4eb616d39e5211e2cfe3689de860ca", - "sha256:42e63904ee37ae46aa23de50dac8b145b3596f43598fa33fe1098ab2cbda6ff5", - "sha256:4eb37dd8dd1aa40d601212afa27ca5be255ba792e2e0b24d67b8af5e012cdb7d", - "sha256:51173e8fa6d9a2d85c14426bdee5f5c4a0654fd5fddcc21fe9d09ab0f6eb8b35", - "sha256:5144feb20fe76e73e60c7d73ec3bf54f320247d1ebe737d10672480371878b48", - "sha256:5344be476ac37eb9c9ad09c22f4ea193c1316bf074f1daf85bddb1b31fda5116", - "sha256:6108e5933eb8c22cd3646e72d5b54772c29f57482fd4c41a0640aab99eb5071d", - "sha256:6a007a541dff984264981fbafeb052bfe361db63578948d857907df9488d8774", - "sha256:6ee26e9dfb3996aff7c870f09dc7ad44a5f6732b8bdb5a5f9905737ac6fd4ef1", - "sha256:750de923b456ca8c0f1354d6befca45d1f3b3a789e76efc16741bd4132752d95", - "sha256:7c5ede2e2558f088c49a1ddda19080e4c23fb5d171de80a726b61b567e3766ed", - "sha256:830215173ad45d670140ff99aac3b461f9be9a6b11bee1a17265aaaa746a641a", - "sha256:8391cea5ce72f4a12368afd17799474015d5d3dc00c936a907eb7c7eaaea98a5", - "sha256:8940d6de7068af018dfa9a959a3510e9b7b543f4c405e88463a1cbaa3b2b379a", - "sha256:89a49cc5ad08a38b6141af17e00d1dd482dc927c7605bc77af457b5a0fca807c", - "sha256:900bc0096c2ca2d53f2e5cebf98293a7c32f532c4aeb926345e9747452233950", - "sha256:97e0efaebbfd222bcaac2f1735c010c1d3b167112d9d237daebbeedaaccf3d1d", - "sha256:9e04d4e4cfafa7c5264e535b5d28e786f0571bea609c3f0aaab13e891e933e9c", - "sha256:a4c60abd950d6de3e4f1ddbc318075654d275c29c846ab6a043d6ed2c52e4c8c", - "sha256:a6ff459dac39541e6a2763a4439c4ca6bc9ecb4acc05a99b79246751f9894756", - "sha256:a72797549935c9e0b9bc1def1768c8b5a709538fa6ab0678e671aec47ebfd55e", - "sha256:af4063ef2b11b96d949dccbc5a987272f38d55c23c4c01841ea65a517906397f", - "sha256:b975b85d1d5efc36cf8b237c5f3849b64d1ba33d6282f5e991f28751317504a1", - "sha256:bf0b9959e673505ee5869950642428046edb91f99942607c2ecf635f8a4b31c9", - "sha256:c0c85c5cbe8b30a32fa6d802588d55ffabf720e985abe9590c7c886919d875d4", - "sha256:c3f3237a57e42f79f1e560726576aedb3a7ef931f4e3accb84ebf6acc485d316", - "sha256:c3fa3ab0fb200a2c66493828ed06ccd1a94b12eddbfb985e7fd3e5723ff156c6", - "sha256:c435f5ce1705de48e08fcbcfaf8aee660d199c90536e3e06f2016af7d6a938dd", - "sha256:c90da4b124647547a68cf2f197174ada30c7bb9523cb976665dfd26a9963d328", - "sha256:cbdf2c498e077282cd427cfd88bdce4668019791deef0be8155385ab2ba7837f", - "sha256:d1fbad1f9077372b6587ec589c1fc120b417b6c8ad72d3e3cc86bbbd0a3cee93", - "sha256:d39f5d4af48c138cb146763eda14eb7d8b3ccbbec9fe86fb724cd16e0e914c64", - "sha256:ddb4a6061933bd9332b74eac0da25f17f32afa7145a33a0f9711ad74f924b1b8", - "sha256:ded637176addc1d3eef35331c39acc598bac550d213f0a1bedabfceaa2244c87", - "sha256:f20fd21f7538f8107451156dd1fe203300b79a9ddceba1ee0ac8132521a008ed", - "sha256:fda2783c12f553cdca11c08e5af6eecbd717280dc8fbe28a110897af1c15a88c" + "sha256:073f959c6f570797272f4ee9464a9997eaf1e98c27cb680225b82b53390d61e6", + "sha256:0fd3b3968ffe7643144580f260f04d39d869fcc2cddb745deef078b09fd2b328", + "sha256:1434ca77d6fed4ea312901122dc8da6c4389738bf5788f43efb19a838ac03ead", + "sha256:1c30bb23a41df95109db130a6cc1b974844300ae2e5d68dd4947aacba5985aa5", + "sha256:20e7a4f7ded59097c84059d28230907cd97130fa74f4a8bfd1d8e5ba18c81491", + "sha256:2199165a1affb666aa24adf0c97436686d0a61bc5fc113c037701fb7c7fceb96", + "sha256:297eef542156d6b15174a1231c2493ea9ea54af8d016b8ca7d5d9cc65cfcc444", + "sha256:2aef56e85901c2397bd557c5ba514f84de1f0ae5dd132f5d5fed042858115951", + "sha256:30943b9530fe3620e3b195c03130396cd0ee3a0d10a66c1bee715d1819001eaf", + "sha256:3b36a2c6d4920ba88fa98075fdd58ff94ebeb8acc1215ae07d01a418af4c0253", + "sha256:428d699c8553c27e98f4d29fdc0f0edc50e9a8a7590bfd294d2edb0da7be3629", + "sha256:43e636dc2ce9ece583b3e2ca41df5c983f4302eabc6d5f9cd04f0562ee8ec1ae", + "sha256:452ca5b4afed30e7274445dd9b441a35ece656ec1600b77fff8c216fdf07df43", + "sha256:467a7d31554892eed2aa6c2d47ded1079fc40ea0b9601d9f79204afa8902274b", + "sha256:4b44d7e39964e808b071714666a812049765b26b3ea48c4434a3b317bac82f14", + "sha256:4c86343cf9ff7b2514dd229bdd88ebba760bd8973dac192ae687ff75e39ebfab", + "sha256:5208a57eae445ae84a219dfd8b56e04313445d146873117b5fa75f3245bc1390", + "sha256:5ff21e000ff2f658430bde5288cb1ac440ff15c0d7d18b5fb222f941b46cb0d2", + "sha256:675997222f2e2f22928fbba640824aebd43791116034f62006e19730715166c0", + "sha256:676e4a44e740deaba0f4d95ba1d8c5c89a2fcc43d02c39f69450b1fa19d39590", + "sha256:6e306b97966369b889985a562ede9d99180def39ad42c8014628dd3cc343f508", + "sha256:6fd9584bf1bccdfff1512719316efa77be235469e1e3295dce64538c4773840b", + "sha256:705a68a973c4c76db5d369ed573fec3367d7d196673fa86614b33d8c8e9ebb08", + "sha256:74d7d9fa97809c5b892449b28a65ec2bfa458a4735ddad46074f9f7d9550ad13", + "sha256:77c8a317f0fd5a0a2be8ed5cbe5341537d5c00bb79b3bb27ba7c5378ba77dbca", + "sha256:79a050889eb8d57a93ed21d9585bb63fca881666fc709f5d9f7f9372f5e7fd03", + "sha256:7db16dd4ea1b05ada504f08d0dca1cd9b926bed3770f50e715d087c6f00ad748", + "sha256:83f2292ae292ed5a47cdcb9821039ca8e88902923198f2193f13959360c01860", + "sha256:87c9224acba0ad8bacddf427a1c2772e17ce50b3042a789547af27099c5f751d", + "sha256:8a97a681e82bc11a42d4372fe57898d270a2707f36c45c6676e49ce0d5c41353", + "sha256:9073513ec380434eb8d21970e1ab3161041de121f4018bbed3146839451a6d8e", + "sha256:90bdd76b3f04bdb21de5398b8a7c629676c81dfac290f5f19883857e9371d28c", + "sha256:91229d7203f1ef0ab420c9b53fe2ca5c1fbeb34f69b3bc1b5089466237a4a134", + "sha256:92f88ca1b956eb8427a11bb8b4a0c0b2b03377235fc5102cb05e533b8693a415", + "sha256:95ae3e8e2c1b9bf671817f86f155c5da7d49a2289c5cf27a319458c3e025c320", + "sha256:9e30be89a75ee66aec7f9e60086fadb37ff8c0ba49a022887c28c134341f7179", + "sha256:a48edde788b99214613e440fce495bbe2b1e142a7f214cce9e0832146c41e324", + "sha256:a7152fa6e597c20cb97923407cf0934e14224af42c2b8d915f48bc3ad2d9ac18", + "sha256:a9c7b71211f066908e518a2ef7a5e211670761651039f0d6a80d8d40054047df", + "sha256:b0571a5aef36ba9177e262dc88a9240c866d903a62799e44fd4aae3f9a2ec17e", + "sha256:b0fb2d4801546598ac5cd18e3ec79c1a9af8b8f2a86283c55a5337c5aeca4b1b", + "sha256:b10241250cb77657ab315270b064a6c7f1add58af94befa20687e7c8d8603ae6", + "sha256:b87efe4a380887425bb15f220079aa8336276398dc33fce38c64d278164f963d", + "sha256:b98f43fcdb16172dec5f4b49f2fece4b16a99fd284d81c6bbac1b3b69fcbe0ff", + "sha256:c193109ca4070cdcaa6eff00fdb5a56233dc7610216d58fb81638f89f02e4968", + "sha256:c826f93050c73e7769806f92e601e0efdb83ec8d7c76ddf45d514fee54e8e619", + "sha256:d020cfa595d1f8f5c6b343530cd3ca16ae5aefdd1e832b777f9f0eb105f5b139", + "sha256:d6a478581b1a1a8fdf3318ecb5f4d0cda41cacdffe2b527c23707c9c1b8fdb55", + "sha256:de2ad69c9a094bf37c1102b5744c9aec6cf74d2b635558b779085d0263166454", + "sha256:e278eafb406f7e1b1b637c2cf51d3ad45883bb5bd1ca56bc05e4fc135dfdaa65", + "sha256:e381fe0c2aa6c03b056ad8f52f8efca7be29fb4d9ae2f8873520843b6039612a", + "sha256:e61e76020e0c332a98290323ecfec721c9544f5b739fab925b6e8cbe1944cf19", + "sha256:f897c3b127532e6befdcf961c415c97f320d45614daf84deba0a54e64ea2457b", + "sha256:fb464479934778d7cc5baf463d959d361954d6533ad34c3a4f1d267e86ee25fd" ], "index": "pypi", - "version": "==1.56.2" + "version": "==1.60.0" }, "grpcio-tools": { "hashes": [ - "sha256:0059dfc9bea8f7bca69c15ca62c88904c4f907fde1137e0743b5eee054661873", - "sha256:014da3ed176beb2b1c8430ccc34c8fe962cdd5480e56fb4ab9de60e60c315f3f", - "sha256:0a4f9cce5a16613b6d3123c89f9d50e0d13b466799af17bc723dc7d2901a54e4", - "sha256:13388a22fcba9a1a87f217130a1a01365716af74bd5d0a8a54fc383b8e048ef2", - "sha256:14120fb2c6f7894fac5b689934368c692ec50f50a320e8073277ab7778fd612f", - "sha256:1f334718eb796799bfadbac5567456fb745cee8c7b438c93b74d1ce676c6ad07", - "sha256:2037109c1ce253a8e013c9e3ad3722e887d28a1807acdeb1a51b295c8200137b", - "sha256:216e86d3a6ccc31b27fa4c12491981a0a39d4787d2358b6df05baffa40084494", - "sha256:24fc857252181c9950ed2d8cee3df5bd0f42861c4ad0db2a57400186827f96e5", - "sha256:26751f69cbbc8ea19cf0657b7d109a6db7df81f80caf16380ebcd20eea27652c", - "sha256:28444615b7a76b3d9267f81d1487fcad21a581d00564164d9e25ccc28635a811", - "sha256:31d1183d28ffc8da242333cb9f683f5093941da80dd5281db0fa93077aecb518", - "sha256:355204d1b33c7a19e7d69afda411e6595d39ba1e9cbf561770ac1d5403296554", - "sha256:380985b8d95ea2469e103945bd83a815d1213e370f580631fdd5a3dbaa17e446", - "sha256:3a74a5e4fc8121a51401665f96f9a70aee50a2f1221e4a199e67b3b8f55881e8", - "sha256:4056ff13e30813d42a30ce1cdfeaeb6bbee915515c161c1df896dac3143ae643", - "sha256:41af279cf5359b123138236c0980440f4cb4d3d18f03b5c1c314cc1512048351", - "sha256:42272376e9a5a1c631863cda056c143c98d21e5b670db5c8c5b7ed0ba3a1a6eb", - "sha256:45d8b5ad6716848d5b68d9cee29a1a9c5c4baa1824ec5b92d9e35acedddba076", - "sha256:483256d5f5be6a77b24d8a5f06ca152d1571c62bf5c738834da61107c7563afe", - "sha256:493775d17ea09cea6047ba81e4d3f0eb82e34d2fbd3b96e43f72b44ce74726ee", - "sha256:506d00a86950adf4017395551a4547c0b7fcefa90e4c220135fc3e34e31be81b", - "sha256:5223668649172d879ee780253b8e4a79144c56a3cc1bb021847f583508c2b0be", - "sha256:54da410124547bacb97a54546c1a95f1af0125e48edc8b5679412ef8b2844f81", - "sha256:68ef3aa7509e5e7a6e7c0ecc183e28118e73da4bef0fc77079648601ce35e58f", - "sha256:6bfb375eb4f1946d68b8bc7b963c756defa31aa573a35c152a7233d06c0ad6ad", - "sha256:6dc43300189a69807857c52a3d782e9d3bbfb1cb72dcb27b4043c25161919601", - "sha256:778224fcbc1cc7eaf222ce94676afbac8d72b4f84cf4239e30b01d2450a46126", - "sha256:7a26160bc0ea5b464715789d4d2a66f01816271677673d65da39bac65b9ea838", - "sha256:7d86e24eb6e3973c55e9c74412ff755d1b9d15518c4eaf95676acff49d0162a2", - "sha256:82af2f4040084141a732f0ef1ecf3f14fdf629923d74d850415e4d09a077e77a", - "sha256:857d72e991d449ec4d2f8337e5e24ddf77b4539965f5cabc84d4b63585832982", - "sha256:878b9269ceb0dd934b61697a9dd9a5c3e9552521e8f46ab32cf4d72a223f7b6c", - "sha256:8da04f033b8f4c597e8fc990e2f626bad2b269227bdd554592ea618f624f1aa9", - "sha256:8febb4f90b8fab3179f5bdaa159f1d2a20523ea17ec0d66bdec7732f9532de91", - "sha256:a8735d7aa34be99dddfbd476eff6005e684bb2c893c0f62a5811528b84c5b371", - "sha256:bec47db5d8b5c3b2a44afdbc3f3bf306e34279289a206d20222824381ca3cb13", - "sha256:c0640728d63c9fa56e9a1679943ae4e33ad43a10802dd7a93255870731f44d07", - "sha256:c0dbaac63a25c088f864295f394230eeb7be48dac2264433fda2603f86c36b25", - "sha256:c7ca2272022f90b73efe900244aaebe9dd7cf3b379e99e08a88984e2fdd229c2", - "sha256:e7009623635ebcd3dd7fe974883fc2d9a3ff0fcef419bfc0a2da8071b372d9f5", - "sha256:ea5d108d28b4cd2e28539241c6aee96bda83086d8888c36785d9f84ea690d896", - "sha256:ea5fc1b49514b44a3e5a45156c025002f172ade4c509e58c51967865c7c6fa45", - "sha256:ff16dd0b086e75f574dbc122e018a44dbd1c6dae3f3621ea99e8e5a6b2706e12", - "sha256:ffae7df3318266614f7aa440acb2098c064b6b5ae061fc22125092386349e526" + "sha256:081336d8258f1a56542aa8a7a5dec99a2b38d902e19fbdd744594783301b0210", + "sha256:1748893efd05cf4a59a175d7fa1e4fbb652f4d84ccaa2109f7869a2be48ed25e", + "sha256:17a32b3da4fc0798cdcec0a9c974ac2a1e98298f151517bf9148294a3b1a5742", + "sha256:18976684a931ca4bcba65c78afa778683aefaae310f353e198b1823bf09775a0", + "sha256:1b93ae8ffd18e9af9a965ebca5fa521e89066267de7abdde20721edc04e42721", + "sha256:1fbb9554466d560472f07d906bfc8dcaf52f365c2a407015185993e30372a886", + "sha256:24c4ead4a03037beaeb8ef2c90d13d70101e35c9fae057337ed1a9144ef10b53", + "sha256:2a8a758701f3ac07ed85f5a4284c6a9ddefcab7913a8e552497f919349e72438", + "sha256:2dd01257e4feff986d256fa0bac9f56de59dc735eceeeb83de1c126e2e91f653", + "sha256:2e00de389729ca8d8d1a63c2038703078a887ff738dc31be640b7da9c26d0d4f", + "sha256:2fb4cf74bfe1e707cf10bc9dd38a1ebaa145179453d150febb121c7e9cd749bf", + "sha256:2fd1671c52f96e79a2302c8b1c1f78b8a561664b8b3d6946f20d8f1cc6b4225a", + "sha256:321b18f42a70813545e416ddcb8bf20defa407a8114906711c9710a69596ceda", + "sha256:3456df087ea61a0972a5bc165aed132ed6ddcc63f5749e572f9fff84540bdbad", + "sha256:4041538f55aad5b3ae7e25ab314d7995d689e968bfc8aa169d939a3160b1e4c6", + "sha256:559ce714fe212aaf4abbe1493c5bb8920def00cc77ce0d45266f4fd9d8b3166f", + "sha256:5a907a4f1ffba86501b2cdb8682346249ea032b922fc69a92f082ba045cca548", + "sha256:5ce6bbd4936977ec1114f2903eb4342781960d521b0d82f73afedb9335251f6f", + "sha256:6170873b1e5b6580ebb99e87fb6e4ea4c48785b910bd7af838cc6e44b2bccb04", + "sha256:6192184b1f99372ff1d9594bd4b12264e3ff26440daba7eb043726785200ff77", + "sha256:6807b7a3f3e6e594566100bd7fe04a2c42ce6d5792652677f1aaf5aa5adaef3d", + "sha256:687f576d7ff6ce483bc9a196d1ceac45144e8733b953620a026daed8e450bc38", + "sha256:74025fdd6d1cb7ba4b5d087995339e9a09f0c16cf15dfe56368b23e41ffeaf7a", + "sha256:7a5263a0f2ddb7b1cfb2349e392cfc4f318722e0f48f886393e06946875d40f3", + "sha256:7a6fe752205caae534f29fba907e2f59ff79aa42c6205ce9a467e9406cbac68c", + "sha256:7c1cde49631732356cb916ee1710507967f19913565ed5f9991e6c9cb37e3887", + "sha256:811abb9c4fb6679e0058dfa123fb065d97b158b71959c0e048e7972bbb82ba0f", + "sha256:857c5351e9dc33a019700e171163f94fcc7e3ae0f6d2b026b10fda1e3c008ef1", + "sha256:87cf439178f3eb45c1a889b2e4a17cbb4c450230d92c18d9c57e11271e239c55", + "sha256:9970d384fb0c084b00945ef57d98d57a8d32be106d8f0bd31387f7cbfe411b5b", + "sha256:9ee35234f1da8fba7ddbc544856ff588243f1128ea778d7a1da3039be829a134", + "sha256:addc9b23d6ff729d9f83d4a2846292d4c84f5eb2ec38f08489a6a0d66ac2b91e", + "sha256:b22b1299b666eebd5752ba7719da536075eae3053abcf2898b65f763c314d9da", + "sha256:b8f7a5094adb49e85db13ea3df5d99a976c2bdfd83b0ba26af20ebb742ac6786", + "sha256:b96981f3a31b85074b73d97c8234a5ed9053d65a36b18f4a9c45a2120a5b7a0a", + "sha256:bbf0ed772d2ae7e8e5d7281fcc00123923ab130b94f7a843eee9af405918f924", + "sha256:bd2a17b0193fbe4793c215d63ce1e01ae00a8183d81d7c04e77e1dfafc4b2b8a", + "sha256:c771b19dce2bfe06899247168c077d7ab4e273f6655d8174834f9a6034415096", + "sha256:d941749bd8dc3f8be58fe37183143412a27bec3df8482d5abd6b4ec3f1ac2924", + "sha256:dba6e32c87b4af29b5f475fb2f470f7ee3140bfc128644f17c6c59ddeb670680", + "sha256:dd1e68c232fe01dd5312a8dbe52c50ecd2b5991d517d7f7446af4ba6334ba872", + "sha256:e5614cf0960456d21d8a0f4902e3e5e3bcacc4e400bf22f196e5dd8aabb978b7", + "sha256:e5c519a0d4ba1ab44a004fa144089738c59278233e2010b2cf4527dc667ff297", + "sha256:e68dc4474f30cad11a965f0eb5d37720a032b4720afa0ec19dbcea2de73b5aae", + "sha256:e70d867c120d9849093b0ac24d861e378bc88af2552e743d83b9f642d2caa7c2", + "sha256:e87cabac7969bdde309575edc2456357667a1b28262b2c1f12580ef48315b19d", + "sha256:eae27f9b16238e2aaee84c77b5923c6924d6dccb0bdd18435bf42acc8473ae1a", + "sha256:ec0e401e9a43d927d216d5169b03c61163fb52b665c5af2fed851357b15aef88", + "sha256:ed30499340228d733ff69fcf4a66590ed7921f94eb5a2bf692258b1280b9dac7", + "sha256:f10ef47460ce3c6fd400f05fe757b90df63486c9b84d1ecad42dcc5f80c8ac14", + "sha256:f3d916606dcf5610d4367918245b3d9d8cd0d2ec0b7043d1bbb8c50fe9815c3a", + "sha256:f610384dee4b1ca705e8da66c5b5fe89a2de3d165c5282c3d1ddf40cb18924e4", + "sha256:fb4df80868b3e397d5fbccc004c789d2668b622b51a9d2387b4c89c80d31e2c5", + "sha256:fc01bc1079279ec342f0f1b6a107b3f5dc3169c33369cf96ada6e2e171f74e86" ], "index": "pypi", - "version": "==1.56.2" + "version": "==1.60.0" }, "idna": { "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" ], "markers": "python_version >= '3.5'", - "version": "==3.4" + "version": "==3.6" }, "importlib-metadata": { "hashes": [ - "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb", - "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743" + "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e", + "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc" ], "markers": "python_version >= '3.8'", - "version": "==6.8.0" + "version": "==7.0.1" }, "iniconfig": { "hashes": [ @@ -381,11 +414,11 @@ }, "keyring": { "hashes": [ - "sha256:4901caaf597bfd3bbd78c9a0c7c4c29fcd8310dab2cffefe749e916b6527acd6", - "sha256:ca0746a19ec421219f4d713f848fa297a661a8a8c1504867e55bfb5e09091509" + "sha256:4446d35d636e6a10b8bce7caa66913dd9eca5fd222ca03a3d42c38608ac30836", + "sha256:e730ecffd309658a08ee82535a3b5ec4b4c8669a9be11efb66249d8e0aeb9a25" ], "markers": "python_version >= '3.8'", - "version": "==24.2.0" + "version": "==24.3.0" }, "markdown-it-py": { "hashes": [ @@ -411,6 +444,27 @@ "markers": "python_version >= '3.8'", "version": "==10.1.0" }, + "nh3": { + "hashes": [ + "sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770", + "sha256:3277481293b868b2715907310c7be0f1b9d10491d5adf9fce11756a97e97eddf", + "sha256:3b803a5875e7234907f7d64777dfde2b93db992376f3d6d7af7f3bc347deb305", + "sha256:427fecbb1031db085eaac9931362adf4a796428ef0163070c484b5a768e71601", + "sha256:5f0d77272ce6d34db6c87b4f894f037d55183d9518f948bba236fe81e2bb4e28", + "sha256:60684857cfa8fdbb74daa867e5cad3f0c9789415aba660614fe16cd66cbb9ec7", + "sha256:6f42f99f0cf6312e470b6c09e04da31f9abaadcd3eb591d7d1a88ea931dca7f3", + "sha256:86e447a63ca0b16318deb62498db4f76fc60699ce0a1231262880b38b6cff911", + "sha256:8d595df02413aa38586c24811237e95937ef18304e108b7e92c890a06793e3bf", + "sha256:9c0d415f6b7f2338f93035bba5c0d8c1b464e538bfbb1d598acd47d7969284f0", + "sha256:a5167a6403d19c515217b6bcaaa9be420974a6ac30e0da9e84d4fc67a5d474c5", + "sha256:ac19c0d68cd42ecd7ead91a3a032fdfff23d29302dbb1311e641a130dfefba97", + "sha256:b1e97221cedaf15a54f5243f2c5894bb12ca951ae4ddfd02a9d4ea9df9e1a29d", + "sha256:bc2d086fb540d0fa52ce35afaded4ea526b8fc4d3339f783db55c95de40ef02e", + "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3", + "sha256:f3b53ba93bb7725acab1e030bc2ecd012a817040fd7851b332f86e2f9bb98dc6" + ], + "version": "==0.2.15" + }, "orderedmultidict": { "hashes": [ "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad", @@ -428,19 +482,19 @@ }, "pep517": { "hashes": [ - "sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b", - "sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59" + "sha256:1b2fa2ffd3938bb4beffe5d6146cbcb2bda996a5a4da9f31abffd8b24e07b317", + "sha256:31b206f67165b3536dd577c5c3f1518e8fbaf38cbc57efff8369a392feff1721" ], "markers": "python_version >= '3.6'", - "version": "==0.13.0" + "version": "==0.13.1" }, "pip": { "hashes": [ - "sha256:7ccf472345f20d35bdc9d1841ff5f313260c2c33fe417f48c30ac46cccabf5be", - "sha256:fb0bd5435b3200c602b5bf61d2d43c2f13c02e29c1707567ae7fbc514eb9faf2" + "sha256:5052d7889c1f9d05224cd41741acb7c5d6fa735ab34e339624a614eaaa7e7d76", + "sha256:7fd9972f96db22c8077a1ee2691b172c8089b17a5652a44494a9ecb0d78f9149" ], "markers": "python_version >= '3.7'", - "version": "==23.2.1" + "version": "==23.3.2" }, "pip-shims": { "hashes": [ @@ -474,11 +528,11 @@ }, "platformdirs": { "hashes": [ - "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d", - "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d" + "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", + "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" ], - "markers": "python_version >= '3.7'", - "version": "==3.10.0" + "markers": "python_version >= '3.8'", + "version": "==4.1.0" }, "plette": { "extras": [ @@ -493,46 +547,36 @@ }, "pluggy": { "hashes": [ - "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", - "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3" + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" ], - "markers": "python_version >= '3.7'", - "version": "==1.2.0" + "markers": "python_version >= '3.8'", + "version": "==1.3.0" }, "protobuf": { "hashes": [ - "sha256:0a5759f5696895de8cc913f084e27fd4125e8fb0914bb729a17816a33819f474", - "sha256:351cc90f7d10839c480aeb9b870a211e322bf05f6ab3f55fcb2f51331f80a7d2", - "sha256:5fea3c64d41ea5ecf5697b83e41d09b9589e6f20b677ab3c48e5f242d9b7897b", - "sha256:6dd9b9940e3f17077e820b75851126615ee38643c2c5332aa7a359988820c720", - "sha256:7b19b6266d92ca6a2a87effa88ecc4af73ebc5cfde194dc737cf8ef23a9a3b12", - "sha256:8547bf44fe8cec3c69e3042f5c4fb3e36eb2a7a013bb0a44c018fc1e427aafbd", - "sha256:9053df6df8e5a76c84339ee4a9f5a2661ceee4a0dab019e8663c50ba324208b0", - "sha256:c3e0939433c40796ca4cfc0fac08af50b00eb66a40bbbc5dee711998fb0bbc1e", - "sha256:ccd9430c0719dce806b93f89c91de7977304729e55377f872a92465d548329a9", - "sha256:e1c915778d8ced71e26fcf43c0866d7499891bca14c4368448a82edc61fdbc70", - "sha256:e9d0be5bf34b275b9f87ba7407796556abeeba635455d036c7351f7c183ef8ff", - "sha256:effeac51ab79332d44fba74660d40ae79985901ac21bca408f8dc335a81aa597", - "sha256:fee88269a090ada09ca63551bf2f573eb2424035bcf2cb1b121895b01a46594a" + "sha256:0bf384e75b92c42830c0a679b0cd4d6e2b36ae0cf3dbb1e1dfdda48a244f4bcd", + "sha256:0f881b589ff449bf0b931a711926e9ddaad3b35089cc039ce1af50b21a4ae8cb", + "sha256:1484f9e692091450e7edf418c939e15bfc8fc68856e36ce399aed6889dae8bb0", + "sha256:193f50a6ab78a970c9b4f148e7c750cfde64f59815e86f686c22e26b4fe01ce7", + "sha256:3497c1af9f2526962f09329fd61a36566305e6c72da2590ae0d7d1322818843b", + "sha256:57d65074b4f5baa4ab5da1605c02be90ac20c8b40fb137d6a8df9f416b0d0ce2", + "sha256:8bdbeaddaac52d15c6dce38c71b03038ef7772b977847eb6d374fc86636fa510", + "sha256:a19731d5e83ae4737bb2a089605e636077ac001d18781b3cf489b9546c7c80d6", + "sha256:abc0525ae2689a8000837729eef7883b9391cd6aa7950249dcf5a4ede230d5dd", + "sha256:becc576b7e6b553d22cbdf418686ee4daa443d7217999125c045ad56322dda10", + "sha256:ca37bf6a6d0046272c152eea90d2e4ef34593aaa32e8873fc14c16440f22d4b7" ], "index": "pypi", - "version": "==4.23.4" - }, - "pycodestyle": { - "hashes": [ - "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0", - "sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8" - ], - "markers": "python_version >= '3.8'", - "version": "==2.11.0" + "version": "==4.25.1" }, "pygments": { "hashes": [ - "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c", - "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1" + "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", + "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" ], "markers": "python_version >= '3.7'", - "version": "==2.15.1" + "version": "==2.17.2" }, "pyparsing": { "hashes": [ @@ -544,11 +588,27 @@ }, "pytest": { "hashes": [ - "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32", - "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a" + "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", + "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" + ], + "index": "pypi", + "version": "==7.4.3" + }, + "pytest-asyncio": { + "hashes": [ + "sha256:c16052382554c7b22d48782ab3438d5b10f8cf7a4bdcae7f0f67f097d95beecc", + "sha256:ea9021364e32d58f0be43b91c6233fb8d2224ccef2398d6837559e587682808f" ], "index": "pypi", - "version": "==7.4.0" + "version": "==0.23.2" + }, + "pytest-mock": { + "hashes": [ + "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f", + "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9" + ], + "index": "pypi", + "version": "==3.12.0" }, "python-dateutil": { "hashes": [ @@ -560,11 +620,11 @@ }, "readme-renderer": { "hashes": [ - "sha256:9f77b519d96d03d7d7dce44977ba543090a14397c4f60de5b6eb5b8048110aa4", - "sha256:e18feb2a1e7706f2865b81ebb460056d93fb29d69daa10b223c00faa7bd9a00a" + "sha256:13d039515c1f24de668e2c93f2e877b9dbe6c6c32328b90a40a49d8b2b85f36d", + "sha256:2d55489f83be4992fe4454939d1a051c33edbab778e82761d060c9fc6b308cd1" ], "markers": "python_version >= '3.8'", - "version": "==40.0" + "version": "==42.0" }, "requests": { "hashes": [ @@ -600,11 +660,11 @@ }, "rich": { "hashes": [ - "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808", - "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39" + "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa", + "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235" ], "markers": "python_full_version >= '3.7.0'", - "version": "==13.5.2" + "version": "==13.7.0" }, "setuptools": { "hashes": [ @@ -630,13 +690,21 @@ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + }, "tomlkit": { "hashes": [ - "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86", - "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899" + "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4", + "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba" ], "markers": "python_version >= '3.7'", - "version": "==0.12.1" + "version": "==0.12.3" }, "twine": { "hashes": [ @@ -648,11 +716,11 @@ }, "urllib3": { "hashes": [ - "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11", - "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4" + "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", + "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" ], - "markers": "python_version >= '3.7'", - "version": "==2.0.4" + "markers": "python_version >= '3.8'", + "version": "==2.1.0" }, "vistir": { "hashes": [ @@ -662,28 +730,21 @@ "index": "pypi", "version": "==0.6.1" }, - "webencodings": { - "hashes": [ - "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", - "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" - ], - "version": "==0.5.1" - }, "wheel": { "hashes": [ - "sha256:55a0f0a5a84869bce5ba775abfd9c462e3a6b1b7b7ec69d72c0b83d673a5114d", - "sha256:7e9be3bbd0078f6147d82ed9ed957e323e7708f57e134743d2edef3a7b7972a9" + "sha256:177f9c9b0d45c47873b619f5b650346d632cdc35fb5e4d25058e09c9e581433d", + "sha256:c45be39f7882c9d34243236f2d63cbd58039e360f85d0913425fbd7ceea617a8" ], "markers": "python_version >= '3.7'", - "version": "==0.41.0" + "version": "==0.42.0" }, "zipp": { "hashes": [ - "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0", - "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147" + "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31", + "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0" ], "markers": "python_version >= '3.8'", - "version": "==3.16.2" + "version": "==3.17.0" } } } diff --git a/README.md b/README.md index 3a3b304..44d0420 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Install the File Security SDK package with pip: ## Obtain an API Key -The File Security SDK requires a valid API Key provided as parameter to the SDK client object. It can accept Trend Vision One API keys. +The File Security SDK requires a valid API Key provided as parameter to the SDK client object. It can accept Trend Vision One API keys. When obtaining the API Key, ensure that the API Key is associated with the region that you plan to use. It is important to note that Trend Vision One API Keys are associated with different regions, please refer to the region flag below to obtain a better understanding of the valid regions associated with the respective API Key. @@ -29,9 +29,9 @@ If you plan on using a Trend Vision One region, be sure to pass in region parame 1. Login to the Trend Vision One. 2. Create a new Trend Vision One API key: -* Navigate to the Trend Vision One User Roles page. -* Verify that there is a role with the "Run file scan via SDK" permissions enabled. If not, create a role by clicking on "Add Role" and "Save" once finished. -* Directly configure a new key on the Trend Vision One API Keys page, using the role which contains the "Run file scan via SDK" permission. It is advised to set an expiry time for the API key and make a record of it for future reference. +- Navigate to the Trend Vision One User Roles page. +- Verify that there is a role with the "Run file scan via SDK" permissions enabled. If not, create a role by clicking on "Add Role" and "Save" once finished. +- Directly configure a new key on the Trend Vision One API Keys page, using the role which contains the "Run file scan via SDK" permission. It is advised to set an expiry time for the API key and make a record of it for future reference. ## Run SDK @@ -52,12 +52,14 @@ If you plan on using a Trend Vision One region, be sure to pass in region parame 3. Current Python examples support following command line arguments - | Command Line Arguments | Value | Optional | - | :------------------ | :----------------------- | :------- | - | --region or -r | The region you obtained your API key. Value provided must be one of the Vision One regions, e.g. `us-east-1`, `eu-central-1`, `ap-southeast-1`, `ap-southeast-2`, `ap-northeast-1` | Yes, either -r or -a | - | --addr or -a | Trend Vision One File Security server, such as: antimalware.__REGION__.cloudone.trendmicro.com:443 | Yes, either -r or -a | - | --api_key | Vision One API Key | No | - | --filename or -f | File to be scanned | No | + | Command Line Arguments | Value | Optional | + |------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------| + | --region or -r | The region you obtained your API key. Value provided must be one of the Vision One regions, e.g. `us-east-1`, `eu-central-1`, `ap-southeast-1`, `ap-southeast-2`, `ap-northeast-1` | Yes, either -r or -a | + | --addr or -a | Trend Vision One File Security server, such as: antimalware.__REGION__.cloudone.trendmicro.com:443 | Yes, either -r or -a | + | --api_key | Vision One API Key | No | + | --filename or -f | File to be scanned | No | + | --pml | Predictive Machine Learning | Yes | + | --tags or -f | List of tags | Yes | 4. Run one of the examples. @@ -82,46 +84,3 @@ If you plan on using a Trend Vision One region, be sure to pass in region parame ```sh python3 client_aio.py -f FILENAME -a antimalware._REGION_.cloudone.trendmicro.com:443 --tls true --api_key API_KEY ``` - -### Code Examples - -```python -import json -import amaas.grpc - -handle = amaas.grpc.init(YOUR_FILE_SECURITY_SERVER, YOUR_VISION_ONE_KEY, True) - -result = amaas.grpc.scan_file(args.filename, handle) -print(result) - -result_json = json.loads(result) -print("Got scan result: %d" % result_json['scanResult']) - -amaas.grpc.quit(handle) - -``` - -to use asyncio with coroutines and tasks, - -```python: -import json -import pprint -import asyncio -import amaas.grpc.aio - -async def scan_files(): - handle = amaas.grpc.aio.init(YOUR_FILE_SECURITY_SERVER, YOUR_VISION_ONE_KEY, True) - - tasks = [asyncio.create_task(amaas.grpc.aio.scan_file(file_name, handle))] - - scan_results = await asyncio.gather(*tasks) - - for scan_result in scan_results: - pprint.pprint(json.loads(scan_result)) - - await amaas.grpc.aio.quit(handle) - - -asyncio.run(scan_files()) - -``` diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..9084fa2 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.1.0 diff --git a/amaas/_version.py b/amaas/_version.py deleted file mode 100644 index 7863915..0000000 --- a/amaas/_version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "1.0.2" diff --git a/amaas/grpc/__init__.py b/amaas/grpc/__init__.py index f95e7a6..a69c0ce 100644 --- a/amaas/grpc/__init__.py +++ b/amaas/grpc/__init__.py @@ -56,33 +56,56 @@ def init(host, api_key=None, enable_tls=False, ca_cert=None): return _init_util(host, api_key, enable_tls, ca_cert, False) -def _generate_messages(pipeline: _Pipeline, data_reader: BinaryIO, stats: dict) -> None: +def _generate_messages(pipeline: _Pipeline, data_reader: BinaryIO, bulk: bool, stats: dict) -> None: + responses = [] + while True: + for r in responses: + if r[0] == "INIT": + response = r[1] + elif r[0] == "RUN": + offset = r[1] + length = r[2] + data_reader.seek(offset) + chunk = data_reader.read(length) + response = scan_pb2.C2S( + stage=scan_pb2.STAGE_RUN, + file_name=None, + rs_size=0, + offset=offset, + chunk=chunk, + ) + stats["total_upload"] = stats.get("total_upload", 0) + len(chunk) + else: + raise AMaasException(AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE, "None", r[0]) + yield response + + responses.clear() message = pipeline.get_message() if message.stage == scan_pb2.STAGE_INIT: logger.debug("stage INIT") - yield message + responses.append(("INIT", message)) elif message.stage == scan_pb2.STAGE_RUN: if message.cmd != scan_pb2.CMD_RETR: raise AMaasException(AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE, message.cmd, message.stage) - logger.debug( - f"stage RUN, try to read {message.length} at offset {message.offset}") + length = [] + offset = [] - data_reader.seek(message.offset) - chunk = data_reader.read(message.length) + if bulk: + offset = message.bulk_offset[:] + length = message.bulk_length[:] - message = scan_pb2.C2S( - stage=scan_pb2.STAGE_RUN, - file_name=None, - rs_size=0, - offset=data_reader.tell(), - chunk=chunk) + if len(length) > 1: + logger.debug("bulk transfer triggered") + else: + offset.append(message.offset) + length.append(message.length) - stats["total_upload"] = stats.get( - "total_upload", 0) + len(chunk) - yield message + for i in range(len(length)): + logger.debug(f"stage RUN, try to read {length[i]} at offset {offset[i]}") + responses.append(("RUN", offset[i], length[i])) elif message.stage == scan_pb2.STAGE_FINI: if message.cmd != scan_pb2.CMD_QUIT: raise AMaasException(AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE, message.cmd, message.stage) @@ -99,27 +122,31 @@ def quit(handle): def _scan_data(channel: grpc.Channel, data_reader: BinaryIO, size: int, identifier: str, tags: List[str], - app_name: str) -> str: + pml: bool, feedback: bool) -> str: _validate_tags(tags) stub = scan_pb2_grpc.ScanStub(channel) pipeline = _Pipeline() stats = {} result = None + bulk = True try: metadata = ( - (APP_NAME_HEADER, app_name), + (APP_NAME_HEADER, APP_NAME_FILE_SCAN), ) - responses = stub.Run(_generate_messages(pipeline, data_reader, stats), timeout=timeout_in_seconds, + responses = stub.Run(_generate_messages(pipeline, data_reader, bulk, stats), timeout=timeout_in_seconds, metadata=metadata) message = scan_pb2.C2S(stage=scan_pb2.STAGE_INIT, file_name=identifier, rs_size=size, offset=0, chunk=None, + trendx=pml, tags=tags, file_sha1="sha1:" + _digest_hex(data_reader, "sha1"), - file_sha256="sha256:" + _digest_hex(data_reader, "sha256")) + file_sha256="sha256:" + _digest_hex(data_reader, "sha256"), + bulk=bulk, + spn_feedback=feedback) pipeline.set_message(message) @@ -129,7 +156,7 @@ def _scan_data(channel: grpc.Channel, data_reader: BinaryIO, size: int, identifi elif response.cmd == scan_pb2.CMD_QUIT: result = response.result pipeline.set_message(response) - logger.debug("receive QUIT, exit loop...\n") + logger.debug("receive QUIT, exit loop...") break else: logger.debug("unknown command...") @@ -153,7 +180,8 @@ def _scan_data(channel: grpc.Channel, data_reader: BinaryIO, size: int, identifi return result -def scan_file(channel: grpc.Channel, file_name: str, tags: List[str] = None, app_name: str = APP_NAME_FILE_SCAN) -> str: +def scan_file(channel: grpc.Channel, file_name: str, tags: List[str] = None, + pml: bool = False, feedback: bool = False) -> str: try: f = open(file_name, "rb") fid = os.path.basename(file_name) @@ -165,10 +193,10 @@ def scan_file(channel: grpc.Channel, file_name: str, tags: List[str] = None, app logger.debug("Permission error: " + str(err)) raise AMaasException(AMaasErrorCode.MSG_ID_ERR_FILE_NO_PERMISSION, file_name) - return _scan_data(channel, f, n, fid, tags, app_name) + return _scan_data(channel, f, n, fid, tags, pml, feedback) def scan_buffer(channel: grpc.Channel, bytes_buffer: bytes, uid: str, tags: List[str] = None, - app_name: str = APP_NAME_FILE_SCAN) -> str: + pml: bool = False, feedback: bool = False) -> str: f = io.BytesIO(bytes_buffer) - return _scan_data(channel, f, len(bytes_buffer), uid, tags, app_name) + return _scan_data(channel, f, len(bytes_buffer), uid, tags, pml, feedback) diff --git a/amaas/grpc/aio/__init__.py b/amaas/grpc/aio/__init__.py index 7452cf7..74745d8 100644 --- a/amaas/grpc/aio/__init__.py +++ b/amaas/grpc/aio/__init__.py @@ -40,14 +40,16 @@ async def quit(handle): async def _scan_data(channel: grpc.Channel, data_reader: BinaryIO, size: int, identifier: str, tags: List[str], - app_name: str) -> str: + pml: bool, feedback: bool) -> str: _validate_tags(tags) stub = scan_pb2_grpc.ScanStub(channel) stats = {} result = None + bulk = True + try: metadata = ( - (APP_NAME_HEADER, app_name), + (APP_NAME_HEADER, APP_NAME_FILE_SCAN), ) call = stub.Run(timeout=timeout_in_seconds, metadata=metadata) @@ -57,8 +59,11 @@ async def _scan_data(channel: grpc.Channel, data_reader: BinaryIO, size: int, id offset=0, chunk=None, tags=tags, + trendx=pml, file_sha1="sha1:" + _digest_hex(data_reader, "sha1"), - file_sha256="sha256:" + _digest_hex(data_reader, "sha256")) + file_sha256="sha256:" + _digest_hex(data_reader, "sha256"), + bulk=bulk, + spn_feedback=feedback) await call.write(request) @@ -69,28 +74,45 @@ async def _scan_data(channel: grpc.Channel, data_reader: BinaryIO, size: int, id if response.stage != scan_pb2.STAGE_RUN: raise AMaasException(AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE, response.cmd, response.stage) - logger.debug( - f"stage RUN, try to read {response.length} at offset {response.offset}") - - data_reader.seek(response.offset) - chunk = data_reader.read(response.length) - - request = scan_pb2.C2S( - stage=scan_pb2.STAGE_RUN, - file_name=None, - rs_size=0, - offset=data_reader.tell(), - chunk=chunk) - - stats["total_upload"] = stats.get( - "total_upload", 0) + len(chunk) - await call.write(request) + length = [] + offset = [] + + if bulk: + logger.debug("enter bulk mode") + bulk_count = len(response.bulk_offset) + + if bulk_count > 1: + logger.debug("bulk transfer triggered") + + length = response.bulk_length[:] + offset = response.bulk_offset[:] + else: + logger.debug("enter non-bulk mode") + length.append(response.length) + offset.append(response.offset) + + for i in range(len(length)): + logger.debug(f"try to read {length[i]} at offset {offset[i]}") + data_reader.seek(offset[i]) + chunk = data_reader.read(length[i]) + + request = scan_pb2.C2S( + stage=scan_pb2.STAGE_RUN, + file_name=None, + rs_size=0, + offset=offset[i], + chunk=chunk) + + stats["total_upload"] = stats.get( + "total_upload", 0) + len(chunk) + + await call.write(request) elif response.cmd == scan_pb2.CMD_QUIT: if response.stage != scan_pb2.STAGE_FINI: raise AMaasException(AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE, response.cmd, response.stage) result = response.result - logger.debug("receive QUIT, exit loop...\n") + logger.debug("receive QUIT, exit loop...") break else: logger.debug("unknown command...") @@ -117,7 +139,7 @@ async def _scan_data(channel: grpc.Channel, data_reader: BinaryIO, size: int, id async def scan_file(channel: grpc.Channel, file_name: str, tags: List[str] = None, - app_name: str = APP_NAME_FILE_SCAN) -> str: + pml: bool = False, feedback: bool = False) -> str: try: f = open(file_name, "rb") fid = os.path.basename(file_name) @@ -128,10 +150,10 @@ async def scan_file(channel: grpc.Channel, file_name: str, tags: List[str] = Non except (PermissionError, IOError) as err: logger.debug("Permission error: " + str(err)) raise AMaasException(AMaasErrorCode.MSG_ID_ERR_FILE_NO_PERMISSION, file_name) - return await _scan_data(channel, f, n, fid, tags, app_name) + return await _scan_data(channel, f, n, fid, tags, pml, feedback) async def scan_buffer(channel: grpc.Channel, bytes_buffer: bytes, uid: str, tags: List[str] = None, - app_name: str = APP_NAME_FILE_SCAN) -> str: + pml: bool = False, feedback: bool = False) -> str: f = io.BytesIO(bytes_buffer) - return await _scan_data(channel, f, len(bytes_buffer), uid, tags, app_name) + return await _scan_data(channel, f, len(bytes_buffer), uid, tags, pml, feedback) diff --git a/amaas/grpc/util.py b/amaas/grpc/util.py index 9160712..9fb1a6f 100644 --- a/amaas/grpc/util.py +++ b/amaas/grpc/util.py @@ -32,7 +32,7 @@ C1Regions = [C1_AU_REGION, C1_CA_REGION, C1_DE_REGION, C1_GB_REGION, C1_IN_REGION, C1_JP_REGION, C1_SG_REGION, C1_US_REGION, C1_TREND_REGION] V1Regions = [AWS_AU_REGION, AWS_DE_REGION, AWS_IN_REGION, AWS_JP_REGION, AWS_SG_REGION, AWS_US_REGION] -SupportedV1Regions = [AWS_AU_REGION, AWS_DE_REGION, AWS_JP_REGION, AWS_SG_REGION, AWS_US_REGION] +SupportedV1Regions = V1Regions SupportedC1Regions = [C1_AU_REGION, C1_CA_REGION, C1_DE_REGION, C1_GB_REGION, C1_IN_REGION, C1_JP_REGION, C1_SG_REGION, C1_US_REGION] diff --git a/docs/sdk.md b/docs/sdk.md deleted file mode 100644 index 16dd87c..0000000 --- a/docs/sdk.md +++ /dev/null @@ -1,9 +0,0 @@ -# Cloud One AMaaS gRPC SDK for Python - -A Python library for interacting with the Cloud One Antimalware as a Service API. - -## Prerequisites - -- Python 3.7 or newer -- [CloudOne API Key](https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/) - diff --git a/examples/README.md b/examples/README.md index cff9691..e3b2ba0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,9 +7,9 @@ After completing a scan, [File Security displays scan results](https://docs.tren ## Procedure - If File Security indicates a file contains malware, you can take the following actions: - - Quarantine the file. See a [AWS quarantine example](https://github.com/trendmicro/tm-v1-fs-python-sdk/blob/main/examples/aws_quarantine.py). - - Send notification messages using: - - Slack - - Email - - Microsoft Teams -- If File Security does not find malware in a file, you can promote the file. \ No newline at end of file + - Quarantine the file. See a [AWS quarantine example](https://github.com/trendmicro/tm-v1-fs-python-sdk/blob/main/examples/aws_quarantine.py). + - Send notification messages using: + - Slack + - Email + - Microsoft Teams +- If File Security does not find malware in a file, you can promote the file. diff --git a/examples/client.py b/examples/client.py index 4c90c6d..665c5f7 100644 --- a/examples/client.py +++ b/examples/client.py @@ -3,7 +3,6 @@ import time import amaas.grpc -from distutils.util import strtobool if __name__ == "__main__": @@ -14,13 +13,15 @@ parser.add_argument('-a', '--addr', action='store', default='127.0.0.1:50051', required=False, help='gRPC server address and port (default 127.0.0.1:50051)') parser.add_argument('-r', '--region', action='store', - help='AMaaS service region; e.g. us-1 or dev') + help='AMaaS service region; e.g. us-east-1 or eu-central-1') parser.add_argument('--api_key', action='store', help='api key for authentication') - parser.add_argument('--tls', type=lambda x: bool(strtobool(x)), default=False, + parser.add_argument('--tls', action=argparse.BooleanOptionalAction, default=False, help='enable TLS gRPC ') parser.add_argument('--ca_cert', action='store', help='CA certificate') + parser.add_argument('--pml', action=argparse.BooleanOptionalAction, default=False, + help='enable predictive machine learning detection') parser.add_argument('-t', '--tags', action='store', nargs='+', help='list of tags') @@ -34,7 +35,7 @@ s = time.perf_counter() try: - result = amaas.grpc.scan_file(handle, args.filename, args.tags) + result = amaas.grpc.scan_file(handle, file_name=args.filename, pml=args.pml, tags=args.tags) elapsed = time.perf_counter() - s print(f"scan executed in {elapsed:0.2f} seconds.") print(result) diff --git a/examples/client_aio.py b/examples/client_aio.py index b1b85cf..f3131c4 100644 --- a/examples/client_aio.py +++ b/examples/client_aio.py @@ -1,5 +1,4 @@ import argparse -from distutils.util import strtobool import time import asyncio @@ -14,7 +13,7 @@ async def main(args): tasks = set() for file_name in args.filename: - task = asyncio.create_task(amaas.grpc.aio.scan_file(handle, file_name, args.tags)) + task = asyncio.create_task(amaas.grpc.aio.scan_file(handle, file_name=file_name, pml=args.pml, tags=args.tags)) tasks.add(task) s = time.perf_counter() @@ -38,13 +37,15 @@ async def main(args): parser.add_argument('-a', '--addr', action='store', default='127.0.0.1:50051', required=False, help='gRPC server address and port (default 127.0.0.1:50051)') parser.add_argument('-r', '--region', action='store', - help='AMaaS service region; e.g. us-1 or dev') + help='AMaaS service region; e.g. us-east-1 or eu-central-1') parser.add_argument('--api_key', action='store', help='api key for authentication') - parser.add_argument('--tls', type=lambda x: bool(strtobool(x)), default=False, + parser.add_argument('--tls', action=argparse.BooleanOptionalAction, default=False, help='enable TLS gRPC ') parser.add_argument('--ca_cert', action='store', help='CA certificate') + parser.add_argument('--pml', action=argparse.BooleanOptionalAction, default=False, + help='enable predictive machine learning detection') parser.add_argument('-t', '--tags', action='store', nargs='+', help='list of tags') diff --git a/amaas/protos/scan.proto b/protos/scan.proto similarity index 81% rename from amaas/protos/scan.proto rename to protos/scan.proto index b553bc9..c052249 100644 --- a/amaas/protos/scan.proto +++ b/protos/scan.proto @@ -20,13 +20,15 @@ enum Stage { message C2S { Stage stage = 1; string file_name = 2; - int32 rs_size = 3; + uint64 rs_size = 3; int32 offset = 4; bytes chunk = 5; bool trendx = 6; string file_sha1 = 7; string file_sha256 = 8; repeated string tags = 9; + bool bulk = 10; + bool spn_feedback = 11; } enum Command { @@ -40,4 +42,7 @@ message S2C { int32 offset = 3; int32 length = 4; string result = 5; + repeated int32 bulk_offset = 6; + repeated int32 bulk_length = 7; + string session_id = 8; } diff --git a/setup.py b/setup.py index dd4864d..1b2de2e 100644 --- a/setup.py +++ b/setup.py @@ -1,38 +1,29 @@ -import re -from os.path import abspath, dirname, join from setuptools import setup, find_namespace_packages with open("README.md", "r") as fh: long_description = fh.read() - -base_dir = abspath(dirname(__file__)) -package_name = "amaas" - -with open(join(base_dir, package_name, "_version.py")) as f: - package_version = re.search( - r"__version__\s+=\s+[\"\']([^\"\']+)[\"\']", f.read() - ).group(1) - -setup(install_requires=['grpcio==1.51.3', 'protobuf==4.22.0'], - name="visionone-filesecurity", - version=package_version, - author="Trend Micro VisionOne File Security Team", - description="Trend Micro VisionOne File Security SDK for python", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/trendmicro/tm-v1-fs-python-sdk", - packages=find_namespace_packages(exclude=['tests*', 'examples']), - package_data={'amaas': ['grpc/protos/*']}, - include_package_data=True, - classifiers=[ - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - "License :: OSI Approved :: MIT License", -], - python_requires='>=3.7', +with open("VERSION", "r") as fh: + package_version = fh.read().strip() +setup( + name="visionone-filesecurity", + version=package_version, + author="Trend Micro VisionOne File Security Team", + description="Trend Micro VisionOne File Security SDK for python", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/trendmicro/tm-v1-fs-python-sdk", + packages=find_namespace_packages(exclude=["tests*", "examples"]), + package_data={"amaas": ["grpc/protos/*"]}, + include_package_data=True, + classifiers=[ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "License :: OSI Approved :: MIT License", + ], + python_requires=">=3.7", ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..4eef093 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +# The inclusion of the tests module is not meant to offer best practices for +# testing in general. diff --git a/tests/fake_server_cert.pem b/tests/fake_server_cert.pem new file mode 100644 index 0000000..4f7994e --- /dev/null +++ b/tests/fake_server_cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDfjCCAmagAwIBAgIEMslTxTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJV +UzEQMA4GA1UEAwwHbm9ubmFtZTELMAkGA1UECAwCQ0ExDzANBgNVBAcMBkFCQ0VG +RzEZMBcGA1UECgwQRGVmYXVsdCBDb21wYWFueTAeFw0yMzA4MTYwMTExNTNaFw0z +MzA4MTMwMTExNTNaMFgxCzAJBgNVBAYTAlVTMRAwDgYDVQQDDAdub25uYW1lMQsw +CQYDVQQIDAJDQTEPMA0GA1UEBwwGQUJDRUZHMRkwFwYDVQQKDBBEZWZhdWx0IENv +bXBhYW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxjbl/9tHoK+U +AWNF/bkys8YolpBf8JT9VpQ3cfJkLBhIo6HamBwNGtCduILt4pC4qFF5qJjUY414 +XXTQX2ZAp1Hyc1+t6MUNqIVcWpqcTlUMixstOKaJI21R0aBlq5tVeNuJQxKp6h6S ++cRD2R3x32vCZjAYwBAgvSrwk6Cdk8f6SGsEldQGuvTxmcow8HK9gFfB29gwZwvz +3a+pIFeBUh69YAQS0NpXA/F742Tk1JEQgZkLTvHsA5u4YzofYK4limSshzT2YyNZ +yUMZ2CM5Y4tEXhkt/oOj9gRZw1vzzWYL2Rmbmv5sGQZWqsq/JmX+eFZmesxeUuAw +ZNP+sB68VQIDAQABo1AwTjAdBgNVHQ4EFgQU/uX8KHU6TxnVZQhv1jIK4NPWLgQw +HwYDVR0jBBgwFoAU/uX8KHU6TxnVZQhv1jIK4NPWLgQwDAYDVR0TBAUwAwEB/zAN +BgkqhkiG9w0BAQsFAAOCAQEAkPPn8SuOcbNujtxYIPtUfpVx87twFA4RIPXepunk +NtxAouP/AqVaLopk56nVjNv5/OVku8IOA9aLe2O4Gp8cEfCBclTyKVqpFHKdZY3O +EauSiRNsioAFJJ0k9f3H0HxzKcMYaV8M2LmIY1dLnSWuxawp3ACxgYyhUUIHIF1r +aYN2daPfi9Hd+Qff2AW9vMebZGjO3ex1HNMaLJMagvN4swbe0JSc831RWM+aFEYK ++pVGCd9+LDDux3oXwmsFMT6OJil5mKo47F5YHWIuvV10eyLC0WHYi+jqnsWFXf1J +QcfvddE7JwZnWRbiV35w/3MTrnclxCF2AH7Yv27YTHat6A== +-----END CERTIFICATE----- diff --git a/tests/mock_server.py b/tests/mock_server.py new file mode 100644 index 0000000..0a604ae --- /dev/null +++ b/tests/mock_server.py @@ -0,0 +1,86 @@ +import json +import random +import grpc + +from amaas.grpc.protos.scan_pb2_grpc import ScanServicer +from amaas.grpc.protos.scan_pb2 import STAGE_INIT, STAGE_FINI, STAGE_RUN +from amaas.grpc.protos.scan_pb2 import CMD_QUIT, CMD_RETR +from amaas.grpc.protos.scan_pb2 import S2C + + +SDK_SCHEMA_VERSION = "1.0.0" +MAX_RUN = 5 +FINAL_MSG = S2C(stage=STAGE_FINI, cmd=CMD_QUIT) + + +class MockScanServicer(ScanServicer): + IDENTIFIER_VIRUS = "virus" + IDENTIFIER_UNKNOWN_CMD = "unknown_cmd" + IDENTIFIER_MISMATCHED = "mismatched" + IDENTIFIER_GRPC_ERROR = "grpc_error" + IDENTIFIER_EXCEED_RATE = "exceed_rate" + UNKNOWN_CMD = 999 + + def __init__(self): + self.fsize = 0 + self.identifier = "" + + def getS2CMsg(self): + start = random.randint(0, self.fsize - 1) + end = random.randint(start, self.fsize) + s2cmsg = S2C(stage=STAGE_RUN, cmd=CMD_RETR, bulk_offset=[start], bulk_length=[end - start]) + return s2cmsg + + def getUnknwonCmd(self): + start = random.randint(0, self.fsize - 1) + end = random.randint(start, self.fsize) + s2cmsg = S2C( + stage=STAGE_RUN, + cmd=MockScanServicer.UNKNOWN_CMD, + offset=start, + length=end - start, + ) + return s2cmsg + + def getMismatchedCmdStage(self): + start = random.randint(0, self.fsize - 1) + end = random.randint(start, self.fsize) + s2cmsg = S2C(stage=STAGE_RUN, cmd=CMD_QUIT, offset=start, length=end - start) + return s2cmsg + + def Run(self, request_iterator, context): + count = 0 + for req in request_iterator: + if req.stage == STAGE_INIT: + self.fsize = req.rs_size + self.identifier = req.file_name + msg = self.getS2CMsg() + elif req.stage == STAGE_RUN: + if self.identifier == MockScanServicer.IDENTIFIER_UNKNOWN_CMD: + msg = self.getUnknwonCmd() + elif self.identifier == MockScanServicer.IDENTIFIER_MISMATCHED: + msg = self.getMismatchedCmdStage() + elif self.identifier == MockScanServicer.IDENTIFIER_GRPC_ERROR: + context.set_details("Ouch!") + context.set_code(grpc.StatusCode.INTERNAL) + msg = "" + elif self.identifier == MockScanServicer.IDENTIFIER_EXCEED_RATE: + context.set_details("Http Error Code: 429") + context.set_code(grpc.StatusCode.INTERNAL) + msg = "" + elif count >= MAX_RUN: + msg = FINAL_MSG + result = { + "scannerVersion": "1.0.0-1", + "schemaVersion": SDK_SCHEMA_VERSION, + "scanResult": 0, + "foundMalwares": [], + } + if self.identifier == MockScanServicer.IDENTIFIER_VIRUS: + result["scanResult"] = 1 + result["foundMalwares"] = ["virus1", "virus2"] + msg.result = json.dumps(result) + else: + msg = self.getS2CMsg() + count += 1 + yield msg diff --git a/tests/test_aio_client_sdk.py b/tests/test_aio_client_sdk.py new file mode 100644 index 0000000..4cb8916 --- /dev/null +++ b/tests/test_aio_client_sdk.py @@ -0,0 +1,170 @@ +import grpc +import json +import os +import pytest +import random +import tempfile +from concurrent import futures +from unittest.mock import patch + +import amaas.grpc.aio +from .mock_server import MockScanServicer +from amaas.grpc.exception import AMaasErrorCode +from amaas.grpc.exception import AMaasException + + +NUM_DATA_LOOP = 128 +_, TEST_DATA_FILE_NAME = tempfile.mkstemp() +SERVER_PORT = random.randint(49152, 65535) + + +# +# Test init_by_region method +# +@patch("amaas.grpc.aio._init_by_region_util") +def test_init_by_region(utilmock): + amaas.grpc.aio.init_by_region("us-east-1", "dummy_key") + + # ensure True is passed to is_aio_channel parameter + utilmock.assert_called_with("us-east-1", "dummy_key", True, None, True) + + +# +# Test init method +# +@patch("amaas.grpc.aio._init_util") +def test_init(utilmock): + amaas.grpc.aio.init("us-east-1", "dummy_key") + + # ensure False is passed to is_aio_channel parameter + utilmock.assert_called_with("us-east-1", "dummy_key", False, None, True) + + +# +# Create a mock gRPC server with MockScanServicer +# +@pytest.fixture(scope="module", autouse=True) +def run_setup(): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + + amaas.grpc.protos.scan_pb2_grpc.add_ScanServicer_to_server( + MockScanServicer(), server + ) + server.add_insecure_port(f"[::]:{SERVER_PORT}") + server.start() + yield server + server.stop(None) + + +# +# This routine is to create a data file for testing. +# The binary file contains of NUM_DATA_LOOP of consecutive bytes from 0x00 to 0xff. +# This is to make checking the correctness of the content of the retrieval easier. +# The grpc client receives a sequence of instructions from grpc server to retrieve chunks of data +# from a data file during scanning. +# One of the important checking is to ensure the data retrived is what is actually rqeuested by the server. +# +@pytest.fixture(scope="session", autouse=True) +def create_data_file(): + with open(TEST_DATA_FILE_NAME, "wb") as binary_file: + for j in range(NUM_DATA_LOOP): + for i in range(256): + # Convert the number to a single byte and write to the file + byte_value = i.to_bytes(1, byteorder="big") + binary_file.write(byte_value) + yield + os.remove(TEST_DATA_FILE_NAME) + + +# +# Testing the SDK scan_file method failed with MSG_ID_ERR_FILE_NOT_FOUND exception. +# +@pytest.mark.asyncio +async def test_scan_file_not_found(): + handle = grpc.aio.insecure_channel(f"localhost:{SERVER_PORT}") + NOT_EXIST_FILE = "012345.txt" + with pytest.raises(AMaasException) as exc_info: + await amaas.grpc.scan_file(handle, NOT_EXIST_FILE) + assert exc_info.value.args[0] == AMaasErrorCode.MSG_ID_ERR_FILE_NOT_FOUND + assert exc_info.value.args[1] == NOT_EXIST_FILE + + +# +# Testing the SDK scan_file method failed with MSG_ID_ERR_FILE_NO_PERMISSION exception +# +@pytest.mark.asyncio +async def test_scan_file_no_permission(): + handle = grpc.insecure_channel(f"localhost:{SERVER_PORT}") + NOT_PERMISSION_FILE = tempfile.NamedTemporaryFile().name + open(NOT_PERMISSION_FILE, "x") + os.chmod(NOT_PERMISSION_FILE, 0x000) + with pytest.raises(AMaasException) as exc_info: + await amaas.grpc.scan_file(handle, NOT_PERMISSION_FILE) + assert exc_info.value.args[0] == AMaasErrorCode.MSG_ID_ERR_FILE_NO_PERMISSION + assert exc_info.value.args[1] == NOT_PERMISSION_FILE + os.remove(NOT_PERMISSION_FILE) + + +# +# Testing the SDK scan_buffer method succeeded without viruses. +# +@pytest.mark.asyncio +async def test_scan_buffer_success(): + handle = grpc.aio.insecure_channel(f"localhost:{SERVER_PORT}") + with open(TEST_DATA_FILE_NAME, mode="rb") as bfile: + buffer = bfile.read() + response = await amaas.grpc.aio.scan_buffer(handle, buffer, TEST_DATA_FILE_NAME) + assert json.loads(response)["scanResult"] == 0 + + +# +# Testing the SDK scan_buffer method succeeded with viruses. +# +@pytest.mark.asyncio +async def test_scan_buffer_virus(): + handle = grpc.aio.insecure_channel(f"localhost:{SERVER_PORT}") + with open(TEST_DATA_FILE_NAME, mode="rb") as bfile: + buffer = bfile.read() + response = await amaas.grpc.aio.scan_buffer(handle, buffer, "virus") + jobj = json.loads(response) + assert jobj["scanResult"] == 1 + assert jobj["foundMalwares"] == ["virus1", "virus2"] + + +# +# Test different exceptions throwed by scan_buffer API. +# +@pytest.mark.asyncio +@pytest.mark.parametrize( + "error_type, expected_exception", + [ + ( + MockScanServicer.IDENTIFIER_EXCEED_RATE, + [AMaasErrorCode.MSG_ID_ERR_RATE_LIMIT_EXCEEDED], + ), + ( + MockScanServicer.IDENTIFIER_MISMATCHED, + [ + AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE, + amaas.grpc.scan_pb2.CMD_QUIT, + amaas.grpc.scan_pb2.STAGE_RUN, + ], + ), + ( + MockScanServicer.IDENTIFIER_UNKNOWN_CMD, + [AMaasErrorCode.MSG_ID_ERR_UNKNOWN_CMD, MockScanServicer.UNKNOWN_CMD], + ), + ( + MockScanServicer.IDENTIFIER_GRPC_ERROR, + [AMaasErrorCode.MSG_ID_GRPC_ERROR, grpc.StatusCode.INTERNAL.value[0]], + ), + ], +) +async def test_scan_buffer_exceptions(error_type, expected_exception): + handle = grpc.aio.insecure_channel(f"localhost:{SERVER_PORT}") + with open(TEST_DATA_FILE_NAME, mode="rb") as bfile: + buffer = bfile.read() + with pytest.raises(AMaasException) as exc_info: + await amaas.grpc.aio.scan_buffer(handle, buffer, error_type) + for cnt in range(len(expected_exception)): + assert exc_info.value.args[cnt] == expected_exception[cnt] diff --git a/tests/test_client_sdk.py b/tests/test_client_sdk.py new file mode 100644 index 0000000..bc23a1e --- /dev/null +++ b/tests/test_client_sdk.py @@ -0,0 +1,333 @@ +import grpc +import json +import os +import pytest +import random +import tempfile +import uuid +from concurrent import futures +from unittest.mock import patch + +import amaas.grpc +from .mock_server import MockScanServicer +from amaas.grpc.exception import AMaasErrorCode +from amaas.grpc.exception import AMaasException + + +NUM_DATA_LOOP = 128 +_, TEST_DATA_FILE_NAME = tempfile.mkstemp() +SERVER_PORT = random.randint(49152, 65535) + + +@patch("amaas.grpc._init_by_region_util") +def test_init_by_region(utilmock): + amaas.grpc.init_by_region("ap-southeast-2", "dummy_key") + + # ensure False is passed to is_aio_channel parameter + utilmock.assert_called_with("ap-southeast-2", "dummy_key", True, None, False) + + +@patch("amaas.grpc._init_util") +def test_init(utilmock): + amaas.grpc.init("ap-southeast-2", "dummy_key") + + # ensure False is passed to is_aio_channel parameter + utilmock.assert_called_with("ap-southeast-2", "dummy_key", False, None, False) + + +# +# Create a mock gRPC server with MockScanServicer +# +@pytest.fixture(scope="module", autouse=True) +def run_setup(): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + + amaas.grpc.protos.scan_pb2_grpc.add_ScanServicer_to_server( + MockScanServicer(), server + ) + server.add_insecure_port(f"[::]:{SERVER_PORT}") + server.start() + yield server + server.stop(None) + + +# +# This routine is to create a data file for testing. +# The binary file contains of NUM_DATA_LOOP of consecutive bytes from 0x00 to 0xff. +# This is to make checking the correctness of the content of the retrieval easier. +# The grpc client receives a sequence of instructions from grpc server to retrieve chunks of data +# from a data file during scanning. +# One of the important checking is to ensure the data retrived is what is actually rqeuested by the server. +# +@pytest.fixture(scope="session", autouse=True) +def create_data_file(): + with open(TEST_DATA_FILE_NAME, "wb") as binary_file: + for j in range(NUM_DATA_LOOP): + for i in range(256): + # Convert the number to a single byte and write to the file + byte_value = i.to_bytes(1, byteorder="big") + binary_file.write(byte_value) + yield + os.remove(TEST_DATA_FILE_NAME) + + +# +# The method is to verify the chunk matching the data chunk at the given location in the file. +# @param buf buf of the data chunk +# @param offset location in the data file +# @return True if matched False otherwise. +# +def verify_buf_with_data(buf, offset): + blen = len(buf) + cnt = 0 + for posn in range(offset, blen): + val = posn % 256 + # byte_value = val.to_bytes(1, byteorder='big') + assert buf[cnt] == val + cnt += 1 + + +# +# Testing authentication failure. +# +def test_scan_file_failed_authentication(): + dir_path = os.path.dirname(os.path.realpath(__file__)) + handle = amaas.grpc.init_by_region("us-east-1", "api-key", True, None) + with pytest.raises(AMaasException) as exc_info: + amaas.grpc.scan_file(handle, dir_path + "/fake_server_cert.pem") + assert exc_info.value.args[0] == AMaasErrorCode.MSG_ID_ERR_KEY_AUTH_FAILED + + +# +# Testing the generate_message method correctly processing the initial message from grpc client +# +def test_generate_message_init(): + pipeline = amaas.grpc._Pipeline() + stats = {} + with open(TEST_DATA_FILE_NAME, "rb") as f: + size = os.stat(TEST_DATA_FILE_NAME).st_size + server_resp = amaas.grpc.scan_pb2.C2S( + stage=amaas.grpc.scan_pb2.STAGE_INIT, + file_name=TEST_DATA_FILE_NAME, + rs_size=size, + offset=0, + chunk=None, + tags=None, + ) + + pipeline.set_message(server_resp) + c2s_msg = next(amaas.grpc._generate_messages(pipeline, f, True, stats)) + assert c2s_msg.stage == amaas.grpc.scan_pb2.STAGE_INIT + assert c2s_msg.offset == 0 + assert c2s_msg.rs_size == size + assert c2s_msg.file_name == TEST_DATA_FILE_NAME + + +# +# Testing the generate_message method correctly processing a number of back and forth +# data retrieval message from grpc server. +# +def test_generate_message_data(): + pipeline = amaas.grpc._Pipeline() + stats = {} + f = open(TEST_DATA_FILE_NAME, "rb") + size = os.stat(TEST_DATA_FILE_NAME).st_size + # + # lets loop 5 times to check + # + for _ in range(5): + start = random.randint(0, size - 1) + end = random.randint(start, size) + server_resp = amaas.grpc.scan_pb2.S2C( + stage=amaas.grpc.scan_pb2.STAGE_RUN, + cmd=amaas.grpc.scan_pb2.CMD_RETR, + bulk_offset=[start], + bulk_length=[end - start], + ) + + pipeline.set_message(server_resp) + c2s_msg = next(amaas.grpc._generate_messages(pipeline, f, True, stats)) + assert c2s_msg.stage == amaas.grpc.scan_pb2.STAGE_RUN + assert c2s_msg.offset == start + verify_buf_with_data(c2s_msg.chunk, start) + + +# +# Testing the generate_message method correctly raise exception when recieved +# wrong command in STAGE RUN from server. +# +def test_generate_message_data_wrong_cmd_exception(): + pipeline = amaas.grpc._Pipeline() + stats = {} + with open(TEST_DATA_FILE_NAME, "rb") as f: + size = os.stat(TEST_DATA_FILE_NAME).st_size + start = random.randint(0, size - 1) + end = random.randint(start, size) + server_resp = amaas.grpc.scan_pb2.S2C( + stage=amaas.grpc.scan_pb2.STAGE_RUN, + cmd=amaas.grpc.scan_pb2.CMD_QUIT, + bulk_offset=[start], + bulk_length=[end - start], + ) + pipeline.set_message(server_resp) + with pytest.raises(AMaasException) as exc_info: + next(amaas.grpc._generate_messages(pipeline, f, True, stats)) + assert ( + exc_info.value.args[0] == AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE + ) + assert exc_info.value.args[1] == amaas.grpc.scan_pb2.CMD_QUIT + assert exc_info.value.args[2] == amaas.grpc.scan_pb2.STAGE_RUN + + +# +# Testing the generate_message method correctly processing the final message from grpc server. +# +def test_generate_message_final(): + pipeline = amaas.grpc._Pipeline() + stats = {} + f = open(TEST_DATA_FILE_NAME, "rb") + server_resp = amaas.grpc.scan_pb2.S2C( + stage=amaas.grpc.scan_pb2.STAGE_FINI, cmd=amaas.grpc.scan_pb2.CMD_QUIT + ) + + pipeline.set_message(server_resp) + client_msg = next(amaas.grpc._generate_messages(pipeline, f, True, stats), None) + assert client_msg is None + + +# +# Testing the generate_message method correctly raise exception when recieved wrong command in STAGE_FINI from server. +# +def test_generate_message_final_wrong_cmd_exception(): + pipeline = amaas.grpc._Pipeline() + stats = {} + with open(TEST_DATA_FILE_NAME, "rb") as f: + size = os.stat(TEST_DATA_FILE_NAME).st_size + start = random.randint(0, size - 1) + end = random.randint(start, size) + server_resp = amaas.grpc.scan_pb2.S2C( + stage=amaas.grpc.scan_pb2.STAGE_FINI, + cmd=amaas.grpc.scan_pb2.CMD_RETR, + offset=start, + length=end - start, + ) + pipeline.set_message(server_resp) + with pytest.raises(AMaasException) as exc_info: + next(amaas.grpc._generate_messages(pipeline, f, True, stats)) + assert ( + exc_info.value.args[0] == AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE + ) + assert exc_info.value.args[1] == amaas.grpc.scan_pb2.CMD_RETR + assert exc_info.value.args[2] == amaas.grpc.scan_pb2.STAGE_FINI + + +# +# Testing the generate_message method correctly processing the unknown stage message from grpc server. +# +def test_generate_message_unknwon_stage(): + pipeline = amaas.grpc._Pipeline() + stats = {} + f = open(TEST_DATA_FILE_NAME, "rb") + UNKNOWN_STAGE = 9999 + server_resp = amaas.grpc.scan_pb2.S2C( + stage=UNKNOWN_STAGE, cmd=amaas.grpc.scan_pb2.CMD_QUIT + ) + + pipeline.set_message(server_resp) + with pytest.raises(AMaasException) as exc_info: + next(amaas.grpc._generate_messages(pipeline, f, True, stats), None) + assert exc_info.value.args[0] == AMaasErrorCode.MSG_ID_ERR_UNKNOWN_STAGE + assert exc_info.value.args[1] == UNKNOWN_STAGE + + +# +# Testing the SDK scan_file method sucessfully scans a file with no virus. +# +def test_scan_file_success(): + dir_path = os.path.dirname(os.path.realpath(__file__)) + handle = grpc.insecure_channel(f"localhost:{SERVER_PORT}") + response = amaas.grpc.scan_file(handle, dir_path + "/fake_server_cert.pem") + assert json.loads(response)["scanResult"] == 0 + + +# +# Testing the SDK scan_file method sucessfully failed with MSG_ID_ERR_FILE_NOT_FOUND exception. +# +def test_scan_file_not_found(): + handle = grpc.insecure_channel(f"localhost:{SERVER_PORT}") + NOT_EXIST_FILE = f"{str(uuid.uuid4())}.txt" + with pytest.raises(AMaasException) as exc_info: + amaas.grpc.scan_file(handle, NOT_EXIST_FILE) + assert exc_info.value.args[0] == AMaasErrorCode.MSG_ID_ERR_FILE_NOT_FOUND + assert exc_info.value.args[1] == NOT_EXIST_FILE + + +# +# Testing the SDK scan_file method failed with MSG_ID_ERR_FILE_NO_PERMISSION exception +# +def test_scan_file_no_permission(): + handle = grpc.insecure_channel(f"localhost:{SERVER_PORT}") + NOT_PERMISSION_FILE = tempfile.NamedTemporaryFile().name + open(NOT_PERMISSION_FILE, "x") + os.chmod(NOT_PERMISSION_FILE, 0x000) + with pytest.raises(AMaasException) as exc_info: + amaas.grpc.scan_file(handle, NOT_PERMISSION_FILE) + assert exc_info.value.args[0] == AMaasErrorCode.MSG_ID_ERR_FILE_NO_PERMISSION + assert exc_info.value.args[1] == NOT_PERMISSION_FILE + os.remove(NOT_PERMISSION_FILE) + + +# +# Testing the SDK scan_buffer method sucessfully scans a file with no virus. +# +def test_scan_buffer_success(): + handle = grpc.insecure_channel(f"localhost:{SERVER_PORT}") + with open(TEST_DATA_FILE_NAME, mode="rb") as bfile: + buffer = bfile.read() + response = amaas.grpc.scan_buffer(handle, buffer, TEST_DATA_FILE_NAME) + assert json.loads(response)["scanResult"] == 0 + + +# +# Testing the SDK scan_buffer method sucessfully scans a file with virus. +# +def test_scan_buffer_virus(): + handle = grpc.insecure_channel(f"localhost:{SERVER_PORT}") + with open(TEST_DATA_FILE_NAME, mode="rb") as bfile: + buffer = bfile.read() + response = amaas.grpc.scan_buffer( + handle, buffer, MockScanServicer.IDENTIFIER_VIRUS + ) + jobj = json.loads(response) + assert jobj["scanResult"] == 1 + assert jobj["foundMalwares"] == ["virus1", "virus2"] + + +# +# Test different exceptions throwed by scan_buffer API. +# +@pytest.mark.parametrize( + "error_type, expected_exception", + [ + ( + MockScanServicer.IDENTIFIER_EXCEED_RATE, + [AMaasErrorCode.MSG_ID_ERR_RATE_LIMIT_EXCEEDED], + ), + ( + MockScanServicer.IDENTIFIER_UNKNOWN_CMD, + [AMaasErrorCode.MSG_ID_ERR_UNKNOWN_CMD, MockScanServicer.UNKNOWN_CMD], + ), + ( + MockScanServicer.IDENTIFIER_GRPC_ERROR, + [AMaasErrorCode.MSG_ID_GRPC_ERROR, grpc.StatusCode.INTERNAL.value[0]], + ), + ], +) +def test_scan_buffer_exceptions(error_type, expected_exception): + handle = grpc.insecure_channel(f"localhost:{SERVER_PORT}") + with open(TEST_DATA_FILE_NAME, mode="rb") as bfile: + buffer = bfile.read() + with pytest.raises(AMaasException) as exc_info: + amaas.grpc.scan_buffer(handle, buffer, error_type) + for cnt in range(len(expected_exception)): + assert exc_info.value.args[cnt] == expected_exception[cnt] diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 0000000..6c44312 --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,100 @@ +import os +import grpc +from unittest.mock import patch + +import amaas.grpc.util + + +# +# Test _init_by_region_util is invoked with correct arguments. +# +@patch("amaas.grpc.util._init_util") +def test_init_by_region_util(utilmock): + amaas.grpc.util._init_by_region_util("ap-southeast-2", "dummy_key") + + utilmock.assert_called_with( + "antimalware.au-1.cloudone.trendmicro.com:443", "dummy_key", True, None, False + ) + + +# +# Test insecure channel is created. +# +def test_insecure_channel(): + channel = amaas.grpc.util._init_by_region_util( + "us-east-1", None, False, None, False + ) + assert type(channel) == grpc._channel.Channel + + +# +# Test insecure aio channel is created. +# +def test_aio_insecure_channel(): + channel = amaas.grpc.util._init_by_region_util( + "us-east-1", None, is_aio_channel=True + ) + assert type(channel) == grpc.aio._channel.Channel + + +# +# Test secure channel is created. +# +def test_secure_channel(): + channel = amaas.grpc.util._init_by_region_util("us-east-1", None, True, None, False) + assert type(channel) == grpc._channel.Channel + + +# +# Test secure aio channel is created. +# +def test_aio_secure_channel(): + channel = amaas.grpc.util._init_by_region_util( + "us-east-1", None, True, None, is_aio_channel=True + ) + assert type(channel) == grpc.aio._channel.Channel + + +# +# Test default SSL only channel. +# +@patch("grpc.secure_channel") +def test_def_ssl_only_channel(channel_mock): + amaas.grpc.util._init_by_region_util("us-east-1", None, True, None, False) + + args = channel_mock.call_args.args + assert type(args[1]._credentials) == grpc._cython.cygrpc.SSLChannelCredentials + + +# +# Test SSL only channel. +# +# @patch("builtins.open", new_callable=mock_open, read_data=SERVER_CERT) +@patch("grpc.secure_channel") +def test_ssl_only_channel(channel_mock): + dir_path = os.path.dirname(os.path.realpath(__file__)) + amaas.grpc.util._init_by_region_util( + "us-east-1", None, True, dir_path + "/fake_server_cert.pem", False + ) + + args = channel_mock.call_args.args + assert type(args[1]._credentials) == grpc._cython.cygrpc.SSLChannelCredentials + + +# +# Test api key composite channel credential. +# +@patch("amaas.grpc.util._GrpcAuth") +@patch("grpc.secure_channel") +def test_composite_channel_with_apikey(channel_mock, auth_mock): + dir_path = os.path.dirname(os.path.realpath(__file__)) + amaas.grpc.util._init_by_region_util( + "us-east-1", "abcabc12345678", True, dir_path + "/fake_server_cert.pem", False + ) + + auth_mock.assert_called_with( + "ApiKey abcabc12345678", + ) + + args = channel_mock.call_args.args + assert type(args[1]._credentials) == grpc._cython.cygrpc.CompositeChannelCredentials diff --git a/tools/Makefile.dev b/tools/Makefile.dev deleted file mode 100644 index 5e88e4d..0000000 --- a/tools/Makefile.dev +++ /dev/null @@ -1,22 +0,0 @@ -all: build - -build: clean - pipenv sync --dev - pipenv run python -m grpc_tools.protoc -Iamaas/grpc/protos=./amaas/protos \ - --python_out=. \ - --pyi_out=. \ - --grpc_python_out=. \ - ./amaas/protos/scan.proto - pipenv run pipenv-setup sync - pipenv run python setup.py sdist bdist_wheel - -upload: - @echo "*** NOT IMPLEMENTED YET ***" - -test: - @echo "*** NOT IMPLEMENTED YET ***" - -clean: - @rm -rf dist build *.egg-info amaas/grpc/protos/ - -.PHONY: all build upload test clean diff --git a/tox.ini b/tox.ini index 2628485..79530bb 100644 --- a/tox.ini +++ b/tox.ini @@ -38,10 +38,10 @@ allowlist_externals= make commands = - make -f {toxinidir}/tools/Makefile clean + make -f {toxinidir}/Makefile clean flake8 . - check-manifest --ignore 'tox.ini,tests/**,tools/**,examples/**,docs/**,*.md,Pipfile*,**/scan.proto' - make -f {toxinidir}/tools/Makefile build + check-manifest --ignore 'tox.ini,tests/**,examples/**,docs/**,*.md,Pipfile*,**/scan.proto' + make -f {toxinidir}/Makefile build # pytest tests {posargs} python -m twine check dist/*