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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [4.3.0] - 2026-03-24

### Added

- `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.
- `getPolicyImpactReport()` / `getPolicyImpactReportAsync()` — test a single policy against multiple inputs and get aggregate match/block statistics.
- `detectPolicyConflicts()` / `detectPolicyConflictsAsync()` — analyze active policies for contradictions, shadows, and redundancies. Optionally filter to conflicts involving a specific policy.
- Types in `com.getaxonflow.sdk.simulation` package: `SimulatePoliciesRequest`, `SimulatePoliciesResponse`, `SimulationDailyUsage`, `ImpactReportInput`, `ImpactReportRequest`, `ImpactReportResult`, `ImpactReportResponse`, `PolicyConflictRef`, `PolicyConflict`, `PolicyConflictResponse`

### Security

- 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.
Expand Down
188 changes: 188 additions & 0 deletions src/main/java/com/getaxonflow/sdk/AxonFlow.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.getaxonflow.sdk.types.policies.PolicyTypes.*;
import com.getaxonflow.sdk.masfeat.MASFEATTypes.*;
import com.getaxonflow.sdk.types.webhook.WebhookTypes.*;
import com.getaxonflow.sdk.simulation.*;
import com.getaxonflow.sdk.util.*;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
Expand Down Expand Up @@ -821,6 +822,193 @@ public CompletableFuture<CircuitBreakerConfigUpdateResponse> updateCircuitBreake
return CompletableFuture.supplyAsync(() -> updateCircuitBreakerConfig(config), asyncExecutor);
}

// ========================================================================
// Policy Simulation
// ========================================================================

/**
* Simulates policy evaluation against a query without actually enforcing policies.
*
* <p>This is a dry-run mode that shows which policies would match and what actions
* would be taken, without blocking the request.
*
* <p>Example usage:
* <pre>{@code
* SimulatePoliciesResponse result = axonflow.simulatePolicies(
* SimulatePoliciesRequest.builder()
* .query("Transfer $50,000 to external account")
* .requestType("execute")
* .build());
* System.out.println("Allowed: " + result.isAllowed());
* System.out.println("Applied policies: " + result.getAppliedPolicies());
* System.out.println("Risk score: " + result.getRiskScore());
* }</pre>
*
* <p><b>Evaluation+ Feature:</b> Requires AxonFlow Evaluation tier or higher.
*
* @param request the simulation request
* @return the simulation result
* @throws NullPointerException if request is null
* @throws AxonFlowException if the request fails
*/
public SimulatePoliciesResponse simulatePolicies(SimulatePoliciesRequest request) {
Objects.requireNonNull(request, "request cannot be null");

return retryExecutor.execute(() -> {
Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/policies/simulate", request);
try (Response response = httpClient.newCall(httpRequest).execute()) {
JsonNode node = parseResponseNode(response);
if (node.has("data") && node.get("data").isObject()) {
return objectMapper.treeToValue(node.get("data"), SimulatePoliciesResponse.class);
}
return objectMapper.treeToValue(node, SimulatePoliciesResponse.class);
}
}, "simulatePolicies");
}

/**
* Asynchronously simulates policy evaluation against a query.
*
* @param request the simulation request
* @return a future containing the simulation result
*/
public CompletableFuture<SimulatePoliciesResponse> simulatePoliciesAsync(SimulatePoliciesRequest request) {
return CompletableFuture.supplyAsync(() -> simulatePolicies(request), asyncExecutor);
}

/**
* Generates a policy impact report by testing a set of inputs against a specific policy.
*
* <p>This helps you understand how a policy would affect real traffic before deploying it.
*
* <p>Example usage:
* <pre>{@code
* ImpactReportResponse report = axonflow.getPolicyImpactReport(
* ImpactReportRequest.builder()
* .policyId("policy_block_pii")
* .inputs(List.of(
* ImpactReportInput.builder().query("My SSN is 123-45-6789").build(),
* ImpactReportInput.builder().query("What is the weather?").build()))
* .build());
* System.out.println("Match rate: " + report.getMatchRate());
* System.out.println("Block rate: " + report.getBlockRate());
* }</pre>
*
* <p><b>Evaluation+ Feature:</b> Requires AxonFlow Evaluation tier or higher.
*
* @param request the impact report request
* @return the impact report
* @throws NullPointerException if request is null
* @throws AxonFlowException if the request fails
*/
public ImpactReportResponse getPolicyImpactReport(ImpactReportRequest request) {
Objects.requireNonNull(request, "request cannot be null");

return retryExecutor.execute(() -> {
Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/policies/impact-report", request);
try (Response response = httpClient.newCall(httpRequest).execute()) {
JsonNode node = parseResponseNode(response);
if (node.has("data") && node.get("data").isObject()) {
return objectMapper.treeToValue(node.get("data"), ImpactReportResponse.class);
}
return objectMapper.treeToValue(node, ImpactReportResponse.class);
}
}, "getPolicyImpactReport");
}

/**
* Asynchronously generates a policy impact report.
*
* @param request the impact report request
* @return a future containing the impact report
*/
public CompletableFuture<ImpactReportResponse> getPolicyImpactReportAsync(ImpactReportRequest request) {
return CompletableFuture.supplyAsync(() -> getPolicyImpactReport(request), asyncExecutor);
}

/**
* Scans all active policies for conflicts.
*
* <p>Example usage:
* <pre>{@code
* PolicyConflictResponse conflicts = axonflow.detectPolicyConflicts();
* System.out.println("Conflicts found: " + conflicts.getConflictCount());
* for (PolicyConflict conflict : conflicts.getConflicts()) {
* System.out.println(conflict.getConflictType() + ": " + conflict.getDescription());
* }
* }</pre>
*
* <p><b>Evaluation+ Feature:</b> Requires AxonFlow Evaluation tier or higher.
*
* @return the conflict detection result
* @throws AxonFlowException if the request fails
*/
public PolicyConflictResponse detectPolicyConflicts() {
return detectPolicyConflicts(null);
}

/**
* Detects conflicts between a specific policy and other active policies,
* or scans all policies if policyId is null.
*
* <p>Example usage:
* <pre>{@code
* PolicyConflictResponse conflicts = axonflow.detectPolicyConflicts("policy_block_pii");
* System.out.println("Conflicts found: " + conflicts.getConflictCount());
* for (PolicyConflict conflict : conflicts.getConflicts()) {
* System.out.println(conflict.getConflictType() + ": " + conflict.getDescription());
* }
* }</pre>
*
* <p><b>Evaluation+ Feature:</b> Requires AxonFlow Evaluation tier or higher.
*
* @param policyId the policy ID to check for conflicts, or null to scan all policies
* @return the conflict detection result
* @throws IllegalArgumentException if policyId is non-null and empty
* @throws AxonFlowException if the request fails
*/
public PolicyConflictResponse detectPolicyConflicts(String policyId) {
if (policyId != null && policyId.isEmpty()) {
throw new IllegalArgumentException("policyId cannot be empty");
}

return retryExecutor.execute(() -> {
Object body;
if (policyId != null) {
body = java.util.Map.of("policy_id", policyId);
} else {
body = java.util.Map.of();
}
Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/policies/conflicts", body);
try (Response response = httpClient.newCall(httpRequest).execute()) {
JsonNode node = parseResponseNode(response);
if (node.has("data") && node.get("data").isObject()) {
return objectMapper.treeToValue(node.get("data"), PolicyConflictResponse.class);
}
return objectMapper.treeToValue(node, PolicyConflictResponse.class);
}
}, "detectPolicyConflicts");
}

/**
* Asynchronously scans all active policies for conflicts.
*
* @return a future containing the conflict detection result
*/
public CompletableFuture<PolicyConflictResponse> detectPolicyConflictsAsync() {
return CompletableFuture.supplyAsync(() -> detectPolicyConflicts(), asyncExecutor);
}

/**
* Asynchronously detects conflicts between a specific policy and other active policies.
*
* @param policyId the policy ID to check for conflicts, or null to scan all policies
* @return a future containing the conflict detection result
*/
public CompletableFuture<PolicyConflictResponse> detectPolicyConflictsAsync(String policyId) {
return CompletableFuture.supplyAsync(() -> detectPolicyConflicts(policyId), asyncExecutor);
}

// ========================================================================
// Proxy Mode - Query Execution
// ========================================================================
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2026 AxonFlow
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.getaxonflow.sdk.simulation;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.Map;
import java.util.Objects;

/**
* A single input to test against a policy in an impact report.
*
* <p>Use the {@link Builder} to construct instances:
* <pre>{@code
* ImpactReportInput input = ImpactReportInput.builder()
* .query("Transfer funds to external account")
* .requestType("execute")
* .build();
* }</pre>
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public final class ImpactReportInput {

@JsonProperty("query")
private final String query;

@JsonProperty("request_type")
private final String requestType;

@JsonProperty("context")
private final Map<String, Object> context;

private ImpactReportInput(Builder builder) {
this.query = Objects.requireNonNull(builder.query, "query cannot be null");
this.requestType = builder.requestType;
this.context = builder.context;
}

public static Builder builder() {
return new Builder();
}

public String getQuery() { return query; }
public String getRequestType() { return requestType; }
public Map<String, Object> getContext() { return context; }

/**
* Builder for {@link ImpactReportInput}.
*/
public static final class Builder {
private String query;
private String requestType;
private Map<String, Object> context;

public Builder query(String query) { this.query = query; return this; }
public Builder requestType(String requestType) { this.requestType = requestType; return this; }
public Builder context(Map<String, Object> context) { this.context = context; return this; }

public ImpactReportInput build() {
return new ImpactReportInput(this);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2026 AxonFlow
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.getaxonflow.sdk.simulation;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.List;
import java.util.Objects;

/**
* Request to generate a policy impact report.
*
* <p>Use the {@link Builder} to construct instances:
* <pre>{@code
* ImpactReportRequest request = ImpactReportRequest.builder()
* .policyId("policy_block_pii")
* .inputs(List.of(
* ImpactReportInput.builder().query("My SSN is 123-45-6789").build(),
* ImpactReportInput.builder().query("What is the weather?").build()
* ))
* .build();
* }</pre>
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public final class ImpactReportRequest {

@JsonProperty("policy_id")
private final String policyId;

@JsonProperty("inputs")
private final List<ImpactReportInput> inputs;

private ImpactReportRequest(Builder builder) {
this.policyId = Objects.requireNonNull(builder.policyId, "policyId cannot be null");
if (this.policyId.isEmpty()) {
throw new IllegalArgumentException("policyId cannot be empty");
}
this.inputs = Objects.requireNonNull(builder.inputs, "inputs cannot be null");
if (this.inputs.isEmpty()) {
throw new IllegalArgumentException("inputs cannot be empty");
}
}

public static Builder builder() {
return new Builder();
}

public String getPolicyId() { return policyId; }
public List<ImpactReportInput> getInputs() { return inputs; }

/**
* Builder for {@link ImpactReportRequest}.
*/
public static final class Builder {
private String policyId;
private List<ImpactReportInput> inputs;

public Builder policyId(String policyId) { this.policyId = policyId; return this; }
public Builder inputs(List<ImpactReportInput> inputs) { this.inputs = inputs; return this; }

/**
* Builds the ImpactReportRequest.
*
* @return the request
* @throws NullPointerException if policyId or inputs is null
* @throws IllegalArgumentException if policyId is empty or inputs is empty
*/
public ImpactReportRequest build() {
return new ImpactReportRequest(this);
}
}
}
Loading
Loading