feat: Complete target system with C# bindings and resource inference (#458)

* feat: Add Schema Registry and Validation Framework

This commit introduces a comprehensive schema registry and validation framework, providing schema-based validation of resources and policy effects.

- Thread-safe, in-memory registry for schema storage and management
- Global registry patterns for effects and resources
- Concurrent access with proper error handling
- Unicode schema names support

- JSON Schema-compliant validation for all primitive types
- Advanced constraint validation (patterns, ranges, length limits)
- Discriminated union support with anyOf schemas
- Detailed error reporting with nested validation paths
- Discriminated subobject validation for polymorphic schemas

- **Registry Tests**: All registry operations
- **Effect Tests**: Policy effect validation
- **Resource Tests**: Resource validation
- **Validation Tests**: Core validation engine
- Thread-safety, error handling, integration scenarios, edge cases

- **Dependencies**: dashmap, once_cell, regex
- **Thread Safety**: Minimal locking with Rc<Schema> sharing
- **Error Types**: TypeMismatch, OutOfRange, PatternMismatch, etc.

- Complete schema registry and validation subsystem
- Comprehensive test coverage
- Foundation for policy validation in Regorus

Benchmarks:

- Criterion benchmarks for basic types, effects and Azure resources
- Performance range: 3.22ns (string) to 34.74µs (Azure VM resource schema validation)
- String withs patterns validation: 30.2µs. Need to explore whether regex caching helps
  bring this down.
- Azure policy effects: 188ns-1.4µs

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* feat: Complete target system with C# bindings and resource inference

- Add comprehensive target system with TargetRegistry and target-aware compilation
- Implement resource type inference from policy equality expressions
- Create modular C# bindings with separate wrapper classes for each concept
- Add thread-safe CompiledPolicy with reference counting for safe disposal
- Enhance FFI with detailed error propagation and target functionality
- Create TargetExampleApp demonstrating Azure Policy integration
- Add CI/CD pipeline testing for all C# applications
- Support target definitions with schema validation and resource selectors
- Implement PolicyModule struct and target-aware compilation methods
- Add comprehensive test coverage for target functionality

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

---------

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
Anand Krishnamoorthi
2025-08-19 20:23:43 -05:00
committed by GitHub
parent 3c33d31d08
commit cc917ea75d
71 changed files with 10278 additions and 1000 deletions

View File

@@ -140,12 +140,38 @@ jobs:
uses: actions/download-artifact@v4
with:
name: regorus-nuget
path: ./bindings/csharp/Regorus.Tests/regorus-nuget/
path: ./bindings/csharp/regorus-nuget/
- name: Restore Regorus.Tests
run: dotnet restore /p:RestoreAdditionalProjectSources=./regorus-nuget
run: dotnet restore /p:RestoreAdditionalProjectSources=../regorus-nuget
working-directory: ./bindings/csharp/Regorus.Tests
- name: Run Regorus.Tests
run: dotnet test --no-restore
working-directory: ./bindings/csharp/Regorus.Tests
- name: Restore TestApp
run: dotnet restore /p:RestoreAdditionalProjectSources=../regorus-nuget
working-directory: ./bindings/csharp/TestApp
- name: Build TestApp
run: dotnet build --no-restore
working-directory: ./bindings/csharp/TestApp
- name: Run TestApp
run: dotnet run --no-build --framework net8.0
working-directory: ./bindings/csharp/TestApp
- name: Restore TargetExampleApp
run: dotnet restore /p:RestoreAdditionalProjectSources=../regorus-nuget
working-directory: ./bindings/csharp/TargetExampleApp
- name: Build TargetExampleApp
run: dotnet build --no-restore
working-directory: ./bindings/csharp/TargetExampleApp
- name: Run TargetExampleApp
run: dotnet run --no-build --framework net8.0
working-directory: ./bindings/csharp/TargetExampleApp

101
Cargo.lock generated
View File

@@ -98,9 +98,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.98"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
[[package]]
name = "autocfg"
@@ -264,9 +264,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6"
dependencies = [
"heck",
"proc-macro2 1.0.96",
"proc-macro2 1.0.97",
"quote 1.0.40",
"syn 2.0.104",
"syn 2.0.105",
]
[[package]]
@@ -377,9 +377,9 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2 1.0.96",
"proc-macro2 1.0.97",
"quote 1.0.40",
"syn 2.0.104",
"syn 2.0.105",
]
[[package]]
@@ -982,9 +982,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.96"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beef09f85ae72cea1ef96ba6870c51e6382ebfa4f0e85b643459331f3daa5be0"
checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1"
dependencies = [
"unicode-ident",
]
@@ -1004,7 +1004,7 @@ version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2 1.0.96",
"proc-macro2 1.0.97",
]
[[package]]
@@ -1044,9 +1044,9 @@ dependencies = [
[[package]]
name = "rayon"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
@@ -1054,9 +1054,9 @@ dependencies = [
[[package]]
name = "rayon-core"
version = "1.12.1"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
@@ -1086,9 +1086,9 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
dependencies = [
"proc-macro2 1.0.96",
"proc-macro2 1.0.97",
"quote 1.0.40",
"syn 2.0.104",
"syn 2.0.105",
]
[[package]]
@@ -1159,6 +1159,7 @@ dependencies = [
"serde_json",
"serde_yaml",
"test-generator",
"thiserror",
"url",
"uuid",
"walkdir",
@@ -1200,9 +1201,9 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ee4885492bb655bfa05d039cd9163eb8fe9f79ddebf00ca23a1637510c2fd2"
dependencies = [
"proc-macro2 1.0.96",
"proc-macro2 1.0.97",
"quote 1.0.40",
"syn 2.0.104",
"syn 2.0.105",
]
[[package]]
@@ -1232,9 +1233,9 @@ version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2 1.0.96",
"proc-macro2 1.0.97",
"quote 1.0.40",
"syn 2.0.104",
"syn 2.0.105",
]
[[package]]
@@ -1311,11 +1312,11 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.104"
version = "2.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619"
dependencies = [
"proc-macro2 1.0.96",
"proc-macro2 1.0.97",
"quote 1.0.40",
"unicode-ident",
]
@@ -1326,9 +1327,9 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2 1.0.96",
"proc-macro2 1.0.97",
"quote 1.0.40",
"syn 2.0.104",
"syn 2.0.105",
]
[[package]]
@@ -1343,6 +1344,26 @@ dependencies = [
"syn 0.15.44",
]
[[package]]
name = "thiserror"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227"
dependencies = [
"proc-macro2 1.0.97",
"quote 1.0.40",
"syn 2.0.105",
]
[[package]]
name = "tinystr"
version = "0.8.1"
@@ -1478,9 +1499,9 @@ checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2 1.0.96",
"proc-macro2 1.0.97",
"quote 1.0.40",
"syn 2.0.104",
"syn 2.0.105",
"wasm-bindgen-shared",
]
@@ -1500,9 +1521,9 @@ version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2 1.0.96",
"proc-macro2 1.0.97",
"quote 1.0.40",
"syn 2.0.104",
"syn 2.0.105",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -1554,9 +1575,9 @@ version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2 1.0.96",
"proc-macro2 1.0.97",
"quote 1.0.40",
"syn 2.0.104",
"syn 2.0.105",
]
[[package]]
@@ -1565,9 +1586,9 @@ version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2 1.0.96",
"proc-macro2 1.0.97",
"quote 1.0.40",
"syn 2.0.104",
"syn 2.0.105",
]
[[package]]
@@ -1774,9 +1795,9 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [
"proc-macro2 1.0.96",
"proc-macro2 1.0.97",
"quote 1.0.40",
"syn 2.0.104",
"syn 2.0.105",
"synstructure",
]
@@ -1795,9 +1816,9 @@ version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
dependencies = [
"proc-macro2 1.0.96",
"proc-macro2 1.0.97",
"quote 1.0.40",
"syn 2.0.104",
"syn 2.0.105",
]
[[package]]
@@ -1815,9 +1836,9 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2 1.0.96",
"proc-macro2 1.0.97",
"quote 1.0.40",
"syn 2.0.104",
"syn 2.0.105",
"synstructure",
]
@@ -1849,7 +1870,7 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [
"proc-macro2 1.0.96",
"proc-macro2 1.0.97",
"quote 1.0.40",
"syn 2.0.104",
"syn 2.0.105",
]

View File

@@ -93,6 +93,7 @@ anyhow = { version = "1.0.45", default-features = false }
serde = {version = "1.0.150", default-features = false, features = ["derive", "rc"] }
serde_json = { version = "1.0.89", default-features = false, features = ["alloc"] }
lazy_static = { version = "1.4.0", default-features = false }
thiserror = { version = "2.0", default-features = false }
data-encoding = { version = "2.8.0", optional = true, default-features=false, features = ["alloc"] }
scientific = { version = "0.5.3" }

View File

@@ -43,27 +43,27 @@ int main() {
// Turn on rego v0 since policy uses v0.
r = regorus_engine_set_rego_v0(engine, true);
if (r.status != RegorusStatusOk)
if (r.status != Ok)
goto error;
// Load policies.
r = regorus_engine_add_policy(engine, "framework.rego", (buffer = file_to_string("../../../tests/aci/framework.rego")));
free(buffer);
if (r.status != RegorusStatusOk)
if (r.status != Ok)
goto error;
printf("Loaded package %s\n", r.output);
regorus_result_drop(r);
r = regorus_engine_add_policy(engine, "api.rego", (buffer = file_to_string("../../../tests/aci/api.rego")));
free(buffer);
if (r.status != RegorusStatusOk)
if (r.status != Ok)
goto error;
printf("Loaded package %s\n", r.output);
regorus_result_drop(r);
r = regorus_engine_add_policy(engine, "policy.rego", (buffer = file_to_string("../../../tests/aci/policy.rego")));
free(buffer);
if (r.status != RegorusStatusOk)
if (r.status != Ok)
goto error;
printf("Loaded package %s\n", r.output);
regorus_result_drop(r);
@@ -71,20 +71,20 @@ int main() {
// Add data
r = regorus_engine_add_data_json(engine, (buffer = file_to_string("../../../tests/aci/data.json")));
free(buffer);
if (r.status != RegorusStatusOk)
if (r.status != Ok)
goto error;
regorus_result_drop(r);
// Set input
r = regorus_engine_set_input_json(engine, (buffer = file_to_string("../../../tests/aci/input.json")));
free(buffer);
if (r.status != RegorusStatusOk)
if (r.status != Ok)
goto error;
regorus_result_drop(r);
// Eval rule.
r = regorus_engine_eval_rule(engine, "data.framework.mount_overlay");
if (r.status != RegorusStatusOk)
if (r.status != Ok)
goto error;
// Print output

View File

@@ -8,43 +8,43 @@ int main() {
// Turn on rego v0 since policy uses v0.
r = regorus_engine_set_rego_v0(engine, true);
if (r.status != RegorusStatusOk)
if (r.status != Ok)
goto error;
// Load policies.
r = regorus_engine_add_policy_from_file(engine, "../../../tests/aci/framework.rego");
if (r.status != RegorusStatusOk)
if (r.status != Ok)
goto error;
printf("Loaded package %s\n", r.output);
regorus_result_drop(r);
r = regorus_engine_add_policy_from_file(engine, "../../../tests/aci/api.rego");
if (r.status != RegorusStatusOk)
if (r.status != Ok)
goto error;
printf("Loaded package %s\n", r.output);
regorus_result_drop(r);
r = regorus_engine_add_policy_from_file(engine, "../../../tests/aci/policy.rego");
if (r.status != RegorusStatusOk)
if (r.status != Ok)
goto error;
printf("Loaded package %s\n", r.output);
regorus_result_drop(r);
// Add data
r = regorus_engine_add_data_from_json_file(engine, "../../../tests/aci/data.json");
if (r.status != RegorusStatusOk)
if (r.status != Ok)
goto error;
regorus_result_drop(r);
// Set input
r = regorus_engine_set_input_from_json_file(engine, "../../../tests/aci/input.json");
if (r.status != RegorusStatusOk)
if (r.status != Ok)
goto error;
regorus_result_drop(r);
// Eval rule.
r = regorus_engine_eval_query(engine, "data.framework.mount_overlay");
if (r.status != RegorusStatusOk)
if (r.status != Ok)
goto error;
// Print output
@@ -66,14 +66,14 @@ int main() {
);
// Evaluate rule.
if (r.status != RegorusStatusOk)
if (r.status != Ok)
goto error;
r = regorus_engine_set_enable_coverage(engine, true);
regorus_result_drop(r);
r = regorus_engine_eval_query(engine, "data.test.message");
if (r.status != RegorusStatusOk)
if (r.status != Ok)
goto error;
// Print output
@@ -82,7 +82,7 @@ int main() {
// Print pretty coverage report.
r = regorus_engine_get_coverage_report_pretty(engine);
if (r.status != RegorusStatusOk)
if (r.status != Ok)
goto error;
printf("%s\n", r.output);

View File

@@ -11,8 +11,8 @@ namespace regorus {
class Result {
public:
operator bool() const { return result.status == RegorusStatus::RegorusStatusOk; }
bool operator !() const { return result.status != RegorusStatus::RegorusStatusOk; }
operator bool() const { return result.status == RegorusStatus::Ok; }
bool operator !() const { return result.status != RegorusStatus::Ok; }
const char* output() const {
if (*this && result.output) {

421
bindings/csharp/API.md Normal file
View File

@@ -0,0 +1,421 @@
# Regorus C# API Documentation
This document describes the C# API for Regorus, focusing on the compiled policy approach for high-performance policy evaluation.
## Overview
The Regorus C# bindings provide a modern, thread-safe API for compiling and evaluating Open Policy Agent (OPA) Rego policies. The API is designed around pre-compiled policies that can be evaluated efficiently multiple times with different inputs.
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ CompiledPolicy Workflow │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Policy Modules │ │ Target/Schema │ │ Static Data │
│ (.rego files) │ │ Registries │ │ (JSON) │
└─────────┬───────┘ └────────┬─────────┘ └─────────┬───────┘
│ │ │
└─────────────────────┼────────────────────────┘
┌─────────────────────────┐
│ Compile │
│ ┌─────────────────────┐│
│ │ Parse & Analyze ││
│ │ Infer Resource Types││
│ │ Build AST & Rules ││
│ │ Target Integration ││
│ └─────────────────────┘│
└─────────────┬───────────┘
┌─────────────────────────┐
│ CompiledPolicy │
│ ┌─────────────────────┐ │
│ │ AST & Rules │ │
│ │ Target Info │ │
│ │ Resource Types │ │
│ │ Function Table │ │
│ │ Compiled Modules │ │
│ └─────────────────────┘ │
└─────────────┬───────────┘
┌─────────────────────┐
│ Service Cache │
│ (Policy Framework, │
│ MS Graph, etc.) │
│ ┌─────────────────┐ │
│ │ CompiledPolicy │ │ ◄─── Same LOCK-FREE policy
│ │ (cached) │ │ instance shared across
│ └─────────────────┘ │ all threads
└─────────┬───────────┘
┌───────┼───────┬───────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Thread 1 │ │ Thread 2 │ │ Thread N │
│ │ │ │ │ │
│ input1 ────▶│ │ input2 ────▶│ │ inputN ────▶│
│ ◄─── result │ │ ◄─── result │ │ ◄─── result │
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Key Benefits │
├─────────────────────────────────────────────────────────────────┤
│ ✓ Compile Once, Evaluate Many ✓ Lock-Free Concurrent Eval │
│ ✓ No Re-parsing Overhead ✓ Reference Counting Safety │
│ ✓ Reduced GC Pressure ✓ Proper Resource Management │
│ ✓ Cache-Friendly Design ✓ Target System Integration │
└─────────────────────────────────────────────────────────────────┘
```
## Key Features
- **Pre-compiled Policies**: Compile once, evaluate many times for optimal performance
- **Target System Support**: Built-in support for Azure Policy targets with resource type inference
- **Thread Safety**: All operations are thread-safe without external synchronization
- **Registry Management**: Centralized management of targets and schemas
- **Policy Introspection**: Rich metadata about compiled policies
## Core Classes
### CompiledPolicy
The `CompiledPolicy` class represents a pre-compiled Rego policy that can be evaluated efficiently.
```csharp
public sealed class CompiledPolicy : IDisposable
{
// Evaluate the policy with input data
public string? EvalWithInput(string inputJson);
// Get comprehensive policy metadata
public PolicyInfo GetPolicyInfo();
// Dispose of unmanaged resources
public void Dispose();
}
```
**Thread Safety**: All methods are thread-safe. Multiple threads can call `EvalWithInput()` concurrently, and `Dispose()` will safely wait for active evaluations to complete.
### Compiler
The `Compiler` class provides static methods for compiling policies.
```csharp
public static class Compiler
{
// Compile a policy with a specific entrypoint rule
public static CompiledPolicy CompilePolicyWithEntrypoint(
string dataJson,
IEnumerable<PolicyModule> modules,
string entryPointRule);
// Compile a target-aware policy (requires azure_policy feature)
public static CompiledPolicy CompilePolicyForTarget(
string dataJson,
IEnumerable<PolicyModule> modules);
}
```
### PolicyModule
Represents a single policy module to be compiled. Each PolicyModule corresponds to a Rego file (.rego), and each Rego file defines a Rego package using the `package` declaration at the top of the file.
```csharp
public struct PolicyModule
{
public string Id { get; set; }
public string Content { get; set; }
public PolicyModule(string id, string content);
}
```
**Properties:**
- `Id`: A unique identifier for the module, typically the filename (e.g., "policy.rego", "rules/storage.rego")
- `Content`: The complete Rego policy content, including the `package` declaration and all rules
**Example:**
```csharp
var module = new PolicyModule("storage-policy.rego", @"
package azure.storage
import rego.v1
default allow := false
allow if input.type == ""Microsoft.Storage/storageAccounts""
");
```
### PolicyInfo
Provides comprehensive metadata about a compiled policy.
```csharp
public class PolicyInfo
{
// List of module identifiers
public List<string> ModuleIds { get; set; }
// Target name (for target-aware policies)
public string? TargetName { get; set; }
// Resource types this policy can evaluate
public List<string> ApplicableResourceTypes { get; set; }
// Primary rule/entrypoint
public string EntrypointRule { get; set; }
// Effect rule (for target-aware policies)
public string? EffectRule { get; set; }
// Policy parameters
public List<PolicyParameters> Parameters { get; set; }
}
```
## Registry Classes
### TargetRegistry
Manages target definitions for Azure Policy-style evaluations.
```csharp
public static class TargetRegistry
{
// Register a target from JSON
public static void RegisterFromJson(string targetJson);
// Check if a target exists
public static bool Contains(string name);
// List all registered targets
public static string ListNames();
// Remove a target
public static bool Remove(string name);
// Clear all targets
public static void Clear();
// Get count of registered targets
public static int Count { get; }
// Check if registry is empty
public static bool IsEmpty { get; }
}
```
### SchemaRegistry
Manages schema definitions for validation.
```csharp
public static class SchemaRegistry
{
// Register resource schemas
public static void RegisterResourceSchema(string name, string schemaJson);
public static bool ContainsResourceSchema(string name);
public static string ListResourceSchemas();
// Register effect schemas
public static void RegisterEffectSchema(string name, string schemaJson);
public static bool ContainsEffectSchema(string name);
public static string ListEffectSchemas();
// Clear methods
public static void ClearResourceSchemas();
public static void ClearEffectSchemas();
}
```
## Usage Examples
### Basic Policy Compilation and Evaluation
```csharp
// Define policy modules
var modules = new List<PolicyModule>
{
new PolicyModule("policy.rego", @"
package example
import rego.v1
default allow := false
allow if input.user == ""admin""
")
};
// Compile the policy
using var policy = Compiler.CompilePolicyWithEntrypoint("{}", modules, "data.example.allow");
// Evaluate with different inputs
var result1 = policy.EvalWithInput(@"{""user"": ""admin""}"); // true
var result2 = policy.EvalWithInput(@"{""user"": ""guest""}"); // false
```
### Target-Aware Policy (Azure Policy Style)
```csharp
// Register target definition
TargetRegistry.RegisterFromJson(@"{
""name"": ""azure.storage"",
""resource_schema_selector"": ""type"",
""resource_types"": {
""Microsoft.Storage/storageAccounts"": {
""schema"": { /* JSON Schema */ }
}
}
}");
// Define policy with target
var modules = new List<PolicyModule>
{
new PolicyModule("policy.rego", @"
package policy
import rego.v1
__target__ := ""azure.storage""
default effect := ""deny""
effect := ""allow"" if {
input.type == ""Microsoft.Storage/storageAccounts""
input.properties.supportsHttpsTrafficOnly == true
}
")
};
// Compile for target
using var policy = Compiler.CompilePolicyForTarget("{}", modules);
// Evaluate Azure resource
var resource = @"{
""type"": ""Microsoft.Storage/storageAccounts"",
""properties"": {
""supportsHttpsTrafficOnly"": true
}
}";
var result = policy.EvalWithInput(resource); // "allow"
```
### Policy Introspection
```csharp
// Get policy metadata
var info = policy.GetPolicyInfo();
Console.WriteLine($"Target: {info.TargetName}");
Console.WriteLine($"Effect Rule: {info.EffectRule}");
Console.WriteLine($"Modules: {string.Join(", ", info.ModuleIds)}");
Console.WriteLine($"Resource Types: {string.Join(", ", info.ApplicableResourceTypes)}");
// Access parameters
if (info.Parameters != null && info.Parameters.Count > 0)
{
foreach (var parameterSet in info.Parameters)
{
Console.WriteLine($"Module: {parameterSet.SourceFile}");
foreach (var param in parameterSet.Parameters)
{
Console.WriteLine($"Parameter: {param.Name} ({param.Type})");
if (param.Default != null)
Console.WriteLine($" Default: {param.Default}");
}
}
}
```
### Concurrent Evaluation
```csharp
// CompiledPolicy is thread-safe
var tasks = Enumerable.Range(0, 100).Select(i =>
Task.Run(() => policy.EvalWithInput($@"{{""id"": {i}}}"))
).ToArray();
var results = await Task.WhenAll(tasks);
```
## Performance Considerations
### Compilation Overhead
- Policy compilation has significant overhead due to parsing and analysis
- **Best Practice**: Compile once, reuse many times
- Consider caching compiled policies for repeated use
### Memory Management
- `CompiledPolicy` manages unmanaged resources
- **Always** dispose of compiled policies using `using` statements or explicit `Dispose()`
- Disposal is thread-safe and waits for active evaluations
### Thread Safety
- All classes are thread-safe for concurrent reads/evaluations
- Registry modifications should be done during initialization
- No external synchronization required
## Error Handling
All methods throw `Exception` on errors with descriptive messages:
```csharp
try
{
var policy = Compiler.CompilePolicyWithEntrypoint(data, modules, rule);
var result = policy.EvalWithInput(input);
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
```
## Feature Flags
Some functionality requires specific Rust feature flags:
- **azure_policy**: Required for target-aware compilation and policy parameters
- Without this feature, target-related methods will not be available
## Version Compatibility
- Requires .NET Standard 2.0 or later
- Compatible with .NET Framework 4.6.1+, .NET Core 2.0+, .NET 5+
- Uses System.Text.Json for JSON serialization (added as dependency)
## Best Practices
1. **Compile Once, Evaluate Many**: Pre-compile policies for repeated evaluation
2. **Use Disposable Pattern**: Always dispose of CompiledPolicy instances
3. **Thread-Safe Design**: Take advantage of built-in thread safety
4. **Registry Setup**: Configure targets and schemas during application startup
5. **Error Handling**: Wrap operations in try-catch blocks for robust error handling
6. **Performance Monitoring**: Monitor evaluation times for performance optimization
## Migration from Engine-Based API
If migrating from an engine-based approach:
```csharp
// Old approach (if it existed)
// var engine = new Engine();
// engine.AddPolicy("policy.rego", policyContent);
// engine.SetInputJson(inputJson);
// var result = engine.EvalRule("data.policy.allow");
// New compiled approach
var modules = new[] { new PolicyModule("policy.rego", policyContent) };
using var policy = Compiler.CompilePolicyWithEntrypoint("{}", modules, "data.policy.allow");
var result = policy.EvalWithInput(inputJson);
```
The compiled approach provides better performance for repeated evaluations and clearer resource management.

View File

@@ -0,0 +1,168 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Text;
using System.Text.Json;
#nullable enable
namespace Regorus
{
/// <summary>
/// Represents a compiled Regorus policy that can be evaluated efficiently.
/// This class wraps a pre-compiled policy that can be evaluated multiple times
/// with different inputs without recompilation overhead.
///
/// This class manages unmanaged resources and should not be copied or cloned.
/// Each instance represents a unique native policy object.
///
/// Thread Safety: This class is thread-safe for all operations. Multiple threads
/// can safely call EvalWithInput() concurrently, and Dispose() will safely wait
/// for all active evaluations to complete before freeing resources. No external
/// synchronization is required.
/// </summary>
public unsafe sealed class CompiledPolicy : IDisposable
{
private Internal.RegorusCompiledPolicy* _policy;
private int _isDisposed;
private int _activeEvaluations;
internal CompiledPolicy(Internal.RegorusCompiledPolicy* policy)
{
_policy = policy;
}
/// <summary>
/// Evaluates the compiled policy with the given input.
/// For target policies, evaluates the target's effect rule.
/// For regular policies, evaluates the originally compiled rule.
/// </summary>
/// <param name="inputJson">JSON encoded input data (resource) to validate against the policy</param>
/// <returns>The evaluation result as JSON string</returns>
/// <exception cref="Exception">Thrown when policy evaluation fails</exception>
/// <exception cref="ObjectDisposedException">Thrown when the policy has been disposed</exception>
public string? EvalWithInput(string inputJson)
{
// Increment active evaluations count
System.Threading.Interlocked.Increment(ref _activeEvaluations);
try
{
ThrowIfDisposed();
var inputBytes = Encoding.UTF8.GetBytes(inputJson + char.MinValue);
fixed (byte* inputPtr = inputBytes)
{
return CheckAndDropResult(Internal.API.regorus_compiled_policy_eval_with_input(_policy, inputPtr));
}
}
finally
{
// Decrement active evaluations count
System.Threading.Interlocked.Decrement(ref _activeEvaluations);
}
}
/// <summary>
/// Gets information about the compiled policy including metadata about modules,
/// target configuration, and resource types.
/// </summary>
/// <returns>Policy information containing module IDs, target name, applicable resource types, entry point rule, and parameters</returns>
/// <exception cref="Exception">Thrown when getting policy info fails</exception>
/// <exception cref="ObjectDisposedException">Thrown when the policy has been disposed</exception>
public PolicyInfo GetPolicyInfo()
{
ThrowIfDisposed();
var jsonResult = CheckAndDropResult(Internal.API.regorus_compiled_policy_get_policy_info(_policy));
if (string.IsNullOrEmpty(jsonResult))
{
throw new Exception("Failed to get policy info: empty response");
}
try
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
return JsonSerializer.Deserialize<PolicyInfo>(jsonResult!, options)
?? throw new Exception("Failed to deserialize policy info");
}
catch (JsonException ex)
{
throw new Exception($"Failed to parse policy info JSON: {ex.Message}", ex);
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (System.Threading.Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0)
{
if (_policy != null)
{
// Wait for all active evaluations to complete
while (System.Threading.Volatile.Read(ref _activeEvaluations) > 0)
{
System.Threading.Thread.Yield();
}
Internal.API.regorus_compiled_policy_drop(_policy);
_policy = null;
}
}
}
~CompiledPolicy() => Dispose(disposing: false);
private void ThrowIfDisposed()
{
if (_isDisposed != 0)
throw new ObjectDisposedException(nameof(CompiledPolicy));
}
private string? StringFromUTF8(IntPtr ptr)
{
#if NETSTANDARD2_1
return System.Runtime.InteropServices.Marshal.PtrToStringUTF8(ptr);
#else
int len = 0;
while (System.Runtime.InteropServices.Marshal.ReadByte(ptr, len) != 0) { ++len; }
byte[] buffer = new byte[len];
System.Runtime.InteropServices.Marshal.Copy(ptr, buffer, 0, buffer.Length);
return Encoding.UTF8.GetString(buffer);
#endif
}
private string? CheckAndDropResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
}
return result.data_type switch
{
Internal.RegorusDataType.String => StringFromUTF8((IntPtr)result.output),
Internal.RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(),
Internal.RegorusDataType.Integer => result.int_value.ToString(),
Internal.RegorusDataType.None => null,
_ => StringFromUTF8((IntPtr)result.output)
};
}
finally
{
Internal.API.regorus_result_drop(result);
}
}
}
}

View File

@@ -0,0 +1,196 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
#nullable enable
namespace Regorus
{
/// <summary>
/// Represents a policy module with an ID and content.
/// </summary>
public struct PolicyModule
{
/// <summary>
/// Gets or sets the unique identifier for this policy module.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Gets or sets the Rego policy content.
/// </summary>
public string Content { get; set; }
/// <summary>
/// Initializes a new instance of the PolicyModule struct.
/// </summary>
/// <param name="id">The unique identifier for this policy module</param>
/// <param name="content">The Rego policy content</param>
public PolicyModule(string id, string content)
{
Id = id;
Content = content;
}
}
/// <summary>
/// Provides static methods for compiling policies into efficient compiled representations.
/// These are convenience methods that create an engine internally and perform compilation.
/// </summary>
public static unsafe class Compiler
{
/// <summary>
/// Compiles a policy from data and modules with a specific entry point rule.
/// This is a convenience function that sets up an Engine internally and calls the appropriate compilation method.
/// </summary>
/// <param name="dataJson">JSON string containing static data for policy evaluation</param>
/// <param name="modules">List of policy modules to compile</param>
/// <param name="entryPointRule">The specific rule path to evaluate (e.g., "data.policy.allow")</param>
/// <returns>A compiled policy that can be evaluated efficiently</returns>
/// <exception cref="Exception">Thrown when compilation fails</exception>
public static CompiledPolicy CompilePolicyWithEntrypoint(string dataJson, IEnumerable<PolicyModule> modules, string entryPointRule)
{
var dataBytes = Encoding.UTF8.GetBytes(dataJson + char.MinValue);
var entryPointBytes = Encoding.UTF8.GetBytes(entryPointRule + char.MinValue);
var modulesArray = modules.ToArray();
// Convert C# modules to native structs
var nativeModules = new Internal.RegorusPolicyModule[modulesArray.Length];
var pinnedHandles = new List<GCHandle>();
try
{
for (int i = 0; i < modulesArray.Length; i++)
{
var idBytes = Encoding.UTF8.GetBytes(modulesArray[i].Id + char.MinValue);
var contentBytes = Encoding.UTF8.GetBytes(modulesArray[i].Content + char.MinValue);
var idHandle = GCHandle.Alloc(idBytes, GCHandleType.Pinned);
var contentHandle = GCHandle.Alloc(contentBytes, GCHandleType.Pinned);
pinnedHandles.Add(idHandle);
pinnedHandles.Add(contentHandle);
nativeModules[i] = new Internal.RegorusPolicyModule
{
id = (byte*)idHandle.AddrOfPinnedObject(),
content = (byte*)contentHandle.AddrOfPinnedObject()
};
}
fixed (byte* dataPtr = dataBytes)
fixed (byte* entryPointPtr = entryPointBytes)
fixed (Internal.RegorusPolicyModule* modulesPtr = nativeModules)
{
var result = Internal.API.regorus_compile_policy_with_entrypoint(
dataPtr, modulesPtr, (UIntPtr)modulesArray.Length, entryPointPtr);
var policy = GetCompiledPolicyResult(result);
return policy;
}
}
finally
{
foreach (var handle in pinnedHandles)
{
handle.Free();
}
}
}
/// <summary>
/// Compiles a target-aware policy from data and modules.
/// This is a convenience function that sets up an Engine internally and calls target-aware compilation.
/// At least one module must contain a `__target__` declaration.
/// </summary>
/// <param name="dataJson">JSON string containing static data for policy evaluation</param>
/// <param name="modules">List of policy modules to compile</param>
/// <returns>A compiled policy that can be evaluated efficiently</returns>
/// <exception cref="Exception">Thrown when compilation fails</exception>
public static CompiledPolicy CompilePolicyForTarget(string dataJson, IEnumerable<PolicyModule> modules)
{
var dataBytes = Encoding.UTF8.GetBytes(dataJson + char.MinValue);
var modulesArray = modules.ToArray();
// Convert C# modules to native structs
var nativeModules = new Internal.RegorusPolicyModule[modulesArray.Length];
var pinnedHandles = new List<GCHandle>();
try
{
for (int i = 0; i < modulesArray.Length; i++)
{
var idBytes = Encoding.UTF8.GetBytes(modulesArray[i].Id + char.MinValue);
var contentBytes = Encoding.UTF8.GetBytes(modulesArray[i].Content + char.MinValue);
var idHandle = GCHandle.Alloc(idBytes, GCHandleType.Pinned);
var contentHandle = GCHandle.Alloc(contentBytes, GCHandleType.Pinned);
pinnedHandles.Add(idHandle);
pinnedHandles.Add(contentHandle);
nativeModules[i] = new Internal.RegorusPolicyModule
{
id = (byte*)idHandle.AddrOfPinnedObject(),
content = (byte*)contentHandle.AddrOfPinnedObject()
};
}
fixed (byte* dataPtr = dataBytes)
fixed (Internal.RegorusPolicyModule* modulesPtr = nativeModules)
{
var result = Internal.API.regorus_compile_policy_for_target(
dataPtr, modulesPtr, (UIntPtr)modulesArray.Length);
var policy = GetCompiledPolicyResult(result);
return policy;
}
}
finally
{
foreach (var handle in pinnedHandles)
{
handle.Free();
}
}
}
private static string? StringFromUTF8(IntPtr ptr)
{
#if NETSTANDARD2_1
return System.Runtime.InteropServices.Marshal.PtrToStringUTF8(ptr);
#else
int len = 0;
while (System.Runtime.InteropServices.Marshal.ReadByte(ptr, len) != 0) { ++len; }
byte[] buffer = new byte[len];
System.Runtime.InteropServices.Marshal.Copy(ptr, buffer, 0, buffer.Length);
return Encoding.UTF8.GetString(buffer);
#endif
}
private static CompiledPolicy GetCompiledPolicyResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown compilation error occurred");
}
if (result.data_type != Internal.RegorusDataType.Pointer || result.pointer_value == null)
{
throw new Exception("Expected compiled policy pointer but got different data type");
}
return new CompiledPolicy((Internal.RegorusCompiledPolicy*)result.pointer_value);
}
finally
{
Internal.API.regorus_result_drop(result);
}
}
}
}

View File

@@ -243,7 +243,7 @@ namespace Regorus
string? CheckAndDropResult(Regorus.Internal.RegorusResult result)
{
if (result.status != Regorus.Internal.RegorusStatus.RegorusStatusOk)
if (result.status != Regorus.Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
var ex = new Exception(message);

View File

@@ -0,0 +1,546 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Runtime.InteropServices;
#pragma warning disable CS8500
#pragma warning disable CS8981
namespace Regorus.Internal
{
/// <summary>
/// Native FFI method declarations for Regorus.
/// This file contains all P/Invoke declarations for the Regorus native library.
/// </summary>
internal static unsafe partial class API
{
private const string LibraryName = "regorus_ffi";
#region Common Methods
/// <summary>
/// Drop a RegorusResult.
/// output and error_message strings are not valid after drop.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_result_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_result_drop(RegorusResult result);
#endregion
#region Engine Methods
/// <summary>
/// Construct a new Engine.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusEngine* regorus_engine_new();
/// <summary>
/// Clone a RegorusEngine.
/// To avoid having to parse same policy again, the engine can be cloned
/// after policies and data have been added.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_clone", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusEngine* regorus_engine_clone(RegorusEngine* engine);
/// <summary>
/// Drop a RegorusEngine.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_engine_drop(RegorusEngine* engine);
/// <summary>
/// Add a policy.
/// The policy is parsed into AST.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_policy
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_add_policy", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_add_policy(RegorusEngine* engine, byte* path, byte* rego);
/// <summary>
/// Add a policy from file.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_add_policy_from_file", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_add_policy_from_file(RegorusEngine* engine, byte* path);
/// <summary>
/// Add policy data.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_data
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_add_data_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_add_data_json(RegorusEngine* engine, byte* data);
/// <summary>
/// Get list of loaded Rego packages as JSON.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_packages
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_get_packages", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_get_packages(RegorusEngine* engine);
/// <summary>
/// Get list of policies as JSON.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_policies
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_get_policies", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_get_policies(RegorusEngine* engine);
/// <summary>
/// Add data from JSON file.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_add_data_from_json_file", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_add_data_from_json_file(RegorusEngine* engine, byte* path);
/// <summary>
/// Clear policy data.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.clear_data
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_clear_data", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_clear_data(RegorusEngine* engine);
/// <summary>
/// Set input.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_input
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_set_input_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_set_input_json(RegorusEngine* engine, byte* input);
/// <summary>
/// Set input from JSON file.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_set_input_from_json_file", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_set_input_from_json_file(RegorusEngine* engine, byte* path);
/// <summary>
/// Evaluate query.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.eval_query
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_eval_query", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_eval_query(RegorusEngine* engine, byte* query);
/// <summary>
/// Evaluate specified rule.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.eval_rule
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_eval_rule", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_eval_rule(RegorusEngine* engine, byte* rule);
/// <summary>
/// Enable/disable coverage.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_enable_coverage
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_set_enable_coverage", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_set_enable_coverage(RegorusEngine* engine, [MarshalAs(UnmanagedType.U1)] bool enable);
/// <summary>
/// Get coverage report.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_coverage_report
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_get_coverage_report", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_get_coverage_report(RegorusEngine* engine);
/// <summary>
/// Enable/disable strict builtin errors.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_strict_builtin_errors
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_set_strict_builtin_errors", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_set_strict_builtin_errors(RegorusEngine* engine, [MarshalAs(UnmanagedType.U1)] bool strict);
/// <summary>
/// Get pretty printed coverage report.
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Report.html#method.to_string_pretty
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_get_coverage_report_pretty", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_get_coverage_report_pretty(RegorusEngine* engine);
/// <summary>
/// Clear coverage data.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.clear_coverage_data
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_clear_coverage_data", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_clear_coverage_data(RegorusEngine* engine);
/// <summary>
/// Whether to gather output of print statements.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_gather_prints
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_set_gather_prints", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_set_gather_prints(RegorusEngine* engine, [MarshalAs(UnmanagedType.U1)] bool enable);
/// <summary>
/// Take all the gathered print statements.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.take_prints
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_take_prints", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_take_prints(RegorusEngine* engine);
/// <summary>
/// Get AST of policies.
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Engine.html#method.get_ast_as_json
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_get_ast_as_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_get_ast_as_json(RegorusEngine* engine);
/// <summary>
/// Gets the package names defined in each policy added to the engine.
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Engine.html#method.get_policy_package_names
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_get_policy_package_names", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_get_policy_package_names(RegorusEngine* engine);
/// <summary>
/// Gets the parameters defined in each policy added to the engine.
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Engine.html#method.get_policy_parameters
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_get_policy_parameters", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_get_policy_parameters(RegorusEngine* engine);
/// <summary>
/// Enable/disable rego v1.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_rego_v0
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_set_rego_v0", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_set_rego_v0(RegorusEngine* engine, [MarshalAs(UnmanagedType.U1)] bool enable);
/// <summary>
/// Compile a target-aware policy from the current engine state.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.compile_for_target
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_compile_for_target", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_compile_for_target(RegorusEngine* engine);
/// <summary>
/// Compile a policy with a specific entry point rule.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.compile_with_entrypoint
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_compile_with_entrypoint", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_compile_with_entrypoint(RegorusEngine* engine, byte* rule);
#endregion
#region Compilation Methods
/// <summary>
/// Compiles a policy from data and modules with a specific entry point rule.
/// This is a convenience function that wraps regorus::compile_policy_with_entrypoint.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_compile_policy_with_entrypoint", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_compile_policy_with_entrypoint(byte* data_json, RegorusPolicyModule* modules, UIntPtr modules_len, byte* entry_point_rule);
/// <summary>
/// Compiles a target-aware policy from data and modules.
/// This is a convenience function that wraps regorus::compile_policy_for_target.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_compile_policy_for_target", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_compile_policy_for_target(byte* data_json, RegorusPolicyModule* modules, UIntPtr modules_len);
#endregion
#region Compiled Policy Methods
/// <summary>
/// Drop a RegorusCompiledPolicy.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_compiled_policy_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_compiled_policy_drop(RegorusCompiledPolicy* compiled_policy);
/// <summary>
/// Evaluate the compiled policy with the given input.
/// For target policies, evaluates the target's effect rule.
/// For regular policies, evaluates the originally compiled rule.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_compiled_policy_eval_with_input", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_compiled_policy_eval_with_input(RegorusCompiledPolicy* compiled_policy, byte* input);
/// <summary>
/// Get information about the compiled policy including metadata about modules,
/// target configuration, and resource types.
/// Returns a JSON-encoded PolicyInfo struct containing comprehensive
/// information about the compiled policy such as module IDs, target name,
/// applicable resource types, entry point rule, and parameters.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_compiled_policy_get_policy_info", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_compiled_policy_get_policy_info(RegorusCompiledPolicy* compiled_policy);
#endregion
#region Target Registry Methods
/// <summary>
/// Register a target from JSON definition.
/// The target JSON should follow the target schema format.
/// Once registered, the target can be referenced in Rego policies using __target__ rules.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_register_target_from_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_register_target_from_json(byte* target_json);
/// <summary>
/// Check if a target is registered.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_target_registry_contains", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_target_registry_contains(byte* name);
/// <summary>
/// Get a list of all registered target names as JSON array.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_target_registry_list_names", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_target_registry_list_names();
/// <summary>
/// Remove a target from the registry by name.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_target_registry_remove", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_target_registry_remove(byte* name);
/// <summary>
/// Clear all targets from the registry.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_target_registry_clear", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_target_registry_clear();
/// <summary>
/// Get the number of registered targets.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_target_registry_len", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_target_registry_len();
/// <summary>
/// Check if the target registry is empty.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_target_registry_is_empty", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_target_registry_is_empty();
#endregion
#region Resource Schema Registry Methods
/// <summary>
/// Register a resource schema from JSON with a given name.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_resource_schema_register", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_resource_schema_register(byte* name, byte* schema_json);
/// <summary>
/// Check if a resource schema with the given name exists.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_resource_schema_contains", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_resource_schema_contains(byte* name);
/// <summary>
/// Get the number of registered resource schemas.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_resource_schema_len", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_resource_schema_len();
/// <summary>
/// Check if the resource schema registry is empty.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_resource_schema_is_empty", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_resource_schema_is_empty();
/// <summary>
/// List all registered resource schema names as a JSON array.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_resource_schema_list_names", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_resource_schema_list_names();
/// <summary>
/// Remove a resource schema by name.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_resource_schema_remove", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_resource_schema_remove(byte* name);
/// <summary>
/// Clear all resource schemas from the registry.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_resource_schema_clear", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_resource_schema_clear();
#endregion
#region Effect Schema Registry Methods
/// <summary>
/// Register an effect schema from JSON with a given name.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_effect_schema_register", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_effect_schema_register(byte* name, byte* schema_json);
/// <summary>
/// Check if an effect schema with the given name exists.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_effect_schema_contains", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_effect_schema_contains(byte* name);
/// <summary>
/// Get the number of registered effect schemas.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_effect_schema_len", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_effect_schema_len();
/// <summary>
/// Check if the effect schema registry is empty.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_effect_schema_is_empty", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_effect_schema_is_empty();
/// <summary>
/// List all registered effect schema names as a JSON array.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_effect_schema_list_names", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_effect_schema_list_names();
/// <summary>
/// Remove an effect schema by name.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_effect_schema_remove", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_effect_schema_remove(byte* name);
/// <summary>
/// Clear all effect schemas from the registry.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_effect_schema_clear", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_effect_schema_clear();
#endregion
}
#region Native Structures
/// <summary>
/// Type of data contained in RegorusResult.
/// </summary>
internal enum RegorusDataType : uint
{
/// <summary>
/// No data / void.
/// </summary>
None,
/// <summary>
/// String data (output field is valid).
/// </summary>
String,
/// <summary>
/// Boolean data (bool_value field is valid).
/// </summary>
Boolean,
/// <summary>
/// Integer data (int_value field is valid).
/// </summary>
Integer,
/// <summary>
/// Pointer data (pointer_value field is valid).
/// </summary>
Pointer,
}
/// <summary>
/// Status of a call on RegorusEngine.
/// </summary>
internal enum RegorusStatus : uint
{
/// <summary>
/// The operation was successful.
/// </summary>
Ok,
/// <summary>
/// The operation was unsuccessful.
/// </summary>
Error,
/// <summary>
/// Invalid data format provided.
/// </summary>
InvalidDataFormat,
/// <summary>
/// Invalid entrypoint rule specified.
/// </summary>
InvalidEntrypoint,
/// <summary>
/// Compilation failed.
/// </summary>
CompilationFailed,
/// <summary>
/// Invalid argument provided.
/// </summary>
InvalidArgument,
/// <summary>
/// Invalid module ID.
/// </summary>
InvalidModuleId,
/// <summary>
/// Invalid policy content.
/// </summary>
InvalidPolicy,
}
/// <summary>
/// Result of a call on RegorusEngine.
/// Must be freed using regorus_result_drop.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct RegorusResult
{
/// <summary>
/// Status.
/// </summary>
public RegorusStatus status;
/// <summary>
/// Type of data contained in this result.
/// </summary>
public RegorusDataType data_type;
/// <summary>
/// String output produced by the call.
/// Valid when data_type is String. Owned by Rust.
/// </summary>
public byte* output;
/// <summary>
/// Boolean value.
/// Valid when data_type is Boolean.
/// </summary>
public bool bool_value;
/// <summary>
/// Integer value.
/// Valid when data_type is Integer.
/// </summary>
public long int_value;
/// <summary>
/// Pointer value.
/// Valid when data_type is Pointer.
/// </summary>
public void* pointer_value;
/// <summary>
/// Errors produced by the call.
/// Owned by Rust.
/// </summary>
public byte* error_message;
}
/// <summary>
/// Wrapper for regorus::Engine.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct RegorusEngine
{
}
/// <summary>
/// Wrapper for regorus::CompiledPolicy.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct RegorusCompiledPolicy
{
}
/// <summary>
/// FFI wrapper for PolicyModule struct.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct RegorusPolicyModule
{
public byte* id;
public byte* content;
}
#endregion
}

View File

@@ -0,0 +1,141 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Generic;
using System.Text.Json.Serialization;
#nullable enable
namespace Regorus
{
/// <summary>
/// Information about a compiled policy, including metadata about modules,
/// target configuration, and resource types that the policy can evaluate.
/// </summary>
public class PolicyInfo
{
/// <summary>
/// List of module identifiers that were compiled into this policy.
/// Each module ID represents a unique policy module that contributes
/// rules, functions, or data to the compiled policy.
/// </summary>
[JsonPropertyName("module_ids")]
public List<string> ModuleIds { get; set; } = new List<string>();
/// <summary>
/// Name of the target configuration used during compilation, if any.
/// This indicates which target schema and validation rules were applied.
/// </summary>
[JsonPropertyName("target_name")]
public string? TargetName { get; set; }
/// <summary>
/// List of resource types that this policy can evaluate.
/// For target-aware policies, this contains the inferred or configured
/// resource types. For general policies, this may be empty.
/// </summary>
[JsonPropertyName("applicable_resource_types")]
public List<string> ApplicableResourceTypes { get; set; } = new List<string>();
/// <summary>
/// The primary rule or entrypoint that this policy evaluates.
/// This is the rule path that will be executed when the policy runs.
/// </summary>
[JsonPropertyName("entrypoint_rule")]
public string EntrypointRule { get; set; } = string.Empty;
/// <summary>
/// The effect rule name for target-aware policies, if applicable.
/// This is the specific effect rule (e.g., "effect", "allow", "deny")
/// that determines the policy decision for target evaluation.
/// </summary>
[JsonPropertyName("effect_rule")]
public string? EffectRule { get; set; }
/// <summary>
/// Parameters that can be configured for this policy.
/// Contains parameter names and their expected types or default values.
/// Used for parameterized policies that accept configuration at evaluation time.
/// Each element represents parameters from a different module.
/// </summary>
[JsonPropertyName("parameters")]
public List<PolicyParameters> Parameters { get; set; } = new List<PolicyParameters>();
}
/// <summary>
/// Parameters that can be configured for a policy.
/// </summary>
public class PolicyParameters
{
/// <summary>
/// Source file where the parameters are defined.
/// </summary>
[JsonPropertyName("source_file")]
public string SourceFile { get; set; } = string.Empty;
/// <summary>
/// List of parameter definitions.
/// </summary>
[JsonPropertyName("parameters")]
public List<PolicyParameter> Parameters { get; set; } = new List<PolicyParameter>();
/// <summary>
/// List of parameter modifiers.
/// </summary>
[JsonPropertyName("modifiers")]
public List<PolicyParameterModifier> Modifiers { get; set; } = new List<PolicyParameterModifier>();
}
/// <summary>
/// A single parameter definition.
/// </summary>
public class PolicyParameter
{
/// <summary>
/// Name of the parameter.
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Type of the parameter.
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;
/// <summary>
/// Default value of the parameter, if any.
/// </summary>
[JsonPropertyName("default")]
public object? Default { get; set; }
/// <summary>
/// Description of the parameter.
/// </summary>
[JsonPropertyName("description")]
public string? Description { get; set; }
/// <summary>
/// Allowed values for the parameter, if constrained.
/// </summary>
[JsonPropertyName("allowed_values")]
public List<object>? AllowedValues { get; set; }
}
/// <summary>
/// A parameter modifier that affects parameter behavior.
/// </summary>
public class PolicyParameterModifier
{
/// <summary>
/// Name of the modifier.
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Value of the modifier.
/// </summary>
[JsonPropertyName("value")]
public object? Value { get; set; }
}
}

View File

@@ -13,6 +13,10 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="8.0.0" />
</ItemGroup>
<!--
$(RegorusFFIArtifactsDir) is the location where regorus shared libraries have been
built for various platforms and copied to. RegorusFFIArtifactsDir is passed in
@@ -20,8 +24,10 @@
For each target triple, `Pack` expects the regorus ffi shared library
to be found in $(RegorusFFIArtifactsDir)/<target-triple>/release.
If $(IgnoreMissingArtifacts) is not set, ensure that the binaries for officially supported platforms exists.
-->
<Target Name="ChecksRegorusFFIArtifactsDir" BeforeTargets="Pack">
<Target Name="ChecksRegorusFFIArtifactsDir" BeforeTargets="Pack" Condition="'$(IgnoreMissingArtifacts)' == ''">
<Error Text="RegorusFFIArtifactsDir must be supplied." Condition="$(RegorusFFIArtifactsDir) == ''" />
<!-- Ensure that the binaries for officially supported platforms exists. -->

View File

@@ -1,244 +0,0 @@
// <auto-generated>
// This code is generated by csbindgen.
// DON'T CHANGE THIS DIRECTLY.
// </auto-generated>
#pragma warning disable CS8500
#pragma warning disable CS8981
using System;
using System.Runtime.InteropServices;
namespace Regorus.Internal
{
internal static unsafe partial class API
{
const string __DllName = "regorus_ffi";
/// <summary>
/// Drop a `RegorusResult`.
///
/// `output` and `error_message` strings are not valid after drop.
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_result_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_result_drop(RegorusResult r);
/// <summary>
/// Construct a new Engine
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusEngine* regorus_engine_new();
/// <summary>
/// Clone a [`RegorusEngine`]
///
/// To avoid having to parse same policy again, the engine can be cloned
/// after policies and data have been added.
///
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_clone", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusEngine* regorus_engine_clone(RegorusEngine* engine);
[DllImport(__DllName, EntryPoint = "regorus_engine_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_engine_drop(RegorusEngine* engine);
/// <summary>
/// Add a policy
///
/// The policy is parsed into AST.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_policy
///
/// * `path`: A filename to be associated with the policy.
/// * `rego`: Rego policy.
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_add_policy", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_add_policy(RegorusEngine* engine, byte* path, byte* rego);
[DllImport(__DllName, EntryPoint = "regorus_engine_add_policy_from_file", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_add_policy_from_file(RegorusEngine* engine, byte* path);
/// <summary>
/// Add policy data.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_data
/// * `data`: JSON encoded value to be used as policy data.
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_add_data_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_add_data_json(RegorusEngine* engine, byte* data);
/// <summary>
/// Get list of loaded Rego packages as JSON.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_packages
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_get_packages", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_get_packages(RegorusEngine* engine);
/// <summary>
/// Get list of policies as JSON.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_policies
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_get_policies", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_get_policies(RegorusEngine* engine);
[DllImport(__DllName, EntryPoint = "regorus_engine_add_data_from_json_file", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_add_data_from_json_file(RegorusEngine* engine, byte* path);
/// <summary>
/// Clear policy data.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.clear_data
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_clear_data", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_clear_data(RegorusEngine* engine);
/// <summary>
/// Set input.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_input
/// * `input`: JSON encoded value to be used as input to query.
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_set_input_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_set_input_json(RegorusEngine* engine, byte* input);
[DllImport(__DllName, EntryPoint = "regorus_engine_set_input_from_json_file", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_set_input_from_json_file(RegorusEngine* engine, byte* path);
/// <summary>
/// Evaluate query.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.eval_query
/// * `query`: Rego expression to be evaluate.
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_eval_query", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_eval_query(RegorusEngine* engine, byte* query);
/// <summary>
/// Evaluate specified rule.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.eval_rule
/// * `rule`: Path to the rule.
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_eval_rule", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_eval_rule(RegorusEngine* engine, byte* rule);
/// <summary>
/// Enable/disable coverage.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_enable_coverage
/// * `enable`: Whether to enable or disable coverage.
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_set_enable_coverage", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_set_enable_coverage(RegorusEngine* engine, [MarshalAs(UnmanagedType.U1)] bool enable);
/// <summary>
/// Get coverage report.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_coverage_report
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_get_coverage_report", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_get_coverage_report(RegorusEngine* engine);
/// <summary>
/// Enable/disable strict builtin errors.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_strict_builtin_errors
/// * `strict`: Whether to raise errors or return undefined on certain scenarios.
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_set_strict_builtin_errors", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_set_strict_builtin_errors(RegorusEngine* engine, [MarshalAs(UnmanagedType.U1)] bool strict);
/// <summary>
/// Get pretty printed coverage report.
///
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Report.html#method.to_string_pretty
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_get_coverage_report_pretty", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_get_coverage_report_pretty(RegorusEngine* engine);
/// <summary>
/// Clear coverage data.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.clear_coverage_data
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_clear_coverage_data", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_clear_coverage_data(RegorusEngine* engine);
/// <summary>
/// Whether to gather output of print statements.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_gather_prints
/// * `enable`: Whether to enable or disable gathering print statements.
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_set_gather_prints", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_set_gather_prints(RegorusEngine* engine, [MarshalAs(UnmanagedType.U1)] bool enable);
/// <summary>
/// Take all the gathered print statements.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.take_prints
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_take_prints", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_take_prints(RegorusEngine* engine);
/// <summary>
/// Get AST of policies.
///
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Engine.html#method.get_ast_as_json
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_get_ast_as_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_get_ast_as_json(RegorusEngine* engine);
/// <summary>
/// Gets the package names of policies added to the engine.
///
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Engine.html#method.get_policy_package_names
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_get_policy_package_names", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_get_policy_package_names(RegorusEngine* engine);
/// <summary>
/// Gets the parameters defined in each policy added to the engine
///
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Engine.html#method.get_policy_parameters
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_get_policy_parameters", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_get_policy_parameters(RegorusEngine* engine);
/// <summary>
/// Enable/disable rego v1.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_rego_v0
/// </summary>
[DllImport(__DllName, EntryPoint = "regorus_engine_set_rego_v0", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_set_rego_v0(RegorusEngine* engine, [MarshalAs(UnmanagedType.U1)] bool enable);
}
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct RegorusResult
{
public RegorusStatus status;
public byte* output;
public byte* error_message;
}
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct RegorusEngine
{
}
internal enum RegorusStatus : uint
{
RegorusStatusOk,
RegorusStatusError,
}
}

View File

@@ -0,0 +1,284 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Text;
#nullable enable
namespace Regorus
{
/// <summary>
/// Provides static methods for managing the global resource schema registry.
/// Resource schemas define the structure and validation rules for Azure Policy resources.
/// </summary>
public static unsafe class SchemaRegistry
{
/// <summary>
/// Register a resource schema from JSON with a given name.
/// </summary>
/// <param name="name">Name to register the schema under</param>
/// <param name="schemaJson">JSON string representing the schema</param>
/// <exception cref="Exception">Thrown when schema registration fails</exception>
public static void RegisterResource(string name, string schemaJson)
{
var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue);
var schemaBytes = Encoding.UTF8.GetBytes(schemaJson + char.MinValue);
fixed (byte* namePtr = nameBytes)
fixed (byte* schemaPtr = schemaBytes)
{
CheckAndDropResult(Internal.API.regorus_resource_schema_register(namePtr, schemaPtr));
}
}
/// <summary>
/// Check if a resource schema with the given name exists.
/// </summary>
/// <param name="name">Name of the schema to check</param>
/// <returns>True if the schema exists, false otherwise</returns>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static bool ContainsResource(string name)
{
var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue);
fixed (byte* namePtr = nameBytes)
{
var result = Internal.API.regorus_resource_schema_contains(namePtr);
return GetBoolResult(result);
}
}
/// <summary>
/// Get the number of registered resource schemas.
/// </summary>
/// <returns>The number of registered resource schemas</returns>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static long ResourceCount
{
get
{
var result = Internal.API.regorus_resource_schema_len();
return GetIntResult(result);
}
}
/// <summary>
/// Check if the resource schema registry is empty.
/// </summary>
/// <returns>True if the registry is empty, false otherwise</returns>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static bool IsResourceRegistryEmpty
{
get
{
var result = Internal.API.regorus_resource_schema_is_empty();
return GetBoolResult(result);
}
}
/// <summary>
/// List all registered resource schema names.
/// </summary>
/// <returns>JSON array of schema names</returns>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static string ListResourceNames()
{
return CheckAndDropResult(Internal.API.regorus_resource_schema_list_names()) ?? "[]";
}
/// <summary>
/// Remove a resource schema by name.
/// </summary>
/// <param name="name">Name of the schema to remove</param>
/// <returns>True if the schema was removed, false if it wasn't found</returns>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static bool RemoveResource(string name)
{
var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue);
fixed (byte* namePtr = nameBytes)
{
var result = Internal.API.regorus_resource_schema_remove(namePtr);
return GetBoolResult(result);
}
}
/// <summary>
/// Clear all resource schemas from the registry.
/// </summary>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static void ClearResources()
{
CheckAndDropResult(Internal.API.regorus_resource_schema_clear());
}
/// <summary>
/// Register an effect schema from JSON with a given name.
/// </summary>
/// <param name="name">Name to register the schema under</param>
/// <param name="schemaJson">JSON string representing the schema</param>
/// <exception cref="Exception">Thrown when schema registration fails</exception>
public static void RegisterEffect(string name, string schemaJson)
{
var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue);
var schemaBytes = Encoding.UTF8.GetBytes(schemaJson + char.MinValue);
fixed (byte* namePtr = nameBytes)
fixed (byte* schemaPtr = schemaBytes)
{
CheckAndDropResult(Internal.API.regorus_effect_schema_register(namePtr, schemaPtr));
}
}
/// <summary>
/// Check if an effect schema with the given name exists.
/// </summary>
/// <param name="name">Name of the schema to check</param>
/// <returns>True if the schema exists, false otherwise</returns>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static bool ContainsEffect(string name)
{
var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue);
fixed (byte* namePtr = nameBytes)
{
var result = Internal.API.regorus_effect_schema_contains(namePtr);
return GetBoolResult(result);
}
}
/// <summary>
/// Get the number of registered effect schemas.
/// </summary>
/// <returns>The number of registered effect schemas</returns>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static long EffectCount
{
get
{
var result = Internal.API.regorus_effect_schema_len();
return GetIntResult(result);
}
}
/// <summary>
/// Check if the effect schema registry is empty.
/// </summary>
/// <returns>True if the registry is empty, false otherwise</returns>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static bool IsEffectRegistryEmpty
{
get
{
var result = Internal.API.regorus_effect_schema_is_empty();
return GetBoolResult(result);
}
}
/// <summary>
/// List all registered effect schema names.
/// </summary>
/// <returns>JSON array of schema names</returns>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static string ListEffectNames()
{
return CheckAndDropResult(Internal.API.regorus_effect_schema_list_names()) ?? "[]";
}
/// <summary>
/// Remove an effect schema by name.
/// </summary>
/// <param name="name">Name of the schema to remove</param>
/// <returns>True if the schema was removed, false if it wasn't found</returns>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static bool RemoveEffect(string name)
{
var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue);
fixed (byte* namePtr = nameBytes)
{
var result = Internal.API.regorus_effect_schema_remove(namePtr);
return GetBoolResult(result);
}
}
/// <summary>
/// Clear all effect schemas from the registry.
/// </summary>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static void ClearEffects()
{
CheckAndDropResult(Internal.API.regorus_effect_schema_clear());
}
private static string? StringFromUTF8(IntPtr ptr)
{
#if NETSTANDARD2_1
return System.Runtime.InteropServices.Marshal.PtrToStringUTF8(ptr);
#else
int len = 0;
while (System.Runtime.InteropServices.Marshal.ReadByte(ptr, len) != 0) { ++len; }
byte[] buffer = new byte[len];
System.Runtime.InteropServices.Marshal.Copy(ptr, buffer, 0, buffer.Length);
return Encoding.UTF8.GetString(buffer);
#endif
}
private static string? CheckAndDropResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
}
return result.data_type switch
{
Internal.RegorusDataType.String => StringFromUTF8((IntPtr)result.output),
Internal.RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(),
Internal.RegorusDataType.Integer => result.int_value.ToString(),
Internal.RegorusDataType.None => null,
_ => StringFromUTF8((IntPtr)result.output)
};
}
finally
{
Internal.API.regorus_result_drop(result);
}
}
private static bool GetBoolResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
}
return result.data_type == Internal.RegorusDataType.Boolean ? result.bool_value : false;
}
finally
{
Internal.API.regorus_result_drop(result);
}
}
private static long GetIntResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
}
return result.data_type == Internal.RegorusDataType.Integer ? result.int_value : 0;
}
finally
{
Internal.API.regorus_result_drop(result);
}
}
}
}

View File

@@ -0,0 +1,185 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Text;
#nullable enable
namespace Regorus
{
/// <summary>
/// Provides static methods for managing the global target registry.
/// Targets define resource types and their associated schemas for Azure Policy evaluation.
/// </summary>
public static unsafe class TargetRegistry
{
/// <summary>
/// Register a target from JSON definition.
/// The target JSON should follow the target schema format.
/// Once registered, the target can be referenced in Rego policies using `__target__` rules.
/// </summary>
/// <param name="targetJson">JSON encoded target definition</param>
/// <exception cref="Exception">Thrown when target registration fails</exception>
public static void RegisterFromJson(string targetJson)
{
var targetBytes = Encoding.UTF8.GetBytes(targetJson + char.MinValue);
fixed (byte* targetPtr = targetBytes)
{
CheckAndDropResult(Internal.API.regorus_register_target_from_json(targetPtr));
}
}
/// <summary>
/// Check if a target is registered.
/// </summary>
/// <param name="name">Name of the target to check</param>
/// <returns>True if the target is registered, false otherwise</returns>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static bool Contains(string name)
{
var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue);
fixed (byte* namePtr = nameBytes)
{
var result = Internal.API.regorus_target_registry_contains(namePtr);
return GetBoolResult(result);
}
}
/// <summary>
/// Get a list of all registered target names.
/// </summary>
/// <returns>JSON array of target names</returns>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static string ListNames()
{
return CheckAndDropResult(Internal.API.regorus_target_registry_list_names()) ?? "[]";
}
/// <summary>
/// Remove a target from the registry by name.
/// </summary>
/// <param name="name">The target name to remove</param>
/// <returns>True if the target was removed, false if it wasn't found</returns>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static bool Remove(string name)
{
var nameBytes = Encoding.UTF8.GetBytes(name + char.MinValue);
fixed (byte* namePtr = nameBytes)
{
var result = Internal.API.regorus_target_registry_remove(namePtr);
return GetBoolResult(result);
}
}
/// <summary>
/// Clear all targets from the registry.
/// </summary>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static void Clear()
{
CheckAndDropResult(Internal.API.regorus_target_registry_clear());
}
/// <summary>
/// Get the number of registered targets.
/// </summary>
/// <returns>The number of registered targets</returns>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static long Count
{
get
{
var result = Internal.API.regorus_target_registry_len();
return GetIntResult(result);
}
}
/// <summary>
/// Check if the target registry is empty.
/// </summary>
/// <returns>True if the registry is empty, false otherwise</returns>
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static bool IsEmpty
{
get
{
var result = Internal.API.regorus_target_registry_is_empty();
return GetBoolResult(result);
}
}
private static string? StringFromUTF8(IntPtr ptr)
{
#if NETSTANDARD2_1
return System.Runtime.InteropServices.Marshal.PtrToStringUTF8(ptr);
#else
int len = 0;
while (System.Runtime.InteropServices.Marshal.ReadByte(ptr, len) != 0) { ++len; }
byte[] buffer = new byte[len];
System.Runtime.InteropServices.Marshal.Copy(ptr, buffer, 0, buffer.Length);
return Encoding.UTF8.GetString(buffer);
#endif
}
private static string? CheckAndDropResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
}
return result.data_type switch
{
Internal.RegorusDataType.String => StringFromUTF8((IntPtr)result.output),
Internal.RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(),
Internal.RegorusDataType.Integer => result.int_value.ToString(),
Internal.RegorusDataType.None => null,
_ => StringFromUTF8((IntPtr)result.output)
};
}
finally
{
Internal.API.regorus_result_drop(result);
}
}
private static bool GetBoolResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
}
return result.data_type == Internal.RegorusDataType.Boolean ? result.bool_value : false;
}
finally
{
Internal.API.regorus_result_drop(result);
}
}
private static long GetIntResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
}
return result.data_type == Internal.RegorusDataType.Integer ? result.int_value : 0;
}
finally
{
Internal.API.regorus_result_drop(result);
}
}
}
}

View File

@@ -0,0 +1,291 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Text.Json;
namespace TargetExampleApp;
class Program
{
// Policy definition constants
private const string AZURE_STORAGE_POLICY_DEFINITION = @"
package policy
import rego.v1
# Target declaration for Azure Policy
__target__ := ""target.tests.azure_policy""
default parameters.requiredTLSVersion = """"
default parameters.allowedPorts = []
# Policy rules for storage accounts
default allow := false
# Allow storage accounts with HTTPS-only traffic and proper encryption
allow if {
input.type == ""Microsoft.Storage/storageAccounts""
input.properties.supportsHttpsTrafficOnly == true
input.properties.encryption.services.blob.enabled == true
input.properties.minimumTlsVersion in [parameters.requiredTLSVersion]
}
# Allow network security groups with proper inbound rules
allow if {
input.type == ""Microsoft.Network/networkSecurityGroups""
count([rule |
rule := input.properties.securityRules[_]
rule.properties.direction == ""Inbound""
rule.properties.access == ""Allow""
rule.properties.sourceAddressPrefix == ""*""
rule.properties.destinationPortRange in [parameters.allowedPorts]
]) == 0
}";
private const string AZURE_STORAGE_POLICY_ASSIGNMENT = @"
package policy
import rego.v1
parameters.requiredTLSVersion = ""TLS1_2""
parameters.allowedPorts = [""22"", ""3389""]";
// Test data constants
private const string COMPLIANT_STORAGE_ACCOUNT = @"{
""type"": ""Microsoft.Storage/storageAccounts"",
""name"": ""compliantstorageacct"",
""location"": ""eastus"",
""kind"": ""StorageV2"",
""properties"": {
""supportsHttpsTrafficOnly"": true,
""minimumTlsVersion"": ""TLS1_2"",
""allowBlobPublicAccess"": false,
""encryption"": {
""services"": {
""blob"": { ""enabled"": true },
""file"": { ""enabled"": true }
}
}
},
""tags"": {
""environment"": ""production""
}
}";
private const string NON_COMPLIANT_STORAGE_ACCOUNT = @"{
""type"": ""Microsoft.Storage/storageAccounts"",
""name"": ""insecurestorageacct"",
""location"": ""westus"",
""kind"": ""Storage"",
""properties"": {
""supportsHttpsTrafficOnly"": false,
""minimumTlsVersion"": ""TLS1_0"",
""allowBlobPublicAccess"": true,
""encryption"": {
""services"": {
""blob"": { ""enabled"": false },
""file"": { ""enabled"": false }
}
}
}
}";
static void Main(string[] args)
{
Console.WriteLine("=== Regorus Target Example Application ===\n");
try
{
DemonstrateTargetFunctionality();
Console.WriteLine("\n=== Target demonstration completed successfully! ===");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
Environment.Exit(1);
}
}
static void DemonstrateTargetFunctionality()
{
Console.WriteLine("REGORUS TARGET FUNCTIONALITY DEMONSTRATION");
Console.WriteLine("==========================================");
// 1. Register target using JSON from file
var targetJsonPath = Path.Combine(AppContext.BaseDirectory, "azure_policy.target.json");
var targetJson = File.ReadAllText(targetJsonPath);
Console.WriteLine("1. Registering target from JSON file:");
Console.WriteLine(targetJson);
Regorus.TargetRegistry.RegisterFromJson(targetJson);
Console.WriteLine($"Target registered. Registry contains {Regorus.TargetRegistry.Count} target(s)");
Console.WriteLine($"Registered targets: {Regorus.TargetRegistry.ListNames()}");
// 2. Compile policy for target
var policyModules = new List<Regorus.PolicyModule>
{
new Regorus.PolicyModule($"definition-{Guid.NewGuid():N}", AZURE_STORAGE_POLICY_DEFINITION),
new Regorus.PolicyModule($"assignment-{Guid.NewGuid():N}", AZURE_STORAGE_POLICY_ASSIGNMENT)
};
var policyDataJson = "{}";
Console.WriteLine("\n2. Compiling policy for target...");
using var compiledPolicy = Regorus.Compiler.CompilePolicyForTarget(policyDataJson, policyModules);
Console.WriteLine("Policy compiled successfully!");
// 2.5. Demonstrate policy information retrieval
Console.WriteLine("\n2.5. Retrieving policy information:");
DemonstratePolicyInfo(compiledPolicy);
// 3. Evaluate with different inputs
Console.WriteLine("\n3. Testing policy evaluation:");
Console.WriteLine("Compliant storage account:");
Console.WriteLine(COMPLIANT_STORAGE_ACCOUNT);
var compliantResult = compiledPolicy.EvalWithInput(COMPLIANT_STORAGE_ACCOUNT);
Console.WriteLine($"Result: {compliantResult}");
Console.WriteLine("\nNon-compliant storage account:");
Console.WriteLine(NON_COMPLIANT_STORAGE_ACCOUNT);
var nonCompliantResult = compiledPolicy.EvalWithInput(NON_COMPLIANT_STORAGE_ACCOUNT);
Console.WriteLine($"Result: {nonCompliantResult}");
// 4. Demonstrate thread-safe concurrent evaluation
Console.WriteLine("\n4. Testing concurrent evaluation from multiple threads:");
DemonstrateConcurrentEvaluation(compiledPolicy);
}
static void DemonstrateConcurrentEvaluation(Regorus.CompiledPolicy compiledPolicy)
{
var testInputs = new[]
{
("Thread-1-Compliant", COMPLIANT_STORAGE_ACCOUNT),
("Thread-2-NonCompliant", NON_COMPLIANT_STORAGE_ACCOUNT),
("Thread-3-Compliant", COMPLIANT_STORAGE_ACCOUNT.Replace("compliantstorageacct", "thread3storage")),
("Thread-4-NonCompliant", NON_COMPLIANT_STORAGE_ACCOUNT.Replace("insecurestorageacct", "thread4storage")),
("Thread-5-Compliant", COMPLIANT_STORAGE_ACCOUNT.Replace("compliantstorageacct", "thread5storage"))
};
Console.WriteLine($"Starting {testInputs.Length} concurrent evaluations...");
var tasks = testInputs.Select(input =>
Task.Run(() => {
var (threadName, json) = input;
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
// Multiple evaluations per thread to stress test
var results = new List<string>();
for (int i = 0; i < 1000; i++)
{
var result = compiledPolicy.EvalWithInput(json);
results.Add(result);
}
stopwatch.Stop();
var microseconds = stopwatch.ElapsedTicks * 1000000 / System.Diagnostics.Stopwatch.Frequency;
// Verify all results are identical (thread safety)
var firstResult = results[0];
var allIdentical = results.All(r => r == firstResult);
Console.WriteLine($"✓ {threadName}: {results.Count} evaluations in {microseconds}μs, " +
$"Results consistent: {allIdentical}");
return (threadName, results.Count, microseconds, allIdentical);
})
).ToArray();
// Wait for all threads to complete
var results = Task.WhenAll(tasks).Result;
Console.WriteLine("\nConcurrency test results:");
var totalEvaluations = results.Sum(r => r.Item2);
var maxTime = results.Max(r => r.Item3);
var allConsistent = results.All(r => r.allIdentical);
Console.WriteLine($"✓ Total evaluations: {totalEvaluations}");
Console.WriteLine($"✓ Max thread time: {maxTime}μs");
Console.WriteLine($"✓ All threads consistent: {allConsistent}");
Console.WriteLine($"✓ Approximate throughput: {totalEvaluations * 1000000.0 / maxTime:F0} evaluations/second");
Console.WriteLine("✓ No locks required - CompiledPolicy is thread-safe!");
}
static void DemonstratePolicyInfo(Regorus.CompiledPolicy compiledPolicy)
{
Console.WriteLine("Getting policy metadata using GetPolicyInfo()...");
try
{
var policyInfo = compiledPolicy.GetPolicyInfo();
Console.WriteLine($"✓ Policy Information Retrieved:");
Console.WriteLine($" Target Name: {policyInfo.TargetName ?? "None"}");
Console.WriteLine($" Effect Rule: {policyInfo.EffectRule ?? "None"}");
Console.WriteLine($" Entrypoint Rule: {policyInfo.EntrypointRule}");
Console.WriteLine($" Module IDs ({policyInfo.ModuleIds.Count}):");
foreach (var moduleId in policyInfo.ModuleIds)
{
Console.WriteLine($" - {moduleId}");
}
Console.WriteLine($" Applicable Resource Types ({policyInfo.ApplicableResourceTypes.Count}):");
foreach (var resourceType in policyInfo.ApplicableResourceTypes)
{
Console.WriteLine($" - {resourceType}");
}
if (policyInfo.Parameters != null && policyInfo.Parameters.Count > 0)
{
Console.WriteLine($" Policy Parameters:");
foreach (var parameterSet in policyInfo.Parameters)
{
Console.WriteLine($" From '{parameterSet.SourceFile}':");
Console.WriteLine($" Parameters ({parameterSet.Parameters.Count}):");
foreach (var param in parameterSet.Parameters)
{
Console.WriteLine($" - {param.Name} ({param.Type})");
if (param.Default != null)
{
Console.WriteLine($" Default: {param.Default}");
}
if (!string.IsNullOrEmpty(param.Description))
{
Console.WriteLine($" Description: {param.Description}");
}
}
if (parameterSet.Modifiers.Count > 0)
{
Console.WriteLine($" Modifiers ({parameterSet.Modifiers.Count}):");
foreach (var modifier in parameterSet.Modifiers)
{
Console.WriteLine($" - {modifier.Name}: {modifier.Value}");
}
}
}
}
else
{
Console.WriteLine(" No parameter information available");
}
// Demonstrate JSON serialization of policy info
Console.WriteLine("\n✓ Policy Info as JSON:");
var jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var policyInfoJson = JsonSerializer.Serialize(policyInfo, jsonOptions);
Console.WriteLine(policyInfoJson);
}
catch (Exception ex)
{
Console.WriteLine($"✗ Failed to get policy info: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
<!-- If the environment variable is set (such as in a Github Action run), append the suffix to the version number -->
<RegorusPackageVersionSuffix Condition="'$(VersionSuffix)' != ''">-$(VersionSuffix)</RegorusPackageVersionSuffix>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Regorus" Version="0.6.0$(RegorusPackageVersionSuffix)"/>
</ItemGroup>
<ItemGroup>
<Content Include="azure_policy.target.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,125 @@
{
"name": "target.tests.azure_policy",
"description": "Azure Policy target for comprehensive policy evaluation testing",
"version": "1.0.0",
"resource_schema_selector": "type",
"resource_schemas": [
{
"type": "object",
"properties": {
"type": { "const": "Microsoft.Resources/subscriptions" },
"subscriptionId": { "type": "string" },
"tenantId": { "type": "string" },
"displayName": { "type": "string" }
},
"required": ["type", "subscriptionId"]
},
{
"type": "object",
"properties": {
"type": { "const": "Microsoft.Storage/storageAccounts" },
"name": { "type": "string" },
"location": { "type": "string" },
"kind": { "enum": ["Storage", "StorageV2", "BlobStorage", "FileStorage", "BlockBlobStorage"] },
"properties": {
"type": "object",
"properties": {
"supportsHttpsTrafficOnly": { "type": "boolean" },
"minimumTlsVersion": { "enum": ["TLS1_0", "TLS1_1", "TLS1_2"] },
"allowBlobPublicAccess": { "type": "boolean" },
"encryption": {
"type": "object",
"properties": {
"services": {
"type": "object",
"properties": {
"blob": { "type": "object", "properties": { "enabled": { "type": "boolean" } } },
"file": { "type": "object", "properties": { "enabled": { "type": "boolean" } } }
}
}
}
}
}
},
"tags": { "type": "object" }
},
"required": ["type", "name", "location"]
},
{
"type": "object",
"properties": {
"type": { "const": "Microsoft.Network/networkSecurityGroups" },
"name": { "type": "string" },
"location": { "type": "string" },
"properties": {
"type": "object",
"properties": {
"securityRules": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"properties": {
"type": "object",
"properties": {
"direction": { "enum": ["Inbound", "Outbound"] },
"access": { "enum": ["Allow", "Deny"] },
"protocol": { "enum": ["Tcp", "Udp", "*"] },
"sourcePortRange": { "type": "string" },
"destinationPortRange": { "type": "string" },
"sourceAddressPrefix": { "type": "string" },
"destinationAddressPrefix": { "type": "string" },
"priority": { "type": "integer", "minimum": 100, "maximum": 4096 }
}
}
}
}
}
}
}
},
"required": ["type", "name", "location"]
}
],
"effects": {
"allow": { "type": "boolean" },
"deny": {
"type": "object",
"properties": {
"message": { "type": "string" }
}
},
"audit": {
"type": "object",
"properties": {
"level": { "enum": ["info", "warning", "error"] },
"message": { "type": "string" },
"complianceState": { "enum": ["Compliant", "NonCompliant", "Unknown"] }
}
},
"modify": {
"type": "object",
"properties": {
"operations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"operation": { "enum": ["add", "replace", "remove"] },
"field": { "type": "string" },
"value": { "type": "any" }
}
}
}
}
},
"deployIfNotExists": {
"type": "object",
"properties": {
"template": { "type": "object" },
"parameters": { "type": "object" }
}
}
}
}

View File

@@ -5,6 +5,6 @@
"sdk": {
"allowPrerelease": false,
"version": "8.0.412",
"rollForward": "disable"
"rollForward": "latestFeature"
}
}

View File

@@ -92,9 +92,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.98"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
[[package]]
name = "autocfg"
@@ -217,18 +217,18 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.43"
version = "4.5.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f"
checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.43"
version = "4.5.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65"
checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8"
dependencies = [
"anstream",
"anstyle",
@@ -255,22 +255,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "csbindgen"
version = "1.9.3"
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c26b9831049b947d154bba920e4124053def72447be6fb106a96f483874b482a"
dependencies = [
"regex",
"syn",
]
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "dashmap"
version = "5.5.3"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
@@ -816,9 +813,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.96"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beef09f85ae72cea1ef96ba6870c51e6382ebfa4f0e85b643459331f3daa5be0"
checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1"
dependencies = [
"unicode-ident",
]
@@ -959,6 +956,7 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml",
"thiserror",
"url",
"uuid",
]
@@ -969,7 +967,6 @@ version = "0.5.0"
dependencies = [
"anyhow",
"cbindgen",
"csbindgen",
"regorus",
"serde_json",
]
@@ -1117,9 +1114,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.104"
version = "2.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619"
dependencies = [
"proc-macro2",
"quote",
@@ -1150,6 +1147,26 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "thiserror"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinystr"
version = "0.8.1"

View File

@@ -32,4 +32,3 @@ custom_allocator = []
[build-dependencies]
cbindgen = "0.28.0"
csbindgen = "=1.9.3"

View File

@@ -1,5 +1,4 @@
extern crate cbindgen;
extern crate csbindgen;
use std::env;
@@ -21,12 +20,4 @@ fn main() {
.generate()
.expect("Unable to generate bindings")
.write_to_file("regorus.ffi.hpp");
csbindgen::Builder::default()
.input_extern_file("src/lib.rs")
.csharp_dll_name("regorus_ffi")
.csharp_class_name("API")
.csharp_namespace("Regorus.Internal")
.generate_csharp_file("./RegorusFFI.g.cs")
.unwrap();
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#[cfg(feature = "custom_allocator")]
extern "C" {
fn regorus_aligned_alloc(alignment: usize, size: usize) -> *mut u8;
fn regorus_free(ptr: *mut u8);
}
#[cfg(feature = "custom_allocator")]
mod allocator {
use std::alloc::{GlobalAlloc, Layout};
struct RegorusAllocator {}
unsafe impl GlobalAlloc for RegorusAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let size = layout.size();
let align = layout.align();
crate::allocator::regorus_aligned_alloc(align, size)
}
unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
crate::allocator::regorus_free(ptr)
}
}
#[global_allocator]
static ALLOCATOR: RegorusAllocator = RegorusAllocator {};
}

229
bindings/ffi/src/common.rs Normal file
View File

@@ -0,0 +1,229 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use anyhow::{anyhow, bail, Result};
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_longlong};
/// Status of a call on `RegorusEngine`.
#[repr(C)]
pub enum RegorusStatus {
/// The operation was successful.
Ok,
/// The operation was unsuccessful.
Error,
/// Invalid data format provided.
InvalidDataFormat,
/// Invalid entrypoint rule specified.
InvalidEntrypoint,
/// Compilation failed.
CompilationFailed,
/// Invalid argument provided.
InvalidArgument,
/// Invalid module ID.
InvalidModuleId,
/// Invalid policy content.
InvalidPolicy,
}
/// Type of data contained in RegorusResult
#[repr(C)]
#[allow(unused)]
pub enum RegorusDataType {
/// No data / void
None,
/// String data (output field is valid)
String,
/// Boolean data (bool_value field is valid)
Boolean,
/// Integer data (int_value field is valid)
Integer,
/// Pointer data (pointer_value field is valid)
Pointer,
}
/// Result of a call on `RegorusEngine`.
///
/// Must be freed using `regorus_result_drop`.
#[repr(C)]
pub struct RegorusResult {
/// Status
pub(crate) status: RegorusStatus,
/// Type of data contained in this result
pub(crate) data_type: RegorusDataType,
/// String output produced by the call.
/// Valid when data_type is String. Owned by Rust.
pub(crate) output: *mut c_char,
/// Boolean value.
/// Valid when data_type is Boolean.
pub(crate) bool_value: bool,
/// Integer value.
/// Valid when data_type is Integer.
pub(crate) int_value: c_longlong,
/// Pointer value.
/// Valid when data_type is Pointer.
pub(crate) pointer_value: *mut std::os::raw::c_void,
/// Errors produced by the call.
/// Owned by Rust.
pub(crate) error_message: *mut c_char,
}
impl RegorusResult {
/// Create a successful result with no data.
pub(crate) fn ok_void() -> Self {
Self {
status: RegorusStatus::Ok,
data_type: RegorusDataType::None,
output: std::ptr::null_mut(),
bool_value: false,
int_value: 0,
pointer_value: std::ptr::null_mut(),
error_message: std::ptr::null_mut(),
}
}
/// Create a successful result with string output.
pub(crate) fn ok_string(output: String) -> Self {
Self {
status: RegorusStatus::Ok,
data_type: RegorusDataType::String,
output: to_c_str(output),
bool_value: false,
int_value: 0,
pointer_value: std::ptr::null_mut(),
error_message: std::ptr::null_mut(),
}
}
/// Create a successful result with boolean value.
#[allow(unused)]
pub(crate) fn ok_bool(value: bool) -> Self {
Self {
status: RegorusStatus::Ok,
data_type: RegorusDataType::Boolean,
output: std::ptr::null_mut(),
bool_value: value,
int_value: 0,
pointer_value: std::ptr::null_mut(),
error_message: std::ptr::null_mut(),
}
}
/// Create a successful result with integer value.
#[allow(unused)]
pub(crate) fn ok_int(value: i64) -> Self {
Self {
status: RegorusStatus::Ok,
data_type: RegorusDataType::Integer,
output: std::ptr::null_mut(),
bool_value: false,
int_value: value as c_longlong,
pointer_value: std::ptr::null_mut(),
error_message: std::ptr::null_mut(),
}
}
/// Create a successful result with pointer value.
pub(crate) fn ok_pointer(pointer: *mut std::os::raw::c_void) -> Self {
Self {
status: RegorusStatus::Ok,
data_type: RegorusDataType::Pointer,
output: std::ptr::null_mut(),
bool_value: false,
int_value: 0,
pointer_value: pointer,
error_message: std::ptr::null_mut(),
}
}
/// Create an error result with specific status.
pub(crate) fn err(status: RegorusStatus) -> Self {
Self {
status,
data_type: RegorusDataType::None,
output: std::ptr::null_mut(),
bool_value: false,
int_value: 0,
pointer_value: std::ptr::null_mut(),
error_message: std::ptr::null_mut(),
}
}
/// Create an error result with status and message.
pub(crate) fn err_with_message(status: RegorusStatus, message: String) -> Self {
Self {
status,
data_type: RegorusDataType::None,
output: std::ptr::null_mut(),
bool_value: false,
int_value: 0,
pointer_value: std::ptr::null_mut(),
error_message: to_c_str(message),
}
}
}
pub(crate) fn to_c_str(s: String) -> *mut c_char {
match CString::new(s) {
Ok(cs) => cs.into_raw(),
_ => to_c_str("binding error: failed to create c-style string".to_string()),
}
}
pub(crate) fn from_c_str(s: *const c_char) -> Result<String> {
if s.is_null() {
bail!("null pointer");
}
unsafe {
CStr::from_ptr(s)
.to_str()
.map_err(|e| anyhow!("invalid utf8: {e}"))
.map(|s| s.to_string())
}
}
pub(crate) fn to_ref<'a, T>(t: *mut T) -> Result<&'a mut T> {
unsafe { t.as_mut().ok_or_else(|| anyhow!("null pointer")) }
}
pub(crate) fn to_regorus_result(r: Result<()>) -> RegorusResult {
match r {
Ok(()) => RegorusResult::ok_void(),
Err(e) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{e}")),
}
}
pub(crate) fn to_regorus_string_result(r: Result<String>) -> RegorusResult {
match r {
Ok(s) => RegorusResult::ok_string(s),
Err(e) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{e}")),
}
}
/// Drop a `RegorusResult`.
///
/// `output` and `error_message` strings are not valid after drop.
#[no_mangle]
pub extern "C" fn regorus_result_drop(r: RegorusResult) {
unsafe {
if !r.error_message.is_null() {
let _ = CString::from_raw(r.error_message);
}
if !r.output.is_null() {
let _ = CString::from_raw(r.output);
}
}
}

208
bindings/ffi/src/compile.rs Normal file
View File

@@ -0,0 +1,208 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::common::{from_c_str, RegorusResult, RegorusStatus};
use crate::compiled_policy::RegorusCompiledPolicy;
use regorus::{compile_policy_with_entrypoint, PolicyModule, Value};
#[cfg(feature = "azure_policy")]
use regorus::compile_policy_for_target;
use std::os::raw::c_char;
/// FFI wrapper for PolicyModule struct.
#[repr(C)]
pub struct RegorusPolicyModule {
pub id: *const c_char,
pub content: *const c_char,
}
/// Compiles a policy from data and modules with a specific entry point rule.
///
/// This is a convenience function that wraps [`regorus::compile_policy_with_entrypoint`].
/// It sets up an Engine internally and calls the appropriate compilation method.
///
/// # Parameters
/// * `data_json` - JSON string containing static data for policy evaluation
/// * `modules` - Array of policy modules to compile
/// * `modules_len` - Number of modules in the array
/// * `entry_point_rule` - The specific rule path to evaluate (e.g., "data.policy.allow")
///
/// # Returns
/// Returns a RegorusResult containing a RegorusCompiledPolicy handle on success.
///
/// # Safety
/// All string parameters must be valid null-terminated UTF-8 strings.
/// The modules array must contain exactly `modules_len` valid elements.
/// The caller must eventually call regorus_compiled_policy_drop on the returned handle.
#[no_mangle]
pub extern "C" fn regorus_compile_policy_with_entrypoint(
data_json: *const c_char,
modules: *const RegorusPolicyModule,
modules_len: usize,
entry_point_rule: *const c_char,
) -> RegorusResult {
let data_str = match from_c_str(data_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid data JSON string: {e}"),
)
}
};
let entry_rule = match from_c_str(entry_point_rule) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidEntrypoint,
format!("Invalid entry point rule string: {e}"),
)
}
};
// Parse data JSON
let data = match Value::from_json_str(&data_str) {
Ok(data) => data,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse data JSON: {e}"),
)
}
};
// Convert C modules array to Rust Vec
let policy_modules = match convert_c_modules_to_rust(modules, modules_len) {
Ok(modules) => modules,
Err(status) => return RegorusResult::err(status),
};
// Call the convenience function
match compile_policy_with_entrypoint(data, &policy_modules, entry_rule.into()) {
Ok(compiled_policy) => {
let wrapped_policy = RegorusCompiledPolicy { compiled_policy };
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut std::os::raw::c_void)
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Policy compilation failed: {e}"),
),
}
}
/// Compiles a target-aware policy from data and modules.
///
/// This is a convenience function that wraps [`regorus::compile_policy_for_target`].
/// It sets up an Engine internally and calls target-aware compilation.
///
/// # Parameters
/// * `data_json` - JSON string containing static data for policy evaluation
/// * `modules` - Array of policy modules to compile
/// * `modules_len` - Number of modules in the array
///
/// # Returns
/// Returns a RegorusResult containing a RegorusCompiledPolicy handle on success.
///
/// # Note
/// This function is only available when the `azure_policy` feature is enabled.
/// At least one module must contain a `__target__` declaration.
///
/// # Safety
/// All string parameters must be valid null-terminated UTF-8 strings.
/// The modules array must contain exactly `modules_len` valid elements.
/// The caller must eventually call regorus_compiled_policy_drop on the returned handle.
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_compile_policy_for_target(
data_json: *const c_char,
modules: *const RegorusPolicyModule,
modules_len: usize,
) -> RegorusResult {
let data_str = match from_c_str(data_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid data JSON string: {e}"),
)
}
};
// Parse data JSON
let data = match Value::from_json_str(&data_str) {
Ok(data) => data,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse data JSON: {e}"),
)
}
};
// Convert C modules array to Rust Vec
let policy_modules = match convert_c_modules_to_rust(modules, modules_len) {
Ok(modules) => modules,
Err(status) => return RegorusResult::err(status),
};
// Call the convenience function
match compile_policy_for_target(data, &policy_modules) {
Ok(compiled_policy) => {
let wrapped_policy = RegorusCompiledPolicy { compiled_policy };
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut std::os::raw::c_void)
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Target-aware policy compilation failed: {e}"),
),
}
}
/// Helper function to convert C module array to Rust Vec<PolicyModule>.
fn convert_c_modules_to_rust(
modules: *const RegorusPolicyModule,
modules_len: usize,
) -> Result<Vec<PolicyModule>, RegorusStatus> {
if modules.is_null() && modules_len > 0 {
return Err(RegorusStatus::InvalidArgument);
}
let mut policy_modules = Vec::with_capacity(modules_len);
for i in 0..modules_len {
unsafe {
let module = modules.add(i);
if module.is_null() {
return Err(RegorusStatus::InvalidArgument);
}
let module_ref = &*module;
let id = match from_c_str(module_ref.id) {
Ok(s) => s,
Err(e) => {
eprintln!("Invalid module ID at index {}: {}", i, e);
return Err(RegorusStatus::InvalidModuleId);
}
};
let content = match from_c_str(module_ref.content) {
Ok(s) => s,
Err(e) => {
eprintln!("Invalid module content at index {}: {}", i, e);
return Err(RegorusStatus::InvalidPolicy);
}
};
policy_modules.push(PolicyModule {
id: id.into(),
content: content.into(),
});
}
}
Ok(policy_modules)
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::common::*;
use anyhow::Result;
use std::os::raw::c_char;
/// Wrapper for `regorus::CompiledPolicy`.
#[derive(Clone)]
pub struct RegorusCompiledPolicy {
pub(crate) compiled_policy: regorus::CompiledPolicy,
}
/// Drop a `RegorusCompiledPolicy`.
#[no_mangle]
pub extern "C" fn regorus_compiled_policy_drop(compiled_policy: *mut RegorusCompiledPolicy) {
if let Ok(cp) = to_ref(compiled_policy) {
unsafe {
let _ = Box::from_raw(std::ptr::from_mut(cp));
}
}
}
/// Evaluate the compiled policy with the given input.
///
/// For target policies, evaluates the target's effect rule.
/// For regular policies, evaluates the originally compiled rule.
///
/// * `input`: JSON encoded input data (resource) to validate against the policy.
#[no_mangle]
pub extern "C" fn regorus_compiled_policy_eval_with_input(
compiled_policy: *mut RegorusCompiledPolicy,
input: *const c_char,
) -> RegorusResult {
let output = || -> Result<String> {
let input_value = regorus::Value::from_json_str(&from_c_str(input)?)?;
let result = to_ref(compiled_policy)?
.compiled_policy
.eval_with_input(input_value)?;
result.to_json_str()
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
}
/// Get information about the compiled policy including metadata about modules,
/// target configuration, and resource types.
///
/// Returns a JSON-encoded `PolicyInfo` struct containing comprehensive
/// information about the compiled policy such as module IDs, target name,
/// applicable resource types, entry point rule, and parameters.
#[no_mangle]
pub extern "C" fn regorus_compiled_policy_get_policy_info(
compiled_policy: *mut RegorusCompiledPolicy,
) -> RegorusResult {
let output = || -> Result<String> {
let info = to_ref(compiled_policy)?.compiled_policy.get_policy_info()?;
serde_json::to_string(&info)
.map_err(|e| anyhow::anyhow!("Failed to serialize policy info: {}", e))
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
}

View File

@@ -0,0 +1,175 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Effect schema registry functions for FFI.
//!
//! These functions provide access to regorus's effect schema registry functionality,
//! enabling registration and management of Azure Policy effect schemas.
#![cfg(feature = "azure_policy")]
use crate::common::{from_c_str, RegorusResult, RegorusStatus};
use regorus::{registry::schemas, Schema};
use std::os::raw::c_char;
/// Register an effect schema from JSON with a given name.
///
/// # Parameters
/// * `name` - Name to register the schema under
/// * `schema_json` - JSON string representing the schema
///
/// # Returns
/// Returns a RegorusResult with success/error status.
///
/// # Safety
/// All string parameters must be valid null-terminated UTF-8 strings.
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_register(
name: *const c_char,
schema_json: *const c_char,
) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid effect schema name string: {e}"),
)
}
};
let schema_str = match from_c_str(schema_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid effect schema JSON string: {e}"),
)
}
};
// Parse schema from JSON
let schema = match Schema::from_json_str(&schema_str) {
Ok(schema) => schema,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse effect schema JSON: {e}"),
)
}
};
// Register the schema
match schemas::effect::register(schema_name, schema.into()) {
Ok(()) => RegorusResult::ok_pointer(std::ptr::null_mut()),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to register effect schema: {e}"),
),
}
}
/// Check if an effect schema with the given name exists.
///
/// # Parameters
/// * `name` - Name of the schema to check
///
/// # Returns
/// Returns a RegorusResult with "true" or "false" string output.
///
/// # Safety
/// The name parameter must be a valid null-terminated UTF-8 string.
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_contains(name: *const c_char) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid effect schema name string: {e}"),
)
}
};
let contains = schemas::effect::contains(&schema_name);
RegorusResult::ok_bool(contains)
}
/// Get the number of registered effect schemas.
///
/// # Returns
/// Returns a RegorusResult with the count as a string.
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_len() -> RegorusResult {
let count = schemas::effect::len();
RegorusResult::ok_int(count as i64)
}
/// Check if the effect schema registry is empty.
///
/// # Returns
/// Returns a RegorusResult with "true" or "false" string output.
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_is_empty() -> RegorusResult {
let is_empty = schemas::effect::is_empty();
RegorusResult::ok_bool(is_empty)
}
/// List all registered effect schema names as a JSON array.
///
/// # Returns
/// Returns a RegorusResult with a JSON array of schema names.
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_list_names() -> RegorusResult {
let names = schemas::effect::list_names();
match serde_json::to_string(&names) {
Ok(json_str) => RegorusResult::ok_string(json_str),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to serialize effect schema names to JSON: {e}"),
),
}
}
/// Remove an effect schema by name.
///
/// # Parameters
/// * `name` - Name of the schema to remove
///
/// # Returns
/// Returns a RegorusResult with "true" if removed, "false" if not found.
///
/// # Safety
/// The name parameter must be a valid null-terminated UTF-8 string.
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_remove(name: *const c_char) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid effect schema name string: {e}"),
)
}
};
let removed = schemas::effect::remove(&schema_name).is_some();
RegorusResult::ok_bool(removed)
}
/// Clear all effect schemas from the registry.
///
/// # Returns
/// Returns a RegorusResult with success status.
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_clear() -> RegorusResult {
schemas::effect::clear();
RegorusResult::ok_pointer(std::ptr::null_mut())
}

454
bindings/ffi/src/engine.rs Normal file
View File

@@ -0,0 +1,454 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::common::{
from_c_str, to_ref, to_regorus_result, to_regorus_string_result, RegorusResult, RegorusStatus,
};
use crate::compiled_policy::RegorusCompiledPolicy;
use anyhow::Result;
use std::os::raw::c_char;
/// Wrapper for `regorus::Engine`.
#[derive(Clone)]
pub struct RegorusEngine {
engine: ::regorus::Engine,
}
#[no_mangle]
/// Construct a new Engine
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html
pub extern "C" fn regorus_engine_new() -> *mut RegorusEngine {
let mut engine = ::regorus::Engine::new();
// For more OPA compatibility out of the box, we ask builtins to return undefined
// instead of raising errors in certain failure scenarios.
engine.set_strict_builtin_errors(false);
Box::into_raw(Box::new(RegorusEngine { engine }))
}
/// Clone a [`RegorusEngine`]
///
/// To avoid having to parse same policy again, the engine can be cloned
/// after policies and data have been added.
///
#[no_mangle]
pub extern "C" fn regorus_engine_clone(engine: *mut RegorusEngine) -> *mut RegorusEngine {
match to_ref(engine) {
Ok(e) => Box::into_raw(Box::new(e.clone())),
_ => std::ptr::null_mut(),
}
}
#[no_mangle]
pub extern "C" fn regorus_engine_drop(engine: *mut RegorusEngine) {
if let Ok(e) = to_ref(engine) {
unsafe {
let _ = Box::from_raw(std::ptr::from_mut(e));
}
}
}
/// Add a policy
///
/// The policy is parsed into AST.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_policy
///
/// * `path`: A filename to be associated with the policy.
/// * `rego`: Rego policy.
#[no_mangle]
pub extern "C" fn regorus_engine_add_policy(
engine: *mut RegorusEngine,
path: *const c_char,
rego: *const c_char,
) -> RegorusResult {
to_regorus_string_result(|| -> Result<String> {
to_ref(engine)?
.engine
.add_policy(from_c_str(path)?, from_c_str(rego)?)
}())
}
#[cfg(feature = "std")]
#[no_mangle]
pub extern "C" fn regorus_engine_add_policy_from_file(
engine: *mut RegorusEngine,
path: *const c_char,
) -> RegorusResult {
to_regorus_string_result(|| -> Result<String> {
to_ref(engine)?
.engine
.add_policy_from_file(from_c_str(path)?)
}())
}
/// Add policy data.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_data
/// * `data`: JSON encoded value to be used as policy data.
#[no_mangle]
pub extern "C" fn regorus_engine_add_data_json(
engine: *mut RegorusEngine,
data: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?
.engine
.add_data(regorus::Value::from_json_str(&from_c_str(data)?)?)
}())
}
/// Get list of loaded Rego packages as JSON.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_packages
#[no_mangle]
pub extern "C" fn regorus_engine_get_packages(engine: *mut RegorusEngine) -> RegorusResult {
to_regorus_string_result(|| -> Result<String> {
serde_json::to_string_pretty(&to_ref(engine)?.engine.get_packages()?)
.map_err(anyhow::Error::msg)
}())
}
/// Get list of policies as JSON.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_policies
#[no_mangle]
pub extern "C" fn regorus_engine_get_policies(engine: *mut RegorusEngine) -> RegorusResult {
to_regorus_string_result(|| -> Result<String> {
to_ref(engine)?.engine.get_policies_as_json()
}())
}
#[cfg(feature = "std")]
#[no_mangle]
pub extern "C" fn regorus_engine_add_data_from_json_file(
engine: *mut RegorusEngine,
path: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?
.engine
.add_data(regorus::Value::from_json_file(from_c_str(path)?)?)
}())
}
/// Clear policy data.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.clear_data
#[no_mangle]
pub extern "C" fn regorus_engine_clear_data(engine: *mut RegorusEngine) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?.engine.clear_data();
Ok(())
}())
}
/// Set input.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_input
/// * `input`: JSON encoded value to be used as input to query.
#[no_mangle]
pub extern "C" fn regorus_engine_set_input_json(
engine: *mut RegorusEngine,
input: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?
.engine
.set_input(regorus::Value::from_json_str(&from_c_str(input)?)?);
Ok(())
}())
}
#[cfg(feature = "std")]
#[no_mangle]
pub extern "C" fn regorus_engine_set_input_from_json_file(
engine: *mut RegorusEngine,
path: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?
.engine
.set_input(regorus::Value::from_json_file(from_c_str(path)?)?);
Ok(())
}())
}
/// Evaluate query.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.eval_query
/// * `query`: Rego expression to be evaluate.
#[no_mangle]
pub extern "C" fn regorus_engine_eval_query(
engine: *mut RegorusEngine,
query: *const c_char,
) -> RegorusResult {
let output = || -> Result<String> {
let results = to_ref(engine)?
.engine
.eval_query(from_c_str(query)?, false)?;
Ok(serde_json::to_string_pretty(&results)?)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
}
/// Evaluate specified rule.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.eval_rule
/// * `rule`: Path to the rule.
#[no_mangle]
pub extern "C" fn regorus_engine_eval_rule(
engine: *mut RegorusEngine,
rule: *const c_char,
) -> RegorusResult {
let output = || -> Result<String> {
to_ref(engine)?
.engine
.eval_rule(from_c_str(rule)?)?
.to_json_str()
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
}
/// Enable/disable coverage.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_enable_coverage
/// * `enable`: Whether to enable or disable coverage.
#[no_mangle]
#[cfg(feature = "coverage")]
pub extern "C" fn regorus_engine_set_enable_coverage(
engine: *mut RegorusEngine,
enable: bool,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?.engine.set_enable_coverage(enable);
Ok(())
}())
}
/// Get coverage report.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_coverage_report
#[no_mangle]
#[cfg(feature = "coverage")]
pub extern "C" fn regorus_engine_get_coverage_report(engine: *mut RegorusEngine) -> RegorusResult {
let output = || -> Result<String> {
Ok(serde_json::to_string_pretty(
&to_ref(engine)?.engine.get_coverage_report()?,
)?)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
}
/// Enable/disable strict builtin errors.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_strict_builtin_errors
/// * `strict`: Whether to raise errors or return undefined on certain scenarios.
#[no_mangle]
pub extern "C" fn regorus_engine_set_strict_builtin_errors(
engine: *mut RegorusEngine,
strict: bool,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?.engine.set_strict_builtin_errors(strict);
Ok(())
}())
}
/// Get pretty printed coverage report.
///
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Report.html#method.to_string_pretty
#[no_mangle]
#[cfg(feature = "coverage")]
pub extern "C" fn regorus_engine_get_coverage_report_pretty(
engine: *mut RegorusEngine,
) -> RegorusResult {
let output = || -> Result<String> {
to_ref(engine)?
.engine
.get_coverage_report()?
.to_string_pretty()
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
}
/// Clear coverage data.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.clear_coverage_data
#[no_mangle]
#[cfg(feature = "coverage")]
pub extern "C" fn regorus_engine_clear_coverage_data(engine: *mut RegorusEngine) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?.engine.clear_coverage_data();
Ok(())
}())
}
/// Whether to gather output of print statements.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_gather_prints
/// * `enable`: Whether to enable or disable gathering print statements.
#[no_mangle]
pub extern "C" fn regorus_engine_set_gather_prints(
engine: *mut RegorusEngine,
enable: bool,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?.engine.set_gather_prints(enable);
Ok(())
}())
}
/// Take all the gathered print statements.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.take_prints
#[no_mangle]
pub extern "C" fn regorus_engine_take_prints(engine: *mut RegorusEngine) -> RegorusResult {
let output = || -> Result<String> {
Ok(serde_json::to_string_pretty(
&to_ref(engine)?.engine.take_prints()?,
)?)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
}
/// Get AST of policies.
///
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Engine.html#method.get_ast_as_json
#[no_mangle]
#[cfg(feature = "ast")]
pub extern "C" fn regorus_engine_get_ast_as_json(engine: *mut RegorusEngine) -> RegorusResult {
let output = || -> Result<String> { to_ref(engine)?.engine.get_ast_as_json() }();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
}
/// Gets the package names defined in each policy added to the engine.
///
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Engine.html#method.get_policy_package_names
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_engine_get_policy_package_names(
engine: *mut RegorusEngine,
) -> RegorusResult {
let output = || -> Result<String> {
serde_json::to_string_pretty(&to_ref(engine)?.engine.get_policy_package_names()?)
.map_err(anyhow::Error::msg)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
}
/// Gets the parameters defined in each policy added to the engine.
///
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Engine.html#method.get_policy_parameters
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_engine_get_policy_parameters(
engine: *mut RegorusEngine,
) -> RegorusResult {
let output = || -> Result<String> {
serde_json::to_string_pretty(&to_ref(engine)?.engine.get_policy_parameters()?)
.map_err(anyhow::Error::msg)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
}
/// Enable/disable rego v1.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_rego_v0
#[no_mangle]
pub extern "C" fn regorus_engine_set_rego_v0(
engine: *mut RegorusEngine,
enable: bool,
) -> RegorusResult {
let output = || -> Result<()> {
to_ref(engine)?.engine.set_rego_v0(enable);
Ok(())
}();
match output {
Ok(()) => RegorusResult::ok_void(),
Err(e) => to_regorus_result(Err(e)),
}
}
/// Compile a target-aware policy from the current engine state.
///
/// This method creates a compiled policy that can work with Azure Policy targets,
/// enabling resource type inference and target-specific evaluation.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.compile_for_target
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_engine_compile_for_target(engine: *mut RegorusEngine) -> RegorusResult {
match to_ref(engine) {
Ok(e) => match e.engine.compile_for_target() {
Ok(compiled_policy) => {
let wrapped_policy = RegorusCompiledPolicy { compiled_policy };
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut std::os::raw::c_void)
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Failed to compile for target: {e}"),
),
},
Err(e) => RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Failed to get engine reference: {e}"),
),
}
}
/// Compile a policy with a specific entry point rule.
///
/// This method creates a compiled policy that evaluates a specific rule as the entry point.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.compile_with_entrypoint
/// * `rule`: The specific rule path to evaluate (e.g., "data.policy.allow")
#[no_mangle]
pub extern "C" fn regorus_engine_compile_with_entrypoint(
engine: *mut RegorusEngine,
rule: *const c_char,
) -> RegorusResult {
let result = || -> Result<RegorusCompiledPolicy> {
let rule_str = from_c_str(rule)?;
let rule_rc: regorus::Rc<str> = rule_str.into();
let compiled_policy = to_ref(engine)?.engine.compile_with_entrypoint(&rule_rc)?;
Ok(RegorusCompiledPolicy { compiled_policy })
}();
match result {
Ok(wrapped_policy) => {
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut std::os::raw::c_void)
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Failed to compile with entrypoint: {e}"),
),
}
}

View File

@@ -1,553 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use anyhow::{anyhow, bail, Result};
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
/// Status of a call on `RegorusEngine`.
#[repr(C)]
pub enum RegorusStatus {
/// The operation was successful.
RegorusStatusOk,
/// The operation was unsuccessful.
RegorusStatusError,
}
/// Result of a call on `RegorusEngine`.
///
/// Must be freed using `regorus_result_drop`.
#[repr(C)]
pub struct RegorusResult {
/// Status
status: RegorusStatus,
/// Output produced by the call.
/// Owned by Rust.
output: *mut c_char,
/// Errors produced by the call.
/// Owned by Rust.
error_message: *mut c_char,
}
fn to_c_str(s: String) -> *mut c_char {
match CString::new(s) {
Ok(cs) => cs.into_raw(),
_ => to_c_str("binding error: failed to create c-style string".to_string()),
}
}
fn from_c_str(name: &str, s: *const c_char) -> Result<String> {
if s.is_null() {
bail!("null pointer");
}
unsafe {
CStr::from_ptr(s)
.to_str()
.map_err(|e| anyhow!("`{name}`: invalid utf8.\n{e}"))
.map(|s| s.to_string())
}
}
fn to_ref<'a, T>(t: *mut T) -> Result<&'a mut T> {
unsafe { t.as_mut().ok_or_else(|| anyhow!("null pointer")) }
}
fn to_regorus_result(r: Result<()>) -> RegorusResult {
match r {
Ok(()) => RegorusResult {
status: RegorusStatus::RegorusStatusOk,
output: std::ptr::null_mut(),
error_message: std::ptr::null_mut(),
},
Err(e) => RegorusResult {
status: RegorusStatus::RegorusStatusError,
output: std::ptr::null_mut(),
error_message: to_c_str(format!("{e}")),
},
}
}
fn to_regorus_string_result(r: Result<String>) -> RegorusResult {
match r {
Ok(s) => RegorusResult {
status: RegorusStatus::RegorusStatusOk,
output: to_c_str(s),
error_message: std::ptr::null_mut(),
},
Err(e) => RegorusResult {
status: RegorusStatus::RegorusStatusError,
output: std::ptr::null_mut(),
error_message: to_c_str(format!("{e}")),
},
}
}
/// Wrapper for `regorus::Engine`.
#[derive(Clone)]
pub struct RegorusEngine {
engine: ::regorus::Engine,
}
/// Drop a `RegorusResult`.
///
/// `output` and `error_message` strings are not valid after drop.
#[no_mangle]
pub extern "C" fn regorus_result_drop(r: RegorusResult) {
unsafe {
if !r.error_message.is_null() {
let _ = CString::from_raw(r.error_message);
}
if !r.output.is_null() {
let _ = CString::from_raw(r.output);
}
}
}
#[no_mangle]
/// Construct a new Engine
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html
pub extern "C" fn regorus_engine_new() -> *mut RegorusEngine {
let mut engine = ::regorus::Engine::new();
// For more OPA compatibility out of the box, we ask builtins to return undefined
// instead of raising errors in certain failure scenarios.
engine.set_strict_builtin_errors(false);
Box::into_raw(Box::new(RegorusEngine { engine }))
}
/// Clone a [`RegorusEngine`]
///
/// To avoid having to parse same policy again, the engine can be cloned
/// after policies and data have been added.
///
#[no_mangle]
pub extern "C" fn regorus_engine_clone(engine: *mut RegorusEngine) -> *mut RegorusEngine {
match to_ref(engine) {
Ok(e) => Box::into_raw(Box::new(e.clone())),
_ => std::ptr::null_mut(),
}
}
#[no_mangle]
pub extern "C" fn regorus_engine_drop(engine: *mut RegorusEngine) {
if let Ok(e) = to_ref(engine) {
unsafe {
let _ = Box::from_raw(std::ptr::from_mut(e));
}
}
}
/// Add a policy
///
/// The policy is parsed into AST.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_policy
///
/// * `path`: A filename to be associated with the policy.
/// * `rego`: Rego policy.
#[no_mangle]
pub extern "C" fn regorus_engine_add_policy(
engine: *mut RegorusEngine,
path: *const c_char,
rego: *const c_char,
) -> RegorusResult {
to_regorus_string_result(|| -> Result<String> {
to_ref(engine)?
.engine
.add_policy(from_c_str("path", path)?, from_c_str("rego", rego)?)
}())
}
#[cfg(feature = "std")]
#[no_mangle]
pub extern "C" fn regorus_engine_add_policy_from_file(
engine: *mut RegorusEngine,
path: *const c_char,
) -> RegorusResult {
to_regorus_string_result(|| -> Result<String> {
to_ref(engine)?
.engine
.add_policy_from_file(from_c_str("path", path)?)
}())
}
/// Add policy data.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_data
/// * `data`: JSON encoded value to be used as policy data.
#[no_mangle]
pub extern "C" fn regorus_engine_add_data_json(
engine: *mut RegorusEngine,
data: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?
.engine
.add_data(regorus::Value::from_json_str(&from_c_str("data", data)?)?)
}())
}
/// Get list of loaded Rego packages as JSON.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_packages
#[no_mangle]
pub extern "C" fn regorus_engine_get_packages(engine: *mut RegorusEngine) -> RegorusResult {
to_regorus_string_result(|| -> Result<String> {
serde_json::to_string_pretty(&to_ref(engine)?.engine.get_packages()?)
.map_err(anyhow::Error::msg)
}())
}
/// Get list of policies as JSON.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_policies
#[no_mangle]
pub extern "C" fn regorus_engine_get_policies(engine: *mut RegorusEngine) -> RegorusResult {
to_regorus_string_result(|| -> Result<String> {
to_ref(engine)?.engine.get_policies_as_json()
}())
}
#[cfg(feature = "std")]
#[no_mangle]
pub extern "C" fn regorus_engine_add_data_from_json_file(
engine: *mut RegorusEngine,
path: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?
.engine
.add_data(regorus::Value::from_json_file(from_c_str("path", path)?)?)
}())
}
/// Clear policy data.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.clear_data
#[no_mangle]
pub extern "C" fn regorus_engine_clear_data(engine: *mut RegorusEngine) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?.engine.clear_data();
Ok(())
}())
}
/// Set input.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_input
/// * `input`: JSON encoded value to be used as input to query.
#[no_mangle]
pub extern "C" fn regorus_engine_set_input_json(
engine: *mut RegorusEngine,
input: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?
.engine
.set_input(regorus::Value::from_json_str(&from_c_str("input", input)?)?);
Ok(())
}())
}
#[cfg(feature = "std")]
#[no_mangle]
pub extern "C" fn regorus_engine_set_input_from_json_file(
engine: *mut RegorusEngine,
path: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?
.engine
.set_input(regorus::Value::from_json_file(from_c_str("path", path)?)?);
Ok(())
}())
}
/// Evaluate query.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.eval_query
/// * `query`: Rego expression to be evaluate.
#[no_mangle]
pub extern "C" fn regorus_engine_eval_query(
engine: *mut RegorusEngine,
query: *const c_char,
) -> RegorusResult {
let output = || -> Result<String> {
let results = to_ref(engine)?
.engine
.eval_query(from_c_str("query", query)?, false)?;
Ok(serde_json::to_string_pretty(&results)?)
}();
match output {
Ok(out) => RegorusResult {
status: RegorusStatus::RegorusStatusOk,
output: to_c_str(out),
error_message: std::ptr::null_mut(),
},
Err(e) => to_regorus_result(Err(e)),
}
}
/// Evaluate specified rule.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.eval_rule
/// * `rule`: Path to the rule.
#[no_mangle]
pub extern "C" fn regorus_engine_eval_rule(
engine: *mut RegorusEngine,
rule: *const c_char,
) -> RegorusResult {
let output = || -> Result<String> {
to_ref(engine)?
.engine
.eval_rule(from_c_str("rule", rule)?)?
.to_json_str()
}();
match output {
Ok(out) => RegorusResult {
status: RegorusStatus::RegorusStatusOk,
output: to_c_str(out),
error_message: std::ptr::null_mut(),
},
Err(e) => to_regorus_result(Err(e)),
}
}
/// Enable/disable coverage.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_enable_coverage
/// * `enable`: Whether to enable or disable coverage.
#[no_mangle]
#[cfg(feature = "coverage")]
pub extern "C" fn regorus_engine_set_enable_coverage(
engine: *mut RegorusEngine,
enable: bool,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?.engine.set_enable_coverage(enable);
Ok(())
}())
}
/// Get coverage report.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_coverage_report
#[no_mangle]
#[cfg(feature = "coverage")]
pub extern "C" fn regorus_engine_get_coverage_report(engine: *mut RegorusEngine) -> RegorusResult {
let output = || -> Result<String> {
Ok(serde_json::to_string_pretty(
&to_ref(engine)?.engine.get_coverage_report()?,
)?)
}();
match output {
Ok(out) => RegorusResult {
status: RegorusStatus::RegorusStatusOk,
output: to_c_str(out),
error_message: std::ptr::null_mut(),
},
Err(e) => to_regorus_result(Err(e)),
}
}
/// Enable/disable strict builtin errors.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_strict_builtin_errors
/// * `strict`: Whether to raise errors or return undefined on certain scenarios.
#[no_mangle]
pub extern "C" fn regorus_engine_set_strict_builtin_errors(
engine: *mut RegorusEngine,
strict: bool,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?.engine.set_strict_builtin_errors(strict);
Ok(())
}())
}
/// Get pretty printed coverage report.
///
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Report.html#method.to_string_pretty
#[no_mangle]
#[cfg(feature = "coverage")]
pub extern "C" fn regorus_engine_get_coverage_report_pretty(
engine: *mut RegorusEngine,
) -> RegorusResult {
let output = || -> Result<String> {
to_ref(engine)?
.engine
.get_coverage_report()?
.to_string_pretty()
}();
match output {
Ok(out) => RegorusResult {
status: RegorusStatus::RegorusStatusOk,
output: to_c_str(out),
error_message: std::ptr::null_mut(),
},
Err(e) => to_regorus_result(Err(e)),
}
}
/// Clear coverage data.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.clear_coverage_data
#[no_mangle]
#[cfg(feature = "coverage")]
pub extern "C" fn regorus_engine_clear_coverage_data(engine: *mut RegorusEngine) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?.engine.clear_coverage_data();
Ok(())
}())
}
/// Whether to gather output of print statements.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_gather_prints
/// * `enable`: Whether to enable or disable gathering print statements.
#[no_mangle]
pub extern "C" fn regorus_engine_set_gather_prints(
engine: *mut RegorusEngine,
enable: bool,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
to_ref(engine)?.engine.set_gather_prints(enable);
Ok(())
}())
}
/// Take all the gathered print statements.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.take_prints
#[no_mangle]
pub extern "C" fn regorus_engine_take_prints(engine: *mut RegorusEngine) -> RegorusResult {
let output = || -> Result<String> {
Ok(serde_json::to_string_pretty(
&to_ref(engine)?.engine.take_prints()?,
)?)
}();
match output {
Ok(out) => RegorusResult {
status: RegorusStatus::RegorusStatusOk,
output: to_c_str(out),
error_message: std::ptr::null_mut(),
},
Err(e) => to_regorus_result(Err(e)),
}
}
/// Get AST of policies.
///
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Engine.html#method.get_ast_as_json
#[no_mangle]
#[cfg(feature = "ast")]
pub extern "C" fn regorus_engine_get_ast_as_json(engine: *mut RegorusEngine) -> RegorusResult {
let output = || -> Result<String> { to_ref(engine)?.engine.get_ast_as_json() }();
match output {
Ok(out) => RegorusResult {
status: RegorusStatus::RegorusStatusOk,
output: to_c_str(out),
error_message: std::ptr::null_mut(),
},
Err(e) => to_regorus_result(Err(e)),
}
}
/// Gets the package names defined in each policy added to the engine.
///
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Engine.html#method.get_policy_package_names
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_engine_get_policy_package_names(
engine: *mut RegorusEngine,
) -> RegorusResult {
let output = || -> Result<String> {
serde_json::to_string_pretty(&to_ref(engine)?.engine.get_policy_package_names()?)
.map_err(anyhow::Error::msg)
}();
match output {
Ok(out) => RegorusResult {
status: RegorusStatus::RegorusStatusOk,
output: to_c_str(out),
error_message: std::ptr::null_mut(),
},
Err(e) => to_regorus_result(Err(e)),
}
}
/// Gets the parameters defined in each policy added to the engine.
///
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Engine.html#method.get_policy_parameters
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_engine_get_policy_parameters(
engine: *mut RegorusEngine,
) -> RegorusResult {
let output = || -> Result<String> {
serde_json::to_string_pretty(&to_ref(engine)?.engine.get_policy_parameters()?)
.map_err(anyhow::Error::msg)
}();
match output {
Ok(out) => RegorusResult {
status: RegorusStatus::RegorusStatusOk,
output: to_c_str(out),
error_message: std::ptr::null_mut(),
},
Err(e) => to_regorus_result(Err(e)),
}
}
/// Enable/disable rego v1.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_rego_v0
#[no_mangle]
pub extern "C" fn regorus_engine_set_rego_v0(
engine: *mut RegorusEngine,
enable: bool,
) -> RegorusResult {
let output = || -> Result<()> {
to_ref(engine)?.engine.set_rego_v0(enable);
Ok(())
}();
match output {
Ok(()) => RegorusResult {
status: RegorusStatus::RegorusStatusOk,
output: std::ptr::null_mut(),
error_message: std::ptr::null_mut(),
},
Err(e) => to_regorus_result(Err(e)),
}
}
#[cfg(feature = "custom_allocator")]
extern "C" {
fn regorus_aligned_alloc(alignment: usize, size: usize) -> *mut u8;
fn regorus_free(ptr: *mut u8);
}
#[cfg(feature = "custom_allocator")]
mod allocator {
use std::alloc::{GlobalAlloc, Layout};
struct RegorusAllocator {}
unsafe impl GlobalAlloc for RegorusAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let size = layout.size();
let align = layout.align();
crate::regorus_aligned_alloc(align, size)
}
unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
crate::regorus_free(ptr)
}
}
#[global_allocator]
static ALLOCATOR: RegorusAllocator = RegorusAllocator {};
}
mod allocator;
mod common;
mod compile;
mod compiled_policy;
mod effect_registry;
mod engine;
mod schema_registry;
mod target_registry;

View File

@@ -0,0 +1,178 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Schema registry functions for FFI.
//!
//! These functions provide access to regorus's resource schema registry functionality,
//! enabling registration and management of Azure Policy resource schemas.
#![cfg(feature = "azure_policy")]
use crate::common::{from_c_str, RegorusResult, RegorusStatus};
use regorus::{registry::schemas, Schema};
use std::os::raw::c_char;
// Resource Schema Registry Functions
/// Register a resource schema from JSON with a given name.
///
/// # Parameters
/// * `name` - Name to register the schema under
/// * `schema_json` - JSON string representing the schema
///
/// # Returns
/// Returns a RegorusResult with success/error status.
///
/// # Safety
/// All string parameters must be valid null-terminated UTF-8 strings.
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_register(
name: *const c_char,
schema_json: *const c_char,
) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid schema name string: {e}"),
)
}
};
let schema_str = match from_c_str(schema_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid schema JSON string: {e}"),
)
}
};
// Parse schema from JSON
let schema = match Schema::from_json_str(&schema_str) {
Ok(schema) => schema,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse schema JSON: {e}"),
)
}
};
// Register the schema
match schemas::resource::register(schema_name, schema.into()) {
Ok(()) => RegorusResult::ok_pointer(std::ptr::null_mut()),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to register schema: {e}"),
),
}
}
/// Check if a resource schema with the given name exists.
///
/// # Parameters
/// * `name` - Name of the schema to check
///
/// # Returns
/// Returns a RegorusResult with "true" or "false" string output.
///
/// # Safety
/// The name parameter must be a valid null-terminated UTF-8 string.
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_contains(name: *const c_char) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid schema name string: {e}"),
)
}
};
let contains = schemas::resource::contains(&schema_name);
RegorusResult::ok_bool(contains)
}
/// Get the number of registered resource schemas.
///
/// # Returns
/// Returns a RegorusResult with the count as a string.
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_len() -> RegorusResult {
let count = schemas::resource::len();
RegorusResult::ok_int(count as i64)
}
/// Check if the resource schema registry is empty.
///
/// # Returns
/// Returns a RegorusResult with "true" or "false" string output.
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_is_empty() -> RegorusResult {
let is_empty = schemas::resource::is_empty();
RegorusResult::ok_bool(is_empty)
}
/// List all registered resource schema names as a JSON array.
///
/// # Returns
/// Returns a RegorusResult with a JSON array of schema names.
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_list_names() -> RegorusResult {
let names = schemas::resource::list_names();
match serde_json::to_string(&names) {
Ok(json_str) => RegorusResult::ok_string(json_str),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to serialize schema names to JSON: {e}"),
),
}
}
/// Remove a resource schema by name.
///
/// # Parameters
/// * `name` - Name of the schema to remove
///
/// # Returns
/// Returns a RegorusResult with "true" if removed, "false" if not found.
///
/// # Safety
/// The name parameter must be a valid null-terminated UTF-8 string.
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_remove(name: *const c_char) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid schema name string: {e}"),
)
}
};
let removed = schemas::resource::remove(&schema_name).is_some();
RegorusResult::ok_bool(removed)
}
/// Clear all resource schemas from the registry.
///
/// # Returns
/// Returns a RegorusResult with success status.
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_clear() -> RegorusResult {
schemas::resource::clear();
RegorusResult::ok_pointer(std::ptr::null_mut())
}

View File

@@ -0,0 +1,107 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![cfg(feature = "azure_policy")]
use crate::common::*;
use anyhow::Result;
use std::os::raw::c_char;
/// Register a target from JSON definition.
///
/// The target JSON should follow the target schema format.
/// Once registered, the target can be referenced in Rego policies using `__target__` rules.
///
/// * `target_json`: JSON encoded target definition
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_register_target_from_json(target_json: *const c_char) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let target_str = from_c_str(target_json)?;
let target = regorus::Target::from_json_str(&target_str)?;
regorus::registry::targets::register(regorus::Rc::new(target))?;
Ok(())
}())
}
/// Check if a target is registered.
///
/// # Parameters
/// * `name` - Name of the target to check
///
/// # Returns
/// Returns a RegorusResult with boolean value indicating if the target is registered.
///
/// # Safety
/// The name parameter must be a valid null-terminated UTF-8 string.
#[no_mangle]
pub extern "C" fn regorus_target_registry_contains(name: *const c_char) -> RegorusResult {
let target_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid target name string: {e}"),
)
}
};
let contains = regorus::registry::targets::contains(&target_name);
RegorusResult::ok_bool(contains)
}
/// Get a list of all registered target names as JSON array.
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_target_registry_list_names() -> RegorusResult {
let names = regorus::registry::targets::list_names();
let output = serde_json::to_string_pretty(&names).map_err(anyhow::Error::msg);
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
}
/// Remove a target from the registry by name.
///
/// * `name`: The target name to remove
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_target_registry_remove(name: *const c_char) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let name_str = from_c_str(name)?;
regorus::registry::targets::remove(&name_str);
Ok(())
}())
}
/// Clear all targets from the registry.
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_target_registry_clear() -> RegorusResult {
regorus::registry::targets::clear();
RegorusResult::ok_void()
}
/// Get the number of registered targets.
///
/// # Returns
/// Returns a RegorusResult with the count as an integer value.
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_target_registry_len() -> RegorusResult {
let count = regorus::registry::targets::len();
RegorusResult::ok_int(count as i64)
}
/// Check if the target registry is empty.
///
/// # Returns
/// Returns a RegorusResult with boolean value indicating if the registry is empty.
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_target_registry_is_empty() -> RegorusResult {
let is_empty = regorus::registry::targets::is_empty();
RegorusResult::ok_bool(is_empty)
}

View File

@@ -32,7 +32,7 @@ func (e *Engine) SetRegoV0(enable bool) error {
result := C.regorus_engine_set_rego_v0(e.e, C.bool(enable))
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
@@ -48,7 +48,7 @@ func (e *Engine) AddPolicy(path string, rego string) (string, error) {
result := C.regorus_engine_add_policy(e.e, path_c, rego_c)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
if result.status != C.Ok {
return "", fmt.Errorf("%s", C.GoString(result.error_message))
}
return C.GoString(result.output), nil
@@ -60,7 +60,7 @@ func (e *Engine) AddPolicyFromFile(path string) (string, error) {
result := C.regorus_engine_add_policy_from_file(e.e, path_c)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
if result.status != C.Ok {
return "", fmt.Errorf("%s", C.GoString(result.error_message))
}
return C.GoString(result.output), nil
@@ -69,7 +69,7 @@ func (e *Engine) AddPolicyFromFile(path string) (string, error) {
func (e *Engine) GetPackages() (string, error) {
result := C.regorus_engine_get_packages(e.e)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
if result.status != C.Ok {
return "", fmt.Errorf("%s", C.GoString(result.error_message))
}
return C.GoString(result.output), nil
@@ -78,7 +78,7 @@ func (e *Engine) GetPackages() (string, error) {
func (e *Engine) GetPolicies() (string, error) {
result := C.regorus_engine_get_policies(e.e)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
if result.status != C.Ok {
return "", fmt.Errorf("%s", C.GoString(result.error_message))
}
return C.GoString(result.output), nil
@@ -90,7 +90,7 @@ func (e *Engine) AddDataJson(data string) error {
result := C.regorus_engine_add_data_json(e.e, data_c)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
@@ -102,7 +102,7 @@ func (e *Engine) AddDataFromJsonFile(path string) error {
result := C.regorus_engine_add_data_from_json_file(e.e, path_c)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
@@ -114,7 +114,7 @@ func (e *Engine) SetInputJson(input string) error {
result := C.regorus_engine_set_input_json(e.e, input_c)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
@@ -126,7 +126,7 @@ func (e *Engine) SetInputFromJsonFile(path string) error {
result := C.regorus_engine_set_input_from_json_file(e.e, path_c)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
@@ -138,7 +138,7 @@ func (e *Engine) EvalQuery(query string) (string, error) {
result := C.regorus_engine_eval_query(e.e, query_c)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
if result.status != C.Ok {
return "", fmt.Errorf("%s", C.GoString(result.error_message))
}
@@ -151,7 +151,7 @@ func (e *Engine) EvalRule(rule string) (string, error) {
result := C.regorus_engine_eval_rule(e.e, rule_c)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
if result.status != C.Ok {
return "", fmt.Errorf("%s", C.GoString(result.error_message))
}
@@ -161,7 +161,7 @@ func (e *Engine) EvalRule(rule string) (string, error) {
func (e *Engine) SetEnableCoverage(enable bool) error {
result := C.regorus_engine_set_enable_coverage(e.e, C.bool(enable))
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
@@ -170,7 +170,7 @@ func (e *Engine) SetEnableCoverage(enable bool) error {
func (e *Engine) ClearCoverageData() error {
result := C.regorus_engine_clear_coverage_data(e.e)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
@@ -179,7 +179,7 @@ func (e *Engine) ClearCoverageData() error {
func (e *Engine) GetCoverageReport() (string, error) {
result := C.regorus_engine_get_coverage_report(e.e)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
if result.status != C.Ok {
return "", fmt.Errorf("%s", C.GoString(result.error_message))
}
@@ -189,7 +189,7 @@ func (e *Engine) GetCoverageReport() (string, error) {
func (e *Engine) GetCoverageReportPretty() (string, error) {
result := C.regorus_engine_get_coverage_report_pretty(e.e)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
if result.status != C.Ok {
return "", fmt.Errorf("%s", C.GoString(result.error_message))
}
@@ -199,7 +199,7 @@ func (e *Engine) GetCoverageReportPretty() (string, error) {
func (e *Engine) SetGatherPrints(b bool) error {
result := C.regorus_engine_set_gather_prints(e.e, C.bool(b))
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
@@ -208,7 +208,7 @@ func (e *Engine) SetGatherPrints(b bool) error {
func (e *Engine) TakePrints() (string, error) {
result := C.regorus_engine_take_prints(e.e)
defer C.regorus_result_drop(result)
if result.status != C.RegorusStatusOk {
if result.status != C.Ok {
return "", fmt.Errorf("%s", C.GoString(result.error_message))
}

View File

@@ -42,9 +42,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.98"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
[[package]]
name = "autocfg"
@@ -435,7 +435,7 @@ dependencies = [
"combine",
"jni-sys",
"log",
"thiserror",
"thiserror 1.0.69",
"walkdir",
"windows-sys 0.45.0",
]
@@ -689,9 +689,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.96"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beef09f85ae72cea1ef96ba6870c51e6382ebfa4f0e85b643459331f3daa5be0"
checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1"
dependencies = [
"unicode-ident",
]
@@ -831,6 +831,7 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml",
"thiserror 2.0.14",
"url",
"uuid",
]
@@ -969,9 +970,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "syn"
version = "2.0.104"
version = "2.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619"
dependencies = [
"proc-macro2",
"quote",
@@ -995,7 +996,16 @@ version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e"
dependencies = [
"thiserror-impl 2.0.14",
]
[[package]]
@@ -1009,6 +1019,17 @@ dependencies = [
"syn",
]
[[package]]
name = "thiserror-impl"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinystr"
version = "0.8.1"

View File

@@ -42,9 +42,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.98"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
[[package]]
name = "autocfg"
@@ -681,9 +681,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.96"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beef09f85ae72cea1ef96ba6870c51e6382ebfa4f0e85b643459331f3daa5be0"
checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1"
dependencies = [
"unicode-ident",
]
@@ -887,6 +887,7 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml",
"thiserror",
"url",
"uuid",
]
@@ -1017,9 +1018,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "syn"
version = "2.0.104"
version = "2.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619"
dependencies = [
"proc-macro2",
"quote",
@@ -1043,6 +1044,26 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
[[package]]
name = "thiserror"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinystr"
version = "0.8.1"

View File

@@ -42,9 +42,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.98"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
[[package]]
name = "autocfg"
@@ -761,9 +761,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.96"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beef09f85ae72cea1ef96ba6870c51e6382ebfa4f0e85b643459331f3daa5be0"
checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1"
dependencies = [
"unicode-ident",
]
@@ -933,6 +933,7 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml",
"thiserror",
"url",
"uuid",
]
@@ -1091,9 +1092,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "syn"
version = "2.0.104"
version = "2.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619"
dependencies = [
"proc-macro2",
"quote",
@@ -1117,6 +1118,26 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "thiserror"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinystr"
version = "0.8.1"

View File

@@ -42,9 +42,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.98"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
[[package]]
name = "autocfg"
@@ -670,9 +670,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.96"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beef09f85ae72cea1ef96ba6870c51e6382ebfa4f0e85b643459331f3daa5be0"
checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1"
dependencies = [
"unicode-ident",
]
@@ -812,6 +812,7 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml",
"thiserror",
"url",
"uuid",
]
@@ -953,9 +954,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "syn"
version = "2.0.104"
version = "2.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619"
dependencies = [
"proc-macro2",
"quote",
@@ -973,6 +974,26 @@ dependencies = [
"syn",
]
[[package]]
name = "thiserror"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinystr"
version = "0.8.1"

View File

@@ -465,6 +465,9 @@ pub struct Module {
#[cfg_attr(feature = "ast", serde(rename(serialize = "rules")))]
pub policy: Vec<Ref<Rule>>,
pub rego_v1: bool,
// Target name if specified via __target__ rule
#[cfg_attr(feature = "ast", serde(skip_serializing_if = "Option::is_none"))]
pub target: Option<String>,
// Number of expressions in the module.
pub num_expressions: u32,
// Number of statements in the module.

View File

@@ -280,19 +280,11 @@ fn sprintf(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) ->
args_idx += 1;
// Handle Golang flags.
let mut emit_sign = false;
let mut leave_space_for_elided_sign = false;
match chars.peek() {
Some('+') => {
emit_sign = true;
chars.next();
}
Some(' ') => {
leave_space_for_elided_sign = true;
chars.next();
}
_ => (),
}
let emit_sign = false;
let leave_space_for_elided_sign = false;
// Note: Golang flags come BEFORE the format verb, not after.
// This code was incorrectly consuming characters after the verb.
// Removing the incorrect flag handling to fix sprintf spacing.
let get_sign_value = |f: &Number| match (emit_sign, f) {
(_, v) if v < &Number::from(0.0) => ("-", v.clone()),

91
src/compile.rs Normal file
View File

@@ -0,0 +1,91 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::compiled_policy::CompiledPolicy;
use crate::engine::Engine;
use crate::value::Value;
use crate::*;
use anyhow::Result;
/// Represents a Rego policy module with an identifier and content.
#[derive(Debug, Clone)]
pub struct PolicyModule {
pub id: Rc<str>,
pub content: Rc<str>,
}
/// Compiles a target-aware policy from data and modules.
///
/// This is a convenience function that sets up an [`Engine`] and calls
/// [`Engine::compile_for_target`]. For more control over the compilation process
/// or to reuse an engine, use the engine method directly.
///
/// # Arguments
///
/// * `data` - Static data to be available during policy evaluation
/// * `modules` - Array of Rego policy modules to compile together
///
/// # Returns
///
/// Returns a [`CompiledPolicy`] for target-aware evaluation.
///
/// # Note
///
/// This function is only available when the `azure_policy` feature is enabled.
///
/// # See Also
///
/// - [`Engine::compile_for_target`] for detailed documentation and examples
/// - [`compile_policy_with_entrypoint`] for explicit rule-based compilation
#[cfg(feature = "azure_policy")]
#[cfg_attr(docsrs, doc(cfg(feature = "azure_policy")))]
pub fn compile_policy_for_target(data: Value, modules: &[PolicyModule]) -> Result<CompiledPolicy> {
let mut engine = setup_engine_with_modules(data, modules)?;
engine.compile_for_target()
}
/// Compiles a policy from data and modules with a specific entry point rule.
///
/// This is a convenience function that sets up an [`Engine`] and calls
/// [`Engine::compile_with_entrypoint`]. For more control over the compilation process
/// or to reuse an engine, use the engine method directly.
///
/// # Arguments
///
/// * `data` - Static data to be available during policy evaluation
/// * `modules` - Array of Rego policy modules to compile together
/// * `entry_point_rule` - The specific rule path to evaluate (e.g., "data.policy.allow")
///
/// # Returns
///
/// Returns a [`CompiledPolicy`] focused on the specified entry point rule.
///
/// # See Also
///
/// - [`Engine::compile_with_entrypoint`] for detailed documentation and examples
/// - [`compile_policy_for_target`] for target-aware compilation
pub fn compile_policy_with_entrypoint(
data: Value,
modules: &[PolicyModule],
entry_point_rule: Rc<str>,
) -> Result<CompiledPolicy> {
let mut engine = setup_engine_with_modules(data, modules)?;
engine.compile_with_entrypoint(&entry_point_rule)
}
/// Helper function to set up an engine with data and modules.
fn setup_engine_with_modules(data: Value, modules: &[PolicyModule]) -> Result<Engine> {
let mut engine = Engine::new();
// Add data to the engine
engine.add_data(data)?;
engine.set_gather_prints(true);
// Add all modules to the engine
for module in modules {
engine.add_policy(module.id.to_string(), module.content.to_string())?;
}
Ok(engine)
}

215
src/compiled_policy.rs Normal file
View File

@@ -0,0 +1,215 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::ast::*;
use crate::engine::Engine;
use crate::scheduler::*;
use crate::utils::*;
use crate::*;
use alloc::collections::BTreeMap;
use anyhow::Result;
#[cfg(feature = "azure_policy")]
use crate::target::Target;
pub(crate) type DefaultRuleInfo = (Ref<Rule>, Option<crate::String>);
#[cfg(feature = "azure_policy")]
pub(crate) type ResourceTypeInfo = (Rc<str>, Rc<Schema>);
#[cfg(feature = "azure_policy")]
pub(crate) type InferredResourceTypes = BTreeMap<Ref<Query>, ResourceTypeInfo>;
/// Wrapper around CompiledPolicyData that holds an Rc reference.
#[derive(Debug, Clone)]
pub struct CompiledPolicy {
inner: Rc<CompiledPolicyData>,
}
impl CompiledPolicy {
/// Create a new CompiledPolicy from CompiledPolicyData.
pub(crate) fn new(inner: Rc<CompiledPolicyData>) -> Self {
Self { inner }
}
}
impl CompiledPolicy {
/// Evaluate the compiled policy with the given input.
///
/// For target policies, evaluates the target's effect rule.
/// For regular policies, evaluates the originally compiled rule.
///
/// * `input`: Input data (resource) to validate against the policy.
///
/// Returns the result of evaluating the rule.
pub fn eval_with_input(&self, input: Value) -> Result<Value> {
let mut engine = Engine::new_from_compiled_policy(self.inner.clone());
// Set input
engine.set_input(input);
// Evaluate the rule
#[cfg(feature = "azure_policy")]
if let Some(target_info) = self.inner.target_info.as_ref() {
return engine.eval_rule(target_info.effect_path.to_string());
}
engine.eval_rule(self.inner.rule_to_evaluate.to_string())
}
/// Get information about the compiled policy including metadata about modules,
/// target configuration, and resource types.
///
/// Returns a [`crate::policy_info::PolicyInfo`] struct containing comprehensive
/// information about the compiled policy such as module IDs, target name,
/// applicable resource types, entry point rule, and parameters.
///
/// # Examples
///
/// ```no_run
/// use regorus::*;
/// # use std::sync::Arc;
///
/// # fn main() -> anyhow::Result<()> {
/// # // Register a target for the example
/// # #[cfg(feature = "azure_policy")]
/// # {
/// # let target = regorus::target::Target::from_json_file("tests/interpreter/cases/target/definitions/sample_target.json")?;
/// # regorus::registry::targets::register(std::sync::Arc::new(target))?;
/// # }
///
/// // Compile the policy
/// let policy_rego = r#"
/// package policy.example
/// import rego.v1
/// __target__ := "target.tests.sample_test_target"
///
/// effect := "allow" if {
/// input.type == "storage_account"
/// input.location in ["eastus", "westus"]
/// }
/// "#;
///
/// let modules = vec![regorus::PolicyModule {
/// id: "policy.rego".into(),
/// content: policy_rego.into(),
/// }];
///
/// #[cfg(feature = "azure_policy")]
/// let compiled = regorus::compile_policy_for_target(Value::new_object(), &modules)?;
/// #[cfg(not(feature = "azure_policy"))]
/// let compiled = regorus::compile_policy_with_entrypoint(Value::new_object(), &modules, "allow".into())?;
/// let info = compiled.get_policy_info()?;
///
/// assert_eq!(info.target_name, Some("target.tests.sample_test_target".into()));
/// assert_eq!(info.effect_rule, Some("effect".into()));
/// assert!(info.module_ids.len() > 0);
/// # Ok(())
/// # }
/// ```
pub fn get_policy_info(&self) -> Result<crate::policy_info::PolicyInfo> {
// Extract module IDs from the compiled policy
let module_ids: Vec<Rc<str>> = self
.inner
.modules
.iter()
.enumerate()
.map(|(i, module)| {
// Use source file path if available, otherwise generate an ID
let source_path = module.package.span.source.get_path();
if source_path.is_empty() {
format!("module_{}", i).into()
} else {
source_path.clone().into()
}
})
.collect();
// Extract target name and effect rule
#[cfg(feature = "azure_policy")]
let (target_name, effect_rule) = if let Some(target_info) = &self.inner.target_info {
(
Some(target_info.target.name.clone()),
Some(target_info.effect_name.clone()),
)
} else {
(None, None)
};
#[cfg(not(feature = "azure_policy"))]
let (target_name, effect_rule) = (None, None);
// Extract applicable resource types from inferred types
#[cfg(feature = "azure_policy")]
let applicable_resource_types: Vec<Rc<str>> =
if let Some(inferred_types) = &self.inner.inferred_resource_types {
inferred_types
.values()
.map(|(resource_type, _schema)| resource_type.clone())
.collect::<std::collections::BTreeSet<_>>() // Remove duplicates
.into_iter()
.collect()
} else {
Vec::new()
};
#[cfg(not(feature = "azure_policy"))]
let applicable_resource_types: Vec<Rc<str>> = Vec::new();
// Get parameters from the modules
#[cfg(feature = "azure_policy")]
let parameters = {
// Create a new engine from the compiled modules to extract parameters
let temp_engine = crate::engine::Engine::new_from_compiled_policy(self.inner.clone());
temp_engine.get_policy_parameters()?
};
Ok(crate::policy_info::PolicyInfo {
module_ids,
target_name,
applicable_resource_types,
entrypoint_rule: self.inner.rule_to_evaluate.clone(),
effect_rule,
#[cfg(feature = "azure_policy")]
parameters,
})
}
}
#[cfg(feature = "azure_policy")]
#[derive(Debug, Clone)]
pub(crate) struct TargetInfo {
pub(crate) target: Rc<Target>,
pub(crate) package: String,
pub(crate) effect_schema: Rc<Schema>,
pub(crate) effect_name: Rc<str>,
pub(crate) effect_path: Rc<str>,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct CompiledPolicyData {
pub(crate) modules: Rc<Vec<Ref<Module>>>,
pub(crate) schedule: Option<Schedule>,
pub(crate) rules: Map<String, Vec<Ref<Rule>>>,
pub(crate) default_rules: Map<String, Vec<DefaultRuleInfo>>,
pub(crate) imports: BTreeMap<String, Ref<Expr>>,
pub(crate) functions: FunctionTable,
pub(crate) rule_paths: Set<String>,
#[cfg(feature = "azure_policy")]
pub(crate) target_info: Option<TargetInfo>,
#[cfg(feature = "azure_policy")]
pub(crate) inferred_resource_types: Option<InferredResourceTypes>,
// User-defined rule to evaluate
pub(crate) rule_to_evaluate: Rc<str>,
// User-defined data
pub(crate) data: Option<Value>,
// Evaluation settings
pub(crate) strict_builtin_errors: bool,
// The semantics of extensions ought to be changes to be more Clone friendly.
pub(crate) extensions: Map<String, (u8, Rc<Box<dyn Extension>>)>,
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT License.
use crate::ast::*;
use crate::compiled_policy::CompiledPolicy;
use crate::interpreter::*;
use crate::lexer::*;
use crate::parser::*;
@@ -346,7 +347,7 @@ impl Engine {
/// Get the data document.
///
/// The returned value is the data document that has been constructed using
/// one or more calls to [`Engine::add_data`]. The values of policy rules are
/// one or more calls to [`Engine::pre`]. The values of policy rules are
/// not included in the returned document.
///
///
@@ -397,6 +398,259 @@ impl Engine {
&self.modules
}
/// Compiles a target-aware policy from the current engine state.
///
/// This method creates a compiled policy that can work with Azure Policy targets,
/// enabling resource type inference and target-specific evaluation. The compiled
/// policy will automatically detect and handle `__target__` declarations in the
/// loaded modules.
///
/// The engine must have been prepared with:
/// - Policy modules added via [`Engine::add_policy`]
/// - Data added via [`Engine::add_data`] (optional)
///
/// # Returns
///
/// Returns a [`CompiledPolicy`] that can be used for efficient policy evaluation
/// with target support, including resource type inference capabilities.
///
/// # Examples
///
/// ## Basic Target-Aware Compilation
///
/// ```no_run
/// use regorus::*;
///
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
/// engine.add_data(Value::from_json_str(r#"{"allowed_sizes": ["small", "medium"]}"#)?)?;
/// engine.add_policy("policy.rego".to_string(), r#"
/// package policy.test
/// import rego.v1
/// __target__ := "target.tests.sample_test_target"
///
/// default allow := false
/// allow if {
/// input.type == "vm"
/// input.size in data.allowed_sizes
/// }
/// "#.to_string())?;
///
/// let compiled = engine.compile_for_target()?;
/// let result = compiled.eval_with_input(Value::from_json_str(r#"{"type": "vm", "size": "small"}"#)?)?;
/// # Ok(())
/// # }
/// ```
///
/// ## Target Registration and Usage
///
/// ```no_run
/// use regorus::*;
/// use regorus::registry::targets;
/// use regorus::target::Target;
/// use std::sync::Arc;
///
/// # fn main() -> anyhow::Result<()> {
/// // Register a target first
/// let target_json = r#"
/// {
/// "name": "target.example.vm_policy",
/// "description": "Simple VM validation target",
/// "version": "1.0.0",
/// "resource_schema_selector": "type",
/// "resource_schemas": [
/// {
/// "type": "object",
/// "properties": {
/// "name": { "type": "string" },
/// "type": { "const": "vm" },
/// "size": { "enum": ["small", "medium", "large"] }
/// },
/// "required": ["name", "type", "size"]
/// }
/// ],
/// "effects": {
/// "allow": { "type": "boolean" },
/// "deny": { "type": "boolean" }
/// }
/// }
/// "#;
///
/// let target = Target::from_json_str(target_json)?;
/// targets::register(Arc::new(target))?;
///
/// // Use the target in a policy
/// let mut engine = Engine::new();
/// engine.add_data(Value::from_json_str(r#"{"allowed_locations": ["us-east"]}"#)?)?;
/// engine.add_policy("vm_policy.rego".to_string(), r#"
/// package vm.validation
/// import rego.v1
/// __target__ := "target.example.vm_policy"
///
/// default allow := false
/// allow if {
/// input.type == "vm"
/// input.size in ["small", "medium"]
/// }
/// "#.to_string())?;
///
/// let compiled = engine.compile_for_target()?;
/// let result = compiled.eval_with_input(Value::from_json_str(r#"
/// {
/// "name": "test-vm",
/// "type": "vm",
/// "size": "small"
/// }"#)?)?;
/// assert_eq!(result, Value::from(true));
/// # Ok(())
/// # }
/// ```
///
/// # Notes
///
/// - This method is only available when the `azure_policy` feature is enabled
/// - Automatically enables print gathering for debugging purposes
/// - Requires that at least one module contains a `__target__` declaration
/// - The target referenced must be registered in the target registry
///
/// # See Also
///
/// - [`Engine::compile_with_entrypoint`] for explicit rule-based compilation
/// - [`crate::compile_policy_for_target`] for a higher-level convenience function
#[cfg(feature = "azure_policy")]
#[cfg_attr(docsrs, doc(cfg(feature = "azure_policy")))]
pub fn compile_for_target(&mut self) -> Result<CompiledPolicy> {
self.prepare_for_eval(false, true)?;
self.interpreter.clean_internal_evaluation_state();
self.interpreter.compile(None).map(CompiledPolicy::new)
}
/// Compiles a policy with a specific entry point rule.
///
/// This method creates a compiled policy that evaluates a specific rule as the entry point.
/// Unlike [`Engine::compile_for_target`], this method requires you to explicitly specify which
/// rule should be evaluated and does not automatically handle target-specific features.
///
/// The engine must have been prepared with:
/// - Policy modules added via [`Engine::add_policy`]
/// - Data added via [`Engine::add_data`] (optional)
///
/// # Arguments
///
/// * `rule` - The specific rule path to evaluate (e.g., "data.policy.allow")
///
/// # Returns
///
/// Returns a [`CompiledPolicy`] that can be used for efficient policy evaluation
/// focused on the specified entry point rule.
///
/// # Examples
///
/// ## Basic Usage
///
/// ```no_run
/// use regorus::*;
/// use std::rc::Rc;
///
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
/// engine.add_data(Value::from_json_str(r#"{"allowed_users": ["alice", "bob"]}"#)?)?;
/// engine.add_policy("authz.rego".to_string(), r#"
/// package authz
/// import rego.v1
///
/// default allow := false
/// allow if {
/// input.user in data.allowed_users
/// input.action == "read"
/// }
///
/// deny if {
/// input.user == "guest"
/// }
/// "#.to_string())?;
///
/// let compiled = engine.compile_with_entrypoint(&"data.authz.allow".into())?;
/// let result = compiled.eval_with_input(Value::from_json_str(r#"{"user": "alice", "action": "read"}"#)?)?;
/// assert_eq!(result, Value::from(true));
/// # Ok(())
/// # }
/// ```
///
/// ## Multi-Module Policy
///
/// ```no_run
/// use regorus::*;
/// use std::rc::Rc;
///
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
/// engine.add_data(Value::from_json_str(r#"{"departments": {"engineering": ["alice"], "hr": ["bob"]}}"#)?)?;
///
/// engine.add_policy("users.rego".to_string(), r#"
/// package users
/// import rego.v1
///
/// user_department(user) := dept if {
/// dept := [d | data.departments[d][_] == user][0]
/// }
/// "#.to_string())?;
///
/// engine.add_policy("permissions.rego".to_string(), r#"
/// package permissions
/// import rego.v1
/// import data.users
///
/// default allow := false
/// allow if {
/// users.user_department(input.user) == "engineering"
/// input.resource.type == "code"
/// }
///
/// allow if {
/// users.user_department(input.user) == "hr"
/// input.resource.type == "personnel_data"
/// }
/// "#.to_string())?;
///
/// let compiled = engine.compile_with_entrypoint(&"data.permissions.allow".into())?;
///
/// // Test engineering access to code
/// let result = compiled.eval_with_input(Value::from_json_str(r#"
/// {
/// "user": "alice",
/// "resource": {"type": "code", "name": "main.rs"}
/// }"#)?)?;
/// assert_eq!(result, Value::from(true));
/// # Ok(())
/// # }
/// ```
///
/// # Entry Point Rule Format
///
/// The `rule` parameter should follow the Rego rule path format:
/// - `"data.package.rule"` - For rules in a specific package
/// - `"data.package.subpackage.rule"` - For nested packages
/// - `"allow"` - For rules in the default package (though this is not recommended)
///
/// # Notes
///
/// - Automatically enables print gathering for debugging purposes
/// - If you need target-aware compilation with automatic `__target__` handling,
/// consider using [`Engine::compile_for_target`] instead (requires `azure_policy` feature)
///
/// # See Also
///
/// - [`Engine::compile_for_target`] for target-aware compilation
/// - [`crate::compile_policy_with_entrypoint`] for a higher-level convenience function
pub fn compile_with_entrypoint(&mut self, rule: &Rc<str>) -> Result<CompiledPolicy> {
self.prepare_for_eval(false, false)?;
self.interpreter.clean_internal_evaluation_state();
self.interpreter
.compile(Some(rule.clone()))
.map(CompiledPolicy::new)
}
/// Evaluate specified rule(s).
///
/// [`Engine::eval_rule`] is often faster than [`Engine::eval_query`] and should be preferred if
@@ -438,7 +692,7 @@ impl Engine {
/// # }
/// ```
pub fn eval_rule(&mut self, rule: String) -> Result<Value> {
self.prepare_for_eval(false)?;
self.prepare_for_eval(false, false)?;
self.interpreter.clean_internal_evaluation_state();
self.interpreter.eval_rule_in_path(rule)
}
@@ -479,7 +733,7 @@ impl Engine {
/// # }
/// ```
pub fn eval_query(&mut self, query: String, enable_tracing: bool) -> Result<QueryResults> {
self.prepare_for_eval(enable_tracing)?;
self.prepare_for_eval(enable_tracing, false)?;
self.interpreter.clean_internal_evaluation_state();
self.interpreter.create_rule_prefixes()?;
@@ -622,7 +876,7 @@ impl Engine {
}
#[doc(hidden)]
fn prepare_for_eval(&mut self, enable_tracing: bool) -> Result<()> {
fn prepare_for_eval(&mut self, enable_tracing: bool, for_target: bool) -> Result<()> {
self.interpreter.set_traces(enable_tracing);
// if the data/policies have changed or the interpreter has never been prepared
@@ -644,6 +898,23 @@ impl Engine {
.set_functions(gather_functions(&self.modules)?);
self.interpreter.gather_rules()?;
self.interpreter.process_imports()?;
#[cfg(feature = "azure_policy")]
if for_target {
// Resolve and validate target specifications across all modules
crate::interpreter::target::resolve::resolve_and_apply_target(
&mut self.interpreter,
)?;
// Infer resource types
crate::interpreter::target::infer::infer_resource_type(&mut self.interpreter)?;
}
if !for_target {
// Check if any module specifies a target and warn if so
#[cfg(feature = "azure_policy")]
self.warn_if_targets_present();
}
self.prepared = true;
}
@@ -657,7 +928,7 @@ impl Engine {
rule: &Ref<Rule>,
enable_tracing: bool,
) -> Result<Value> {
self.prepare_for_eval(enable_tracing)?;
self.prepare_for_eval(enable_tracing, false)?;
self.interpreter.clean_internal_evaluation_state();
self.interpreter.eval_rule(module, rule)?;
@@ -667,7 +938,7 @@ impl Engine {
#[doc(hidden)]
pub fn eval_modules(&mut self, enable_tracing: bool) -> Result<Value> {
self.prepare_for_eval(enable_tracing)?;
self.prepare_for_eval(enable_tracing, false)?;
self.interpreter.clean_internal_evaluation_state();
// Ensure that empty modules are created.
@@ -1043,6 +1314,29 @@ impl Engine {
Ok(policy_parameter_definitions)
}
/// Emit a warning if any modules contain target specifications but we're not using target-aware compilation.
#[cfg(feature = "azure_policy")]
fn warn_if_targets_present(&self) {
let mut has_target = false;
let mut target_files = Vec::new();
for module in self.modules.iter() {
if module.target.is_some() {
has_target = true;
target_files.push(module.package.span.source.get_path());
}
}
if has_target {
std::eprintln!("Warning: Target specifications found in policy modules but not using target-aware compilation.");
std::eprintln!(" The following files contain __target__ declarations:");
for file in target_files {
std::eprintln!(" - {}", file);
}
std::eprintln!(" Consider using compile_for_target() instead of compile_with_entrypoint() for target-aware evaluation.");
}
}
fn make_parser<'a>(&self, source: &'a Source) -> Result<Parser<'a>> {
let mut parser = Parser::new(source)?;
if self.rego_v1 {
@@ -1050,4 +1344,18 @@ impl Engine {
}
Ok(parser)
}
/// Create a new Engine from a compiled policy.
#[doc(hidden)]
pub(crate) fn new_from_compiled_policy(
compiled_policy: Rc<crate::compiled_policy::CompiledPolicyData>,
) -> Self {
let modules = compiled_policy.modules.clone();
Self {
modules,
interpreter: Interpreter::new_from_compiled_policy(compiled_policy),
rego_v1: true, // Value doesn't matter since this is used only for policy parsing
prepared: true,
}
}
}

View File

@@ -3,6 +3,9 @@
use crate::ast::*;
use crate::builtins::{self, BuiltinFcn};
use crate::compiled_policy::CompiledPolicyData;
#[cfg(feature = "azure_policy")]
use crate::compiled_policy::TargetInfo;
use crate::lexer::*;
use crate::parser::Parser;
use crate::scheduler::*;
@@ -18,7 +21,14 @@ use core::ops::Bound::*;
type Scope = BTreeMap<SourceStr, Value>;
type DefaultRuleInfo = (Ref<Rule>, Option<String>);
#[cfg(feature = "azure_policy")]
pub mod error;
#[cfg(feature = "azure_policy")]
pub mod target {
pub mod infer;
pub mod resolve;
}
type ContextExprs = (Option<Ref<Expr>>, Option<Ref<Expr>>);
type State = (
Value,
@@ -36,22 +46,11 @@ enum FunctionModifier {
Value(Value),
}
#[derive(Debug, Clone, Default)]
pub struct CompiledPolicy {
modules: Rc<Vec<Ref<Module>>>,
schedule: Option<Schedule>,
rules: Map<String, Vec<Ref<Rule>>>,
default_rules: Map<String, Vec<DefaultRuleInfo>>,
imports: BTreeMap<String, Ref<Expr>>,
functions: FunctionTable,
rule_paths: Set<String>,
}
type RuleValues = BTreeMap<Vec<Value>, (Value, Ref<Expr>)>;
#[derive(Debug)]
pub struct Interpreter {
compiled_policy: Rc<CompiledPolicy>,
compiled_policy: Rc<CompiledPolicyData>,
data: Value,
@@ -61,7 +60,6 @@ pub struct Interpreter {
enable_coverage: bool,
traces: Option<Vec<Rc<str>>>,
strict_builtin_errors: bool,
gather_prints: bool,
prints: Vec<String>,
@@ -107,7 +105,6 @@ impl Clone for Interpreter {
gather_prints: self.gather_prints,
prints: self.prints.clone(),
strict_builtin_errors: self.strict_builtin_errors,
traces: self.traces.clone(),
extensions: self.extensions.clone(),
@@ -216,8 +213,12 @@ impl LoopExpr {
impl Interpreter {
pub fn new() -> Interpreter {
let compiled_policy = compiled_policy::CompiledPolicyData {
strict_builtin_errors: true, // Preserve current behavior
..Default::default()
};
Interpreter {
compiled_policy: Rc::new(CompiledPolicy::default()),
compiled_policy: Rc::new(compiled_policy),
data: Value::new_object(),
module: None,
@@ -239,7 +240,6 @@ impl Interpreter {
builtins_cache: BTreeMap::new(),
no_rules_lookup: false,
traces: None,
strict_builtin_errors: true,
extensions: Map::new(),
#[cfg(feature = "coverage")]
@@ -252,7 +252,21 @@ impl Interpreter {
}
}
fn compiled_policy_mut(&mut self) -> &mut CompiledPolicy {
/// Create a new Interpreter from a compiled policy.
pub fn new_from_compiled_policy(compiled_policy: Rc<CompiledPolicyData>) -> Self {
let mut interpreter = Self::new();
interpreter.extensions = compiled_policy.extensions.clone();
interpreter.compiled_policy = compiled_policy;
// Set initial data if available
if let Some(data) = &interpreter.compiled_policy.data {
interpreter.init_data = data.clone();
}
interpreter
}
fn compiled_policy_mut(&mut self) -> &mut CompiledPolicyData {
Rc::make_mut(&mut self.compiled_policy)
}
@@ -284,6 +298,12 @@ impl Interpreter {
&mut self.init_data
}
// Used by tests.
#[allow(dead_code)]
pub fn get_compiled_policy(&self) -> &Rc<CompiledPolicyData> {
&self.compiled_policy
}
pub fn set_traces(&mut self, enable_tracing: bool) {
self.traces = match enable_tracing {
true => Some(vec![]),
@@ -292,7 +312,7 @@ impl Interpreter {
}
pub fn set_strict_builtin_errors(&mut self, b: bool) {
self.strict_builtin_errors = b;
self.compiled_policy_mut().strict_builtin_errors = b;
}
pub fn set_input(&mut self, input: Value) {
@@ -649,7 +669,7 @@ impl Interpreter {
rhs,
lhs_value,
rhs_value,
self.strict_builtin_errors,
self.compiled_policy.strict_builtin_errors,
),
}
}
@@ -2233,10 +2253,15 @@ impl Interpreter {
}
}
let v = match builtin.0(span, params, &args[..], self.strict_builtin_errors) {
let v = match builtin.0(
span,
params,
&args[..],
self.compiled_policy.strict_builtin_errors,
) {
Ok(v) => v,
// Ignore errors if we are not evaluating in strict mode.
Err(_) if !self.strict_builtin_errors => return Ok(Value::Undefined),
Err(_) if !self.compiled_policy.strict_builtin_errors => return Ok(Value::Undefined),
Err(e) => Err(e)?,
};
@@ -2550,7 +2575,7 @@ impl Interpreter {
}
}
if self.strict_builtin_errors && !errors.is_empty() {
if self.compiled_policy.strict_builtin_errors && !errors.is_empty() {
return Err(anyhow!(errors[0].to_string()));
}
@@ -2947,7 +2972,7 @@ impl Interpreter {
uexpr,
Value::from(0),
self.eval_expr(uexpr)?,
self.strict_builtin_errors,
self.compiled_policy.strict_builtin_errors,
)
}
_ => bail!(expr
@@ -3658,9 +3683,7 @@ impl Interpreter {
for c in 0..comps.len() {
let path = self.current_module_path.clone() + "." + &comps[0..c + 1].join(".");
if c + 1 == comps.len() {
Rc::make_mut(&mut self.compiled_policy)
.rule_paths
.insert(path.clone());
self.compiled_policy_mut().rule_paths.insert(path.clone());
}
match self.compiled_policy_mut().rules.entry(path) {
@@ -3687,9 +3710,7 @@ impl Interpreter {
for (idx, c) in (0..comps.len()).enumerate() {
let path = self.current_module_path.clone() + "." + &comps[0..c + 1].join(".");
if c + 1 == comps.len() {
Rc::make_mut(&mut self.compiled_policy)
.rule_paths
.insert(path.clone());
self.compiled_policy_mut().rule_paths.insert(path.clone());
}
match self.compiled_policy_mut().default_rules.entry(path) {
@@ -3957,6 +3978,35 @@ impl Interpreter {
self.ensure_rule_evaluated(path.clone())?;
let parts: Vec<&str> = path.split('.').collect();
Ok(Self::get_value_chained(self.data.clone(), &parts[1..]))
let value = Self::get_value_chained(self.data.clone(), &parts[1..]);
#[cfg(feature = "azure_policy")]
{
if let Some(target_info) = &self.compiled_policy.target_info {
// Allow undefined values to pass through without schema validation
if value != Value::Undefined {
target_info.effect_schema.validate(&value)?;
}
}
}
Ok(value)
}
pub fn compile(&mut self, rule: Option<Rc<str>>) -> Result<Rc<CompiledPolicyData>> {
let data = Some(self.init_data.clone());
let extensions = self.extensions.clone();
let compiled_policy = self.compiled_policy_mut();
compiled_policy.data = data;
compiled_policy.extensions = extensions;
if let Some(rule) = rule {
if !compiled_policy.rule_paths.contains(rule.as_ref()) {
bail!("not a valid rule path");
}
compiled_policy.rule_to_evaluate = rule;
} else {
compiled_policy.rule_to_evaluate = "".into();
}
Ok(self.compiled_policy.clone())
}
}

58
src/interpreter/error.rs Normal file
View File

@@ -0,0 +1,58 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::Rc;
use thiserror::Error;
type String = Rc<str>;
/// Error type for interpreter target resolution operations.
#[derive(Debug, Clone, Error)]
pub enum TargetCompileError {
/// Multiple different targets specified across modules
#[error("Multiple different targets specified: '{existing}' and '{conflicting}'")]
ConflictingTargets {
existing: String,
conflicting: String,
},
/// Target not found in registry
#[error("Target '{0}' not found in registry")]
TargetNotFound(String),
/// No target specified when one is required
#[error("No target specified. When using compile_for_target, at least one module must specify a target using the __target__ annotation")]
NoTargetSpecified,
/// Modules with targets have different packages
#[error("Modules with target '{target}' have different packages: '{existing_package}' and '{conflicting_package}'")]
ConflictingPackages {
target: String,
existing_package: String,
conflicting_package: String,
},
/// No effects have rules defined for the target
#[error(
"Target '{target_name}' requires a rule with name {effect_names} in package '{package}'"
)]
NoEffectRules {
target_name: String,
package: String,
effect_names: String,
},
/// Multiple effect rules found for the same effect
#[error("Multiple effects have rules defined for target '{target_name}': {effect_names}. Only one effect should have rules defined in package '{path}'")]
MultipleEffectRules {
target_name: String,
effect_names: String,
path: String,
},
/// Missing default resource schema error
#[error("Missing default resource schema: {0}")]
MissingDefaultResourceSchema(String),
/// Incompatible default schema error
#[error("Incompatible default schema: {0}")]
IncompatibleDefaultSchema(String),
/// Invalid default schema type error
#[error("Invalid default schema type: {0}")]
InvalidDefaultSchemaType(String),
}

View File

@@ -0,0 +1,278 @@
use super::super::error::TargetCompileError;
use super::super::*;
use crate::ast::{BoolOp, Expr, Literal, Query, Rule};
use crate::compiled_policy::InferredResourceTypes;
use crate::value::Value;
use crate::{Rc, Schema};
type String = Rc<str>;
/// Analyzes policy rules to infer resource types from equality expressions.
///
/// This function examines the compiled policy rules corresponding to the effect path
/// and searches for equality statements that compare the resource selector field with
/// string literals. It identifies patterns like:
/// - `input.<resource_selector> == "resource_type_name"`
/// - `input["resource_selector"] == "resource_type_name"`
/// - `"resource_type_name" == input.<resource_selector>`
/// - `"resource_type_name" == input["resource_selector"]`
///
/// The `resource_selector` is determined by the target's resource schema selector
/// configuration (e.g., "type", "@odata.type").
///
/// # Schema Resolution
/// For each inferred resource type, the function attempts to find the corresponding
/// schema from the target's resource_schema_lookup table. If no specific schema is
/// found, it falls back to the default_resource_schema after validating compatibility.
///
/// # Returns
/// An InferredResourceTypes map mapping Query references to ResourceTypeInfo tuples
/// containing (resource_type_name, schema).
/// The results are also stored in the compiled policy's inferred_resource_types field
/// for later use during policy evaluation.
///
/// # Errors
/// Returns `TargetCompileError` if:
/// - Default resource schema is missing when needed
/// - Default schema is incompatible with the resource selector
/// - Default schema is not an object type
///
/// # Examples
/// For a policy with rules like:
/// ```rego
/// effect := "allow" { input.type == "Microsoft.Storage/storageAccounts" }
/// effect := "deny" { input["@odata.type"] == "microsoft.graph.user" }
/// ```
/// This function returns a map with entries for each query containing the resource type
/// conditions, mapping queries to their respective type names and schemas.
pub fn infer_resource_type(
interpreter: &mut Interpreter,
) -> Result<InferredResourceTypes, TargetCompileError> {
// Check if we have target info
if let Some(ref target_info) = interpreter.compiled_policy.target_info {
let target = &target_info.target;
let effect_path = &target_info.effect_path;
let resource_selector = &target.resource_schema_selector;
let mut result = InferredResourceTypes::new();
// Get rules for the effect path
if let Some(rules) = interpreter.compiled_policy.rules.get(effect_path.as_ref()) {
for rule in rules {
analyze_rule_for_resource_types(rule, resource_selector, target, &mut result)?;
}
}
// Note: We don't check default_rules because default rules cannot access input
// Store the result in the compiled policy for later use
let compiled_policy = Rc::make_mut(&mut interpreter.compiled_policy);
compiled_policy.inferred_resource_types = Some(result.clone());
Ok(result)
} else {
// No target info available
Ok(InferredResourceTypes::new())
}
}
fn analyze_rule_for_resource_types(
rule: &Rule,
resource_selector: &str,
target: &crate::target::Target,
result: &mut InferredResourceTypes,
) -> Result<(), TargetCompileError> {
if let Rule::Spec { bodies, .. } = rule {
for body in bodies {
analyze_query_for_resource_types(&body.query, resource_selector, target, result)?;
}
}
// Default rules typically don't contain resource type conditions
Ok(())
}
fn analyze_query_for_resource_types(
query: &Ref<Query>,
resource_selector: &str,
target: &crate::target::Target,
result: &mut InferredResourceTypes,
) -> Result<(), TargetCompileError> {
let mut found_resource_type: Option<String> = None;
for stmt in &query.stmts {
if let Literal::Expr { expr, .. } = &stmt.literal {
if let Some(resource_type) = analyze_expr_for_resource_types(expr, resource_selector) {
found_resource_type = Some(resource_type);
break; // Found resource type, no need to continue searching
}
}
// Note: We don't analyze NotExpr because it contains the opposite of type equality
// (e.g., not input.type == "value" means the type is NOT that value)
// Other literal statement (SomeVars, SomeIn, Every) don't typically contain
// direct resource type comparisons
}
// Now handle the insertion outside the loop
if let Some(resource_type) = found_resource_type {
// Look up the schema for this resource type
let resource_type_value = Value::String(resource_type.clone());
if let Some(schema) = target.resource_schema_lookup.get(&resource_type_value) {
result.insert(query.clone(), (resource_type, schema.clone()));
return Ok(());
}
// If not found in lookup, use default schema
let default_schema = get_validated_default_schema(target, resource_selector)?;
result.insert(query.clone(), (resource_type, default_schema));
} else {
// If no resource type was found for this query, use default schema
let default_schema = get_validated_default_schema(target, resource_selector)?;
result.insert(query.clone(), ("<default>".into(), default_schema));
}
Ok(())
}
fn analyze_expr_for_resource_types(expr: &Expr, resource_selector: &str) -> Option<String> {
// Only look for direct equality expressions: input.<resource_selector> == "string"
if let Expr::BoolExpr {
op: BoolOp::Eq,
lhs,
rhs,
..
} = expr
{
// Check if this is input.<resource_selector> == "string"
if let (Some(input_field), Some(string_value)) = (
extract_input_field_access(lhs, resource_selector),
extract_string_literal(rhs),
) {
if input_field.as_ref() == resource_selector {
return Some(string_value);
}
}
// Also check the reverse: "string" == input.<resource_selector>
else if let (Some(string_value), Some(input_field)) = (
extract_string_literal(lhs),
extract_input_field_access(rhs, resource_selector),
) {
if input_field.as_ref() == resource_selector {
return Some(string_value);
}
}
}
// We only look for direct equality expressions, no nested analysis
None
}
/// Extract input field access like `input.type` or `input["@odata.type"]`
fn extract_input_field_access(expr: &Expr, _expected_field: &str) -> Option<String> {
use crate::value::Value;
match expr {
// Handle input.field
Expr::RefDot { refr, field, .. } => {
if let (
Expr::Var {
value: Value::String(var_name),
..
},
Value::String(field_name),
) = (refr.as_ref(), &field.1)
{
if var_name.as_ref() == "input" {
return Some(field_name.clone());
}
}
}
// Handle input["field"] - the field is always a string literal
Expr::RefBrack { refr, index, .. } => {
if let (
Expr::Var {
value: Value::String(var_name),
..
},
Some(field_name),
) = (refr.as_ref(), extract_string_literal(index))
{
if var_name.as_ref() == "input" {
return Some(field_name);
}
}
}
_ => {}
}
None
}
/// Extract string literal from expression
fn extract_string_literal(expr: &Expr) -> Option<String> {
use crate::value::Value;
if let Expr::String {
value: Value::String(s),
..
} = expr
{
Some(s.clone())
} else {
None
}
}
/// Get and validate the default resource schema.
/// Returns the default schema if it exists and is compatible with the resource selector.
fn get_validated_default_schema(
target: &crate::target::Target,
resource_selector: &str,
) -> Result<Rc<Schema>, TargetCompileError> {
if let Some(default_schema) = &target.default_resource_schema {
// Validate that default schema can handle the resource selector field
validate_default_schema_compatibility(default_schema, resource_selector)?;
Ok(default_schema.clone())
} else {
Err(TargetCompileError::MissingDefaultResourceSchema(
format!("Target '{}' has no default resource schema", target.name).into(),
))
}
}
/// Validate that the default schema is compatible with the resource selector field.
/// The schema must either allow additional properties or have a property matching the resource selector.
fn validate_default_schema_compatibility(
schema: &Rc<Schema>,
resource_selector: &str,
) -> Result<(), TargetCompileError> {
use crate::schema::Type;
match schema.as_type() {
Type::Object {
properties,
additional_properties,
..
} => {
// Check if the schema has a property matching the resource selector
if properties.contains_key(resource_selector) {
return Ok(());
}
// Check if additional properties are allowed
if additional_properties.is_some() {
return Ok(());
}
// Neither condition is met
Err(TargetCompileError::IncompatibleDefaultSchema(
format!(
"Default resource schema must either have additional properties enabled or contain a '{}' property",
resource_selector
).into()
))
}
_ => {
// Default schema is not an object type
Err(TargetCompileError::InvalidDefaultSchemaType(
"Default resource schema must be an object type".into(),
))
}
}
}

View File

@@ -0,0 +1,248 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use super::super::error::TargetCompileError;
#[cfg(feature = "azure_policy")]
use super::super::TargetInfo;
use super::super::*;
fn format_effect_names(names: &[String]) -> String {
match names.len() {
0 => String::new(),
1 => names[0].clone(),
2 => format!("{} or {}", names[0], names[1]),
_ => {
if let Some((last, rest)) = names.split_last() {
format!("{} or {}", rest.join(", "), last)
} else {
String::new()
}
}
}
}
pub fn resolve_target(interpreter: &mut Interpreter) -> Result<(), TargetCompileError> {
use crate::registry::targets;
let mut target_name: Option<String> = None;
let mut target_package: Option<String> = None;
// Check all modules for target specifications
for module in interpreter.compiled_policy.modules.iter() {
if let Some(ref module_target) = module.target {
// Get the package path for this module
let module_package = Interpreter::get_path_string(&module.package.refr, None)
.map_err(|_| TargetCompileError::TargetNotFound(module_target.clone().into()))?;
match &target_name {
None => {
// First target found
target_name = Some(module_target.clone());
target_package = Some(module_package);
}
Some(existing_target) => {
// Ensure all modules specify the same target
if existing_target != module_target {
return Err(TargetCompileError::ConflictingTargets {
existing: existing_target.as_str().into(),
conflicting: module_target.as_str().into(),
});
}
// Ensure all modules with targets have the same package
if let Some(ref existing_package) = target_package {
if existing_package != &module_package {
return Err(TargetCompileError::ConflictingPackages {
target: module_target.as_str().into(),
existing_package: existing_package.as_str().into(),
conflicting_package: module_package.as_str().into(),
});
}
}
}
}
}
}
// If a target is specified, retrieve it from the registry
if let Some(target_name) = target_name {
match targets::get(&target_name) {
Some(target) => {
// Target found in registry - store it in the compiled policy
// We'll set a default effect schema here, but it will be updated in resolve_effect
// once we determine which effect actually has rules defined
let default_effect_schema = match target.effects.values().next() {
Some(schema) => schema.clone(),
None => {
return Err(TargetCompileError::TargetNotFound(
format!("Target '{}' has no effects defined", target_name)
.as_str()
.into(),
));
}
};
let target_info = TargetInfo {
target,
package: match target_package {
Some(pkg) => pkg.as_str().into(),
None => {
return Err(TargetCompileError::TargetNotFound(
format!("No package found for target '{}'", target_name)
.as_str()
.into(),
));
}
},
effect_schema: default_effect_schema,
effect_name: "".into(), // Will be updated in resolve_effect
effect_path: "".into(), // Will be updated in resolve_effect
};
interpreter.compiled_policy_mut().target_info = Some(target_info);
}
None => {
return Err(TargetCompileError::TargetNotFound(
target_name.as_str().into(),
));
}
}
} else {
// No target specified - this is an error when using compile_for_target
return Err(TargetCompileError::NoTargetSpecified);
}
Ok(())
}
pub fn resolve_effect(interpreter: &mut Interpreter) -> Result<(), TargetCompileError> {
// Check if we have target info from resolve_target
if let Some(ref target_info) = interpreter.compiled_policy.target_info {
let target = &target_info.target;
let package = &target_info.package;
let mut effects_with_rules = Vec::new();
// For each effect defined in the target, check if rules exist
for effect_name in target.effects.keys() {
// Rule keys are stored with "data." prefix in CompiledPolicy
let expected_path = format!("data.{}.{}", package, effect_name);
// Disallow sub-paths for effects in rules.
for rule_path in interpreter.compiled_policy.rules.keys() {
if rule_path.starts_with(&expected_path) && rule_path.len() > expected_path.len() {
// Sub-paths are not allowed for effects - they must be exact matches only
// This prevents effect rules from being defined at deeper nested paths
let all_effect_names: Vec<String> =
target.effects.keys().map(|k| k.to_string()).collect();
let formatted_names = format_effect_names(&all_effect_names);
return Err(TargetCompileError::NoEffectRules {
target_name: target.name.to_string().into(),
package: package.to_string().into(),
effect_names: formatted_names.as_str().into(),
});
}
}
// Disallow sub-paths for effects in default_rules.
for rule_path in interpreter.compiled_policy.default_rules.keys() {
if rule_path.starts_with(&expected_path) && rule_path.len() > expected_path.len() {
// Sub-paths are not allowed for effects - they must be exact matches only
let all_effect_names: Vec<String> =
target.effects.keys().map(|k| k.to_string()).collect();
let formatted_names = format_effect_names(&all_effect_names);
return Err(TargetCompileError::NoEffectRules {
target_name: target.name.to_string().into(),
package: package.to_string().into(),
effect_names: formatted_names.as_str().into(),
});
}
}
// Check if rules exist at the expected path or any sub-path
let mut has_rules = false;
// Check for exact match in rules
if let Some(rules) = interpreter.compiled_policy.rules.get(&expected_path) {
if !rules.is_empty() {
has_rules = true;
}
}
// Check for exact match in default_rules
if !has_rules {
if let Some(default_rules) = interpreter
.compiled_policy
.default_rules
.get(&expected_path)
{
if !default_rules.is_empty() {
has_rules = true;
}
}
}
if has_rules {
effects_with_rules.push(effect_name.clone());
}
}
// Ensure exactly one effect has rules defined
match effects_with_rules.len() {
0 => {
let all_effect_names: Vec<String> =
target.effects.keys().map(|k| k.to_string()).collect();
let formatted_names = format_effect_names(&all_effect_names);
return Err(TargetCompileError::NoEffectRules {
target_name: target.name.to_string().into(),
package: package.to_string().into(),
effect_names: formatted_names.as_str().into(),
});
}
1 => {
// Exactly one effect has rules - this is correct
// Update the target info with the correct effect schema
let effect_name = &effects_with_rules[0];
let effect_schema = match target.effects.get(effect_name) {
Some(schema) => schema.clone(),
None => {
// This should not happen since we got the effect_name from target.effects.keys()
return Err(TargetCompileError::TargetNotFound(
format!(
"Effect '{}' not found in target '{}'",
effect_name, target.name
)
.as_str()
.into(),
));
}
};
// Update the target info with the correct effect schema, name, and path
let expected_path = format!("data.{}.{}", package, effect_name);
if let Some(ref mut target_info) = interpreter.compiled_policy_mut().target_info {
target_info.effect_schema = effect_schema;
target_info.effect_name = effect_name.as_ref().into();
target_info.effect_path = expected_path.as_str().into();
}
}
_ => {
return Err(TargetCompileError::MultipleEffectRules {
target_name: target.name.to_string().into(),
effect_names: effects_with_rules.join(", ").as_str().into(),
path: package.to_string().into(),
});
}
}
}
Ok(())
}
pub fn resolve_and_apply_target(interpreter: &mut Interpreter) -> Result<(), TargetCompileError> {
// Resolve the target first
resolve_target(interpreter)?;
// Then resolve the effect
resolve_effect(interpreter)?;
Ok(())
}

View File

@@ -21,31 +21,44 @@ extern crate std;
mod ast;
mod builtins;
mod compile;
mod compiled_policy;
mod engine;
mod indexchecker;
mod interpreter;
mod lexer;
mod number;
mod parser;
mod policy_info;
#[cfg(feature = "azure_policy")]
mod registry;
pub mod registry;
mod scheduler;
#[cfg(feature = "azure_policy")]
mod schema;
#[cfg(feature = "azure_policy")]
pub mod target;
mod utils;
mod value;
#[cfg(feature = "azure_policy")]
pub use {
compile::compile_policy_for_target,
schema::{error::ValidationError, validate::SchemaValidator, Schema},
target::Target,
};
pub use compile::{compile_policy_with_entrypoint, PolicyModule};
pub use compiled_policy::CompiledPolicy;
pub use engine::Engine;
pub use lexer::Source;
#[cfg(feature = "azure_policy")]
pub use schema::{error::ValidationError, validate::SchemaValidator, Schema};
pub use policy_info::PolicyInfo;
pub use value::Value;
#[cfg(feature = "arc")]
use alloc::sync::Arc as Rc;
pub use alloc::sync::Arc as Rc;
#[cfg(not(feature = "arc"))]
use alloc::rc::Rc;
pub use alloc::rc::Rc;
#[cfg(feature = "std")]
use std::collections::{hash_map::Entry as MapEntry, HashMap as Map, HashSet as Set};

View File

@@ -1881,18 +1881,75 @@ impl<'source> Parser<'source> {
Ok(imports)
}
fn parse_string_literal(&mut self) -> Result<String> {
if self.tok.0 != TokenKind::String {
bail!(self.tok.1.error("expected string literal"));
}
let string_span = self.tok.1.clone();
let target_value =
match serde_json::from_str::<Value>(format!("\"{}\"", string_span.text()).as_str()) {
Ok(v) => v,
Err(e) => {
bail!(string_span.error(&format!("invalid string literal: {}", e)));
}
};
self.next_token()?;
match target_value.as_string() {
Ok(s) => Ok(s.as_ref().to_string()),
Err(_) => {
bail!(string_span.error("invalid string value"));
}
}
}
fn parse_target_rule(&mut self) -> Result<Option<String>> {
// Check if the current token starts a target rule: __target__
if self.tok.0 == TokenKind::Ident && self.token_text() == "__target__" {
// Parse __target__
self.next_token()?;
// Expect := operator
if self.token_text() != ":=" {
bail!(self.tok.1.error("expected ':=' after __target__"));
}
self.next_token()?;
// Parse the target name string using the helper function
let target_string = self.parse_string_literal()?;
Ok(Some(target_string))
} else {
Ok(None)
}
}
pub fn parse(&mut self) -> Result<Module> {
let package = self.parse_package()?;
let imports = self.parse_imports()?;
let target = self.parse_target_rule()?;
if target.is_some() {
self.rego_v1 = true;
}
let mut policy = vec![];
while self.tok.0 != TokenKind::Eof {
policy.push(Ref::new(self.parse_rule()?));
if self.token_text() == "__target__" {
bail!(self
.tok
.1
.error("__target__ must be defined before any rules"));
}
}
let m = Module {
package,
imports,
target,
policy,
rego_v1: self.rego_v1,
num_expressions: self.eidx,

42
src/policy_info.rs Normal file
View File

@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#[cfg(feature = "azure_policy")]
use crate::engine::PolicyParameters;
use crate::*;
type String = Rc<str>;
/// Information about a compiled policy, including metadata about modules,
/// target configuration, and resource types that the policy can evaluate.
#[derive(serde::Serialize)]
pub struct PolicyInfo {
/// List of module identifiers that were compiled into this policy.
/// Each module ID represents a unique policy module that contributes
/// rules, functions, or data to the compiled policy.
pub module_ids: Vec<String>,
/// Name of the target configuration used during compilation, if any.
/// This indicates which target schema and validation rules were applied.
pub target_name: Option<String>,
/// List of resource types that this policy can evaluate.
/// For target-aware policies, this contains the inferred or configured
/// resource types. For general policies, this may be empty.
pub applicable_resource_types: Vec<String>,
/// The primary rule or entrypoint that this policy evaluates.
/// This is the rule path that will be executed when the policy runs.
pub entrypoint_rule: String,
/// The effect rule name for target-aware policies, if applicable.
/// This is the specific effect rule (e.g., "effect", "allow", "deny")
/// that determines the policy decision for target evaluation.
pub effect_rule: Option<String>,
/// Parameters that can be configured for this policy.
/// Contains parameter names and their expected types or default values.
/// Used for parameterized policies that accept configuration at evaluation time.
/// Each element represents parameters from a different module.
#[cfg(feature = "azure_policy")]
pub parameters: Vec<PolicyParameters>,
}

View File

@@ -13,6 +13,7 @@ mod tests {
mod core;
mod effect;
mod resource;
mod target;
}
/// Errors that can occur when interacting with a Registry.
@@ -166,6 +167,9 @@ impl<T> Registry<T> {
/// Type alias for Schema registry
pub type SchemaRegistry = Registry<crate::Schema>;
/// Type alias for Target registry
pub type TargetRegistry = Registry<crate::target::Target>;
/// Global registry instances
pub mod instances {
use super::*;
@@ -179,6 +183,11 @@ pub mod instances {
/// Global singleton instance of effect schemas registry.
pub static ref EFFECT_SCHEMA_REGISTRY: Registry<crate::Schema> = Registry::new("EFFECT_SCHEMA_REGISTRY");
}
lazy_static::lazy_static! {
/// Global singleton instance of targets registry.
pub static ref TARGET_REGISTRY: Registry<crate::target::Target> = Registry::new("TARGET_REGISTRY");
}
}
/// Macro to generate helper functions for registry operations.
@@ -287,3 +296,50 @@ pub mod schemas {
"effect schemas"
);
}
/// Helper functions for target registry operations.
pub mod targets {
use super::*;
use instances::*;
/// Register a target using its name property.
pub fn register(item: Rc<crate::target::Target>) -> Result<(), RegistryError> {
let name = item.name.as_ref().to_string();
TARGET_REGISTRY.register(name, item)
}
/// Retrieve a target by name.
pub fn get(name: &str) -> Option<Rc<crate::target::Target>> {
TARGET_REGISTRY.get(name)
}
/// Remove a target by name.
pub fn remove(name: &str) -> Option<Rc<crate::target::Target>> {
TARGET_REGISTRY.remove(name)
}
/// List all registered target names.
pub fn list_names() -> Vec<String> {
TARGET_REGISTRY.list_names()
}
/// Check if a target with the given name exists.
pub fn contains(name: &str) -> bool {
TARGET_REGISTRY.contains(name)
}
/// Get the number of registered targets.
pub fn len() -> usize {
TARGET_REGISTRY.len()
}
/// Check if the target registry is empty.
pub fn is_empty() -> bool {
TARGET_REGISTRY.is_empty()
}
/// Clear all targets from the registry.
pub fn clear() {
TARGET_REGISTRY.clear();
}
}

1254
src/registry/tests/target.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -301,7 +301,7 @@ impl Schema {
}
/// Returns a reference to the underlying type definition.
fn as_type(&self) -> &Type {
pub fn as_type(&self) -> &Type {
&self.t
}
@@ -311,12 +311,13 @@ impl Schema {
schema: serde_json::Value,
) -> Result<Self, Box<dyn core::error::Error + Send + Sync>> {
let meta_schema_validation_result = meta::validate_schema_detailed(&schema);
let result = serde_json::from_value::<Schema>(schema)
let schema = serde_json::from_value::<Schema>(schema)
.map_err(|e| format!("Failed to parse schema: {e}"))?;
if let Err(errors) = meta_schema_validation_result {
return Err(format!("Schema validation failed: {}", errors.join("\n")).into());
}
Ok(result)
Ok(schema)
}
/// Parse a JSON Schema document from a string into a `Schema` instance.
@@ -326,6 +327,31 @@ impl Schema {
serde_json::from_str(s).map_err(|e| format!("Failed to parse schema: {e}"))?;
Self::from_serde_json_value(value)
}
/// Validates a `Value` against this schema.
///
/// Returns `Ok(())` if the value conforms to the schema, or a `ValidationError`
/// with detailed error information if validation fails.
///
/// # Example
/// ```rust
/// use regorus::schema::Schema;
/// use regorus::Value;
/// use serde_json::json;
///
/// let schema_json = json!({
/// "type": "string",
/// "minLength": 1,
/// "maxLength": 10
/// });
/// let schema = Schema::from_serde_json_value(schema_json).unwrap();
/// let value = Value::from("hello");
///
/// assert!(schema.validate(&value).is_ok());
/// ```
pub fn validate(&self, value: &Value) -> Result<(), error::ValidationError> {
validate::SchemaValidator::validate(value, self)
}
}
impl<'de> Deserialize<'de> for Schema {

79
src/target.rs Normal file
View File

@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(dead_code)]
use crate::{Rc, Schema, Value, Vec};
use alloc::collections::BTreeMap;
use serde::Deserialize;
mod deserialize;
mod error;
mod resource_schema_selector;
type String = Rc<str>;
use deserialize::{deserialize_effects, deserialize_resource_schemas};
pub use error::TargetError;
/// A target defines the domain for which a set of policies are written.
/// It specifies the types of input resources, possible policy effects,
/// and configuration for policy evaluation.
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Target {
/// Name of the target domain
/// A Rego module can specify a target by defining a rule named `__target__`:
/// __target__ = "my_target"
pub name: String,
/// Description of what this target is for
pub description: Option<String>,
/// Version of the target
pub version: String,
/// Types of input resources that policies can evaluate
#[serde(deserialize_with = "deserialize_resource_schemas")]
pub resource_schemas: Vec<Rc<Schema>>,
/// The discriminator property that can be used to select
/// a specific resource schema
pub resource_schema_selector: String,
/// Set of effects that policies can produce
#[serde(deserialize_with = "deserialize_effects")]
pub effects: BTreeMap<String, Rc<Schema>>,
/// Lookup table for resource schemas by discrimiator values.
#[serde(skip)]
pub resource_schema_lookup: BTreeMap<Value, Rc<Schema>>,
/// Resource chemas that cannot be distinguished by the discriminator
#[serde(skip)]
pub default_resource_schema: Option<Rc<Schema>>,
}
impl Target {
pub fn from_json_str(json: &str) -> Result<Self, TargetError> {
let mut target: Target = serde_json::from_str(json).map_err(TargetError::from)?;
// Validate that resource schemas is not empty
if target.resource_schemas.is_empty() {
return Err(TargetError::EmptyResourceSchemas(
"Target must have at least one resource schema defined".into(),
));
}
if target.effects.is_empty() {
return Err(TargetError::EmptyEffectSchemas(
"Target must have at least one effect defined".into(),
));
}
resource_schema_selector::populate_target_lookup_fields(&mut target)?;
Ok(target)
}
}
#[cfg(test)]
mod tests {
mod deserialize;
}

76
src/target/deserialize.rs Normal file
View File

@@ -0,0 +1,76 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::registry::instances::{EFFECT_SCHEMA_REGISTRY, RESOURCE_SCHEMA_REGISTRY};
use crate::{format, Rc, Schema, Vec};
use alloc::collections::BTreeMap;
use serde::de::{Deserializer, Error};
use serde::Deserialize;
type String = Rc<str>;
/// Deserialize resource schemas from either an array of schemas or schema names.
/// If specified as schema names, look them up from RESOURCE_SCHEMA_REGISTRY.
pub fn deserialize_resource_schemas<'de, D>(deserializer: D) -> Result<Vec<Rc<Schema>>, D::Error>
where
D: Deserializer<'de>,
{
let array: Vec<serde_json::Value> = Vec::deserialize(deserializer)
.map_err(|e| D::Error::custom(format!("Failed to deserialize resource_schemas: {}", e)))?;
let mut schemas = Vec::new();
for item in array.into_iter() {
let schema =
if let Some(name) = item.as_str() {
// Look up schema by name in the registry
RESOURCE_SCHEMA_REGISTRY.get(name).ok_or_else(|| {
D::Error::custom(format!("Resource schema '{}' not found in registry", name))
})?
} else {
// Treat as a direct schema definition
Rc::new(Schema::deserialize(item.clone()).map_err(|e| {
D::Error::custom(format!("Failed to deserialize schema: {}", e))
})?)
};
// Assert that the schema represents an object type
if !matches!(schema.as_type(), crate::schema::Type::Object { .. }) {
return Err(D::Error::custom("Resource schema must be an object type"));
}
schemas.push(schema);
}
Ok(schemas)
}
/// Deserialize effects from either an object of schemas or schema names.
/// If specified as schema names, look them up from EFFECT_SCHEMA_REGISTRY.
pub fn deserialize_effects<'de, D>(
deserializer: D,
) -> Result<BTreeMap<String, Rc<Schema>>, D::Error>
where
D: Deserializer<'de>,
{
let object: BTreeMap<String, serde_json::Value> = BTreeMap::deserialize(deserializer)
.map_err(|e| D::Error::custom(format!("Failed to deserialize effects: {}", e)))?;
let mut effects = BTreeMap::new();
for (key, item) in object.into_iter() {
if let Some(name) = item.as_str() {
// Look up schema by name in the registry
let schema = EFFECT_SCHEMA_REGISTRY.get(name).ok_or_else(|| {
D::Error::custom(format!("Effect schema '{}' not found in registry", name))
})?;
effects.insert(key, schema);
} else {
// Treat as a direct schema definition
let schema = Schema::deserialize(item.clone())
.map_err(|e| D::Error::custom(format!("Failed to deserialize schema: {}", e)))?;
effects.insert(key, Rc::new(schema));
}
}
Ok(effects)
}

35
src/target/error.rs Normal file
View File

@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::{format, Rc};
type String = Rc<str>;
/// Error type for target parsing operations.
#[derive(Debug, Clone, thiserror::Error)]
pub enum TargetError {
/// JSON parsing error
#[error("JSON parse error: {0}")]
JsonParseError(String),
/// Target deserialization error
#[error("Deserialization error: {0}")]
DeserializationError(String),
/// Duplicate constant value error
#[error("Duplicate constant value: {0}")]
DuplicateConstantValue(String),
/// Multiple default resource schemas error
#[error("Multiple default schemas: {0}")]
MultipleDefaultSchemas(String),
/// Empty resource schemas error
#[error("Empty resource schemas: {0}")]
EmptyResourceSchemas(String),
/// Empty effect schemas error
#[error("Empty effect schemas: {0}")]
EmptyEffectSchemas(String),
}
impl From<serde_json::Error> for TargetError {
fn from(error: serde_json::Error) -> Self {
TargetError::JsonParseError(format!("{}", error).into())
}
}

View File

@@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::schema::Type;
use crate::{format, Rc, Schema, Value};
use alloc::collections::BTreeMap;
type String = Rc<str>;
use super::{Target, TargetError};
/// Populates the resource_schema_lookup and default_resource_schema fields
/// in a Target based on its resource_schema_selector field and resource_schemas.
///
/// This function analyzes each resource schema to:
/// - Find constant properties that match the selector field name
/// - Build a lookup table mapping constant values to schemas
/// - Collect schemas that don't have the constant property
/// - Raise an error if duplicate constant values are found
pub fn populate_target_lookup_fields(target: &mut Target) -> Result<(), TargetError> {
target.resource_schema_lookup.clear();
target.default_resource_schema = None;
// Track which schema index corresponds to each constant value
let mut value_to_index = BTreeMap::new();
// Analyze each schema for constant properties
for (index, schema) in target.resource_schemas.iter().enumerate() {
if let Some(constant_value) =
find_constant_property(schema, &target.resource_schema_selector)
{
// Check if this constant value already exists
if let Some(existing_index) = value_to_index.get(&constant_value) {
return Err(TargetError::DuplicateConstantValue(format!(
"Duplicate constant value '{}' found for resource schema selector field '{}' in schemas at indexes {} and {}",
constant_value,
target.resource_schema_selector,
existing_index,
index
).into()));
}
// Record the mapping and add to lookup table
value_to_index.insert(constant_value.clone(), index);
target
.resource_schema_lookup
.insert(constant_value, schema.clone());
} else {
// Schema doesn't have the constant property
if target.default_resource_schema.is_some() {
return Err(TargetError::MultipleDefaultSchemas(format!(
"Multiple schemas found without discriminator property '{}'. Only one default resource schema is allowed.",
target.resource_schema_selector
).into()));
}
target.default_resource_schema = Some(schema.clone());
}
}
Ok(())
}
/// Finds a constant property value in a schema for the specified field name.
/// Returns the constant value if found, or None if the schema doesn't have
/// a constant property for the given field.
fn find_constant_property(schema: &Rc<Schema>, field_name: &str) -> Option<Value> {
match schema.as_type() {
Type::Object { properties, .. } => {
// Look for the field in the schema's properties
if let Some(property_schema) = properties.get(field_name) {
match property_schema.as_type() {
Type::Const { value, .. } => {
// Found a constant property - return its value
Some(value.clone())
}
_ => {
// Property exists but is not a constant
None
}
}
} else {
// Field doesn't exist in this schema
None
}
}
_ => {
// Schema is not an object type
None
}
}
}

View File

@@ -0,0 +1,358 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use super::super::*;
use crate::Value;
use alloc::string::ToString;
use serde_json::json;
#[test]
fn test_target_deserialization_with_direct_schemas() {
let target_json = json!({
"name": "test_target",
"description": "A test target for validation",
"version": "1.0.0",
"resource_schema_selector": "type",
"resource_schemas": [
{
"type": "object",
"properties": {
"name": { "type": "string" },
"type": { "const": "user" }
},
"required": ["name", "type"]
},
{
"type": "object",
"properties": {
"name": { "type": "string" },
"type": { "const": "group" }
},
"required": ["name", "type"]
}
],
"effects": {
"allow": { "type": "boolean" },
"deny": { "type": "boolean" }
}
});
let target = Target::from_json_str(&target_json.to_string()).unwrap();
assert_eq!(target.name.as_ref(), "test_target");
assert_eq!(
target.description.as_ref().unwrap().as_ref(),
"A test target for validation"
);
assert_eq!(target.version.as_ref(), "1.0.0");
assert_eq!(target.resource_schema_selector.as_ref(), "type");
assert_eq!(target.resource_schemas.len(), 2);
assert_eq!(target.effects.len(), 2);
// Check that lookup table was populated
assert_eq!(target.resource_schema_lookup.len(), 2);
assert!(target
.resource_schema_lookup
.contains_key(&Value::String("user".into())));
assert!(target
.resource_schema_lookup
.contains_key(&Value::String("group".into())));
// Check that default_resource_schema is None since all schemas have the discriminator
assert!(target.default_resource_schema.is_none());
}
#[test]
fn test_target_deserialization_with_mixed_schemas() {
let target_json = json!({
"name": "mixed_target",
"version": "1.0.0",
"resource_schema_selector": "resourceType",
"resource_schemas": [
{
"type": "object",
"properties": {
"id": { "type": "string" },
"resourceType": { "const": "storage" }
},
"required": ["id", "resourceType"]
},
{
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
},
"required": ["id"]
}
],
"effects": {
"permit": { "type": "string" }
}
});
let target = Target::from_json_str(&target_json.to_string()).unwrap();
assert_eq!(target.name.as_ref(), "mixed_target");
assert!(target.description.is_none());
assert_eq!(target.resource_schema_selector.as_ref(), "resourceType");
// One schema has discriminator, one doesn't
assert_eq!(target.resource_schema_lookup.len(), 1);
assert!(target
.resource_schema_lookup
.contains_key(&Value::String("storage".into())));
assert!(target.default_resource_schema.is_some());
}
#[test]
fn test_target_deserialization_multiple_default_schemas_error() {
let target_json = json!({
"name": "multiple_default_target",
"version": "1.0.0",
"resource_schema_selector": "kind",
"resource_schemas": [
{
"type": "object",
"properties": {
"id": { "type": "string" },
"data": { "type": "object" }
},
"required": ["id"]
},
{
"type": "object",
"properties": {
"name": { "type": "string" },
"value": { "type": "number" }
},
"required": ["name"]
}
],
"effects": {
"allow": { "type": "boolean" },
"deny": { "type": "boolean" }
}
});
// No schemas have the discriminator field - this should fail with MultipleDefaultSchemas error
let result = Target::from_json_str(&target_json.to_string());
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(error, TargetError::MultipleDefaultSchemas(_)));
}
#[test]
fn test_target_deserialization_single_default_schema() {
let target_json = json!({
"name": "single_default_target",
"version": "1.0.0",
"resource_schema_selector": "kind",
"resource_schemas": [
{
"type": "object",
"properties": {
"id": { "type": "string" },
"data": { "type": "object" }
},
"required": ["id"]
}
],
"effects": {
"allow": { "type": "boolean" }
}
});
let target = Target::from_json_str(&target_json.to_string()).unwrap();
// Single schema without discriminator should work fine
assert_eq!(target.resource_schema_lookup.len(), 0);
assert!(target.default_resource_schema.is_some());
}
#[test]
fn test_target_deserialization_duplicate_discriminator_error() {
let target_json = json!({
"name": "duplicate_target",
"version": "1.0.0",
"resource_schema_selector": "type",
"resource_schemas": [
{
"type": "object",
"properties": {
"id": { "type": "string" },
"type": { "const": "duplicate" }
},
"required": ["id", "type"]
},
{
"type": "object",
"properties": {
"name": { "type": "string" },
"type": { "const": "duplicate" }
},
"required": ["name", "type"]
}
],
"effects": {
"allow": { "type": "boolean" }
}
});
let result = Target::from_json_str(&target_json.to_string());
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(error, TargetError::DuplicateConstantValue(_)));
}
#[test]
fn test_target_deserialization_missing_required_field() {
let target_json = json!({
"name": "incomplete_target",
"version": "1.0.0",
// Missing resource_schema_selector
"resource_schemas": [
{
"type": "object",
"properties": {
"id": { "type": "string" }
}
}
],
"effects": {}
});
let result = Target::from_json_str(&target_json.to_string());
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(
error,
TargetError::JsonParseError(_) | TargetError::DeserializationError(_)
));
}
#[test]
fn test_target_deserialization_invalid_json() {
let invalid_json = "{ invalid json }";
let result = Target::from_json_str(invalid_json);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(error, TargetError::JsonParseError(_)));
}
#[test]
fn test_target_deserialization_with_registry_schemas() {
// This test assumes that there are some schemas in the registries
// If the registries are empty, this test will fail with appropriate errors
let target_json = json!({
"name": "registry_target",
"version": "1.0.0",
"resource_schema_selector": "type",
"resource_schemas": [
"some_registry_schema_name" // This will be looked up from RESOURCE_SCHEMA_REGISTRY
],
"effects": {
"allow": "some_effect_schema_name" // This will be looked up from EFFECT_SCHEMA_REGISTRY
}
});
// This test will likely fail if the registries are empty, but it demonstrates
// the structure for testing registry-based schema resolution
let result = Target::from_json_str(&target_json.to_string());
// We expect this to fail with a "not found in registry" error since we haven't
// populated the registries with test data
if result.is_err() {
let error = result.unwrap_err();
assert!(matches!(
error,
TargetError::JsonParseError(_) | TargetError::DeserializationError(_)
));
}
}
#[test]
fn test_target_deserialization_numeric_discriminator() {
let target_json = json!({
"name": "numeric_target",
"version": "1.0.0",
"resource_schema_selector": "level",
"resource_schemas": [
{
"type": "object",
"properties": {
"name": { "type": "string" },
"level": { "const": 1 }
},
"required": ["name", "level"]
},
{
"type": "object",
"properties": {
"name": { "type": "string" },
"level": { "const": 2 }
},
"required": ["name", "level"]
}
],
"effects": {
"grant": { "type": "string" }
}
});
let target = Target::from_json_str(&target_json.to_string()).unwrap();
// Check that numeric discriminator values work
assert_eq!(target.resource_schema_lookup.len(), 2);
assert!(target.resource_schema_lookup.contains_key(&Value::from(1)));
assert!(target.resource_schema_lookup.contains_key(&Value::from(2)));
assert!(target.default_resource_schema.is_none());
}
#[test]
fn test_target_deserialization_boolean_discriminator() {
let target_json = json!({
"name": "boolean_target",
"version": "1.0.0",
"resource_schema_selector": "enabled",
"resource_schemas": [
{
"type": "object",
"properties": {
"name": { "type": "string" },
"enabled": { "const": true }
},
"required": ["name", "enabled"]
},
{
"type": "object",
"properties": {
"name": { "type": "string" },
"enabled": { "const": false }
},
"required": ["name", "enabled"]
}
],
"effects": {
"activate": { "type": "boolean" }
}
});
let target = Target::from_json_str(&target_json.to_string()).unwrap();
// Check that boolean discriminator values work
assert_eq!(target.resource_schema_lookup.len(), 2);
assert!(target
.resource_schema_lookup
.contains_key(&Value::from(true)));
assert!(target
.resource_schema_lookup
.contains_key(&Value::from(false)));
assert!(target.default_resource_schema.is_none());
}

View File

@@ -9,6 +9,119 @@ use anyhow::{bail, Result};
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
use test_generator::test_resources;
#[cfg(feature = "azure_policy")]
mod load_target_definitions {
use super::*;
use std::{eprintln, sync::Once};
static INIT: Once = Once::new();
/// Load and register all target definitions from tests/interpreter/target/definitions
/// This function is called once and loads all JSON target definition files.
pub fn load() -> Result<()> {
INIT.call_once(|| {
if let Err(e) = load_target_definitions_impl() {
eprintln!("Failed to load target definitions: {}", e);
}
});
Ok(())
}
fn load_target_definitions_impl() -> Result<()> {
use crate::registry::targets;
use crate::target::Target;
use std::fs;
use std::path::Path;
let definitions_path = Path::new("tests/interpreter/cases/target/definitions");
if !definitions_path.exists() {
eprintln!("Target definitions directory does not exist");
return Ok(());
}
let entries = fs::read_dir(definitions_path)?;
let mut found = false;
for entry in entries {
let entry = entry?;
let path = entry.path();
// Only process JSON files
if path.extension().and_then(|s| s.to_str()) == Some("json") {
let contents = fs::read_to_string(&path)?;
match Target::from_json_str(&contents) {
Ok(target) => {
let target_name = target.name.clone();
let target_rc = Rc::new(target);
found = true;
if let Err(e) = targets::register(target_rc.clone()) {
eprintln!("Failed to register target '{}': {}", target_name, e);
}
}
Err(e) => {
eprintln!(
"Failed to parse target definition from {}: {}",
path.display(),
e
);
}
}
}
}
if !found {
eprintln!("No target definitions were found");
}
Ok(())
}
#[test]
fn test_load_target_definitions() -> Result<()> {
use crate::registry::targets;
// Load target definitions
let _ = load()?;
// Check that the sample targets were loaded
assert!(
targets::contains("target.tests.sample_test_target"),
"Sample target should be loaded"
);
assert!(
targets::contains("target.tests.azure_compute"),
"Azure compute target should be loaded"
);
// Verify we can retrieve the targets
let sample_target = targets::get("target.tests.sample_test_target");
assert!(
sample_target.is_some(),
"Should be able to retrieve sample target"
);
let azure_target = targets::get("target.tests.azure_compute");
assert!(
azure_target.is_some(),
"Should be able to retrieve azure target"
);
// Verify target properties
if let Some(target) = sample_target {
assert_eq!(target.name.as_ref(), "target.tests.sample_test_target");
assert_eq!(target.version.as_ref(), "1.0.0");
}
if let Some(target) = azure_target {
assert_eq!(target.name.as_ref(), "target.tests.azure_compute");
assert_eq!(target.version.as_ref(), "1.0.0");
}
Ok(())
}
}
// Process test value specified in json/yaml to interpret special encodings.
pub fn process_value(v: &Value) -> Result<Value> {
match v {
@@ -211,6 +324,64 @@ pub fn eval_file(
Ok((results, engine.take_prints()?))
}
#[cfg(feature = "azure_policy")]
pub fn eval_file_with_rule_evaluation(
regos: &[String],
data_opt: Option<Value>,
input_opt: Option<ValueOrVec>,
query: &str,
_enable_tracing: bool,
strict: bool,
) -> Result<(Vec<Value>, Vec<String>)> {
let mut engine: Engine = Engine::new();
engine.set_rego_v0(true);
engine.set_strict_builtin_errors(strict);
engine.set_gather_prints(true);
#[cfg(feature = "coverage")]
engine.set_enable_coverage(true);
let mut results = vec![];
let mut files = vec![];
for (idx, _) in regos.iter().enumerate() {
files.push(format!("rego_{idx}"));
}
for (idx, file) in files.iter().enumerate() {
let contents = regos[idx].as_str();
engine.add_policy(file.to_string(), contents.to_string())?;
}
if let Some(data) = data_opt {
engine.add_data(data)?;
}
// Also test using the newer CompilerPolicy API.
let compiled_policy = engine.clone().compile_for_target()?;
let mut inputs = vec![];
match input_opt {
Some(ValueOrVec::Single(single_input)) => inputs.push(single_input),
Some(ValueOrVec::Many(mut many_input)) => inputs.append(&mut many_input),
_ => {
// For target tests without input, use an empty object as default
inputs.push(Value::new_object());
}
}
for input in inputs {
engine.set_input(input.clone());
// Use eval_rule instead of eval_query for target tests
let r_engine = engine.eval_rule(query.to_string())?;
let r_compiled_policy = compiled_policy.eval_with_input(input)?;
assert_eq!(r_engine, r_compiled_policy);
results.push(r_engine);
}
Ok((results, engine.take_prints()?))
}
#[derive(PartialEq, Debug)]
pub enum ValueOrVec {
Single(Value),
@@ -280,6 +451,9 @@ fn yaml_test_impl(file: &str) -> Result<()> {
let yaml_str = std::fs::read_to_string(file)?;
let test: YamlTest = serde_yaml::from_str(&yaml_str)?;
#[cfg(feature = "azure_policy")]
load_target_definitions::load().expect("Failed to load target definitions");
#[cfg(not(feature = "std"))]
{
// Skip tests that depend on bultins that need std feature.
@@ -329,14 +503,36 @@ fn yaml_test_impl(file: &str) -> Result<()> {
let enable_tracing = case.traces.is_some() && case.traces.unwrap();
match eval_file(
&case.modules,
case.data,
case.input,
case.query.as_str(),
enable_tracing,
case.strict,
) {
let is_target_test = file.contains("target");
let result = if is_target_test {
#[cfg(feature = "azure_policy")]
{
eval_file_with_rule_evaluation(
&case.modules,
case.data,
case.input,
case.query.as_str(),
enable_tracing,
case.strict,
)
}
#[cfg(not(feature = "azure_policy"))]
{
panic!("Target tests require azure_policy feature")
}
} else {
eval_file(
&case.modules,
case.data,
case.input,
case.query.as_str(),
enable_tracing,
case.strict,
)
};
match result {
Ok((results, prints)) => match case.want_result {
Some(want_result) => {
let mut expected_results = vec![];
@@ -392,6 +588,12 @@ fn yaml_test(file: &str) -> Result<()> {
return Ok(());
}
// Targets are supported only with azure_policy feature.
#[cfg(not(feature = "azure_policy"))]
if file.contains("target") {
return Ok(());
}
match yaml_test_impl(file) {
Ok(_) => Ok(()),
Err(e) => {

View File

@@ -3,10 +3,9 @@
#![no_std]
#![no_main]
use core::panic::PanicInfo;
#[cfg(not(test))]
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}

View File

@@ -0,0 +1,80 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
cases:
- note: basic string formatting with spaces
data: {}
modules:
- |
package test
test1 := sprintf("User %s from %s has access", ["alice", "engineering"])
test2 := sprintf("A %s B %s C", ["X", "Y"])
test3 := sprintf("No spaces%s%s", ["A", "B"])
test4 := sprintf("Single %s", ["word"])
test5 := sprintf("Start %s end", ["middle"])
query: data.test
want_result:
test1: "User alice from engineering has access"
test2: "A X B Y C"
test3: "No spacesAB"
test4: "Single word"
test5: "Start middle end"
- note: numeric formatting
data: {}
modules:
- |
package test
decimal := sprintf("Number: %d", [42])
float := sprintf("Float: %f", [3.14])
hex := sprintf("Hex: %x", [255])
octal := sprintf("Octal: %o", [64])
binary := sprintf("Binary: %b", [15])
query: data.test
want_result:
decimal: "Number: 42"
float: "Float: 3.14"
hex: "Hex: ff"
octal: "Octal: 0O100"
binary: "Binary: 1111"
- note: mixed types and escaping
data: {}
modules:
- |
package test
mixed := sprintf("String: %s, Number: %d, Percent: %%", ["hello", 123])
verbose := sprintf("Value: %v", [{"key": "value"}])
query: data.test
want_result:
mixed: "String: hello, Number: 123, Percent: %"
verbose: "Value: {\"key\": \"value\"}"
- note: width formatting
data: {}
modules:
- |
package test
padded := sprintf("Padded: %5d", [42])
zero_padded := sprintf("Zero padded: %05d", [42])
decimal_places := sprintf("Decimal: %.2f", [3.14159])
query: data.test
want_result:
padded: "Padded: 42"
zero_padded: "Zero padded: 00042"
decimal_places: "Decimal: 3.14"
- note: error cases
data: {}
modules:
- |
package test
# This should cause an error - missing argument
error_case := sprintf("Value: %s %d", ["only_one"])
query: data.test
error: "no argument specified for format verb 1"

View File

@@ -0,0 +1,79 @@
cases:
- note: "Azure Policy Basic Allow Test"
data: {}
input:
type: "Microsoft.Storage/storageAccounts"
name: "mystorageaccount"
location: "East US"
kind: "StorageV2"
properties:
supportsHttpsTrafficOnly: true
minimumTlsVersion: "TLS1_2"
tags:
environment: "production"
modules:
- |
package azure.policy.allow
import rego.v1
__target__ := "target.tests.azure_policy"
default allow := false
allow if {
input.type == "Microsoft.Storage/storageAccounts"
input.properties.supportsHttpsTrafficOnly == true
}
query: data.azure.policy.allow.allow
want_result: true
- note: "Azure Policy Deny Test - HTTP Traffic"
data: {}
input:
type: "Microsoft.Storage/storageAccounts"
name: "insecurestorage"
location: "West US"
kind: "Storage"
properties:
supportsHttpsTrafficOnly: false
minimumTlsVersion: "TLS1_0"
modules:
- |
package azure.policy.deny
import rego.v1
__target__ := "target.tests.azure_policy"
deny := {
"message": "HTTPS traffic must be enabled"
} if {
input.type == "Microsoft.Storage/storageAccounts"
input.properties.supportsHttpsTrafficOnly == false
}
query: data.azure.policy.deny.deny
want_result:
message: "HTTPS traffic must be enabled"
- note: "Azure Policy Invalid Resource Type"
data: {}
input:
type: "Microsoft.UnknownService/unknownResource"
name: "test"
modules:
- |
package azure.policy.invalid
import rego.v1
__target__ := "target.tests.azure_policy"
default allow := false
allow if {
input.type == "Microsoft.Storage/storageAccounts"
input.properties.supportsHttpsTrafficOnly == true
}
query: data.azure.policy.invalid.allow
want_result: false

View File

@@ -0,0 +1,682 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
cases:
- note: "target/valid_target"
data: {}
input: {"name": "valid"}
modules:
- |
package test.allow
import rego.v1
__target__ := "target.tests.sample_test_target"
default allow := false
allow if {
input.name == "valid"
}
query: data.test.allow.allow
want_result: true
- note: "target/missing_assignment_operator"
data: {}
modules:
- |
package test.missing_assignment
import rego.v1
__target__ := "target.tests.sample_test_target"
allow if {
input.name == "valid"
}
query: data.test.missing_assignment.allow
want_result: "#undefined"
- note: "target/effect_validation_undefined_result"
data: {}
input: {"name": "invalid"}
modules:
- |
package test.undefined
import rego.v1
__target__ := "target.tests.sample_test_target"
# No default, rule doesn't match, so allow is undefined
allow if {
input.name == "valid"
}
query: data.test.undefined.allow
want_result: "#undefined"
- note: "target/target_resolution_timing"
data: {}
input: {"name": "valid"}
modules:
- |
package test
import rego.v1
__target__ "target.tests.sample_test_target"
allow if {
input.name == "valid"
}
query: data.test.allow
error: "expected ':=' after __target__"
- note: "target/missing_string_literal"
data: {}
modules:
- |
package test
import rego.v1
__target__ := 123
allow if {
input.name == "valid"
}
query: data.test.allow
error: "expected string literal"
- note: "target/invalid_string_format"
data: {}
modules:
- |
package test
import rego.v1
__target__ := "unterminated string
allow if {
input.name == "valid"
}
query: data.test.allow
error: "unterminated string"
- note: "target/nonexistent_target"
data: {}
input: {"name": "valid"}
modules:
- |
package test
import rego.v1
__target__ := "nonexistent.target.name"
allow if {
input.name == "valid"
}
query: data.test.allow
error: "Target 'nonexistent.target.name' not found in registry"
- note: "target/target_before_imports"
data: {}
modules:
- |
package test
__target__ := "target.tests.sample_test_target"
import rego.v1
allow if {
input.name == "valid"
}
query: data.test.allow
error: "unexpected keyword `import`"
- note: "target/target_after_rule"
data: {}
input: {"name": "valid"}
modules:
- |
package test
import rego.v1
allow if {
input.name == "valid"
}
__target__ := "target.tests.sample_test_target"
query: data.test.allow
error: "__target__ must be defined before any rules"
- note: "target/multiple_modules_same_target"
data: {}
input: {"name": "valid"}
modules:
- |
package test.module1
import rego.v1
__target__ := "target.tests.sample_test_target"
allow if {
input.name == "valid"
}
- |
package test.module2
import rego.v1
__target__ := "target.tests.sample_test_target"
deny if {
input.name == "invalid"
}
query: data.test.module1.allow
error: "Modules with target 'target.tests.sample_test_target' have different packages: 'test.module1' and 'test.module2'"
- note: "target/multiple_modules_same_target_same_package"
data: {}
input: {"name": "valid"}
modules:
- |
package test.shared
import rego.v1
__target__ := "target.tests.sample_test_target"
allow if {
input.name == "valid"
}
- |
package test.shared
import rego.v1
__target__ := "target.tests.sample_test_target"
allow if {
input.name == "invalid"
}
query: data.test.shared.allow
want_result: true
- note: "target/multiple_modules_different_targets"
data: {}
input: {"name": "valid"}
modules:
- |
package test.module1
import rego.v1
__target__ := "target.tests.sample_test_target"
allow if {
input.name == "valid"
}
- |
package test.module2
import rego.v1
__target__ := "target.tests.azure_compute"
deny if {
input.name == "invalid"
}
query: data.test.module1.allow
error: "Multiple different targets specified: 'target.tests.sample_test_target' and 'target.tests.azure_compute'"
- note: "target/multiple_different_effects_same_package"
data: {}
input: {"name": "valid"}
modules:
- |
package test.multieffect
import rego.v1
__target__ := "target.tests.sample_test_target"
allow if {
input.name == "valid"
}
deny if {
input.name == "invalid"
}
query: data.test.multieffect.allow
error: "Multiple effects have rules defined for target 'target.tests.sample_test_target': allow, deny. Only one effect should have rules defined in package 'test.multieffect'"
- note: "target/no_effect_rules_defined"
data: {}
input: {"name": "valid"}
modules:
- |
package test.noeffects
import rego.v1
__target__ := "target.tests.sample_test_target"
helper_function(x) if {
x == "valid"
}
query: data.test.noeffects.helper_function("valid")
error: "Target 'target.tests.sample_test_target' requires a rule with name allow, deny or test_effect in package 'test.noeffects'"
- note: "target/valid_deny_effect"
data: {}
input: {"name": "invalid"}
modules:
- |
package test.deny
import rego.v1
__target__ := "target.tests.sample_test_target"
default deny := false
deny if {
input.name == "invalid"
}
query: data.test.deny.deny
want_result: true
- note: "target/valid_test_effect"
data: {}
input: {"level": "warning", "message": "test message"}
modules:
- |
package test.custom
import rego.v1
__target__ := "target.tests.sample_test_target"
test_effect := {
"level": input.level,
"message": input.message
} if {
input.level
input.message
}
query: data.test.custom.test_effect
want_result: {"level": "warning", "message": "test message"}
- note: "target/multiple_rules_same_effect_same_module"
data: {}
input: {"name": "valid", "role": "admin"}
modules:
- |
package test.multirules
import rego.v1
__target__ := "target.tests.sample_test_target"
# Multiple rules for the same effect in the same module
allow if {
input.name == "valid"
}
allow if {
input.role == "admin"
}
query: data.test.multirules.allow
want_result: true
- note: "target/multiple_rules_same_effect_different_modules"
data: {}
input: {"name": "valid", "role": "admin"}
modules:
- |
package test.distributed
import rego.v1
__target__ := "target.tests.sample_test_target"
allow if {
input.name == "valid"
}
- |
package test.distributed
import rego.v1
__target__ := "target.tests.sample_test_target"
allow if {
input.role == "admin"
}
query: data.test.distributed.allow
want_result: true
- note: "target/effect_with_non_effect_rules_same_module"
data: {}
input: {"name": "valid"}
modules:
- |
package test.mixed
import rego.v1
__target__ := "target.tests.sample_test_target"
allow if {
input.name == "valid"
}
# Non-effect helper rule
is_admin if {
input.role == "admin"
}
# Another non-effect rule
helper_data := {"status": "active"}
query: data.test.mixed.allow
want_result: true
- note: "target/effect_with_non_effect_rules_different_modules"
data: {}
input: {"name": "valid", "role": "admin"}
modules:
- |
package test.separate
import rego.v1
__target__ := "target.tests.sample_test_target"
allow if {
input.name == "valid"
}
- |
package test.separate
import rego.v1
__target__ := "target.tests.sample_test_target"
# Non-effect helper rules in different module
is_admin if {
input.role == "admin"
}
user_data := {"type": "user", "active": true}
query: data.test.separate.allow
want_result: true
- note: "target/effect_subpath_should_fail"
data: {}
input: {"name": "valid"}
modules:
- |
package test.subpath
import rego.v1
__target__ := "target.tests.sample_test_target"
# This creates a rule at data.test.subpath.allow.nested.rule
# which is a subpath of the expected effect path data.test.subpath.allow
allow.nested.rule if {
input.name == "valid"
}
query: data.test.subpath.allow.nested.rule
error: "Target 'target.tests.sample_test_target' requires a rule with name allow, deny or test_effect in package 'test.subpath'"
- note: "target/multiple_subpath_rules_should_fail"
data: {}
input: {"name": "valid"}
modules:
- |
package test.multisubpath
import rego.v1
__target__ := "target.tests.sample_test_target"
# Multiple subpath rules under allow
allow.users.admin if {
input.name == "valid"
}
allow.users.guest if {
input.name == "guest"
}
query: data.test.multisubpath.allow.users.admin
error: "Target 'target.tests.sample_test_target' requires a rule with name allow, deny or test_effect in package 'test.multisubpath'"
- note: "target/effect_schema_validation_valid"
data: {}
input: {"name": "valid"}
modules:
- |
package test.schema_valid
import rego.v1
__target__ := "target.tests.sample_test_target"
allow if {
input.name == "valid"
}
query: data.test.schema_valid.allow
want_result: true
- note: "target/effect_schema_validation_invalid_type"
data: {}
input: {"name": "valid"}
modules:
- |
package test.schema_invalid
import rego.v1
__target__ := "target.tests.sample_test_target"
# test_effect schema expects object with string properties, but we return a string
test_effect := "invalid_string_instead_of_object" if {
input.name == "valid"
}
query: data.test.schema_invalid.test_effect
error: "Type mismatch"
- note: "target/effect_schema_validation_missing_required_field"
data: {}
input: {"name": "valid"}
modules:
- |
package test.schema_missing_field
import rego.v1
__target__ := "target.tests.sample_test_target"
# test_effect expects an object, but with invalid property types
test_effect := {
"level": 123, # should be string, not number
"message": "test message"
} if {
input.name == "valid"
}
query: data.test.schema_missing_field.test_effect
error: "Type mismatch"
- note: "target/effect_schema_validation_complex_object"
data: {}
input: {"name": "valid", "details": {"severity": "high", "category": "security"}}
modules:
- |
package test.schema_complex
import rego.v1
__target__ := "target.tests.sample_test_target"
test_effect := {
"level": "error",
"message": "Security violation detected",
"details": input.details
} if {
input.name == "valid"
input.details.severity == "high"
}
query: data.test.schema_complex.test_effect
want_result: {"level": "error", "message": "Security violation detected", "details": {"severity": "high", "category": "security"}}
- note: "target/effect_schema_validation_enum_constraint"
data: {}
input: {"name": "valid"}
modules:
- |
package test.schema_enum
import rego.v1
__target__ := "target.tests.sample_test_target"
# test_effect requires an object, but we return an array instead
test_effect := ["invalid", "array", "instead", "of", "object"] if {
input.name == "valid"
}
query: data.test.schema_enum.test_effect
error: "Type mismatch"
- note: "target/multiple_targets_different_schemas"
data: {}
input: {"name": "valid"}
modules:
- |
package test.multi_target_a
import rego.v1
__target__ := "target.tests.sample_test_target"
allow if {
input.name == "valid"
}
- |
package test.multi_target_b
import rego.v1
__target__ := "target.tests.azure_compute"
deny if {
input.name == "invalid"
}
query: data.test.multi_target_a.allow
error: "Multiple different targets specified"
- note: "target/effect_validation_with_default_value"
data: {}
input: {"name": "valid"}
modules:
- |
package test.with_default
import rego.v1
__target__ := "target.tests.sample_test_target"
default allow := false
allow if {
input.name == "valid"
}
query: data.test.with_default.allow
want_result: true
- note: "target/effect_validation_undefined_result"
data: {}
input: {"name": "invalid"}
modules:
- |
package test.undefined
import rego.v1
__target__ := "target.tests.sample_test_target"
# No default, rule doesn't match, so allow is undefined
allow if {
input.name == "valid"
}
query: data.test.undefined.allow
want_result: "#undefined"
- note: "target/target_resolution_timing"
data: {}
input: {"name": "valid"}
modules:
- |
package test.timing
import rego.v1
__target__ := "target.tests.sample_test_target"
# Rule defined after target - should work due to proper timing
allow if {
input.name == "valid"
}
# Helper rule that's not an effect
helper := "test" if true
query: data.test.timing.allow
want_result: true
- note: "target/nested_package_structure"
data: {}
input: {"name": "valid"}
modules:
- |
package test.nested.deep.structures
import rego.v1
__target__ := "target.tests.sample_test_target"
allow if {
input.name == "valid"
}
query: data.test.nested.deep.structures.allow
want_result: true
- note: "target/package_validation_error_consistency"
data: {}
input: {"name": "valid"}
modules:
- |
package test.package_a
import rego.v1
__target__ := "target.tests.sample_test_target"
allow if {
input.name == "valid"
}
- |
package test.package_b
import rego.v1
__target__ := "target.tests.sample_test_target"
deny if {
input.name == "invalid"
}
query: data.test.package_a.allow
error: "Modules with target 'target.tests.sample_test_target' have different packages: 'test.package_a' and 'test.package_b'"

View File

@@ -0,0 +1,561 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
cases:
- note: "target/complex_nested_object_validation"
data: {}
input: {"user": {"name": "alice", "roles": ["admin", "user"], "metadata": {"department": "engineering", "level": 5}}}
modules:
- |
package policy.complex
import rego.v1
__target__ := "target.tests.sample_test_target"
test_effect := {
"level": "info",
"message": sprintf("User %s from %s has access", [input.user.name, input.user.metadata.department]),
"details": {
"user_roles": input.user.roles,
"access_level": input.user.metadata.level,
"timestamp": "2025-08-14T10:00:00Z"
}
} if {
"admin" in input.user.roles
input.user.metadata.level >= 3
}
query: data.policy.complex.test_effect
want_result: {
"details": {
"access_level": 5,
"timestamp": "2025-08-14T10:00:00Z",
"user_roles": ["admin", "user"]
},
"level": "info",
"message": "User alice from engineering has access"
}
- note: "target/complex_conditional_logic_with_multiple_rules"
data: {}
input: {"resource": {"type": "document", "classification": "confidential", "owner": "alice"}, "requester": {"id": "bob", "clearance": "secret"}}
modules:
- |
package policy.access_control
import rego.v1
__target__ := "target.tests.sample_test_target"
default allow := false
# Multiple complex rules for the same effect
allow if {
input.resource.classification == "public"
}
allow if {
input.resource.owner == input.requester.id
}
allow if {
input.resource.classification == "confidential"
input.requester.clearance in ["secret", "top_secret"]
count([role | role := input.requester.roles[_]; role == "analyst"]) > 0
}
allow if {
input.resource.type == "document"
input.requester.clearance == "top_secret"
}
query: data.policy.access_control.allow
want_result: false
- note: "target/complex_data_manipulation_with_comprehensions"
data: {
"users": [
{"id": "u1", "name": "alice", "department": "eng", "salary": 100000},
{"id": "u2", "name": "bob", "department": "sales", "salary": 80000},
{"id": "u3", "name": "charlie", "department": "eng", "salary": 120000}
]
}
input: {"department_filter": "eng", "min_salary": 90000}
modules:
- |
package policy.hr_analysis
import rego.v1
__target__ := "target.tests.sample_test_target"
test_effect := {
"level": "info",
"message": sprintf("Found %d qualified users", [count(qualified_users)]),
"details": {
"qualified_users": qualified_users,
"avg_salary": avg_salary,
"total_budget": total_budget
}
} if {
count(qualified_users) > 0
}
qualified_users := [user |
user := data.users[_]
user.department == input.department_filter
user.salary >= input.min_salary
]
# Extract total_budget calculation to avoid scheduling error
total_budget := v if {
count(qualified_users) > 0
v := sum([salary |
true
user := qualified_users[_]
salary := user.salary
])
}
# Extract avg_salary calculation to avoid scheduling error
avg_salary := v if {
count(qualified_users) > 0
v := sum([salary |
true
user := qualified_users[_]
salary := user.salary
]) / count(qualified_users)
}
query: data.policy.hr_analysis.test_effect
want_result: {
"level": "info",
"message": "Found 2 qualified users",
"details": {
"qualified_users": [
{"id": "u1", "name": "alice", "department": "eng", "salary": 100000},
{"id": "u3", "name": "charlie", "department": "eng", "salary": 120000}
],
"avg_salary": 110000,
"total_budget": 220000
}
}
- note: "target/corner_case_empty_collections"
data: {"empty_array": [], "empty_object": {}}
input: {"filters": []}
modules:
- |
package policy.empty_collections
import rego.v1
__target__ := "target.tests.sample_test_target"
allow if {
count(data.empty_array) == 0
count(object.keys(data.empty_object)) == 0
count(input.filters) == 0
}
query: data.policy.empty_collections.allow
want_result: true
- note: "target/corner_case_null_and_undefined_handling"
data: {"nullable_field": null}
input: {"optional_field": null}
modules:
- |
package policy.null_handling
import rego.v1
__target__ := "target.tests.sample_test_target"
default test_effect := {}
test_effect := {
"level": "warning",
"message": "Handling null values correctly"
} if {
data.nullable_field == null
input.optional_field == null
# Check that missing_field is undefined by ensuring it's not present
not "missing_field" in object.keys(data)
}
query: data.policy.null_handling.test_effect
want_result: {
"level": "warning",
"message": "Handling null values correctly"
}
- note: "target/corner_case_deeply_nested_structures"
data: {}
input: {
"request": {
"metadata": {
"auth": {
"user": {
"profile": {
"permissions": {
"read": ["doc1", "doc2"],
"write": ["doc1"]
}
}
}
}
}
}
}
modules:
- |
package policy.deep_nesting
import rego.v1
__target__ := "target.tests.sample_test_target"
allow if {
permissions := input.request.metadata.auth.user.profile.permissions
"doc1" in permissions.read
"doc1" in permissions.write
}
query: data.policy.deep_nesting.allow
want_result: true
- note: "target/corner_case_unicode_and_special_characters"
data: {}
input: {
"user": "測試用戶",
"message": "Hello, 世界! 🌍",
"special_chars": "!@#$%^&*()_+-=[]{}|;':\",./<>?"
}
modules:
- |
package policy.unicode_handling
import rego.v1
__target__ := "target.tests.sample_test_target"
test_effect := {
"level": "info",
"message": sprintf("Processing for user: %s", [input.user])
} if {
contains(input.message, "世界")
startswith(input.special_chars, "!")
}
query: data.policy.unicode_handling.test_effect
want_result: {
"level": "info",
"message": "Processing for user: 測試用戶"
}
- note: "target/corner_case_large_numbers_and_precision"
data: {}
input: {
"large_int": 9223372036854775807,
"small_float": 0.000000000001,
"large_float": 1.7976931348623157e+308
}
modules:
- |
package policy.numeric_precision
import rego.v1
__target__ := "target.tests.sample_test_target"
allow if {
input.large_int > 9000000000000000000
input.small_float < 0.001
input.large_float > 1e100
}
query: data.policy.numeric_precision.allow
want_result: true
- note: "target/corner_case_circular_references_in_data"
data: {
"users": {
"alice": {"id": "alice", "manager": "bob"},
"bob": {"id": "bob", "manager": "charlie"},
"charlie": {"id": "charlie", "manager": "alice"}
}
}
input: {"check_user": "alice"}
modules:
- |
package policy.circular_refs
import rego.v1
__target__ := "target.tests.sample_test_target"
# Detect circular management chain
deny if {
has_circular_management(input.check_user, set())
}
has_circular_management(user_id, visited) if {
user_id in visited
}
has_circular_management(user_id, visited) if {
not user_id in visited
manager := data.users[user_id].manager
manager != null
has_circular_management(manager, visited | {user_id})
}
query: data.policy.circular_refs.deny
want_result: true
- note: "target/complex_schema_validation_failure_detailed"
data: {}
input: {"name": "test"}
modules:
- |
package policy.schema_failure
import rego.v1
__target__ := "target.tests.sample_test_target"
# This should fail schema validation - wrong structure entirely
test_effect := {
"invalid_field": "should_not_exist",
"level": 123, # should be string
"message": ["array", "instead", "of", "string"], # should be string
"extra_nested": {
"deep": {
"structure": "not_allowed"
}
}
} if {
input.name == "test"
}
query: data.policy.schema_failure.test_effect
error: "Type mismatch"
- note: "target/complex_multiple_modules_with_helper_functions"
data: {}
input: {"operation": "delete", "resource_id": "sensitive_doc", "user_role": "admin"}
modules:
- |
package policy.authorization
import rego.v1
__target__ := "target.tests.sample_test_target"
default allow := false
# Reference helper functions from the same package
allow if {
is_admin(input.user_role)
is_allowed_operation(input.operation)
not is_sensitive_resource(input.resource_id)
}
- |
package policy.authorization
import rego.v1
__target__ := "target.tests.sample_test_target"
is_admin(role) if {
role == "admin"
}
is_allowed_operation(op) if {
op in ["read", "write", "delete"]
}
is_sensitive_resource(resource_id) if {
startswith(resource_id, "sensitive_")
}
query: data.policy.authorization.allow
want_result: false
- note: "target/corner_case_very_long_strings"
data: {}
input: {
"long_string": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo."
}
modules:
- |
package policy.long_strings
import rego.v1
__target__ := "target.tests.sample_test_target"
test_effect := {
"level": "info",
"message": sprintf("String length: %d characters", [count(input.long_string)])
} if {
count(input.long_string) > 500
contains(input.long_string, "Lorem ipsum")
}
query: data.policy.long_strings.test_effect
want_result: {
"level": "info",
"message": "String length: 661 characters"
}
- note: "target/complex_regex_pattern_matching"
data: {}
input: {
"emails": [
"valid@example.com",
"also.valid+tag@domain.co.uk",
"invalid.email",
"another@valid-domain.org"
]
}
modules:
- |
package policy.email_validation
import rego.v1
__target__ := "target.tests.sample_test_target"
test_effect := {
"level": "info",
"message": sprintf("Validated %d emails, %d valid", [count(input.emails), count(valid_emails)])
} if {
count(valid_emails) > 0
}
valid_emails := [email |
email := input.emails[_]
regex.match(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`, email)
]
query: data.policy.email_validation.test_effect
want_result: {
"level": "info",
"message": "Validated 4 emails, 3 valid"
}
- note: "target/corner_case_recursive_data_structures"
data: {
"filesystem": {
"root": {
"type": "directory",
"children": {
"home": {
"type": "directory",
"children": {
"user": {
"type": "directory",
"children": {
"document.txt": {"type": "file", "size": 1024}
}
}
}
},
"etc": {
"type": "directory",
"children": {
"config.ini": {"type": "file", "size": 512}
}
}
}
}
}
}
input: {"search_type": "file"}
modules:
- |
package policy.filesystem
import rego.v1
__target__ := "target.tests.sample_test_target"
test_effect := {
"level": "info",
"message": sprintf("Found %d files", [count(all_files)])
} if {
count(all_files) > 0
}
all_files[path] := file if {
walk(data.filesystem, [path, file])
file.type == input.search_type
}
query: data.policy.filesystem.test_effect
want_result: {
"level": "info",
"message": "Found 2 files"
}
- note: "target/complex_error_propagation_and_recovery"
data: {}
input: {"values": [1, 2, 0, 4, 5]}
modules:
- |
package policy.error_handling
import rego.v1
__target__ := "target.tests.sample_test_target"
test_effect := {
"level": "warning",
"message": sprintf("Division results: %v", [safe_divisions])
} if {
count(safe_divisions) > 0
}
safe_divisions := [result |
value := input.values[i]
value != 0 # Skip zero values to avoid division by zero
result := 100 / value
]
query: data.policy.error_handling.test_effect
want_result: {
"level": "warning",
"message": "Division results: [100, 50, 25, 20]"
}
- note: "target/corner_case_edge_conditions_with_sets"
data: {}
input: {
"set1": ["a", "b", "c"],
"set2": ["b", "c", "d"],
"set3": ["c", "d", "e"]
}
modules:
- |
package policy.set_operations
import rego.v1
__target__ := "target.tests.sample_test_target"
test_effect := {
"level": "info",
"message": "Set operations completed",
"details": {
"intersection_all": intersection_all,
"union_all": union_all,
"symmetric_diff": symmetric_diff
}
} if {
count(intersection_all) > 0
}
s1 := {x | x := input.set1[_]}
s2 := {x | x := input.set2[_]}
s3 := {x | x := input.set3[_]}
intersection_all := s1 & s2 & s3
union_all := s1 | s2 | s3
symmetric_diff := (s1 | s2) - (s1 & s2)
query: data.policy.set_operations.test_effect
want_result: {
"level": "info",
"message": "Set operations completed",
"details": {
"intersection_all": { "set!": ["c"] },
"union_all": { "set!": ["a", "b", "c", "d", "e"] },
"symmetric_diff": { "set!": ["a", "d"] }
}
}

View File

@@ -0,0 +1,47 @@
{
"name": "target.tests.azure_compute",
"description": "Azure compute resources target for testing",
"version": "1.0.0",
"resource_schema_selector": "type",
"resource_schemas": [
{
"type": "object",
"properties": {
"type": { "const": "Microsoft.Compute/virtualMachines" },
"name": { "type": "string" },
"location": { "type": "string" },
"properties": {
"type": "object",
"properties": {
"vmSize": { "type": "string" },
"storageProfile": {
"type": "object",
"properties": {
"imageReference": {
"type": "object",
"properties": {
"publisher": { "type": "string" },
"offer": { "type": "string" },
"sku": { "type": "string" }
}
}
}
}
}
}
},
"required": ["type", "name", "location"]
}
],
"effects": {
"allow": { "type": "boolean" },
"deny": { "type": "boolean" },
"audit": {
"type": "object",
"properties": {
"level": { "enum": ["info", "warning", "error"] },
"message": { "type": "string" }
}
}
}
}

View File

@@ -0,0 +1,125 @@
{
"name": "target.tests.azure_policy",
"description": "Azure Policy target for comprehensive policy evaluation testing",
"version": "1.0.0",
"resource_schema_selector": "type",
"resource_schemas": [
{
"type": "object",
"properties": {
"type": { "const": "Microsoft.Resources/subscriptions" },
"subscriptionId": { "type": "string" },
"tenantId": { "type": "string" },
"displayName": { "type": "string" }
},
"required": ["type", "subscriptionId"]
},
{
"type": "object",
"properties": {
"type": { "const": "Microsoft.Storage/storageAccounts" },
"name": { "type": "string" },
"location": { "type": "string" },
"kind": { "enum": ["Storage", "StorageV2", "BlobStorage", "FileStorage", "BlockBlobStorage"] },
"properties": {
"type": "object",
"properties": {
"supportsHttpsTrafficOnly": { "type": "boolean" },
"minimumTlsVersion": { "enum": ["TLS1_0", "TLS1_1", "TLS1_2"] },
"allowBlobPublicAccess": { "type": "boolean" },
"encryption": {
"type": "object",
"properties": {
"services": {
"type": "object",
"properties": {
"blob": { "type": "object", "properties": { "enabled": { "type": "boolean" } } },
"file": { "type": "object", "properties": { "enabled": { "type": "boolean" } } }
}
}
}
}
}
},
"tags": { "type": "object" }
},
"required": ["type", "name", "location"]
},
{
"type": "object",
"properties": {
"type": { "const": "Microsoft.Network/networkSecurityGroups" },
"name": { "type": "string" },
"location": { "type": "string" },
"properties": {
"type": "object",
"properties": {
"securityRules": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"properties": {
"type": "object",
"properties": {
"direction": { "enum": ["Inbound", "Outbound"] },
"access": { "enum": ["Allow", "Deny"] },
"protocol": { "enum": ["Tcp", "Udp", "*"] },
"sourcePortRange": { "type": "string" },
"destinationPortRange": { "type": "string" },
"sourceAddressPrefix": { "type": "string" },
"destinationAddressPrefix": { "type": "string" },
"priority": { "type": "integer", "minimum": 100, "maximum": 4096 }
}
}
}
}
}
}
}
},
"required": ["type", "name", "location"]
}
],
"effects": {
"allow": { "type": "boolean" },
"deny": {
"type": "object",
"properties": {
"message": { "type": "string" }
}
},
"audit": {
"type": "object",
"properties": {
"level": { "enum": ["info", "warning", "error"] },
"message": { "type": "string" },
"complianceState": { "enum": ["Compliant", "NonCompliant", "Unknown"] }
}
},
"modify": {
"type": "object",
"properties": {
"operations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"operation": { "enum": ["add", "replace", "remove"] },
"field": { "type": "string" },
"value": { "type": "any" }
}
}
}
}
},
"deployIfNotExists": {
"type": "object",
"properties": {
"template": { "type": "object" },
"parameters": { "type": "object" }
}
}
}
}

View File

@@ -0,0 +1,195 @@
{
"name": "target.tests.complex_target",
"description": "A complex target for testing advanced features including discriminated unions",
"version": "2.0.0",
"resource_schema_selector": "resourceType",
"resource_schemas": [
{
"type": "object",
"properties": {
"name": { "type": "string" },
"resourceType": { "const": "compute" },
"spec": {
"type": "object",
"properties": {
"cpu": { "type": "integer", "minimum": 1, "maximum": 64 },
"memory": { "type": "string", "pattern": "^[0-9]+[GM]i$" }
},
"required": ["cpu", "memory"]
}
},
"required": ["name", "resourceType", "spec"]
},
{
"type": "object",
"properties": {
"name": { "type": "string" },
"resourceType": { "const": "storage" },
"spec": {
"type": "object",
"properties": {
"size": { "type": "string", "pattern": "^[0-9]+[GTM]i$" },
"type": { "enum": ["ssd", "hdd", "nvme"] }
},
"required": ["size", "type"]
}
},
"required": ["name", "resourceType", "spec"]
},
{
"type": "object",
"properties": {
"name": { "type": "string" },
"resourceType": { "type": "string" },
"spec": { "type": "object" }
},
"required": ["name"],
"additionalProperties": true
}
],
"effects": {
"allow": { "type": "boolean" },
"deny": { "type": "boolean" },
"audit": {
"type": "object",
"properties": {
"action": { "type": "string" },
"severity": { "enum": ["low", "medium", "high", "critical"] },
"details": {
"type": "object",
"properties": {
"eventType": { "type": "string" }
},
"required": ["eventType"],
"allOf": [
{
"if": {
"properties": {
"eventType": { "const": "access" }
}
},
"then": {
"properties": {
"user": { "type": "string" },
"resource": { "type": "string" },
"timestamp": { "type": "string" }
},
"required": ["user", "resource", "timestamp"]
}
},
{
"if": {
"properties": {
"eventType": { "const": "modification" }
}
},
"then": {
"properties": {
"user": { "type": "string" },
"resource": { "type": "string" },
"changes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"field": { "type": "string" },
"oldValue": { "type": "any" },
"newValue": { "type": "any" }
},
"required": ["field", "oldValue", "newValue"]
}
}
},
"required": ["user", "resource", "changes"]
}
},
{
"if": {
"properties": {
"eventType": { "const": "security" }
}
},
"then": {
"properties": {
"threatLevel": { "enum": ["low", "medium", "high"] },
"source": { "type": "string" },
"indicators": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["threatLevel", "source", "indicators"]
}
}
]
}
},
"required": ["action", "severity", "details"]
},
"remediate": {
"type": "object",
"properties": {
"actionType": { "type": "string" },
"config": {
"type": "object",
"properties": {
"operation": { "type": "string" }
},
"required": ["operation"],
"allOf": [
{
"if": {
"properties": {
"operation": { "const": "quarantine" }
}
},
"then": {
"properties": {
"duration": { "type": "string", "pattern": "^[0-9]+[hmd]$" },
"reason": { "type": "string" }
},
"required": ["duration", "reason"]
}
},
{
"if": {
"properties": {
"operation": { "const": "scale" }
}
},
"then": {
"properties": {
"targetSize": { "type": "integer", "minimum": 0, "maximum": 100 },
"metric": { "enum": ["cpu", "memory", "requests"] }
},
"required": ["targetSize", "metric"]
}
},
{
"if": {
"properties": {
"operation": { "const": "replace" }
}
},
"then": {
"properties": {
"newResource": {
"type": "object",
"properties": {
"type": { "type": "string" },
"spec": { "type": "object" }
},
"required": ["type", "spec"]
},
"preserveData": { "type": "boolean" }
},
"required": ["newResource", "preserveData"]
}
}
]
}
},
"required": ["actionType", "config"]
}
}
}

View File

@@ -0,0 +1,189 @@
{
"name": "target.tests.msgraph",
"description": "Microsoft Graph API target for identity and access management testing",
"version": "1.0.0",
"resource_schema_selector": "@odata.type",
"resource_schemas": [
{
"type": "object",
"properties": {
"@odata.type": { "const": "#microsoft.graph.user" },
"id": { "type": "string" },
"userPrincipalName": { "type": "string" },
"displayName": { "type": "string" },
"givenName": { "type": "string" },
"surname": { "type": "string" },
"mail": { "type": "string" },
"jobTitle": { "type": "string" },
"department": { "type": "string" },
"accountEnabled": { "type": "boolean" },
"userType": { "enum": ["Member", "Guest"] },
"assignedLicenses": {
"type": "array",
"items": {
"type": "object",
"properties": {
"skuId": { "type": "string" },
"disabledPlans": { "type": "array", "items": { "type": "string" } }
}
}
},
"signInActivity": {
"type": "object",
"properties": {
"lastSignInDateTime": { "type": "string" },
"lastNonInteractiveSignInDateTime": { "type": "string" }
}
}
},
"required": ["@odata.type", "id", "userPrincipalName"]
},
{
"type": "object",
"properties": {
"@odata.type": { "const": "#microsoft.graph.group" },
"id": { "type": "string" },
"displayName": { "type": "string" },
"description": { "type": "string" },
"groupTypes": { "type": "array", "items": { "type": "string" } },
"securityEnabled": { "type": "boolean" },
"mailEnabled": { "type": "boolean" },
"mail": { "type": "string" },
"visibility": { "enum": ["Public", "Private", "HiddenMembership"] },
"members": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"@odata.type": { "type": "string" }
}
}
}
},
"required": ["@odata.type", "id", "displayName"]
},
{
"type": "object",
"properties": {
"@odata.type": { "const": "#microsoft.graph.application" },
"id": { "type": "string" },
"appId": { "type": "string" },
"displayName": { "type": "string" },
"publisherDomain": { "type": "string" },
"signInAudience": { "enum": ["AzureADMyOrg", "AzureADMultipleOrgs", "AzureADandPersonalMicrosoftAccount", "PersonalMicrosoftAccount"] },
"requiredResourceAccess": {
"type": "array",
"items": {
"type": "object",
"properties": {
"resourceAppId": { "type": "string" },
"resourceAccess": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"type": { "enum": ["Scope", "Role"] }
}
}
}
}
}
},
"web": {
"type": "object",
"properties": {
"redirectUris": { "type": "array", "items": { "type": "string" } },
"implicitGrantSettings": {
"type": "object",
"properties": {
"enableAccessTokenIssuance": { "type": "boolean" },
"enableIdTokenIssuance": { "type": "boolean" }
}
}
}
}
},
"required": ["@odata.type", "id", "appId", "displayName"]
},
{
"type": "object",
"properties": {
"@odata.type": { "const": "#microsoft.graph.conditionalAccessPolicy" },
"id": { "type": "string" },
"displayName": { "type": "string" },
"state": { "enum": ["enabled", "disabled", "enabledForReportingButNotEnforced"] },
"conditions": {
"type": "object",
"properties": {
"users": {
"type": "object",
"properties": {
"includeUsers": { "type": "array", "items": { "type": "string" } },
"excludeUsers": { "type": "array", "items": { "type": "string" } },
"includeGroups": { "type": "array", "items": { "type": "string" } },
"excludeGroups": { "type": "array", "items": { "type": "string" } }
}
},
"applications": {
"type": "object",
"properties": {
"includeApplications": { "type": "array", "items": { "type": "string" } },
"excludeApplications": { "type": "array", "items": { "type": "string" } }
}
},
"locations": {
"type": "object",
"properties": {
"includeLocations": { "type": "array", "items": { "type": "string" } },
"excludeLocations": { "type": "array", "items": { "type": "string" } }
}
},
"riskLevels": { "type": "array", "items": { "enum": ["low", "medium", "high", "none"] } }
}
},
"grantControls": {
"type": "object",
"properties": {
"operator": { "enum": ["AND", "OR"] },
"builtInControls": { "type": "array", "items": { "enum": ["block", "mfa", "compliantDevice", "domainJoinedDevice", "approvedApplication", "compliantApplication"] } }
}
}
},
"required": ["@odata.type", "id", "displayName", "state"]
}
],
"effects": {
"allow": { "type": "boolean" },
"block": {
"type": "object",
"properties": {
"reason": { "type": "string" },
"blockType": { "enum": ["signin", "access", "registration"] }
}
},
"requireMfa": {
"type": "object",
"properties": {
"methods": { "type": "array", "items": { "enum": ["sms", "voice", "app", "oath"] } }
}
},
"audit": {
"type": "object",
"properties": {
"level": { "enum": ["info", "warning", "error"] },
"message": { "type": "string" },
"category": { "enum": ["signin", "audit", "risk", "provisioning"] }
}
},
"remediate": {
"type": "object",
"properties": {
"action": { "enum": ["disable", "enable", "reset", "notify"] },
"target": { "type": "string" },
"parameters": { "type": "object" }
}
}
}
}

View File

@@ -0,0 +1,21 @@
{
"name": "target.tests.no_default_schema_target",
"description": "A target without a default schema for testing missing default schema error",
"version": "1.0.0",
"resource_schema_selector": "type",
"resource_schemas": [
{
"type": "object",
"properties": {
"name": { "type": "string" },
"type": { "const": "specific_resource_type" },
"value": { "type": "string" }
},
"required": ["name", "type"]
}
],
"effects": {
"allow": { "type": "boolean" },
"deny": { "type": "boolean" }
}
}

View File

@@ -0,0 +1,38 @@
{
"name": "target.tests.sample_test_target",
"description": "A sample target for testing target loading functionality",
"version": "1.0.0",
"resource_schema_selector": "type",
"resource_schemas": [
{
"type": "object",
"properties": {
"name": { "type": "string" },
"type": { "const": "test_resource" },
"value": { "type": "string" }
},
"required": ["name", "type"]
},
{
"type": "object",
"properties": {
"name": { "type": "string" },
"type": { "type": "string" },
"value": { "type": "string" }
},
"required": ["name"],
"additionalProperties": true
}
],
"effects": {
"allow": { "type": "boolean" },
"deny": { "type": "boolean" },
"test_effect": {
"type": "object",
"properties": {
"level": { "type": "string" },
"message": { "type": "string" }
}
}
}
}

View File

@@ -0,0 +1,83 @@
cases:
- note: "Microsoft Graph User Access Control - Allow Active User"
data: {}
input:
"@odata.type": "#microsoft.graph.user"
id: "12345678-1234-1234-1234-123456789012"
userPrincipalName: "john.doe@company.com"
displayName: "John Doe"
givenName: "John"
surname: "Doe"
mail: "john.doe@company.com"
jobTitle: "Software Engineer"
department: "Engineering"
accountEnabled: true
userType: "Member"
modules:
- |
package msgraph.user.allow
import rego.v1
__target__ := "target.tests.msgraph"
default allow := false
allow if {
input["@odata.type"] == "#microsoft.graph.user"
input.accountEnabled == true
input.userType == "Member"
}
query: data.msgraph.user.allow.allow
want_result: true
- note: "Microsoft Graph User Access Control - Block Disabled User"
data: {}
input:
"@odata.type": "#microsoft.graph.user"
id: "87654321-4321-4321-4321-210987654321"
userPrincipalName: "disabled.user@company.com"
displayName: "Disabled User"
accountEnabled: false
userType: "Member"
modules:
- |
package msgraph.user.block
import rego.v1
__target__ := "target.tests.msgraph"
block := {
"reason": "User account is disabled",
"blockType": "signin"
} if {
input["@odata.type"] == "#microsoft.graph.user"
input.accountEnabled == false
}
query: data.msgraph.user.block.block
want_result:
reason: "User account is disabled"
blockType: "signin"
- note: "Microsoft Graph Invalid Resource Type"
data: {}
input:
"@odata.type": "#microsoft.graph.unknownResource"
id: "test"
modules:
- |
package msgraph.invalid
import rego.v1
__target__ := "target.tests.msgraph"
default allow := false
allow if {
input["@odata.type"] == "#microsoft.graph.user"
input.accountEnabled == true
}
query: data.msgraph.invalid.allow
want_result: false

View File

@@ -0,0 +1,455 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for resource type inference functionality
# The infer_resource_type function looks for patterns like input.<selector> == "resource_type"
# in effect rules and builds a mapping of queries to their inferred resource types and schemas.
cases:
- note: "target/infer_resource_type_basic_equality"
data: {}
input: {"type": "test_resource", "name": "example"}
modules:
- |
package policy.basic_inference
import rego.v1
__target__ := "target.tests.sample_test_target"
default allow := false
# This should infer resource type "test_resource" from the equality check
allow if {
input.type == "test_resource"
input.name != ""
}
query: data.policy.basic_inference.allow
want_result: true
want_inferred_resource_types: ["test_resource"]
- note: "target/infer_resource_type_multiple_equality_checks"
data: {}
input: {"type": "test_resource", "name": "example", "status": "active"}
modules:
- |
package policy.multiple_checks
import rego.v1
__target__ := "target.tests.sample_test_target"
default allow := false
# Multiple conditions with resource type check
allow if {
input.type == "test_resource"
input.status == "active"
input.name != ""
}
query: data.policy.multiple_checks.allow
want_result: true
want_inferred_resource_types: ["test_resource"]
- note: "target/infer_resource_type_complex_target_compute"
data: {}
input: {"resourceType": "compute", "name": "vm1", "spec": {"cpu": 4, "memory": "8Gi"}}
modules:
- |
package policy.complex_compute
import rego.v1
__target__ := "target.tests.complex_target"
default allow := false
# Should infer "compute" resource type from complex target
allow if {
input.resourceType == "compute"
input.spec.cpu >= 2
input.spec.memory
}
query: data.policy.complex_compute.allow
want_result: true
want_inferred_resource_types: ["compute"]
- note: "target/infer_resource_type_complex_target_storage"
data: {}
input: {"resourceType": "storage", "name": "disk1", "spec": {"size": "100Gi", "type": "ssd"}}
modules:
- |
package policy.complex_storage
import rego.v1
__target__ := "target.tests.complex_target"
default deny := false
# Should infer "storage" resource type from complex target
deny if {
input.resourceType == "storage"
input.spec.type == "hdd" # Deny HDDs
}
query: data.policy.complex_storage.deny
want_result: false
want_inferred_resource_types: ["storage"]
- note: "target/infer_resource_type_default_schema_usage"
data: {}
input: {"resourceType": "unknown_type", "name": "mystery_resource"}
modules:
- |
package policy.default_schema
import rego.v1
__target__ := "target.tests.complex_target"
default allow := false
# Should use default schema for unknown resource types
allow if {
input.resourceType == "unknown_type"
input.name != ""
}
query: data.policy.default_schema.allow
want_result: true
want_inferred_resource_types: ["unknown_type"]
- note: "target/infer_resource_type_multiple_rules_same_effect"
data: {}
input: {"type": "test_resource", "name": "example"}
modules:
- |
package policy.multiple_rules
import rego.v1
__target__ := "target.tests.sample_test_target"
default allow := false
# First rule with resource type check
allow if {
input.type == "test_resource"
input.name == "example"
}
# Second rule with different resource type check (should also be inferred)
allow if {
input.type == "other_resource"
input.status == "approved"
}
query: data.policy.multiple_rules.allow
want_result: true
want_inferred_resource_types: ["test_resource", "other_resource"] # Should infer both types from different rules
- note: "target/infer_resource_type_no_equality_check"
data: {}
input: {"name": "example", "status": "active"}
modules:
- |
package policy.no_type_check
import rego.v1
__target__ := "target.tests.sample_test_target"
default allow := false
# No resource type equality check - should resolve to default schema
allow if {
input.name == "example"
input.status == "active"
}
query: data.policy.no_type_check.allow
want_result: true
want_inferred_resource_types: ["default"] # Should resolve to default schema
- note: "target/infer_resource_type_wrong_equality_direction"
data: {}
input: {"type": "test_resource", "name": "example"}
modules:
- |
package policy.wrong_direction
import rego.v1
__target__ := "target.tests.sample_test_target"
default allow := false
# Equality check in wrong direction (should still be detected)
allow if {
"test_resource" == input.type
input.name != ""
}
query: data.policy.wrong_direction.allow
want_result: true
want_inferred_resource_types: ["test_resource"]
- note: "target/infer_resource_type_array_access_selector"
data: {}
input: {"metadata": {"type": "test_resource"}, "name": "example"}
modules:
- |
package policy.array_access
import rego.v1
__target__ := "target.tests.sample_test_target"
default allow := false
# Using array-style access for the selector field
allow if {
input["type"] == "test_resource"
input.name != ""
}
query: data.policy.array_access.allow
want_result: false # This input doesn't have input.type, only input.metadata.type
want_inferred_resource_types: ["test_resource"]
- note: "target/infer_resource_type_variable_in_equality"
data: {}
input: {"type": "test_resource", "name": "example"}
modules:
- |
package policy.variable_equality
import rego.v1
__target__ := "target.tests.sample_test_target"
default allow := false
# Using a variable in the equality (resolves to default schema since not a literal string)
resource_type := "test_resource"
allow if {
input.type == resource_type
input.name != ""
}
query: data.policy.variable_equality.allow
want_result: true
want_inferred_resource_types: ["default"] # Should resolve to default schema since not a literal string
- note: "target/infer_resource_type_non_string_literal"
data: {}
input: {"priority": 5, "name": "example"}
modules:
- |
package policy.non_string
import rego.v1
__target__ := "target.tests.sample_test_target"
default allow := false
# Non-string literal equality (resolves to default schema)
allow if {
input.priority == 5
input.name != ""
}
query: data.policy.non_string.allow
want_result: true
want_inferred_resource_types: ["default"] # Should resolve to default schema
- note: "target/infer_resource_type_not_first_statement"
data: {}
input: {"type": "test_resource", "name": "example", "status": "active"}
modules:
- |
package policy.not_first_statement
import rego.v1
__target__ := "target.tests.sample_test_target"
default allow := false
# Resource type check is not the first statement (should still be inferred)
allow if {
input.name != ""
input.status == "active"
input.type == "test_resource"
}
query: data.policy.not_first_statement.allow
want_result: true
want_inferred_resource_types: ["test_resource"]
- note: "target/infer_resource_type_nested_condition_not_inferred"
data: {}
input: {"type": "test_resource", "name": "example", "nested": {"resourceType": "compute"}}
modules:
- |
package policy.nested_condition
import rego.v1
__target__ := "target.tests.sample_test_target"
default allow := false
# Nested condition with resource type check (should not be inferred at nested level)
allow if {
input.name == "example"
some condition
condition := input.nested.resourceType == "compute"
condition
}
query: data.policy.nested_condition.allow
want_result: true
want_inferred_resource_types: ["default"] # Should resolve to default schema (nested not inferred)
- note: "target/infer_resource_type_nested_rule_not_inferred"
data: {}
input: {"type": "test_resource", "name": "example"}
modules:
- |
package policy.nested_rule
import rego.v1
__target__ := "target.tests.sample_test_target"
default allow := false
# Helper rule with nested type check (should not be inferred from helper rules)
is_valid_resource(resource_type) if {
resource_type == "test_resource"
}
# Main rule that uses helper (should resolve to default schema)
allow if {
input.name == "example"
is_valid_resource(input.type)
}
query: data.policy.nested_rule.allow
want_result: true
want_inferred_resource_types: ["default"] # Should resolve to default schema (helper rule not inferred)
- note: "target/infer_resource_type_comprehension_not_inferred"
data: {}
input: {"type": "test_resource", "name": "example", "resources": [{"type": "compute"}, {"type": "storage"}]}
modules:
- |
package policy.comprehension
import rego.v1
__target__ := "target.tests.sample_test_target"
default allow := false
# Type check inside comprehension (should not be inferred)
allow if {
input.name == "example"
valid_resources := [r | r := input.resources[_]; r.type == "compute"]
count(valid_resources) > 0
}
query: data.policy.comprehension.allow
want_result: true
want_inferred_resource_types: ["default"] # Should resolve to default schema (comprehension not inferred)
- note: "target/infer_resource_type_multiple_types_same_rule"
data: {}
input: {"type": "test_resource", "name": "example"}
modules:
- |
package policy.multiple_types_same_rule
import rego.v1
__target__ := "target.tests.sample_test_target"
default allow := false
# Single rule with multiple resource type checks (should infer both)
allow if {
input.name == "example"
input.type == "test_resource"
input.type == "other_resource" # This will never match, but should still be inferred
}
query: data.policy.multiple_types_same_rule.allow
want_result: false # Will never match since input.type can't be both values
want_inferred_resource_types: ["test_resource", "other_resource"]
- note: "target/infer_resource_type_multiple_types_different_rules"
data: {}
input: {"resourceType": "compute", "name": "vm1", "spec": {"cpu": 4}}
modules:
- |
package policy.multiple_types_different_rules
import rego.v1
__target__ := "target.tests.complex_target"
default allow := false
# First rule checks for compute
allow if {
input.resourceType == "compute"
input.spec.cpu >= 2
}
# Second rule checks for storage
allow if {
input.resourceType == "storage"
input.spec.size
}
# Third rule checks for network
allow if {
input.resourceType == "network"
input.spec.subnet
}
query: data.policy.multiple_types_different_rules.allow
want_result: true
want_inferred_resource_types: ["compute", "storage", "network"]
- note: "target/infer_resource_type_invalid_schema_error"
data: {}
input: {"type": "invalid_resource", "name": "example"}
modules:
- |
package policy.invalid_schema
import rego.v1
__target__ := "target.tests.nonexistent_target"
default allow := false
# Reference to a target that doesn't exist in the registry
allow if {
input.type == "test_resource"
input.name != ""
}
query: data.policy.invalid_schema.allow
error: "Target 'target.tests.nonexistent_target' not found in registry"
- note: "target/infer_resource_type_missing_default_schema_error"
data: {}
input: {"name": "example", "status": "active"}
modules:
- |
package policy.missing_default_schema
import rego.v1
__target__ := "target.tests.no_default_schema_target"
default allow := false
# No type check, should trigger missing default schema error
allow if {
input.name == "example"
input.status == "active"
}
query: data.policy.missing_default_schema.allow
error: "Missing default resource schema: Target 'target.tests.no_default_schema_target' has no default resource schema"