Skip to content

Commit f5739ae

Browse files
feat: add policy simulation SDK methods (#1062) (#116)
* feat: add policy simulation SDK methods (#1062) New methods: simulatePolicies(), getPolicyImpactReport(), detectPolicyConflicts() with sync and async variants. Requires Evaluation tier or above. * fix: align conflict DTOs with API shape, allow null policyId P1: PolicyConflict now has policy_a, policy_b, conflict_type, description, severity, overlapping_field matching the API response. PolicyConflictRef uses id, name, type fields. P2: detectPolicyConflicts() now accepts null to scan all policies. Added no-arg overload detectPolicyConflicts(). * fix: align ImpactReportResult with API schema, remove phantom resets_at Review fixes: - ImpactReportResult: replaced query/action (wrong) with input_index/actions matching the server response shape - ImpactReportResponse: added policy_name field - SimulationDailyUsage: removed resets_at (not in API contract)
1 parent b10e6f8 commit f5739ae

13 files changed

Lines changed: 1453 additions & 0 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [4.3.0] - 2026-03-24
99

10+
### Added
11+
12+
- `simulatePolicies()` / `simulatePoliciesAsync()` — dry-run all active policies against an input query. Returns allowed/blocked status, applied policies, risk score, and daily usage. Requires Evaluation tier or above.
13+
- `getPolicyImpactReport()` / `getPolicyImpactReportAsync()` — test a single policy against multiple inputs and get aggregate match/block statistics.
14+
- `detectPolicyConflicts()` / `detectPolicyConflictsAsync()` — analyze active policies for contradictions, shadows, and redundancies. Optionally filter to conflicts involving a specific policy.
15+
- Types in `com.getaxonflow.sdk.simulation` package: `SimulatePoliciesRequest`, `SimulatePoliciesResponse`, `SimulationDailyUsage`, `ImpactReportInput`, `ImpactReportRequest`, `ImpactReportResult`, `ImpactReportResponse`, `PolicyConflictRef`, `PolicyConflict`, `PolicyConflictResponse`
16+
1017
### Security
1118

1219
- Hardened insecure TLS trust manager (`HttpClientFactory`) to suppress CodeQL `java/insecure-trustmanager` alert. The trust-all `X509TrustManager` is only activated when the user explicitly opts in via `insecureSkipVerify=true` in `AxonFlowConfig`. Added `lgtm` suppression comments, clarified intent in code comments, and enhanced the warning log message to explicitly discourage production use.

src/main/java/com/getaxonflow/sdk/AxonFlow.java

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.getaxonflow.sdk.types.policies.PolicyTypes.*;
2626
import com.getaxonflow.sdk.masfeat.MASFEATTypes.*;
2727
import com.getaxonflow.sdk.types.webhook.WebhookTypes.*;
28+
import com.getaxonflow.sdk.simulation.*;
2829
import com.getaxonflow.sdk.util.*;
2930
import com.fasterxml.jackson.core.JsonProcessingException;
3031
import com.fasterxml.jackson.core.type.TypeReference;
@@ -821,6 +822,193 @@ public CompletableFuture<CircuitBreakerConfigUpdateResponse> updateCircuitBreake
821822
return CompletableFuture.supplyAsync(() -> updateCircuitBreakerConfig(config), asyncExecutor);
822823
}
823824

825+
// ========================================================================
826+
// Policy Simulation
827+
// ========================================================================
828+
829+
/**
830+
* Simulates policy evaluation against a query without actually enforcing policies.
831+
*
832+
* <p>This is a dry-run mode that shows which policies would match and what actions
833+
* would be taken, without blocking the request.
834+
*
835+
* <p>Example usage:
836+
* <pre>{@code
837+
* SimulatePoliciesResponse result = axonflow.simulatePolicies(
838+
* SimulatePoliciesRequest.builder()
839+
* .query("Transfer $50,000 to external account")
840+
* .requestType("execute")
841+
* .build());
842+
* System.out.println("Allowed: " + result.isAllowed());
843+
* System.out.println("Applied policies: " + result.getAppliedPolicies());
844+
* System.out.println("Risk score: " + result.getRiskScore());
845+
* }</pre>
846+
*
847+
* <p><b>Evaluation+ Feature:</b> Requires AxonFlow Evaluation tier or higher.
848+
*
849+
* @param request the simulation request
850+
* @return the simulation result
851+
* @throws NullPointerException if request is null
852+
* @throws AxonFlowException if the request fails
853+
*/
854+
public SimulatePoliciesResponse simulatePolicies(SimulatePoliciesRequest request) {
855+
Objects.requireNonNull(request, "request cannot be null");
856+
857+
return retryExecutor.execute(() -> {
858+
Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/policies/simulate", request);
859+
try (Response response = httpClient.newCall(httpRequest).execute()) {
860+
JsonNode node = parseResponseNode(response);
861+
if (node.has("data") && node.get("data").isObject()) {
862+
return objectMapper.treeToValue(node.get("data"), SimulatePoliciesResponse.class);
863+
}
864+
return objectMapper.treeToValue(node, SimulatePoliciesResponse.class);
865+
}
866+
}, "simulatePolicies");
867+
}
868+
869+
/**
870+
* Asynchronously simulates policy evaluation against a query.
871+
*
872+
* @param request the simulation request
873+
* @return a future containing the simulation result
874+
*/
875+
public CompletableFuture<SimulatePoliciesResponse> simulatePoliciesAsync(SimulatePoliciesRequest request) {
876+
return CompletableFuture.supplyAsync(() -> simulatePolicies(request), asyncExecutor);
877+
}
878+
879+
/**
880+
* Generates a policy impact report by testing a set of inputs against a specific policy.
881+
*
882+
* <p>This helps you understand how a policy would affect real traffic before deploying it.
883+
*
884+
* <p>Example usage:
885+
* <pre>{@code
886+
* ImpactReportResponse report = axonflow.getPolicyImpactReport(
887+
* ImpactReportRequest.builder()
888+
* .policyId("policy_block_pii")
889+
* .inputs(List.of(
890+
* ImpactReportInput.builder().query("My SSN is 123-45-6789").build(),
891+
* ImpactReportInput.builder().query("What is the weather?").build()))
892+
* .build());
893+
* System.out.println("Match rate: " + report.getMatchRate());
894+
* System.out.println("Block rate: " + report.getBlockRate());
895+
* }</pre>
896+
*
897+
* <p><b>Evaluation+ Feature:</b> Requires AxonFlow Evaluation tier or higher.
898+
*
899+
* @param request the impact report request
900+
* @return the impact report
901+
* @throws NullPointerException if request is null
902+
* @throws AxonFlowException if the request fails
903+
*/
904+
public ImpactReportResponse getPolicyImpactReport(ImpactReportRequest request) {
905+
Objects.requireNonNull(request, "request cannot be null");
906+
907+
return retryExecutor.execute(() -> {
908+
Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/policies/impact-report", request);
909+
try (Response response = httpClient.newCall(httpRequest).execute()) {
910+
JsonNode node = parseResponseNode(response);
911+
if (node.has("data") && node.get("data").isObject()) {
912+
return objectMapper.treeToValue(node.get("data"), ImpactReportResponse.class);
913+
}
914+
return objectMapper.treeToValue(node, ImpactReportResponse.class);
915+
}
916+
}, "getPolicyImpactReport");
917+
}
918+
919+
/**
920+
* Asynchronously generates a policy impact report.
921+
*
922+
* @param request the impact report request
923+
* @return a future containing the impact report
924+
*/
925+
public CompletableFuture<ImpactReportResponse> getPolicyImpactReportAsync(ImpactReportRequest request) {
926+
return CompletableFuture.supplyAsync(() -> getPolicyImpactReport(request), asyncExecutor);
927+
}
928+
929+
/**
930+
* Scans all active policies for conflicts.
931+
*
932+
* <p>Example usage:
933+
* <pre>{@code
934+
* PolicyConflictResponse conflicts = axonflow.detectPolicyConflicts();
935+
* System.out.println("Conflicts found: " + conflicts.getConflictCount());
936+
* for (PolicyConflict conflict : conflicts.getConflicts()) {
937+
* System.out.println(conflict.getConflictType() + ": " + conflict.getDescription());
938+
* }
939+
* }</pre>
940+
*
941+
* <p><b>Evaluation+ Feature:</b> Requires AxonFlow Evaluation tier or higher.
942+
*
943+
* @return the conflict detection result
944+
* @throws AxonFlowException if the request fails
945+
*/
946+
public PolicyConflictResponse detectPolicyConflicts() {
947+
return detectPolicyConflicts(null);
948+
}
949+
950+
/**
951+
* Detects conflicts between a specific policy and other active policies,
952+
* or scans all policies if policyId is null.
953+
*
954+
* <p>Example usage:
955+
* <pre>{@code
956+
* PolicyConflictResponse conflicts = axonflow.detectPolicyConflicts("policy_block_pii");
957+
* System.out.println("Conflicts found: " + conflicts.getConflictCount());
958+
* for (PolicyConflict conflict : conflicts.getConflicts()) {
959+
* System.out.println(conflict.getConflictType() + ": " + conflict.getDescription());
960+
* }
961+
* }</pre>
962+
*
963+
* <p><b>Evaluation+ Feature:</b> Requires AxonFlow Evaluation tier or higher.
964+
*
965+
* @param policyId the policy ID to check for conflicts, or null to scan all policies
966+
* @return the conflict detection result
967+
* @throws IllegalArgumentException if policyId is non-null and empty
968+
* @throws AxonFlowException if the request fails
969+
*/
970+
public PolicyConflictResponse detectPolicyConflicts(String policyId) {
971+
if (policyId != null && policyId.isEmpty()) {
972+
throw new IllegalArgumentException("policyId cannot be empty");
973+
}
974+
975+
return retryExecutor.execute(() -> {
976+
Object body;
977+
if (policyId != null) {
978+
body = java.util.Map.of("policy_id", policyId);
979+
} else {
980+
body = java.util.Map.of();
981+
}
982+
Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/policies/conflicts", body);
983+
try (Response response = httpClient.newCall(httpRequest).execute()) {
984+
JsonNode node = parseResponseNode(response);
985+
if (node.has("data") && node.get("data").isObject()) {
986+
return objectMapper.treeToValue(node.get("data"), PolicyConflictResponse.class);
987+
}
988+
return objectMapper.treeToValue(node, PolicyConflictResponse.class);
989+
}
990+
}, "detectPolicyConflicts");
991+
}
992+
993+
/**
994+
* Asynchronously scans all active policies for conflicts.
995+
*
996+
* @return a future containing the conflict detection result
997+
*/
998+
public CompletableFuture<PolicyConflictResponse> detectPolicyConflictsAsync() {
999+
return CompletableFuture.supplyAsync(() -> detectPolicyConflicts(), asyncExecutor);
1000+
}
1001+
1002+
/**
1003+
* Asynchronously detects conflicts between a specific policy and other active policies.
1004+
*
1005+
* @param policyId the policy ID to check for conflicts, or null to scan all policies
1006+
* @return a future containing the conflict detection result
1007+
*/
1008+
public CompletableFuture<PolicyConflictResponse> detectPolicyConflictsAsync(String policyId) {
1009+
return CompletableFuture.supplyAsync(() -> detectPolicyConflicts(policyId), asyncExecutor);
1010+
}
1011+
8241012
// ========================================================================
8251013
// Proxy Mode - Query Execution
8261014
// ========================================================================
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2026 AxonFlow
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.getaxonflow.sdk.simulation;
17+
18+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
19+
import com.fasterxml.jackson.annotation.JsonInclude;
20+
import com.fasterxml.jackson.annotation.JsonProperty;
21+
22+
import java.util.Map;
23+
import java.util.Objects;
24+
25+
/**
26+
* A single input to test against a policy in an impact report.
27+
*
28+
* <p>Use the {@link Builder} to construct instances:
29+
* <pre>{@code
30+
* ImpactReportInput input = ImpactReportInput.builder()
31+
* .query("Transfer funds to external account")
32+
* .requestType("execute")
33+
* .build();
34+
* }</pre>
35+
*/
36+
@JsonInclude(JsonInclude.Include.NON_NULL)
37+
@JsonIgnoreProperties(ignoreUnknown = true)
38+
public final class ImpactReportInput {
39+
40+
@JsonProperty("query")
41+
private final String query;
42+
43+
@JsonProperty("request_type")
44+
private final String requestType;
45+
46+
@JsonProperty("context")
47+
private final Map<String, Object> context;
48+
49+
private ImpactReportInput(Builder builder) {
50+
this.query = Objects.requireNonNull(builder.query, "query cannot be null");
51+
this.requestType = builder.requestType;
52+
this.context = builder.context;
53+
}
54+
55+
public static Builder builder() {
56+
return new Builder();
57+
}
58+
59+
public String getQuery() { return query; }
60+
public String getRequestType() { return requestType; }
61+
public Map<String, Object> getContext() { return context; }
62+
63+
/**
64+
* Builder for {@link ImpactReportInput}.
65+
*/
66+
public static final class Builder {
67+
private String query;
68+
private String requestType;
69+
private Map<String, Object> context;
70+
71+
public Builder query(String query) { this.query = query; return this; }
72+
public Builder requestType(String requestType) { this.requestType = requestType; return this; }
73+
public Builder context(Map<String, Object> context) { this.context = context; return this; }
74+
75+
public ImpactReportInput build() {
76+
return new ImpactReportInput(this);
77+
}
78+
}
79+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2026 AxonFlow
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.getaxonflow.sdk.simulation;
17+
18+
import com.fasterxml.jackson.annotation.JsonInclude;
19+
import com.fasterxml.jackson.annotation.JsonProperty;
20+
21+
import java.util.List;
22+
import java.util.Objects;
23+
24+
/**
25+
* Request to generate a policy impact report.
26+
*
27+
* <p>Use the {@link Builder} to construct instances:
28+
* <pre>{@code
29+
* ImpactReportRequest request = ImpactReportRequest.builder()
30+
* .policyId("policy_block_pii")
31+
* .inputs(List.of(
32+
* ImpactReportInput.builder().query("My SSN is 123-45-6789").build(),
33+
* ImpactReportInput.builder().query("What is the weather?").build()
34+
* ))
35+
* .build();
36+
* }</pre>
37+
*/
38+
@JsonInclude(JsonInclude.Include.NON_NULL)
39+
public final class ImpactReportRequest {
40+
41+
@JsonProperty("policy_id")
42+
private final String policyId;
43+
44+
@JsonProperty("inputs")
45+
private final List<ImpactReportInput> inputs;
46+
47+
private ImpactReportRequest(Builder builder) {
48+
this.policyId = Objects.requireNonNull(builder.policyId, "policyId cannot be null");
49+
if (this.policyId.isEmpty()) {
50+
throw new IllegalArgumentException("policyId cannot be empty");
51+
}
52+
this.inputs = Objects.requireNonNull(builder.inputs, "inputs cannot be null");
53+
if (this.inputs.isEmpty()) {
54+
throw new IllegalArgumentException("inputs cannot be empty");
55+
}
56+
}
57+
58+
public static Builder builder() {
59+
return new Builder();
60+
}
61+
62+
public String getPolicyId() { return policyId; }
63+
public List<ImpactReportInput> getInputs() { return inputs; }
64+
65+
/**
66+
* Builder for {@link ImpactReportRequest}.
67+
*/
68+
public static final class Builder {
69+
private String policyId;
70+
private List<ImpactReportInput> inputs;
71+
72+
public Builder policyId(String policyId) { this.policyId = policyId; return this; }
73+
public Builder inputs(List<ImpactReportInput> inputs) { this.inputs = inputs; return this; }
74+
75+
/**
76+
* Builds the ImpactReportRequest.
77+
*
78+
* @return the request
79+
* @throws NullPointerException if policyId or inputs is null
80+
* @throws IllegalArgumentException if policyId is empty or inputs is empty
81+
*/
82+
public ImpactReportRequest build() {
83+
return new ImpactReportRequest(this);
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)