├── VERSION ├── amaas ├── __init__.py └── grpc │ ├── exception │ └── __init__.py │ ├── util.py │ ├── aio │ └── __init__.py │ └── __init__.py ├── MANIFEST.in ├── NOTICE ├── setup.cfg ├── tests ├── __init__.py ├── fake_server_cert.pem ├── test_util.py ├── mock_server.py ├── test_aio_client_sdk.py └── test_client_sdk.py ├── examples ├── run-test.sh ├── aws_quarantine.py ├── client.py ├── client_aio.py └── README.md ├── .gitignore ├── Pipfile ├── Makefile ├── .github └── workflows │ ├── unit-test.yml │ └── publish-to-pypi.yml ├── CONTRIBUTING.md ├── protos └── scan.proto ├── LICENSE ├── setup.py ├── CHANGELOG.md ├── tox.ini ├── README.md └── Pipfile.lock /VERSION: -------------------------------------------------------------------------------- 1 | 1.4.2 2 | -------------------------------------------------------------------------------- /amaas/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include VERSION 3 | include Makefile 4 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Cloud One VSAPI Python Client 2 | Copyright 2023 Trend Micro Inc. 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 0 3 | 4 | [flake8] 5 | ignore = E123,E128,E203,E501,W292,W503,W504 6 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # The inclusion of the tests module is not meant to offer best practices for 2 | # testing in general. 3 | -------------------------------------------------------------------------------- /examples/run-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | for i in {1..100} 3 | do 4 | python client.py -f client.py -a $TM_AM_SERVER_ADDR --api_key $TM_AM_AUTH_KEY 5 | done 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # general things to ignore 2 | build/ 3 | dist/ 4 | *.egg-info/ 5 | *.egg 6 | *.py[cod] 7 | __pycache__/ 8 | *.so 9 | *~ 10 | 11 | # due to using tox and pytest 12 | .tox 13 | .cache 14 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | protobuf = "~=5.29.4" 8 | grpcio = "~=1.71.0" 9 | 10 | [dev-packages] 11 | grpcio-tools = "~=1.71.0" 12 | setuptools = "~=65.3" 13 | pytest = "~=7.1" 14 | pipenv-setup = "~=3.2" 15 | vistir = "==0.6.1" 16 | pytest-mock = "~=3.11" 17 | pytest-asyncio = "~=0.21" 18 | twine = "~=5.1" 19 | 20 | [requires] 21 | python_version = "3" 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PIPY_URL ?= https://upload.pypi.org/legacy/ 2 | TOKEN ?= 3 | VERSION := $(shell cat VERSION | tr -d '\n') 4 | 5 | proto: 6 | pipenv sync --dev 7 | pipenv run python -m grpc_tools.protoc -Iamaas/grpc/protos=./protos \ 8 | --python_out=. \ 9 | --pyi_out=. \ 10 | --grpc_python_out=. \ 11 | ./protos/scan.proto 12 | 13 | build: proto 14 | pipenv run pipenv-setup sync 15 | pipenv run python setup.py sdist bdist_wheel 16 | 17 | test: proto 18 | pipenv run pytest tests 19 | 20 | upload: 21 | pipenv run twine upload --repository-url $(PIPY_URL) -u __token__ -p $(TOKEN) ./dist/*.whl --skip-existing 22 | 23 | clean: 24 | @rm -rf dist build *.egg-info amaas/grpc/protos/ 25 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: unittest 2 | on: 3 | pull_request: 4 | branches: 5 | - "main" 6 | 7 | jobs: 8 | unit-test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 12 | - uses: actions/checkout@v3 13 | 14 | # Set up python 3.9 15 | - name: Prepare python env 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: "3.9" 19 | 20 | # Install pipenv 21 | - name: Install pipenv 22 | run: pip install pipenv 23 | 24 | # Pack and publish Python SDK 25 | - name: Exec py client unit test 26 | run: make test 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Code 2 | 3 | A good pull request: 4 | 5 | - Is clear. 6 | - Works across all supported versions of Python. 7 | - Follows the existing style of the code base (see Codestyle section). 8 | - Has comments included as needed. 9 | - Must be appropriately licensed (MIT). 10 | 11 | ## Reporting An Issue/Feature 12 | 13 | If you have a bugfix or new feature that you would like to contribute to SDK, please find or open an issue about it first. 14 | Talk about what you would like to do. 15 | It may be that somebody is already working on it, or that there are particular issues that you should know about before implementing the change. 16 | 17 | ## Contributing Code Changes 18 | 19 | 1. Run the linter and test suite to ensure your changes do not break existing code: 20 | 21 | ```bash 22 | # Install tox for task management 23 | $ python -m pip install tox 24 | 25 | # lint your changes and run the test suite 26 | $ tox 27 | ``` 28 | -------------------------------------------------------------------------------- /protos/scan.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | // https://cloud.google.com/apis/design/versioning 4 | package amaas.scan.v1; 5 | 6 | option go_package = "amaas/scanner/base"; 7 | option java_package = "com.trend.cloudone.amaas.scan"; 8 | 9 | // Interface exported by the server. 10 | service Scan { 11 | rpc Run(stream C2S) returns (stream S2C) {} 12 | } 13 | 14 | enum Stage { 15 | STAGE_INIT = 0; 16 | STAGE_RUN = 1; 17 | STAGE_FINI = 2; 18 | } 19 | 20 | message C2S { 21 | Stage stage = 1; 22 | string file_name = 2; 23 | uint64 rs_size = 3; 24 | int32 offset = 4; 25 | bytes chunk = 5; 26 | bool trendx = 6; 27 | string file_sha1 = 7; 28 | string file_sha256 = 8; 29 | repeated string tags = 9; 30 | bool bulk = 10; 31 | bool spn_feedback = 11; 32 | bool verbose = 12; 33 | } 34 | 35 | enum Command { 36 | CMD_RETR = 0; 37 | CMD_QUIT = 1; 38 | } 39 | 40 | message S2C { 41 | Stage stage = 1; 42 | Command cmd = 2; 43 | int32 offset = 3; 44 | int32 length = 4; 45 | string result = 5; 46 | repeated int32 bulk_offset = 6; 47 | repeated int32 bulk_length = 7; 48 | string session_id = 8; 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Trend Micro Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_namespace_packages 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | with open("VERSION", "r") as fh: 6 | package_version = fh.read().strip() 7 | setup( 8 | name="visionone-filesecurity", 9 | version=package_version, 10 | author="Trend Micro VisionOne File Security Team", 11 | description="Trend Micro VisionOne File Security SDK for python", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/trendmicro/tm-v1-fs-python-sdk", 15 | packages=find_namespace_packages(exclude=["tests*", "examples"]), 16 | package_data={"amaas": ["grpc/protos/*"]}, 17 | include_package_data=True, 18 | classifiers=[ 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | "Programming Language :: Python :: 3.12", 25 | "Programming Language :: Python :: 3.13", 26 | "License :: OSI Approved :: MIT License", 27 | ], 28 | python_requires=">=3.9, <=3.13", 29 | ) 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.4.2 - 2025-05-13 4 | 5 | * Update the minimum supported version to Python 3.9 6 | * Update the maximum supported version to Python 3.13 7 | 8 | ## 1.4.1 - 2025-03-03 9 | 10 | * Support new region me-central-1 11 | 12 | ## 1.4.0 - 2024-08-21 13 | 14 | * Update README.md 15 | * Support digest calculation bypass 16 | 17 | ## 1.3.0 - 2024-08-20 18 | 19 | * Update README.md 20 | * Support CA cert import 21 | 22 | ## 1.2.0 - 2024-07-05 23 | 24 | * Support verbose scan result 25 | 26 | ## 1.1.1 - 2024-04-10 27 | 28 | * Update README.md 29 | * Extend the scan default timeout to 300 seconds 30 | 31 | ## 1.1.0 - 2024-04-03 32 | 33 | * Update protos 34 | * Enable PML (Predictive Machine Learning) detection and smart feedback 35 | * Enable bulk mode 36 | * Enable India region 37 | * Support for scanning large files (over 2GB) 38 | 39 | ## 1.0.5 - 2023-12-28 40 | 41 | * fix linting issues 42 | 43 | ## 1.0.4 - 2023-05-18 44 | 45 | * set default timeout_in_seconds to 180 seconds 46 | 47 | ## 1.0.3 - 2023-05-10 48 | 49 | * Change LICENSE 50 | 51 | ## 1.0.2 - 2023-05-10 52 | 53 | * Change README.md 54 | 55 | ## 1.0.1 - 2023-05-04 56 | 57 | * Add scan_buffer() function 58 | 59 | ## 1.0.0 - 2023-05-01 60 | 61 | * Initial release 62 | -------------------------------------------------------------------------------- /tests/fake_server_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDfjCCAmagAwIBAgIEMslTxTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJV 3 | UzEQMA4GA1UEAwwHbm9ubmFtZTELMAkGA1UECAwCQ0ExDzANBgNVBAcMBkFCQ0VG 4 | RzEZMBcGA1UECgwQRGVmYXVsdCBDb21wYWFueTAeFw0yMzA4MTYwMTExNTNaFw0z 5 | MzA4MTMwMTExNTNaMFgxCzAJBgNVBAYTAlVTMRAwDgYDVQQDDAdub25uYW1lMQsw 6 | CQYDVQQIDAJDQTEPMA0GA1UEBwwGQUJDRUZHMRkwFwYDVQQKDBBEZWZhdWx0IENv 7 | bXBhYW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxjbl/9tHoK+U 8 | AWNF/bkys8YolpBf8JT9VpQ3cfJkLBhIo6HamBwNGtCduILt4pC4qFF5qJjUY414 9 | XXTQX2ZAp1Hyc1+t6MUNqIVcWpqcTlUMixstOKaJI21R0aBlq5tVeNuJQxKp6h6S 10 | +cRD2R3x32vCZjAYwBAgvSrwk6Cdk8f6SGsEldQGuvTxmcow8HK9gFfB29gwZwvz 11 | 3a+pIFeBUh69YAQS0NpXA/F742Tk1JEQgZkLTvHsA5u4YzofYK4limSshzT2YyNZ 12 | yUMZ2CM5Y4tEXhkt/oOj9gRZw1vzzWYL2Rmbmv5sGQZWqsq/JmX+eFZmesxeUuAw 13 | ZNP+sB68VQIDAQABo1AwTjAdBgNVHQ4EFgQU/uX8KHU6TxnVZQhv1jIK4NPWLgQw 14 | HwYDVR0jBBgwFoAU/uX8KHU6TxnVZQhv1jIK4NPWLgQwDAYDVR0TBAUwAwEB/zAN 15 | BgkqhkiG9w0BAQsFAAOCAQEAkPPn8SuOcbNujtxYIPtUfpVx87twFA4RIPXepunk 16 | NtxAouP/AqVaLopk56nVjNv5/OVku8IOA9aLe2O4Gp8cEfCBclTyKVqpFHKdZY3O 17 | EauSiRNsioAFJJ0k9f3H0HxzKcMYaV8M2LmIY1dLnSWuxawp3ACxgYyhUUIHIF1r 18 | aYN2daPfi9Hd+Qff2AW9vMebZGjO3ex1HNMaLJMagvN4swbe0JSc831RWM+aFEYK 19 | +pVGCd9+LDDux3oXwmsFMT6OJil5mKo47F5YHWIuvV10eyLC0WHYi+jqnsWFXf1J 20 | QcfvddE7JwZnWRbiV35w/3MTrnclxCF2AH7Yv27YTHat6A== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /amaas/grpc/exception/__init__.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | # 5 | # AMAAS exceptions class. 6 | # 7 | class AMaasException(Exception): 8 | def __init__(self, error_code, *params): 9 | self.error_code = error_code 10 | self.params = params 11 | self.message = error_code.value % params 12 | 13 | def __str__(self): 14 | return f"{self.error_code.name}: {self.message}" 15 | 16 | 17 | # 18 | # Error codes for AMAAS exceptions 19 | # 20 | class AMaasErrorCode(Enum): 21 | MSG_ID_ERR_FILE_NOT_FOUND = "Failed to open file. No such file or directory %s." 22 | MSG_ID_ERR_FILE_NO_PERMISSION = "Failed to open file. Permission denied to open %s." 23 | MSG_ID_ERR_INVALID_REGION = "%s is not a supported region, region value should be one of %s" 24 | MSG_ID_ERR_MISSING_AUTH = "Must provide an API key to use the client." 25 | MSG_ID_GRPC_ERROR = "Received gRPC status code: %s, msg: %s." 26 | MSG_ID_ERR_KEY_AUTH_FAILED = "Invalid token or Api Key." 27 | MSG_ID_ERR_UNKNOWN_CMD = "Received unknown command from server: %d" 28 | MSG_ID_ERR_UNKNOWN_STAGE = "Received unknown stage from server: %d" 29 | MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE = "Received unexpected command %d and stage %d." 30 | MSG_ID_ERR_UNEXPECTED_ERROR = "Unexpected error encountered. %s" 31 | MSG_ID_ERR_RATE_LIMIT_EXCEEDED = "Raised by the SDK library to indicate http 429 too many request error." 32 | MSG_ID_ERR_INVALID_TAG = "Invalid tag format: %s." 33 | MSG_ID_ERR_TAG_NUMBER_EXCEED = "Too many tags: %d." 34 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # this file is *not* meant to cover or endorse the use of tox or pytest or 2 | # testing in general, 3 | # 4 | # It's meant to show the use of: 5 | # 6 | # - check-manifest 7 | # confirm items checked into vcs are in your sdist 8 | # - readme_renderer (when using a ReStructuredText README) 9 | # confirms your long_description will render correctly on PyPI. 10 | # 11 | # and also to help confirm pull requests to this project. 12 | 13 | [tox] 14 | envlist = py{39,310,311} 15 | 16 | # Define the minimal tox version required to run; 17 | # if the host tox is less than this the tool with create an environment and 18 | # provision it with a tox that satisfies it under provision_tox_env. 19 | # At least this version is needed for PEP 517/518 support. 20 | minversion = 3.3.0 21 | 22 | # Activate isolated build environment. tox will use a virtual environment 23 | # to build a source distribution from the source tree. For build tools and 24 | # arguments use the pyproject.toml file as specified in PEP-517 and PEP-518. 25 | isolated_build = true 26 | 27 | [testenv] 28 | deps = 29 | check-manifest >= 0.42 30 | # If your project uses README.rst, uncomment the following: 31 | # readme_renderer 32 | flake8 33 | pytest 34 | build 35 | twine 36 | 37 | allowlist_externals= 38 | make 39 | 40 | commands = 41 | make -f {toxinidir}/Makefile clean 42 | flake8 . 43 | check-manifest --ignore 'tox.ini,tests/**,examples/**,docs/**,*.md,Pipfile*,**/scan.proto' 44 | make -f {toxinidir}/Makefile build 45 | # pytest tests {posargs} 46 | python -m twine check dist/* 47 | 48 | [flake8] 49 | exclude = .tox,*.egg,build,data 50 | select = E,W,F 51 | -------------------------------------------------------------------------------- /examples/aws_quarantine.py: -------------------------------------------------------------------------------- 1 | import json 2 | import urllib 3 | import os 4 | 5 | import boto3 6 | 7 | import amaas.grpc 8 | 9 | # define a bucket to test quarantine 10 | 11 | quarantine_bucket = os.environ.get('QUARANTINEBUCKET') 12 | 13 | v1_amaas_server = os.getenv('TM_AM_SERVER_ADDR') 14 | v1_amaas_key = os.getenv('TM_AM_AUTH_KEY') 15 | 16 | s3 = boto3.resource('s3') 17 | 18 | 19 | def lambda_handler(event, context): 20 | # create v1fs connection handle 21 | handle = amaas.grpc.init(v1_amaas_server, v1_amaas_key, True) 22 | # or use v1 regions 23 | # handle = init_by_region(Your_V1_Region, v1_amaas_key, True) 24 | 25 | for record in event['Records']: 26 | bucket = record['s3']['bucket']['name'] 27 | key = urllib.parse.unquote_plus(record['s3']['object']['key'], encoding='utf-8') 28 | try: 29 | object = s3.Object(bucket, key) 30 | buffer = object.get().get('Body').read() 31 | scan_resp = amaas.grpc.scan_buffer(handle, buffer, key, ["test-tag"]) 32 | scan_result = json.loads(scan_resp) 33 | if scan_result.get('scanResult', 0) > 0: 34 | quarantine(bucket, key) 35 | except Exception as e: 36 | print(e) 37 | print('Error scan object {} from bucket {}.'.format(key, bucket)) 38 | 39 | amaas.grpc.quit(handle) 40 | 41 | 42 | def quarantine(bucket: str, key: str): 43 | print(f"start to quarantine {bucket}, {key}") 44 | copy_source = { 45 | 'Bucket': bucket, 46 | 'Key': key 47 | } 48 | s3.meta.client.copy(copy_source, quarantine_bucket, f"{bucket}/{key}") 49 | 50 | s3.meta.client.delete_object( 51 | Bucket=bucket, 52 | Key=key 53 | ) 54 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | build-n-publish: 20 | name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v4 27 | with: 28 | python-version: "3.11" 29 | 30 | - name: Install pypa/build 31 | run: | 32 | python -m pip install --upgrade pip 33 | python -m pip install --upgrade setuptools 34 | python -m pip install build pipenv tox --user 35 | 36 | - name: Build a binary wheel and a source tarball 37 | run: | 38 | pipenv --python 3.11 39 | make build 40 | 41 | - name: tox 42 | run: >- 43 | tox 44 | 45 | - name: Publish distribution 📦 to Test PyPI 46 | uses: pypa/gh-action-pypi-publish@release/v1 47 | with: 48 | skip-existing: true 49 | verbose: true 50 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 51 | repository-url: https://test.pypi.org/legacy/ 52 | - name: Publish distribution 📦 to PyPI 53 | if: startsWith(github.ref, 'refs/tags') 54 | uses: pypa/gh-action-pypi-publish@release/v1 55 | with: 56 | skip-existing: true 57 | verbose: true 58 | password: ${{ secrets.PYPI_API_TOKEN }} 59 | -------------------------------------------------------------------------------- /examples/client.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | import time 4 | 5 | import amaas.grpc 6 | 7 | if __name__ == "__main__": 8 | 9 | parser = argparse.ArgumentParser() 10 | 11 | parser.add_argument('-f', '--filename', action='store', default=sys.argv[0], 12 | help='file to be scanned') 13 | parser.add_argument('-a', '--addr', action='store', default='127.0.0.1:50051', required=False, 14 | help='gRPC server address and port (default 127.0.0.1:50051)') 15 | parser.add_argument('-r', '--region', action='store', 16 | help='AMaaS service region; e.g. us-east-1 or eu-central-1') 17 | parser.add_argument('--api_key', action='store', 18 | help='api key for authentication') 19 | parser.add_argument('--tls', action=argparse.BooleanOptionalAction, default=False, 20 | help='enable/disable TLS gRPC ') 21 | parser.add_argument('--ca_cert', action='store', 22 | help='CA certificate') 23 | parser.add_argument('--pml', action=argparse.BooleanOptionalAction, default=False, 24 | help='enable/disable predictive machine learning detection') 25 | parser.add_argument('-t', '--tags', action='store', nargs='+', 26 | help='list of tags') 27 | parser.add_argument('--feedback', action=argparse.BooleanOptionalAction, default=False, 28 | help='enable/disable feedback for predictive machine learning detection') 29 | parser.add_argument('-v', '--verbose', action=argparse.BooleanOptionalAction, default=False, 30 | help='enable/disable log verbose mode') 31 | parser.add_argument('--digest', action=argparse.BooleanOptionalAction, default=True, 32 | help='enable/disable digest calculation') 33 | 34 | args = parser.parse_args() 35 | 36 | if args.region: 37 | handle = amaas.grpc.init_by_region(args.region, args.api_key, args.tls, args.ca_cert) 38 | else: 39 | handle = amaas.grpc.init(args.addr, args.api_key, args.tls, args.ca_cert) 40 | 41 | s = time.perf_counter() 42 | 43 | try: 44 | result = amaas.grpc.scan_file( 45 | channel=handle, file_name=args.filename, pml=args.pml, 46 | tags=args.tags, feedback=args.feedback, verbose=args.verbose, digest=args.digest) 47 | elapsed = time.perf_counter() - s 48 | print(f"scan executed in {elapsed:0.2f} seconds.") 49 | print(result) 50 | except Exception as e: 51 | print(e) 52 | 53 | amaas.grpc.quit(handle) 54 | -------------------------------------------------------------------------------- /examples/client_aio.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import time 3 | import asyncio 4 | 5 | import amaas.grpc.aio 6 | 7 | 8 | async def main(args): 9 | if args.region: 10 | handle = amaas.grpc.aio.init_by_region(args.region, args.api_key, args.tls, args.ca_cert) 11 | else: 12 | handle = amaas.grpc.aio.init(args.addr, args.api_key, args.tls, args.ca_cert) 13 | 14 | tasks = set() 15 | for file_name in args.filename: 16 | task = asyncio.create_task( 17 | amaas.grpc.aio.scan_file( 18 | channel=handle, file_name=file_name, pml=args.pml, 19 | tags=args.tags, feedback=args.feedback, verbose=args.verbose, digest=args.digest) 20 | ) 21 | tasks.add(task) 22 | 23 | s = time.perf_counter() 24 | 25 | results = await asyncio.gather(*tasks) 26 | 27 | elapsed = time.perf_counter() - s 28 | 29 | print(f"scan tasks are executed in {elapsed:0.2f} seconds.") 30 | 31 | await amaas.grpc.aio.quit(handle) 32 | 33 | return results 34 | 35 | 36 | if __name__ == "__main__": 37 | parser = argparse.ArgumentParser() 38 | 39 | parser.add_argument('-f', '--filename', action='store', nargs='+', required=True, 40 | help='list of files to be scanned') 41 | parser.add_argument('-a', '--addr', action='store', default='127.0.0.1:50051', required=False, 42 | help='gRPC server address and port (default 127.0.0.1:50051)') 43 | parser.add_argument('-r', '--region', action='store', 44 | help='AMaaS service region; e.g. us-east-1 or eu-central-1') 45 | parser.add_argument('--api_key', action='store', 46 | help='api key for authentication') 47 | parser.add_argument('--tls', action=argparse.BooleanOptionalAction, default=False, 48 | help='enable/disable TLS gRPC ') 49 | parser.add_argument('--ca_cert', action='store', 50 | help='CA certificate') 51 | parser.add_argument('--pml', action=argparse.BooleanOptionalAction, default=False, 52 | help='enable/disable predictive machine learning detection') 53 | parser.add_argument('-t', '--tags', action='store', nargs='+', 54 | help='list of tags') 55 | parser.add_argument('--feedback', action=argparse.BooleanOptionalAction, default=False, 56 | help='enable/disable feedback for predictive machine learning detection') 57 | parser.add_argument('-v', '--verbose', action=argparse.BooleanOptionalAction, default=False, 58 | help='enable/disable log verbose mode') 59 | parser.add_argument('--digest', action=argparse.BooleanOptionalAction, default=True, 60 | help='enable/disable digest calculation') 61 | 62 | arguments = parser.parse_args() 63 | 64 | scan_results = asyncio.run(main(arguments)) 65 | 66 | for scan_result in scan_results: 67 | print(scan_result) 68 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | import os 2 | import grpc 3 | from unittest.mock import patch 4 | 5 | import amaas.grpc.util 6 | 7 | 8 | # 9 | # Test _init_by_region_util is invoked with correct arguments. 10 | # 11 | @patch("amaas.grpc.util._init_util") 12 | def test_init_by_region_util(utilmock): 13 | amaas.grpc.util._init_by_region_util("ap-southeast-2", "dummy_key") 14 | 15 | utilmock.assert_called_with( 16 | "antimalware.au-1.cloudone.trendmicro.com:443", "dummy_key", True, None, False 17 | ) 18 | 19 | 20 | # 21 | # Test insecure channel is created. 22 | # 23 | def test_insecure_channel(): 24 | channel = amaas.grpc.util._init_by_region_util( 25 | "us-east-1", None, False, None, False 26 | ) 27 | assert type(channel) is grpc._channel.Channel 28 | 29 | 30 | # 31 | # Test insecure aio channel is created. 32 | # 33 | def test_aio_insecure_channel(): 34 | channel = amaas.grpc.util._init_by_region_util( 35 | "us-east-1", None, is_aio_channel=True 36 | ) 37 | assert type(channel) is grpc.aio._channel.Channel 38 | 39 | 40 | # 41 | # Test secure channel is created. 42 | # 43 | def test_secure_channel(): 44 | channel = amaas.grpc.util._init_by_region_util("us-east-1", None, True, None, False) 45 | assert type(channel) is grpc._channel.Channel 46 | 47 | 48 | # 49 | # Test secure aio channel is created. 50 | # 51 | def test_aio_secure_channel(): 52 | channel = amaas.grpc.util._init_by_region_util( 53 | "us-east-1", None, True, None, is_aio_channel=True 54 | ) 55 | assert type(channel) is grpc.aio._channel.Channel 56 | 57 | 58 | # 59 | # Test default SSL only channel. 60 | # 61 | @patch("grpc.secure_channel") 62 | def test_def_ssl_only_channel(channel_mock): 63 | amaas.grpc.util._init_by_region_util("us-east-1", None, True, None, False) 64 | 65 | args = channel_mock.call_args.args 66 | assert type(args[1]._credentials) is grpc._cython.cygrpc.SSLChannelCredentials 67 | 68 | 69 | # 70 | # Test SSL only channel. 71 | # 72 | # @patch("builtins.open", new_callable=mock_open, read_data=SERVER_CERT) 73 | @patch("grpc.secure_channel") 74 | def test_ssl_only_channel(channel_mock): 75 | dir_path = os.path.dirname(os.path.realpath(__file__)) 76 | amaas.grpc.util._init_by_region_util( 77 | "us-east-1", None, True, dir_path + "/fake_server_cert.pem", False 78 | ) 79 | 80 | args = channel_mock.call_args.args 81 | assert type(args[1]._credentials) is grpc._cython.cygrpc.SSLChannelCredentials 82 | 83 | 84 | # 85 | # Test api key composite channel credential. 86 | # 87 | @patch("amaas.grpc.util._GrpcAuth") 88 | @patch("grpc.secure_channel") 89 | def test_composite_channel_with_apikey(channel_mock, auth_mock): 90 | dir_path = os.path.dirname(os.path.realpath(__file__)) 91 | amaas.grpc.util._init_by_region_util( 92 | "us-east-1", "abcabc12345678", True, dir_path + "/fake_server_cert.pem", False 93 | ) 94 | 95 | auth_mock.assert_called_with( 96 | "ApiKey abcabc12345678", 97 | ) 98 | 99 | args = channel_mock.call_args.args 100 | assert type(args[1]._credentials) is grpc._cython.cygrpc.CompositeChannelCredentials 101 | -------------------------------------------------------------------------------- /tests/mock_server.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | import grpc 4 | 5 | from amaas.grpc.protos.scan_pb2_grpc import ScanServicer 6 | from amaas.grpc.protos.scan_pb2 import STAGE_INIT, STAGE_FINI, STAGE_RUN 7 | from amaas.grpc.protos.scan_pb2 import CMD_QUIT, CMD_RETR 8 | from amaas.grpc.protos.scan_pb2 import S2C 9 | 10 | 11 | SDK_SCHEMA_VERSION = "1.0.0" 12 | MAX_RUN = 5 13 | FINAL_MSG = S2C(stage=STAGE_FINI, cmd=CMD_QUIT) 14 | 15 | 16 | class MockScanServicer(ScanServicer): 17 | IDENTIFIER_VIRUS = "virus" 18 | IDENTIFIER_UNKNOWN_CMD = "unknown_cmd" 19 | IDENTIFIER_MISMATCHED = "mismatched" 20 | IDENTIFIER_GRPC_ERROR = "grpc_error" 21 | IDENTIFIER_EXCEED_RATE = "exceed_rate" 22 | UNKNOWN_CMD = 999 23 | 24 | def __init__(self): 25 | self.fsize = 0 26 | self.identifier = "" 27 | 28 | def getS2CMsg(self): 29 | start = random.randint(0, self.fsize - 1) 30 | end = random.randint(start, self.fsize) 31 | s2cmsg = S2C(stage=STAGE_RUN, cmd=CMD_RETR, bulk_offset=[start], bulk_length=[end - start]) 32 | return s2cmsg 33 | 34 | def getUnknwonCmd(self): 35 | start = random.randint(0, self.fsize - 1) 36 | end = random.randint(start, self.fsize) 37 | s2cmsg = S2C( 38 | stage=STAGE_RUN, 39 | cmd=MockScanServicer.UNKNOWN_CMD, 40 | offset=start, 41 | length=end - start, 42 | ) 43 | return s2cmsg 44 | 45 | def getMismatchedCmdStage(self): 46 | start = random.randint(0, self.fsize - 1) 47 | end = random.randint(start, self.fsize) 48 | s2cmsg = S2C(stage=STAGE_RUN, cmd=CMD_QUIT, offset=start, length=end - start) 49 | return s2cmsg 50 | 51 | def Run(self, request_iterator, context): 52 | count = 0 53 | for req in request_iterator: 54 | if req.stage == STAGE_INIT: 55 | self.fsize = req.rs_size 56 | self.identifier = req.file_name 57 | msg = self.getS2CMsg() 58 | elif req.stage == STAGE_RUN: 59 | if self.identifier == MockScanServicer.IDENTIFIER_UNKNOWN_CMD: 60 | msg = self.getUnknwonCmd() 61 | elif self.identifier == MockScanServicer.IDENTIFIER_MISMATCHED: 62 | msg = self.getMismatchedCmdStage() 63 | elif self.identifier == MockScanServicer.IDENTIFIER_GRPC_ERROR: 64 | context.set_details("Ouch!") 65 | context.set_code(grpc.StatusCode.INTERNAL) 66 | msg = "" 67 | elif self.identifier == MockScanServicer.IDENTIFIER_EXCEED_RATE: 68 | context.set_details("Http Error Code: 429") 69 | context.set_code(grpc.StatusCode.INTERNAL) 70 | msg = "" 71 | elif count >= MAX_RUN: 72 | msg = FINAL_MSG 73 | result = { 74 | "scannerVersion": "1.0.0-1", 75 | "schemaVersion": SDK_SCHEMA_VERSION, 76 | "scanResult": 0, 77 | "foundMalwares": [], 78 | } 79 | if self.identifier == MockScanServicer.IDENTIFIER_VIRUS: 80 | result["scanResult"] = 1 81 | result["foundMalwares"] = ["virus1", "virus2"] 82 | msg.result = json.dumps(result) 83 | else: 84 | msg = self.getS2CMsg() 85 | count += 1 86 | yield msg 87 | -------------------------------------------------------------------------------- /amaas/grpc/util.py: -------------------------------------------------------------------------------- 1 | import grpc 2 | import hashlib 3 | from typing import BinaryIO, List 4 | from .exception import AMaasException 5 | from .exception import AMaasErrorCode 6 | 7 | HASH_CHUNK_SIZE = 512 * 1024 8 | 9 | APP_NAME_HEADER = "tm-app-name" 10 | APP_NAME_FILE_SCAN = "V1FS" 11 | 12 | # regions 13 | AWS_JP_REGION = "ap-northeast-1" 14 | AWS_SG_REGION = "ap-southeast-1" 15 | AWS_AU_REGION = "ap-southeast-2" 16 | AWS_IN_REGION = "ap-south-1" 17 | AWS_US_REGION = "us-east-1" 18 | AWS_DE_REGION = "eu-central-1" 19 | AWS_CA_REGION = "ca-central-1" 20 | AWS_TREND_REGION = "us-east-2" 21 | AWS_GB_REGION = "eu-west-2" 22 | AWS_AE_REGION = "me-central-1" 23 | C1_JP_REGION = "jp-1" 24 | C1_SG_REGION = "sg-1" 25 | C1_AU_REGION = "au-1" 26 | C1_IN_REGION = "in-1" 27 | C1_US_REGION = "us-1" 28 | C1_DE_REGION = "de-1" 29 | C1_CA_REGION = "ca-1" 30 | C1_TREND_REGION = "trend-us-1" 31 | C1_GB_REGION = "gb-1" 32 | C1_AE_REGION = "ae-1" 33 | 34 | C1Regions = [C1_AU_REGION, C1_CA_REGION, C1_DE_REGION, C1_GB_REGION, C1_IN_REGION, C1_JP_REGION, C1_SG_REGION, 35 | C1_US_REGION, C1_TREND_REGION] 36 | V1Regions = [AWS_AU_REGION, AWS_DE_REGION, AWS_IN_REGION, AWS_JP_REGION, AWS_SG_REGION, AWS_US_REGION, AWS_AE_REGION] 37 | SupportedV1Regions = V1Regions 38 | SupportedC1Regions = [C1_AU_REGION, C1_CA_REGION, C1_DE_REGION, C1_GB_REGION, C1_IN_REGION, C1_JP_REGION, C1_SG_REGION, 39 | C1_US_REGION] 40 | 41 | AllRegions = C1Regions + V1Regions 42 | AllValidRegions = SupportedC1Regions + SupportedV1Regions 43 | 44 | V1ToC1RegionMapping = {AWS_AU_REGION: C1_AU_REGION, 45 | AWS_DE_REGION: C1_DE_REGION, 46 | AWS_IN_REGION: C1_IN_REGION, 47 | AWS_JP_REGION: C1_JP_REGION, 48 | AWS_SG_REGION: C1_SG_REGION, 49 | AWS_US_REGION: C1_US_REGION, 50 | AWS_AE_REGION: C1_AE_REGION, 51 | } 52 | 53 | 54 | class _GrpcAuth(grpc.AuthMetadataPlugin): 55 | def __init__(self, key): 56 | self._key = key 57 | 58 | def __call__(self, context, callback): 59 | callback((('authorization', self._key),), None) 60 | 61 | 62 | def _init_util(host, api_key=None, enable_tls=False, ca_cert=None, is_aio_channel=False): 63 | call_creds = None 64 | if api_key: 65 | auth_key_str = 'ApiKey ' + api_key 66 | call_creds = grpc.metadata_call_credentials(_GrpcAuth(auth_key_str)) 67 | 68 | if enable_tls: 69 | if ca_cert: 70 | # Bring Your Own Certificate case 71 | with open(ca_cert, 'rb') as f: 72 | ssl_creds = grpc.ssl_channel_credentials(f.read()) 73 | else: 74 | ssl_creds = grpc.ssl_channel_credentials() 75 | 76 | # if authentication is necessary, combined call_creds with ssl_creds. 77 | # Otherwise, just use ssl_creds for channel credentials. 78 | if call_creds is None: 79 | creds = ssl_creds 80 | else: 81 | creds = grpc.composite_channel_credentials(ssl_creds, call_creds) 82 | 83 | channel = grpc.aio.secure_channel(host, creds) if is_aio_channel else grpc.secure_channel(host, creds) 84 | else: 85 | channel = grpc.aio.insecure_channel(host) if is_aio_channel else grpc.insecure_channel(host) 86 | 87 | return channel 88 | 89 | 90 | def _init_by_region_util(region, api_key, enable_tls=True, ca_cert=None, is_aio_channel=False): 91 | mapping = { 92 | C1_US_REGION: 'antimalware.us-1.cloudone.trendmicro.com:443', 93 | C1_IN_REGION: 'antimalware.in-1.cloudone.trendmicro.com:443', 94 | C1_DE_REGION: 'antimalware.de-1.cloudone.trendmicro.com:443', 95 | C1_SG_REGION: 'antimalware.sg-1.cloudone.trendmicro.com:443', 96 | C1_AU_REGION: 'antimalware.au-1.cloudone.trendmicro.com:443', 97 | C1_JP_REGION: 'antimalware.jp-1.cloudone.trendmicro.com:443', 98 | C1_GB_REGION: 'antimalware.gb-1.cloudone.trendmicro.com:443', 99 | C1_CA_REGION: 'antimalware.ca-1.cloudone.trendmicro.com:443', 100 | C1_AE_REGION: 'antimalware.ae-1.cloudone.trendmicro.com:443', 101 | } 102 | 103 | # make sure it is valid V1 or C1 region 104 | if region not in SupportedV1Regions: 105 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_INVALID_REGION, region, SupportedV1Regions) 106 | else: 107 | # map it to C1 region if it is V1 region 108 | c1_region = V1ToC1RegionMapping.get(region) 109 | if not c1_region: 110 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_INVALID_REGION, region, SupportedV1Regions) 111 | region = c1_region 112 | 113 | host = mapping.get(region, None) 114 | if host is None: 115 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_INVALID_REGION, region) 116 | return _init_util(host, api_key, enable_tls, ca_cert, is_aio_channel) 117 | 118 | 119 | def _validate_tags(tags: List[str]): 120 | if tags is not None: 121 | if len(tags) > 8: 122 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_TAG_NUMBER_EXCEED, len(tags)) 123 | 124 | for t in tags: 125 | if not (0 < len(t) < 64): 126 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_INVALID_TAG, t) 127 | 128 | 129 | def _digest_hex(data_reader: BinaryIO, algorithm: str): 130 | if algorithm == "sha1": 131 | file_hash = hashlib.sha1() 132 | elif algorithm == "sha256": 133 | file_hash = hashlib.sha256() 134 | else: 135 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_ERROR, "unsupported hash algorithm " + algorithm) 136 | 137 | w = data_reader.tell() 138 | data_reader.seek(0) 139 | 140 | chunk = data_reader.read(HASH_CHUNK_SIZE) 141 | while chunk: 142 | file_hash.update(chunk) 143 | chunk = data_reader.read(HASH_CHUNK_SIZE) 144 | 145 | data_reader.seek(w) 146 | return file_hash.hexdigest() 147 | -------------------------------------------------------------------------------- /tests/test_aio_client_sdk.py: -------------------------------------------------------------------------------- 1 | import grpc 2 | import json 3 | import os 4 | import pytest 5 | import random 6 | import tempfile 7 | from concurrent import futures 8 | from unittest.mock import patch 9 | 10 | import amaas.grpc.aio 11 | from .mock_server import MockScanServicer 12 | from amaas.grpc.exception import AMaasErrorCode 13 | from amaas.grpc.exception import AMaasException 14 | 15 | 16 | NUM_DATA_LOOP = 128 17 | _, TEST_DATA_FILE_NAME = tempfile.mkstemp() 18 | SERVER_PORT = random.randint(49152, 65535) 19 | 20 | 21 | # 22 | # Test init_by_region method 23 | # 24 | @patch("amaas.grpc.aio._init_by_region_util") 25 | def test_init_by_region(utilmock): 26 | amaas.grpc.aio.init_by_region("us-east-1", "dummy_key") 27 | 28 | # ensure True is passed to is_aio_channel parameter 29 | utilmock.assert_called_with("us-east-1", "dummy_key", True, None, True) 30 | 31 | 32 | # 33 | # Test init method 34 | # 35 | @patch("amaas.grpc.aio._init_util") 36 | def test_init(utilmock): 37 | amaas.grpc.aio.init("us-east-1", "dummy_key") 38 | 39 | # ensure False is passed to is_aio_channel parameter 40 | utilmock.assert_called_with("us-east-1", "dummy_key", False, None, True) 41 | 42 | 43 | # 44 | # Create a mock gRPC server with MockScanServicer 45 | # 46 | @pytest.fixture(scope="module", autouse=True) 47 | def run_setup(): 48 | server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) 49 | 50 | amaas.grpc.protos.scan_pb2_grpc.add_ScanServicer_to_server( 51 | MockScanServicer(), server 52 | ) 53 | server.add_insecure_port(f"[::]:{SERVER_PORT}") 54 | server.start() 55 | yield server 56 | server.stop(None) 57 | 58 | 59 | # 60 | # This routine is to create a data file for testing. 61 | # The binary file contains of NUM_DATA_LOOP of consecutive bytes from 0x00 to 0xff. 62 | # This is to make checking the correctness of the content of the retrieval easier. 63 | # The grpc client receives a sequence of instructions from grpc server to retrieve chunks of data 64 | # from a data file during scanning. 65 | # One of the important checking is to ensure the data retrived is what is actually rqeuested by the server. 66 | # 67 | @pytest.fixture(scope="session", autouse=True) 68 | def create_data_file(): 69 | with open(TEST_DATA_FILE_NAME, "wb") as binary_file: 70 | for j in range(NUM_DATA_LOOP): 71 | for i in range(256): 72 | # Convert the number to a single byte and write to the file 73 | byte_value = i.to_bytes(1, byteorder="big") 74 | binary_file.write(byte_value) 75 | yield 76 | os.remove(TEST_DATA_FILE_NAME) 77 | 78 | 79 | # 80 | # Testing the SDK scan_file method failed with MSG_ID_ERR_FILE_NOT_FOUND exception. 81 | # 82 | @pytest.mark.asyncio 83 | async def test_scan_file_not_found(): 84 | handle = grpc.aio.insecure_channel(f"localhost:{SERVER_PORT}") 85 | NOT_EXIST_FILE = "012345.txt" 86 | with pytest.raises(AMaasException) as exc_info: 87 | await amaas.grpc.scan_file(handle, NOT_EXIST_FILE) 88 | assert exc_info.value.args[0] == AMaasErrorCode.MSG_ID_ERR_FILE_NOT_FOUND 89 | assert exc_info.value.args[1] == NOT_EXIST_FILE 90 | 91 | 92 | # 93 | # Testing the SDK scan_file method failed with MSG_ID_ERR_FILE_NO_PERMISSION exception 94 | # 95 | @pytest.mark.asyncio 96 | async def test_scan_file_no_permission(): 97 | handle = grpc.insecure_channel(f"localhost:{SERVER_PORT}") 98 | NOT_PERMISSION_FILE = tempfile.NamedTemporaryFile().name 99 | open(NOT_PERMISSION_FILE, "x") 100 | os.chmod(NOT_PERMISSION_FILE, 0x000) 101 | with pytest.raises(AMaasException) as exc_info: 102 | await amaas.grpc.scan_file(handle, NOT_PERMISSION_FILE) 103 | assert exc_info.value.args[0] == AMaasErrorCode.MSG_ID_ERR_FILE_NO_PERMISSION 104 | assert exc_info.value.args[1] == NOT_PERMISSION_FILE 105 | os.remove(NOT_PERMISSION_FILE) 106 | 107 | 108 | # 109 | # Testing the SDK scan_buffer method succeeded without viruses. 110 | # 111 | @pytest.mark.asyncio 112 | async def test_scan_buffer_success(): 113 | handle = grpc.aio.insecure_channel(f"localhost:{SERVER_PORT}") 114 | with open(TEST_DATA_FILE_NAME, mode="rb") as bfile: 115 | buffer = bfile.read() 116 | response = await amaas.grpc.aio.scan_buffer(handle, buffer, TEST_DATA_FILE_NAME) 117 | assert json.loads(response)["scanResult"] == 0 118 | 119 | 120 | # 121 | # Testing the SDK scan_buffer method succeeded with viruses. 122 | # 123 | @pytest.mark.asyncio 124 | async def test_scan_buffer_virus(): 125 | handle = grpc.aio.insecure_channel(f"localhost:{SERVER_PORT}") 126 | with open(TEST_DATA_FILE_NAME, mode="rb") as bfile: 127 | buffer = bfile.read() 128 | response = await amaas.grpc.aio.scan_buffer(handle, buffer, "virus") 129 | jobj = json.loads(response) 130 | assert jobj["scanResult"] == 1 131 | assert jobj["foundMalwares"] == ["virus1", "virus2"] 132 | 133 | 134 | # 135 | # Test different exceptions throwed by scan_buffer API. 136 | # 137 | @pytest.mark.asyncio 138 | @pytest.mark.parametrize( 139 | "error_type, expected_exception", 140 | [ 141 | ( 142 | MockScanServicer.IDENTIFIER_EXCEED_RATE, 143 | [AMaasErrorCode.MSG_ID_ERR_RATE_LIMIT_EXCEEDED], 144 | ), 145 | ( 146 | MockScanServicer.IDENTIFIER_MISMATCHED, 147 | [ 148 | AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE, 149 | amaas.grpc.scan_pb2.CMD_QUIT, 150 | amaas.grpc.scan_pb2.STAGE_RUN, 151 | ], 152 | ), 153 | ( 154 | MockScanServicer.IDENTIFIER_UNKNOWN_CMD, 155 | [AMaasErrorCode.MSG_ID_ERR_UNKNOWN_CMD, MockScanServicer.UNKNOWN_CMD], 156 | ), 157 | ( 158 | MockScanServicer.IDENTIFIER_GRPC_ERROR, 159 | [AMaasErrorCode.MSG_ID_GRPC_ERROR, grpc.StatusCode.INTERNAL.value[0]], 160 | ), 161 | ], 162 | ) 163 | async def test_scan_buffer_exceptions(error_type, expected_exception): 164 | handle = grpc.aio.insecure_channel(f"localhost:{SERVER_PORT}") 165 | with open(TEST_DATA_FILE_NAME, mode="rb") as bfile: 166 | buffer = bfile.read() 167 | with pytest.raises(AMaasException) as exc_info: 168 | await amaas.grpc.aio.scan_buffer(handle, buffer, error_type) 169 | for cnt in range(len(expected_exception)): 170 | assert exc_info.value.args[cnt] == expected_exception[cnt] 171 | -------------------------------------------------------------------------------- /amaas/grpc/aio/__init__.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | from typing import BinaryIO, List 4 | 5 | import grpc 6 | import logging 7 | 8 | from ..protos import scan_pb2 9 | from ..protos import scan_pb2_grpc 10 | from ..exception import AMaasException 11 | from ..exception import AMaasErrorCode 12 | from ..util import _init_by_region_util 13 | from ..util import _init_util 14 | from ..util import _validate_tags 15 | from ..util import _digest_hex 16 | from ..util import APP_NAME_HEADER, APP_NAME_FILE_SCAN 17 | 18 | logger = logging.getLogger(__name__) 19 | logger.addHandler(logging.StreamHandler()) 20 | LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO') 21 | logger.setLevel(LOG_LEVEL) 22 | logger.propagate = False 23 | 24 | timeout_in_seconds = int(os.environ.get('TM_AM_SCAN_TIMEOUT_SECS', 300)) 25 | 26 | 27 | def init_by_region(region, api_key, enable_tls=True, ca_cert=None): 28 | return _init_by_region_util(region, api_key, enable_tls, ca_cert, True) 29 | 30 | 31 | def init(host, api_key=None, enable_tls=False, ca_cert=None): 32 | return _init_util(host, api_key, enable_tls, ca_cert, True) 33 | 34 | 35 | async def quit(handle): 36 | await handle.close() 37 | 38 | 39 | # https://github.com/grpc/grpc/blob/91083659fa88c938779dd41e57a7f97981b6c9a1/src/python/grpcio_tests/tests_aio/unit/channel_test.py#L180 40 | 41 | 42 | async def _scan_data(channel: grpc.Channel, data_reader: BinaryIO, size: int, identifier: str, tags: List[str], 43 | pml: bool, feedback: bool, verbose: bool, digest: bool) -> str: 44 | _validate_tags(tags) 45 | stub = scan_pb2_grpc.ScanStub(channel) 46 | stats = {} 47 | result = None 48 | bulk = True 49 | file_sha1 = "" 50 | file_sha256 = "" 51 | 52 | if digest: 53 | file_sha1 = "sha1:" + _digest_hex(data_reader, "sha1") 54 | file_sha256 = "sha256:" + _digest_hex(data_reader, "sha256") 55 | 56 | try: 57 | metadata = ( 58 | (APP_NAME_HEADER, APP_NAME_FILE_SCAN), 59 | ) 60 | call = stub.Run(timeout=timeout_in_seconds, metadata=metadata) 61 | 62 | request = scan_pb2.C2S(stage=scan_pb2.STAGE_INIT, 63 | file_name=identifier, 64 | rs_size=size, 65 | offset=0, 66 | chunk=None, 67 | tags=tags, 68 | trendx=pml, 69 | file_sha1=file_sha1, 70 | file_sha256=file_sha256, 71 | bulk=bulk, 72 | spn_feedback=feedback, 73 | verbose=verbose) 74 | 75 | await call.write(request) 76 | 77 | while True: 78 | response = await call.read() 79 | 80 | if response.cmd == scan_pb2.CMD_RETR: 81 | if response.stage != scan_pb2.STAGE_RUN: 82 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE, response.cmd, 83 | response.stage) 84 | length = [] 85 | offset = [] 86 | 87 | if bulk: 88 | logger.debug("enter bulk mode") 89 | bulk_count = len(response.bulk_offset) 90 | 91 | if bulk_count > 1: 92 | logger.debug("bulk transfer triggered") 93 | 94 | length = response.bulk_length[:] 95 | offset = response.bulk_offset[:] 96 | else: 97 | logger.debug("enter non-bulk mode") 98 | length.append(response.length) 99 | offset.append(response.offset) 100 | 101 | for i in range(len(length)): 102 | logger.debug(f"try to read {length[i]} at offset {offset[i]}") 103 | data_reader.seek(offset[i]) 104 | chunk = data_reader.read(length[i]) 105 | 106 | request = scan_pb2.C2S( 107 | stage=scan_pb2.STAGE_RUN, 108 | file_name=None, 109 | rs_size=0, 110 | offset=offset[i], 111 | chunk=chunk) 112 | 113 | stats["total_upload"] = stats.get( 114 | "total_upload", 0) + len(chunk) 115 | 116 | await call.write(request) 117 | elif response.cmd == scan_pb2.CMD_QUIT: 118 | if response.stage != scan_pb2.STAGE_FINI: 119 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE, response.cmd, 120 | response.stage) 121 | result = response.result 122 | logger.debug("receive QUIT, exit loop...") 123 | break 124 | else: 125 | logger.debug("unknown command...") 126 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_UNKNOWN_CMD, response.cmd) 127 | 128 | await call.done_writing() 129 | 130 | total_upload = stats.get("total_upload", 0) 131 | logger.debug(f"total upload {total_upload} bytes") 132 | 133 | except AMaasException: 134 | raise 135 | except grpc.aio.AioRpcError as rpc_error: 136 | if "429" in str(rpc_error): 137 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_RATE_LIMIT_EXCEEDED) 138 | elif rpc_error.code() == grpc.StatusCode.UNAUTHENTICATED: 139 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_KEY_AUTH_FAILED) 140 | else: 141 | raise AMaasException(AMaasErrorCode.MSG_ID_GRPC_ERROR, rpc_error.code().value[0], rpc_error.details()) 142 | except Exception as err: 143 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_ERROR, str(err)) 144 | 145 | return result 146 | 147 | 148 | async def scan_file(channel: grpc.Channel, file_name: str, tags: List[str] = None, 149 | pml: bool = False, feedback: bool = False, verbose: bool = False, digest: bool = True) -> str: 150 | try: 151 | f = open(file_name, "rb") 152 | fid = os.path.basename(file_name) 153 | n = os.stat(file_name).st_size 154 | except FileNotFoundError as err: 155 | logger.debug("File not exist: " + str(err)) 156 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_FILE_NOT_FOUND, file_name) 157 | except (PermissionError, IOError) as err: 158 | logger.debug("Permission error: " + str(err)) 159 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_FILE_NO_PERMISSION, file_name) 160 | return await _scan_data(channel, f, n, fid, tags, pml, feedback, verbose, digest) 161 | 162 | 163 | async def scan_buffer(channel: grpc.Channel, bytes_buffer: bytes, uid: str, tags: List[str] = None, 164 | pml: bool = False, feedback: bool = False, verbose: bool = False, digest: bool = True) -> str: 165 | f = io.BytesIO(bytes_buffer) 166 | return await _scan_data(channel, f, len(bytes_buffer), uid, tags, pml, feedback, verbose, digest) 167 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Trend Vision One File Security Python SDK Example User Guide 2 | 3 | The Trend Vision One File Security Python SDK empowers developers to craft applications seamlessly integrating with the cloud-based Trend Vision One anti-malware file scanning service. This ensures a thorough scan of data and artifacts within the applications, identifying potential malicious elements. 4 | 5 | This guide outlines the steps to establish your development environment and configure your project, laying the foundation for utilizing the File Security Python SDK effectively. 6 | 7 | ## Requirements 8 | 9 | - Python 3.9 or newer 10 | - Trend Vision One account with a chosen region - for more information, see the [Trend Vision One documentation](https://docs.trendmicro.com/en-us/enterprise/trend-micro-xdr-help/Home). 11 | - A Trend Vision One API key with proper role - for more information, see the [Trend Vision One API key documentation](https://docs.trendmicro.com/en-us/enterprise/trend-vision-one/administrative-setti/accountspartfoundati/api-keys.aspx). 12 | 13 | ## Installation 14 | 15 | Install the File Security SDK package with pip: 16 | 17 | ```sh 18 | python -m pip install visionone-filesecurity 19 | ``` 20 | 21 | ## Obtain an API Key 22 | 23 | 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. 24 | 25 | 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. 26 | 27 | If you plan on using a Trend Vision One region, be sure to pass in region parameter when running custom program with File Security SDK to specify the region of that API key and to ensure you have proper authorization. The list of supported Trend Vision One regions can be found at API Reference section below. 28 | 29 | 1. Login to the Trend Vision One. 30 | 2. Create a new Trend Vision One API key: 31 | 32 | - Navigate to the Trend Vision One User Roles page. 33 | - 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. 34 | - 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. 35 | 36 | ## Run SDK 37 | 38 | ### Run with File Security SDK examples 39 | 40 | 1. Go to `/examples/` in current directory. 41 | 42 | ```sh 43 | cd examples/ 44 | ``` 45 | 46 | 2. There are two Python examples in the folder, one with regular file i/o and one with asynchronous file i/o 47 | 48 | ```text 49 | client_aio.py 50 | client.py 51 | ``` 52 | 53 | 3. Current Python examples support following command line arguments 54 | 55 | | Command Line Arguments | Value | Optional | 56 | |--------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------| 57 | | -f FILENAME, --filename FILENAME | File to be scanned | No | 58 | | -a ADDR, --addr ADDR | Trend Vision One File Security server | Yes, either -r or -a | 59 | | -r REGION, --region REGION | 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`, `ap-south-1` | Yes, either -r or -a | 60 | | --api_key API_KEY | Vision One API Key | Yes | 61 | | --tls, --no-tls | Enable or disable TLS | Yes | 62 | | --ca_cert CA_CERT | CA certificate used to connect to self hosted AMaaS | Yes | 63 | | --pml, --no-pml | Predictive Machine Learning | Yes | 64 | | -t TAGS [TAGS ...], --tags TAGS [TAGS ...] | List of tags | Yes | 65 | | --feedback, --no-feedback | Feedback for Predictive Machine Learning detection | Yes | 66 | | -v, --verbose, --no-verbose | Log verbose mode | Yes | 67 | | --digest, --no-digest | Calculate digests for cache search and result lookup | Yes | 68 | 69 | 4. Run one of the examples. 70 | 71 | Make sure to customize the example program by configuring it with the API key from your Vision One account, found in your Vision One Dashboard. Assign the value of your Vision One Region's `API_KEY` to the variable and set `FILENAME` to the desired target file. 72 | 73 | ```sh 74 | python3 client.py -f FILENAME -r us-east-1 --tls --api_key API_KEY 75 | ``` 76 | 77 | or 78 | 79 | using File Security server address `-a` instead of region `-r`: 80 | 81 | ```sh 82 | python3 client.py -f FILENAME -a antimalware._REGION_.cloudone.trendmicro.com:443 --tls --api_key API_KEY 83 | ``` 84 | 85 | or 86 | 87 | using asynchronous IO example program: 88 | 89 | ```sh 90 | python3 client_aio.py -f FILENAME -a antimalware._REGION_.cloudone.trendmicro.com:443 --tls --api_key API_KEY 91 | ``` 92 | 93 | ## File Security Post Scan Actions 94 | 95 | Actions to perform after scanning files with Trend Vision One™ File Security 96 | 97 | After completing a scan, [File Security displays scan results](https://docs.trendmicro.com/en-us/documentation/article/trend-vision-one-fs-cli#supported_targets) immediately so you can take action right away to reduce risk and fulfill compliance requirements. Scan results include custom information that is passed to the SDK so you can take further action. 98 | 99 | ## Procedure 100 | 101 | - If File Security indicates a file contains malware, you can take the following actions: 102 | - Quarantine the file. See a [AWS quarantine example](https://github.com/trendmicro/tm-v1-fs-python-sdk/blob/main/examples/aws_quarantine.py). 103 | - Send notification messages using: 104 | - Slack 105 | - Email 106 | - Microsoft Teams 107 | - If File Security does not find malware in a file, you can promote the file. 108 | -------------------------------------------------------------------------------- /amaas/grpc/__init__.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import logging 3 | from typing import BinaryIO, List 4 | 5 | import grpc 6 | import os 7 | import io 8 | 9 | from .protos import scan_pb2 10 | from .protos import scan_pb2_grpc 11 | from .exception import AMaasException 12 | from .exception import AMaasErrorCode 13 | from .util import _init_by_region_util 14 | from .util import _init_util 15 | from .util import _validate_tags 16 | from .util import _digest_hex 17 | from .util import APP_NAME_HEADER, APP_NAME_FILE_SCAN 18 | 19 | logger = logging.getLogger(__name__) 20 | logger.addHandler(logging.StreamHandler()) 21 | LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO') 22 | logger.setLevel(LOG_LEVEL) 23 | logger.propagate = False 24 | 25 | timeout_in_seconds = int(os.environ.get('TM_AM_SCAN_TIMEOUT_SECS', 300)) 26 | 27 | 28 | class _Pipeline: 29 | """ 30 | Class to allow a single element pipeline between producer and consumer. 31 | """ 32 | 33 | def __init__(self): 34 | self._message = 0 35 | self._producer_lock = threading.Lock() 36 | self._consumer_lock = threading.Lock() 37 | self._consumer_lock.acquire() 38 | 39 | def get_message(self): 40 | self._consumer_lock.acquire() 41 | message = self._message 42 | self._producer_lock.release() 43 | return message 44 | 45 | def set_message(self, message): 46 | self._producer_lock.acquire() 47 | self._message = message 48 | self._consumer_lock.release() 49 | 50 | 51 | def init_by_region(region, api_key, enable_tls=True, ca_cert=None): 52 | return _init_by_region_util(region, api_key, enable_tls, ca_cert, False) 53 | 54 | 55 | def init(host, api_key=None, enable_tls=False, ca_cert=None): 56 | return _init_util(host, api_key, enable_tls, ca_cert, False) 57 | 58 | 59 | def _generate_messages(pipeline: _Pipeline, data_reader: BinaryIO, bulk: bool, stats: dict) -> None: 60 | responses = [] 61 | 62 | while True: 63 | for r in responses: 64 | if r[0] == "INIT": 65 | response = r[1] 66 | elif r[0] == "RUN": 67 | offset = r[1] 68 | length = r[2] 69 | data_reader.seek(offset) 70 | chunk = data_reader.read(length) 71 | response = scan_pb2.C2S( 72 | stage=scan_pb2.STAGE_RUN, 73 | file_name=None, 74 | rs_size=0, 75 | offset=offset, 76 | chunk=chunk, 77 | ) 78 | stats["total_upload"] = stats.get("total_upload", 0) + len(chunk) 79 | else: 80 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE, "None", r[0]) 81 | yield response 82 | 83 | responses.clear() 84 | message = pipeline.get_message() 85 | 86 | if message.stage == scan_pb2.STAGE_INIT: 87 | logger.debug("stage INIT") 88 | responses.append(("INIT", message)) 89 | elif message.stage == scan_pb2.STAGE_RUN: 90 | if message.cmd != scan_pb2.CMD_RETR: 91 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE, message.cmd, message.stage) 92 | 93 | length = [] 94 | offset = [] 95 | 96 | if bulk: 97 | offset = message.bulk_offset[:] 98 | length = message.bulk_length[:] 99 | 100 | if len(length) > 1: 101 | logger.debug("bulk transfer triggered") 102 | else: 103 | offset.append(message.offset) 104 | length.append(message.length) 105 | 106 | for i in range(len(length)): 107 | logger.debug(f"stage RUN, try to read {length[i]} at offset {offset[i]}") 108 | responses.append(("RUN", offset[i], length[i])) 109 | elif message.stage == scan_pb2.STAGE_FINI: 110 | if message.cmd != scan_pb2.CMD_QUIT: 111 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE, message.cmd, message.stage) 112 | 113 | logger.debug("final stage, quit generating C2S messages...") 114 | break 115 | else: 116 | logger.debug("unknown stage.....!!!") 117 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_UNKNOWN_STAGE, message.stage) 118 | 119 | 120 | def quit(handle): 121 | handle.close() 122 | 123 | 124 | def _scan_data(channel: grpc.Channel, data_reader: BinaryIO, size: int, identifier: str, tags: List[str], 125 | pml: bool, feedback: bool, verbose: bool, digest: bool) -> str: 126 | _validate_tags(tags) 127 | stub = scan_pb2_grpc.ScanStub(channel) 128 | pipeline = _Pipeline() 129 | stats = {} 130 | result = None 131 | bulk = True 132 | file_sha1 = "" 133 | file_sha256 = "" 134 | 135 | if digest: 136 | file_sha1 = "sha1:" + _digest_hex(data_reader, "sha1") 137 | file_sha256 = "sha256:" + _digest_hex(data_reader, "sha256") 138 | 139 | try: 140 | metadata = ( 141 | (APP_NAME_HEADER, APP_NAME_FILE_SCAN), 142 | ) 143 | responses = stub.Run(_generate_messages(pipeline, data_reader, bulk, stats), timeout=timeout_in_seconds, 144 | metadata=metadata) 145 | message = scan_pb2.C2S(stage=scan_pb2.STAGE_INIT, 146 | file_name=identifier, 147 | rs_size=size, 148 | offset=0, 149 | chunk=None, 150 | trendx=pml, 151 | tags=tags, 152 | file_sha1=file_sha1, 153 | file_sha256=file_sha256, 154 | bulk=bulk, 155 | spn_feedback=feedback, 156 | verbose=verbose) 157 | 158 | pipeline.set_message(message) 159 | 160 | for response in responses: 161 | if response.cmd == scan_pb2.CMD_RETR: 162 | pipeline.set_message(response) 163 | elif response.cmd == scan_pb2.CMD_QUIT: 164 | result = response.result 165 | pipeline.set_message(response) 166 | logger.debug("receive QUIT, exit loop...") 167 | break 168 | else: 169 | logger.debug("unknown command...") 170 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_UNKNOWN_CMD, response.cmd) 171 | 172 | total_upload = stats.get("total_upload", 0) 173 | logger.debug(f"total upload {total_upload} bytes") 174 | 175 | except AMaasException: 176 | raise 177 | except grpc.RpcError as rpc_error: 178 | if "429" in str(rpc_error): 179 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_RATE_LIMIT_EXCEEDED) 180 | elif rpc_error.code() == grpc.StatusCode.UNAUTHENTICATED: 181 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_KEY_AUTH_FAILED) 182 | else: 183 | raise AMaasException(AMaasErrorCode.MSG_ID_GRPC_ERROR, rpc_error.code().value[0], rpc_error.details()) 184 | except Exception as err: 185 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_ERROR, str(err)) 186 | 187 | return result 188 | 189 | 190 | def scan_file(channel: grpc.Channel, file_name: str, tags: List[str] = None, 191 | pml: bool = False, feedback: bool = False, verbose: bool = False, digest: bool = True) -> str: 192 | try: 193 | f = open(file_name, "rb") 194 | fid = os.path.basename(file_name) 195 | n = os.stat(file_name).st_size 196 | except FileNotFoundError as err: 197 | logger.debug("File not exist: " + str(err)) 198 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_FILE_NOT_FOUND, file_name) 199 | except (PermissionError, IOError) as err: 200 | logger.debug("Permission error: " + str(err)) 201 | raise AMaasException(AMaasErrorCode.MSG_ID_ERR_FILE_NO_PERMISSION, file_name) 202 | 203 | return _scan_data(channel, f, n, fid, tags, pml, feedback, verbose, digest) 204 | 205 | 206 | def scan_buffer(channel: grpc.Channel, bytes_buffer: bytes, uid: str, tags: List[str] = None, 207 | pml: bool = False, feedback: bool = False, verbose: bool = False, digest: bool = True) -> str: 208 | f = io.BytesIO(bytes_buffer) 209 | return _scan_data(channel, f, len(bytes_buffer), uid, tags, pml, feedback, verbose, digest) 210 | -------------------------------------------------------------------------------- /tests/test_client_sdk.py: -------------------------------------------------------------------------------- 1 | import grpc 2 | import json 3 | import os 4 | import pytest 5 | import random 6 | import tempfile 7 | import uuid 8 | from concurrent import futures 9 | from unittest.mock import patch 10 | 11 | import amaas.grpc 12 | from .mock_server import MockScanServicer 13 | from amaas.grpc.exception import AMaasErrorCode 14 | from amaas.grpc.exception import AMaasException 15 | 16 | 17 | NUM_DATA_LOOP = 128 18 | _, TEST_DATA_FILE_NAME = tempfile.mkstemp() 19 | SERVER_PORT = random.randint(49152, 65535) 20 | 21 | 22 | @patch("amaas.grpc._init_by_region_util") 23 | def test_init_by_region(utilmock): 24 | amaas.grpc.init_by_region("ap-southeast-2", "dummy_key") 25 | 26 | # ensure False is passed to is_aio_channel parameter 27 | utilmock.assert_called_with("ap-southeast-2", "dummy_key", True, None, False) 28 | 29 | 30 | @patch("amaas.grpc._init_util") 31 | def test_init(utilmock): 32 | amaas.grpc.init("ap-southeast-2", "dummy_key") 33 | 34 | # ensure False is passed to is_aio_channel parameter 35 | utilmock.assert_called_with("ap-southeast-2", "dummy_key", False, None, False) 36 | 37 | 38 | # 39 | # Create a mock gRPC server with MockScanServicer 40 | # 41 | @pytest.fixture(scope="module", autouse=True) 42 | def run_setup(): 43 | server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) 44 | 45 | amaas.grpc.protos.scan_pb2_grpc.add_ScanServicer_to_server( 46 | MockScanServicer(), server 47 | ) 48 | server.add_insecure_port(f"[::]:{SERVER_PORT}") 49 | server.start() 50 | yield server 51 | server.stop(None) 52 | 53 | 54 | # 55 | # This routine is to create a data file for testing. 56 | # The binary file contains of NUM_DATA_LOOP of consecutive bytes from 0x00 to 0xff. 57 | # This is to make checking the correctness of the content of the retrieval easier. 58 | # The grpc client receives a sequence of instructions from grpc server to retrieve chunks of data 59 | # from a data file during scanning. 60 | # One of the important checking is to ensure the data retrived is what is actually rqeuested by the server. 61 | # 62 | @pytest.fixture(scope="session", autouse=True) 63 | def create_data_file(): 64 | with open(TEST_DATA_FILE_NAME, "wb") as binary_file: 65 | for j in range(NUM_DATA_LOOP): 66 | for i in range(256): 67 | # Convert the number to a single byte and write to the file 68 | byte_value = i.to_bytes(1, byteorder="big") 69 | binary_file.write(byte_value) 70 | yield 71 | os.remove(TEST_DATA_FILE_NAME) 72 | 73 | 74 | # 75 | # The method is to verify the chunk matching the data chunk at the given location in the file. 76 | # @param buf buf of the data chunk 77 | # @param offset location in the data file 78 | # @return True if matched False otherwise. 79 | # 80 | def verify_buf_with_data(buf, offset): 81 | blen = len(buf) 82 | cnt = 0 83 | for posn in range(offset, blen): 84 | val = posn % 256 85 | # byte_value = val.to_bytes(1, byteorder='big') 86 | assert buf[cnt] == val 87 | cnt += 1 88 | 89 | 90 | # 91 | # Testing authentication failure. 92 | # 93 | def test_scan_file_failed_authentication(): 94 | dir_path = os.path.dirname(os.path.realpath(__file__)) 95 | handle = amaas.grpc.init_by_region("us-east-1", "api-key", True, None) 96 | with pytest.raises(AMaasException) as exc_info: 97 | amaas.grpc.scan_file(handle, dir_path + "/fake_server_cert.pem") 98 | assert exc_info.value.args[0] == AMaasErrorCode.MSG_ID_ERR_KEY_AUTH_FAILED 99 | 100 | 101 | # 102 | # Testing the generate_message method correctly processing the initial message from grpc client 103 | # 104 | def test_generate_message_init(): 105 | pipeline = amaas.grpc._Pipeline() 106 | stats = {} 107 | with open(TEST_DATA_FILE_NAME, "rb") as f: 108 | size = os.stat(TEST_DATA_FILE_NAME).st_size 109 | server_resp = amaas.grpc.scan_pb2.C2S( 110 | stage=amaas.grpc.scan_pb2.STAGE_INIT, 111 | file_name=TEST_DATA_FILE_NAME, 112 | rs_size=size, 113 | offset=0, 114 | chunk=None, 115 | tags=None, 116 | ) 117 | 118 | pipeline.set_message(server_resp) 119 | c2s_msg = next(amaas.grpc._generate_messages(pipeline, f, True, stats)) 120 | assert c2s_msg.stage == amaas.grpc.scan_pb2.STAGE_INIT 121 | assert c2s_msg.offset == 0 122 | assert c2s_msg.rs_size == size 123 | assert c2s_msg.file_name == TEST_DATA_FILE_NAME 124 | 125 | 126 | # 127 | # Testing the generate_message method correctly processing a number of back and forth 128 | # data retrieval message from grpc server. 129 | # 130 | def test_generate_message_data(): 131 | pipeline = amaas.grpc._Pipeline() 132 | stats = {} 133 | f = open(TEST_DATA_FILE_NAME, "rb") 134 | size = os.stat(TEST_DATA_FILE_NAME).st_size 135 | # 136 | # lets loop 5 times to check 137 | # 138 | for _ in range(5): 139 | start = random.randint(0, size - 1) 140 | end = random.randint(start, size) 141 | server_resp = amaas.grpc.scan_pb2.S2C( 142 | stage=amaas.grpc.scan_pb2.STAGE_RUN, 143 | cmd=amaas.grpc.scan_pb2.CMD_RETR, 144 | bulk_offset=[start], 145 | bulk_length=[end - start], 146 | ) 147 | 148 | pipeline.set_message(server_resp) 149 | c2s_msg = next(amaas.grpc._generate_messages(pipeline, f, True, stats)) 150 | assert c2s_msg.stage == amaas.grpc.scan_pb2.STAGE_RUN 151 | assert c2s_msg.offset == start 152 | verify_buf_with_data(c2s_msg.chunk, start) 153 | 154 | 155 | # 156 | # Testing the generate_message method correctly raise exception when recieved 157 | # wrong command in STAGE RUN from server. 158 | # 159 | def test_generate_message_data_wrong_cmd_exception(): 160 | pipeline = amaas.grpc._Pipeline() 161 | stats = {} 162 | with open(TEST_DATA_FILE_NAME, "rb") as f: 163 | size = os.stat(TEST_DATA_FILE_NAME).st_size 164 | start = random.randint(0, size - 1) 165 | end = random.randint(start, size) 166 | server_resp = amaas.grpc.scan_pb2.S2C( 167 | stage=amaas.grpc.scan_pb2.STAGE_RUN, 168 | cmd=amaas.grpc.scan_pb2.CMD_QUIT, 169 | bulk_offset=[start], 170 | bulk_length=[end - start], 171 | ) 172 | pipeline.set_message(server_resp) 173 | with pytest.raises(AMaasException) as exc_info: 174 | next(amaas.grpc._generate_messages(pipeline, f, True, stats)) 175 | assert ( 176 | exc_info.value.args[0] == AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE 177 | ) 178 | assert exc_info.value.args[1] == amaas.grpc.scan_pb2.CMD_QUIT 179 | assert exc_info.value.args[2] == amaas.grpc.scan_pb2.STAGE_RUN 180 | 181 | 182 | # 183 | # Testing the generate_message method correctly processing the final message from grpc server. 184 | # 185 | def test_generate_message_final(): 186 | pipeline = amaas.grpc._Pipeline() 187 | stats = {} 188 | f = open(TEST_DATA_FILE_NAME, "rb") 189 | server_resp = amaas.grpc.scan_pb2.S2C( 190 | stage=amaas.grpc.scan_pb2.STAGE_FINI, cmd=amaas.grpc.scan_pb2.CMD_QUIT 191 | ) 192 | 193 | pipeline.set_message(server_resp) 194 | client_msg = next(amaas.grpc._generate_messages(pipeline, f, True, stats), None) 195 | assert client_msg is None 196 | 197 | 198 | # 199 | # Testing the generate_message method correctly raise exception when recieved wrong command in STAGE_FINI from server. 200 | # 201 | def test_generate_message_final_wrong_cmd_exception(): 202 | pipeline = amaas.grpc._Pipeline() 203 | stats = {} 204 | with open(TEST_DATA_FILE_NAME, "rb") as f: 205 | size = os.stat(TEST_DATA_FILE_NAME).st_size 206 | start = random.randint(0, size - 1) 207 | end = random.randint(start, size) 208 | server_resp = amaas.grpc.scan_pb2.S2C( 209 | stage=amaas.grpc.scan_pb2.STAGE_FINI, 210 | cmd=amaas.grpc.scan_pb2.CMD_RETR, 211 | offset=start, 212 | length=end - start, 213 | ) 214 | pipeline.set_message(server_resp) 215 | with pytest.raises(AMaasException) as exc_info: 216 | next(amaas.grpc._generate_messages(pipeline, f, True, stats)) 217 | assert ( 218 | exc_info.value.args[0] == AMaasErrorCode.MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE 219 | ) 220 | assert exc_info.value.args[1] == amaas.grpc.scan_pb2.CMD_RETR 221 | assert exc_info.value.args[2] == amaas.grpc.scan_pb2.STAGE_FINI 222 | 223 | 224 | # 225 | # Testing the generate_message method correctly processing the unknown stage message from grpc server. 226 | # 227 | def test_generate_message_unknwon_stage(): 228 | pipeline = amaas.grpc._Pipeline() 229 | stats = {} 230 | f = open(TEST_DATA_FILE_NAME, "rb") 231 | UNKNOWN_STAGE = 9999 232 | server_resp = amaas.grpc.scan_pb2.S2C( 233 | stage=UNKNOWN_STAGE, cmd=amaas.grpc.scan_pb2.CMD_QUIT 234 | ) 235 | 236 | pipeline.set_message(server_resp) 237 | with pytest.raises(AMaasException) as exc_info: 238 | next(amaas.grpc._generate_messages(pipeline, f, True, stats), None) 239 | assert exc_info.value.args[0] == AMaasErrorCode.MSG_ID_ERR_UNKNOWN_STAGE 240 | assert exc_info.value.args[1] == UNKNOWN_STAGE 241 | 242 | 243 | # 244 | # Testing the SDK scan_file method sucessfully scans a file with no virus. 245 | # 246 | def test_scan_file_success(): 247 | dir_path = os.path.dirname(os.path.realpath(__file__)) 248 | handle = grpc.insecure_channel(f"localhost:{SERVER_PORT}") 249 | response = amaas.grpc.scan_file(handle, dir_path + "/fake_server_cert.pem") 250 | assert json.loads(response)["scanResult"] == 0 251 | 252 | 253 | # 254 | # Testing the SDK scan_file method sucessfully failed with MSG_ID_ERR_FILE_NOT_FOUND exception. 255 | # 256 | def test_scan_file_not_found(): 257 | handle = grpc.insecure_channel(f"localhost:{SERVER_PORT}") 258 | NOT_EXIST_FILE = f"{str(uuid.uuid4())}.txt" 259 | with pytest.raises(AMaasException) as exc_info: 260 | amaas.grpc.scan_file(handle, NOT_EXIST_FILE) 261 | assert exc_info.value.args[0] == AMaasErrorCode.MSG_ID_ERR_FILE_NOT_FOUND 262 | assert exc_info.value.args[1] == NOT_EXIST_FILE 263 | 264 | 265 | # 266 | # Testing the SDK scan_file method failed with MSG_ID_ERR_FILE_NO_PERMISSION exception 267 | # 268 | def test_scan_file_no_permission(): 269 | handle = grpc.insecure_channel(f"localhost:{SERVER_PORT}") 270 | NOT_PERMISSION_FILE = tempfile.NamedTemporaryFile().name 271 | open(NOT_PERMISSION_FILE, "x") 272 | os.chmod(NOT_PERMISSION_FILE, 0x000) 273 | with pytest.raises(AMaasException) as exc_info: 274 | amaas.grpc.scan_file(handle, NOT_PERMISSION_FILE) 275 | assert exc_info.value.args[0] == AMaasErrorCode.MSG_ID_ERR_FILE_NO_PERMISSION 276 | assert exc_info.value.args[1] == NOT_PERMISSION_FILE 277 | os.remove(NOT_PERMISSION_FILE) 278 | 279 | 280 | # 281 | # Testing the SDK scan_buffer method sucessfully scans a file with no virus. 282 | # 283 | def test_scan_buffer_success(): 284 | handle = grpc.insecure_channel(f"localhost:{SERVER_PORT}") 285 | with open(TEST_DATA_FILE_NAME, mode="rb") as bfile: 286 | buffer = bfile.read() 287 | response = amaas.grpc.scan_buffer(handle, buffer, TEST_DATA_FILE_NAME) 288 | assert json.loads(response)["scanResult"] == 0 289 | 290 | 291 | # 292 | # Testing the SDK scan_buffer method sucessfully scans a file with virus. 293 | # 294 | def test_scan_buffer_virus(): 295 | handle = grpc.insecure_channel(f"localhost:{SERVER_PORT}") 296 | with open(TEST_DATA_FILE_NAME, mode="rb") as bfile: 297 | buffer = bfile.read() 298 | response = amaas.grpc.scan_buffer( 299 | handle, buffer, MockScanServicer.IDENTIFIER_VIRUS 300 | ) 301 | jobj = json.loads(response) 302 | assert jobj["scanResult"] == 1 303 | assert jobj["foundMalwares"] == ["virus1", "virus2"] 304 | 305 | 306 | # 307 | # Test different exceptions throwed by scan_buffer API. 308 | # 309 | @pytest.mark.parametrize( 310 | "error_type, expected_exception", 311 | [ 312 | ( 313 | MockScanServicer.IDENTIFIER_EXCEED_RATE, 314 | [AMaasErrorCode.MSG_ID_ERR_RATE_LIMIT_EXCEEDED], 315 | ), 316 | ( 317 | MockScanServicer.IDENTIFIER_UNKNOWN_CMD, 318 | [AMaasErrorCode.MSG_ID_ERR_UNKNOWN_CMD, MockScanServicer.UNKNOWN_CMD], 319 | ), 320 | ( 321 | MockScanServicer.IDENTIFIER_GRPC_ERROR, 322 | [AMaasErrorCode.MSG_ID_GRPC_ERROR, grpc.StatusCode.INTERNAL.value[0]], 323 | ), 324 | ], 325 | ) 326 | def test_scan_buffer_exceptions(error_type, expected_exception): 327 | handle = grpc.insecure_channel(f"localhost:{SERVER_PORT}") 328 | with open(TEST_DATA_FILE_NAME, mode="rb") as bfile: 329 | buffer = bfile.read() 330 | with pytest.raises(AMaasException) as exc_info: 331 | amaas.grpc.scan_buffer(handle, buffer, error_type) 332 | for cnt in range(len(expected_exception)): 333 | assert exc_info.value.args[cnt] == expected_exception[cnt] 334 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trend Vision One™ File Security Python SDK User Guide 2 | 3 | Trend Vision One™ - File Security is a scanner app for files and cloud storage. This scanner can detect all types of malicious software (malware) including trojans, ransomware, spyware, and more. Based on fragments of previously seen malware, File Security detects obfuscated or polymorphic variants of malware. 4 | File Security can assess any file type or size for malware and display real-time results. With the latest file reputation and variant protection technologies backed by leading threat research, File Security automates malware scanning. 5 | File Security can also scan objects across your environment in any application, whether on-premises or in the cloud. 6 | 7 | The Python software development kit (SDK) for Trend Vision One™ File Security empowers you to craft applications which seamlessly integrate with File Security. With this SDK you can perform a thorough scan of data and artifacts within your applications to identify potential malicious elements. 8 | Follow the steps below to set up your development environment and configure your project, laying the foundation to effectively use File Security. 9 | 10 | ## Checking prerequisites 11 | 12 | - Python 3.9 to 3.13 13 | - Trend Vision One account with a chosen region - for more information, see the [Trend Vision One documentation](https://docs.trendmicro.com/en-us/documentation/article/trend-vision-one-trend-micro-xdr-abou_001). 14 | - A Trend Vision One API key with proper role - for more information, see the [Trend Vision One API key documentation](https://docs.trendmicro.com/en-us/documentation/article/trend-vision-one-api-keys). 15 | 16 | When you have all the prerequisites, continue with creating an API key. 17 | 18 | ## Creating an API Key 19 | 20 | The File Security SDK requires a valid application programming interface (API) key provided as a parameter to the SDK client object. Trend Vision One API keys are associated with different regions. Refer to the region flag below to obtain a better understanding of the valid regions associated with the API key. For more information, see the [Trend Vision One API key documentation](https://docs.trendmicro.com/en-us/documentation/article/trend-vision-one-api-keys). 21 | 22 | ### Procedure 23 | 24 | - Go to Administrations > API Keys. 25 | - Click Add API Key. 26 | - Configure the API key to use the role with the 'Run file scan via SDK' permission. 27 | - Verify that the API key is associated with the region you plan to use. 28 | - Set an expiry time for the API key and make a record of it for future reference. 29 | 30 | ## Installing the SDK 31 | 32 | Install the File Security SDK package with pip: 33 | 34 | ```sh 35 | python -m pip install visionone-filesecurity 36 | ``` 37 | 38 | ## Using File Security Python SDK 39 | 40 | Using File Security Python SDK to scan for malware involves the following basic steps: 41 | 42 | 1. Create an AMaaS handle object by specifying preferred Vision One region where scanning should be done and a valid API key. 43 | 2. Replace "YOUR_API_KEY_OR_TOKEN" and "YOUR_REGION" with your actual API key or token and the desired region. 44 | 3. Invoke file scan method to scan the target data. 45 | 4. Parse the JSON response returned by the scan APIs to determine whether the scanned data contains malware or not. 46 | 47 | ### Basic Sample Code 48 | 49 | ```python 50 | api_key = "YOUR_API_KEY_OR_TOKEN" 51 | region = "YOUR_REGION" 52 | 53 | try: 54 | handle = amaas.grpc.init_by_region(region=region, api_key=api_key) 55 | except Exception as err: 56 | print(err) 57 | 58 | s = time.perf_counter() 59 | 60 | try: 61 | result = amaas.grpc.scan_file(handle, file_name=filename, pml=pml, tags=tags) 62 | elapsed = time.perf_counter() - s 63 | print(f"scan executed in {elapsed:0.2f} seconds.") 64 | print(result) 65 | except Exception as e: 66 | print(e) 67 | 68 | amaas.grpc.quit(handle) 69 | ``` 70 | 71 | ### AIO Sample Code 72 | 73 | ```python 74 | api_key = "YOUR_API_KEY_OR_TOKEN" 75 | region = "YOUR_REGION" 76 | 77 | async def main(): 78 | handle = amaas.grpc.aio.init_by_region(region=region, api_key=api_key) 79 | 80 | tasks = set() 81 | for file_name in file_list: 82 | task = asyncio.create_task(amaas.grpc.aio.scan_file(handle, file_name=file_name, pml=pml, tags=tags)) 83 | tasks.add(task) 84 | 85 | s = time.perf_counter() 86 | 87 | results = await asyncio.gather(*tasks) 88 | elapsed = time.perf_counter() - s 89 | print(f"scan tasks are executed in {elapsed:0.2f} seconds.") 90 | 91 | await amaas.grpc.aio.quit(handle) 92 | return results 93 | 94 | scan_results = asyncio.run(main()) 95 | for scan_result in scan_results: 96 | print(scan_result) 97 | ``` 98 | 99 | ### Sample JSON Response 100 | #### Concise Format 101 | 102 | ```json 103 | { 104 | "scannerVersion": "1.0.0-29", 105 | "schemaVersion": "1.0.0", 106 | "scanResult": 1, 107 | "scanId": "74c7362b-8245-48be-81fe-b620a0409ef1", 108 | "scanTimestamp": "2024-04-09T03:17:18.26Z", 109 | "fileName": "EICAR_TEST_FILE-1.exe", 110 | "foundMalwares": [ 111 | { 112 | "fileName": "Eicar.exe", 113 | "malwareName": "Eicar_test_file" 114 | } 115 | ], 116 | "fileSHA1": "96f11a72c53aac4b24a5e4899bc9f2341d0b7a83", 117 | "fileSHA256": "7dddcd0f64165f51291a41f49b6246cf85c3e6e599c096612cccce09566091f2" 118 | } 119 | ``` 120 | #### Verbose Format 121 | ```json 122 | { 123 | "scanType": "sdk", 124 | "objectType": "file", 125 | "timestamp": { 126 | "start": "2024-04-26T18:43:48.639Z", 127 | "end": "2024-04-26T18:43:49.941Z" 128 | }, 129 | "schemaVersion": "1.0.0", 130 | "scannerVersion": "1.0.0-1", 131 | "fileName": "TRENDX_detect.exe", 132 | "rsSize": 356352, 133 | "scanId": "84947a19-b84a-4091-bb7d-8422ab5098a7", 134 | "accountId": "7423a980-b5af-4e28-bf0b-b58cdf623bb8", 135 | "result": { 136 | "atse": { 137 | "elapsedTime": 1004335, 138 | "fileType": 7, 139 | "fileSubType": 2, 140 | "version": { 141 | "engine": "23.57.0-1002", 142 | "lptvpn": 301, 143 | "ssaptn": 721, 144 | "tmblack": 253, 145 | "tmwhite": 227, 146 | "macvpn": 904 147 | }, 148 | "malwareCount": 0, 149 | "malware": null, 150 | "error": null, 151 | "fileTypeName": "EXE", 152 | "fileSubTypeName": "VSDT_EXE_W32" 153 | }, 154 | "trendx": { 155 | "elapsedTime": 296763, 156 | "fileType": 7, 157 | "fileSubType": 2, 158 | "version": { 159 | "engine": "23.57.0-1002", 160 | "tmblack": 253, 161 | "trendx": 331 162 | }, 163 | "malwareCount": 1, 164 | "malware": [ 165 | { 166 | "name": "Ransom.Win32.TRX.XXPE1", 167 | "fileName": "TRENDX_detect.exe", 168 | "type": "Ransom", 169 | "fileType": 7, 170 | "fileSubType": 2, 171 | "fileTypeName": "EXE", 172 | "fileSubTypeName": "VSDT_EXE_W32" 173 | } 174 | ], 175 | "error": null, 176 | "fileTypeName": "EXE", 177 | "fileSubTypeName": "VSDT_EXE_W32" 178 | } 179 | }, 180 | "fileSHA1": "b448479b0a6a5d387c71600e1b75700ba7f42b0a", 181 | "fileSHA256": "4b7593109f81b5a770d440d8c28fa1457cd4b95d51b5d049fb301fc99c41da39", 182 | "appName": "V1FS" 183 | } 184 | ``` 185 | 186 | When malicious content is detected in the scanned object, `scanResult` will show a non-zero value. Otherwise, the value will be `null`. Moreover, when malware is detected, `foundMalwares` will be non-empty containing one or more name/value pairs of `fileName` and `malwareName`. `fileName` will be filename of malware detected while `malwareName` will be the name of the virus/malware found. 187 | 188 | ## Python Client SDK API Reference 189 | 190 | ### Initialization 191 | 192 | #### ```def amaas.grpc.init_by_region(region: str, api_key: str, enable_tls: bool = True, ca_cert: str = None) -> grpc.Channel``` 193 | 194 | Creates a new instance of the grpc Channel, and provisions essential settings, including authentication/authorization credentials (API key), preferred service region, etc. 195 | 196 | **_Parameters_** 197 | 198 | | Parameter | Description | 199 | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 200 | | region | 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-northeast-1`, `ap-southeast-2`, `ap-southeast-1`, `ap-south-1`, `me-central-1`, etc. | 201 | | api_key | Your own Vision One API Key. | 202 | | enable_tls | Enable or disable TLS. TLS should always be enabled when connecting to the AMaaS server. For more information, see the 'Ensuring Secure Communication with TLS' section. | 203 | | ca_cert | `Optional` CA certificate used to connect to self hosted AMaaS server. | 204 | 205 | **_Return_** 206 | A grpc Channel instance 207 | 208 | #### ```def amaas.grpc.aio.init_by_region(region: str, api_key: str, enable_tls: bool = True, ca_cert: str = None) -> grpc.aio.Channel``` 209 | 210 | Creates a new instance of the grpc aio Channel, and provisions essential settings, including authentication/authorization credentials (API key), preferred service region, etc. 211 | 212 | **_Parameters_** 213 | 214 | | Parameter | Description | 215 | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 216 | | region | 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-northeast-1`, `ap-southeast-2`, `ap-southeast-1`, `ap-south-1`, `me-central-1`, etc. | 217 | | api_key | Your own Vision One API Key. | 218 | | enable_tls | Enable or disable TLS. TLS should always be enabled when connecting to the AMaaS server. For more information, see the 'Ensuring Secure Communication with TLS' section. | 219 | | ca_cert | `Optional` CA certificate used to connect to self hosted AMaaS server. | 220 | 221 | **_Return_** 222 | A grpc aio Channel instance 223 | 224 | ### Scan 225 | 226 | #### ```def amaas.grpc.scan_file(handle: grpc.Channel, file_name: str, tags: List[str], pml: bool = False, feedback: bool = False, verbose: bool = False) -> str``` 227 | 228 | Scan a file for malware and retrieves response data from the API. 229 | 230 | **_Parameters_** 231 | 232 | | Parameter | Description | 233 | | --------- | ----------------------------------------------------------------------------------------------------------- | 234 | | handle | The grpc Channel instance was created from the init function. | 235 | | file_name | The name of the file with the path of the directory containing the file to scan. | 236 | | tags | A list of strings to be used to tag the scan result. At most 8 tags with a maximum length of 63 characters. | 237 | | pml | Enable PML (Predictive Machine Learning) Detection. | 238 | | feedback | Enable SPN feedback for Predictive Machine Learning Detection | 239 | | verbose | Enable log verbose mode | 240 | | digest | Calculate digests for cache search and result lookup | 241 | 242 | **_Return_** 243 | String the scanned result in JSON format. 244 | 245 | #### ```def amaas.grpc.aio.scan_file(handle: grpc.aio.Channel, file_name: str, tags: List[str], pml: bool = False, feedback: bool = False, verbose: bool = False) -> str``` 246 | 247 | AsyncIO Scan a file for malware and retrieves response data from the API. 248 | 249 | **_Parameters_** 250 | 251 | | Parameter | Description | 252 | | --------- | ----------------------------------------------------------------------------------------------------------- | 253 | | handle | The grpc aio Channel instance was created from the init function. | 254 | | file_name | The name of the file with the path of the directory containing the file to scan. | 255 | | tags | A list of strings to be used to tag the scan result. At most 8 tags with a maximum length of 63 characters. | 256 | | pml | Enable PML (Predictive Machine Learning) Detection. | 257 | | feedback | Enable SPN feedback for Predictive Machine Learning Detection | 258 | | verbose | Enable log verbose mode | 259 | | digest | Calculate digests for cache search and result lookup | 260 | 261 | **_Return_** 262 | String the scanned result in JSON format. 263 | 264 | ### Cleaning Up 265 | 266 | #### ```def amaas.grpc.quit(handle: grpc.aio.Channel) -> None``` 267 | 268 | Remember to clean up the grpc Channel when you are done using it to release any allocated resources: 269 | 270 | **_Parameters_** 271 | 272 | | Parameter | Description | 273 | | --------- | --------------------------------------------------------- | 274 | | handle | The grpc Channel instance created from the init function. | 275 | 276 | #### ```def amaas.grpc.aio.quit(handle: grpc.aio.Channel) -> None``` 277 | 278 | Remember to clean up the grpc aio Channel when you are done using it to release any allocated resources: 279 | 280 | **_Parameters_** 281 | 282 | | Parameter | Description | 283 | | --------- | ------------------------------------------------------------- | 284 | | handle | The grpc aio Channel instance created from the init function. | 285 | 286 | ## Environment Variables 287 | 288 | The following environment variables are supported by Python Client SDK and can be used in lieu of values specified as function arguments. 289 | 290 | | Variable Name | Description & Purpose | Valid Values | 291 | | ------------------------- | -------------------------------------------------------------------------- | -------------------------- | 292 | | `TM_AM_SCAN_TIMEOUT_SECS` | Specify, in number of seconds, to override the default scan timeout period | 0, 1, 2, ... ; default=300 | 293 | 294 | ## Thread Safety 295 | 296 | - scanFile() or scanBuffer() are designed to be thread-safe. It should be able to invoke scanFile() concurrently from multiple threads without protecting scanFile() with mutex or other synchronization mechanisms. 297 | 298 | ## Ensuring Secure Communication with TLS 299 | 300 | The communication channel between the client program or SDK and the Trend Vision One™ File Security service is fortified with robust server-side TLS encryption. This ensures that all data transmitted between the client and Trend service remains thoroughly encrypted and safeguarded. 301 | The certificate employed by server-side TLS is a publicly-signed certificate from Trend Micro Inc, issued by a trusted Certificate Authority (CA), further bolstering security measures. 302 | 303 | The File Security SDK consistently adopts TLS as the default communication channel, prioritizing security at all times. It is strongly advised not to disable TLS in a production environment while utilizing the File Security SDK, as doing so could compromise the integrity and confidentiality of transmitted data. 304 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "d724fa4375ff2c0aac57a3974442b42ac3e57cb9baf1a6d3abb9cbbfadc8ea0d" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "grpcio": { 20 | "hashes": [ 21 | "sha256:0ab8b2864396663a5b0b0d6d79495657ae85fa37dcb6498a2669d067c65c11ea", 22 | "sha256:0fa05ee31a20456b13ae49ad2e5d585265f71dd19fbd9ef983c28f926d45d0a7", 23 | "sha256:0ff35c8d807c1c7531d3002be03221ff9ae15712b53ab46e2a0b4bb271f38537", 24 | "sha256:1be857615e26a86d7363e8a163fade914595c81fec962b3d514a4b1e8760467b", 25 | "sha256:20e8f653abd5ec606be69540f57289274c9ca503ed38388481e98fa396ed0b41", 26 | "sha256:22c3bc8d488c039a199f7a003a38cb7635db6656fa96437a8accde8322ce2366", 27 | "sha256:24e867651fc67717b6f896d5f0cac0ec863a8b5fb7d6441c2ab428f52c651c6b", 28 | "sha256:2b85f7820475ad3edec209d3d89a7909ada16caab05d3f2e08a7e8ae3200a55c", 29 | "sha256:39983a9245d37394fd59de71e88c4b295eb510a3555e0a847d9965088cdbd033", 30 | "sha256:3d081e859fb1ebe176de33fc3adb26c7d46b8812f906042705346b314bde32c3", 31 | "sha256:469f42a0b410883185eab4689060a20488a1a0a00f8bbb3cbc1061197b4c5a79", 32 | "sha256:47be9584729534660416f6d2a3108aaeac1122f6b5bdbf9fd823e11fe6fbaa29", 33 | "sha256:4be74ddeeb92cc87190e0e376dbc8fc7736dbb6d3d454f2fa1f5be1dee26b9d7", 34 | "sha256:4dd0dfbe4d5eb1fcfec9490ca13f82b089a309dc3678e2edabc144051270a66e", 35 | "sha256:5b08d03ace7aca7b2fadd4baf291139b4a5f058805a8327bfe9aece7253b6d67", 36 | "sha256:63e41b91032f298b3e973b3fa4093cbbc620c875e2da7b93e249d4728b54559a", 37 | "sha256:652350609332de6dac4ece254e5d7e1ff834e203d6afb769601f286886f6f3a8", 38 | "sha256:693bc706c031aeb848849b9d1c6b63ae6bcc64057984bb91a542332b75aa4c3d", 39 | "sha256:74258dce215cb1995083daa17b379a1a5a87d275387b7ffe137f1d5131e2cfbb", 40 | "sha256:789d5e2a3a15419374b7b45cd680b1e83bbc1e52b9086e49308e2c0b5bbae6e3", 41 | "sha256:7c9c80ac6091c916db81131d50926a93ab162a7e97e4428ffc186b6e80d6dda4", 42 | "sha256:7d6ac9481d9d0d129224f6d5934d5832c4b1cddb96b59e7eba8416868909786a", 43 | "sha256:85da336e3649a3d2171e82f696b5cad2c6231fdd5bad52616476235681bee5b3", 44 | "sha256:8700a2a57771cc43ea295296330daaddc0d93c088f0a35cc969292b6db959bf3", 45 | "sha256:8997d6785e93308f277884ee6899ba63baafa0dfb4729748200fcc537858a509", 46 | "sha256:9182e0063112e55e74ee7584769ec5a0b4f18252c35787f48738627e23a62b97", 47 | "sha256:9b91879d6da1605811ebc60d21ab6a7e4bae6c35f6b63a061d61eb818c8168f6", 48 | "sha256:a2242d6950dc892afdf9e951ed7ff89473aaf744b7d5727ad56bdaace363722b", 49 | "sha256:a371e6b6a5379d3692cc4ea1cb92754d2a47bdddeee755d3203d1f84ae08e03e", 50 | "sha256:a76d39b5fafd79ed604c4be0a869ec3581a172a707e2a8d7a4858cb05a5a7637", 51 | "sha256:ad9f30838550695b5eb302add33f21f7301b882937460dd24f24b3cc5a95067a", 52 | "sha256:b2266862c5ad664a380fbbcdbdb8289d71464c42a8c29053820ee78ba0119e5d", 53 | "sha256:b78a99cd1ece4be92ab7c07765a0b038194ded2e0a26fd654591ee136088d8d7", 54 | "sha256:c200cb6f2393468142eb50ab19613229dcc7829b5ccee8b658a36005f6669fdd", 55 | "sha256:c30f393f9d5ff00a71bb56de4aa75b8fe91b161aeb61d39528db6b768d7eac69", 56 | "sha256:c6a0a28450c16809f94e0b5bfe52cabff63e7e4b97b44123ebf77f448534d07d", 57 | "sha256:cebc1b34ba40a312ab480ccdb396ff3c529377a2fce72c45a741f7215bfe8379", 58 | "sha256:d2c170247315f2d7e5798a22358e982ad6eeb68fa20cf7a820bb74c11f0736e7", 59 | "sha256:d35a95f05a8a2cbe8e02be137740138b3b2ea5f80bd004444e4f9a1ffc511e32", 60 | "sha256:d5170929109450a2c031cfe87d6716f2fae39695ad5335d9106ae88cc32dc84c", 61 | "sha256:d6aa986318c36508dc1d5001a3ff169a15b99b9f96ef5e98e13522c506b37eef", 62 | "sha256:d6de81c9c00c8a23047136b11794b3584cdc1460ed7cbc10eada50614baa1444", 63 | "sha256:dc1a1231ed23caac1de9f943d031f1bc38d0f69d2a3b243ea0d664fc1fbd7fec", 64 | "sha256:e6beeea5566092c5e3c4896c6d1d307fb46b1d4bdf3e70c8340b190a69198594", 65 | "sha256:e6d8de076528f7c43a2f576bc311799f89d795aa6c9b637377cc2b1616473804", 66 | "sha256:e6f83a583ed0a5b08c5bc7a3fe860bb3c2eac1f03f1f63e0bc2091325605d2b7", 67 | "sha256:f250ff44843d9a0615e350c77f890082102a0318d66a99540f54769c8766ab73", 68 | "sha256:f71574afdf944e6652203cd1badcda195b2a27d9c83e6d88dc1ce3cfb73b31a5", 69 | "sha256:f903017db76bf9cc2b2d8bdd37bf04b505bbccad6be8a81e1542206875d0e9db", 70 | "sha256:f9a412f55bb6e8f3bb000e020dbc1e709627dcb3a56f6431fa7076b4c1aab0db", 71 | "sha256:f9c30c464cb2ddfbc2ddf9400287701270fdc0f14be5f08a1e3939f1e749b455" 72 | ], 73 | "index": "pypi", 74 | "markers": "python_version >= '3.9'", 75 | "version": "==1.71.0" 76 | }, 77 | "protobuf": { 78 | "hashes": [ 79 | "sha256:13eb236f8eb9ec34e63fc8b1d6efd2777d062fa6aaa68268fb67cf77f6839ad7", 80 | "sha256:1832f0515b62d12d8e6ffc078d7e9eb06969aa6dc13c13e1036e39d73bebc2de", 81 | "sha256:307ecba1d852ec237e9ba668e087326a67564ef83e45a0189a772ede9e854dd0", 82 | "sha256:3fde11b505e1597f71b875ef2fc52062b6a9740e5f7c8997ce878b6009145862", 83 | "sha256:476cb7b14914c780605a8cf62e38c2a85f8caff2e28a6a0bad827ec7d6c85d68", 84 | "sha256:4f1dfcd7997b31ef8f53ec82781ff434a28bf71d9102ddde14d076adcfc78c99", 85 | "sha256:678974e1e3a9b975b8bc2447fca458db5f93a2fb6b0c8db46b6675b5b5346812", 86 | "sha256:aec4962f9ea93c431d5714ed1be1c93f13e1a8618e70035ba2b0564d9e633f2e", 87 | "sha256:bcefcdf3976233f8a502d265eb65ea740c989bacc6c30a58290ed0e519eb4b8d", 88 | "sha256:d7d3f7d1d5a66ed4942d4fefb12ac4b14a29028b209d4bfb25c68ae172059922", 89 | "sha256:fd32223020cb25a2cc100366f1dedc904e2d71d9322403224cdde5fdced0dabe" 90 | ], 91 | "index": "pypi", 92 | "markers": "python_version >= '3.8'", 93 | "version": "==5.29.4" 94 | } 95 | }, 96 | "develop": { 97 | "attrs": { 98 | "hashes": [ 99 | "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", 100 | "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b" 101 | ], 102 | "markers": "python_version >= '3.8'", 103 | "version": "==25.3.0" 104 | }, 105 | "backports.tarfile": { 106 | "hashes": [ 107 | "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", 108 | "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991" 109 | ], 110 | "markers": "python_version >= '3.8'", 111 | "version": "==1.2.0" 112 | }, 113 | "cached-property": { 114 | "hashes": [ 115 | "sha256:484d617105e3ee0e4f1f58725e72a8ef9e93deee462222dbd51cd91230897641", 116 | "sha256:f617d70ab1100b7bcf6e42228f9ddcb78c676ffa167278d9f730d1c2fba69ccb" 117 | ], 118 | "markers": "python_version >= '3.8'", 119 | "version": "==2.0.1" 120 | }, 121 | "cerberus": { 122 | "hashes": [ 123 | "sha256:180e7d1fa1a5765cbff7b5c716e52fddddfab859dc8f625b0d563ace4b7a7ab3", 124 | "sha256:ecf249665400a0b7a9d5e4ee1ffc234fd5d003186d3e1904f70bc14038642c13" 125 | ], 126 | "markers": "python_version >= '3.7'", 127 | "version": "==1.3.7" 128 | }, 129 | "certifi": { 130 | "hashes": [ 131 | "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", 132 | "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3" 133 | ], 134 | "markers": "python_version >= '3.6'", 135 | "version": "==2025.4.26" 136 | }, 137 | "chardet": { 138 | "hashes": [ 139 | "sha256:0368df2bfd78b5fc20572bb4e9bb7fb53e2c094f60ae9993339e8671d0afb8aa", 140 | "sha256:d3e64f022d254183001eccc5db4040520c0f23b1a3f33d6413e099eb7f126557" 141 | ], 142 | "markers": "python_version >= '3.6'", 143 | "version": "==5.0.0" 144 | }, 145 | "charset-normalizer": { 146 | "hashes": [ 147 | "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", 148 | "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45", 149 | "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", 150 | "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", 151 | "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", 152 | "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", 153 | "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d", 154 | "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", 155 | "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184", 156 | "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", 157 | "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b", 158 | "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64", 159 | "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", 160 | "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", 161 | "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", 162 | "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344", 163 | "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58", 164 | "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", 165 | "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", 166 | "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", 167 | "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", 168 | "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", 169 | "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", 170 | "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", 171 | "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", 172 | "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1", 173 | "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01", 174 | "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", 175 | "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58", 176 | "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", 177 | "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", 178 | "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2", 179 | "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a", 180 | "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", 181 | "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", 182 | "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5", 183 | "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb", 184 | "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f", 185 | "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", 186 | "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", 187 | "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", 188 | "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", 189 | "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7", 190 | "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", 191 | "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455", 192 | "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", 193 | "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4", 194 | "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", 195 | "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", 196 | "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", 197 | "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", 198 | "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", 199 | "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", 200 | "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", 201 | "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", 202 | "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", 203 | "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", 204 | "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa", 205 | "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", 206 | "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", 207 | "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", 208 | "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", 209 | "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", 210 | "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", 211 | "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02", 212 | "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", 213 | "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", 214 | "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", 215 | "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", 216 | "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", 217 | "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", 218 | "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", 219 | "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681", 220 | "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", 221 | "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", 222 | "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a", 223 | "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", 224 | "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", 225 | "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", 226 | "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", 227 | "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027", 228 | "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", 229 | "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", 230 | "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", 231 | "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", 232 | "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", 233 | "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", 234 | "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da", 235 | "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", 236 | "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f", 237 | "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", 238 | "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f" 239 | ], 240 | "markers": "python_version >= '3.7'", 241 | "version": "==3.4.2" 242 | }, 243 | "colorama": { 244 | "hashes": [ 245 | "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", 246 | "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" 247 | ], 248 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", 249 | "version": "==0.4.6" 250 | }, 251 | "distlib": { 252 | "hashes": [ 253 | "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", 254 | "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403" 255 | ], 256 | "version": "==0.3.9" 257 | }, 258 | "docutils": { 259 | "hashes": [ 260 | "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", 261 | "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2" 262 | ], 263 | "markers": "python_version >= '3.9'", 264 | "version": "==0.21.2" 265 | }, 266 | "exceptiongroup": { 267 | "hashes": [ 268 | "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", 269 | "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" 270 | ], 271 | "markers": "python_version >= '3.7'", 272 | "version": "==1.2.2" 273 | }, 274 | "grpcio": { 275 | "hashes": [ 276 | "sha256:0ab8b2864396663a5b0b0d6d79495657ae85fa37dcb6498a2669d067c65c11ea", 277 | "sha256:0fa05ee31a20456b13ae49ad2e5d585265f71dd19fbd9ef983c28f926d45d0a7", 278 | "sha256:0ff35c8d807c1c7531d3002be03221ff9ae15712b53ab46e2a0b4bb271f38537", 279 | "sha256:1be857615e26a86d7363e8a163fade914595c81fec962b3d514a4b1e8760467b", 280 | "sha256:20e8f653abd5ec606be69540f57289274c9ca503ed38388481e98fa396ed0b41", 281 | "sha256:22c3bc8d488c039a199f7a003a38cb7635db6656fa96437a8accde8322ce2366", 282 | "sha256:24e867651fc67717b6f896d5f0cac0ec863a8b5fb7d6441c2ab428f52c651c6b", 283 | "sha256:2b85f7820475ad3edec209d3d89a7909ada16caab05d3f2e08a7e8ae3200a55c", 284 | "sha256:39983a9245d37394fd59de71e88c4b295eb510a3555e0a847d9965088cdbd033", 285 | "sha256:3d081e859fb1ebe176de33fc3adb26c7d46b8812f906042705346b314bde32c3", 286 | "sha256:469f42a0b410883185eab4689060a20488a1a0a00f8bbb3cbc1061197b4c5a79", 287 | "sha256:47be9584729534660416f6d2a3108aaeac1122f6b5bdbf9fd823e11fe6fbaa29", 288 | "sha256:4be74ddeeb92cc87190e0e376dbc8fc7736dbb6d3d454f2fa1f5be1dee26b9d7", 289 | "sha256:4dd0dfbe4d5eb1fcfec9490ca13f82b089a309dc3678e2edabc144051270a66e", 290 | "sha256:5b08d03ace7aca7b2fadd4baf291139b4a5f058805a8327bfe9aece7253b6d67", 291 | "sha256:63e41b91032f298b3e973b3fa4093cbbc620c875e2da7b93e249d4728b54559a", 292 | "sha256:652350609332de6dac4ece254e5d7e1ff834e203d6afb769601f286886f6f3a8", 293 | "sha256:693bc706c031aeb848849b9d1c6b63ae6bcc64057984bb91a542332b75aa4c3d", 294 | "sha256:74258dce215cb1995083daa17b379a1a5a87d275387b7ffe137f1d5131e2cfbb", 295 | "sha256:789d5e2a3a15419374b7b45cd680b1e83bbc1e52b9086e49308e2c0b5bbae6e3", 296 | "sha256:7c9c80ac6091c916db81131d50926a93ab162a7e97e4428ffc186b6e80d6dda4", 297 | "sha256:7d6ac9481d9d0d129224f6d5934d5832c4b1cddb96b59e7eba8416868909786a", 298 | "sha256:85da336e3649a3d2171e82f696b5cad2c6231fdd5bad52616476235681bee5b3", 299 | "sha256:8700a2a57771cc43ea295296330daaddc0d93c088f0a35cc969292b6db959bf3", 300 | "sha256:8997d6785e93308f277884ee6899ba63baafa0dfb4729748200fcc537858a509", 301 | "sha256:9182e0063112e55e74ee7584769ec5a0b4f18252c35787f48738627e23a62b97", 302 | "sha256:9b91879d6da1605811ebc60d21ab6a7e4bae6c35f6b63a061d61eb818c8168f6", 303 | "sha256:a2242d6950dc892afdf9e951ed7ff89473aaf744b7d5727ad56bdaace363722b", 304 | "sha256:a371e6b6a5379d3692cc4ea1cb92754d2a47bdddeee755d3203d1f84ae08e03e", 305 | "sha256:a76d39b5fafd79ed604c4be0a869ec3581a172a707e2a8d7a4858cb05a5a7637", 306 | "sha256:ad9f30838550695b5eb302add33f21f7301b882937460dd24f24b3cc5a95067a", 307 | "sha256:b2266862c5ad664a380fbbcdbdb8289d71464c42a8c29053820ee78ba0119e5d", 308 | "sha256:b78a99cd1ece4be92ab7c07765a0b038194ded2e0a26fd654591ee136088d8d7", 309 | "sha256:c200cb6f2393468142eb50ab19613229dcc7829b5ccee8b658a36005f6669fdd", 310 | "sha256:c30f393f9d5ff00a71bb56de4aa75b8fe91b161aeb61d39528db6b768d7eac69", 311 | "sha256:c6a0a28450c16809f94e0b5bfe52cabff63e7e4b97b44123ebf77f448534d07d", 312 | "sha256:cebc1b34ba40a312ab480ccdb396ff3c529377a2fce72c45a741f7215bfe8379", 313 | "sha256:d2c170247315f2d7e5798a22358e982ad6eeb68fa20cf7a820bb74c11f0736e7", 314 | "sha256:d35a95f05a8a2cbe8e02be137740138b3b2ea5f80bd004444e4f9a1ffc511e32", 315 | "sha256:d5170929109450a2c031cfe87d6716f2fae39695ad5335d9106ae88cc32dc84c", 316 | "sha256:d6aa986318c36508dc1d5001a3ff169a15b99b9f96ef5e98e13522c506b37eef", 317 | "sha256:d6de81c9c00c8a23047136b11794b3584cdc1460ed7cbc10eada50614baa1444", 318 | "sha256:dc1a1231ed23caac1de9f943d031f1bc38d0f69d2a3b243ea0d664fc1fbd7fec", 319 | "sha256:e6beeea5566092c5e3c4896c6d1d307fb46b1d4bdf3e70c8340b190a69198594", 320 | "sha256:e6d8de076528f7c43a2f576bc311799f89d795aa6c9b637377cc2b1616473804", 321 | "sha256:e6f83a583ed0a5b08c5bc7a3fe860bb3c2eac1f03f1f63e0bc2091325605d2b7", 322 | "sha256:f250ff44843d9a0615e350c77f890082102a0318d66a99540f54769c8766ab73", 323 | "sha256:f71574afdf944e6652203cd1badcda195b2a27d9c83e6d88dc1ce3cfb73b31a5", 324 | "sha256:f903017db76bf9cc2b2d8bdd37bf04b505bbccad6be8a81e1542206875d0e9db", 325 | "sha256:f9a412f55bb6e8f3bb000e020dbc1e709627dcb3a56f6431fa7076b4c1aab0db", 326 | "sha256:f9c30c464cb2ddfbc2ddf9400287701270fdc0f14be5f08a1e3939f1e749b455" 327 | ], 328 | "index": "pypi", 329 | "markers": "python_version >= '3.9'", 330 | "version": "==1.71.0" 331 | }, 332 | "grpcio-tools": { 333 | "hashes": [ 334 | "sha256:072b2a5805ac97e4623b3aa8f7818275f3fb087f4aa131b0fce00471065f6eaa", 335 | "sha256:0ccfb837152b7b858b9f26bb110b3ae8c46675d56130f6c2f03605c4f129be13", 336 | "sha256:0e647794bd7138b8c215e86277a9711a95cf6a03ff6f9e555d54fdf7378b9f9d", 337 | "sha256:1291a6136c07a86c3bb09f6c33f5cf227cc14956edd1b85cb572327a36e0aef8", 338 | "sha256:1331e726e08b7bdcbf2075fcf4b47dff07842b04845e6e220a08a4663e232d7f", 339 | "sha256:145985c0bf12131f0a1503e65763e0f060473f7f3928ed1ff3fb0e8aad5bc8ac", 340 | "sha256:192808cf553cedca73f0479cc61d5684ad61f24db7a5f3c4dfe1500342425866", 341 | "sha256:1f19b16b49afa5d21473f49c0966dd430c88d089cd52ac02404d8cef67134efb", 342 | "sha256:28784f39921d061d2164a9dcda5164a69d07bf29f91f0ea50b505958292312c9", 343 | "sha256:2a1712f12102b60c8d92779b89d0504e0d6f3a59f2b933e5622b8583f5c02992", 344 | "sha256:3059c14035e5dc03d462f261e5900b9a077fd1a36976c3865b8507474520bad4", 345 | "sha256:38dba8e0d5e0fb23a034e09644fdc6ed862be2371887eee54901999e8f6792a8", 346 | "sha256:41878cb7a75477e62fdd45e7e9155b3af1b7a5332844021e2511deaf99ac9e6c", 347 | "sha256:459c8f5e00e390aecd5b89de67deb3ec7188a274bc6cb50e43cef35ab3a3f45d", 348 | "sha256:48debc879570972d28bfe98e4970eff25bb26da3f383e0e49829b2d2cd35ad87", 349 | "sha256:541a756276c8a55dec991f6c0106ae20c8c8f5ce8d0bdbfcb01e2338d1a8192b", 350 | "sha256:56ecd6cc89b5e5eed1de5eb9cafce86c9c9043ee3840888cc464d16200290b53", 351 | "sha256:57e3e2544c306b60ef2d76570bac4e977be1ad548641c9eec130c3bc47e80141", 352 | "sha256:580ac88141c9815557e63c9c04f5b1cdb19b4db8d0cb792b573354bde1ee8b12", 353 | "sha256:61c0409d5bdac57a7bd0ce0ab01c1c916728fe4c8a03d77a25135ad481eb505c", 354 | "sha256:64bdb291df61cf570b5256777ad5fe2b1db6d67bc46e55dc56a0a862722ae329", 355 | "sha256:65aa082f4435571d65d5ce07fc444f23c3eff4f3e34abef599ef8c9e1f6f360f", 356 | "sha256:6693a7d3ba138b0e693b3d1f687cdd9db9e68976c3fa2b951c17a072fea8b583", 357 | "sha256:682e958b476049ccc14c71bedf3f979bced01f6e0c04852efc5887841a32ad6b", 358 | "sha256:6ae5f2efa9e644c10bf1021600bfc099dfbd8e02b184d2d25dc31fcd6c2bc59e", 359 | "sha256:6d11ed3ff7b6023b5c72a8654975324bb98c1092426ba5b481af406ff559df00", 360 | "sha256:753270e2d06d37e6d7af8967d1d059ec635ad215882041a36294f4e2fd502b2e", 361 | "sha256:77fe6db1334e0ce318b2cb4e70afa94e0c173ed1a533d37aea69ad9f61ae8ea9", 362 | "sha256:82c430edd939bb863550ee0fecf067d78feff828908a1b529bbe33cc57f2419c", 363 | "sha256:834959b6eceb85de5217a411aba1643b5f782798680c122202d6a06177226644", 364 | "sha256:83e90724e3f02415c628e4ead1d6ffe063820aaaa078d9a39176793df958cd5a", 365 | "sha256:870c0097700d13c403e5517cb7750ab5b4a791ce3e71791c411a38c5468b64bd", 366 | "sha256:8b93b9f6adc7491d4c10144c0643409db298e5e63c997106a804f6f0248dbaf4", 367 | "sha256:8dd9795e982d77a4b496f7278b943c2563d9afde2069cdee78c111a40cc4d675", 368 | "sha256:8e6cdbba4dae7b37b0d25d074614be9936fb720144420f03d9f142a80be69ba2", 369 | "sha256:8f987d0053351217954543b174b0bddbf51d45b3cfcf8d6de97b0a43d264d753", 370 | "sha256:989ee9da61098230d3d4c8f8f8e27c2de796f1ff21b1c90110e636d9acd9432b", 371 | "sha256:9a78d07d6c301a25ef5ede962920a522556a1dfee1ccc05795994ceb867f766c", 372 | "sha256:abd57f615e88bf93c3c6fd31f923106e3beb12f8cd2df95b0d256fa07a7a0a57", 373 | "sha256:af39e245fa56f7f5c2fe86b7d6c1b78f395c07e54d5613cbdbb3c24769a92b6e", 374 | "sha256:bfe3888c3bbe16a5aa39409bc38744a31c0c3d2daa2b0095978c56e106c85b42", 375 | "sha256:c1b5860c41a36b26fec4f52998f1a451d0525a5c9a4fb06b6ea3e9211abdb925", 376 | "sha256:d3adc8b229e60c77bab5a5d62b415667133bd5ced7d59b5f71d6317c9143631e", 377 | "sha256:e3ae9556e2a1cd70e7d7b0e0459c35af71d51a7dae4cf36075068011a69f13ec", 378 | "sha256:e52a041afc20ab2431d756b6295d727bd7adee813b21b06a3483f4a7a15ea15f", 379 | "sha256:edab7e6518de01196be37f96cb1e138c3819986bf5e2a6c9e1519b4d716b2f5a", 380 | "sha256:f360981b215b1d5aff9235b37e7e1826246e35bbac32a53e41d4e990a37b8f4c", 381 | "sha256:f4ad7f0d756546902597053d70b3af2606fbd70d7972876cd75c1e241d22ae00", 382 | "sha256:f68334d28a267fabec6e70cb5986e9999cfbfd14db654094ddf9aedd804a293a", 383 | "sha256:f7c678e68ece0ae908ecae1c4314a0c2c7f83e26e281738b9609860cc2c82d96", 384 | "sha256:ffff9bc5eacb34dd26b487194f7d44a3e64e752fc2cf049d798021bf25053b87" 385 | ], 386 | "index": "pypi", 387 | "markers": "python_version >= '3.9'", 388 | "version": "==1.71.0" 389 | }, 390 | "idna": { 391 | "hashes": [ 392 | "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", 393 | "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" 394 | ], 395 | "markers": "python_version >= '3.6'", 396 | "version": "==3.10" 397 | }, 398 | "importlib-metadata": { 399 | "hashes": [ 400 | "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", 401 | "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd" 402 | ], 403 | "markers": "python_version >= '3.9'", 404 | "version": "==8.7.0" 405 | }, 406 | "iniconfig": { 407 | "hashes": [ 408 | "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", 409 | "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760" 410 | ], 411 | "markers": "python_version >= '3.8'", 412 | "version": "==2.1.0" 413 | }, 414 | "jaraco.classes": { 415 | "hashes": [ 416 | "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", 417 | "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" 418 | ], 419 | "markers": "python_version >= '3.8'", 420 | "version": "==3.4.0" 421 | }, 422 | "jaraco.context": { 423 | "hashes": [ 424 | "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", 425 | "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4" 426 | ], 427 | "markers": "python_version >= '3.8'", 428 | "version": "==6.0.1" 429 | }, 430 | "jaraco.functools": { 431 | "hashes": [ 432 | "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", 433 | "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649" 434 | ], 435 | "markers": "python_version >= '3.8'", 436 | "version": "==4.1.0" 437 | }, 438 | "keyring": { 439 | "hashes": [ 440 | "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", 441 | "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd" 442 | ], 443 | "markers": "python_version >= '3.9'", 444 | "version": "==25.6.0" 445 | }, 446 | "markdown-it-py": { 447 | "hashes": [ 448 | "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", 449 | "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" 450 | ], 451 | "markers": "python_version >= '3.8'", 452 | "version": "==3.0.0" 453 | }, 454 | "mdurl": { 455 | "hashes": [ 456 | "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", 457 | "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" 458 | ], 459 | "markers": "python_version >= '3.7'", 460 | "version": "==0.1.2" 461 | }, 462 | "more-itertools": { 463 | "hashes": [ 464 | "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", 465 | "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e" 466 | ], 467 | "markers": "python_version >= '3.9'", 468 | "version": "==10.7.0" 469 | }, 470 | "nh3": { 471 | "hashes": [ 472 | "sha256:087ffadfdcd497658c3adc797258ce0f06be8a537786a7217649fc1c0c60c293", 473 | "sha256:20979783526641c81d2f5bfa6ca5ccca3d1e4472474b162c6256745fbfe31cd1", 474 | "sha256:2a5174551f95f2836f2ad6a8074560f261cf9740a48437d6151fd2d4d7d617ab", 475 | "sha256:31eedcd7d08b0eae28ba47f43fd33a653b4cdb271d64f1aeda47001618348fde", 476 | "sha256:4990e7ee6a55490dbf00d61a6f476c9a3258e31e711e13713b2ea7d6616f670e", 477 | "sha256:55823c5ea1f6b267a4fad5de39bc0524d49a47783e1fe094bcf9c537a37df251", 478 | "sha256:6141caabe00bbddc869665b35fc56a478eb774a8c1dfd6fba9fe1dfdf29e6efa", 479 | "sha256:637d4a10c834e1b7d9548592c7aad760611415fcd5bd346f77fd8a064309ae6d", 480 | "sha256:63ca02ac6f27fc80f9894409eb61de2cb20ef0a23740c7e29f9ec827139fa578", 481 | "sha256:6ae319f17cd8960d0612f0f0ddff5a90700fa71926ca800e9028e7851ce44a6f", 482 | "sha256:6c9c30b8b0d291a7c5ab0967ab200598ba33208f754f2f4920e9343bdd88f79a", 483 | "sha256:713d16686596e556b65e7f8c58328c2df63f1a7abe1277d87625dcbbc012ef82", 484 | "sha256:818f2b6df3763e058efa9e69677b5a92f9bc0acff3295af5ed013da544250d5b", 485 | "sha256:9d67709bc0d7d1f5797b21db26e7a8b3d15d21c9c5f58ccfe48b5328483b685b", 486 | "sha256:a5f77e62aed5c4acad635239ac1290404c7e940c81abe561fd2af011ff59f585", 487 | "sha256:a772dec5b7b7325780922dd904709f0f5f3a79fbf756de5291c01370f6df0967", 488 | "sha256:a7ea28cd49293749d67e4fcf326c554c83ec912cd09cd94aa7ec3ab1921c8283", 489 | "sha256:ac7006c3abd097790e611fe4646ecb19a8d7f2184b882f6093293b8d9b887431", 490 | "sha256:b3b5c58161e08549904ac4abd450dacd94ff648916f7c376ae4b2c0652b98ff9", 491 | "sha256:b8d55ea1fc7ae3633d758a92aafa3505cd3cc5a6e40470c9164d54dff6f96d42", 492 | "sha256:bb0014948f04d7976aabae43fcd4cb7f551f9f8ce785a4c9ef66e6c2590f8629", 493 | "sha256:d002b648592bf3033adfd875a48f09b8ecc000abd7f6a8769ed86b6ccc70c759", 494 | "sha256:d426d7be1a2f3d896950fe263332ed1662f6c78525b4520c8e9861f8d7f0d243", 495 | "sha256:fcff321bd60c6c5c9cb4ddf2554e22772bb41ebd93ad88171bbbb6f271255286" 496 | ], 497 | "markers": "python_version >= '3.8'", 498 | "version": "==0.2.21" 499 | }, 500 | "orderedmultidict": { 501 | "hashes": [ 502 | "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad", 503 | "sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3" 504 | ], 505 | "version": "==1.0.1" 506 | }, 507 | "packaging": { 508 | "hashes": [ 509 | "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", 510 | "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" 511 | ], 512 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 513 | "version": "==20.9" 514 | }, 515 | "pep517": { 516 | "hashes": [ 517 | "sha256:1b2fa2ffd3938bb4beffe5d6146cbcb2bda996a5a4da9f31abffd8b24e07b317", 518 | "sha256:31b206f67165b3536dd577c5c3f1518e8fbaf38cbc57efff8369a392feff1721" 519 | ], 520 | "markers": "python_version >= '3.6'", 521 | "version": "==0.13.1" 522 | }, 523 | "pip": { 524 | "hashes": [ 525 | "sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af", 526 | "sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077" 527 | ], 528 | "markers": "python_version >= '3.9'", 529 | "version": "==25.1.1" 530 | }, 531 | "pip-shims": { 532 | "hashes": [ 533 | "sha256:089e3586a92b1b8dbbc16b2d2859331dc1c412d3e3dbcd91d80e6b30d73db96c", 534 | "sha256:2ae9f21c0155ca5c37d2734eb5f9a7d98c4c42a122d1ba3eddbacc9d9ea9fbae" 535 | ], 536 | "markers": "python_version >= '3.6'", 537 | "version": "==0.7.3" 538 | }, 539 | "pipenv-setup": { 540 | "hashes": [ 541 | "sha256:0def7ec3363f58b38a43dc59b2078fcee67b47301fd51a41b8e34e6f79812b1a", 542 | "sha256:6ceda7145a3088494d8ca68fded4b0473022dc62eb786a021c137632c44298b5" 543 | ], 544 | "index": "pypi", 545 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2' and python_version < '4'", 546 | "version": "==3.2.0" 547 | }, 548 | "pipfile": { 549 | "hashes": [ 550 | "sha256:f7d9f15de8b660986557eb3cc5391aa1a16207ac41bc378d03f414762d36c984" 551 | ], 552 | "version": "==0.0.2" 553 | }, 554 | "pkginfo": { 555 | "hashes": [ 556 | "sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297", 557 | "sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097" 558 | ], 559 | "markers": "python_version >= '3.6'", 560 | "version": "==1.10.0" 561 | }, 562 | "platformdirs": { 563 | "hashes": [ 564 | "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", 565 | "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351" 566 | ], 567 | "markers": "python_version >= '3.9'", 568 | "version": "==4.3.7" 569 | }, 570 | "plette": { 571 | "extras": [ 572 | "validation" 573 | ], 574 | "hashes": [ 575 | "sha256:0e9898513eacbcf06c6b05e9e042a7733cfb2030335532044b9b3ff84431821c", 576 | "sha256:65cea1259d69339e518481c9f59130cea2a6f712117bee340bc4c1c10e47f9e7" 577 | ], 578 | "markers": "python_version >= '3.7'", 579 | "version": "==2.1.0" 580 | }, 581 | "pluggy": { 582 | "hashes": [ 583 | "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", 584 | "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" 585 | ], 586 | "markers": "python_version >= '3.8'", 587 | "version": "==1.5.0" 588 | }, 589 | "protobuf": { 590 | "hashes": [ 591 | "sha256:13eb236f8eb9ec34e63fc8b1d6efd2777d062fa6aaa68268fb67cf77f6839ad7", 592 | "sha256:1832f0515b62d12d8e6ffc078d7e9eb06969aa6dc13c13e1036e39d73bebc2de", 593 | "sha256:307ecba1d852ec237e9ba668e087326a67564ef83e45a0189a772ede9e854dd0", 594 | "sha256:3fde11b505e1597f71b875ef2fc52062b6a9740e5f7c8997ce878b6009145862", 595 | "sha256:476cb7b14914c780605a8cf62e38c2a85f8caff2e28a6a0bad827ec7d6c85d68", 596 | "sha256:4f1dfcd7997b31ef8f53ec82781ff434a28bf71d9102ddde14d076adcfc78c99", 597 | "sha256:678974e1e3a9b975b8bc2447fca458db5f93a2fb6b0c8db46b6675b5b5346812", 598 | "sha256:aec4962f9ea93c431d5714ed1be1c93f13e1a8618e70035ba2b0564d9e633f2e", 599 | "sha256:bcefcdf3976233f8a502d265eb65ea740c989bacc6c30a58290ed0e519eb4b8d", 600 | "sha256:d7d3f7d1d5a66ed4942d4fefb12ac4b14a29028b209d4bfb25c68ae172059922", 601 | "sha256:fd32223020cb25a2cc100366f1dedc904e2d71d9322403224cdde5fdced0dabe" 602 | ], 603 | "index": "pypi", 604 | "markers": "python_version >= '3.8'", 605 | "version": "==5.29.4" 606 | }, 607 | "pygments": { 608 | "hashes": [ 609 | "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", 610 | "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c" 611 | ], 612 | "markers": "python_version >= '3.8'", 613 | "version": "==2.19.1" 614 | }, 615 | "pyparsing": { 616 | "hashes": [ 617 | "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", 618 | "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" 619 | ], 620 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", 621 | "version": "==2.4.7" 622 | }, 623 | "pytest": { 624 | "hashes": [ 625 | "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", 626 | "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8" 627 | ], 628 | "index": "pypi", 629 | "markers": "python_version >= '3.7'", 630 | "version": "==7.4.4" 631 | }, 632 | "pytest-asyncio": { 633 | "hashes": [ 634 | "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2", 635 | "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3" 636 | ], 637 | "index": "pypi", 638 | "markers": "python_version >= '3.8'", 639 | "version": "==0.23.8" 640 | }, 641 | "pytest-mock": { 642 | "hashes": [ 643 | "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", 644 | "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0" 645 | ], 646 | "index": "pypi", 647 | "markers": "python_version >= '3.8'", 648 | "version": "==3.14.0" 649 | }, 650 | "python-dateutil": { 651 | "hashes": [ 652 | "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", 653 | "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" 654 | ], 655 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", 656 | "version": "==2.9.0.post0" 657 | }, 658 | "readme-renderer": { 659 | "hashes": [ 660 | "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", 661 | "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1" 662 | ], 663 | "markers": "python_version >= '3.9'", 664 | "version": "==44.0" 665 | }, 666 | "requests": { 667 | "hashes": [ 668 | "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", 669 | "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" 670 | ], 671 | "markers": "python_version >= '3.8'", 672 | "version": "==2.32.3" 673 | }, 674 | "requests-toolbelt": { 675 | "hashes": [ 676 | "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", 677 | "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" 678 | ], 679 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 680 | "version": "==1.0.0" 681 | }, 682 | "requirementslib": { 683 | "hashes": [ 684 | "sha256:28924cf11a2fa91adb03f8431d80c2a8c3dc386f1c48fb2be9a58e4c39072354", 685 | "sha256:d26ec6ad45e1ffce9532303543996c9c71a99dc65f783908f112e3f2aae7e49c" 686 | ], 687 | "markers": "python_version >= '3.7'", 688 | "version": "==1.6.9" 689 | }, 690 | "rfc3986": { 691 | "hashes": [ 692 | "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", 693 | "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" 694 | ], 695 | "markers": "python_version >= '3.7'", 696 | "version": "==2.0.0" 697 | }, 698 | "rich": { 699 | "hashes": [ 700 | "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", 701 | "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725" 702 | ], 703 | "markers": "python_full_version >= '3.8.0'", 704 | "version": "==14.0.0" 705 | }, 706 | "setuptools": { 707 | "hashes": [ 708 | "sha256:4d3c92fac8f1118bb77a22181355e29c239cabfe2b9effdaa665c66b711136d7", 709 | "sha256:8ab4f1dbf2b4a65f7eec5ad0c620e84c34111a68d3349833494b9088212214dd" 710 | ], 711 | "index": "pypi", 712 | "markers": "python_version >= '3.7'", 713 | "version": "==65.7.0" 714 | }, 715 | "six": { 716 | "hashes": [ 717 | "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", 718 | "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" 719 | ], 720 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", 721 | "version": "==1.17.0" 722 | }, 723 | "toml": { 724 | "hashes": [ 725 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 726 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 727 | ], 728 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", 729 | "version": "==0.10.2" 730 | }, 731 | "tomli": { 732 | "hashes": [ 733 | "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", 734 | "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", 735 | "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", 736 | "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", 737 | "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", 738 | "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", 739 | "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", 740 | "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", 741 | "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", 742 | "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", 743 | "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", 744 | "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", 745 | "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", 746 | "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", 747 | "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", 748 | "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", 749 | "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", 750 | "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", 751 | "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", 752 | "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", 753 | "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", 754 | "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", 755 | "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", 756 | "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", 757 | "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", 758 | "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", 759 | "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", 760 | "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", 761 | "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", 762 | "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", 763 | "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", 764 | "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7" 765 | ], 766 | "markers": "python_version >= '3.8'", 767 | "version": "==2.2.1" 768 | }, 769 | "tomlkit": { 770 | "hashes": [ 771 | "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", 772 | "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79" 773 | ], 774 | "markers": "python_version >= '3.8'", 775 | "version": "==0.13.2" 776 | }, 777 | "twine": { 778 | "hashes": [ 779 | "sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997", 780 | "sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db" 781 | ], 782 | "index": "pypi", 783 | "markers": "python_version >= '3.8'", 784 | "version": "==5.1.1" 785 | }, 786 | "typing-extensions": { 787 | "hashes": [ 788 | "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", 789 | "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef" 790 | ], 791 | "markers": "python_version >= '3.8'", 792 | "version": "==4.13.2" 793 | }, 794 | "urllib3": { 795 | "hashes": [ 796 | "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", 797 | "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813" 798 | ], 799 | "markers": "python_version >= '3.9'", 800 | "version": "==2.4.0" 801 | }, 802 | "vistir": { 803 | "hashes": [ 804 | "sha256:1a89a612fb667c26ed6b4ed415b01e0261e13200a350c43d1990ace0ef44d35b", 805 | "sha256:a8beb7643d07779cdda3941a08dad77d48de94883dbd3cb2b9b5ecb7eb7c0994" 806 | ], 807 | "index": "pypi", 808 | "markers": "python_version not in '3.0, 3.1, 3.2, 3.3' and python_version >= '3.7'", 809 | "version": "==0.6.1" 810 | }, 811 | "wheel": { 812 | "hashes": [ 813 | "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", 814 | "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248" 815 | ], 816 | "markers": "python_version >= '3.8'", 817 | "version": "==0.45.1" 818 | }, 819 | "zipp": { 820 | "hashes": [ 821 | "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", 822 | "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931" 823 | ], 824 | "markers": "python_version >= '3.9'", 825 | "version": "==3.21.0" 826 | } 827 | } 828 | } 829 | --------------------------------------------------------------------------------