mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
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:
committed by
GitHub
parent
3c33d31d08
commit
cc917ea75d
30
.github/workflows/test-csharp.yml
vendored
30
.github/workflows/test-csharp.yml
vendored
@@ -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
101
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
421
bindings/csharp/API.md
Normal 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.
|
||||
168
bindings/csharp/Regorus/CompiledPolicy.cs
Normal file
168
bindings/csharp/Regorus/CompiledPolicy.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
196
bindings/csharp/Regorus/Compiler.cs
Normal file
196
bindings/csharp/Regorus/Compiler.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
546
bindings/csharp/Regorus/NativeMethods.cs
Normal file
546
bindings/csharp/Regorus/NativeMethods.cs
Normal 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
|
||||
}
|
||||
141
bindings/csharp/Regorus/PolicyInfo.cs
Normal file
141
bindings/csharp/Regorus/PolicyInfo.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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. -->
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
284
bindings/csharp/Regorus/SchemaRegistry.cs
Normal file
284
bindings/csharp/Regorus/SchemaRegistry.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
185
bindings/csharp/Regorus/TargetRegistry.cs
Normal file
185
bindings/csharp/Regorus/TargetRegistry.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
291
bindings/csharp/TargetExampleApp/Program.cs
Normal file
291
bindings/csharp/TargetExampleApp/Program.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
26
bindings/csharp/TargetExampleApp/TargetExampleApp.csproj
Normal file
26
bindings/csharp/TargetExampleApp/TargetExampleApp.csproj
Normal 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>
|
||||
125
bindings/csharp/TargetExampleApp/azure_policy.target.json
Normal file
125
bindings/csharp/TargetExampleApp/azure_policy.target.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,6 @@
|
||||
"sdk": {
|
||||
"allowPrerelease": false,
|
||||
"version": "8.0.412",
|
||||
"rollForward": "disable"
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
57
bindings/ffi/Cargo.lock
generated
57
bindings/ffi/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -32,4 +32,3 @@ custom_allocator = []
|
||||
|
||||
[build-dependencies]
|
||||
cbindgen = "0.28.0"
|
||||
csbindgen = "=1.9.3"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
31
bindings/ffi/src/allocator.rs
Normal file
31
bindings/ffi/src/allocator.rs
Normal 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
229
bindings/ffi/src/common.rs
Normal 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
208
bindings/ffi/src/compile.rs
Normal 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)
|
||||
}
|
||||
69
bindings/ffi/src/compiled_policy.rs
Normal file
69
bindings/ffi/src/compiled_policy.rs
Normal 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)),
|
||||
}
|
||||
}
|
||||
175
bindings/ffi/src/effect_registry.rs
Normal file
175
bindings/ffi/src/effect_registry.rs
Normal 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
454
bindings/ffi/src/engine.rs
Normal 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}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
178
bindings/ffi/src/schema_registry.rs
Normal file
178
bindings/ffi/src/schema_registry.rs
Normal 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())
|
||||
}
|
||||
107
bindings/ffi/src/target_registry.rs
Normal file
107
bindings/ffi/src/target_registry.rs
Normal 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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
37
bindings/java/Cargo.lock
generated
37
bindings/java/Cargo.lock
generated
@@ -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"
|
||||
|
||||
33
bindings/python/Cargo.lock
generated
33
bindings/python/Cargo.lock
generated
@@ -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"
|
||||
|
||||
33
bindings/ruby/Cargo.lock
generated
33
bindings/ruby/Cargo.lock
generated
@@ -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"
|
||||
|
||||
33
bindings/wasm/Cargo.lock
generated
33
bindings/wasm/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
91
src/compile.rs
Normal 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
215
src/compiled_policy.rs
Normal 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>>)>,
|
||||
}
|
||||
320
src/engine.rs
320
src/engine.rs
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
58
src/interpreter/error.rs
Normal 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),
|
||||
}
|
||||
278
src/interpreter/target/infer.rs
Normal file
278
src/interpreter/target/infer.rs
Normal 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(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
248
src/interpreter/target/resolve.rs
Normal file
248
src/interpreter/target/resolve.rs
Normal 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(())
|
||||
}
|
||||
23
src/lib.rs
23
src/lib.rs
@@ -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};
|
||||
|
||||
@@ -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
42
src/policy_info.rs
Normal 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>,
|
||||
}
|
||||
@@ -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
1254
src/registry/tests/target.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
79
src/target.rs
Normal 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
76
src/target/deserialize.rs
Normal 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
35
src/target/error.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
90
src/target/resource_schema_selector.rs
Normal file
90
src/target/resource_schema_selector.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
358
src/target/tests/deserialize.rs
Normal file
358
src/target/tests/deserialize.rs
Normal 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());
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
|
||||
80
tests/interpreter/cases/builtins/strings/sprintf.yaml
Normal file
80
tests/interpreter/cases/builtins/strings/sprintf.yaml
Normal 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"
|
||||
79
tests/interpreter/cases/target/azure_policy.yaml
Normal file
79
tests/interpreter/cases/target/azure_policy.yaml
Normal 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
|
||||
682
tests/interpreter/cases/target/basic.yaml
Normal file
682
tests/interpreter/cases/target/basic.yaml
Normal 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'"
|
||||
561
tests/interpreter/cases/target/complex.yaml
Normal file
561
tests/interpreter/cases/target/complex.yaml
Normal 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"] }
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
125
tests/interpreter/cases/target/definitions/azure_policy.json
Normal file
125
tests/interpreter/cases/target/definitions/azure_policy.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
195
tests/interpreter/cases/target/definitions/complex_target.json
Normal file
195
tests/interpreter/cases/target/definitions/complex_target.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
189
tests/interpreter/cases/target/definitions/msgraph.json
Normal file
189
tests/interpreter/cases/target/definitions/msgraph.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
83
tests/interpreter/cases/target/msgraph.yaml
Normal file
83
tests/interpreter/cases/target/msgraph.yaml
Normal 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
|
||||
455
tests/interpreter/cases/target/resource_type_inference.yaml
Normal file
455
tests/interpreter/cases/target/resource_type_inference.yaml
Normal 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"
|
||||
Reference in New Issue
Block a user