Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# CHANGELOG

## 1.4.7 - 2026-08-10

- Support new region ap-southeast-3 (Indonesia)
- Support scan gateway for FSCS and FSVA
- Add an Error Handling section to the README documenting SDK and service error codes and messages

## 1.4.6 - 2026-06-24

- `scan_file` now reports the file path you pass as the scan identifier, instead of only
its basename. Scan results (e.g. `foundMalwares[].fileName`) reflect the full path.

## 1.4.5 - 2026-03-18

- Support new region af-south-1
Expand Down
67 changes: 65 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ Creates a new instance of the grpc Channel, and provisions essential settings, i

| Parameter | Description |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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`, `ca-central-1`, `eu-west-2`, `af-south-1`,etc. |
| 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`, `ca-central-1`, `eu-west-2`, `af-south-1`, `ap-southeast-3`,etc. |
| api_key | Your own Vision One API Key. |
| 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. |
| ca_cert | `Optional` CA certificate used to connect to self hosted AMaaS server. |
Expand All @@ -216,7 +216,7 @@ Creates a new instance of the grpc aio Channel, and provisions essential setting

| Parameter | Description |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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`, `ca-central-1`, `eu-west-2`, `af-south-1`, etc. |
| 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`, `ca-central-1`, `eu-west-2`, `af-south-1`, `ap-southeast-3`, etc. |
| api_key | Your own Vision One API Key. |
| 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. |
| ca_cert | `Optional` CA certificate used to connect to self hosted AMaaS server. |
Expand Down Expand Up @@ -286,6 +286,69 @@ Remember to clean up the grpc aio Channel when you are done using it to release
| --------- | ------------------------------------------------------------- |
| handle | The grpc aio Channel instance created from the init function. |

## Error Handling

The File Security Python SDK raises `amaas.grpc.exception.AMaasException` for every error condition produced by `amaas.grpc` and `amaas.grpc.aio`. Each exception carries an `error_code` (a member of the `AMaasErrorCode` enum) and a formatted `message`; `str(exception)` renders both as `<CODE_NAME>: <message>`.

```python
from amaas.grpc.exception import AMaasException

try:
result = amaas.grpc.scan_file(handle, file_name=filename, tags=tags)
except AMaasException as e:
print(e.error_code, e.message)
except Exception as e:
print(e)
```

Some conditions are detected by the SDK itself, independently of any network call: an unsupported region, a missing/unreadable file, invalid tags, or an unexpected message in the scan protocol stream. Everything else comes from the File Security service: the SDK catches `grpc.RpcError` (`grpc.aio.AioRpcError` in the asyncio client) and re-raises it as an `AMaasException`.

For most service errors the SDK preserves the gRPC status code and message text exactly as sent by the service; for two conditions — authentication failures and rate limiting — the SDK discards the service's own message and substitutes a fixed string of its own. Both the synchronous client and the asyncio client apply the same mapping.

The **Source** column classifies each error:

- **SDK-native** — produced entirely by the SDK on the client side, without a network call (unsupported region, missing/unreadable file, invalid tags, unexpected protocol message).
- **SDK-mapped** — triggered by a gRPC response from the service, but the caller-visible message is a fixed string produced by the SDK (authentication failures and rate limiting).
- **Service** — the service's gRPC code and message are relayed to the caller unchanged (via `MSG_ID_GRPC_ERROR`).

| gRPC status code (as received) | `AMaasErrorCode` / message seen by the caller | Cause | Source |
| --- | --- | --- | --- |
| — | `MSG_ID_ERR_INVALID_REGION`: `<region> is not a supported region, region value should be one of <list>` | Region passed to `init_by_region` is not a supported Vision One region | SDK-native |
| — | `MSG_ID_ERR_FILE_NOT_FOUND`: `Failed to open file. No such file or directory <path>.` | File passed to `scan_file` does not exist | SDK-native |
| — | `MSG_ID_ERR_FILE_NO_PERMISSION`: `Failed to open file. Permission denied to open <path>.` | No OS permission to read the file | SDK-native |
| — | `MSG_ID_ERR_INVALID_TAG`: `Invalid tag format: <tag>.` | A tag is empty or longer than 63 characters, checked client-side before the scan request is sent | SDK-native |
| — | `MSG_ID_ERR_TAG_NUMBER_EXCEED`: `Too many tags: <n>.` | More than 8 tags supplied, checked client-side before the scan request is sent | SDK-native |
| — | `MSG_ID_ERR_UNKNOWN_CMD` / `MSG_ID_ERR_UNKNOWN_STAGE` / `MSG_ID_ERR_UNEXPECTED_CMD_AND_STAGE`: `Received unknown command from server: <n>` / `Received unknown stage from server: <n>` / `Received unexpected command <n> and stage <n>.` | The scan protocol stream produced a command/stage the SDK does not recognize | SDK-native |
| — | `MSG_ID_ERR_UNEXPECTED_ERROR`: `Unexpected error encountered. <detail>` | Any other, non-gRPC exception raised while scanning (also used internally if an unsupported hash algorithm is requested) | SDK-native |
| `Unauthenticated` (16) | `MSG_ID_ERR_KEY_AUTH_FAILED`: `Invalid token or Api Key.` | Service rejected the request as `Unauthenticated` — covers a missing key, an invalid/expired key, and an account without file-scan permission. The SDK substitutes this fixed string for all three cases; the service's actual message is not shown to the caller | SDK-mapped |
| `Internal` (13), details containing `429` ¹ | `MSG_ID_ERR_RATE_LIMIT_EXCEEDED`: `Raised by the SDK library to indicate http 429 too many request error.` | Rate limit exceeded. The SDK detects this by scanning the raw error text for the substring `429` and substitutes this fixed string; the service's actual message is not shown to the caller | SDK-mapped |
| `InvalidArgument` (3) | `MSG_ID_GRPC_ERROR`: `Too many tags. Decrease to eight tags or less.` | Too many tags (only reaches the service if the SDK's own client-side check didn't already catch it) | Service |
| `InvalidArgument` (3) | `MSG_ID_GRPC_ERROR`: `Tag is too long. Decrease length to 63 characters or less.` | A tag longer than 63 characters (only reaches the service if the client-side check didn't already catch it) | Service |
| `InvalidArgument` (3) | `MSG_ID_GRPC_ERROR`: `Tag is empty. Remove the tag or add at least one character.` | An empty tag | Service |
| `InvalidArgument` (3) | `MSG_ID_GRPC_ERROR`: `cloudAccountId contains illegal characters (#, @)` | Illegal characters in a `cloudAccountId` tag | Service |
| `InvalidArgument` (3) | `MSG_ID_GRPC_ERROR`: `Prefix format or length of SHA1 from the SDK is incorrect. Contact Support.` / `SHA1 format from the SDK is incorrect. Contact Support.` | Malformed SHA1 digest sent by the SDK | Service |
| `InvalidArgument` (3) | `MSG_ID_GRPC_ERROR`: `Prefix format or length of SHA256 from the SDK is incorrect. Contact Support.` / `SHA256 format from the SDK is incorrect. Contact Support.` | Malformed SHA256 digest sent by the SDK | Service |
| `NotFound` (5) | `MSG_ID_GRPC_ERROR`: `Customer ID not found. Contact Support.` | Account / customer ID not found | Service |
| `PermissionDenied` (7) | `MSG_ID_GRPC_ERROR`: `SDK feature is not enabled for this account. Contact your administrator to enable the SDK feature.` | The SDK feature is not enabled for the account | Service |
| `ResourceExhausted` (8) | `MSG_ID_GRPC_ERROR`: `This account has performed five scans in the last hour. Purchase and allocate credits to File Security or wait an hour to make five more scans.` | Hourly scan quota exhausted (Essential accounts) | Service |
| `ResourceExhausted` (8) | `MSG_ID_GRPC_ERROR`: `file size <n> is over maximum allowed size <m>` | Scanned file/buffer exceeds the maximum allowed size | Service |
| `ResourceExhausted` (8) | `MSG_ID_GRPC_ERROR`: `Cannot allocate resource. Try again later. If the issue persists, contact Support.` | Service could not allocate a scan resource | Service |
| `FailedPrecondition` (9) | `MSG_ID_GRPC_ERROR`: `Incorrect stage <n> from the SDK. Contact Support.` | Incorrect protocol stage | Service |
| `Unimplemented` (12) | `MSG_ID_GRPC_ERROR`: `Predictive Machine Learning is not supported. Contact Support.` | PML requested but not supported for the account/region | Service |
| `Internal` (13) | `MSG_ID_GRPC_ERROR`: `Failed to retrieve metadata. Try again later. If the issue persists, contact Support.` | Service could not retrieve request metadata | Service |
| `Internal` (13) | `MSG_ID_GRPC_ERROR`: `Network connection error. Try again later. If the issue persists, contact Support.` | Service-side network / connection error | Service |
| `Internal` (13) | `MSG_ID_GRPC_ERROR`: `Internal error. Try again later. If the issue persists, contact Support.` | Generic internal service error | Service |
| `Internal` (13) | `MSG_ID_GRPC_ERROR`: `Missing preamble information from the SDK. Contact Support.` | Missing preamble information in the scan request | Service |
| `Internal` (13) | `MSG_ID_GRPC_ERROR`: `Unclear scan result: <detail>. Contact Support.` | Service could not parse the scan result | Service |
| any other code (preserved) | `MSG_ID_GRPC_ERROR`: `Received gRPC status code: <code>, msg: <details>.` | Any other error relayed from the service, with the numeric gRPC code and message exactly as the service sent them | Service |

**Notes**

1. The service currently signals rate limiting with `Internal` and a details string containing `Http Error Code: 429`; the SDK matches on that substring rather than on a dedicated gRPC code, so this row is reached before the generic `MSG_ID_GRPC_ERROR` case below.
2. Rows marked `MSG_ID_GRPC_ERROR` are relayed from the service with the gRPC status code and message preserved exactly; `str(exception)` renders them as `MSG_ID_GRPC_ERROR: Received gRPC status code: <code>, msg: <message>.`. These messages are owned by the File Security service and may change independently of the SDK.
The Python SDK does not expose Encode/Decode operations, so the service's encode/decode messages cannot occur here.
3. Engine findings such as `ATSE_*` codes are not errors — they are returned inside a successful scan result payload, not raised as an exception.

## Environment Variables

The following environment variables are supported by Python Client SDK and can be used in lieu of values specified as function arguments.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.4.5
1.4.7
17 changes: 13 additions & 4 deletions amaas/grpc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
logger.propagate = False

timeout_in_seconds = int(os.environ.get('TM_AM_SCAN_TIMEOUT_SECS', 300))
heartbeat_interval_in_seconds = int(os.environ.get('TM_AM_HEARTBEAT_INTERVAL_SECS', 30))


class _Pipeline:
Expand All @@ -36,8 +37,10 @@ def __init__(self):
self._consumer_lock = threading.Lock()
self._consumer_lock.acquire()

def get_message(self):
self._consumer_lock.acquire()
def get_message(self, timeout=None):
acquired = self._consumer_lock.acquire(timeout=timeout)
if not acquired:
return None
message = self._message
self._producer_lock.release()
return message
Expand Down Expand Up @@ -81,7 +84,13 @@ def _generate_messages(pipeline: _Pipeline, data_reader: BinaryIO, bulk: bool, s
yield response

responses.clear()
message = pipeline.get_message()
while True:
message = pipeline.get_message(timeout=heartbeat_interval_in_seconds)
if message is None:
logger.debug("sending heartbeat to keep connection alive")
yield scan_pb2.C2S(stage=scan_pb2.STAGE_HEARTBEAT)
continue
break

if message.stage == scan_pb2.STAGE_INIT:
logger.debug("stage INIT")
Expand Down Expand Up @@ -191,7 +200,7 @@ def scan_file(channel: grpc.Channel, file_name: str, tags: List[str] = None,
pml: bool = False, feedback: bool = False, verbose: bool = False, digest: bool = True) -> str:
try:
f = open(file_name, "rb")
fid = os.path.basename(file_name)
fid = file_name
n = os.stat(file_name).st_size
except FileNotFoundError as err:
logger.debug("File not exist: " + str(err))
Expand Down
14 changes: 12 additions & 2 deletions amaas/grpc/aio/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import io
import os
from typing import BinaryIO, List
Expand All @@ -22,6 +23,7 @@
logger.propagate = False

timeout_in_seconds = int(os.environ.get('TM_AM_SCAN_TIMEOUT_SECS', 300))
heartbeat_interval_in_seconds = int(os.environ.get('TM_AM_HEARTBEAT_INTERVAL_SECS', 30))


def init_by_region(region, api_key, enable_tls=True, ca_cert=None):
Expand Down Expand Up @@ -75,7 +77,15 @@ async def _scan_data(channel: grpc.Channel, data_reader: BinaryIO, size: int, id
await call.write(request)

while True:
response = await call.read()
while True:
try:
response = await asyncio.wait_for(
call.read(), timeout=heartbeat_interval_in_seconds
)
break
except asyncio.TimeoutError:
logger.debug("sending heartbeat to keep connection alive")
await call.write(scan_pb2.C2S(stage=scan_pb2.STAGE_HEARTBEAT))

if response.cmd == scan_pb2.CMD_RETR:
if response.stage != scan_pb2.STAGE_RUN:
Expand Down Expand Up @@ -149,7 +159,7 @@ async def scan_file(channel: grpc.Channel, file_name: str, tags: List[str] = Non
pml: bool = False, feedback: bool = False, verbose: bool = False, digest: bool = True) -> str:
try:
f = open(file_name, "rb")
fid = os.path.basename(file_name)
fid = file_name
n = os.stat(file_name).st_size
except FileNotFoundError as err:
logger.debug("File not exist: " + str(err))
Expand Down
10 changes: 7 additions & 3 deletions amaas/grpc/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
AWS_GB_REGION = "eu-west-2"
AWS_AE_REGION = "me-central-1"
AWS_ZA_REGION = "af-south-1"
AWS_ID_REGION = "ap-southeast-3"
C1_JP_REGION = "jp-1"
C1_SG_REGION = "sg-1"
C1_AU_REGION = "au-1"
Expand All @@ -32,13 +33,14 @@
C1_GB_REGION = "gb-1"
C1_AE_REGION = "ae-1"
C1_ZA_REGION = "za-1"
C1_ID_REGION = "id-1"

C1Regions = [C1_AU_REGION, C1_CA_REGION, C1_DE_REGION, C1_GB_REGION, C1_IN_REGION, C1_JP_REGION, C1_SG_REGION,
C1_US_REGION, C1_TREND_REGION, C1_ZA_REGION]
V1Regions = [AWS_AU_REGION, AWS_DE_REGION, AWS_IN_REGION, AWS_JP_REGION, AWS_SG_REGION, AWS_US_REGION, AWS_AE_REGION, AWS_CA_REGION, AWS_GB_REGION, AWS_ZA_REGION]
C1_US_REGION, C1_TREND_REGION, C1_ZA_REGION, C1_ID_REGION]
V1Regions = [AWS_AU_REGION, AWS_DE_REGION, AWS_IN_REGION, AWS_JP_REGION, AWS_SG_REGION, AWS_US_REGION, AWS_AE_REGION, AWS_CA_REGION, AWS_GB_REGION, AWS_ZA_REGION, AWS_ID_REGION]
SupportedV1Regions = V1Regions
SupportedC1Regions = [C1_AU_REGION, C1_CA_REGION, C1_DE_REGION, C1_GB_REGION, C1_IN_REGION, C1_JP_REGION, C1_SG_REGION,
C1_US_REGION, C1_ZA_REGION]
C1_US_REGION, C1_ZA_REGION, C1_ID_REGION]

AllRegions = C1Regions + V1Regions
AllValidRegions = SupportedC1Regions + SupportedV1Regions
Expand All @@ -54,6 +56,7 @@
AWS_CA_REGION: C1_CA_REGION,
AWS_GB_REGION: C1_GB_REGION,
AWS_ZA_REGION: C1_ZA_REGION,
AWS_ID_REGION: C1_ID_REGION,
}


Expand Down Expand Up @@ -105,6 +108,7 @@ def _init_by_region_util(region, api_key, enable_tls=True, ca_cert=None, is_aio_
C1_CA_REGION: 'antimalware.ca-1.cloudone.trendmicro.com:443',
C1_AE_REGION: 'antimalware.ae-1.cloudone.trendmicro.com:443',
C1_ZA_REGION: 'antimalware.za-1.cloudone.trendmicro.com:443',
C1_ID_REGION: 'antimalware.id-1.cloudone.trendmicro.com:443',
}

# make sure it is valid V1 or C1 region
Expand Down
Loading
Loading