├── 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 | ap-arch 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 | --------------------------------------------------------------------------------