├── ansible_policy
├── __init__.py
├── __version__.py
├── policybook
│ ├── image
│ │ ├── flow.png
│ │ ├── example_result.png
│ │ └── flow.drawio
│ ├── policybook_models.py
│ ├── rego_model.py
│ ├── to_ast.py
│ ├── policy_parser.py
│ ├── README.md
│ ├── json_generator.py
│ ├── transpiler.py
│ └── condition_parser.py
├── rego
│ └── utils.rego
└── eval_policy.py
├── .flake8
├── tests
└── unit
│ ├── integration
│ ├── affirm_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── affirm_operator_test.rego
│ ├── in_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── in_operator_test.rego
│ ├── int_equal_operator
│ │ ├── input_pass.json
│ │ ├── input_fail.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── equal_operator_test.rego
│ ├── match_operator
│ │ ├── input_pass.json
│ │ ├── input_fail.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── match_operator_test.rego
│ ├── negate_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── negate_operator_test.rego
│ ├── str_equal_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── equal_operator_test.rego
│ ├── not_in_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── in_operator_test.rego
│ ├── not_match_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── match_operator_test.rego
│ ├── regex_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── search_operator_test.rego
│ ├── search_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── search_operator_test.rego
│ ├── select_compare_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── select_operator_test.rego
│ ├── is_defined_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── is_defined_operator_test.rego
│ ├── item_in_list_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── item_in_list_test.rego
│ ├── item_not_in_list_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── item_not_in_list_test.rego
│ ├── list_contains_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── list_contains_test.rego
│ ├── not_regex_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── search_operator_test.rego
│ ├── not_search_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── search_operator_test.rego
│ ├── not_select_compare_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── select_operator_test.rego
│ ├── null_not_equal_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── equal_operator_test.rego
│ ├── select_search_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── select_operator_test.rego
│ ├── selectattr_compare_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── selectattr_operator_test.rego
│ ├── list_not_contains_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── list_not_contains_test.rego
│ ├── not_select_search_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── select_operator_test.rego
│ ├── not_selectattr_compare_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── not_selectattr_operator_test.rego
│ ├── selectattr_search_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── selectattr_operator_test.rego
│ ├── not_selectattr_search_operator
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── not_selectattr_operator_test.rego
│ ├── multi_condition_all
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── multi_condition_all.rego
│ ├── multi_condition_any
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ │ └── policy
│ │ │ └── multi_condition_any.rego
│ └── multi_condition_notall
│ │ ├── input_fail.json
│ │ ├── input_pass.json
│ │ ├── policybook.yml
│ │ └── extensions
│ │ └── policy
│ │ └── multi_condition_not_all.rego
│ ├── policybooks
│ ├── policies_with_multiple_conditions.yml
│ ├── policies_with_multiple_conditions2.yml
│ ├── policies_with_multiple_conditions3.yml
│ └── policies_with_multiple_conditions4.yml
│ ├── asts
│ ├── policies_with_multiple_conditions.yml
│ ├── policies_with_multiple_conditions2.yml
│ ├── policies_with_multiple_conditions3.yml
│ └── policies_with_multiple_conditions4.yml
│ ├── test_policybook_e2e.py
│ ├── test_transpiler.py
│ └── test_ast.py
├── images
├── ap-arch.png
├── example_output.png
├── example_output_json.png
├── example_output_policybook.png
└── example_output_event_stream.png
├── .gitignore
├── examples
├── ansible-policy.cfg
├── check_rest
│ ├── policies
│ │ └── check_rest_username.yml
│ └── rest_hook.py
├── check_event
│ ├── policies
│ │ └── check_changed_event.yml
│ └── event_handler.py
└── check_project
│ ├── policies
│ ├── check_pkg.yml
│ ├── check_collection.yml
│ └── check_become.yml
│ ├── playbook_mongodb.yml
│ └── playbook.yml
├── .pre-commit-config.yaml
├── pyproject.toml
├── README.md
└── LICENSE
/ansible_policy/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ansible_policy/__version__.py:
--------------------------------------------------------------------------------
1 | __version__ = "v0.0.1"
2 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore=W503,E203,E402
3 | max-line-length=150
--------------------------------------------------------------------------------
/tests/unit/integration/affirm_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": false
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/affirm_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val" : true
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/in_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val3"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/in_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/int_equal_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": 1
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/match_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/negate_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": true
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/negate_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val" : false
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/str_equal_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": 1
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/int_equal_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/match_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "test_val1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/not_in_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/not_in_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val3"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/not_match_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/regex_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "test_1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/regex_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "test_val1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/search_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "test_1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/select_compare_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": 25
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/select_compare_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": 15
3 | }
--------------------------------------------------------------------------------
/images/ap-arch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansible/ansible-policy/HEAD/images/ap-arch.png
--------------------------------------------------------------------------------
/tests/unit/integration/is_defined_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val1": "val1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/is_defined_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "test_val"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/item_in_list_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val3"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/item_in_list_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val2"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/item_not_in_list_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/item_not_in_list_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val3"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/list_contains_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val3"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/list_contains_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val2"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/not_match_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "match_val1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/not_regex_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "test_val1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/not_regex_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "test_1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/not_search_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "test_val1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/not_search_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "test_1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/not_select_compare_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": 5
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/not_select_compare_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": 25
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/null_not_equal_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": null
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/search_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "test_val1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/select_search_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val3"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/select_search_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/selectattr_compare_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": 50
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/selectattr_compare_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": 15
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/str_equal_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "str_val"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/list_not_contains_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/list_not_contains_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val3"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/not_select_search_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val1"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/not_select_search_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val3"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/not_selectattr_compare_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": 15
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/not_selectattr_compare_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": 25
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/null_not_equal_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "str_val"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/selectattr_search_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "name"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/selectattr_search_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val1"
3 | }
--------------------------------------------------------------------------------
/images/example_output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansible/ansible-policy/HEAD/images/example_output.png
--------------------------------------------------------------------------------
/tests/unit/integration/not_selectattr_search_operator/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val2"
3 | }
--------------------------------------------------------------------------------
/tests/unit/integration/not_selectattr_search_operator/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "name"
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | *.egg-info
3 | build
4 | dist
5 | venv
6 | output.json
7 | *.bak
8 | test
9 | .DS_Store
--------------------------------------------------------------------------------
/images/example_output_json.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansible/ansible-policy/HEAD/images/example_output_json.png
--------------------------------------------------------------------------------
/tests/unit/integration/multi_condition_all/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val1",
3 | "test_val2": "val3"
4 | }
--------------------------------------------------------------------------------
/tests/unit/integration/multi_condition_all/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val1",
3 | "test_val2": "val2"
4 | }
--------------------------------------------------------------------------------
/tests/unit/integration/multi_condition_any/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val3",
3 | "test_val2": "val3"
4 | }
--------------------------------------------------------------------------------
/tests/unit/integration/multi_condition_any/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val1",
3 | "test_val2": "val2"
4 | }
--------------------------------------------------------------------------------
/tests/unit/integration/multi_condition_notall/input_fail.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val1",
3 | "test_val2": "val2"
4 | }
--------------------------------------------------------------------------------
/tests/unit/integration/multi_condition_notall/input_pass.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_val": "val1",
3 | "test_val2": "val3"
4 | }
--------------------------------------------------------------------------------
/images/example_output_policybook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansible/ansible-policy/HEAD/images/example_output_policybook.png
--------------------------------------------------------------------------------
/ansible_policy/policybook/image/flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansible/ansible-policy/HEAD/ansible_policy/policybook/image/flow.png
--------------------------------------------------------------------------------
/images/example_output_event_stream.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansible/ansible-policy/HEAD/images/example_output_event_stream.png
--------------------------------------------------------------------------------
/ansible_policy/policybook/image/example_result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ansible/ansible-policy/HEAD/ansible_policy/policybook/image/example_result.png
--------------------------------------------------------------------------------
/examples/ansible-policy.cfg:
--------------------------------------------------------------------------------
1 | [policy]
2 | default disabled
3 | policies.community.* tag=security enabled
4 | policies.org.compliance tag=compliance enabled
5 |
6 | [source]
7 | policies.org.compliance = examples/check_project/policies # org-wide compliance policy
8 |
--------------------------------------------------------------------------------
/tests/unit/integration/affirm_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: affirm operator test
3 | hosts: localhost
4 | policies:
5 | - name: affirm operator test
6 | target: task
7 | condition: input.test_val
8 | actions:
9 | - allow:
10 | msg: affirm operator test
11 | tags:
12 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/negate_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: negate operator test
3 | hosts: localhost
4 | policies:
5 | - name: negate operator test
6 | target: task
7 | condition: not input.test_val
8 | actions:
9 | - allow:
10 | msg: negate operator test
11 | tags:
12 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/is_defined_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: is defined operator test
3 | hosts: localhost
4 | policies:
5 | - name: is defined operator test
6 | target: task
7 | condition: input.test_val is defined
8 | actions:
9 | - allow:
10 | msg: is defined operator test
11 | tags:
12 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/int_equal_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: equal operator test
3 | hosts: localhost
4 | policies:
5 | - name: equal operator test
6 | target: task
7 | condition:
8 | any:
9 | - input.test_val == 1
10 | actions:
11 | - allow:
12 | msg: equal operator test
13 | tags:
14 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/null_not_equal_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: equal operator test
3 | hosts: localhost
4 | policies:
5 | - name: equal operator test
6 | target: task
7 | condition:
8 | any:
9 | - input.test_val != null
10 | actions:
11 | - allow:
12 | msg: equal operator test
13 | tags:
14 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/str_equal_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: equal operator test
3 | hosts: localhost
4 | policies:
5 | - name: equal operator test
6 | target: task
7 | condition:
8 | any:
9 | - input.test_val == "str_val"
10 | actions:
11 | - allow:
12 | msg: equal operator test
13 | tags:
14 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/match_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: match operator test
3 | hosts: localhost
4 | policies:
5 | - name: match operator test
6 | target: task
7 | condition:
8 | any:
9 | - input.test_val is match("val", ignorecase=true)
10 | actions:
11 | - allow:
12 | msg: match operator test
13 | tags:
14 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/regex_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: search operator test
3 | hosts: localhost
4 | policies:
5 | - name: search operator test
6 | target: task
7 | condition:
8 | any:
9 | - input.test_val is regex("v.l", ignorecase=true)
10 | actions:
11 | - allow:
12 | msg: search operator test
13 | tags:
14 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/not_match_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: match operator test
3 | hosts: localhost
4 | policies:
5 | - name: match operator test
6 | target: task
7 | condition:
8 | any:
9 | - input.test_val is not match("val", ignorecase=true)
10 | actions:
11 | - allow:
12 | msg: match operator test
13 | tags:
14 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/search_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: search operator test
3 | hosts: localhost
4 | policies:
5 | - name: search operator test
6 | target: task
7 | condition:
8 | any:
9 | - input.test_val is search("Val", ignorecase=true)
10 | actions:
11 | - allow:
12 | msg: search operator test
13 | tags:
14 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/not_regex_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: search operator test
3 | hosts: localhost
4 | policies:
5 | - name: search operator test
6 | target: task
7 | condition:
8 | any:
9 | - input.test_val is not regex("v.l", ignorecase=true)
10 | actions:
11 | - allow:
12 | msg: search operator test
13 | tags:
14 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/not_search_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: search operator test
3 | hosts: localhost
4 | policies:
5 | - name: search operator test
6 | target: task
7 | condition:
8 | any:
9 | - input.test_val is not search("val", ignorecase=true)
10 | actions:
11 | - allow:
12 | msg: search operator test
13 | tags:
14 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/item_in_list_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: item in list test
3 | hosts: localhost
4 | vars:
5 | sample_list:
6 | - val1
7 | - val2
8 | policies:
9 | - name: item in list test
10 | target: task
11 | condition: input.test_val in sample_list
12 | actions:
13 | - allow:
14 | msg: item in list test
15 | tags:
16 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/list_contains_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: list contains test
3 | hosts: localhost
4 | vars:
5 | sample_list:
6 | - val1
7 | - val2
8 | policies:
9 | - name: list contains test
10 | target: task
11 | condition: sample_list contains input.test_val
12 | actions:
13 | - allow:
14 | msg: list contains test
15 | tags:
16 | - security
--------------------------------------------------------------------------------
/tests/unit/policybooks/policies_with_multiple_conditions.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Demo policies multiple conditions all
3 | hosts: localhost
4 | vars:
5 | vars_i: 10
6 | policies:
7 | - name: multiple conditions
8 | target: task
9 | condition:
10 | any:
11 | - input.first <= vars_i
12 | - input.second > vars_i
13 | actions:
14 | - info:
15 | msg: "multiple conditions any"
--------------------------------------------------------------------------------
/tests/unit/integration/in_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: in operator test
3 | hosts: localhost
4 | vars:
5 | sample_list:
6 | - val1
7 | - val2
8 | policies:
9 | - name: in operator test
10 | target: task
11 | condition:
12 | any:
13 | - input.test_val in sample_list
14 | actions:
15 | - allow:
16 | msg: in operator test
17 | tags:
18 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/item_not_in_list_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: item not in list test
3 | hosts: localhost
4 | vars:
5 | sample_list:
6 | - val1
7 | - val2
8 | policies:
9 | - name: item not in list test
10 | target: task
11 | condition: input.test_val not in sample_list
12 | actions:
13 | - allow:
14 | msg: item not in list test
15 | tags:
16 | - security
--------------------------------------------------------------------------------
/tests/unit/policybooks/policies_with_multiple_conditions2.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Demo policies multiple conditions all
3 | hosts: localhost
4 | vars:
5 | vars_i: 10
6 | policies:
7 | - name: multiple conditions
8 | target: task
9 | condition:
10 | all:
11 | - input.first <= vars_i
12 | - input.second > vars_i
13 | actions:
14 | - info:
15 | msg: "multiple conditions all"
--------------------------------------------------------------------------------
/tests/unit/integration/list_not_contains_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: list not contains test
3 | hosts: localhost
4 | vars:
5 | sample_list:
6 | - val1
7 | - val2
8 | policies:
9 | - name: list not contains test
10 | target: task
11 | condition: sample_list not contains input.test_val
12 | actions:
13 | - allow:
14 | msg: list not contains test
15 | tags:
16 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/not_in_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: in operator test
3 | hosts: localhost
4 | vars:
5 | sample_list:
6 | - val1
7 | - val2
8 | policies:
9 | - name: in operator test
10 | target: task
11 | condition:
12 | any:
13 | - input.test_val not in sample_list
14 | actions:
15 | - allow:
16 | msg: in operator test
17 | tags:
18 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/select_compare_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: select operator test
3 | hosts: localhost
4 | vars:
5 | sample_list:
6 | - 10
7 | - 20
8 | policies:
9 | - name: select operator test
10 | target: task
11 | condition:
12 | any:
13 | - sample_list is select('>=', input.test_val)
14 | actions:
15 | - allow:
16 | msg: select operator test
17 | tags:
18 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/not_select_compare_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: select operator test
3 | hosts: localhost
4 | vars:
5 | sample_list:
6 | - 10
7 | - 20
8 | policies:
9 | - name: select operator test
10 | target: task
11 | condition:
12 | any:
13 | - sample_list is not select('>=', input.test_val)
14 | actions:
15 | - allow:
16 | msg: select operator test
17 | tags:
18 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/select_search_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: select operator test
3 | hosts: localhost
4 | vars:
5 | sample_list:
6 | - val1
7 | - val2
8 | policies:
9 | - name: select operator test
10 | target: task
11 | condition:
12 | any:
13 | - sample_list is select('search', input.test_val)
14 | actions:
15 | - allow:
16 | msg: select operator test
17 | tags:
18 | - security
--------------------------------------------------------------------------------
/tests/unit/policybooks/policies_with_multiple_conditions3.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Demo rules multiple conditions and
3 | hosts: localhost
4 | vars:
5 | vars_i: 10
6 | policies:
7 | - name: multiple conditions
8 | target: task
9 | condition:
10 | all:
11 | - input.first != vars_i and input.first != 0
12 | - input.second != vars_i and input.second != 0
13 | actions:
14 | - info:
15 | msg: "multiple conditions and"
--------------------------------------------------------------------------------
/examples/check_rest/policies/check_rest_username.yml:
--------------------------------------------------------------------------------
1 | # check_rest_username.yml
2 | ---
3 | - name: Check username in rest data
4 | hosts: localhost
5 | policies:
6 | - name: Check if username is admin
7 | target: rest
8 | condition:
9 | all:
10 | - input.method == "POST"
11 | - input.data.username == "admin"
12 | actions:
13 | - deny:
14 | msg: "`username` must not be 'admin'"
15 | tags:
16 | - compliance
17 |
--------------------------------------------------------------------------------
/tests/unit/integration/not_select_search_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: select operator test
3 | hosts: localhost
4 | vars:
5 | sample_list:
6 | - val1
7 | - val2
8 | policies:
9 | - name: select operator test
10 | target: task
11 | condition:
12 | any:
13 | - sample_list is not select('search', input.test_val)
14 | actions:
15 | - allow:
16 | msg: select operator test
17 | tags:
18 | - security
--------------------------------------------------------------------------------
/tests/unit/policybooks/policies_with_multiple_conditions4.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Demo rules multiple conditions or
3 | hosts: localhost
4 | vars:
5 | vars_i: 10
6 | policies:
7 | - name: multiple conditions
8 | target: task
9 | condition:
10 | all:
11 | - input.first != vars_i or input.first != 0
12 | - input.second != vars_i or input.second != 0 and input.third != 0
13 | actions:
14 | - info:
15 | msg: "multiple conditions or"
16 |
--------------------------------------------------------------------------------
/tests/unit/integration/multi_condition_notall/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: multi condition not all
3 | hosts: localhost
4 | vars:
5 | sample_list:
6 | - val1
7 | - val2
8 | policies:
9 | - name: multi condition not all
10 | target: task
11 | condition:
12 | not_all:
13 | - input.test_val in sample_list
14 | - input.test_val2 == "val2"
15 | actions:
16 | - allow:
17 | msg: multi condition not all
18 | tags:
19 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/selectattr_compare_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: selectattr operator test
3 | hosts: localhost
4 | vars:
5 | sample_list:
6 | - {"age": 10}
7 | - {"age": 20}
8 | policies:
9 | - name: selectattr operator test
10 | target: task
11 | condition:
12 | any:
13 | - sample_list is selectattr('age', '>=', input.test_val)
14 | actions:
15 | - allow:
16 | msg: selectattr operator test
17 | tags:
18 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/multi_condition_all/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: multi condition all
3 | hosts: localhost
4 | vars:
5 | sample_list:
6 | - val1
7 | - val2
8 | policies:
9 | - name: multi condition all
10 | target: task
11 | condition:
12 | all:
13 | - input.test_val in sample_list
14 | - input.test_val2 in sample_list or input.test_val2 == "val2"
15 | actions:
16 | - allow:
17 | msg: multi condition all
18 | tags:
19 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/multi_condition_any/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: multi condition any
3 | hosts: localhost
4 | vars:
5 | sample_list:
6 | - val1
7 | - val2
8 | policies:
9 | - name: multi condition any
10 | target: task
11 | condition:
12 | any:
13 | - input.test_val in sample_list
14 | - input.test_val2 in sample_list or input.test_val2 == "val2"
15 | actions:
16 | - allow:
17 | msg: multi condition any
18 | tags:
19 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/selectattr_search_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: selectattr operator test
3 | hosts: localhost
4 | vars:
5 | sample_list:
6 | - {"name": "val1"}
7 | - {"name": "val2"}
8 | policies:
9 | - name: selectattr operator test
10 | target: task
11 | condition:
12 | any:
13 | - sample_list is selectattr('name', 'search', input.test_val)
14 | actions:
15 | - allow:
16 | msg: selectattr operator test
17 | tags:
18 | - security
--------------------------------------------------------------------------------
/tests/unit/integration/not_selectattr_compare_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: not selectattr operator test
3 | hosts: localhost
4 | vars:
5 | sample_list:
6 | - {"age": 10}
7 | - {"age": 20}
8 | policies:
9 | - name: not selectattr operator test
10 | target: task
11 | condition:
12 | any:
13 | - sample_list is not selectattr('age', '>=', input.test_val)
14 | actions:
15 | - allow:
16 | msg: not selectattr operator test
17 | tags:
18 | - security
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: "https://github.com/ambv/black"
3 | rev: "22.12.0"
4 | hooks:
5 | - id: "black"
6 | language_version: "python3"
7 | args:
8 | - --line-length=150
9 | - --include='\.pyi?$'
10 | - --exclude="""\.git |
11 | \.hg|
12 | \.mypy_cache|
13 | \.tox|
14 | \.venv|
15 | _build|
16 | buck-out|
17 | build|
18 | dist
19 | """
20 | - repo: https://github.com/pycqa/flake8
21 | rev: "6.0.0"
22 | hooks:
23 | - id: "flake8"
--------------------------------------------------------------------------------
/tests/unit/integration/not_selectattr_search_operator/policybook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: not selectattr operator test
3 | hosts: localhost
4 | vars:
5 | sample_list:
6 | - {"name": "val1"}
7 | - {"name": "val2"}
8 | policies:
9 | - name: not selectattr operator test
10 | target: task
11 | condition:
12 | any:
13 | - sample_list is not selectattr('name', 'search', input.test_val)
14 | actions:
15 | - allow:
16 | msg: not selectattr operator test
17 | tags:
18 | - security
--------------------------------------------------------------------------------
/examples/check_event/policies/check_changed_event.yml:
--------------------------------------------------------------------------------
1 | # check_changed_event.yml
2 | ---
3 | - name: Check for job event with changed & ufw
4 | hosts: localhost
5 | policies:
6 | - name: Check for event with changed
7 | target: event
8 | condition:
9 | all:
10 | - input.event_data.resolved_action == "community.general.ufw"
11 | - input.event_data.changed
12 | actions:
13 | - deny:
14 | msg: "`Changed` event is detected for a `community.general.ufw` task"
15 | tags:
16 | - compliance
17 |
--------------------------------------------------------------------------------
/tests/unit/integration/affirm_operator/extensions/policy/affirm_operator_test.rego:
--------------------------------------------------------------------------------
1 | package affirm_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 |
14 | affirm_operator_test_1_1 = true if {
15 | input.test_val
16 | }
17 |
18 |
19 | affirm_operator_test_0_1 = true if {
20 | affirm_operator_test_1_1
21 | }
22 |
23 |
24 | allow = true if {
25 | affirm_operator_test_0_1
26 | print("affirm operator test")
27 | } else = false
28 |
--------------------------------------------------------------------------------
/tests/unit/integration/int_equal_operator/extensions/policy/equal_operator_test.rego:
--------------------------------------------------------------------------------
1 | package equal_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 |
14 | equal_operator_test_0_2 = true if {
15 | input.test_val == 1
16 | }
17 |
18 |
19 | equal_operator_test_0_1 = true if {
20 | equal_operator_test_0_2
21 | }
22 |
23 |
24 | allow = true if {
25 | equal_operator_test_0_1
26 | print("equal operator test")
27 | } else = false
28 |
--------------------------------------------------------------------------------
/examples/check_project/policies/check_pkg.yml:
--------------------------------------------------------------------------------
1 | # package-example.yml
2 | ---
3 | - name: Check for mysql package installation
4 | hosts: localhost
5 | vars:
6 | allowed_packages:
7 | - "mysql-server"
8 | policies:
9 | - name: Check for package name
10 | target: task
11 | condition: input["ansible.builtin.package"].name not in allowed_packages
12 | actions:
13 | - deny:
14 | msg: The package {{ input["ansible.builtin.package"].name }} is not allowed, allowed packages are one of {{ allowed_packages }}
15 | tags:
16 | - compliance
17 |
--------------------------------------------------------------------------------
/tests/unit/integration/negate_operator/extensions/policy/negate_operator_test.rego:
--------------------------------------------------------------------------------
1 | package negate_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 |
14 | negate_operator_test_1_1 = true if {
15 | not input.test_val
16 | }
17 |
18 |
19 | negate_operator_test_0_1 = true if {
20 | negate_operator_test_1_1
21 | }
22 |
23 |
24 | allow = true if {
25 | negate_operator_test_0_1
26 | print("negate operator test")
27 | } else = false
28 |
--------------------------------------------------------------------------------
/tests/unit/integration/null_not_equal_operator/extensions/policy/equal_operator_test.rego:
--------------------------------------------------------------------------------
1 | package equal_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 |
14 | equal_operator_test_0_2 = true if {
15 | input.test_val != null
16 | }
17 |
18 |
19 | equal_operator_test_0_1 = true if {
20 | equal_operator_test_0_2
21 | }
22 |
23 |
24 | allow = true if {
25 | equal_operator_test_0_1
26 | print("equal operator test")
27 | } else = false
28 |
--------------------------------------------------------------------------------
/tests/unit/integration/str_equal_operator/extensions/policy/equal_operator_test.rego:
--------------------------------------------------------------------------------
1 | package equal_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 |
14 | equal_operator_test_0_2 = true if {
15 | input.test_val == "str_val"
16 | }
17 |
18 |
19 | equal_operator_test_0_1 = true if {
20 | equal_operator_test_0_2
21 | }
22 |
23 |
24 | allow = true if {
25 | equal_operator_test_0_1
26 | print("equal operator test")
27 | } else = false
28 |
--------------------------------------------------------------------------------
/tests/unit/integration/not_match_operator/extensions/policy/match_operator_test.rego:
--------------------------------------------------------------------------------
1 | package match_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 |
14 | match_operator_test_0_2 = true if {
15 | not startswith(input.test_val, "val")
16 | }
17 |
18 |
19 | match_operator_test_0_1 = true if {
20 | match_operator_test_0_2
21 | }
22 |
23 |
24 | allow = true if {
25 | match_operator_test_0_1
26 | print("match operator test")
27 | } else = false
28 |
--------------------------------------------------------------------------------
/tests/unit/integration/match_operator/extensions/policy/match_operator_test.rego:
--------------------------------------------------------------------------------
1 | package match_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 |
14 | match_operator_test_0_2 = true if {
15 | startswith(lower(input.test_val), lower("val"))
16 | }
17 |
18 |
19 | match_operator_test_0_1 = true if {
20 | match_operator_test_0_2
21 | }
22 |
23 |
24 | allow = true if {
25 | match_operator_test_0_1
26 | print("match operator test")
27 | } else = false
28 |
--------------------------------------------------------------------------------
/tests/unit/integration/not_search_operator/extensions/policy/search_operator_test.rego:
--------------------------------------------------------------------------------
1 | package search_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 |
14 | search_operator_test_0_2 = true if {
15 | not contains(input.test_val, "val")
16 | }
17 |
18 |
19 | search_operator_test_0_1 = true if {
20 | search_operator_test_0_2
21 | }
22 |
23 |
24 | allow = true if {
25 | search_operator_test_0_1
26 | print("search operator test")
27 | } else = false
28 |
--------------------------------------------------------------------------------
/tests/unit/integration/not_regex_operator/extensions/policy/search_operator_test.rego:
--------------------------------------------------------------------------------
1 | package search_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 |
14 | search_operator_test_0_2 = true if {
15 | regex.find_n("v.l", input.test_val, 1) == []
16 | }
17 |
18 |
19 | search_operator_test_0_1 = true if {
20 | search_operator_test_0_2
21 | }
22 |
23 |
24 | allow = true if {
25 | search_operator_test_0_1
26 | print("search operator test")
27 | } else = false
28 |
--------------------------------------------------------------------------------
/tests/unit/integration/search_operator/extensions/policy/search_operator_test.rego:
--------------------------------------------------------------------------------
1 | package search_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 |
14 | search_operator_test_0_2 = true if {
15 | contains(lower(input.test_val), lower("Val"))
16 | }
17 |
18 |
19 | search_operator_test_0_1 = true if {
20 | search_operator_test_0_2
21 | }
22 |
23 |
24 | allow = true if {
25 | search_operator_test_0_1
26 | print("search operator test")
27 | } else = false
28 |
--------------------------------------------------------------------------------
/tests/unit/integration/regex_operator/extensions/policy/search_operator_test.rego:
--------------------------------------------------------------------------------
1 | package search_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 |
14 | search_operator_test_0_2 = true if {
15 | regex.find_n(lower("v.l"), lower(input.test_val), 1) != []
16 | }
17 |
18 |
19 | search_operator_test_0_1 = true if {
20 | search_operator_test_0_2
21 | }
22 |
23 |
24 | allow = true if {
25 | search_operator_test_0_1
26 | print("search operator test")
27 | } else = false
28 |
--------------------------------------------------------------------------------
/examples/check_project/policies/check_collection.yml:
--------------------------------------------------------------------------------
1 | # package-example.yml
2 | ---
3 | - name: Check for using collection
4 | hosts: localhost
5 | vars:
6 | allowed_collections:
7 | - ansible.builtin
8 | - amazon.aws
9 | policies:
10 | - name: Check for collection name
11 | target: task
12 | condition: input._agk.task.module_info.collection not in allowed_collections
13 | actions:
14 | - deny:
15 | msg: The collection {{ input._agk.task.module_info.collection }} is not allowed, allowed collection are one of {{ allowed_collections }}
16 | tags:
17 | - compliance
18 |
--------------------------------------------------------------------------------
/ansible_policy/policybook/policybook_models.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 | from typing import Dict, List, NamedTuple, Union
3 |
4 | import ansible_rulebook.condition_types as ct
5 |
6 |
7 | class Action(NamedTuple):
8 | action: str
9 | action_args: dict
10 |
11 |
12 | class Condition(NamedTuple):
13 | when: str
14 | value: List[ct.Condition]
15 |
16 |
17 | class Policy(NamedTuple):
18 | name: str
19 | condition: Condition
20 | actions: List[Action]
21 | enabled: bool
22 | tags: List[str]
23 | target: str
24 |
25 |
26 | class PolicySet(NamedTuple):
27 | name: str
28 | hosts: Union[str, List[str]]
29 | vars: Dict
30 | policies: List[Policy]
31 | match_multiple_policies: bool = False
32 |
--------------------------------------------------------------------------------
/examples/check_project/playbook_mongodb.yml:
--------------------------------------------------------------------------------
1 | - hosts: localhost
2 | tasks:
3 | - name: Set variables
4 | set_fact:
5 | database_name: "not-allowed-db"
6 | database_user: "abc"
7 |
8 | - name: Touch a file
9 | ansible.builtin.file:
10 | path: /etc/foo.conf
11 | state: touch
12 |
13 | - name: Create mongodb user
14 | community.mongodb.mongodb_user:
15 | database: "{{ database_name }}"
16 | name: "{{ database_user }}"
17 | state: present
18 | password: "test"
19 | login_user: admin
20 | login_password: admin
21 |
22 | - name: Touch a file with root permission
23 | become: True
24 | ansible.builtin.file:
25 | path: /etc/bar.conf
26 | state: touch
27 |
--------------------------------------------------------------------------------
/tests/unit/asts/policies_with_multiple_conditions.yml:
--------------------------------------------------------------------------------
1 | - PolicySet:
2 | hosts:
3 | - localhost
4 | name: Demo policies multiple conditions all
5 | policies:
6 | - Policy:
7 | actions:
8 | - Action:
9 | action: info
10 | action_args:
11 | msg: multiple conditions any
12 | condition:
13 | AnyCondition:
14 | - LessThanOrEqualToExpression:
15 | lhs:
16 | Input: input.first
17 | rhs:
18 | Variable: vars_i
19 | - GreaterThanExpression:
20 | lhs:
21 | Input: input.second
22 | rhs:
23 | Variable: vars_i
24 | enabled: true
25 | name: multiple conditions
26 | tags: []
27 | target: task
28 | vars:
29 | vars_i: 10
30 |
--------------------------------------------------------------------------------
/tests/unit/asts/policies_with_multiple_conditions2.yml:
--------------------------------------------------------------------------------
1 | - PolicySet:
2 | hosts:
3 | - localhost
4 | name: Demo policies multiple conditions all
5 | policies:
6 | - Policy:
7 | actions:
8 | - Action:
9 | action: info
10 | action_args:
11 | msg: multiple conditions all
12 | condition:
13 | AllCondition:
14 | - LessThanOrEqualToExpression:
15 | lhs:
16 | Input: input.first
17 | rhs:
18 | Variable: vars_i
19 | - GreaterThanExpression:
20 | lhs:
21 | Input: input.second
22 | rhs:
23 | Variable: vars_i
24 | enabled: true
25 | name: multiple conditions
26 | tags: []
27 | target: task
28 | vars:
29 | vars_i: 10
30 |
--------------------------------------------------------------------------------
/examples/check_event/event_handler.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 | import json
4 | import argparse
5 | from ansible_policy.models import (
6 | PolicyEvaluator,
7 | ResultFormatter,
8 | FORMAT_EVENT_STREAM,
9 | )
10 |
11 |
12 | def load_event():
13 | for line in sys.stdin:
14 | yield json.loads(line)
15 |
16 |
17 | def main():
18 | parser = argparse.ArgumentParser(description="TODO")
19 | parser.add_argument("--policy-dir", help="path to a directory containing policies to be evaluated")
20 | args = parser.parse_args()
21 |
22 | evaluator = PolicyEvaluator(policy_dir=args.policy_dir)
23 | formatter = ResultFormatter(format_type=FORMAT_EVENT_STREAM, base_dir=os.getcwd())
24 | for event in load_event():
25 | result = evaluator.run(
26 | eval_type="event",
27 | event=event,
28 | )
29 | formatter.print(result)
30 |
31 |
32 | if __name__ == "__main__":
33 | main()
34 |
--------------------------------------------------------------------------------
/tests/unit/integration/is_defined_operator/extensions/policy/is_defined_operator_test.rego:
--------------------------------------------------------------------------------
1 | package is_defined_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 |
14 | to_list(val) = output if {
15 | is_array(val)
16 | output = val
17 | }
18 |
19 | to_list(val) = output if {
20 | not is_array(val)
21 | output = [val]
22 | }
23 |
24 |
25 | check_item_not_in_list(lhs_list, rhs_list) = true if {
26 | array := [item | item := lhs_list[_]; item in rhs_list]
27 | count(array) == 0
28 | } else = false
29 |
30 |
31 | is_defined_operator_test_1_1 = true if {
32 | input
33 | input.test_val
34 | }
35 |
36 |
37 | is_defined_operator_test_0_1 = true if {
38 | is_defined_operator_test_1_1
39 | }
40 |
41 |
42 | allow = true if {
43 | is_defined_operator_test_0_1
44 | print("is defined operator test")
45 | } else = false
46 |
--------------------------------------------------------------------------------
/tests/unit/integration/in_operator/extensions/policy/in_operator_test.rego:
--------------------------------------------------------------------------------
1 | package in_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 | sample_list = ["val1", "val2"]
14 |
15 | to_list(val) = output if {
16 | is_array(val)
17 | output = val
18 | }
19 |
20 | to_list(val) = output if {
21 | not is_array(val)
22 | output = [val]
23 | }
24 |
25 |
26 | check_item_in_list(lhs_list, rhs_list) = true if {
27 | array := [item | item := lhs_list[_]; item in rhs_list]
28 | count(array) > 0
29 | } else = false
30 |
31 |
32 | in_operator_test_0_2 = true if {
33 | lhs_list = to_list(input.test_val)
34 | check_item_in_list(lhs_list, sample_list)
35 | }
36 |
37 |
38 | in_operator_test_0_1 = true if {
39 | in_operator_test_0_2
40 | }
41 |
42 |
43 | allow = true if {
44 | in_operator_test_0_1
45 | print("in operator test")
46 | } else = false
47 |
--------------------------------------------------------------------------------
/tests/unit/integration/item_in_list_operator/extensions/policy/item_in_list_test.rego:
--------------------------------------------------------------------------------
1 | package item_in_list_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 | sample_list = ["val1", "val2"]
14 |
15 | to_list(val) = output if {
16 | is_array(val)
17 | output = val
18 | }
19 |
20 | to_list(val) = output if {
21 | not is_array(val)
22 | output = [val]
23 | }
24 |
25 |
26 | check_item_in_list(lhs_list, rhs_list) = true if {
27 | array := [item | item := lhs_list[_]; item in rhs_list]
28 | count(array) > 0
29 | } else = false
30 |
31 |
32 | item_in_list_test_1_1 = true if {
33 | lhs_list = to_list(input.test_val)
34 | check_item_in_list(lhs_list, sample_list)
35 | }
36 |
37 |
38 | item_in_list_test_0_1 = true if {
39 | item_in_list_test_1_1
40 | }
41 |
42 |
43 | allow = true if {
44 | item_in_list_test_0_1
45 | print("item in list test")
46 | } else = false
47 |
--------------------------------------------------------------------------------
/tests/unit/integration/not_in_operator/extensions/policy/in_operator_test.rego:
--------------------------------------------------------------------------------
1 | package in_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 | sample_list = ["val1", "val2"]
14 |
15 | to_list(val) = output if {
16 | is_array(val)
17 | output = val
18 | }
19 |
20 | to_list(val) = output if {
21 | not is_array(val)
22 | output = [val]
23 | }
24 |
25 |
26 | check_item_not_in_list(lhs_list, rhs_list) = true if {
27 | array := [item | item := lhs_list[_]; item in rhs_list]
28 | count(array) == 0
29 | } else = false
30 |
31 |
32 | in_operator_test_0_2 = true if {
33 | lhs_list = to_list(input.test_val)
34 | check_item_not_in_list(lhs_list, sample_list)
35 | }
36 |
37 |
38 | in_operator_test_0_1 = true if {
39 | in_operator_test_0_2
40 | }
41 |
42 |
43 | allow = true if {
44 | in_operator_test_0_1
45 | print("in operator test")
46 | } else = false
47 |
--------------------------------------------------------------------------------
/tests/unit/integration/list_contains_operator/extensions/policy/list_contains_test.rego:
--------------------------------------------------------------------------------
1 | package list_contains_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 | sample_list = ["val1", "val2"]
14 |
15 | to_list(val) = output if {
16 | is_array(val)
17 | output = val
18 | }
19 |
20 | to_list(val) = output if {
21 | not is_array(val)
22 | output = [val]
23 | }
24 |
25 |
26 | check_item_in_list(lhs_list, rhs_list) = true if {
27 | array := [item | item := lhs_list[_]; item in rhs_list]
28 | count(array) > 0
29 | } else = false
30 |
31 |
32 | list_contains_test_1_1 = true if {
33 | lhs_list = to_list(input.test_val)
34 | check_item_in_list(lhs_list, sample_list)
35 | }
36 |
37 |
38 | list_contains_test_0_1 = true if {
39 | list_contains_test_1_1
40 | }
41 |
42 |
43 | allow = true if {
44 | list_contains_test_0_1
45 | print("list contains test")
46 | } else = false
47 |
--------------------------------------------------------------------------------
/tests/unit/integration/select_compare_operator/extensions/policy/select_operator_test.rego:
--------------------------------------------------------------------------------
1 | package select_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 | sample_list = [10, 20]
14 |
15 | to_list(val) = output if {
16 | is_array(val)
17 | output = val
18 | }
19 |
20 | to_list(val) = output if {
21 | not is_array(val)
22 | output = [val]
23 | }
24 |
25 |
26 | check_item_in_list(lhs_list, rhs_list) = true if {
27 | array := [item | item := lhs_list[_]; item in rhs_list]
28 | count(array) > 0
29 | } else = false
30 |
31 |
32 | select_operator_test_0_2 = true if {
33 | array := [item | item := sample_list[_]; item >= input.test_val]
34 | count(array) > 0
35 | }
36 |
37 |
38 | select_operator_test_0_1 = true if {
39 | select_operator_test_0_2
40 | }
41 |
42 |
43 | allow = true if {
44 | select_operator_test_0_1
45 | print("select operator test")
46 | } else = false
47 |
--------------------------------------------------------------------------------
/tests/unit/integration/select_search_operator/extensions/policy/select_operator_test.rego:
--------------------------------------------------------------------------------
1 | package select_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 | sample_list = ["val1", "val2"]
14 |
15 | to_list(val) = output if {
16 | is_array(val)
17 | output = val
18 | }
19 |
20 | to_list(val) = output if {
21 | not is_array(val)
22 | output = [val]
23 | }
24 |
25 |
26 | check_item_in_list(lhs_list, rhs_list) = true if {
27 | array := [item | item := lhs_list[_]; item in rhs_list]
28 | count(array) > 0
29 | } else = false
30 |
31 |
32 | select_operator_test_0_2 = true if {
33 | rhs_list = to_list(input.test_val)
34 | check_item_in_list(sample_list, rhs_list)
35 | }
36 |
37 |
38 | select_operator_test_0_1 = true if {
39 | select_operator_test_0_2
40 | }
41 |
42 |
43 | allow = true if {
44 | select_operator_test_0_1
45 | print("select operator test")
46 | } else = false
47 |
--------------------------------------------------------------------------------
/tests/unit/integration/not_select_compare_operator/extensions/policy/select_operator_test.rego:
--------------------------------------------------------------------------------
1 | package select_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 | sample_list = [10, 20]
14 |
15 | to_list(val) = output if {
16 | is_array(val)
17 | output = val
18 | }
19 |
20 | to_list(val) = output if {
21 | not is_array(val)
22 | output = [val]
23 | }
24 |
25 |
26 | check_item_not_in_list(lhs_list, rhs_list) = true if {
27 | array := [item | item := lhs_list[_]; item in rhs_list]
28 | count(array) == 0
29 | } else = false
30 |
31 |
32 | select_operator_test_0_2 = true if {
33 | array := [item | item := sample_list[_]; item >= input.test_val]
34 | count(array) == 0
35 | }
36 |
37 |
38 | select_operator_test_0_1 = true if {
39 | select_operator_test_0_2
40 | }
41 |
42 |
43 | allow = true if {
44 | select_operator_test_0_1
45 | print("select operator test")
46 | } else = false
47 |
--------------------------------------------------------------------------------
/tests/unit/integration/not_select_search_operator/extensions/policy/select_operator_test.rego:
--------------------------------------------------------------------------------
1 | package select_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 | sample_list = ["val1", "val2"]
14 |
15 | to_list(val) = output if {
16 | is_array(val)
17 | output = val
18 | }
19 |
20 | to_list(val) = output if {
21 | not is_array(val)
22 | output = [val]
23 | }
24 |
25 |
26 | check_item_not_in_list(lhs_list, rhs_list) = true if {
27 | array := [item | item := lhs_list[_]; item in rhs_list]
28 | count(array) == 0
29 | } else = false
30 |
31 |
32 | select_operator_test_0_2 = true if {
33 | rhs_list = to_list(input.test_val)
34 | check_item_not_in_list(sample_list, rhs_list)
35 | }
36 |
37 |
38 | select_operator_test_0_1 = true if {
39 | select_operator_test_0_2
40 | }
41 |
42 |
43 | allow = true if {
44 | select_operator_test_0_1
45 | print("select operator test")
46 | } else = false
47 |
--------------------------------------------------------------------------------
/examples/check_project/policies/check_become.yml:
--------------------------------------------------------------------------------
1 | # package-example.yml
2 | ---
3 | - name: Check for privilage escalation
4 | hosts: localhost
5 | vars:
6 | allowed_users:
7 | - "trusted_user"
8 | policies:
9 | - name: Check for using become in task
10 | target: task
11 | condition:
12 | any:
13 | - input.become == true and input.become_user not in allowed_users
14 | - input.become == true and input lacks key become_user
15 | actions:
16 | - deny:
17 | msg: privilage escalation is detected. allowed users are one of {{ allowed_users }}
18 | tags:
19 | - compliance
20 | - name: Check for using become in play
21 | target: play
22 | condition:
23 | any:
24 | - input.become == true and input.become_user not in allowed_users
25 | - input.become == true and input lacks key become_user
26 | actions:
27 | - deny:
28 | msg: privilage escalation is detected. allowed users are one of {{ allowed_users }}
29 | tags:
30 | - compliance
--------------------------------------------------------------------------------
/tests/unit/integration/item_not_in_list_operator/extensions/policy/item_not_in_list_test.rego:
--------------------------------------------------------------------------------
1 | package item_not_in_list_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 | sample_list = ["val1", "val2"]
14 |
15 | to_list(val) = output if {
16 | is_array(val)
17 | output = val
18 | }
19 |
20 | to_list(val) = output if {
21 | not is_array(val)
22 | output = [val]
23 | }
24 |
25 |
26 | check_item_not_in_list(lhs_list, rhs_list) = true if {
27 | array := [item | item := lhs_list[_]; item in rhs_list]
28 | count(array) == 0
29 | } else = false
30 |
31 |
32 | item_not_in_list_test_1_1 = true if {
33 | lhs_list = to_list(input.test_val)
34 | check_item_not_in_list(lhs_list, sample_list)
35 | }
36 |
37 |
38 | item_not_in_list_test_0_1 = true if {
39 | item_not_in_list_test_1_1
40 | }
41 |
42 |
43 | allow = true if {
44 | item_not_in_list_test_0_1
45 | print("item not in list test")
46 | } else = false
47 |
--------------------------------------------------------------------------------
/tests/unit/integration/list_not_contains_operator/extensions/policy/list_not_contains_test.rego:
--------------------------------------------------------------------------------
1 | package list_not_contains_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 | sample_list = ["val1", "val2"]
14 |
15 | to_list(val) = output if {
16 | is_array(val)
17 | output = val
18 | }
19 |
20 | to_list(val) = output if {
21 | not is_array(val)
22 | output = [val]
23 | }
24 |
25 |
26 | check_item_not_in_list(lhs_list, rhs_list) = true if {
27 | array := [item | item := lhs_list[_]; item in rhs_list]
28 | count(array) == 0
29 | } else = false
30 |
31 |
32 | list_not_contains_test_1_1 = true if {
33 | lhs_list = to_list(input.test_val)
34 | check_item_not_in_list(lhs_list, sample_list)
35 | }
36 |
37 |
38 | list_not_contains_test_0_1 = true if {
39 | list_not_contains_test_1_1
40 | }
41 |
42 |
43 | allow = true if {
44 | list_not_contains_test_0_1
45 | print("list not contains test")
46 | } else = false
47 |
--------------------------------------------------------------------------------
/tests/unit/integration/selectattr_search_operator/extensions/policy/selectattr_operator_test.rego:
--------------------------------------------------------------------------------
1 | package selectattr_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 | sample_list = [{"name": "val1"}, {"name": "val2"}]
14 |
15 | to_list(val) = output if {
16 | is_array(val)
17 | output = val
18 | }
19 |
20 | to_list(val) = output if {
21 | not is_array(val)
22 | output = [val]
23 | }
24 |
25 |
26 | check_item_key_in_list(lhs_list, rhs_list, key) = true if {
27 | array := [item | item := lhs_list[_]; object.get(item, key, "none") in rhs_list]
28 | count(array) > 0
29 | } else = false
30 |
31 |
32 | selectattr_operator_test_0_2 = true if {
33 | rhs_list = to_list(input.test_val)
34 | check_item_key_in_list(sample_list, rhs_list, ["name"])
35 | }
36 |
37 |
38 | selectattr_operator_test_0_1 = true if {
39 | selectattr_operator_test_0_2
40 | }
41 |
42 |
43 | allow = true if {
44 | selectattr_operator_test_0_1
45 | print("selectattr operator test")
46 | } else = false
47 |
--------------------------------------------------------------------------------
/tests/unit/integration/selectattr_compare_operator/extensions/policy/selectattr_operator_test.rego:
--------------------------------------------------------------------------------
1 | package selectattr_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 | sample_list = [{"age": 10}, {"age": 20}]
14 |
15 | to_list(val) = output if {
16 | is_array(val)
17 | output = val
18 | }
19 |
20 | to_list(val) = output if {
21 | not is_array(val)
22 | output = [val]
23 | }
24 |
25 |
26 | check_item_key_in_list(lhs_list, rhs_list, key) = true if {
27 | array := [item | item := lhs_list[_]; object.get(item, key, "none") in rhs_list]
28 | count(array) > 0
29 | } else = false
30 |
31 |
32 | selectattr_operator_test_0_2 = true if {
33 | array := [item | item := sample_list[_]; object.get(item, ["age"], "none") >= input.test_val]
34 | count(array) > 0
35 | }
36 |
37 |
38 | selectattr_operator_test_0_1 = true if {
39 | selectattr_operator_test_0_2
40 | }
41 |
42 |
43 | allow = true if {
44 | selectattr_operator_test_0_1
45 | print("selectattr operator test")
46 | } else = false
47 |
--------------------------------------------------------------------------------
/tests/unit/integration/not_selectattr_search_operator/extensions/policy/not_selectattr_operator_test.rego:
--------------------------------------------------------------------------------
1 | package not_selectattr_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 | sample_list = [{"name": "val1"}, {"name": "val2"}]
14 |
15 | to_list(val) = output if {
16 | is_array(val)
17 | output = val
18 | }
19 |
20 | to_list(val) = output if {
21 | not is_array(val)
22 | output = [val]
23 | }
24 |
25 |
26 | check_item_key_not_in_list(lhs_list, rhs_list, key) = true if {
27 | array := [item | item := lhs_list[_]; object.get(item, key, "none") in rhs_list]
28 | count(array) == 0
29 | } else = false
30 |
31 |
32 | not_selectattr_operator_test_0_2 = true if {
33 | rhs_list = to_list(input.test_val)
34 | check_item_key_not_in_list(sample_list, rhs_list, ["name"])
35 | }
36 |
37 |
38 | not_selectattr_operator_test_0_1 = true if {
39 | not_selectattr_operator_test_0_2
40 | }
41 |
42 |
43 | allow = true if {
44 | not_selectattr_operator_test_0_1
45 | print("not selectattr operator test")
46 | } else = false
47 |
--------------------------------------------------------------------------------
/tests/unit/integration/not_selectattr_compare_operator/extensions/policy/not_selectattr_operator_test.rego:
--------------------------------------------------------------------------------
1 | package not_selectattr_operator_test
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 | sample_list = [{"age": 10}, {"age": 20}]
14 |
15 | to_list(val) = output if {
16 | is_array(val)
17 | output = val
18 | }
19 |
20 | to_list(val) = output if {
21 | not is_array(val)
22 | output = [val]
23 | }
24 |
25 |
26 | check_item_key_not_in_list(lhs_list, rhs_list, key) = true if {
27 | array := [item | item := lhs_list[_]; object.get(item, key, "none") in rhs_list]
28 | count(array) == 0
29 | } else = false
30 |
31 |
32 | not_selectattr_operator_test_0_2 = true if {
33 | array := [item | item := sample_list[_]; object.get(item, ["age"], "none") >= input.test_val]
34 | count(array) == 0
35 | }
36 |
37 |
38 | not_selectattr_operator_test_0_1 = true if {
39 | not_selectattr_operator_test_0_2
40 | }
41 |
42 |
43 | allow = true if {
44 | not_selectattr_operator_test_0_1
45 | print("not selectattr operator test")
46 | } else = false
47 |
--------------------------------------------------------------------------------
/tests/unit/integration/multi_condition_notall/extensions/policy/multi_condition_not_all.rego:
--------------------------------------------------------------------------------
1 | package multi_condition_not_all
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 | sample_list = ["val1", "val2"]
14 |
15 | to_list(val) = output if {
16 | is_array(val)
17 | output = val
18 | }
19 |
20 | to_list(val) = output if {
21 | not is_array(val)
22 | output = [val]
23 | }
24 |
25 |
26 | check_item_in_list(lhs_list, rhs_list) = true if {
27 | array := [item | item := lhs_list[_]; item in rhs_list]
28 | count(array) > 0
29 | } else = false
30 |
31 |
32 | multi_condition_not_all_1_1 = true if {
33 | lhs_list = to_list(input.test_val)
34 | check_item_in_list(lhs_list, sample_list)
35 | }
36 |
37 |
38 | multi_condition_not_all_1_2 = true if {
39 | input.test_val2 == "val2"
40 | }
41 |
42 |
43 | multi_condition_not_all_0_1 = true if {
44 | not multi_condition_not_all_1_1
45 | }
46 |
47 | multi_condition_not_all_0_1 = true if {
48 | not multi_condition_not_all_1_2
49 | }
50 |
51 |
52 | allow = true if {
53 | multi_condition_not_all_0_1
54 | print("multi condition not all")
55 | } else = false
56 |
--------------------------------------------------------------------------------
/tests/unit/asts/policies_with_multiple_conditions3.yml:
--------------------------------------------------------------------------------
1 | - PolicySet:
2 | hosts:
3 | - localhost
4 | name: Demo rules multiple conditions and
5 | policies:
6 | - Policy:
7 | actions:
8 | - Action:
9 | action: info
10 | action_args:
11 | msg: multiple conditions and
12 | condition:
13 | AllCondition:
14 | - AndExpression:
15 | lhs:
16 | NotEqualsExpression:
17 | lhs:
18 | Input: input.first
19 | rhs:
20 | Variable: vars_i
21 | rhs:
22 | NotEqualsExpression:
23 | lhs:
24 | Input: input.first
25 | rhs:
26 | Integer: 0
27 | - AndExpression:
28 | lhs:
29 | NotEqualsExpression:
30 | lhs:
31 | Input: input.second
32 | rhs:
33 | Variable: vars_i
34 | rhs:
35 | NotEqualsExpression:
36 | lhs:
37 | Input: input.second
38 | rhs:
39 | Integer: 0
40 | enabled: true
41 | name: multiple conditions
42 | tags: []
43 | target: task
44 | vars:
45 | vars_i: 10
46 |
--------------------------------------------------------------------------------
/examples/check_project/playbook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Provision EC2 instance and set up MySQL
3 | hosts: localhost
4 | gather_facts: false
5 | become: True
6 | vars:
7 | region: "your_aws_region"
8 | instance_type: "t2.micro"
9 | ami_id: "your_ami_id"
10 | key_name: "your_key_name"
11 | security_group: "your_security_group_id"
12 | subnet_id: "your_subnet_id"
13 | mysql_root_password: "your_mysql_root_password"
14 | package_list:
15 | - unauthorized-app
16 | tasks:
17 | - name: Create EC2 instance
18 | amazon.aws.ec2_instance:
19 | region: "{{ region }}"
20 | key_name: "{{ key_name }}"
21 | instance_type: "{{ instance_type }}"
22 | image_id: "{{ ami_id }}"
23 | security_group: "{{ security_group }}"
24 | subnet_id: "{{ subnet_id }}"
25 | assign_public_ip: true
26 | wait: yes
27 | count: 1
28 | instance_tags:
29 | Name: "MySQLInstance"
30 | register: ec2
31 |
32 | - name: Install Unauthorized App
33 | become: true
34 | ansible.builtin.package:
35 | name: "{{ package_list }}"
36 | state: present
37 |
38 | - name: Set MySQL root password [using unauthorized collection]
39 | community.mysql.mysql_user:
40 | name: root
41 | password: "{{ mysql_root_password }}"
42 | host: "{{ item }}"
43 | login_unix_socket: yes
44 | with_items: ["localhost", "127.0.0.1", "::1"]
45 |
--------------------------------------------------------------------------------
/tests/unit/integration/multi_condition_all/extensions/policy/multi_condition_all.rego:
--------------------------------------------------------------------------------
1 | package multi_condition_all
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 | sample_list = ["val1", "val2"]
14 |
15 | to_list(val) = output if {
16 | is_array(val)
17 | output = val
18 | }
19 |
20 | to_list(val) = output if {
21 | not is_array(val)
22 | output = [val]
23 | }
24 |
25 |
26 | check_item_in_list(lhs_list, rhs_list) = true if {
27 | array := [item | item := lhs_list[_]; item in rhs_list]
28 | count(array) > 0
29 | } else = false
30 |
31 |
32 | multi_condition_all_1_1 = true if {
33 | lhs_list = to_list(input.test_val)
34 | check_item_in_list(lhs_list, sample_list)
35 | }
36 |
37 |
38 | multi_condition_all_1_3 = true if {
39 | lhs_list = to_list(input.test_val2)
40 | check_item_in_list(lhs_list, sample_list)
41 | }
42 |
43 |
44 | multi_condition_all_1_4 = true if {
45 | input.test_val2 == "val2"
46 | }
47 |
48 |
49 | multi_condition_all_1_2 = true if {
50 | multi_condition_all_1_3
51 | }
52 |
53 | multi_condition_all_1_2 = true if {
54 | multi_condition_all_1_4
55 | }
56 |
57 |
58 | multi_condition_all_0_1 = true if {
59 | multi_condition_all_1_1
60 | multi_condition_all_1_2
61 | }
62 |
63 |
64 | allow = true if {
65 | multi_condition_all_0_1
66 | print("multi condition all")
67 | } else = false
68 |
--------------------------------------------------------------------------------
/tests/unit/integration/multi_condition_any/extensions/policy/multi_condition_any.rego:
--------------------------------------------------------------------------------
1 | package multi_condition_any
2 |
3 |
4 | import future.keywords.if
5 | import future.keywords.in
6 | import data.ansible_policy.resolve_var
7 |
8 |
9 | __target__ = "task"
10 | __tags__ = ["security"]
11 |
12 |
13 | sample_list = ["val1", "val2"]
14 |
15 | to_list(val) = output if {
16 | is_array(val)
17 | output = val
18 | }
19 |
20 | to_list(val) = output if {
21 | not is_array(val)
22 | output = [val]
23 | }
24 |
25 |
26 | check_item_in_list(lhs_list, rhs_list) = true if {
27 | array := [item | item := lhs_list[_]; item in rhs_list]
28 | count(array) > 0
29 | } else = false
30 |
31 |
32 | multi_condition_any_0_2 = true if {
33 | lhs_list = to_list(input.test_val)
34 | check_item_in_list(lhs_list, sample_list)
35 | }
36 |
37 |
38 | multi_condition_any_0_4 = true if {
39 | lhs_list = to_list(input.test_val2)
40 | check_item_in_list(lhs_list, sample_list)
41 | }
42 |
43 |
44 | multi_condition_any_0_5 = true if {
45 | input.test_val2 == "val2"
46 | }
47 |
48 |
49 | multi_condition_any_0_3 = true if {
50 | multi_condition_any_0_4
51 | }
52 |
53 | multi_condition_any_0_3 = true if {
54 | multi_condition_any_0_5
55 | }
56 |
57 |
58 | multi_condition_any_0_1 = true if {
59 | multi_condition_any_0_2
60 | }
61 |
62 | multi_condition_any_0_1 = true if {
63 | multi_condition_any_0_3
64 | }
65 |
66 |
67 | allow = true if {
68 | multi_condition_any_0_1
69 | print("multi condition any")
70 | } else = false
71 |
--------------------------------------------------------------------------------
/tests/unit/asts/policies_with_multiple_conditions4.yml:
--------------------------------------------------------------------------------
1 | - PolicySet:
2 | hosts:
3 | - localhost
4 | name: Demo rules multiple conditions or
5 | policies:
6 | - Policy:
7 | actions:
8 | - Action:
9 | action: info
10 | action_args:
11 | msg: multiple conditions or
12 | condition:
13 | AllCondition:
14 | - OrExpression:
15 | lhs:
16 | NotEqualsExpression:
17 | lhs:
18 | Input: input.first
19 | rhs:
20 | Variable: vars_i
21 | rhs:
22 | NotEqualsExpression:
23 | lhs:
24 | Input: input.first
25 | rhs:
26 | Integer: 0
27 | - AndExpression:
28 | lhs:
29 | OrExpression:
30 | lhs:
31 | NotEqualsExpression:
32 | lhs:
33 | Input: input.second
34 | rhs:
35 | Variable: vars_i
36 | rhs:
37 | NotEqualsExpression:
38 | lhs:
39 | Input: input.second
40 | rhs:
41 | Integer: 0
42 | rhs:
43 | NotEqualsExpression:
44 | lhs:
45 | Input: input.third
46 | rhs:
47 | Integer: 0
48 | enabled: true
49 | name: multiple conditions
50 | tags: []
51 | target: task
52 | vars:
53 | vars_i: 10
54 |
--------------------------------------------------------------------------------
/examples/check_rest/rest_hook.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 | import jsonpickle
4 | import argparse
5 | from flask import Flask, request
6 | from ansible_policy.models import (
7 | PolicyEvaluator,
8 | EvalTypeRest,
9 | ResultFormatter,
10 | FORMAT_REST,
11 | )
12 | from ansible_policy.rego_data import APIRequest
13 |
14 |
15 | app = Flask(__name__)
16 |
17 | log = logging.getLogger("werkzeug")
18 | log.setLevel(logging.ERROR)
19 | app.logger.disabled = True
20 | log.disabled = True
21 |
22 | parser = argparse.ArgumentParser(description="TODO")
23 | parser.add_argument("--policy-dir", help="path to a directory containing policies to be evaluated")
24 | args = parser.parse_args()
25 |
26 | evaluator = PolicyEvaluator(policy_dir=args.policy_dir)
27 | formatter = ResultFormatter(format_type=FORMAT_REST, base_dir=os.getcwd())
28 |
29 |
30 | @app.route("/", methods=["GET", "POST"])
31 | def index():
32 |
33 | headers = {}
34 | for key, val in request.headers.items():
35 | headers[key] = val
36 | query_params = None
37 | if request.args:
38 | query_params = {}
39 | for key, val in request.args.items():
40 | query_params[key] = val
41 | post_data = request.json if request.mimetype == "application/json" else None
42 | rest_request = APIRequest(
43 | headers=headers,
44 | path=request.path,
45 | method=request.method,
46 | query_params=query_params,
47 | post_data=post_data,
48 | )
49 |
50 | print("[DEBUG] Received POST data:", post_data)
51 | result = evaluator.run(
52 | eval_type=EvalTypeRest,
53 | rest_request=rest_request,
54 | )
55 | formatter.print(result=result)
56 |
57 | return jsonpickle.encode(result, make_refs=False)
58 |
59 |
60 | if __name__ == "__main__":
61 | app.run(debug=True)
62 |
--------------------------------------------------------------------------------
/ansible_policy/policybook/rego_model.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 | from typing import List
3 | import json
4 |
5 |
6 | @dataclass
7 | class RegoFunc:
8 | name: str = ""
9 | body: str = ""
10 | util_funcs: List[str] = field(default_factory=list)
11 |
12 |
13 | @dataclass
14 | class RegoPolicy:
15 | package: str = ""
16 | import_statements: List[str] = field(default_factory=list)
17 | root_condition_func: RegoFunc = field(default_factory=RegoFunc)
18 | condition_funcs: List[RegoFunc] = field(default_factory=list)
19 | util_funcs: List[str] = field(default_factory=list)
20 | action_func: str = ""
21 | vars_declaration: dict = field(default_factory=dict)
22 | tags: List[str] = field(default_factory=list)
23 | target: str = ""
24 |
25 | def to_rego(self):
26 | content = []
27 | content.append(f"package {self.package}")
28 | content.append("\n")
29 | content.extend(self.import_statements)
30 | content.append("\n")
31 | # target
32 | content.append(f'__target__ = "{self.target}"')
33 | # tags
34 | if self.tags:
35 | tags_str = json.dumps(self.tags)
36 | content.append(f"__tags__ = {tags_str}")
37 | content.append("\n")
38 | # vars
39 | if self.vars_declaration:
40 | for var_name, val in self.vars_declaration.items():
41 | val_str = json.dumps(val)
42 | content.append(f"{var_name} = {val_str}")
43 |
44 | # util funcs
45 | for uf in self.util_funcs:
46 | content.append(uf)
47 |
48 | # rules
49 | for rf in self.condition_funcs:
50 | content.append(rf.body)
51 |
52 | content.append(self.action_func)
53 |
54 | content_str = "\n".join(content)
55 | return content_str
56 |
--------------------------------------------------------------------------------
/ansible_policy/policybook/to_ast.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Copyright 2022 Red Hat, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | import argparse
18 | import os
19 | import glob
20 | import yaml
21 | from ansible_policy.policybook.transpiler import PolicyTranspiler
22 |
23 |
24 | if __name__ == "__main__":
25 | parser = argparse.ArgumentParser(description="TODO")
26 | parser.add_argument("-f", "--file", help="")
27 | parser.add_argument("-d", "--dir", help="")
28 | parser.add_argument("-o", "--output", help="")
29 | args = parser.parse_args()
30 |
31 | ansible_policy = args.file
32 | ansible_policy_dir = args.dir
33 | output = args.output
34 |
35 | pt = PolicyTranspiler()
36 | if ansible_policy:
37 | policyset = pt.policybook_to_ast(ansible_policy)
38 | os.makedirs(os.path.dirname(output), exist_ok=True)
39 | with open(output, "w") as f:
40 | f.write(yaml.dump(policyset))
41 |
42 | elif ansible_policy_dir:
43 | path = f"{ansible_policy_dir}/*.yml"
44 | policy_list = glob.glob(path)
45 | for p in policy_list:
46 | out_file = f"{output}/{os.path.basename(p)}"
47 | policyset = pt.policybook_to_ast(p)
48 | os.makedirs(os.path.dirname(out_file), exist_ok=True)
49 | with open(out_file, "w") as f:
50 | f.write(yaml.dump(policyset))
51 |
--------------------------------------------------------------------------------
/ansible_policy/rego/utils.rego:
--------------------------------------------------------------------------------
1 | # util functions that can be imported by rego policies executed from ansible-policy
2 |
3 | package ansible_policy
4 |
5 | import future.keywords.if
6 |
7 |
8 | has_key(x, key) if { _ = x[key] }
9 |
10 | # trial1: when task.module is FQCN
11 | get_module_fqcn(task) := fqcn if {
12 | has_key(data.galaxy.modules, task.module)
13 | fqcn := data.galaxy.modules[task.module].fqcn
14 | }
15 |
16 | # trial2: when task.module is a valid short name
17 | get_module_fqcn(task) := fqcn if {
18 | not has_key(data.galaxy.modules, task.module)
19 | has_key(data.galaxy.module_name_mappings, task.module)
20 | fqcn := data.galaxy.module_name_mappings[task.module][0]
21 | }
22 |
23 | request_http_data_source(url) := ext_data if {
24 | resp := http.send({
25 | "method": "get",
26 | "headers": {
27 | "Content-Type": "application/json"
28 | },
29 | "url": url
30 | })
31 | ext_data := resp.body
32 | }
33 |
34 | _find_playbook_by_task(task) := playbook_key if {
35 | playbook := input._agk.playbooks[_]
36 | current_task = playbook.tasks[_]
37 | current_task.key == task.key
38 | playbook_key := playbook.key
39 | }
40 |
41 | _find_taskfile_by_task(task) := taskfile_key if {
42 | taskfile := input._agk.taskfiles[_]
43 | current_task = taskfile.tasks[_]
44 | current_task.key == task.key
45 | taskfile_key := taskfile.key
46 | }
47 |
48 | _find_entrypoint_by_task(task) := playbook_key if {
49 | playbook_key := _find_playbook_by_task(task)
50 | playbook_key
51 | }
52 |
53 | _find_entrypoint_by_task(task) := taskfile_key if {
54 | taskfile_key := _find_taskfile_by_task(task)
55 | taskfile_key
56 | }
57 |
58 | resolve_var(ref, task) := var_value if {
59 | var_name_tmp1 := replace(ref, "{{", "")
60 | var_name_tmp2 := replace(var_name_tmp1, "}}", "")
61 | var_name := replace(var_name_tmp2, " ", "")
62 | entrypoint_key := _find_entrypoint_by_task(task)
63 | variables := input._agk.variables[entrypoint_key]
64 | var_value := variables[var_name]
65 | }
66 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools", "setuptools-scm"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "ansible-policy-eval"
7 | description = "My package description"
8 | readme = "README.rst"
9 | requires-python = ">=3.11"
10 | keywords = ["one", "two"]
11 | license = {text = "Apache License 2.0"}
12 | classifiers = [
13 | "Programming Language :: Python :: 3",
14 | ]
15 | dependencies = [
16 | "ansible-core>=2.17.1",
17 | "ansible-content-capture@git+https://github.com/ansible/ansible-content-capture",
18 | "ansible-rulebook>=1.0.4",
19 | "ansible-runner>=2.4.0",
20 |
21 | "aiohttp>=3.9.5",
22 | "aiosignal>=1.3.1",
23 | "attrs>=23.2.0",
24 | "certifi>=2024.6.2",
25 | "cffi>=1.16.0",
26 | "charset-normalizer>=3.3.2",
27 | "cryptography>=42.0.8",
28 | "docutils>=0.21.2",
29 | "dpath>=2.2.0",
30 | "drools-jpy>=0.3.8",
31 | "filelock>=3.15.1",
32 | "frozenlist>=1.4.1",
33 | "gitdb>=4.0.11",
34 | "idna>=3.7",
35 | "janus>=1.0.0",
36 | "Jinja2>=3.1.4",
37 | "joblib>=1.4.2",
38 | "jpy>=0.17.0",
39 | "jsonpickle>=3.2.1",
40 | "jsonschema>=4.22.0",
41 | "jsonschema-specifications>=2023.12.1",
42 | "lockfile>=0.12.2",
43 | "MarkupSafe>=2.1.5",
44 | "multidict>=6.0.5",
45 | "packaging>=24.1",
46 | "pexpect>=4.9.0",
47 | "ptyprocess>=0.7.0",
48 | "pycparser>=2.22",
49 | "pyparsing>=3.1.2",
50 | "python-daemon>=3.0.1",
51 | "PyYAML>=6.0.1",
52 | "rapidfuzz>=3.9.3",
53 | "referencing>=0.35.1",
54 | "requests>=2.32.3",
55 | "resolvelib>=1.0.1",
56 | "rpds-py>=0.18.1",
57 | "ruamel.yaml>=0.18.6",
58 | "ruamel.yaml.clib>=0.2.8",
59 | "smmap>=5.0.1",
60 | "tabulate>=0.9.0",
61 | "typing_extensions>=4.12.2",
62 | "urllib3>=2.2.2",
63 | "watchdog>=4.0.1",
64 | "websockets>=12.0",
65 | "yarl>=1.9.4",
66 | ]
67 |
68 | dynamic = ["version"]
69 |
70 | [tool.setuptools.dynamic]
71 | version = {attr = "ansible_policy.__version__.__version__"}
72 |
73 | [project.scripts]
74 | ansible-policy = "ansible_policy.eval_policy:main"
75 |
76 | [tool.setuptools]
77 | py-modules = ["ansible_policy"]
78 | packages = ["ansible_policy"]
79 |
80 | [tool.black]
81 | line-length = 150
82 | include = '\.pyi?$'
83 | exclude = '''
84 | /(
85 | \.git
86 | | \.hg
87 | | \.mypy_cache
88 | | \.tox
89 | | \.venv
90 | | _build
91 | | buck-out
92 | | build
93 | | dist
94 | )/
95 | '''
96 |
97 | [tool.flake8]
98 | ignore = "E203, W503,"
99 | max-line-length = 150
100 |
--------------------------------------------------------------------------------
/ansible_policy/eval_policy.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | import argparse
4 | from ansible_policy.models import (
5 | PolicyEvaluator,
6 | ResultFormatter,
7 | supported_formats,
8 | )
9 |
10 |
11 | def eval_policy(
12 | eval_type: str,
13 | project_dir: str = None,
14 | target_data: dict = None,
15 | variables_path: str = None,
16 | config_path: str = None,
17 | policy_dir: str = None,
18 | external_data_path: str = None,
19 | ):
20 |
21 | if not external_data_path:
22 | _external_data_path = os.path.join(os.path.dirname(__file__), "galaxy_data.json")
23 | if os.path.exists(_external_data_path):
24 | external_data_path = _external_data_path
25 |
26 | evaluator = PolicyEvaluator(config_path=config_path, policy_dir=policy_dir)
27 | result = evaluator.run(
28 | eval_type=eval_type,
29 | project_dir=project_dir,
30 | target_data=target_data,
31 | external_data_path=external_data_path,
32 | variables_path=variables_path,
33 | )
34 | return result
35 |
36 |
37 | def main():
38 | parser = argparse.ArgumentParser(description="TODO")
39 | parser.add_argument("-t", "--type", default="project", help="policy evaluation type (`jobdata`, `project`, `rest` or `event`)")
40 | parser.add_argument("-p", "--project-dir", help="target project directory for project type")
41 | # The `--event-file` argument here is just for debugging
42 | # Actual events should be handled by `event_handler.py` instead
43 | parser.add_argument("-j", "--json-file", help="target JSON file (only for jobdata/rest/event type evaluation")
44 | parser.add_argument("-v", "--variables", default="", help="filepath to variables JSON data")
45 | parser.add_argument("-c", "--config", help="path to config file which configures policies to be evaluated")
46 | parser.add_argument("--policy-dir", help="path to a directory containing policies to be evaluated")
47 | parser.add_argument("--external-data", default="", help="filepath to external data like knowledge base data")
48 | parser.add_argument("-f", "--format", default="plain", help="output format (`plain` or `json`, default to `plain`)")
49 | args = parser.parse_args()
50 |
51 | if args.format not in supported_formats:
52 | raise ValueError(f"The format type `{args.format}` is not supported; it must be one of {supported_formats}")
53 |
54 | target_data = None
55 | if args.json_file:
56 | with open(args.json_file, "r") as f:
57 | target_data = json.load(f)
58 |
59 | result = eval_policy(
60 | eval_type=args.type,
61 | project_dir=args.project_dir,
62 | target_data=target_data,
63 | variables_path=args.variables,
64 | config_path=args.config,
65 | policy_dir=args.policy_dir,
66 | external_data_path=args.external_data,
67 | )
68 | ResultFormatter(format_type=args.format, base_dir=os.getcwd()).print(result=result)
69 |
70 |
71 | if __name__ == "__main__":
72 | main()
73 |
--------------------------------------------------------------------------------
/ansible_policy/policybook/policy_parser.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict, List
2 | import ansible_policy.policybook.policybook_models as pm
3 | from ansible_policy.policybook.condition_parser import (
4 | parse_condition as parse_condition_value,
5 | )
6 |
7 | VALID_ACTIONS = ["allow", "deny", "info", "warn", "ignore"]
8 |
9 |
10 | def parse_hosts(hosts):
11 | if isinstance(hosts, str):
12 | return [hosts]
13 | elif isinstance(hosts, list):
14 | return hosts
15 | else:
16 | raise Exception(f"Unsupported hosts value {hosts}")
17 |
18 |
19 | def parse_vars(vars):
20 | if isinstance(vars, dict):
21 | return vars
22 | else:
23 | raise Exception(f"unsupported vars value {vars}. vars should be defined by dict.")
24 |
25 |
26 | def parse_policy_sets(policy_sets: Dict) -> List[pm.PolicySet]:
27 | policy_set_list = []
28 | policyset_names = []
29 | for policy_set in policy_sets:
30 | name = policy_set.get("name")
31 | if name is None:
32 | raise Exception("Policyset name not provided")
33 |
34 | name = name.strip()
35 | if name == "":
36 | raise Exception("Policyset name cannot be an empty string")
37 |
38 | if name in policyset_names:
39 | raise Exception(f"Policy with name: {name} defined multiple times")
40 |
41 | policyset_names.append(name)
42 |
43 | policy_set_list.append(
44 | pm.PolicySet(
45 | name=name,
46 | hosts=parse_hosts(policy_set["hosts"]),
47 | vars=parse_vars(policy_set.get("vars", {})),
48 | policies=parse_policies(policy_set.get("policies", {}), policy_set.get("vars", {})),
49 | match_multiple_policies=policy_set.get("match_multiple_policies", False),
50 | )
51 | )
52 | return policy_set_list
53 |
54 |
55 | def parse_policies(policies: Dict, vars: Dict) -> List[pm.Policy]:
56 | pol_list = []
57 | pol_names = []
58 |
59 | for pol in policies:
60 | name = pol.get("name")
61 | if name is None:
62 | raise Exception("Policy name not provided")
63 |
64 | if name == "":
65 | raise Exception("Policy name cannot be an empty string")
66 |
67 | if name in pol_names:
68 | raise Exception(f"Policy with name {name} defined multiple times")
69 |
70 | target = pol.get("target")
71 | if target is None:
72 | raise Exception("Policy target not provided")
73 |
74 | if target == "":
75 | raise Exception("Policy target cannot be an empty string")
76 |
77 | tags = pol.get("tags", [])
78 |
79 | pol_names.append(name)
80 |
81 | parsed_pol = pm.Policy(
82 | name=name,
83 | condition=parse_condition(pol["condition"], vars),
84 | actions=parse_actions(pol),
85 | enabled=pol.get("enabled", True),
86 | tags=tags,
87 | target=target,
88 | )
89 | if parsed_pol.enabled:
90 | pol_list.append(parsed_pol)
91 |
92 | return pol_list
93 |
94 |
95 | def parse_condition(condition: Any, vars: Dict) -> pm.Condition:
96 | if isinstance(condition, str):
97 | return pm.Condition("all", [parse_condition_value(condition, vars)])
98 | elif isinstance(condition, bool):
99 | return pm.Condition("all", [parse_condition_value(str(condition), vars)])
100 | elif isinstance(condition, dict):
101 | keys = list(condition.keys())
102 | if len(condition) == 1 and keys[0] in ["any", "all", "not_all"]:
103 | when = keys[0]
104 | return pm.Condition(
105 | when,
106 | [parse_condition_value(str(c), vars) for c in condition[when]],
107 | )
108 | else:
109 | raise Exception(f"Condition should have one of any, all, not_all: {condition}")
110 | else:
111 | raise Exception(f"Unsupported condition {condition}")
112 |
113 |
114 | def parse_actions(rule: Dict) -> List[pm.Action]:
115 | actions = []
116 | if "actions" in rule:
117 | for action in rule["actions"]:
118 | actions.append(parse_action(action))
119 | elif "action" in rule:
120 | actions.append(parse_action(rule["action"]))
121 |
122 | return actions
123 |
124 |
125 | def parse_action(action: Dict) -> pm.Action:
126 | action_name = list(action.keys())[0]
127 | if action_name not in VALID_ACTIONS:
128 | raise Exception(f"Unsupported action {action_name}. supported actions are {VALID_ACTIONS}")
129 | if action[action_name]:
130 | action_args = {k: v for k, v in action[action_name].items()}
131 | else:
132 | action_args = {}
133 | return pm.Action(action=action_name, action_args=action_args)
134 |
--------------------------------------------------------------------------------
/ansible_policy/policybook/image/flow.drawio:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/tests/unit/test_policybook_e2e.py:
--------------------------------------------------------------------------------
1 | import json
2 | import subprocess
3 | import os
4 | import glob
5 | from ansible_policy.policybook.transpiler import PolicyTranspiler
6 |
7 | INPUT_PASS = "input_pass.json"
8 | INPUT_FAIL = "input_fail.json"
9 | POLICYBOOK = "policybook.yml"
10 |
11 |
12 | def get_rego_main_package_name(rego_path: str):
13 | pkg_name = ""
14 | with open(rego_path, "r") as file:
15 | prefix = "package "
16 | for line in file:
17 | _line = line.strip()
18 | if _line.startswith(prefix):
19 | pkg_name = _line[len(prefix) :]
20 | break
21 | return pkg_name
22 |
23 |
24 | def run_rego(rego_path, input_path):
25 | rego_pkg_name = get_rego_main_package_name(rego_path)
26 | cmd_str = f"opa eval --data {rego_path} --input {input_path} 'data.{rego_pkg_name}'"
27 | proc = subprocess.run(
28 | cmd_str,
29 | shell=True,
30 | stdout=subprocess.PIPE,
31 | stderr=subprocess.PIPE,
32 | text=True,
33 | )
34 | if proc.returncode != 0:
35 | error = f"failed to run `opa eval` command; error details:\nSTDOUT: {proc.stdout}\nSTDERR: {proc.stderr}"
36 | raise ValueError(error)
37 | result = json.loads(proc.stdout)
38 | return result
39 |
40 |
41 | def get_eval_result(output, action="allow"):
42 | result = output.get("result", [])
43 | if not result:
44 | return ValueError("no result found")
45 | expressions = result[0].get("expressions", [])
46 | if expressions:
47 | return expressions[0].get("value", {}).get(action)
48 | else:
49 | return ValueError("no result found")
50 |
51 |
52 | transpiler = PolicyTranspiler()
53 | test_source_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "integration")
54 |
55 |
56 | class TestTranspiler:
57 | def get_test_result(self, target_dir):
58 | target_dir = os.path.join(test_source_dir, "in_operator")
59 | input_policybook = os.path.join(target_dir, POLICYBOOK)
60 | input_pass = os.path.join(target_dir, INPUT_PASS)
61 | input_fail = os.path.join(target_dir, INPUT_FAIL)
62 | transpiler.run(input_policybook, target_dir)
63 | pattern = f"{target_dir}/**/*.rego"
64 | _found = glob.glob(pattern, recursive=True)
65 | rego = _found[0]
66 | result = get_eval_result(run_rego(rego, input_pass))
67 | assert result
68 | result = get_eval_result(run_rego(rego, input_fail))
69 | assert not result
70 |
71 | def test_in_operator(self):
72 | target_dir = os.path.join(test_source_dir, "in_operator")
73 | self.get_test_result(target_dir)
74 |
75 | def test_not_in_operator(self):
76 | target_dir = os.path.join(test_source_dir, "not_in_operator")
77 | self.get_test_result(target_dir)
78 |
79 | def test_int_equal_operator(self):
80 | target_dir = os.path.join(test_source_dir, "int_equal_operator")
81 | self.get_test_result(target_dir)
82 |
83 | def test_str_equal_operator(self):
84 | target_dir = os.path.join(test_source_dir, "str_equal_operator")
85 | self.get_test_result(target_dir)
86 |
87 | def test_null_not_equal_operator(self):
88 | target_dir = os.path.join(test_source_dir, "null_not_equal_operator")
89 | self.get_test_result(target_dir)
90 |
91 | def test_item_in_list_operator(self):
92 | target_dir = os.path.join(test_source_dir, "item_in_list_operator")
93 | self.get_test_result(target_dir)
94 |
95 | def test_item_not_in_list_operator(self):
96 | target_dir = os.path.join(test_source_dir, "item_not_in_list_operator")
97 | self.get_test_result(target_dir)
98 |
99 | def test_list_contains_operator(self):
100 | target_dir = os.path.join(test_source_dir, "list_contains_operator")
101 | self.get_test_result(target_dir)
102 |
103 | def test_list_not_contains_operator(self):
104 | target_dir = os.path.join(test_source_dir, "list_not_contains_operator")
105 | self.get_test_result(target_dir)
106 |
107 | def test_is_defined_operator(self):
108 | target_dir = os.path.join(test_source_dir, "is_defined_operator")
109 | self.get_test_result(target_dir)
110 |
111 | def test_negate_operator(self):
112 | target_dir = os.path.join(test_source_dir, "negate_operator")
113 | self.get_test_result(target_dir)
114 |
115 | def test_affirm_operator(self):
116 | target_dir = os.path.join(test_source_dir, "affirm_operator")
117 | self.get_test_result(target_dir)
118 |
119 | def test_search_operator(self):
120 | target_dir = os.path.join(test_source_dir, "search_operator")
121 | self.get_test_result(target_dir)
122 |
123 | def test_not_search_operator(self):
124 | target_dir = os.path.join(test_source_dir, "not_search_operator")
125 | self.get_test_result(target_dir)
126 |
127 | def test_match_operator(self):
128 | target_dir = os.path.join(test_source_dir, "match_operator")
129 | self.get_test_result(target_dir)
130 |
131 | def test_not_match_operator(self):
132 | target_dir = os.path.join(test_source_dir, "not_match_operator")
133 | self.get_test_result(target_dir)
134 |
135 | def test_regex_operator(self):
136 | target_dir = os.path.join(test_source_dir, "regex_operator")
137 | self.get_test_result(target_dir)
138 |
139 | def test_not_regex_operator(self):
140 | target_dir = os.path.join(test_source_dir, "not_regex_operator")
141 | self.get_test_result(target_dir)
142 |
143 | def test_select_search_operator(self):
144 | target_dir = os.path.join(test_source_dir, "select_search_operator")
145 | self.get_test_result(target_dir)
146 |
147 | def test_select_compare_operator(self):
148 | target_dir = os.path.join(test_source_dir, "select_compare_operator")
149 | self.get_test_result(target_dir)
150 |
151 | def test_not_select_search_operator(self):
152 | target_dir = os.path.join(test_source_dir, "not_select_search_operator")
153 | self.get_test_result(target_dir)
154 |
155 | def test_not_select_compare_operator(self):
156 | target_dir = os.path.join(test_source_dir, "not_select_compare_operator")
157 | self.get_test_result(target_dir)
158 |
159 | def test_selectattr_search_operator(self):
160 | target_dir = os.path.join(test_source_dir, "selectattr_search_operator")
161 | self.get_test_result(target_dir)
162 |
163 | def test_selectattr_compare_operator(self):
164 | target_dir = os.path.join(test_source_dir, "selectattr_compare_operator")
165 | self.get_test_result(target_dir)
166 |
167 | def test_not_selectattr_search_operator(self):
168 | target_dir = os.path.join(test_source_dir, "not_selectattr_search_operator")
169 | self.get_test_result(target_dir)
170 |
171 | def test_not_selectattr_compare_operator(self):
172 | target_dir = os.path.join(test_source_dir, "not_selectattr_compare_operator")
173 | self.get_test_result(target_dir)
174 |
175 | def test_multi_condition_any(self):
176 | target_dir = os.path.join(test_source_dir, "multi_condition_any")
177 | self.get_test_result(target_dir)
178 |
179 | def test_multi_condition_all(self):
180 | target_dir = os.path.join(test_source_dir, "multi_condition_all")
181 | self.get_test_result(target_dir)
182 |
183 | def test_multi_condition_notall(self):
184 | target_dir = os.path.join(test_source_dir, "multi_condition_notall")
185 | self.get_test_result(target_dir)
186 |
--------------------------------------------------------------------------------
/ansible_policy/policybook/README.md:
--------------------------------------------------------------------------------
1 | # Policybooks
2 |
3 | Policybooks contain a list of policysets. Each policyset within a policybook
4 | should have a unique name.
5 |
6 | ### Policysets
7 |
8 | A policyset has the following properties:
9 |
10 | | Name | Description | Required |
11 | |--------------------|----------------------------------------------------------------------------------|----------|
12 | | name | The name to identify the policyset. Each policyset must have a unique name across the policybook. | Yes |
13 | | policies | The list of one or more policies. | Yes |
14 | | hosts | Similar to hosts in an Ansible playbook | Yes |
15 | | vars | Variables used in policy | No |
16 |
17 | The example of policyset is below.
18 |
19 | ```yaml
20 | - name: Check for mysql package installation
21 | hosts: localhost
22 | vars:
23 | allowed_packages:
24 | - "mysql-server"
25 | policies:
26 | - name: Check for package name
27 | target: task
28 | condition: input["ansible.builtin.package"].name not in allowed_packages
29 | actions:
30 | - deny:
31 | msg: The package {{ input["ansible.builtin.package"].name }} is not allowed, allowed packages are one of {{ allowed_packages }}
32 | tags:
33 | - compliance
34 | ```
35 |
36 | ### Policies
37 |
38 | The policies node in a policyset contains a list of policies.
39 | The policy decides to run actions by evaluating the condition(s)
40 | that is defined by the policybook author.
41 |
42 | A policy comprises of:
43 |
44 | | Name | Description | Required |
45 | |-----------|-------------------------------------------------------------------------------------------------------|----------|
46 | | name | The name is a string to identify the policy. This field is mandatory. Each policy in a policieset must have a unique name across the policybook. You can use Jinja2 substitution in the name. | Yes |
47 | | condition | See [conditions](#condition) | Yes |
48 | | actions | Specify an action from `deny`, `allow`, `info`, `warn` or `ignore` | Yes |
49 | | target | Specify the target to evaluate by this policy. Target should be `task`, `play` or `role`. | Yes |
50 | | enabled | If the policy should be enabled, default is true. Can be set to false to disable a policy. | No |
51 | | tags | List of tags used in ansible policy | No |
52 |
53 | ### Conditions
54 |
55 |
56 | A condition can contain
57 | * One condition
58 | * Multiple conditions where all of them have to match
59 | * Multiple conditions where any one of them has to match
60 | * Multiple conditions where not all one of them have to match
61 |
62 | **Supported Operators**
63 |
64 | Conditions support the following operators:
65 |
66 | | Name | Description |
67 | |----------------------|--------------------------------------------------------------------------------------------------------------------|
68 | | == | The equality operator for strings and numbers |
69 | | != | The non-equality operator for strings and numbers |
70 | | and | The conjunctive AND, for making compound expressions |
71 | | or | The disjunctive OR |
72 | | in | To check if a value in the left-hand side exists in the list on the right-hand side |
73 | | not in | To check if a value in the left-hand side does not exist in the list on the right-hand side |
74 | | contains | To check if the list on the left-hand side contains the value on the right-hand side |
75 | | not contains | To check if the list on the left-hand side does not contain the value on the right-hand side |
76 | | has key | To check if a value on the right-hand side exists as a key in dict on the left-hand side |
77 | | lacks key | To check if a value on the right-hand side does not exists as a key in dict on the left-hand side |
78 | | is defined | To check if a variable is defined |
79 | | is not defined | To check if a variable is not defined |
80 |
81 |
85 |
88 |
100 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > **Note:** This repository is in prototype phase and under active development with subject to breaking changes.
2 |
3 | # Ansible Policy
4 |
5 | Ansible Policy is a prototype implementation which allows us to define and set constraints to the Ansible project in OPA Rego language. The key features of Ansible Policy are
6 | - Ansible project is auto scanned as objects and accessible from OPA policy (using ARI project scanning internally).
7 | - Ansible knowledge base acquired from external sources such as Galaxy can be used from OPA policy.
8 | - Multiple policy resolution from the scanned Ansible content.
9 | - Policies can be packed as an ansible collection.
10 | - Users can define policy in YAML format (policybook). It can be auto-transformed with PolicyTranspiler.
11 |
12 |
13 |
14 | ## Getting started
15 |
16 | ### 1. Install `opa` command
17 |
18 | refer to OPA [document](https://github.com/open-policy-agent/opa#want-to-download-opa)
19 |
20 | ### 2. git clone
21 |
22 | clone this repository
23 |
24 | ### 3. Install `ansbile-policy` command
25 |
26 | Ansible Policy requires Python `3.11 or later`. Please install it before this step.
27 |
28 | The following command installs `ansible-policy` command and dependency packages.
29 |
30 | ```bash
31 | $ cd ansible-policy
32 | $ pip install -e .
33 | ```
34 |
35 | ### 4. Prepare Policybook
36 | As examples, the following policybooks can be found in the `examples/check_project/policies` directory.
37 |
38 | - `check_package_policy` [yml](./examples/check_project/policies/check_pkg.yml): Check if only authorized packages are installed.
39 | - `check_collection_policy` [yml](./examples/check_project/policies/check_collection.yml): Check if only authorized collections are used
40 | - `check_become_policy` [yml](./examples/check_project/policies/check_become.yml): check if `become: true` is used and check if only `trusted user` is used
41 |
42 | ansible-policy transpile these policybooks into OPA policy automatically and evaluate the policies.
43 |
44 | See this [doc](./ansible_policy/policybook/README.md) about Policybook specification.
45 |
46 |
47 | ### 5. Running policy evaluation on a playbook
48 |
49 | [The example playbook](examples/check_project/playbook.yml) has some tasks that violate the 3 policies above.
50 |
51 | ansible-policy can report these violations like the following.
52 |
53 | ```bash
54 | $ ansible-policy -p examples/check_project/playbook.yml --policy-dir examples/check_project/policies
55 | ```
56 |
57 |
58 |
59 |
60 | From the result, you can see the details on violations.
61 |
62 | - [The task "Install Unauthorized App"](examples/check_project/playbook.yml#L32) is installing a package `unauthorized-app` with a root permission by using `become: true`. This is not listed in the allowed packages defined in the policybook [check_package_policy]((examples/check_project/policies/check_pkg.yml)). Also the privilege escalation is detected by the policybook [check_become_policy](examples/check_project/policies/check_become.yml).
63 |
64 | - [The task "Set MySQL root password"](examples/check_project/playbook.yml#L38) is using a collection `community.mysql` which is not in the allowed list, and this is detected by the policybook [check_collection_policy](examples/check_project/policies/check_collection.yml).
65 |
66 |
67 | Alternatively, you can output the evaluation result in a JSON format.
68 |
69 | ```bash
70 | $ ansible-policy -p examples/check_project/playbook.yml --policy-dir examples/check_project/policies --format json > agk-result.json
71 | ```
72 |
73 | Then you would get the JSON file like the following.
74 |
75 |
76 |
77 | The `summary` section in the JSON is a summary of the evaluation results such as the number of total policies, the number of policies with one or more violations, total files and OK/NG for each of them.
78 |
79 | For example, you can get a summary about files by `jq` command like this.
80 |
81 | ```bash
82 | $ cat agk-result.json | jq .summary.files
83 | {
84 | "total": 1,
85 | "validated": 0,
86 | "not_validated": 1,
87 | "list": [
88 | "examples/check_project/playbook.yml"
89 | ]
90 | }
91 | ```
92 |
93 | The `files` section contains the details for each file evaluation result.
94 |
95 | Each file result has results per policy, and a policy result contains multiple results for policy evaluation targets like tasks or plays.
96 |
97 | For example, you can use this detailed data by the following commands.
98 |
99 | ```bash
100 | # get overall result for a file
101 | $ cat /tmp/agk-result.json | jq .files[0].violation
102 | true
103 |
104 | # get overall result for the second policy for the file
105 | $ cat /tmp/agk-result.json | jq .files[0].policies[1].violation
106 | true
107 |
108 | # get an policy result for the second task in the file for the second policy
109 | cat /tmp/agk-result.json | jq .files[0].policies[1].targets[1]
110 | {
111 | "name": "Install nginx [installing unauthorized pkg]",
112 | "lines": {
113 | "begin": 31,
114 | "end": 36
115 | },
116 | "validated": false,
117 | "message": "privilage escalation is detected. allowed users are one of [\"trusted_user\"]\n"
118 | }
119 | ```
120 |
121 | ### 6. (OPTIONAL) Prepare your configuration file
122 |
123 | Instead of specifying the policy directory, you can define a configuration for ansible-policy like the following.
124 |
125 | ```ini
126 | [policy]
127 | default disabled
128 | policies.org.compliance tag=compliance enabled
129 |
130 | [source]
131 | policies.org.compliance = examples/check_project # org-wide compliance policy
132 | ```
133 |
134 | `policy` field is a configuration like iptable to enable/disable installed policies. Users can use tag for configuring this in detail.
135 |
136 | `source` field is a list of module packages and their source like ansible-galaxy or local directory. ansible-policy installs policies based on this configuration.
137 |
138 | The example above is configured to enable the 3 policies in step 4.
139 |
140 | You can check [the example config file](examples/ansible-policy.cfg) as reference.
141 |
142 |
143 | ## Policy check for Event streams
144 |
145 | Ansible Policy supports policy checks for runtime events output from `ansible-runner`.
146 |
147 | ansible-runner generates the events while playbook execution. For example, "playbook_on_start" is an event at the start of the playbook execution, and "runner_on_ok" is the one for a task that is completed successfully.
148 |
149 | [event_handler.py](examples/check_event/event_handler.py) is a reference implementation to handle these runner events that are input by standard input and it outputs policy evaluation results to standard output like the following image.
150 |
151 |
152 |
153 |
154 | In the example above, a policybook [here](examples/check_event/policies/check_changed_event.yml) is used.
155 |
156 | An event JSON data and its attributes are accessible by `input.xxxx` in the policybook condition field.
157 |
158 | For example, the `changed` status of a task is `input.event_data.changed`, so the example policy is checking if `input.event_data.changed` as one of the conditions.
159 |
160 | You can implement your policy conditions by using `input.xxxx`.
161 |
162 | Also, you can use `event_handler.py`, in particular, the code block below to implement your event handler depending on the way to receive events.
163 |
164 | ```python
165 | evaluator = PolicyEvaluator(policy_dir="/path/to/your_policy_dir")
166 | formatter = ResultFormatter(format_type="event_stream")
167 | # `load_event()` here should be replaced with your generator to read a single event
168 | for event in load_event():
169 | result = evaluator.run(
170 | eval_type="event",
171 | event=event,
172 | )
173 | formatter.print(result)
174 | ```
175 |
--------------------------------------------------------------------------------
/ansible_policy/policybook/json_generator.py:
--------------------------------------------------------------------------------
1 | # Copyright 2022 Red Hat, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Generate condition AST from Ansible condition."""
16 |
17 | from typing import List
18 |
19 | from ansible_rulebook.condition_types import (
20 | Boolean,
21 | Condition,
22 | ConditionTypes,
23 | Float,
24 | Identifier,
25 | Integer,
26 | KeywordValue,
27 | NegateExpression,
28 | Null,
29 | OperatorExpression,
30 | SearchType,
31 | SelectattrType,
32 | SelectType,
33 | String,
34 | )
35 | from ansible_rulebook.exception import (
36 | InvalidAssignmentException,
37 | )
38 | from ansible_policy.policybook.policybook_models import (
39 | Action,
40 | Condition as RuleCondition,
41 | Policy,
42 | PolicySet,
43 | )
44 |
45 |
46 | OPERATOR_MNEMONIC = {
47 | "!=": "NotEqualsExpression",
48 | "==": "EqualsExpression",
49 | "and": "AndExpression",
50 | "or": "OrExpression",
51 | ">": "GreaterThanExpression",
52 | "<": "LessThanExpression",
53 | ">=": "GreaterThanOrEqualToExpression",
54 | "<=": "LessThanOrEqualToExpression",
55 | "in": "ItemInListExpression",
56 | "not in": "ItemNotInListExpression",
57 | "contains": "ListContainsItemExpression",
58 | "not contains": "ListNotContainsItemExpression",
59 | "has key": "KeyInDictExpression",
60 | "lacks key": "KeyNotInDictExpression",
61 | "is not defined": "IsNotDefinedExpression",
62 | "is defined": "IsDefinedExpression",
63 | }
64 |
65 |
66 | def visit_condition(parsed_condition: ConditionTypes):
67 | """Visit the condition and generate the AST."""
68 | if isinstance(parsed_condition, list):
69 | return [visit_condition(c) for c in parsed_condition]
70 | elif isinstance(parsed_condition, Condition):
71 | return visit_condition(parsed_condition.value)
72 | elif isinstance(parsed_condition, Boolean):
73 | return {"Boolean": True} if parsed_condition.value == "true" else {"Boolean": False}
74 | elif isinstance(parsed_condition, Identifier):
75 | if parsed_condition.value.startswith("input"):
76 | return {"Input": parsed_condition.value}
77 | else:
78 | return {"Variable": parsed_condition.value}
79 | elif isinstance(parsed_condition, String):
80 | return {"String": parsed_condition.value}
81 | elif isinstance(parsed_condition, Null):
82 | return {"NullType": None}
83 | elif isinstance(parsed_condition, Integer):
84 | return {"Integer": parsed_condition.value}
85 | elif isinstance(parsed_condition, Float):
86 | return {"Float": parsed_condition.value}
87 | elif isinstance(parsed_condition, SearchType):
88 | data = dict(
89 | kind=visit_condition(parsed_condition.kind),
90 | pattern=visit_condition(parsed_condition.pattern),
91 | )
92 | if parsed_condition.options:
93 | data["options"] = [visit_condition(v) for v in parsed_condition.options]
94 | return {"SearchType": data}
95 | elif isinstance(parsed_condition, SelectattrType):
96 | return dict(
97 | key=visit_condition(parsed_condition.key),
98 | operator=visit_condition(parsed_condition.operator),
99 | value=visit_condition(parsed_condition.value),
100 | )
101 | elif isinstance(parsed_condition, SelectType):
102 | return dict(
103 | operator=visit_condition(parsed_condition.operator),
104 | value=visit_condition(parsed_condition.value),
105 | )
106 | elif isinstance(parsed_condition, KeywordValue):
107 | return dict(
108 | name=visit_condition(parsed_condition.name),
109 | value=visit_condition(parsed_condition.value),
110 | )
111 | elif isinstance(parsed_condition, OperatorExpression):
112 | if parsed_condition.operator == "<<":
113 | validate_assignment_expression(parsed_condition.left.value)
114 |
115 | if parsed_condition.operator in OPERATOR_MNEMONIC:
116 | return create_binary_node(
117 | OPERATOR_MNEMONIC[parsed_condition.operator],
118 | parsed_condition,
119 | )
120 | elif parsed_condition.operator == "is":
121 | if isinstance(parsed_condition.right, String):
122 | if parsed_condition.right.value == "defined":
123 | return {"IsDefinedExpression": visit_condition(parsed_condition.left)}
124 | elif isinstance(parsed_condition.right, SearchType):
125 | return create_binary_node("SearchMatchesExpression", parsed_condition)
126 | elif isinstance(parsed_condition.right, SelectattrType):
127 | return create_binary_node("SelectAttrExpression", parsed_condition)
128 | elif isinstance(parsed_condition.right, SelectType):
129 | return create_binary_node("SelectExpression", parsed_condition)
130 | elif parsed_condition.operator == "is not":
131 | if isinstance(parsed_condition.right, String):
132 | if parsed_condition.right.value == "defined":
133 | return {"IsNotDefinedExpression": visit_condition(parsed_condition.left)}
134 | elif isinstance(parsed_condition.right, SearchType):
135 | return create_binary_node("SearchNotMatchesExpression", parsed_condition)
136 | elif isinstance(parsed_condition.right, SelectattrType):
137 | return create_binary_node("SelectAttrNotExpression", parsed_condition)
138 | elif isinstance(parsed_condition.right, SelectType):
139 | return create_binary_node("SelectNotExpression", parsed_condition)
140 | else:
141 | raise Exception(f"Unhandled token {parsed_condition}")
142 | elif isinstance(parsed_condition, NegateExpression):
143 | return {"NegateExpression": visit_condition(parsed_condition.value)}
144 | else:
145 | raise Exception(f"Unhandled token {parsed_condition}")
146 |
147 |
148 | def create_binary_node(name, parsed_condition):
149 | return {
150 | name: {
151 | "lhs": visit_condition(parsed_condition.left),
152 | "rhs": visit_condition(parsed_condition.right),
153 | }
154 | }
155 |
156 |
157 | def visit_policy(parsed_policy: Policy):
158 | data = {
159 | "name": parsed_policy.name,
160 | "target": parsed_policy.target,
161 | "condition": generate_condition(parsed_policy.condition),
162 | "actions": visit_actions(parsed_policy.actions),
163 | "enabled": parsed_policy.enabled,
164 | "tags": parsed_policy.tags,
165 | }
166 |
167 | return {"Policy": data}
168 |
169 |
170 | def visit_actions(actions: List[Action]):
171 | return [visit_action(a) for a in actions]
172 |
173 |
174 | def visit_action(parsed_action: Action):
175 | return {
176 | "Action": {
177 | "action": parsed_action.action,
178 | "action_args": parsed_action.action_args,
179 | }
180 | }
181 |
182 |
183 | def generate_condition(ansible_condition: RuleCondition):
184 | """Generate the condition AST."""
185 | condition = visit_condition(ansible_condition.value)
186 | if ansible_condition.when == "any":
187 | data = {"AnyCondition": condition}
188 | elif ansible_condition.when == "all":
189 | data = {"AllCondition": condition}
190 | elif ansible_condition.when == "not_all":
191 | data = {"NotAllCondition": condition}
192 | else:
193 | data = {"AllCondition": condition}
194 |
195 | return data
196 |
197 |
198 | def visit_policyset(policyset: PolicySet):
199 | """Generate JSON compatible rules."""
200 | data = {
201 | "name": policyset.name,
202 | "hosts": policyset.hosts,
203 | "policies": [visit_policy(pol) for pol in policyset.policies],
204 | "vars": policyset.vars,
205 | }
206 |
207 | return {"PolicySet": data}
208 |
209 |
210 | def generate_dict_policysets(policysets: List[PolicySet]):
211 | """Generate JSON compatible policysets."""
212 | return [visit_policyset(policyset) for policyset in policysets]
213 |
214 |
215 | def validate_assignment_expression(value):
216 | tokens = value.split(".")
217 | if len(tokens) != 2:
218 | msg = (
219 | f"Assignment variable: {value} is invalid."
220 | + "Valid values start with events or facts. e.g events.var1 "
221 | + "or facts.var1 "
222 | + "Where var1 can only contain alpha numeric and _ charachters"
223 | )
224 | raise InvalidAssignmentException(msg)
225 |
226 | if tokens[0] not in ["events", "facts"]:
227 | msg = "Only events and facts can be used in assignment. " + f"{value} is invalid."
228 | raise InvalidAssignmentException(msg)
229 |
--------------------------------------------------------------------------------
/ansible_policy/policybook/transpiler.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Copyright 2022 Red Hat, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | import traceback
18 | import yaml
19 | import argparse
20 | import os
21 | import glob
22 | import re
23 | import string
24 | from ansible_policy.policybook.json_generator import generate_dict_policysets
25 | from ansible_policy.policybook.policy_parser import parse_policy_sets, VALID_ACTIONS
26 | from ansible_policy.policybook.rego_model import RegoPolicy, RegoFunc
27 | from ansible_policy.utils import init_logger
28 | from ansible_policy.policybook.expressioin_transpiler import ExpressionTranspiler
29 |
30 | logger = init_logger(__name__, os.getenv("ANSIBLE_GK_LOG_LEVEL", "info"))
31 |
32 | et = ExpressionTranspiler()
33 |
34 | action_func_template = string.Template(
35 | """
36 | ${func_name} = true if {
37 | ${steps}
38 | } else = false
39 | """
40 | )
41 |
42 |
43 | class PolicyTranspiler:
44 | """
45 | PolicyTranspiler transforms a policybook to a Rego policy.
46 | """
47 |
48 | def __init__(self, tmp_dir=None):
49 | self.tmp_dir = tmp_dir
50 |
51 | def run(self, input, outdir):
52 | if "extensions/policy" not in outdir:
53 | outdir = os.path.join(outdir, "extensions/policy")
54 | os.makedirs(outdir, exist_ok=True)
55 | if os.path.isfile(input):
56 | ast = self.policybook_to_ast(input)
57 | self.ast_to_rego(ast, outdir)
58 | elif os.path.isdir(input):
59 | pattern1 = f"{input}/**/policies/**/*.yml"
60 | pattern2 = f"{input}/**/extensions/policy/**/*.yml"
61 | policy_list = []
62 | _found = glob.glob(pattern1, recursive=True)
63 | if _found:
64 | policy_list.extend(_found)
65 | _found = glob.glob(pattern2, recursive=True)
66 | if _found:
67 | policy_list.extend(_found)
68 | if not policy_list:
69 | input_parts = input.split("/")
70 | if "policies" in input_parts or "policy" in input_parts:
71 | pattern3 = f"{input}/**/*.yml"
72 | _found = glob.glob(pattern3, recursive=True)
73 | if _found:
74 | policy_list.extend(_found)
75 | for p in policy_list:
76 | logger.debug(f"Transpiling policy file `{p}`")
77 | outdir_for_this_policy = outdir
78 | if "/post_run" in p and "/post_run" not in outdir_for_this_policy:
79 | outdir_for_this_policy = os.path.join(outdir, "post_run")
80 | if "/pre_run" not in outdir_for_this_policy:
81 | outdir_for_this_policy = os.path.join(outdir, "pre_run")
82 | os.makedirs(outdir_for_this_policy, exist_ok=True)
83 | ast = self.policybook_to_ast(p)
84 | self.ast_to_rego(ast, outdir_for_this_policy)
85 | else:
86 | raise ValueError("invalid input")
87 |
88 | def policybook_to_ast(self, policy_file):
89 | policyset = None
90 | try:
91 | with open(policy_file, "r") as f:
92 | data = yaml.safe_load(f.read())
93 | policyset = generate_dict_policysets(parse_policy_sets(data))
94 | except Exception:
95 | err = traceback.format_exc()
96 | logger.warning(f"Failed to transpile `{policy_file}`. details: {err}")
97 | return policyset
98 |
99 | def ast_to_rego(self, ast, rego_dir):
100 | for ps in ast:
101 | self.policyset_to_rego(ps, rego_dir)
102 |
103 | def policyset_to_rego(self, ast_data, rego_dir):
104 | if "PolicySet" not in ast_data:
105 | raise ValueError("no policy found")
106 |
107 | ps = ast_data["PolicySet"]
108 | if "name" not in ps:
109 | raise ValueError("name field is empty")
110 |
111 | policies = []
112 | for p in ps.get("policies", []):
113 | pol = p.get("Policy", {})
114 |
115 | rego_policy = RegoPolicy()
116 | # package
117 | _package = pol["name"]
118 | _package = self.clean_error_token(pol["name"])
119 | rego_policy.package = _package
120 | # import statements
121 | rego_policy.import_statements = [
122 | "import future.keywords.if",
123 | "import future.keywords.in",
124 | "import data.ansible_policy.resolve_var",
125 | ]
126 | # tags
127 | rego_policy.tags = pol.get("tags", [])
128 | # vars
129 | rego_policy.vars_declaration = ps.get("vars", [])
130 | # target
131 | rego_policy.target = pol.get("target")
132 |
133 | # condition -> rule
134 | _name = self.clean_error_token(pol["name"])
135 | condition = pol.get("condition", {})
136 | root_func, condition_funcs, used_funcs = self.condition_to_rule(condition, _name)
137 | rego_policy.root_condition_func = root_func
138 | rego_policy.condition_funcs = condition_funcs
139 | rego_policy.util_funcs = used_funcs
140 |
141 | action = pol.get("actions", [])[0]
142 | action_func = self.action_to_rule(action, root_func)
143 | rego_policy.action_func = action_func
144 |
145 | policies.append(rego_policy)
146 |
147 | for rpol in policies:
148 | rego_output = rpol.to_rego()
149 | with open(os.path.join(rego_dir, f"{rpol.package}.rego"), "w") as f:
150 | f.write(rego_output)
151 | return
152 |
153 | def action_to_rule(self, input: dict, condition: RegoFunc):
154 | action = input["Action"]
155 | rules = []
156 | action_type = action.get("action", "")
157 | if action_type not in VALID_ACTIONS:
158 | raise ValueError(f"{action_type} is not supported. supported actions are {VALID_ACTIONS}")
159 | action_args = action.get("action_args", "")
160 | rules.append(condition.name)
161 | msg = action_args.get("msg", "")
162 | print_msg = self.make_rego_print(msg)
163 | rules.append(print_msg)
164 | template = action_func_template
165 | return self.make_func_from_cond(action_type, template, rules)
166 |
167 | # func to convert each condition to rego rules
168 | def condition_to_rule(self, condition: dict, policy_name: str):
169 | root_func, condition_funcs = et.trace_ast_tree(condition=condition, policy_name=policy_name)
170 | # util funcs
171 | used_funcs = []
172 | for func in condition_funcs:
173 | used_funcs.extend(func.util_funcs)
174 | used_funcs = list(set(used_funcs))
175 | return root_func, condition_funcs, used_funcs
176 |
177 | def make_rego_print(self, input_text):
178 | pattern = r"{{\s*([^}]+)\s*}}"
179 | replacement = r"%v"
180 | # replace vars part to rego style
181 | result = re.sub(pattern, replacement, input_text)
182 | vals = re.findall(pattern, input_text)
183 | if len(vals) != 0:
184 | # Strip whitespace from all string values in the list
185 | vals = [v.strip() if isinstance(v, str) else v for v in vals]
186 | val_str = ", ".join(vals)
187 | # replace " with '
188 | result = result.replace('"', "'")
189 | return f'print(sprintf("{result}", [{val_str}]))'
190 | else:
191 | return f'print("{input_text}")'
192 |
193 | def make_func_from_cond(self, name, template, conditions):
194 | _steps = self.join_with_separator(conditions, separator="\n ")
195 | rego_block = template.safe_substitute(
196 | {
197 | "func_name": name,
198 | "steps": _steps,
199 | }
200 | )
201 | return rego_block
202 |
203 | def join_with_separator(self, str_or_list: str | list, separator: str = ", "):
204 | value = ""
205 | if isinstance(str_or_list, str):
206 | value = str_or_list
207 | elif isinstance(str_or_list, list):
208 | value = separator.join(str_or_list)
209 | return value
210 |
211 | def clean_error_token(self, in_str):
212 | return in_str.replace(" ", "_").replace("-", "_").replace("?", "").replace("(", "_").replace(")", "_")
213 |
214 |
215 | def load_file(input):
216 | # load yaml file
217 | ast_data = []
218 | with open(input, "r") as f:
219 | ast_data = yaml.safe_load(f)
220 | if not ast_data:
221 | raise ValueError("empty yaml file")
222 | return ast_data
223 |
224 |
225 | def main():
226 | parser = argparse.ArgumentParser(description="TODO")
227 | parser.add_argument("-i", "--input", help="")
228 | parser.add_argument("-o", "--output", help="")
229 | args = parser.parse_args()
230 |
231 | input = args.input
232 | out_dir = args.output
233 |
234 | pt = PolicyTranspiler()
235 | pt.run(input, out_dir)
236 |
237 |
238 | if __name__ == "__main__":
239 | main()
240 |
--------------------------------------------------------------------------------
/ansible_policy/policybook/condition_parser.py:
--------------------------------------------------------------------------------
1 | # Copyright 2022 Red Hat, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import logging
16 |
17 | from pyparsing import (
18 | Combine,
19 | Group,
20 | Keyword,
21 | Literal,
22 | OpAssoc,
23 | Optional,
24 | ParseException,
25 | ParserElement,
26 | QuotedString,
27 | Suppress,
28 | Word,
29 | ZeroOrMore,
30 | DelimitedList,
31 | Forward,
32 | alphanums,
33 | infix_notation,
34 | one_of,
35 | originalTextFor,
36 | pyparsing_common,
37 | )
38 |
39 | from ansible_rulebook.exception import (
40 | ConditionParsingException,
41 | SelectattrOperatorException,
42 | SelectOperatorException,
43 | )
44 |
45 | from typing import Dict
46 |
47 | ParserElement.enable_packrat()
48 |
49 | from ansible_rulebook.condition_types import ( # noqa: E402
50 | Boolean,
51 | Condition,
52 | Identifier,
53 | KeywordValue,
54 | NegateExpression,
55 | Null,
56 | OperatorExpression,
57 | SearchType,
58 | SelectattrType,
59 | SelectType,
60 | String,
61 | to_condition_type,
62 | )
63 |
64 | VALID_SELECT_ATTR_OPERATORS = [
65 | "==",
66 | "!=",
67 | ">",
68 | ">=",
69 | "<",
70 | "<=",
71 | "regex",
72 | "search",
73 | "match",
74 | "in",
75 | "not in",
76 | "contains",
77 | "not contains",
78 | "has key",
79 | "lacks key",
80 | ]
81 |
82 | VALID_SELECT_OPERATORS = [
83 | "==",
84 | "!=",
85 | ">",
86 | ">=",
87 | "<",
88 | "<=",
89 | "regex",
90 | "search",
91 | "match",
92 | ]
93 | SUPPORTED_SEARCH_KINDS = ("match", "regex", "search")
94 |
95 | logger = logging.getLogger(__name__)
96 |
97 |
98 | def as_list(var):
99 | if hasattr(var.__class__, "as_list"):
100 | return var.as_list()
101 | return var
102 |
103 |
104 | def SelectattrTypeFactory(tokens):
105 | if tokens[1].value not in VALID_SELECT_ATTR_OPERATORS:
106 | raise SelectattrOperatorException(f"Operator {tokens[1]} is not supported")
107 |
108 | return SelectattrType(tokens[0], tokens[1], as_list(tokens[2]))
109 |
110 |
111 | def SelectTypeFactory(tokens):
112 | if tokens[0].value not in VALID_SELECT_OPERATORS:
113 | raise SelectOperatorException(f"Operator {tokens[0]} is not supported")
114 |
115 | return SelectType(tokens[0], as_list(tokens[1]))
116 |
117 |
118 | def SearchTypeFactory(kind, tokens):
119 | options = []
120 | if len(tokens) > 1:
121 | for i in range(1, len(tokens), 2):
122 | options.append(KeywordValue(String(tokens[i]), tokens[i + 1]))
123 |
124 | return SearchType(String(kind), tokens[0], options)
125 |
126 |
127 | def OperatorExpressionFactory(tokens):
128 | return_value = None
129 | logger.debug(tokens)
130 | while tokens:
131 | if return_value is None:
132 | if (tokens[1] == "is" or tokens[1] == "is not") and (tokens[2] in SUPPORTED_SEARCH_KINDS):
133 | search_type = SearchTypeFactory(tokens[2], tokens[3])
134 | return_value = OperatorExpression(tokens[0], tokens[1], search_type)
135 | tokens = tokens[4:]
136 | elif tokens[2] == "selectattr":
137 | select_attr_type = SelectattrTypeFactory(tokens[3])
138 | return_value = OperatorExpression(tokens[0], tokens[1], select_attr_type)
139 | tokens = tokens[4:]
140 | elif tokens[2] == "select":
141 | select_type = SelectTypeFactory(tokens[3])
142 | return_value = OperatorExpression(tokens[0], tokens[1], select_type)
143 | tokens = tokens[4:]
144 | else:
145 | return_value = OperatorExpression(as_list(tokens[0]), tokens[1], as_list(tokens[2]))
146 | tokens = tokens[3:]
147 | else:
148 | return_value = OperatorExpression(return_value, tokens[0], tokens[1])
149 | tokens = tokens[2:]
150 | return return_value
151 |
152 |
153 | def make_valid_prefix(vars: Dict):
154 | valid_prefix = Keyword("input")
155 | if not vars:
156 | return valid_prefix
157 | for prefix in vars.keys():
158 | valid_prefix |= Keyword(prefix)
159 | return valid_prefix
160 |
161 |
162 | def define_condition(vars: Dict):
163 | number_t = pyparsing_common.number.copy().add_parse_action(lambda toks: to_condition_type(toks[0]))
164 |
165 | ident = pyparsing_common.identifier
166 |
167 | valid_prefix = make_valid_prefix(vars)
168 | varname = (
169 | Combine(
170 | valid_prefix
171 | + ZeroOrMore(
172 | ("." + ident)
173 | | (("[") + (originalTextFor(QuotedString('"')) | originalTextFor(QuotedString("'")) | pyparsing_common.signed_integer) + ("]"))
174 | )
175 | )
176 | .copy()
177 | .add_parse_action(lambda toks: Identifier(toks[0]))
178 | )
179 | true = Literal("true") | Literal("True")
180 | false = Literal("false") | Literal("False")
181 | boolean = (true | false).copy().add_parse_action(lambda toks: Boolean(toks[0].lower()))
182 |
183 | null_t = Literal("null").copy().add_parse_action(lambda toks: Null())
184 |
185 | string1 = QuotedString("'").copy().add_parse_action(lambda toks: String(toks[0]))
186 | string2 = QuotedString('"').copy().add_parse_action(lambda toks: String(toks[0]))
187 |
188 | plain_string = Word(alphanums + "_").copy().add_parse_action(lambda toks: String(toks[0]))
189 | allowed_values = number_t | boolean | null_t | string1 | string2
190 | key_value = ident + Suppress("=") + allowed_values
191 | string_search_t = (
192 | one_of("regex match search") + Suppress("(") + Group(Optional(DelimitedList(string1 | string2 | varname | key_value))) + Suppress(")")
193 | )
194 |
195 | list_values = Forward()
196 |
197 | allowed_values = number_t | boolean | null_t | string1 | string2
198 |
199 | delim_value = Group(DelimitedList(number_t | null_t | boolean | varname | string1 | string2 | list_values))
200 |
201 | list_values <<= Suppress("[") + delim_value + Suppress("]")
202 |
203 | selectattr_t = Literal("selectattr") + Suppress("(") + Group(DelimitedList(allowed_values | list_values | varname)) + Suppress(")")
204 |
205 | select_t = Literal("select") + Suppress("(") + Group(DelimitedList(allowed_values | list_values | varname)) + Suppress(")")
206 |
207 | all_terms = selectattr_t | select_t | string_search_t | list_values | number_t | null_t | boolean | varname | plain_string | string1 | string2
208 |
209 | condition = infix_notation(
210 | base_expr=all_terms,
211 | op_list=[
212 | (
213 | ">=",
214 | 2,
215 | OpAssoc.LEFT,
216 | lambda toks: OperatorExpressionFactory(toks[0]),
217 | ),
218 | (
219 | "<=",
220 | 2,
221 | OpAssoc.LEFT,
222 | lambda toks: OperatorExpressionFactory(toks[0]),
223 | ),
224 | (
225 | one_of("< >"),
226 | 2,
227 | OpAssoc.LEFT,
228 | lambda toks: OperatorExpressionFactory(toks[0]),
229 | ),
230 | (
231 | "!=",
232 | 2,
233 | OpAssoc.LEFT,
234 | lambda toks: OperatorExpressionFactory(toks[0]),
235 | ),
236 | (
237 | "==",
238 | 2,
239 | OpAssoc.LEFT,
240 | lambda toks: OperatorExpressionFactory(toks[0]),
241 | ),
242 | (
243 | one_of(strs=["is not", "is"]),
244 | 2,
245 | OpAssoc.LEFT,
246 | lambda toks: OperatorExpressionFactory(toks[0]),
247 | ),
248 | (
249 | one_of(
250 | strs=["not in", "in", "not contains", "contains"],
251 | caseless=True,
252 | as_keyword=True,
253 | ),
254 | 2,
255 | OpAssoc.LEFT,
256 | lambda toks: OperatorExpressionFactory(toks[0]),
257 | ),
258 | (
259 | one_of(
260 | strs=["has key", "lacks key"],
261 | caseless=True,
262 | as_keyword=True,
263 | ),
264 | 2,
265 | OpAssoc.LEFT,
266 | lambda toks: OperatorExpressionFactory(toks[0]),
267 | ),
268 | ("not", 1, OpAssoc.RIGHT, lambda toks: NegateExpression(*toks[0])),
269 | (
270 | one_of(["and", "or"]),
271 | 2,
272 | OpAssoc.LEFT,
273 | lambda toks: OperatorExpressionFactory(toks[0]),
274 | ),
275 | ],
276 | ).add_parse_action(lambda toks: Condition(toks[0]))
277 | return condition
278 |
279 |
280 | def parse_condition(condition_string: str, vars: Dict) -> Condition:
281 | condition = define_condition(vars)
282 | condition.debug = True
283 | condition.parseString(condition_string, parse_all=True)[0]
284 | try:
285 | return condition.parseString(condition_string, parse_all=True)[0]
286 | except ParseException as pe:
287 | msg = f"Error parsing: {condition_string}. {pe}"
288 | logger.debug(pe.explain(depth=0))
289 | raise ConditionParsingException(msg)
290 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/tests/unit/test_transpiler.py:
--------------------------------------------------------------------------------
1 | # Copyright 2022 Red Hat, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from ansible_policy.policybook.expressioin_transpiler import ExpressionTranspiler
16 |
17 | et = ExpressionTranspiler()
18 |
19 | ##
20 | # EqualsExpression
21 | ##
22 | ast_equal_1 = {"EqualsExpression": {"lhs": {"Input": "input.range.i"}, "rhs": {"Integer": 1}}}
23 | rego_equal_1 = """
24 | test = true if {
25 | input.range.i == 1
26 | }
27 | """
28 |
29 | ast_equal_2 = {
30 | "EqualsExpression": {
31 | "lhs": {"Input": "input.become_user"},
32 | "rhs": {"String": "malicious-user"},
33 | }
34 | }
35 | rego_equal_2 = """
36 | test = true if {
37 | input.become_user == "malicious-user"
38 | }
39 | """
40 |
41 | ast_equal_3 = {
42 | "EqualsExpression": {
43 | "lhs": {"Input": "input.become_user"},
44 | "rhs": {"Boolean": True},
45 | }
46 | }
47 | rego_equal_3 = """
48 | test = true if {
49 | input.become_user
50 | }
51 | """
52 |
53 | ast_equal_4 = {
54 | "EqualsExpression": {
55 | "lhs": {"Input": "input.become_user"},
56 | "rhs": {"Boolean": False},
57 | }
58 | }
59 | rego_equal_4 = """
60 | test = true if {
61 | not input.become_user
62 | }
63 | """
64 |
65 | ast_equal_5 = {
66 | "EqualsExpression": {
67 | "lhs": {"Input": "input.become_user"},
68 | "rhs": {"Float": 3.1415},
69 | }
70 | }
71 | rego_equal_5 = """
72 | test = true if {
73 | input.become_user == 3.1415
74 | }
75 | """
76 |
77 | ast_equal_6 = {
78 | "EqualsExpression": {
79 | "lhs": {"Input": "input.become_user"},
80 | "rhs": {"Variable": "var1"},
81 | }
82 | }
83 | rego_equal_6 = """
84 | test = true if {
85 | input.become_user == var1
86 | }
87 | """
88 |
89 |
90 | def test_Equals():
91 | assert rego_equal_1 == et.EqualsExpression.make_rego("test", ast_equal_1)
92 | assert rego_equal_2 == et.EqualsExpression.make_rego("test", ast_equal_2)
93 | assert rego_equal_3 == et.EqualsExpression.make_rego("test", ast_equal_3)
94 | assert rego_equal_4 == et.EqualsExpression.make_rego("test", ast_equal_4)
95 | assert rego_equal_5 == et.EqualsExpression.make_rego("test", ast_equal_5)
96 | assert rego_equal_6 == et.EqualsExpression.make_rego("test", ast_equal_6)
97 |
98 |
99 | ##
100 | # NotEqualsExpression
101 | ##
102 | ast_notequal_1 = {"NotEqualsExpression": {"lhs": {"Input": "input.range.i"}, "rhs": {"Integer": 0}}}
103 | rego_notequal_1 = """
104 | test = true if {
105 | input.range.i != 0
106 | }
107 | """
108 |
109 | ast_notequal_2 = {
110 | "NotEqualsExpression": {
111 | "lhs": {"Input": "input.become_user"},
112 | "rhs": {"String": "malicious-user"},
113 | }
114 | }
115 | rego_notequal_2 = """
116 | test = true if {
117 | input.become_user != "malicious-user"
118 | }
119 | """
120 |
121 |
122 | def test_NotEquals():
123 | assert rego_notequal_1 == et.NotEqualsExpression.make_rego("test", ast_notequal_1)
124 | assert rego_notequal_2 == et.NotEqualsExpression.make_rego("test", ast_notequal_2)
125 |
126 |
127 | ##
128 | # ItemInListExpression and ItemNotInListExpression
129 | ##
130 | ast_ItemInList_1 = {
131 | "ItemInListExpression": {
132 | "lhs": {"Input": "input.i"},
133 | "rhs": [{"Integer": 1}, {"Integer": 2}, {"Integer": 3}],
134 | }
135 | }
136 |
137 | rego_ItemInList_1 = """
138 | test = true if {
139 | lhs_list = to_list(input.i)
140 | check_item_in_list(lhs_list, [1, 2, 3])
141 | }
142 | """
143 |
144 | ast_ItemNotInList_1 = {
145 | "ItemNotInListExpression": {
146 | "lhs": {"Input": "input.i"},
147 | "rhs": [{"Integer": 1}, {"Integer": 2}, {"Integer": 3}],
148 | }
149 | }
150 |
151 | rego_ItemNotInList_1 = """
152 | test = true if {
153 | lhs_list = to_list(input.i)
154 | check_item_not_in_list(lhs_list, [1, 2, 3])
155 | }
156 | """
157 |
158 |
159 | def test_ItemInList():
160 | assert rego_ItemInList_1 == et.ItemInListExpression.make_rego("test", ast_ItemInList_1)
161 | assert rego_ItemNotInList_1 == et.ItemNotInListExpression.make_rego("test", ast_ItemNotInList_1)
162 |
163 |
164 | ##
165 | # ListContainsItemExpression and ListNotContainsItemExpression
166 | ##
167 | ast_ListContainsItem_1 = {
168 | "ListContainsItemExpression": {
169 | "lhs": {"Input": "input.mylist"},
170 | "rhs": {"Integer": 1},
171 | }
172 | }
173 |
174 | rego_ListContainsItem_1 = """
175 | test = true if {
176 | lhs_list = to_list(1)
177 | check_item_in_list(lhs_list, input.mylist)
178 | }
179 | """
180 |
181 | ast_ListNotContainsItem_1 = {
182 | "ListNotContainsItemExpression": {
183 | "lhs": {"Input": "input.mylist"},
184 | "rhs": {"Integer": 1},
185 | }
186 | }
187 |
188 | rego_ListNotContainsItem_1 = """
189 | test = true if {
190 | lhs_list = to_list(1)
191 | check_item_not_in_list(lhs_list, input.mylist)
192 | }
193 | """
194 |
195 |
196 | def test_ListContainsItem():
197 | assert rego_ListContainsItem_1 == et.ListContainsItemExpression.make_rego("test", ast_ListContainsItem_1)
198 | assert rego_ListNotContainsItem_1 == et.ListNotContainsItemExpression.make_rego("test", ast_ListNotContainsItem_1)
199 |
200 |
201 | ##
202 | # KeyInDictExpression and KeyNotInDictExpression
203 | ##
204 | ast_KeyInDict_1 = {
205 | "KeyInDictExpression": {
206 | "lhs": {"Input": "input.friends"},
207 | "rhs": {"String": "fred"},
208 | }
209 | }
210 |
211 | rego_KeyInDict_1 = """
212 | test = true if {
213 | input.friends
214 | input_keys := [key | input.friends[key]; key == "fred"]
215 | count(input_keys) > 0
216 | }
217 | """
218 |
219 | ast_KeyNotInDict_1 = {
220 | "KeyNotInDictExpression": {
221 | "lhs": {"Input": "input.friends"},
222 | "rhs": {"String": "fred"},
223 | }
224 | }
225 |
226 | rego_KeyNotInDict_1 = """
227 | test = true if {
228 | input.friends
229 | input_keys := [key | input.friends[key]; key == "fred"]
230 | count(input_keys) == 0
231 | }
232 | """
233 |
234 |
235 | def test_KeyInDict():
236 | assert rego_KeyInDict_1 == et.KeyInDictExpression.make_rego("test", ast_KeyInDict_1)
237 | assert rego_KeyNotInDict_1 == et.KeyNotInDictExpression.make_rego("test", ast_KeyNotInDict_1)
238 |
239 |
240 | ##
241 | # IsDefinedExpression and IsNotDefinedExpression
242 | ##
243 | ast_IsDefined_1 = {"IsDefinedExpression": {"Input": "input.range.i"}}
244 |
245 | rego_IsDefined_1 = """
246 | test = true if {
247 | input.range
248 | input.range.i
249 | }
250 | """
251 |
252 | ast_IsNotDefined_1 = {"IsNotDefinedExpression": {"Input": "input.range.i"}}
253 |
254 | rego_IsNotDefined_1 = """
255 | test = true if {
256 | input.range
257 | not input.range.i
258 | }
259 | """
260 |
261 |
262 | def test_IsDefined():
263 | assert rego_IsDefined_1 == et.IsDefinedExpression.make_rego("test", ast_IsDefined_1)
264 | assert rego_IsNotDefined_1 == et.IsNotDefinedExpression.make_rego("test", ast_IsNotDefined_1)
265 |
266 |
267 | ##
268 | # GreaterThanExpression and GreaterThanOrEqualToExpression
269 | ##
270 | ast_GreaterThan_1 = {
271 | "GreaterThanExpression": {
272 | "lhs": {"Input": "input.range.i"},
273 | "rhs": {"Integer": 1},
274 | }
275 | }
276 |
277 | rego_GreaterThan_1 = """
278 | test = true if {
279 | input.range.i > 1
280 | }
281 | """
282 |
283 | ast_GreaterThanOrEqualTo_1 = {
284 | "GreaterThanOrEqualToExpression": {
285 | "lhs": {"Input": "input.range.i"},
286 | "rhs": {"Integer": 1},
287 | }
288 | }
289 |
290 | rego_GreaterThanOrEqualTo_1 = """
291 | test = true if {
292 | input.range.i >= 1
293 | }
294 | """
295 |
296 |
297 | def test_GreaterThan():
298 | assert rego_GreaterThan_1 == et.GreaterThanExpression.make_rego("test", ast_GreaterThan_1)
299 | assert rego_GreaterThanOrEqualTo_1 == et.GreaterThanOrEqualToExpression.make_rego("test", ast_GreaterThanOrEqualTo_1)
300 |
301 |
302 | ##
303 | # LessThanExpression and LessThanOrEqualToExpression
304 | ##
305 | ast_LessThan_1 = {
306 | "LessThanExpression": {
307 | "lhs": {"Input": "input.range.i"},
308 | "rhs": {"Integer": 1},
309 | }
310 | }
311 |
312 | rego_LessThan_1 = """
313 | test = true if {
314 | input.range.i < 1
315 | }
316 | """
317 |
318 | ast_LessThanOrEqualTo_1 = {
319 | "LessThanOrEqualToExpression": {
320 | "lhs": {"Input": "input.range.i"},
321 | "rhs": {"Integer": 1},
322 | }
323 | }
324 |
325 | rego_LessThanOrEqualTo_1 = """
326 | test = true if {
327 | input.range.i <= 1
328 | }
329 | """
330 |
331 |
332 | def test_LessThan_than():
333 | assert rego_LessThan_1 == et.LessThanExpression.make_rego("test", ast_LessThan_1)
334 | assert rego_LessThanOrEqualTo_1 == et.LessThanOrEqualToExpression.make_rego("test", ast_LessThanOrEqualTo_1)
335 |
336 |
337 | ##
338 | # NegateExpression
339 | ##
340 | ast_Negate_1 = {"NegateExpression": {"Input": "input.friends"}}
341 |
342 | rego_Negate_1 = """
343 | test = true if {
344 | not input.friends
345 | }
346 | """
347 |
348 |
349 | def test_Negate():
350 | assert rego_Negate_1 == et.NegateExpression.make_rego("test", ast_Negate_1)
351 |
352 |
353 | ##
354 | # AffirmExpression
355 | ##
356 | # TODO: Change to Class
357 | ast_Affirm_1 = {"Input": "input.friends"}
358 |
359 | rego_Affirm_1 = """
360 | test = true if {
361 | input.friends
362 | }
363 | """
364 |
365 |
366 | def test_Affirm():
367 | result, _ = et.handle_non_operator_expression(ast_Affirm_1, "test", "", "", "")
368 | assert rego_Affirm_1 == result.body
369 |
370 |
371 | ##
372 | # SearchMatchesExpression and SearchNotMatchesExpression
373 | ##
374 | ast_Search_1 = {
375 | "SearchMatchesExpression": {
376 | "lhs": {"Input": "input.range"},
377 | "rhs": {
378 | "SearchType": {
379 | "kind": {"String": "search"},
380 | "options": [
381 | {
382 | "name": {"String": "ignorecase"},
383 | "value": {"Boolean": True},
384 | },
385 | ],
386 | "pattern": {"String": "example"},
387 | }
388 | },
389 | }
390 | }
391 |
392 | rego_Search_1 = """
393 | test = true if {
394 | contains(lower(input.range), lower("example"))
395 | }
396 | """
397 |
398 | ast_SearchNot_1 = {
399 | "SearchNotMatchesExpression": {
400 | "lhs": {"Input": "input.range"},
401 | "rhs": {
402 | "SearchType": {
403 | "kind": {"String": "search"},
404 | "options": [
405 | {
406 | "name": {"String": "ignorecase"},
407 | "value": {"Boolean": True},
408 | },
409 | ],
410 | "pattern": {"String": "example"},
411 | }
412 | },
413 | }
414 | }
415 |
416 | rego_SearchNot_1 = """
417 | test = true if {
418 | not contains(lower(input.range), lower("example"))
419 | }
420 | """
421 |
422 | ast_Match_1 = {
423 | "SearchMatchesExpression": {
424 | "lhs": {"Input": "input.range"},
425 | "rhs": {
426 | "SearchType": {
427 | "kind": {"String": "match"},
428 | "options": [
429 | {
430 | "name": {"String": "ignorecase"},
431 | "value": {"Boolean": True},
432 | },
433 | ],
434 | "pattern": {"String": "example"},
435 | }
436 | },
437 | }
438 | }
439 |
440 | rego_Match_1 = """
441 | test = true if {
442 | startswith(lower(input.range), lower("example"))
443 | }
444 | """
445 |
446 | ast_MatchNot_1 = {
447 | "SearchNotMatchesExpression": {
448 | "lhs": {"Input": "input.range"},
449 | "rhs": {
450 | "SearchType": {
451 | "kind": {"String": "match"},
452 | "options": [
453 | {
454 | "name": {"String": "ignorecase"},
455 | "value": {"Boolean": True},
456 | },
457 | ],
458 | "pattern": {"String": "example"},
459 | }
460 | },
461 | }
462 | }
463 |
464 | rego_MatchNot_1 = """
465 | test = true if {
466 | not startswith(lower(input.range), lower("example"))
467 | }
468 | """
469 |
470 | ast_Regex_1 = {
471 | "SearchMatchesExpression": {
472 | "lhs": {"Input": "input.range"},
473 | "rhs": {
474 | "SearchType": {
475 | "kind": {"String": "regex"},
476 | "options": [
477 | {
478 | "name": {"String": "ignorecase"},
479 | "value": {"Boolean": True},
480 | },
481 | ],
482 | "pattern": {"String": "ex*e"},
483 | }
484 | },
485 | }
486 | }
487 |
488 | rego_Regex_1 = """
489 | test = true if {
490 | regex.find_n(lower("ex*e"), lower(input.range), 1) != []
491 | }
492 | """
493 |
494 | ast_RegexNot_1 = {
495 | "SearchNotMatchesExpression": {
496 | "lhs": {"Input": "input.range"},
497 | "rhs": {
498 | "SearchType": {
499 | "kind": {"String": "regex"},
500 | "options": [
501 | {
502 | "name": {"String": "ignorecase"},
503 | "value": {"Boolean": True},
504 | },
505 | ],
506 | "pattern": {"String": "ex*e"},
507 | }
508 | },
509 | }
510 | }
511 |
512 | rego_RegexNot_1 = """
513 | test = true if {
514 | regex.find_n(lower("ex*e"), lower(input.range), 1) == []
515 | }
516 | """
517 |
518 |
519 | def test_SearchMatches():
520 | assert rego_Search_1 == et.SearchMatchesExpression.make_rego("test", ast_Search_1)
521 | assert rego_SearchNot_1 == et.SearchNotMatchesExpression.make_rego("test", ast_SearchNot_1)
522 | assert rego_Match_1 == et.SearchMatchesExpression.make_rego("test", ast_Match_1)
523 | assert rego_MatchNot_1 == et.SearchNotMatchesExpression.make_rego("test", ast_MatchNot_1)
524 | assert rego_Regex_1 == et.SearchMatchesExpression.make_rego("test", ast_Regex_1)
525 | assert rego_RegexNot_1 == et.SearchNotMatchesExpression.make_rego("test", ast_RegexNot_1)
526 |
527 |
528 | ##
529 | # SelectExpression and SelectNotExpression
530 | ##
531 | ast_Select_1 = {
532 | "SelectExpression": {
533 | "lhs": {"Input": "input.range"},
534 | "rhs": {
535 | "operator": {
536 | "String": ">=",
537 | },
538 | "value": {"Integer": 10},
539 | },
540 | }
541 | }
542 |
543 | rego_Select_1 = """
544 | test = true if {
545 | array := [item | item := input.range[_]; item >= 10]
546 | count(array) > 0
547 | }
548 | """
549 |
550 | ast_Select_2 = {
551 | "SelectExpression": {
552 | "lhs": {"Input": "input.range"},
553 | "rhs": {
554 | "operator": {
555 | "String": "search",
556 | },
557 | "value": {"String": "val"},
558 | },
559 | }
560 | }
561 |
562 | rego_Select_2 = """
563 | test = true if {
564 | rhs_list = to_list("val")
565 | check_item_in_list(input.range, rhs_list)
566 | }
567 | """
568 |
569 | ast_NotSelect_1 = {
570 | "SelectNotExpression": {
571 | "lhs": {"Input": "input.range"},
572 | "rhs": {
573 | "operator": {
574 | "String": ">=",
575 | },
576 | "value": {"Integer": 10},
577 | },
578 | }
579 | }
580 |
581 | rego_NotSelect_1 = """
582 | test = true if {
583 | array := [item | item := input.range[_]; item >= 10]
584 | count(array) == 0
585 | }
586 | """
587 |
588 | ast_NotSelect_2 = {
589 | "SelectNotExpression": {
590 | "lhs": {"Input": "input.range"},
591 | "rhs": {
592 | "operator": {
593 | "String": "search",
594 | },
595 | "value": {"String": "val"},
596 | },
597 | }
598 | }
599 |
600 | rego_NotSelect_2 = """
601 | test = true if {
602 | rhs_list = to_list("val")
603 | check_item_not_in_list(input.range, rhs_list)
604 | }
605 | """
606 |
607 |
608 | def test_Select():
609 | assert rego_Select_1 == et.SelectExpression.make_rego("test", ast_Select_1)
610 | assert rego_Select_2 == et.SelectExpression.make_rego("test", ast_Select_2)
611 | assert rego_NotSelect_1 == et.SelectNotExpression.make_rego("test", ast_NotSelect_1)
612 | assert rego_NotSelect_2 == et.SelectNotExpression.make_rego("test", ast_NotSelect_2)
613 |
614 |
615 | ##
616 | # SelectAttrExpression and SelectAttrNotExpression
617 | ##
618 | ast_SelectAttr_1 = {
619 | "SelectAttrExpression": {
620 | "lhs": {"Input": "input.range"},
621 | "rhs": {
622 | "key": {"String": "age"},
623 | "operator": {"String": ">="},
624 | "value": {"Integer": 10},
625 | },
626 | }
627 | }
628 |
629 | rego_SelectAttr_1 = """
630 | test = true if {
631 | array := [item | item := input.range[_]; object.get(item, ["age"], "none") >= 10]
632 | count(array) > 0
633 | }
634 | """
635 |
636 | ast_SelectAttr_2 = {
637 | "SelectAttrExpression": {
638 | "lhs": {"Input": "input.range"},
639 | "rhs": {
640 | "key": {"String": "age"},
641 | "operator": {"String": "search"},
642 | "value": {"String": "val"},
643 | },
644 | }
645 | }
646 |
647 | rego_SelectAttr_2 = """
648 | test = true if {
649 | rhs_list = to_list("val")
650 | check_item_key_in_list(input.range, rhs_list, ["age"])
651 | }
652 | """
653 |
654 | ast_NotSelectAttr_1 = {
655 | "SelectAttrNotExpression": {
656 | "lhs": {"Input": "input.range"},
657 | "rhs": {
658 | "key": {"String": "age"},
659 | "operator": {"String": ">="},
660 | "value": {"Integer": 10},
661 | },
662 | }
663 | }
664 |
665 | rego_NotSelectAttr_1 = """
666 | test = true if {
667 | array := [item | item := input.range[_]; object.get(item, ["age"], "none") >= 10]
668 | count(array) == 0
669 | }
670 | """
671 |
672 | ast_NotSelectAttr_2 = {
673 | "SelectAttrNotExpression": {
674 | "lhs": {"Input": "input.range"},
675 | "rhs": {
676 | "key": {"String": "age"},
677 | "operator": {"String": "search"},
678 | "value": {"String": "val"},
679 | },
680 | }
681 | }
682 |
683 | rego_NotSelectAttr_2 = """
684 | test = true if {
685 | rhs_list = to_list("val")
686 | check_item_key_not_in_list(input.range, rhs_list, ["age"])
687 | }
688 | """
689 |
690 |
691 | def test_SelectAttr():
692 | assert rego_SelectAttr_1 == et.SelectAttrExpression.make_rego("test", ast_SelectAttr_1)
693 | assert rego_SelectAttr_2 == et.SelectAttrExpression.make_rego("test", ast_SelectAttr_2)
694 | assert rego_NotSelectAttr_1 == et.SelectAttrNotExpression.make_rego("test", ast_NotSelectAttr_1)
695 | assert rego_NotSelectAttr_2 == et.SelectAttrNotExpression.make_rego("test", ast_NotSelectAttr_2)
696 |
697 |
698 | ##
699 | # OrExpression, AnyCondition, AndExpression, AllCondition, NotAllCondition
700 | ##
701 | rego_OrAny = """
702 | test = true if {
703 | condition1
704 | }
705 |
706 | test = true if {
707 | condition2
708 | }
709 |
710 | test = true if {
711 | condition3
712 | }
713 | """
714 |
715 | rego_AndAll = """
716 | test = true if {
717 | condition1
718 | condition2
719 | condition3
720 | }
721 | """
722 |
723 | rego_NotAll = """
724 | test = true if {
725 | not condition1
726 | }
727 |
728 | test = true if {
729 | not condition2
730 | }
731 |
732 | test = true if {
733 | not condition3
734 | }
735 | """
736 |
737 |
738 | def test_combination():
739 | assert rego_OrAny == et.OrAnyExpression.make_rego("test", ["condition1", "condition2", "condition3"])
740 | assert rego_AndAll == et.AndAllExpression.make_rego("test", ["condition1", "condition2", "condition3"])
741 | assert rego_NotAll == et.NotAllExpression.make_rego("test", ["condition1", "condition2", "condition3"])
742 |
--------------------------------------------------------------------------------
/tests/unit/test_ast.py:
--------------------------------------------------------------------------------
1 | # Copyright 2022 Red Hat, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import os
16 |
17 | import pytest
18 | import yaml
19 |
20 | from ansible_policy.policybook.condition_parser import parse_condition
21 | from ansible_rulebook.exception import (
22 | SelectattrOperatorException,
23 | SelectOperatorException,
24 | )
25 | from ansible_policy.policybook.json_generator import (
26 | generate_dict_policysets,
27 | visit_condition,
28 | )
29 | from ansible_policy.policybook.policy_parser import parse_policy_sets
30 |
31 | HERE = os.path.dirname(os.path.abspath(__file__))
32 |
33 |
34 | def test_parse_condition():
35 | assert {"Input": "input.data"} == visit_condition(parse_condition("input.data", {}))
36 | assert {"Variable": "var1"} == visit_condition(parse_condition("var1", {"var1": "val1"}))
37 | assert {"Boolean": True} == visit_condition(parse_condition("True", {}))
38 | assert {"Boolean": False} == visit_condition(parse_condition("False", {}))
39 | assert {"Integer": 42} == visit_condition(parse_condition("42", {}))
40 | assert {"Float": 3.1415} == visit_condition(parse_condition("3.1415", {}))
41 | assert {"String": "Hello"} == visit_condition(parse_condition("'Hello'", {}))
42 | assert {"EqualsExpression": {"lhs": {"Input": "input.range.i"}, "rhs": {"Integer": 1}}} == visit_condition(
43 | parse_condition("input.range.i == 1", {})
44 | )
45 | assert {"EqualsExpression": {"lhs": {"Input": "input['i']"}, "rhs": {"Integer": 1}}} == visit_condition(parse_condition("input['i'] == 1", {}))
46 | assert {
47 | "EqualsExpression": {
48 | "lhs": {"Input": "input.range.pi"},
49 | "rhs": {"Float": 3.1415},
50 | }
51 | } == visit_condition(parse_condition("input.range.pi == 3.1415", {}))
52 | assert {
53 | "GreaterThanExpression": {
54 | "lhs": {"Input": "input.range.i"},
55 | "rhs": {"Integer": 1},
56 | }
57 | } == visit_condition(parse_condition("input.range.i > 1", {}))
58 |
59 | assert {
60 | "EqualsExpression": {
61 | "lhs": {"Input": "input.range['pi']"},
62 | "rhs": {"Float": 3.1415},
63 | }
64 | } == visit_condition(parse_condition("input.range['pi'] == 3.1415", {}))
65 | assert {
66 | "EqualsExpression": {
67 | "lhs": {"Input": 'input.range["pi"]'},
68 | "rhs": {"Float": 3.1415},
69 | }
70 | } == visit_condition(parse_condition('input.range["pi"] == 3.1415', {}))
71 |
72 | assert {
73 | "EqualsExpression": {
74 | "lhs": {"Input": 'input.range["pi"].value'},
75 | "rhs": {"Float": 3.1415},
76 | }
77 | } == visit_condition(parse_condition('input.range["pi"].value == 3.1415', {}))
78 | assert {
79 | "EqualsExpression": {
80 | "lhs": {"Input": "input.range[0]"},
81 | "rhs": {"Float": 3.1415},
82 | }
83 | } == visit_condition(parse_condition("input.range[0] == 3.1415", {}))
84 | assert {
85 | "EqualsExpression": {
86 | "lhs": {"Input": "input.range[-1]"},
87 | "rhs": {"Float": 3.1415},
88 | }
89 | } == visit_condition(parse_condition("input.range[-1] == 3.1415", {}))
90 |
91 | assert {
92 | "EqualsExpression": {
93 | "lhs": {"Input": 'input.range["x"][1][2].a["b"]'},
94 | "rhs": {"Float": 3.1415},
95 | }
96 | } == visit_condition(parse_condition('input.range["x"][1][2].a["b"] == 3.1415', {}))
97 |
98 | assert {
99 | "EqualsExpression": {
100 | "lhs": {"Input": "input.become_user"},
101 | "rhs": {"String": "malicious-user"},
102 | }
103 | } == visit_condition(parse_condition('input.become_user == "malicious-user"', {}))
104 |
105 | assert {
106 | "NegateExpression": {
107 | "Input": "input.enabled",
108 | }
109 | } == visit_condition(parse_condition("not input.enabled", {}))
110 |
111 | assert {
112 | "NegateExpression": {
113 | "LessThanExpression": {
114 | "lhs": {"Input": "input.range.i"},
115 | "rhs": {"Integer": 1},
116 | }
117 | }
118 | } == visit_condition(parse_condition("not (input.range.i < 1)", {}))
119 |
120 | assert {
121 | "LessThanExpression": {
122 | "lhs": {"Input": "input.range.i"},
123 | "rhs": {"Integer": 1},
124 | }
125 | } == visit_condition(parse_condition("input.range.i < 1", {}))
126 |
127 | assert {
128 | "NegateExpression": {
129 | "Variable": "enabled",
130 | }
131 | } == visit_condition(parse_condition("not enabled", {"enabled": True}))
132 |
133 | assert {
134 | "NegateExpression": {
135 | "LessThanExpression": {
136 | "lhs": {"Input": "input.range.i"},
137 | "rhs": {"Integer": 1},
138 | }
139 | }
140 | } == visit_condition(parse_condition("not (input.range.i < 1)", {}))
141 | assert {
142 | "LessThanOrEqualToExpression": {
143 | "lhs": {"Input": "input.range.i"},
144 | "rhs": {"Integer": 1},
145 | }
146 | } == visit_condition(parse_condition("input.range.i <= 1", {}))
147 | assert {
148 | "GreaterThanOrEqualToExpression": {
149 | "lhs": {"Input": "input.range.i"},
150 | "rhs": {"Integer": 1},
151 | }
152 | } == visit_condition(parse_condition("input.range.i >= 1", {}))
153 | assert {
154 | "EqualsExpression": {
155 | "lhs": {"Input": "input.range.i"},
156 | "rhs": {"String": "Hello"},
157 | }
158 | } == visit_condition(parse_condition("input.range.i == 'Hello'", {}))
159 |
160 | assert {"IsDefinedExpression": {"Input": "input.range.i"}} == visit_condition(parse_condition("input.range.i is defined", {}))
161 | assert {"IsNotDefinedExpression": {"Input": "input.range.i"}} == visit_condition(parse_condition("input.range.i is not defined", {}))
162 |
163 | assert {"IsNotDefinedExpression": {"Input": "input.range.i"}} == visit_condition(parse_condition("(input.range.i is not defined)", {}))
164 |
165 | assert {"IsNotDefinedExpression": {"Input": "input.range.i"}} == visit_condition(parse_condition("(((input.range.i is not defined)))", {}))
166 | assert {
167 | "OrExpression": {
168 | "lhs": {"IsNotDefinedExpression": {"Input": "input.range.i"}},
169 | "rhs": {"IsDefinedExpression": {"Input": "input.range.i"}},
170 | }
171 | } == visit_condition(parse_condition("(input.range.i is not defined) or (input.range.i is defined)", {}))
172 | assert {
173 | "AndExpression": {
174 | "lhs": {"IsNotDefinedExpression": {"Input": "input.range.i"}},
175 | "rhs": {"IsDefinedExpression": {"Input": "input.range.i"}},
176 | }
177 | } == visit_condition(parse_condition("(input.range.i is not defined) and (input.range.i is defined)", {}))
178 | assert {
179 | "AndExpression": {
180 | "lhs": {
181 | "AndExpression": {
182 | "lhs": {"IsNotDefinedExpression": {"Input": "input.range.i"}},
183 | "rhs": {"IsDefinedExpression": {"Input": "input.range.i"}},
184 | }
185 | },
186 | "rhs": {
187 | "EqualsExpression": {
188 | "lhs": {"Input": "input.range.i"},
189 | "rhs": {"Integer": 1},
190 | }
191 | },
192 | }
193 | } == visit_condition(parse_condition("(input.range.i is not defined) and (input.range.i is defined) " "and (input.range.i == 1)", {}))
194 | assert {
195 | "OrExpression": {
196 | "lhs": {
197 | "AndExpression": {
198 | "lhs": {"IsNotDefinedExpression": {"Input": "input.range.i"}},
199 | "rhs": {"IsDefinedExpression": {"Input": "input.range.i"}},
200 | }
201 | },
202 | "rhs": {
203 | "EqualsExpression": {
204 | "lhs": {"Input": "input.range.i"},
205 | "rhs": {"Integer": 1},
206 | }
207 | },
208 | }
209 | } == visit_condition(parse_condition("(input.range.i is not defined) and (input.range.i is defined) or (input.range.i == 1)", {}))
210 |
211 | assert {
212 | "AndExpression": {
213 | "lhs": {"IsNotDefinedExpression": {"Input": "input.range.i"}},
214 | "rhs": {
215 | "OrExpression": {
216 | "lhs": {"IsDefinedExpression": {"Input": "input.range.i"}},
217 | "rhs": {
218 | "EqualsExpression": {
219 | "lhs": {"Input": "input.range.i"},
220 | "rhs": {"Integer": 1},
221 | }
222 | },
223 | }
224 | },
225 | }
226 | } == visit_condition(parse_condition("(input.range.i is not defined) and " "((input.range.i is defined) or (input.range.i == 1))", {}))
227 |
228 | assert {
229 | "ItemInListExpression": {
230 | "lhs": {"Input": "input.i"},
231 | "rhs": [{"Integer": 1}, {"Integer": 2}, {"Integer": 3}],
232 | }
233 | } == visit_condition(parse_condition("input.i in [1,2,3]", {}))
234 |
235 | assert {
236 | "ItemInListExpression": {
237 | "lhs": {"Input": "input.name"},
238 | "rhs": [
239 | {"String": "fred"},
240 | {"String": "barney"},
241 | {"String": "wilma"},
242 | ],
243 | }
244 | } == visit_condition(parse_condition("input.name in ['fred','barney','wilma']", {}))
245 |
246 | assert {
247 | "ItemInListExpression": {
248 | "lhs": {"Input": 'input["ansible.builtin.package"].name'},
249 | "rhs": [[{"String": "A1"}, {"String": "A2"}], {"String": "B"}, {"String": "C"}],
250 | }
251 | } == visit_condition(parse_condition('input["ansible.builtin.package"].name in [["A1", "A2"], "B", "C"]', {}))
252 |
253 | assert {
254 | "ItemNotInListExpression": {
255 | "lhs": {"Input": "input.i"},
256 | "rhs": [{"Integer": 1}, {"Integer": 2}, {"Integer": 3}],
257 | }
258 | } == visit_condition(parse_condition("input.i not in [1,2,3]", {}))
259 |
260 | assert {
261 | "ItemNotInListExpression": {
262 | "lhs": {"Input": "input['ansible.builtin.package'].name"},
263 | "rhs": [
264 | {"String": "fred"},
265 | {"String": "barney"},
266 | {"String": "wilma"},
267 | ],
268 | }
269 | } == visit_condition(parse_condition("input['ansible.builtin.package'].name not in ['fred','barney','wilma']", {}))
270 | assert {
271 | "ItemNotInListExpression": {
272 | "lhs": {"Input": "input.radius"},
273 | "rhs": [
274 | {"Float": 1079.6234},
275 | {"Float": 3985.8},
276 | {"Float": 2106.1234},
277 | ],
278 | }
279 | } == visit_condition(parse_condition("input.radius not in [1079.6234,3985.8,2106.1234]", {}))
280 |
281 | assert {
282 | "ItemNotInListExpression": {
283 | "lhs": {"Input": "input._agk.task.module_info.collection"},
284 | "rhs": {"Variable": "allowed_collections"},
285 | }
286 | } == visit_condition(
287 | parse_condition("input._agk.task.module_info.collection not in allowed_collections", {"allowed_collections": ["ansible.builtin"]})
288 | )
289 |
290 | assert {
291 | "ListContainsItemExpression": {
292 | "lhs": {"Input": "input.mylist"},
293 | "rhs": {"Integer": 1},
294 | }
295 | } == visit_condition(parse_condition("input.mylist contains 1", {}))
296 |
297 | assert {
298 | "ListContainsItemExpression": {
299 | "lhs": {"Input": "input.friends"},
300 | "rhs": {"String": "fred"},
301 | }
302 | } == visit_condition(parse_condition("input.friends contains 'fred'", {}))
303 |
304 | assert {
305 | "ListNotContainsItemExpression": {
306 | "lhs": {"Input": "input.mylist"},
307 | "rhs": {"Integer": 1},
308 | }
309 | } == visit_condition(parse_condition("input.mylist not contains 1", {}))
310 |
311 | assert {
312 | "ListNotContainsItemExpression": {
313 | "lhs": {"Input": "input.friends"},
314 | "rhs": {"String": "fred"},
315 | }
316 | } == visit_condition(parse_condition("input.friends not contains 'fred'", {}))
317 |
318 | assert {
319 | "KeyInDictExpression": {
320 | "lhs": {"Input": "input.friends"},
321 | "rhs": {"String": "fred"},
322 | }
323 | } == visit_condition(parse_condition("input.friends has key 'fred'", {}))
324 |
325 | assert {
326 | "KeyNotInDictExpression": {
327 | "lhs": {"Input": "input.friends"},
328 | "rhs": {"String": "fred"},
329 | }
330 | } == visit_condition(parse_condition("input.friends lacks key 'fred'", {}))
331 |
332 | assert {
333 | "SearchMatchesExpression": {
334 | "lhs": {"Input": "input['url']"},
335 | "rhs": {
336 | "SearchType": {
337 | "kind": {"String": "match"},
338 | "pattern": {"String": "https://example.com/users/.*/resources"},
339 | "options": [
340 | {
341 | "name": {"String": "ignorecase"},
342 | "value": {"Boolean": True},
343 | }
344 | ],
345 | }
346 | },
347 | }
348 | } == visit_condition(parse_condition("input['url'] is " + 'match("https://example.com/users/.*/resources", ' + "ignorecase=true)", {}))
349 | assert {
350 | "SearchMatchesExpression": {
351 | "lhs": {"Input": "input.url"},
352 | "rhs": {
353 | "SearchType": {
354 | "kind": {"String": "match"},
355 | "pattern": {"String": "https://example.com/users/.*/resources"},
356 | "options": [
357 | {
358 | "name": {"String": "ignorecase"},
359 | "value": {"Boolean": True},
360 | }
361 | ],
362 | }
363 | },
364 | }
365 | } == visit_condition(parse_condition("input.url is " + 'match("https://example.com/users/.*/resources", ' + "ignorecase=true)", {}))
366 |
367 | assert {
368 | "SearchNotMatchesExpression": {
369 | "lhs": {"Input": "input.url"},
370 | "rhs": {
371 | "SearchType": {
372 | "kind": {"String": "match"},
373 | "pattern": {"String": "https://example.com/users/.*/resources"},
374 | "options": [
375 | {
376 | "name": {"String": "ignorecase"},
377 | "value": {"Boolean": True},
378 | }
379 | ],
380 | }
381 | },
382 | }
383 | } == visit_condition(parse_condition("input.url is not " + 'match("https://example.com/users/.*/resources",ignorecase=true)', {}))
384 | assert {
385 | "SearchMatchesExpression": {
386 | "lhs": {"Input": "input.url"},
387 | "rhs": {
388 | "SearchType": {
389 | "kind": {"String": "regex"},
390 | "pattern": {"String": "example.com/foo"},
391 | "options": [
392 | {
393 | "name": {"String": "ignorecase"},
394 | "value": {"Boolean": True},
395 | }
396 | ],
397 | }
398 | },
399 | }
400 | } == visit_condition(parse_condition('input.url is regex("example.com/foo",ignorecase=true)', {}))
401 |
402 | assert {
403 | "SelectAttrExpression": {
404 | "lhs": {"Input": "input.persons"},
405 | "rhs": {
406 | "key": {"String": "person.age"},
407 | "operator": {"String": ">="},
408 | "value": {"Integer": 50},
409 | },
410 | }
411 | } == visit_condition(parse_condition('input.persons is selectattr("person.age", ">=", 50)', {}))
412 |
413 | assert {
414 | "SelectAttrExpression": {
415 | "lhs": {"Input": "input.persons"},
416 | "rhs": {
417 | "key": {"String": "person.employed"},
418 | "operator": {"String": "=="},
419 | "value": {"Boolean": True},
420 | },
421 | }
422 | } == visit_condition(parse_condition('input.persons is selectattr("person.employed", "==", true)', {}))
423 |
424 | assert {
425 | "SelectAttrNotExpression": {
426 | "lhs": {"Input": "input.persons"},
427 | "rhs": {
428 | "key": {"String": "person.name"},
429 | "operator": {"String": "=="},
430 | "value": {"String": "fred"},
431 | },
432 | }
433 | } == visit_condition(parse_condition('input.persons is not selectattr("person.name", "==", "fred")', {}))
434 |
435 | assert {
436 | "SelectExpression": {
437 | "lhs": {"Input": "input.ids"},
438 | "rhs": {"operator": {"String": ">="}, "value": {"Integer": 10}},
439 | }
440 | } == visit_condition(parse_condition('input.ids is select(">=", 10)', {}))
441 |
442 | assert {
443 | "SelectNotExpression": {
444 | "lhs": {"Input": "input.persons"},
445 | "rhs": {
446 | "operator": {"String": "regex"},
447 | "value": {"String": "fred|barney"},
448 | },
449 | }
450 | } == visit_condition(
451 | parse_condition('input.persons is not select("regex", "fred|barney")', {}),
452 | )
453 |
454 | assert {
455 | "SelectExpression": {
456 | "lhs": {"Input": "input.is_true"},
457 | "rhs": {"operator": {"String": "=="}, "value": {"Boolean": False}},
458 | }
459 | } == visit_condition(parse_condition('input.is_true is select("==", False)', {}))
460 |
461 | assert {
462 | "SelectExpression": {
463 | "lhs": {"Input": "input.my_list"},
464 | "rhs": {
465 | "operator": {"String": "=="},
466 | "value": {"Input": "input.my_int"},
467 | },
468 | }
469 | } == visit_condition(parse_condition("input.my_list is select('==', input.my_int)", {}))
470 |
471 | assert {
472 | "SelectExpression": {
473 | "lhs": {"Input": "input.my_list"},
474 | "rhs": {
475 | "operator": {"String": "=="},
476 | "value": {"Variable": "my_int"},
477 | },
478 | }
479 | } == visit_condition(parse_condition("input.my_list is select('==', my_int)", {"my_int": 42}))
480 |
481 | assert {
482 | "SelectAttrExpression": {
483 | "lhs": {"Input": "input.persons"},
484 | "rhs": {
485 | "key": {"String": "person.age"},
486 | "operator": {"String": ">"},
487 | "value": {"Variable": "minimum_age"},
488 | },
489 | }
490 | } == visit_condition(parse_condition("input.persons is selectattr('person.age', '>', minimum_age)", dict(minimum_age=42)))
491 |
492 |
493 | def test_invalid_select_operator():
494 | with pytest.raises(SelectOperatorException):
495 | parse_condition('input.persons is not select("in", ["fred","barney"])', {})
496 |
497 |
498 | def test_invalid_selectattr_operator():
499 | with pytest.raises(SelectattrOperatorException):
500 | parse_condition('input.persons is not selectattr("name", "cmp", "fred")', {})
501 |
502 |
503 | def test_null_type():
504 | assert {
505 | "EqualsExpression": {
506 | "lhs": {"Input": "input.friend"},
507 | "rhs": {"NullType": None},
508 | }
509 | } == visit_condition(parse_condition("input.friend == null", {}))
510 |
511 |
512 | @pytest.mark.parametrize(
513 | "policybook",
514 | [
515 | "policies_with_multiple_conditions.yml",
516 | "policies_with_multiple_conditions2.yml",
517 | "policies_with_multiple_conditions3.yml",
518 | "policies_with_multiple_conditions4.yml",
519 | ],
520 | )
521 | def test_generate_dict_policysets(policybook):
522 |
523 | os.chdir(HERE)
524 | with open(os.path.join("policybooks", policybook)) as f:
525 | data = yaml.safe_load(f.read())
526 |
527 | policyset = generate_dict_policysets(parse_policy_sets(data))
528 | print(yaml.dump(policyset))
529 |
530 | with open(os.path.join("asts", policybook)) as f:
531 | ast = yaml.safe_load(f.read())
532 |
533 | assert policyset == ast
534 |
--------------------------------------------------------------------------------