SDK for Java 2.x를 사용한 Amazon Inspector 예제 - AWS SDK for Java 2.x

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

SDK for Java 2.x를 사용한 Amazon Inspector 예제

다음 코드 예제에서는 Amazon Inspector에서를 사용하여 작업을 수행하고 일반적인 시나리오 AWS SDK for Java 2.x 를 구현하는 방법을 보여줍니다.

기본 사항은 서비스 내에서 필수 작업을 수행하는 방법을 보여주는 코드 예제입니다.

작업은 대규모 프로그램에서 발췌한 코드이며 컨텍스트에 맞춰 실행해야 합니다. 작업은 개별 서비스 함수를 직접적으로 호출하는 방법을 보여주며 관련 시나리오의 컨텍스트에 맞는 작업을 볼 수 있습니다.

각 예시에는 전체 소스 코드에 대한 링크가 포함되어 있으며, 여기에서 컨텍스트에 맞춰 코드를 설정하고 실행하는 방법에 대한 지침을 찾을 수 있습니다.

시작하기

다음 코드 예제에서는 를 사용하여 시작하는 방법을 보여 줍니다.

SDK for Java 2.x
참고

GitHub에 더 많은 내용이 있습니다. AWS 코드 예 리포지토리에서 전체 예를 찾고 설정 및 실행하는 방법을 배워보세요.

/** * Before running this Java V2 code example, set up your development * environment, including your credentials. * * For more information, see the following documentation topic: * * https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/get-started.html */ public class HelloInspector { private static final Logger logger = LoggerFactory.getLogger(HelloInspector.class); public static void main(String[] args) { logger.info("Hello Amazon Inspector!"); try (Inspector2Client inspectorClient = Inspector2Client.builder().build()) { logger.info("Listing member accounts for this Inspector administrator account..."); listMembers(inspectorClient); logger.info("The Hello Inspector example completed successfully."); } catch (Inspector2Exception e) { logger.error("Error: {}", e.getMessage()); logger.info("Troubleshooting:"); logger.info("1. Verify AWS credentials are configured"); logger.info("2. Check IAM permissions for Inspector2"); logger.info("3. Ensure Inspector2 is enabled in your account"); logger.info("4. Verify you're using a supported region"); } } /** * Lists all member accounts associated with the current Inspector administrator account. * * @param inspectorClient The Inspector2Client used to interact with AWS Inspector. */ public static void listMembers(Inspector2Client inspectorClient) { try { ListMembersRequest request = ListMembersRequest.builder() .maxResults(50) // optional: limit results .build(); ListMembersResponse response = inspectorClient.listMembers(request); List<Member> members = response.members(); if (members == null || members.isEmpty()) { logger.info("No member accounts found for this Inspector administrator account."); return; } logger.info("Found {} member account(s):", members.size()); for (Member member : members) { logger.info(" - Account ID: {}, Status: {}", member.accountId(), member.relationshipStatusAsString()); } } catch (Inspector2Exception e) { logger.error("Failed to list members: {}", e.awsErrorDetails().errorMessage()); } } }
  • API 세부 정보는 API 참조의 ListMembersAWS SDK for Java 2.x 를 참조하세요.

기본 사항

다음 코드 예제에서는 다음과 같은 작업을 수행하는 방법을 보여줍니다.

  • Inspector 계정 상태를 확인합니다.

  • Inspector가 활성화되어 있는지 확인합니다.

  • 보안 조사 결과를 분석합니다.

  • 스캔 적용 범위를 확인합니다.

  • 결과 필터를 생성합니다.

  • 기존 필터를 나열합니다.

  • 사용량 및 비용을 확인합니다.

  • 적용 범위 통계를 가져옵니다.

  • 필터를 삭제합니다.

SDK for Java 2.x
참고

GitHub에 더 많은 내용이 있습니다. AWS 코드 예 리포지토리에서 전체 예를 찾고 설정 및 실행하는 방법을 배워보세요.

기능을 보여주는 대화형 시나리오를 실행합니다.

/** * Before running this Java V2 code example, set up your development * environment, including your credentials. * <p> * For more information, see the following documentation topic: * <p> * https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/get-started.html */ public class InspectorScenario { public static final String DASHES = new String(new char[80]).replace("\0", "-"); private static final Logger logger = LoggerFactory.getLogger(InspectorScenario.class); private static final Scanner scanner = new Scanner(System.in); public static void main(String[] args) { InspectorActions inspectorActions = new InspectorActions(); logger.info("Amazon Inspector Basics Scenario"); logger.info(""" Amazon Inspector is a security assessment service provided by Amazon Web Services (AWS) that helps improve the security and compliance of applications deployed on AWS. It automatically assesses applications for vulnerabilities or deviations from best practices. By leveraging Amazon Inspector, users can gain insights into the overall security state of their application and identify potential security risks. This service operates by conducting both network and host-based assessments, allowing it to detect a wide range of security issues, including those related to operating systems, network configurations, and application dependencies. """); waitForInputToContinue(); try { runScenario(inspectorActions); logger.info(""); logger.info("Scenario completed successfully!"); logger.info(""); logger.info("What you learned:"); logger.info(" - How to check Inspector account status"); logger.info(" - How to enable Inspector"); logger.info(" - How to list and analyze findings"); logger.info(" - How to check coverage information"); logger.info(" - How to create and manage filters"); logger.info(" - How to track usage and costs"); logger.info(" - How to clean up resources"); logger.info(""); } catch (Exception ex) { logger.error("Scenario failed due to unexpected error: {}", ex.getMessage(), ex); } finally { scanner.close(); logger.info("Exiting..."); } } /** * Runs the Inspector scenario in a step-by-step sequence. * * All InspectorActions methods are asynchronous and return CompletableFutures. * Each step ends with .join(). Any async exception thrown during .join() will bubble up * */ public static void runScenario(InspectorActions actions) { String filterArn = null; boolean inspectorEnabled = false; try { // Step 1 logger.info(DASHES); logger.info("Step 1: Checking Inspector account status..."); String status = actions.getAccountStatusAsync().join(); logger.info(status); waitForInputToContinue(); // Step 2 logger.info(DASHES); logger.info("Step 2: Enabling Inspector..."); String message = actions.enableInspectorAsync(null).join(); logger.info(message); inspectorEnabled = true; // track that Inspector was enabled waitForInputToContinue(); // Step 3 logger.info(DASHES); logger.info("Step 3: Listing LOW severity findings..."); // Call the service method List<String> allFindings = actions.listLowSeverityFindingsAsync().join(); if (!allFindings.isEmpty()) { // Only proceed if there are findings String lastArn = allFindings.get(allFindings.size() - 1); logger.info("Look up details on: {}", lastArn); waitForInputToContinue(); String details = actions.getFindingDetailsAsync(lastArn).join(); logger.info(details); } else { logger.info("No LOW severity findings found."); } waitForInputToContinue(); // Step 4 logger.info(DASHES); logger.info("Step 4: Listing coverage..."); String coverage = actions.listCoverageAsync(5).join(); logger.info(coverage); waitForInputToContinue(); // Step 5 logger.info(DASHES); logger.info("Step 5: Creating filter..."); String filterName = "suppress-low-" + System.currentTimeMillis(); filterArn = actions.createLowSeverityFilterAsync(filterName, "Suppress low severity findings").join(); logger.info("Created filter: {}", filterArn); waitForInputToContinue(); // Step 6 logger.info(DASHES); logger.info("Step 6: Listing filters..."); String filters = actions.listFiltersAsync(10).join(); logger.info(filters); waitForInputToContinue(); // Step 7 logger.info(DASHES); logger.info("Step 7: Usage totals..."); String usage = actions.listUsageTotalsAsync(null, 10).join(); logger.info(usage); waitForInputToContinue(); // Step 8 logger.info(DASHES); logger.info("Step 8: Coverage statistics..."); String stats = actions.listCoverageStatisticsAsync().join(); logger.info(stats); waitForInputToContinue(); // Step 9 logger.info(DASHES); logger.info("Step 9: Delete filter?"); logger.info("Filter ARN: {}", filterArn); logger.info("Delete the filter and disable Inspector? (y/n)"); if (scanner.nextLine().trim().equalsIgnoreCase("y")) { actions.deleteFilterAsync(filterArn).join(); logger.info("Filter deleted."); String disableMsg = actions.disableInspectorAsync(null).join(); logger.info(disableMsg); inspectorEnabled = false; // track that Inspector was disabled } waitForInputToContinue(); } catch (Exception ex) { logger.error("Scenario encountered an error: {}", ex.getMessage(), ex); // Rethrow the exception throw ex; } finally { // Cleanup in case of an exception if (filterArn != null) { try { actions.deleteFilterAsync(filterArn).join(); logger.info("Cleanup: Filter deleted."); } catch (Exception e) { logger.warn("Failed to delete filter during cleanup: {}", e.getMessage(), e); } } if (inspectorEnabled) { try { actions.disableInspectorAsync(null).join(); logger.info("Cleanup: Inspector disabled."); } catch (Exception e) { logger.warn("Failed to disable Inspector during cleanup: {}", e.getMessage(), e); } } } } // Utility Method private static void waitForInputToContinue() { while (true) { logger.info(""); logger.info("Enter 'c' to continue:"); String input = scanner.nextLine().trim(); if (input.equalsIgnoreCase("c")) break; logger.info("Invalid input, try again."); } } }

SDK 메서드의 래퍼 클래스입니다.

public class InspectorActions { private static Inspector2AsyncClient inspectorAsyncClient; private static final Logger logger = LoggerFactory.getLogger(InspectorActions.class); private static Inspector2AsyncClient getAsyncClient() { if (inspectorAsyncClient == null) { SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() .maxConcurrency(100) .connectionTimeout(Duration.ofSeconds(60)) .readTimeout(Duration.ofSeconds(60)) .writeTimeout(Duration.ofSeconds(60)) .build(); ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() .apiCallTimeout(Duration.ofMinutes(2)) .apiCallAttemptTimeout(Duration.ofSeconds(90)) .retryStrategy(RetryMode.STANDARD) .build(); inspectorAsyncClient = Inspector2AsyncClient.builder() .httpClient(httpClient) .overrideConfiguration(overrideConfig) .build(); } return inspectorAsyncClient; } /** * Enables AWS Inspector for the provided account(s) and default resource types. * * @param accountIds Optional list of AWS account IDs. */ public CompletableFuture<String> enableInspectorAsync(List<String> accountIds) { // The resource types to enable. List<ResourceScanType> resourceTypes = List.of( ResourceScanType.EC2, ResourceScanType.ECR, ResourceScanType.LAMBDA, ResourceScanType.LAMBDA_CODE ); // Build the request. EnableRequest.Builder requestBuilder = EnableRequest.builder() .resourceTypes(resourceTypes); if (accountIds != null && !accountIds.isEmpty()) { requestBuilder.accountIds(accountIds); } EnableRequest request = requestBuilder.build(); return getAsyncClient().enable(request) .whenComplete((response, exception) -> { if (exception != null) { Throwable cause = exception.getCause(); if (cause instanceof ValidationException) { throw new CompletionException( "Inspector may already be enabled for this account: %s".formatted(cause.getMessage()), cause ); } if (cause instanceof Inspector2Exception) { Inspector2Exception e = (Inspector2Exception) cause; throw new CompletionException( "AWS Inspector2 service error: %s".formatted(e.awsErrorDetails().errorMessage()), cause ); } throw new CompletionException( "Failed to enable Inspector: %s".formatted(exception.getMessage()), exception ); } }) .thenApply(response -> { StringBuilder summary = new StringBuilder("Enable results:\n"); if (response.accounts() == null || response.accounts().isEmpty()) { summary.append("Inspector may already be enabled for all target accounts."); return summary.toString(); } for (Account account : response.accounts()) { String accountId = account.accountId() != null ? account.accountId() : "Unknown"; String status = account.status() != null ? account.statusAsString() : "Unknown"; summary.append(" • Account: ").append(accountId) .append(" → Status: ").append(status).append("\n"); } return summary.toString(); }); } /** * Retrieves and prints the coverage statistics using a paginator. */ public CompletableFuture<String> listCoverageStatisticsAsync() { ListCoverageStatisticsRequest request = ListCoverageStatisticsRequest.builder() .build(); return getAsyncClient().listCoverageStatistics(request) .whenComplete((response, exception) -> { if (exception != null) { Throwable cause = exception.getCause(); if (cause instanceof ValidationException) { throw new CompletionException( "Validation error listing coverage statistics: %s".formatted(cause.getMessage()), cause ); } if (cause instanceof Inspector2Exception) { Inspector2Exception e = (Inspector2Exception) cause; throw new CompletionException( "Inspector2 service error: %s".formatted(e.awsErrorDetails().errorMessage()), e ); } throw new CompletionException( "Unexpected error listing coverage statistics: %s".formatted(exception.getMessage()), exception ); } }) .thenApply(response -> { List<Counts> countsList = response.countsByGroup(); StringBuilder sb = new StringBuilder(); if (countsList == null || countsList.isEmpty()) { sb.append("No coverage statistics available.\n"); return sb.toString(); } sb.append("Coverage Statistics:\n"); for (Counts c : countsList) { sb.append(" Group: ").append(c.groupKey()).append("\n") .append(" Total Count: ").append(c.count()).append("\n\n"); } return sb.toString(); }); } /** * Asynchronously lists Inspector2 usage totals using a paginator. * * @param accountIds optional list of account IDs * @param maxResults maximum results per page * @return CompletableFuture completed with formatted summary text */ public CompletableFuture<String> listUsageTotalsAsync( List<String> accountIds, int maxResults) { logger.info("Starting usage totals paginator…"); ListUsageTotalsRequest.Builder builder = ListUsageTotalsRequest.builder() .maxResults(maxResults); if (accountIds != null && !accountIds.isEmpty()) { builder.accountIds(accountIds); } ListUsageTotalsRequest request = builder.build(); ListUsageTotalsPublisher paginator = getAsyncClient().listUsageTotalsPaginator(request); StringBuilder summaryBuilder = new StringBuilder(); return paginator.subscribe(response -> { if (response.totals() != null && !response.totals().isEmpty()) { response.totals().forEach(total -> { if (total.usage() != null) { total.usage().forEach(usage -> { logger.info("Usage: {} = {}", usage.typeAsString(), usage.total()); summaryBuilder.append(usage.typeAsString()) .append(": ") .append(usage.total()) .append("\n"); }); } }); } else { logger.info("Page contained no usage totals."); } }).thenRun(() -> logger.info("Successfully listed usage totals.")) .thenApply(v -> { String summary = summaryBuilder.toString(); return summary.isEmpty() ? "No usage totals found." : summary; }).exceptionally(ex -> { Throwable cause = ex.getCause() != null ? ex.getCause() : ex; if (cause instanceof ValidationException ve) { throw new CompletionException( "Validation error listing usage totals: %s".formatted(ve.getMessage()), ve ); } throw new CompletionException("Failed to list usage totals", cause); }); } /** * Retrieves the account status using the Inspector2Client. */ public CompletableFuture<String> getAccountStatusAsync() { BatchGetAccountStatusRequest request = BatchGetAccountStatusRequest.builder() .accountIds(Collections.emptyList()) .build(); return getAsyncClient().batchGetAccountStatus(request) .whenComplete((response, exception) -> { if (exception != null) { Throwable cause = exception.getCause(); if (cause instanceof AccessDeniedException) { throw new CompletionException( "You do not have sufficient access: %s".formatted(cause.getMessage()), cause ); } if (cause instanceof Inspector2Exception) { Inspector2Exception e = (Inspector2Exception) cause; throw new CompletionException( "Inspector2 service error: %s".formatted(e.awsErrorDetails().errorMessage()), e ); } throw new CompletionException( "Unexpected error getting account status: %s".formatted(exception.getMessage()), exception ); } }) .thenApply(response -> { StringBuilder sb = new StringBuilder(); List<AccountState> accounts = response.accounts(); if (accounts == null || accounts.isEmpty()) { sb.append("No account status returned.\n"); return sb.toString(); } sb.append("Inspector Account Status:\n"); for (AccountState account : accounts) { String accountId = account.accountId() != null ? account.accountId() : "Unknown"; sb.append(" Account ID: ").append(accountId).append("\n"); // Overall account state if (account.state() != null && account.state().status() != null) { sb.append(" Overall State: ") .append(account.state().status()) .append("\n"); } else { sb.append(" Overall State: Unknown\n"); } // Resource state (only status available) ResourceState resources = account.resourceState(); if (resources != null) { sb.append(" Resource Status: available\n"); } sb.append("\n"); } return sb.toString(); }); } /** * Asynchronously lists Inspector2 filters using a paginator. * * @param maxResults maximum filters per page (nullable) * @return CompletableFuture completed with summary text */ public CompletableFuture<String> listFiltersAsync(Integer maxResults) { logger.info("Starting async filters paginator…"); ListFiltersRequest.Builder builder = ListFiltersRequest.builder(); if (maxResults != null) { builder.maxResults(maxResults); } ListFiltersRequest request = builder.build(); // Paginator from SDK ListFiltersPublisher paginator = getAsyncClient().listFiltersPaginator(request); StringBuilder collectedFilterIds = new StringBuilder(); return paginator.subscribe(response -> { response.filters().forEach(filter -> { logger.info("Filter: " + filter.arn()); collectedFilterIds.append(filter.arn()).append("\n"); }); }).thenApply(v -> { String result = collectedFilterIds.toString(); logger.info("Successfully listed all filters."); return result.isEmpty() ? "No filters found." : result; }).exceptionally(ex -> { Throwable cause = ex.getCause() != null ? ex.getCause() : ex; if (cause instanceof ValidationException ve) { throw new CompletionException( "Validation error listing filters: %s".formatted(ve.getMessage()), ve ); } throw new RuntimeException("Failed to list filters", ex); }); } /** * Creates a new LOW severity filter in AWS Inspector2 to suppress findings. * * @param filterName the name of the filter to create * @param description a descriptive string explaining the purpose of the filter * @return a CompletableFuture that completes with the ARN of the created filter * @throws CompletionException wraps any validation, Inspector2 service, or unexpected errors */ public CompletableFuture<String> createLowSeverityFilterAsync( String filterName, String description) { // Define a filter to match LOW severity findings. StringFilter severityFilter = StringFilter.builder() .value(Severity.LOW.toString()) .comparison(StringComparison.EQUALS) .build(); // Create filter criteria. FilterCriteria filterCriteria = FilterCriteria.builder() .severity(Collections.singletonList(severityFilter)) .build(); // Build the filter creation request. CreateFilterRequest request = CreateFilterRequest.builder() .name(filterName) .filterCriteria(filterCriteria) .action(FilterAction.SUPPRESS) .description(description) .build(); return getAsyncClient().createFilter(request) .whenComplete((response, exception) -> { if (exception != null) { Throwable cause = exception.getCause() != null ? exception.getCause() : exception; if (cause instanceof ValidationException ve) { throw new CompletionException( "Validation error creating filter: %s".formatted(ve.getMessage()), ve ); } if (cause instanceof Inspector2Exception e) { throw new CompletionException( "Inspector2 service error: %s".formatted(e.awsErrorDetails().errorMessage()), e ); } // Unexpected async error throw new CompletionException( "Unexpected error creating filter: %s".formatted(exception.getMessage()), exception ); } }) // Extract and return the ARN of the created filter. .thenApply(CreateFilterResponse::arn); } /** * Lists all AWS Inspector findings of LOW severity asynchronously. * * @return CompletableFuture containing a List of finding ARNs. * Returns an empty list if no LOW severity findings are found. */ public CompletableFuture<ArrayList<String>> listLowSeverityFindingsAsync() { logger.info("Starting async LOW severity findings paginator…"); // Build a filter criteria for LOW severity. StringFilter severityFilter = StringFilter.builder() .value(Severity.LOW.toString()) .comparison(StringComparison.EQUALS) .build(); FilterCriteria filterCriteria = FilterCriteria.builder() .severity(Collections.singletonList(severityFilter)) .build(); // Build the request. ListFindingsRequest request = ListFindingsRequest.builder() .filterCriteria(filterCriteria) .build(); ListFindingsPublisher paginator = getAsyncClient().listFindingsPaginator(request); List<String> allArns = Collections.synchronizedList(new ArrayList<>()); return paginator.subscribe(response -> { if (response.findings() != null && !response.findings().isEmpty()) { response.findings().forEach(finding -> { logger.info("Finding ARN: {}", finding.findingArn()); allArns.add(finding.findingArn()); }); } else { logger.info("Page contained no findings."); } }) .thenRun(() -> logger.info("Successfully listed all LOW severity findings.")) .thenApply(v -> new ArrayList<>(allArns)) // Return list instead of a formatted string .exceptionally(ex -> { Throwable cause = ex.getCause() != null ? ex.getCause() : ex; if (cause instanceof ValidationException ve) { throw new CompletionException( "Validation error listing LOW severity findings: %s".formatted(ve.getMessage()), ve ); } throw new RuntimeException("Failed to list LOW severity findings", ex); }); } /** * Lists AWS Inspector2 coverage details for scanned resources using a paginator. * * @param maxResults Maximum number of resources to return. */ public CompletableFuture<String> listCoverageAsync(int maxResults) { ListCoverageRequest initialRequest = ListCoverageRequest.builder() .maxResults(maxResults) .build(); ListCoveragePublisher paginator = getAsyncClient().listCoveragePaginator(initialRequest); StringBuilder summary = new StringBuilder(); return paginator.subscribe(response -> { List<CoveredResource> coveredResources = response.coveredResources(); if (coveredResources == null || coveredResources.isEmpty()) { summary.append("No coverage information available for this page.\n"); return; } Map<String, List<CoveredResource>> byType = coveredResources.stream() .collect(Collectors.groupingBy(CoveredResource::resourceTypeAsString)); byType.forEach((type, list) -> summary.append(" ").append(type) .append(": ").append(list.size()) .append(" resource(s)\n") ); // Include up to 3 sample resources per page for (int i = 0; i < Math.min(coveredResources.size(), 3); i++) { CoveredResource r = coveredResources.get(i); summary.append(" - ").append(r.resourceTypeAsString()) .append(": ").append(r.resourceId()).append("\n"); summary.append(" Scan Type: ").append(r.scanTypeAsString()).append("\n"); if (r.scanStatus() != null) { summary.append(" Status: ").append(r.scanStatus().statusCodeAsString()).append("\n"); } if (r.accountId() != null) { summary.append(" Account ID: ").append(r.accountId()).append("\n"); } summary.append("\n"); } }).thenApply(v -> { if (summary.length() == 0) { return "No coverage information found across all pages."; } else { return "Coverage Information:\n" + summary.toString(); } }).exceptionally(ex -> { Throwable cause = ex.getCause(); if (cause instanceof ValidationException) { throw new CompletionException( "Validation error listing coverage: " + cause.getMessage(), cause); } else if (cause instanceof Inspector2Exception e) { throw new CompletionException( "Inspector2 service error: " + e.awsErrorDetails().errorMessage(), e); } throw new CompletionException("Unexpected error listing coverage: " + ex.getMessage(), ex); }); } /** * Deletes an AWS Inspector2 filter. * * @param filterARN The ARN of the filter to delete. */ public CompletableFuture<Void> deleteFilterAsync(String filterARN) { return getAsyncClient().deleteFilter( DeleteFilterRequest.builder() .arn(filterARN) .build() ) .handle((response, exception) -> { if (exception != null) { Throwable cause = exception.getCause() != null ? exception.getCause() : exception; if (cause instanceof ResourceNotFoundException rnfe) { String msg = "Filter not found for ARN: %s".formatted(filterARN); logger.warn(msg, rnfe); throw new CompletionException(msg, rnfe); } throw new RuntimeException("Failed to delete the filter: " + cause, cause); } return null; }); } /** * Retrieves detailed information about a specific AWS Inspector2 finding asynchronously. * * @param findingArn The ARN of the finding to look up. * @return A {@link CompletableFuture} that, when completed, provides a formatted string * containing all available details for the finding. * @throws RuntimeException if the async call to Inspector2 fails. */ public CompletableFuture<String> getFindingDetailsAsync(String findingArn) { BatchGetFindingDetailsRequest request = BatchGetFindingDetailsRequest.builder() .findingArns(findingArn) .build(); return getAsyncClient().batchGetFindingDetails(request) .thenApply(response -> { if (response.findingDetails() == null || response.findingDetails().isEmpty()) { return String.format("No details found for ARN: ", findingArn); } StringBuilder sb = new StringBuilder(); response.findingDetails().forEach(detail -> { sb.append("Finding ARN: ").append(detail.findingArn()).append("\n") .append("Risk Score: ").append(detail.riskScore()).append("\n"); // ExploitObserved timings if (detail.exploitObserved() != null) { sb.append("Exploit First Seen: ").append(detail.exploitObserved().firstSeen()).append("\n") .append("Exploit Last Seen: ").append(detail.exploitObserved().lastSeen()).append("\n"); } // Reference URLs if (detail.hasReferenceUrls()) { sb.append("Reference URLs:\n"); detail.referenceUrls().forEach(url -> sb.append(" • ").append(url).append("\n")); } // Tools if (detail.hasTools()) { sb.append("Tools:\n"); detail.tools().forEach(tool -> sb.append(" • ").append(tool).append("\n")); } // TTPs if (detail.hasTtps()) { sb.append("TTPs:\n"); detail.ttps().forEach(ttp -> sb.append(" • ").append(ttp).append("\n")); } // CWEs if (detail.hasCwes()) { sb.append("CWEs:\n"); detail.cwes().forEach(cwe -> sb.append(" • ").append(cwe).append("\n")); } // Evidence if (detail.hasEvidences()) { sb.append("Evidence:\n"); detail.evidences().forEach(ev -> { sb.append(" - Severity: ").append(ev.severity()).append("\n"); }); } sb.append("\n"); }); return sb.toString(); }) .exceptionally(ex -> { Throwable cause = ex.getCause() != null ? ex.getCause() : ex; if (cause instanceof ResourceNotFoundException rnfe) { return "Finding not found: %s".formatted(findingArn); } // Fallback for other exceptions throw new RuntimeException("Failed to get finding details for ARN: " + findingArn, cause); }); } /** * Asynchronously disables AWS Inspector for the specified accounts and resource types. * * @param accountIds a {@link List} of AWS account IDs for which to disable Inspector; * may be {@code null} or empty to target the current account * @return a {@link CompletableFuture} that, when completed, returns a {@link String} * summarizing the disable results for each account * @throws CompletionException if the disable operation fails due to validation errors, * service errors, or other exceptions * @see <a href="https://docs.aws.amazon.com/inspector/latest/APIReference/API_Disable.html"> * AWS Inspector2 Disable API</a> */ public CompletableFuture<String> disableInspectorAsync(List<String> accountIds) { // The resource types to disable. List<ResourceScanType> resourceTypes = List.of( ResourceScanType.EC2, ResourceScanType.ECR, ResourceScanType.LAMBDA, ResourceScanType.LAMBDA_CODE ); // Build the request. DisableRequest.Builder requestBuilder = DisableRequest.builder() .resourceTypes(resourceTypes); if (accountIds != null && !accountIds.isEmpty()) { requestBuilder.accountIds(accountIds); } DisableRequest request = requestBuilder.build(); return getAsyncClient().disable(request) .whenComplete((response, exception) -> { if (exception != null) { Throwable cause = exception.getCause(); if (cause instanceof ValidationException) { throw new CompletionException( "Inspector may already be disabled for this account: %s".formatted(cause.getMessage()), cause ); } if (cause instanceof Inspector2Exception) { Inspector2Exception e = (Inspector2Exception) cause; throw new CompletionException( "AWS Inspector2 service error: %s".formatted(e.awsErrorDetails().errorMessage()), cause ); } throw new CompletionException( "Failed to disable Inspector: %s".formatted(exception.getMessage()), exception ); } }) .thenApply(response -> { StringBuilder summary = new StringBuilder("Disable results:\n"); if (response.accounts() == null || response.accounts().isEmpty()) { summary.append("Inspector may already be disabled for all target accounts."); return summary.toString(); } for (Account account : response.accounts()) { String accountId = account.accountId() != null ? account.accountId() : "Unknown"; String status = account.status() != null ? account.statusAsString() : "Unknown"; summary.append(" • Account: ").append(accountId) .append(" → Status: ").append(status).append("\n"); } return summary.toString(); }); } }

작업

다음 코드 예시는 BatchGetAccountStatus의 사용 방법을 보여 줍니다.

SDK for Java 2.x
참고

GitHub에 더 많은 내용이 있습니다. AWS 코드 예 리포지토리에서 전체 예를 찾고 설정 및 실행하는 방법을 배워보세요.

/** * Retrieves the account status using the Inspector2Client. */ public CompletableFuture<String> getAccountStatusAsync() { BatchGetAccountStatusRequest request = BatchGetAccountStatusRequest.builder() .accountIds(Collections.emptyList()) .build(); return getAsyncClient().batchGetAccountStatus(request) .whenComplete((response, exception) -> { if (exception != null) { Throwable cause = exception.getCause(); if (cause instanceof AccessDeniedException) { throw new CompletionException( "You do not have sufficient access: %s".formatted(cause.getMessage()), cause ); } if (cause instanceof Inspector2Exception) { Inspector2Exception e = (Inspector2Exception) cause; throw new CompletionException( "Inspector2 service error: %s".formatted(e.awsErrorDetails().errorMessage()), e ); } throw new CompletionException( "Unexpected error getting account status: %s".formatted(exception.getMessage()), exception ); } }) .thenApply(response -> { StringBuilder sb = new StringBuilder(); List<AccountState> accounts = response.accounts(); if (accounts == null || accounts.isEmpty()) { sb.append("No account status returned.\n"); return sb.toString(); } sb.append("Inspector Account Status:\n"); for (AccountState account : accounts) { String accountId = account.accountId() != null ? account.accountId() : "Unknown"; sb.append(" Account ID: ").append(accountId).append("\n"); // Overall account state if (account.state() != null && account.state().status() != null) { sb.append(" Overall State: ") .append(account.state().status()) .append("\n"); } else { sb.append(" Overall State: Unknown\n"); } // Resource state (only status available) ResourceState resources = account.resourceState(); if (resources != null) { sb.append(" Resource Status: available\n"); } sb.append("\n"); } return sb.toString(); }); }

다음 코드 예시는 BatchGetFindingDetails의 사용 방법을 보여 줍니다.

SDK for Java 2.x
참고

GitHub에 더 많은 내용이 있습니다. AWS 코드 예 리포지토리에서 전체 예를 찾고 설정 및 실행하는 방법을 배워보세요.

/** * Retrieves detailed information about a specific AWS Inspector2 finding asynchronously. * * @param findingArn The ARN of the finding to look up. * @return A {@link CompletableFuture} that, when completed, provides a formatted string * containing all available details for the finding. * @throws RuntimeException if the async call to Inspector2 fails. */ public CompletableFuture<String> getFindingDetailsAsync(String findingArn) { BatchGetFindingDetailsRequest request = BatchGetFindingDetailsRequest.builder() .findingArns(findingArn) .build(); return getAsyncClient().batchGetFindingDetails(request) .thenApply(response -> { if (response.findingDetails() == null || response.findingDetails().isEmpty()) { return String.format("No details found for ARN: ", findingArn); } StringBuilder sb = new StringBuilder(); response.findingDetails().forEach(detail -> { sb.append("Finding ARN: ").append(detail.findingArn()).append("\n") .append("Risk Score: ").append(detail.riskScore()).append("\n"); // ExploitObserved timings if (detail.exploitObserved() != null) { sb.append("Exploit First Seen: ").append(detail.exploitObserved().firstSeen()).append("\n") .append("Exploit Last Seen: ").append(detail.exploitObserved().lastSeen()).append("\n"); } // Reference URLs if (detail.hasReferenceUrls()) { sb.append("Reference URLs:\n"); detail.referenceUrls().forEach(url -> sb.append(" • ").append(url).append("\n")); } // Tools if (detail.hasTools()) { sb.append("Tools:\n"); detail.tools().forEach(tool -> sb.append(" • ").append(tool).append("\n")); } // TTPs if (detail.hasTtps()) { sb.append("TTPs:\n"); detail.ttps().forEach(ttp -> sb.append(" • ").append(ttp).append("\n")); } // CWEs if (detail.hasCwes()) { sb.append("CWEs:\n"); detail.cwes().forEach(cwe -> sb.append(" • ").append(cwe).append("\n")); } // Evidence if (detail.hasEvidences()) { sb.append("Evidence:\n"); detail.evidences().forEach(ev -> { sb.append(" - Severity: ").append(ev.severity()).append("\n"); }); } sb.append("\n"); }); return sb.toString(); }) .exceptionally(ex -> { Throwable cause = ex.getCause() != null ? ex.getCause() : ex; if (cause instanceof ResourceNotFoundException rnfe) { return "Finding not found: %s".formatted(findingArn); } // Fallback for other exceptions throw new RuntimeException("Failed to get finding details for ARN: " + findingArn, cause); }); }

다음 코드 예시는 CreateFilter의 사용 방법을 보여 줍니다.

SDK for Java 2.x
참고

GitHub에 더 많은 내용이 있습니다. AWS 코드 예 리포지토리에서 전체 예를 찾고 설정 및 실행하는 방법을 배워보세요.

/** * Creates a new LOW severity filter in AWS Inspector2 to suppress findings. * * @param filterName the name of the filter to create * @param description a descriptive string explaining the purpose of the filter * @return a CompletableFuture that completes with the ARN of the created filter * @throws CompletionException wraps any validation, Inspector2 service, or unexpected errors */ public CompletableFuture<String> createLowSeverityFilterAsync( String filterName, String description) { // Define a filter to match LOW severity findings. StringFilter severityFilter = StringFilter.builder() .value(Severity.LOW.toString()) .comparison(StringComparison.EQUALS) .build(); // Create filter criteria. FilterCriteria filterCriteria = FilterCriteria.builder() .severity(Collections.singletonList(severityFilter)) .build(); // Build the filter creation request. CreateFilterRequest request = CreateFilterRequest.builder() .name(filterName) .filterCriteria(filterCriteria) .action(FilterAction.SUPPRESS) .description(description) .build(); return getAsyncClient().createFilter(request) .whenComplete((response, exception) -> { if (exception != null) { Throwable cause = exception.getCause() != null ? exception.getCause() : exception; if (cause instanceof ValidationException ve) { throw new CompletionException( "Validation error creating filter: %s".formatted(ve.getMessage()), ve ); } if (cause instanceof Inspector2Exception e) { throw new CompletionException( "Inspector2 service error: %s".formatted(e.awsErrorDetails().errorMessage()), e ); } // Unexpected async error throw new CompletionException( "Unexpected error creating filter: %s".formatted(exception.getMessage()), exception ); } }) // Extract and return the ARN of the created filter. .thenApply(CreateFilterResponse::arn); }
  • API에 대한 세부 정보는 AWS SDK for Java 2.x API 참조CreateFilter를 참조하세요.

다음 코드 예시는 DeleteFilter의 사용 방법을 보여 줍니다.

SDK for Java 2.x
참고

GitHub에 더 많은 내용이 있습니다. AWS 코드 예 리포지토리에서 전체 예를 찾고 설정 및 실행하는 방법을 배워보세요.

/** * Deletes an AWS Inspector2 filter. * * @param filterARN The ARN of the filter to delete. */ public CompletableFuture<Void> deleteFilterAsync(String filterARN) { return getAsyncClient().deleteFilter( DeleteFilterRequest.builder() .arn(filterARN) .build() ) .handle((response, exception) -> { if (exception != null) { Throwable cause = exception.getCause() != null ? exception.getCause() : exception; if (cause instanceof ResourceNotFoundException rnfe) { String msg = "Filter not found for ARN: %s".formatted(filterARN); logger.warn(msg, rnfe); throw new CompletionException(msg, rnfe); } throw new RuntimeException("Failed to delete the filter: " + cause, cause); } return null; }); }
  • API 세부 정보는 API 참조의 DeleteFilterAWS SDK for Java 2.x 를 참조하세요.

다음 코드 예시는 Disable의 사용 방법을 보여 줍니다.

SDK for Java 2.x
참고

GitHub에 더 많은 내용이 있습니다. AWS 코드 예 리포지토리에서 전체 예를 찾고 설정 및 실행하는 방법을 배워보세요.

/** * Asynchronously disables AWS Inspector for the specified accounts and resource types. * * @param accountIds a {@link List} of AWS account IDs for which to disable Inspector; * may be {@code null} or empty to target the current account * @return a {@link CompletableFuture} that, when completed, returns a {@link String} * summarizing the disable results for each account * @throws CompletionException if the disable operation fails due to validation errors, * service errors, or other exceptions * @see <a href="https://docs.aws.amazon.com/inspector/latest/APIReference/API_Disable.html"> * AWS Inspector2 Disable API</a> */ public CompletableFuture<String> disableInspectorAsync(List<String> accountIds) { // The resource types to disable. List<ResourceScanType> resourceTypes = List.of( ResourceScanType.EC2, ResourceScanType.ECR, ResourceScanType.LAMBDA, ResourceScanType.LAMBDA_CODE ); // Build the request. DisableRequest.Builder requestBuilder = DisableRequest.builder() .resourceTypes(resourceTypes); if (accountIds != null && !accountIds.isEmpty()) { requestBuilder.accountIds(accountIds); } DisableRequest request = requestBuilder.build(); return getAsyncClient().disable(request) .whenComplete((response, exception) -> { if (exception != null) { Throwable cause = exception.getCause(); if (cause instanceof ValidationException) { throw new CompletionException( "Inspector may already be disabled for this account: %s".formatted(cause.getMessage()), cause ); } if (cause instanceof Inspector2Exception) { Inspector2Exception e = (Inspector2Exception) cause; throw new CompletionException( "AWS Inspector2 service error: %s".formatted(e.awsErrorDetails().errorMessage()), cause ); } throw new CompletionException( "Failed to disable Inspector: %s".formatted(exception.getMessage()), exception ); } }) .thenApply(response -> { StringBuilder summary = new StringBuilder("Disable results:\n"); if (response.accounts() == null || response.accounts().isEmpty()) { summary.append("Inspector may already be disabled for all target accounts."); return summary.toString(); } for (Account account : response.accounts()) { String accountId = account.accountId() != null ? account.accountId() : "Unknown"; String status = account.status() != null ? account.statusAsString() : "Unknown"; summary.append(" • Account: ").append(accountId) .append(" → Status: ").append(status).append("\n"); } return summary.toString(); }); }
  • API 세부 정보는 API 참조의 비활성화AWS SDK for Java 2.x 를 참조하세요.

다음 코드 예시는 Enable의 사용 방법을 보여 줍니다.

SDK for Java 2.x
참고

GitHub에 더 많은 내용이 있습니다. AWS 코드 예 리포지토리에서 전체 예를 찾고 설정 및 실행하는 방법을 배워보세요.

/** * Enables AWS Inspector for the provided account(s) and default resource types. * * @param accountIds Optional list of AWS account IDs. */ public CompletableFuture<String> enableInspectorAsync(List<String> accountIds) { // The resource types to enable. List<ResourceScanType> resourceTypes = List.of( ResourceScanType.EC2, ResourceScanType.ECR, ResourceScanType.LAMBDA, ResourceScanType.LAMBDA_CODE ); // Build the request. EnableRequest.Builder requestBuilder = EnableRequest.builder() .resourceTypes(resourceTypes); if (accountIds != null && !accountIds.isEmpty()) { requestBuilder.accountIds(accountIds); } EnableRequest request = requestBuilder.build(); return getAsyncClient().enable(request) .whenComplete((response, exception) -> { if (exception != null) { Throwable cause = exception.getCause(); if (cause instanceof ValidationException) { throw new CompletionException( "Inspector may already be enabled for this account: %s".formatted(cause.getMessage()), cause ); } if (cause instanceof Inspector2Exception) { Inspector2Exception e = (Inspector2Exception) cause; throw new CompletionException( "AWS Inspector2 service error: %s".formatted(e.awsErrorDetails().errorMessage()), cause ); } throw new CompletionException( "Failed to enable Inspector: %s".formatted(exception.getMessage()), exception ); } }) .thenApply(response -> { StringBuilder summary = new StringBuilder("Enable results:\n"); if (response.accounts() == null || response.accounts().isEmpty()) { summary.append("Inspector may already be enabled for all target accounts."); return summary.toString(); } for (Account account : response.accounts()) { String accountId = account.accountId() != null ? account.accountId() : "Unknown"; String status = account.status() != null ? account.statusAsString() : "Unknown"; summary.append(" • Account: ").append(accountId) .append(" → Status: ").append(status).append("\n"); } return summary.toString(); }); }
  • API 세부 정보는 API 참조의 활성화AWS SDK for Java 2.x 를 참조하세요.

다음 코드 예시는 ListCoverage의 사용 방법을 보여 줍니다.

SDK for Java 2.x
참고

GitHub에 더 많은 내용이 있습니다. AWS 코드 예 리포지토리에서 전체 예를 찾고 설정 및 실행하는 방법을 배워보세요.

/** * Lists AWS Inspector2 coverage details for scanned resources using a paginator. * * @param maxResults Maximum number of resources to return. */ public CompletableFuture<String> listCoverageAsync(int maxResults) { ListCoverageRequest initialRequest = ListCoverageRequest.builder() .maxResults(maxResults) .build(); ListCoveragePublisher paginator = getAsyncClient().listCoveragePaginator(initialRequest); StringBuilder summary = new StringBuilder(); return paginator.subscribe(response -> { List<CoveredResource> coveredResources = response.coveredResources(); if (coveredResources == null || coveredResources.isEmpty()) { summary.append("No coverage information available for this page.\n"); return; } Map<String, List<CoveredResource>> byType = coveredResources.stream() .collect(Collectors.groupingBy(CoveredResource::resourceTypeAsString)); byType.forEach((type, list) -> summary.append(" ").append(type) .append(": ").append(list.size()) .append(" resource(s)\n") ); // Include up to 3 sample resources per page for (int i = 0; i < Math.min(coveredResources.size(), 3); i++) { CoveredResource r = coveredResources.get(i); summary.append(" - ").append(r.resourceTypeAsString()) .append(": ").append(r.resourceId()).append("\n"); summary.append(" Scan Type: ").append(r.scanTypeAsString()).append("\n"); if (r.scanStatus() != null) { summary.append(" Status: ").append(r.scanStatus().statusCodeAsString()).append("\n"); } if (r.accountId() != null) { summary.append(" Account ID: ").append(r.accountId()).append("\n"); } summary.append("\n"); } }).thenApply(v -> { if (summary.length() == 0) { return "No coverage information found across all pages."; } else { return "Coverage Information:\n" + summary.toString(); } }).exceptionally(ex -> { Throwable cause = ex.getCause(); if (cause instanceof ValidationException) { throw new CompletionException( "Validation error listing coverage: " + cause.getMessage(), cause); } else if (cause instanceof Inspector2Exception e) { throw new CompletionException( "Inspector2 service error: " + e.awsErrorDetails().errorMessage(), e); } throw new CompletionException("Unexpected error listing coverage: " + ex.getMessage(), ex); }); }
  • API 세부 정보는 API 참조의 ListCoverageAWS SDK for Java 2.x 를 참조하세요.

다음 코드 예시는 ListCoverageStatistics의 사용 방법을 보여 줍니다.

SDK for Java 2.x
참고

GitHub에 더 많은 내용이 있습니다. AWS 코드 예 리포지토리에서 전체 예를 찾고 설정 및 실행하는 방법을 배워보세요.

/** * Retrieves and prints the coverage statistics using a paginator. */ public CompletableFuture<String> listCoverageStatisticsAsync() { ListCoverageStatisticsRequest request = ListCoverageStatisticsRequest.builder() .build(); return getAsyncClient().listCoverageStatistics(request) .whenComplete((response, exception) -> { if (exception != null) { Throwable cause = exception.getCause(); if (cause instanceof ValidationException) { throw new CompletionException( "Validation error listing coverage statistics: %s".formatted(cause.getMessage()), cause ); } if (cause instanceof Inspector2Exception) { Inspector2Exception e = (Inspector2Exception) cause; throw new CompletionException( "Inspector2 service error: %s".formatted(e.awsErrorDetails().errorMessage()), e ); } throw new CompletionException( "Unexpected error listing coverage statistics: %s".formatted(exception.getMessage()), exception ); } }) .thenApply(response -> { List<Counts> countsList = response.countsByGroup(); StringBuilder sb = new StringBuilder(); if (countsList == null || countsList.isEmpty()) { sb.append("No coverage statistics available.\n"); return sb.toString(); } sb.append("Coverage Statistics:\n"); for (Counts c : countsList) { sb.append(" Group: ").append(c.groupKey()).append("\n") .append(" Total Count: ").append(c.count()).append("\n\n"); } return sb.toString(); }); }

다음 코드 예시는 ListFilters의 사용 방법을 보여 줍니다.

SDK for Java 2.x
참고

GitHub에 더 많은 내용이 있습니다. AWS 코드 예 리포지토리에서 전체 예를 찾고 설정 및 실행하는 방법을 배워보세요.

/** * Asynchronously lists Inspector2 filters using a paginator. * * @param maxResults maximum filters per page (nullable) * @return CompletableFuture completed with summary text */ public CompletableFuture<String> listFiltersAsync(Integer maxResults) { logger.info("Starting async filters paginator…"); ListFiltersRequest.Builder builder = ListFiltersRequest.builder(); if (maxResults != null) { builder.maxResults(maxResults); } ListFiltersRequest request = builder.build(); // Paginator from SDK ListFiltersPublisher paginator = getAsyncClient().listFiltersPaginator(request); StringBuilder collectedFilterIds = new StringBuilder(); return paginator.subscribe(response -> { response.filters().forEach(filter -> { logger.info("Filter: " + filter.arn()); collectedFilterIds.append(filter.arn()).append("\n"); }); }).thenApply(v -> { String result = collectedFilterIds.toString(); logger.info("Successfully listed all filters."); return result.isEmpty() ? "No filters found." : result; }).exceptionally(ex -> { Throwable cause = ex.getCause() != null ? ex.getCause() : ex; if (cause instanceof ValidationException ve) { throw new CompletionException( "Validation error listing filters: %s".formatted(ve.getMessage()), ve ); } throw new RuntimeException("Failed to list filters", ex); }); }
  • API 세부 정보는 API 참조의 ListFiltersAWS SDK for Java 2.x 를 참조하세요.

다음 코드 예시는 ListFindings의 사용 방법을 보여 줍니다.

SDK for Java 2.x
참고

GitHub에 더 많은 내용이 있습니다. AWS 코드 예 리포지토리에서 전체 예를 찾고 설정 및 실행하는 방법을 배워보세요.

/** * Lists all AWS Inspector findings of LOW severity asynchronously. * * @return CompletableFuture containing a List of finding ARNs. * Returns an empty list if no LOW severity findings are found. */ public CompletableFuture<ArrayList<String>> listLowSeverityFindingsAsync() { logger.info("Starting async LOW severity findings paginator…"); // Build a filter criteria for LOW severity. StringFilter severityFilter = StringFilter.builder() .value(Severity.LOW.toString()) .comparison(StringComparison.EQUALS) .build(); FilterCriteria filterCriteria = FilterCriteria.builder() .severity(Collections.singletonList(severityFilter)) .build(); // Build the request. ListFindingsRequest request = ListFindingsRequest.builder() .filterCriteria(filterCriteria) .build(); ListFindingsPublisher paginator = getAsyncClient().listFindingsPaginator(request); List<String> allArns = Collections.synchronizedList(new ArrayList<>()); return paginator.subscribe(response -> { if (response.findings() != null && !response.findings().isEmpty()) { response.findings().forEach(finding -> { logger.info("Finding ARN: {}", finding.findingArn()); allArns.add(finding.findingArn()); }); } else { logger.info("Page contained no findings."); } }) .thenRun(() -> logger.info("Successfully listed all LOW severity findings.")) .thenApply(v -> new ArrayList<>(allArns)) // Return list instead of a formatted string .exceptionally(ex -> { Throwable cause = ex.getCause() != null ? ex.getCause() : ex; if (cause instanceof ValidationException ve) { throw new CompletionException( "Validation error listing LOW severity findings: %s".formatted(ve.getMessage()), ve ); } throw new RuntimeException("Failed to list LOW severity findings", ex); }); }
  • API 세부 정보는 API 참조의 ListFindingsAWS SDK for Java 2.x 를 참조하세요.

다음 코드 예시는 ListUsageTotals의 사용 방법을 보여 줍니다.

SDK for Java 2.x
참고

GitHub에 더 많은 내용이 있습니다. AWS 코드 예 리포지토리에서 전체 예를 찾고 설정 및 실행하는 방법을 배워보세요.

/** * Asynchronously lists Inspector2 usage totals using a paginator. * * @param accountIds optional list of account IDs * @param maxResults maximum results per page * @return CompletableFuture completed with formatted summary text */ public CompletableFuture<String> listUsageTotalsAsync( List<String> accountIds, int maxResults) { logger.info("Starting usage totals paginator…"); ListUsageTotalsRequest.Builder builder = ListUsageTotalsRequest.builder() .maxResults(maxResults); if (accountIds != null && !accountIds.isEmpty()) { builder.accountIds(accountIds); } ListUsageTotalsRequest request = builder.build(); ListUsageTotalsPublisher paginator = getAsyncClient().listUsageTotalsPaginator(request); StringBuilder summaryBuilder = new StringBuilder(); return paginator.subscribe(response -> { if (response.totals() != null && !response.totals().isEmpty()) { response.totals().forEach(total -> { if (total.usage() != null) { total.usage().forEach(usage -> { logger.info("Usage: {} = {}", usage.typeAsString(), usage.total()); summaryBuilder.append(usage.typeAsString()) .append(": ") .append(usage.total()) .append("\n"); }); } }); } else { logger.info("Page contained no usage totals."); } }).thenRun(() -> logger.info("Successfully listed usage totals.")) .thenApply(v -> { String summary = summaryBuilder.toString(); return summary.isEmpty() ? "No usage totals found." : summary; }).exceptionally(ex -> { Throwable cause = ex.getCause() != null ? ex.getCause() : ex; if (cause instanceof ValidationException ve) { throw new CompletionException( "Validation error listing usage totals: %s".formatted(ve.getMessage()), ve ); } throw new CompletionException("Failed to list usage totals", cause); }); }