diff --git a/CHANGELOG.md b/CHANGELOG.md index dbe0be1..01adff6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## 1.6.4 - 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 +- Fix CVE-2026-46340 and CVE-2026-33871 + ## 1.6.3 - 2026-04-13 - Support new regions af-south-1 diff --git a/README.md b/README.md index 2b8e06f..c0b68e3 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ Creates a new instance of the `AmaasClient` class, and provisions essential sett | 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`,`eu-west-2`,`ca-central-1`,`af-south-1`, etc. If host is given, region will be ignored. | +| 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`,`eu-west-2`,`ca-central-1`,`af-south-1`,`ap-southeast-3`, etc. If host is given, region will be ignored. | | host | The host ip address of self hosted AMaaS scanner. Ignore if to use Trend AMaaS service | | apikey | Your own Vision One API Key. | | timeoutInSecs | Timeout to cancel the connection to server in seconds. Valid value is 0, 1, 2, ... ; default to 300 seconds. | @@ -205,7 +205,7 @@ Creates a new instance of the `AmaasClient` class, and provisions essential sett | 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`,`eu-west-2`,`ca-central-1`,`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`,`eu-west-2`,`ca-central-1`,`af-south-1`,`ap-southeast-3` ,etc. | | apikey | Your own Vision One API Key. | | timeoutInSecs | Timeout to cancel the connection to server in seconds. Valid value is 0, 1, 2, ... ; default to 300 seconds. | @@ -367,7 +367,7 @@ public class AMaasScanResultVerbose { ### `AMaasException` -The AMaasException class is the AMaaS SDK exception class. +The AMaasException class is the AMaaS SDK exception class. Every checked error the SDK raises — whether detected locally or relayed from the gRPC server — is thrown as an `AMaasException` carrying an `AMaasErrorCode`. See [Error Handling](#error-handling) below for the full reference of what callers actually receive. ```java public final class AMaasException extends Exception { @@ -381,19 +381,51 @@ public final class AMaasException extends Exception { --- -### `AMaasErrorCode` +## Error Handling -AMaasErrorCode is a enum type containing all the error conditions thrown by the `AMaasException` class. The error conditions are as follows: +Every checked error raised by the SDK is thrown as an `AMaasException`. Call `getErrorCode()` to get the associated `AMaasErrorCode` enum value, and `getMessage()` (inherited from `Exception`) to get the fully-formatted message text. -| Enum Type | Error Message Templates | Description | -| ------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| MSG_ID_ERR_INVALID_REGION | %s is not a supported region. | The region code provided to the AMaasClient constructor is not a valid region. | -| MSG_ID_ERR_MISSING_AUTH | Must provide an API key to use the client. | The API Key provided to the AMaasClient constructor cannot be empty or `null`. | -| MSG_ID_ERR_KEY_AUTH_FAILED | You are not authenticated. Invalid C1 token or Api Key | The API key is invalid. Please make sure a correct Vision One Api key is used. | -| MSG_ID_ERR_FILE_NOT_FOUND | Failed to open file. No such file or directory %s. | The given file cannot be found. Please make sure the file exists. | -| MSG_ID_ERR_FILE_NO_PERMISSION | Failed to open file. Permission denied to open %s. | There is a file access permission issue. Please make sure the SDK has read permission to the file. | -| MSG_ID_GRPC_ERROR | Received gRPC status code: %d, msg: %s. | gRpc error was reported with the status code. For details, please refer to published [gRPC Status Codes](https://grpc.github.io/grpc/core/md_doc_statuscodes.html) | -| MSG_ID_ERR_UNEXPECTED_INTERRUPT | Unexpected interrupt encountered. | An unexpected interrupt signal was received at the client. | +```java +try { + result = client.scanFile(fileName, tags); +} catch (AMaasException e) { + System.out.println("code: " + e.getErrorCode() + ", message: " + e.getMessage()); +} +``` + +The **Source** column classifies each error: + +- **SDK-native** — detected and formatted entirely on the client (bad region, missing key, file I/O, tag validation, TLS setup). +- **SDK-mapped** — triggered by a gRPC response from the service, but the caller-visible message is produced by the SDK. +- **Service** — a message produced by the service and relayed unchanged. **This SDK has no such rows** — it never forwards the service's message text, only the gRPC status code (see below). + +For gRPC errors, the SDK only forwards the **numeric status code and the code's own name** (via [`io.grpc.Status.Code`](https://grpc.github.io/grpc/java/io/grpc/Status.Code.html)) — it does **not** forward the service's descriptive message text, except for `UNAUTHENTICATED`, where it substitutes a fixed message of its own. See note 1 below. + +| AMaasErrorCode | Message returned to the caller | Cause | Source | +|----------------|---------------------------------|-------|--------| +| `MSG_ID_ERR_INVALID_REGION` | ` is not a supported region, region value should be one of ` | The `region` passed to the `AMaasClient` constructor is not a recognized Vision One region | SDK-native | +| `MSG_ID_ERR_MISSING_AUTH` | `Must provide an API key to use the client.` | No API key was supplied to the `AMaasClient` constructor | SDK-native | +| `MSG_ID_ERR_LOAD_SSL_CERT` | `Failed to load SSL certificate.` | The client's TLS trust material (default certificate, or the custom `caCertPath`) could not be loaded | SDK-native | +| `MSG_ID_ERR_FILE_NOT_FOUND` | `Failed to open file. No such file or directory .` | The file passed to `scanFile()` / `AMaasFileReader` does not exist, or could not be opened | SDK-native | +| `MSG_ID_ERR_FILE_NO_PERMISSION` | `Failed to open file. Permission denied to open .` | The SDK process does not have read permission on the file | SDK-native | +| `MSG_ID_ERR_MAX_NUMBER_OF_TAGS` | `Exceeded maximum number of tags: 8` | More than 8 tags were passed to `scanFile()` / `scanBuffer()` / `scanRun()` | SDK-native | +| `MSG_ID_ERR_LENGTH_OF_TAG` | `Tag length must be between 1 and 63: .` | A tag is `null`, empty, or longer than 63 characters | SDK-native | +| `MSG_ID_ERR_UNEXPECTED_INTERRUPT` | `Unexpected interrupt encountered.` | The calling thread was interrupted while waiting for the scan to finish | SDK-native | +| `MSG_ID_ERR_KEY_AUTH_FAILED` | `Authorization key cannot be authenticated.` | The service returned gRPC `UNAUTHENTICATED` (16) — no/invalid API key, or (per the service) the account lacks file-scan permission. The SDK always substitutes this one message for `UNAUTHENTICATED`; the service's more specific reason is not exposed to the caller. | SDK-mapped | +| `MSG_ID_GRPC_ERROR` | `Received gRPC status code: 3, msg: INVALID_ARGUMENT.` | Service reported `INVALID_ARGUMENT` (3) — too many tags, a tag too long or empty, illegal characters in `cloudAccountId`, or a malformed SHA1/SHA256 computed by the SDK | SDK-mapped | +| `MSG_ID_GRPC_ERROR` | `Received gRPC status code: 5, msg: NOT_FOUND.` | Service reported `NOT_FOUND` (5) — customer ID not found | SDK-mapped | +| `MSG_ID_GRPC_ERROR` | `Received gRPC status code: 7, msg: PERMISSION_DENIED.` | Service reported `PERMISSION_DENIED` (7) — the SDK feature is not enabled for the account | SDK-mapped | +| `MSG_ID_GRPC_ERROR` | `Received gRPC status code: 8, msg: RESOURCE_EXHAUSTED.` | Service reported `RESOURCE_EXHAUSTED` (8) — hourly scan quota exceeded, file exceeds the maximum allowed size, or scan resource could not be allocated | SDK-mapped | +| `MSG_ID_GRPC_ERROR` | `Received gRPC status code: 9, msg: FAILED_PRECONDITION.` | Service reported `FAILED_PRECONDITION` (9) — incorrect protocol stage reported by the SDK | SDK-mapped | +| `MSG_ID_GRPC_ERROR` | `Received gRPC status code: 12, msg: UNIMPLEMENTED.` | Service reported `UNIMPLEMENTED` (12) — Predictive Machine Learning (PML) requested but not supported | SDK-mapped | +| `MSG_ID_GRPC_ERROR` | `Received gRPC status code: 13, msg: INTERNAL.` | Service reported `INTERNAL` (13) — metadata retrieval failure, network connection error, generic internal error, missing preamble information, or unclear scan result | SDK-mapped | + +**Notes** + +1. For `MSG_ID_GRPC_ERROR`, the caller sees only the gRPC status code's number and its symbolic name (e.g. `INTERNAL`), **not** the service's descriptive text. Because several distinct service-side conditions share one gRPC code (the Cause column lists the known ones), they are indistinguishable from the exception message alone. Those service strings are not exposed through the Java SDK. +2. Service-side conditions and their code assignments are owned by the File Security service and may change independently of the SDK; this table reflects the catalog current at the time of writing. +3. Engine findings such as `ATSE_*` codes are **not** errors — they are returned inside the scan result payload (see [Sample JSON Response](#sample-json-response)), not as a gRPC status. +4. `MSG_ID_ERR_UNEXPECTED` is defined on `AMaasErrorCode` but is not currently thrown anywhere in the SDK; it is reserved for future use. ## Thread Safety diff --git a/VERSION b/VERSION index 266146b..9edc58b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.6.3 +1.6.4 diff --git a/examples/pom.xml b/examples/pom.xml index ac379b4..5e72e52 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -58,6 +58,17 @@ io.grpc grpc-netty 1.73.0 + + + io.netty + * + + + + + io.netty + netty-all + 4.2.15.Final com.trend diff --git a/examples/s3stream/S3Stream.java b/examples/s3stream/S3Stream.java index e3d0fe1..4192b9d 100644 --- a/examples/s3stream/S3Stream.java +++ b/examples/s3stream/S3Stream.java @@ -99,7 +99,7 @@ public long getLength() { * @throws IOException if read fails */ @Override - public int readBytes(final int offset, final byte[] buff) throws IOException { + public int readBytes(final long offset, final byte[] buff) throws IOException { final ByteBuffer byteBuffer = ByteBuffer.wrap(buff); return readByteRange(offset, byteBuffer); } diff --git a/pom.xml b/pom.xml index fc81d40..86abc11 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.trend file-security-java-sdk - 1.6.3 + 1.6.4 file-security-java-sdk https://github.com/trendmicro/tm-v1-fs-java-sdk @@ -96,7 +96,7 @@ io.netty netty-all - 4.2.9.Final + 4.2.15.Final io.grpc diff --git a/protos/scan.proto b/protos/scan.proto index 547d745..b5b2634 100644 --- a/protos/scan.proto +++ b/protos/scan.proto @@ -22,7 +22,7 @@ message C2S { Stage stage = 1; string file_name = 2; uint64 rs_size = 3; - int32 offset = 4; + int64 offset = 4; bytes chunk = 5; bool trendx = 6; string file_sha1 = 7; @@ -42,10 +42,10 @@ enum Command { message S2C { Stage stage = 1; Command cmd = 2; - int32 offset = 3; + int64 offset = 3; int32 length = 4; string result = 5; - repeated int32 bulk_offset = 6; + repeated int64 bulk_offset = 6; repeated int32 bulk_length = 7; string session_id = 8; } diff --git a/src/main/java/com/trend/cloudone/amaas/AMaasBufferReader.java b/src/main/java/com/trend/cloudone/amaas/AMaasBufferReader.java index e38ce72..4363c4c 100644 --- a/src/main/java/com/trend/cloudone/amaas/AMaasBufferReader.java +++ b/src/main/java/com/trend/cloudone/amaas/AMaasBufferReader.java @@ -35,12 +35,20 @@ public String getIdentifier() { return this.identifier; } - public int readBytes(final int offset, final byte[] buf) throws IOException { + public int readBytes(final long offset, final byte[] buf) throws IOException { + // The in-memory buffer is backed by a Java array, which is indexed by int, so a buffer larger than Integer.MAX_VALUE bytes (~2GiB) + // is not representable here. Casting to int is safe for this implementation; only the on-disk file reader (AMaasFileReader) needs + // the full long range. In normal operation the scan engine never requests an offset beyond the declared buffer length, + // so offset always fits in int. + if (offset < 0 || offset > this.readerBuf.length) { + throw new IOException("offset out of range for buffer reader: " + offset + " (buffer length " + this.readerBuf.length + ")"); + } + int intOffset = (int) offset; int chunkLength = buf.length; - if (chunkLength + offset > this.readerBuf.length) { - chunkLength = this.readerBuf.length - offset; + if (chunkLength + intOffset > this.readerBuf.length) { + chunkLength = this.readerBuf.length - intOffset; } - System.arraycopy(readerBuf, offset, buf, 0, chunkLength); + System.arraycopy(readerBuf, intOffset, buf, 0, chunkLength); return chunkLength; } } diff --git a/src/main/java/com/trend/cloudone/amaas/AMaasClient.java b/src/main/java/com/trend/cloudone/amaas/AMaasClient.java index a09b9fe..a8429f5 100644 --- a/src/main/java/com/trend/cloudone/amaas/AMaasClient.java +++ b/src/main/java/com/trend/cloudone/amaas/AMaasClient.java @@ -422,7 +422,7 @@ public void onNext(final ScanOuterClass.S2C s2cMsg) { return; } java.util.List bulkLength; - java.util.List bulkOffset; + java.util.List bulkOffset; if (this.bulk) { log(Level.FINE, "enter bulk mode"); int bulkCount = s2cMsg.getBulkLengthCount(); @@ -433,17 +433,17 @@ public void onNext(final ScanOuterClass.S2C s2cMsg) { bulkOffset = s2cMsg.getBulkOffsetList(); } else { bulkLength = Arrays.asList(new Integer[]{s2cMsg.getLength()}); - bulkOffset = Arrays.asList(new Integer[]{s2cMsg.getOffset()}); + bulkOffset = Arrays.asList(new Long[]{s2cMsg.getOffset()}); } for (int i = 0; i < bulkLength.size(); i++) { - log(Level.INFO, "Bulk read length={0} at offset={1}", bulkLength.get(i).intValue(), bulkOffset.get(i).intValue()); + log(Level.INFO, "Bulk read length={0} at offset={1}", bulkLength.get(i).intValue(), bulkOffset.get(i).longValue()); byte[] bytes = new byte[bulkLength.get(i).intValue()]; try { - int rtnLength = reader.readBytes(bulkOffset.get(i).intValue(), bytes); + int rtnLength = reader.readBytes(bulkOffset.get(i).longValue(), bytes); ByteString bytestr = ByteString.copyFrom(bytes); this.fetchCount++; this.fetchSize += rtnLength; - ScanOuterClass.C2S request = ScanOuterClass.C2S.newBuilder().setStage(Stage.STAGE_RUN).setChunk(bytestr).setOffset(bulkOffset.get(i).intValue()).build(); + ScanOuterClass.C2S request = ScanOuterClass.C2S.newBuilder().setStage(Stage.STAGE_RUN).setChunk(bytestr).setOffset(bulkOffset.get(i).longValue()).build(); while (!callObserver.isReady()) { try { diff --git a/src/main/java/com/trend/cloudone/amaas/AMaasFileReader.java b/src/main/java/com/trend/cloudone/amaas/AMaasFileReader.java index 90b6466..97fdcb2 100644 --- a/src/main/java/com/trend/cloudone/amaas/AMaasFileReader.java +++ b/src/main/java/com/trend/cloudone/amaas/AMaasFileReader.java @@ -75,7 +75,13 @@ public long getLength() { return this.fileSize; } - public int readBytes(final int offset, final byte[] buff) throws IOException { + public int readBytes(final long offset, final byte[] buff) throws IOException { + // RandomAccessFile.seek already rejects a negative offset and reads past EOF return -1, so this check is not + // strictly required for safety. It is kept for fail-fast symmetry with AMaasBufferReader and to surface a + // clearer message if the scan engine ever requests an offset outside the declared file size. + if (offset < 0 || offset > this.fileSize) { + throw new IOException("offset out of range for file reader: " + offset + " (file size " + this.fileSize + ")"); + } this.randomFile.seek(offset); return this.randomFile.read(buff); } diff --git a/src/main/java/com/trend/cloudone/amaas/AMaasReader.java b/src/main/java/com/trend/cloudone/amaas/AMaasReader.java index 213372d..9d86544 100644 --- a/src/main/java/com/trend/cloudone/amaas/AMaasReader.java +++ b/src/main/java/com/trend/cloudone/amaas/AMaasReader.java @@ -34,7 +34,7 @@ enum HashType { * @param buff byte array to be filled with reader's content. * @return number of bytes read into the buffer. */ - int readBytes(int offset, byte[] buff) throws IOException; + int readBytes(long offset, byte[] buff) throws IOException; /** * Method to return the hashes as a Hex string for the content read by the reader. diff --git a/src/main/java/com/trend/cloudone/amaas/AMaasRegion.java b/src/main/java/com/trend/cloudone/amaas/AMaasRegion.java index e9ba802..1a6e479 100644 --- a/src/main/java/com/trend/cloudone/amaas/AMaasRegion.java +++ b/src/main/java/com/trend/cloudone/amaas/AMaasRegion.java @@ -26,6 +26,7 @@ final class AMaasRegion { static final String AWS_GB_REGION = "eu-west-2"; static final String AWS_AE_REGION = "me-central-1"; static final String AWS_ZA_REGION = "af-south-1"; + static final String AWS_ID_REGION = "ap-southeast-3"; static final String C1_JP_REGION = "jp-1"; static final String C1_SG_REGION = "sg-1"; static final String C1_AU_REGION = "au-1"; @@ -37,11 +38,12 @@ final class AMaasRegion { static final String C1_GB_REGION = "gb-1"; static final String C1_AE_REGION = "ae-1"; static final String C1_ZA_REGION = "za-1"; + static final String C1_ID_REGION = "id-1"; - static final List C1_REGIONS = Arrays.asList(new String[]{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}); - static final List V1_REGIONS = Arrays.asList(new String[]{AWS_AU_REGION, AWS_CA_REGION, AWS_DE_REGION, AWS_GB_REGION, AWS_IN_REGION, AWS_JP_REGION, AWS_SG_REGION, AWS_US_REGION, AWS_AE_REGION, AWS_ZA_REGION}); - static final List SUPPORTED_V1_REGIONS = Arrays.asList(new String[]{AWS_AU_REGION, AWS_DE_REGION, AWS_JP_REGION, AWS_SG_REGION, AWS_US_REGION, AWS_IN_REGION, AWS_AE_REGION, AWS_CA_REGION, AWS_GB_REGION, AWS_ZA_REGION}); - static final List SUPPORTED_C1_REGIONS = Arrays.asList(new String[]{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}); + static final List C1_REGIONS = Arrays.asList(new String[]{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, C1_ID_REGION}); + static final List V1_REGIONS = Arrays.asList(new String[]{AWS_AU_REGION, AWS_CA_REGION, AWS_DE_REGION, AWS_GB_REGION, AWS_IN_REGION, AWS_JP_REGION, AWS_SG_REGION, AWS_US_REGION, AWS_AE_REGION, AWS_ZA_REGION, AWS_ID_REGION}); + static final List SUPPORTED_V1_REGIONS = Arrays.asList(new String[]{AWS_AU_REGION, AWS_DE_REGION, AWS_JP_REGION, AWS_SG_REGION, AWS_US_REGION, AWS_IN_REGION, AWS_AE_REGION, AWS_CA_REGION, AWS_GB_REGION, AWS_ZA_REGION, AWS_ID_REGION}); + static final List SUPPORTED_C1_REGIONS = Arrays.asList(new String[]{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_ID_REGION}); static final Map V1_TO_C1_REGION_MAPPING = new HashMap() { { @@ -55,6 +57,7 @@ final class AMaasRegion { put(AWS_CA_REGION, C1_CA_REGION); put(AWS_GB_REGION, C1_GB_REGION); put(AWS_ZA_REGION, C1_ZA_REGION); + put(AWS_ID_REGION, C1_ID_REGION); } }; @@ -71,6 +74,7 @@ final class AMaasRegion { put(C1_TREND_REGION, "antimalware.trend-us-1.cloudone.trendmicro.com"); put(C1_AE_REGION, "antimalware.ae-1.cloudone.trendmicro.com"); put(C1_ZA_REGION, "antimalware.za-1.cloudone.trendmicro.com"); + put(C1_ID_REGION, "antimalware.id-1.cloudone.trendmicro.com"); } }; diff --git a/src/test/java/com/trend/cloudone/amaas/AMaasRegionTest.java b/src/test/java/com/trend/cloudone/amaas/AMaasRegionTest.java index 12ceb2a..86e029c 100644 --- a/src/test/java/com/trend/cloudone/amaas/AMaasRegionTest.java +++ b/src/test/java/com/trend/cloudone/amaas/AMaasRegionTest.java @@ -25,6 +25,9 @@ public void testGetHostForRegion() { region = AMaasRegion.getServiceFqdn("af-south-1"); assertEquals("antimalware.za-1.cloudone.trendmicro.com", region); + + region = AMaasRegion.getServiceFqdn("ap-southeast-3"); + assertEquals("antimalware.id-1.cloudone.trendmicro.com", region); }