├── setup.py ├── .flake8 ├── docs ├── rules │ ├── R114_file_change.md │ ├── R101_command_exec.md │ ├── R103_download_exec.md │ ├── R115_file_deletion.md │ ├── R301_non_fqcn_use.md │ ├── R106_inbound_transfer.md │ ├── R110_non_builtin_use.md │ ├── R105_outbound_transfer.md │ ├── R109_key_config_change.md │ ├── R108_privilege_escalation.md │ ├── R102_command_instead_of_shell.md │ ├── R116_insecure_file_permission.md │ ├── R104_unauthorized_download_src.md │ ├── R111_parameterized_import_role.md │ ├── R113_parameterized_pkg_install.md │ ├── R112_parameterized_import_taskfile.md │ └── R107_pkg_install_with_insecure_option.md ├── installing.md ├── index.md └── annotation.md ├── test ├── testdata │ ├── roles │ │ └── test_role │ │ │ ├── defaults │ │ │ └── main.yml │ │ │ ├── tasks │ │ │ └── main.yml │ │ │ └── meta │ │ │ └── main.yml │ ├── projects │ │ └── my.collection │ │ │ ├── roles │ │ │ └── sample-role-1 │ │ │ │ ├── defaults │ │ │ │ └── main.yml │ │ │ │ ├── tasks │ │ │ │ └── main.yml │ │ │ │ └── meta │ │ │ │ └── main.yml │ │ │ ├── galaxy.yml │ │ │ └── MANIFEST.json │ ├── files │ │ ├── test_line_number2.yml │ │ └── test_line_number.yml │ └── inline_replace_data │ │ ├── block_and_when_play.yml │ │ └── block_and_when_play_fixed.yml ├── test_inline_replace.py └── test_scanner.py ├── doc └── images │ ├── ari-arch.png │ ├── ari-overview.png │ ├── ari-ram-list.png │ └── ari-apply-rules.png ├── ansible_risk_insight ├── requirements.txt ├── rules │ ├── R110_non_builtin_use.md │ ├── R101_command_exec.md │ ├── R111_parameterized_import_role.md │ ├── R109_key_config_change.md │ ├── R301_non_fqcn_use.md │ ├── R114_file_change.md │ ├── R112_parameterized_import_taskfile.md │ ├── R102_command_instead_of_shell.md │ ├── R116_insecure_file_permission.md │ ├── R106_inbound_transfer.md │ ├── R113_parameterized_pkg_install.md │ ├── R115_file_deletion.md │ ├── R105_outbound_transfer.md │ ├── R108_privilege_escalation.md │ ├── __init__.py │ ├── R107_pkg_install_with_insecure_option.md │ ├── R103_download_exec.md │ ├── R104_unauthorized_download_src.md │ ├── R303_task_without_name.py │ ├── R302_role_without_metadata.py │ ├── sample_rule.py │ ├── R108_privilege_escalation.py │ ├── R304_unresolved_module.py │ ├── R402_list_all_used_variables.py │ ├── R110_non_builtin_use.py │ ├── R116_insecure_file_permission.py │ ├── R117_external_role.py │ ├── R102_command_instead_of_shell.py │ ├── R205_unnecessary_include_vars.py │ ├── R305_unresolved_role.py │ ├── R111_parameterized_import_role.py │ ├── R109_key_config_change.py │ ├── R306_undefined_variable.py │ ├── R101_command_exec.py │ ├── R301_non_fqcn_use.py │ ├── R105_outbound_transfer.py │ ├── R115_file_deletion.py │ ├── R113_parameterized_pkg_install.py │ ├── R201_changed_data_dependence.py │ ├── R401_list_all_inbound_src.py │ ├── R404_show_variables.py │ ├── R106_inbound_transfer.py │ ├── R112_parameterized_import_taskfile.py │ ├── R501_dependency_suggestion.py │ ├── R107_pkg_install_with_insecure_option.py │ ├── R114_file_change.py │ ├── R202_unconditional_override.py │ ├── R203_unused_override.py │ ├── R204_unnecessary_set_fact.py │ ├── R104_unauthorized_download_src.py │ ├── R103_download_exec.py │ └── P004_variable_validation.py ├── task_keywords.txt ├── annotators │ ├── __init__.py │ ├── module_annotator_base.py │ ├── ansible_builtin.py │ ├── ansible.builtin │ │ ├── git.py │ │ ├── get_url.py │ │ ├── subversion.py │ │ ├── pip.py │ │ ├── rpm_key.py │ │ ├── apt.py │ │ ├── raw.py │ │ ├── shell.py │ │ ├── command.py │ │ ├── script.py │ │ ├── assemble.py │ │ ├── replace.py │ │ ├── blockinfile.py │ │ ├── expect.py │ │ ├── dnf.py │ │ ├── yum.py │ │ ├── file.py │ │ ├── template.py │ │ ├── lineinfile.py │ │ ├── uri.py │ │ ├── apt_key.py │ │ └── unarchive.py │ ├── annotator_base.py │ ├── sample_custom_annotator.py │ └── risk_annotator_base.py ├── _version.py ├── batch.sh ├── key_test.py ├── cli │ └── ram │ │ ├── list.py │ │ ├── release.py │ │ ├── diff.py │ │ ├── search.py │ │ ├── update.py │ │ ├── __init__.py │ │ └── generate.py ├── builtin-modules.txt ├── logger.py ├── __init__.py ├── yaml.py ├── ansible_variables.txt ├── findings.py ├── awx_utils.py └── analyzer.py ├── .vscode └── launch.json ├── .pre-commit-config.yaml ├── Makefile ├── example ├── playbooks │ └── sample_playbook.yml ├── readme.md ├── rules │ └── sample_rule.py └── sample.py ├── tox.ini ├── .github └── workflows │ ├── test.yml │ └── lint.yml ├── CONTRIBUTING.md ├── pyproject.toml ├── mkdocs.yml ├── data-struct.txt └── .gitignore /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore=W503,E203 3 | max-line-length=150 4 | -------------------------------------------------------------------------------- /docs/rules/R114_file_change.md: -------------------------------------------------------------------------------- 1 | ../../ansible_risk_insight/rules/R114_file_change.md -------------------------------------------------------------------------------- /docs/rules/R101_command_exec.md: -------------------------------------------------------------------------------- 1 | ../../ansible_risk_insight/rules/R101_command_exec.md -------------------------------------------------------------------------------- /docs/rules/R103_download_exec.md: -------------------------------------------------------------------------------- 1 | ../../ansible_risk_insight/rules/R103_download_exec.md -------------------------------------------------------------------------------- /docs/rules/R115_file_deletion.md: -------------------------------------------------------------------------------- 1 | ../../ansible_risk_insight/rules/R115_file_deletion.md -------------------------------------------------------------------------------- /docs/rules/R301_non_fqcn_use.md: -------------------------------------------------------------------------------- 1 | ../../ansible_risk_insight/rules/R301_non_fqcn_use.md -------------------------------------------------------------------------------- /docs/rules/R106_inbound_transfer.md: -------------------------------------------------------------------------------- 1 | ../../ansible_risk_insight/rules/R106_inbound_transfer.md -------------------------------------------------------------------------------- /docs/rules/R110_non_builtin_use.md: -------------------------------------------------------------------------------- 1 | ../../ansible_risk_insight/rules/R110_non_builtin_use.md -------------------------------------------------------------------------------- /test/testdata/roles/test_role/defaults/main.yml: -------------------------------------------------------------------------------- 1 | download_source_url: "" -------------------------------------------------------------------------------- /docs/rules/R105_outbound_transfer.md: -------------------------------------------------------------------------------- 1 | ../../ansible_risk_insight/rules/R105_outbound_transfer.md -------------------------------------------------------------------------------- /docs/rules/R109_key_config_change.md: -------------------------------------------------------------------------------- 1 | ../../ansible_risk_insight/rules/R109_key_config_change.md -------------------------------------------------------------------------------- /docs/rules/R108_privilege_escalation.md: -------------------------------------------------------------------------------- 1 | ../../ansible_risk_insight/rules/R108_privilege_escalation.md -------------------------------------------------------------------------------- /docs/rules/R102_command_instead_of_shell.md: -------------------------------------------------------------------------------- 1 | ../../ansible_risk_insight/rules/R102_command_instead_of_shell.md -------------------------------------------------------------------------------- /docs/rules/R116_insecure_file_permission.md: -------------------------------------------------------------------------------- 1 | ../../ansible_risk_insight/rules/R116_insecure_file_permission.md -------------------------------------------------------------------------------- /doc/images/ari-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-risk-insight/HEAD/doc/images/ari-arch.png -------------------------------------------------------------------------------- /docs/rules/R104_unauthorized_download_src.md: -------------------------------------------------------------------------------- 1 | ../../ansible_risk_insight/rules/R104_unauthorized_download_src.md -------------------------------------------------------------------------------- /docs/rules/R111_parameterized_import_role.md: -------------------------------------------------------------------------------- 1 | ../../ansible_risk_insight/rules/R111_parameterized_import_role.md -------------------------------------------------------------------------------- /docs/rules/R113_parameterized_pkg_install.md: -------------------------------------------------------------------------------- 1 | ../../ansible_risk_insight/rules/R113_parameterized_pkg_install.md -------------------------------------------------------------------------------- /doc/images/ari-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-risk-insight/HEAD/doc/images/ari-overview.png -------------------------------------------------------------------------------- /doc/images/ari-ram-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-risk-insight/HEAD/doc/images/ari-ram-list.png -------------------------------------------------------------------------------- /docs/rules/R112_parameterized_import_taskfile.md: -------------------------------------------------------------------------------- 1 | ../../ansible_risk_insight/rules/R112_parameterized_import_taskfile.md -------------------------------------------------------------------------------- /test/testdata/projects/my.collection/roles/sample-role-1/defaults/main.yml: -------------------------------------------------------------------------------- 1 | download_source_url: "" -------------------------------------------------------------------------------- /doc/images/ari-apply-rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-risk-insight/HEAD/doc/images/ari-apply-rules.png -------------------------------------------------------------------------------- /docs/rules/R107_pkg_install_with_insecure_option.md: -------------------------------------------------------------------------------- 1 | ../../ansible_risk_insight/rules/R107_pkg_install_with_insecure_option.md -------------------------------------------------------------------------------- /test/testdata/projects/my.collection/roles/sample-role-1/tasks/main.yml: -------------------------------------------------------------------------------- 1 | 2 | - name: include gcloud role in google.cloud collection 3 | include_role: gcloud 4 | -------------------------------------------------------------------------------- /ansible_risk_insight/requirements.txt: -------------------------------------------------------------------------------- 1 | gitdb==4.0.9 2 | GitPython==3.1.41 3 | joblib==1.2.0 4 | jsonpickle==2.2.0 5 | PyYAML==6.0 6 | smmap==5.0.0 7 | tabulate==0.8.10 8 | -------------------------------------------------------------------------------- /docs/installing.md: -------------------------------------------------------------------------------- 1 | # Installing 2 | 3 | ## Installing from GitHub 4 | 5 | You can install ARI from GitHub source code using `pip` command. 6 | 7 | ```bash 8 | $ pip install git+https://github.com/ansible/ansible-risk-insight.git 9 | ``` -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R110_non_builtin_use.md: -------------------------------------------------------------------------------- 1 | ## non builtin use 2 | The `non builtin use` rule identifies the use of a non builtin module. 3 | 4 | ### Problematic code 5 | 6 | ``` 7 | 8 | ``` 9 | ### Correct code 10 | 11 | ``` 12 | 13 | ``` -------------------------------------------------------------------------------- /test/testdata/roles/test_role/tasks/main.yml: -------------------------------------------------------------------------------- 1 | 2 | - name: "download a file from parameterized source" 3 | get_url: 4 | url: "{{ download_source_url }}" 5 | dest: /etc/install.sh 6 | mode: '0755' 7 | 8 | - name: "execute the downloaded file" 9 | command: "/etc/install.sh" 10 | -------------------------------------------------------------------------------- /test/testdata/files/test_line_number2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | gather_facts: no 4 | 5 | tags: 6 | - test 7 | 8 | collections: 9 | - community.general 10 | 11 | tasks: 12 | - name: Task name here 13 | import_role: 14 | name: abc 15 | 16 | - import_role: 17 | name: def 18 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R101_command_exec.md: -------------------------------------------------------------------------------- 1 | ## command exec 2 | The `command exec` rule checks whether a task executes parameterized command. 3 | 4 | ### Problematic code 5 | 6 | ``` 7 | - name: Run command. 8 | command: bash {{ install_script }} # <-- This parameter can be overwritten. 9 | ``` 10 | ### Correct code 11 | 12 | ``` 13 | - name: Run command. 14 | command: bash /tmp/install_script.sh 15 | ``` -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Python Debugger: Remote Attach", 6 | "type": "debugpy", 7 | "request": "attach", 8 | "connect": { 9 | "host": "localhost", 10 | "port": 5678 11 | }, 12 | "justMyCode": false 13 | }, 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R111_parameterized_import_role.md: -------------------------------------------------------------------------------- 1 | ## parameterized import role 2 | The `parameterized import role` rule checks whether a task imports or includes a parameterized role. 3 | 4 | ### Problematic code 5 | 6 | ``` 7 | tasks: 8 | - ansible.builtin.import_role: 9 | name: {{ my_role }} 10 | 11 | ``` 12 | ### Correct code 13 | 14 | ``` 15 | tasks: 16 | - ansible.builtin.import_role: 17 | name: myrole 18 | ``` -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Ansible Risk Insight Documentation 2 | 3 | ## About Ansible Risk Insight 4 | 5 | Ansible Risk Insight (ARI) is a command-line tool for assessing quality and potential risks of the Ansible content of playbooks, roles, and collections. ARI understands the taxonomy of Ansible data structures and content types and do context analysis on task call tree across dependencies and variable tracking in static-analytics fashion. ARI can be extensible with customizable rules. 6 | 7 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R109_key_config_change.md: -------------------------------------------------------------------------------- 1 | ## key config change 2 | The `key config change` rule identifies parameterized key change. 3 | 4 | ### Problematic code 5 | 6 | ``` 7 | - name: Import a key from a url 8 | ansible.builtin.rpm_key: 9 | state: present 10 | key: http://{{ key_server }}/RPM-GPG-KEY.dag.txt 11 | 12 | ``` 13 | ### Correct code 14 | 15 | ``` 16 | - name: Import a key from a url 17 | ansible.builtin.rpm_key: 18 | state: present 19 | key: http://apt.sw.be/RPM-GPG-KEY.dag.txt 20 | ``` -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R301_non_fqcn_use.md: -------------------------------------------------------------------------------- 1 | ## non fqcn use 2 | The `non fqcn use` rule checks whether a task uses a short name module. 3 | 4 | ### Problematic code 5 | 6 | ``` 7 | - name: Install collection community.network 8 | ansible_galaxy_install: 9 | type: collection 10 | name: community.network 11 | ``` 12 | 13 | ### Correct code 14 | 15 | ``` 16 | - name: Install collection community.network 17 | community.general.ansible_galaxy_install: 18 | type: collection 19 | name: community.network 20 | ``` -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R114_file_change.md: -------------------------------------------------------------------------------- 1 | ## file change 2 | The `file change` rule identifies parameterized file change. 3 | 4 | ### Problematic code 5 | 6 | ``` 7 | - name: Update sshd configuration 8 | ansible.builtin.template: 9 | src: {{ sshd_config }} # <-- This parameter can be overwritten. 10 | dest: /etc/ssh/sshd_config 11 | ``` 12 | ### Correct code 13 | 14 | ``` 15 | - name: Update sshd configuration 16 | ansible.builtin.template: 17 | src: etc/ssh/sshd_config.j2 18 | dest: /etc/ssh/sshd_config 19 | ``` -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R112_parameterized_import_taskfile.md: -------------------------------------------------------------------------------- 1 | 2 | ## parameterized import taskfile 3 | The `parameterized import taskfile` rule identifies whether a task imports or includes a parameterized task file. 4 | 5 | ### Problematic code 6 | 7 | ``` 8 | - name: Include task list in play 9 | ansible.builtin.import_tasks: 10 | file: {{ task_file }} 11 | 12 | ``` 13 | ### Correct code 14 | 15 | ``` 16 | - name: Include task list in play 17 | ansible.builtin.import_tasks: 18 | file: stuff.yaml 19 | ``` -------------------------------------------------------------------------------- /.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" 24 | -------------------------------------------------------------------------------- /test/testdata/roles/test_role/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: [] 3 | 4 | galaxy_info: 5 | role_name: test_role 6 | author: test_role 7 | description: A role used for test 8 | company: "N/A" 9 | license: "license (BSD, MIT)" 10 | min_ansible_version: 2.0 11 | platforms: 12 | - name: EL 13 | versions: 14 | - 7 15 | - 8 16 | - name: Debian 17 | versions: 18 | - all 19 | - name: Ubuntu 20 | versions: 21 | - all 22 | galaxy_tags: 23 | - development 24 | - repository 25 | - ci -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R102_command_instead_of_shell.md: -------------------------------------------------------------------------------- 1 | ## command instead of shell 2 | The `command instead of shell` rule checks whether a task uses `shell` module instead of `command` module. 3 | If you want to run a command predictably and securely, it is recommended to use the command module instead of the shell. 4 | 5 | ### Problematic code 6 | 7 | ``` 8 | - name: Cat /etc/foo.conf 9 | ansible.builtin.shell: cat /etc/foo.conf 10 | ``` 11 | 12 | ### Correct code 13 | ``` 14 | - name: Cat /etc/foo.conf 15 | ansible.builtin.command: cat /etc/foo.conf 16 | ``` -------------------------------------------------------------------------------- /test/testdata/projects/my.collection/galaxy.yml: -------------------------------------------------------------------------------- 1 | namespace: my 2 | name: collection 3 | version: 1.2.3 4 | readme: README.md 5 | authors: 6 | - Ansible (https://github.com/ansible) 7 | description: a collection for test 8 | license_file: null 9 | tags: 10 | - test 11 | dependencies: 12 | google.cloud: '>=1.0.2' 13 | repository: https://github.com/ansible/ansible-risk-insight 14 | documentation: https://github.com/ansible/ansible-risk-insight 15 | homepage: https://github.com/ansible/ansible-risk-insight 16 | issues: https://github.com/ansible/ansible-risk-insight 17 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R116_insecure_file_permission.md: -------------------------------------------------------------------------------- 1 | ## insecure file permission 2 | The `insecure file permission` rule checks whether a task gives insecure permissions to a file. 3 | 4 | ### Problematic code 5 | 6 | ``` 7 | - name: Change file permissions 8 | ansible.builtin.file: 9 | path: /work 10 | owner: root 11 | group: root 12 | mode: '1777' 13 | ``` 14 | ### Correct code 15 | 16 | ``` 17 | - name: Change file permissions 18 | ansible.builtin.file: 19 | path: /work 20 | owner: root 21 | group: root 22 | mode: '0755' 23 | ``` -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: install 3 | install: 4 | @echo installing ARI using pip 5 | pip install -e . 6 | 7 | 8 | .PHONY: test 9 | test: 10 | @echo testing codes with pytest 11 | pytest -s test 12 | 13 | 14 | .PHONY: black 15 | black: 16 | @echo linting codes with black \(auto-fix\) 17 | black ansible_risk_insight --line-length=150 --include='\.pyi?$$' --exclude="\.git|\.hg|\.mypy_cache|\.tox|\.venv|_build|buck-out|build|dist" 18 | 19 | 20 | .PHONY: flake8 21 | flake8: 22 | @echo linting codes with flake8 23 | flake8 24 | 25 | 26 | .PHONY: lint 27 | lint: black flake8 -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R106_inbound_transfer.md: -------------------------------------------------------------------------------- 1 | ## inbound data transfer 2 | The `inbound data transfer` rule identifies that an inbound data transfer from a parameterized source. 3 | 4 | ### Problematic code 5 | 6 | ``` 7 | - name: Download file 8 | ansible.builtin.get_url: 9 | url: https://{{ example_url }}/path/file.conf # <-- This parameter can be overwritten. 10 | dest: /etc/file.conf 11 | ``` 12 | ### Correct code 13 | 14 | ``` 15 | - name: Download file 16 | ansible.builtin.get_url: 17 | url: https://example.com/path/file.conf 18 | dest: /etc/file.conf 19 | ``` -------------------------------------------------------------------------------- /test/testdata/projects/my.collection/roles/sample-role-1/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: [] 3 | 4 | galaxy_info: 5 | role_name: sample-role-1 6 | author: test_role 7 | description: A role used for test 8 | company: "N/A" 9 | license: "license (BSD, MIT)" 10 | min_ansible_version: 2.0 11 | platforms: 12 | - name: EL 13 | versions: 14 | - 7 15 | - 8 16 | - name: Debian 17 | versions: 18 | - all 19 | - name: Ubuntu 20 | versions: 21 | - all 22 | galaxy_tags: 23 | - development 24 | - repository 25 | - ci -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R113_parameterized_pkg_install.md: -------------------------------------------------------------------------------- 1 | ## parameterized package install 2 | The `parameterized package install` rule identifies parameterized package installation. 3 | 4 | ### Problematic code 5 | 6 | ``` 7 | - name: Install the nginx rpm from a remote repo 8 | ansible.builtin.yum: 9 | name: {{ nginx_rpm_url }} 10 | state: present 11 | ``` 12 | ### Correct code 13 | 14 | ``` 15 | - name: Install the nginx rpm from a remote repo 16 | ansible.builtin.yum: 17 | name: http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm 18 | state: present 19 | ``` -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R115_file_deletion.md: -------------------------------------------------------------------------------- 1 | ## file deletion 2 | The `file deletion` rule identifies parameterized file deletion. 3 | If state option is absent, directories will be recursively deleted, and files or symlinks will be unlinked. 4 | 5 | ### Problematic code 6 | 7 | ``` 8 | - name: Recursively remove directory 9 | ansible.builtin.file: 10 | path: {{ path_to_dir }} # <-- This parameter can be overwritten. 11 | state: absent 12 | 13 | ``` 14 | ### Correct code 15 | 16 | ``` 17 | - name: Recursively remove directory 18 | ansible.builtin.file: 19 | path: /etc/foo 20 | state: absent 21 | ``` -------------------------------------------------------------------------------- /ansible_risk_insight/task_keywords.txt: -------------------------------------------------------------------------------- 1 | action 2 | any_errors_fatal 3 | args 4 | async 5 | become 6 | become_exe 7 | become_flags 8 | become_method 9 | become_user 10 | changed_when 11 | check_mode 12 | collections 13 | connection 14 | debugger 15 | delay 16 | delegate_facts 17 | delegate_to 18 | diff 19 | environment 20 | failed_when 21 | ignore_errors 22 | ignore_unreachable 23 | local_action 24 | loop 25 | loop_control 26 | module_defaults 27 | name 28 | no_log 29 | notify 30 | poll 31 | port 32 | register 33 | remote_user 34 | retries 35 | run_once 36 | tags 37 | throttle 38 | timeout 39 | until 40 | vars 41 | when 42 | listen -------------------------------------------------------------------------------- /example/playbooks/sample_playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: AWS Cloud operations 3 | hosts: localhost 4 | connection: local 5 | 6 | tasks: 7 | - name: Create a cloud instance 8 | ec2_instance: 9 | state: running 10 | name: k8s_master 11 | region: us-east-1 12 | security_group: default 13 | instance_type: t2.micro 14 | image_id: ami-xxxxxx 15 | key_name: mykey 16 | tags: 17 | Name: K8s Master 18 | vpc_subnet_id: subnet-xxxxx identifier 19 | wait: true 20 | aws_access_key: "1" 21 | aws_secret_key: "1" 22 | register: x -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R105_outbound_transfer.md: -------------------------------------------------------------------------------- 1 | ## outbound data transfer 2 | The `outbound data transfer` rule identifies that an outbound data transfer from a parameterized source. 3 | 4 | ### Problematic code 5 | 6 | ``` 7 | - name: POST from contents of remote file 8 | ansible.builtin.uri: 9 | url: {{ url }} 10 | method: POST 11 | src: /path/to/my/file.json 12 | remote_src: yes 13 | ``` 14 | ### Correct code 15 | 16 | ``` 17 | - name: POST from contents of remote file 18 | ansible.builtin.uri: 19 | url: https://httpbin.org/post 20 | method: POST 21 | src: /path/to/my/file.json 22 | remote_src: yes 23 | ``` -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R108_privilege_escalation.md: -------------------------------------------------------------------------------- 1 | ## privilege escalation 2 | The `privilege escalation` rule identifies a task execution with root privileges or with another user’s permissions. 3 | 4 | ### Problematic code 5 | 6 | ``` 7 | - name: Run command if /path/to/database does not exist (without 'args') 8 | ansible.builtin.command: /usr/bin/make_database.sh db_user db_name creates=/path/to/database 9 | become: true 10 | ``` 11 | ### Correct code 12 | 13 | ``` 14 | - name: Run command if /path/to/database does not exist (without 'args') 15 | ansible.builtin.command: /usr/bin/make_database.sh db_user db_name creates=/path/to/database 16 | ``` -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist=True 3 | envlist=py39,pylint,black,flake8 4 | 5 | [testenv] 6 | usedevelop = True 7 | description = Check with Pytest 8 | deps = 9 | pytest 10 | commands = pytest -s {posargs:test} 11 | 12 | [testenv:black] 13 | description = Check with the Black code formatter 14 | deps = 15 | black 16 | commands = black ansible_risk_insight --check --line-length=150 17 | 18 | [testenv:flake8] 19 | description = Check with YAMLlint 20 | deps = 21 | flake8 22 | commands = flake8 {toxinidir}/ansible_risk_insight 23 | 24 | [testenv:lint] 25 | description = Check with the Black code formatter 26 | deps = 27 | black 28 | commands = black {toxinidir}/ansible_risk_insight --check --line-length=150 29 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R107_pkg_install_with_insecure_option.md: -------------------------------------------------------------------------------- 1 | ## pkg install with insecure option 2 | The `pkg install with insecure option` rule identifies an pkg installation with insecure option. 3 | 4 | ### Problematic code 5 | 6 | ``` 7 | - name: Install the nginx rpm from a remote repo 8 | ansible.builtin.yum: 9 | name: http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm 10 | state: present 11 | validate_certs: false 12 | ``` 13 | ### Correct code 14 | 15 | ``` 16 | - name: Install the nginx rpm from a remote repo 17 | ansible.builtin.yum: 18 | name: http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm 19 | state: present 20 | ``` -------------------------------------------------------------------------------- /ansible_risk_insight/_version.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | __version__ = "0.2.10" 18 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R103_download_exec.md: -------------------------------------------------------------------------------- 1 | ## download exec 2 | The `download exec` rule checks whether a task executes downloaded file from parameterized source. 3 | 4 | ### Problematic code 5 | 6 | ``` 7 | - name: Download sample app installation script. 8 | get_url: 9 | url: "{{ app_installation_script_url }}" # <-- This parameter can be overwritten. 10 | dest: /tmp/install_script.sh 11 | 12 | - name: Install sample app. 13 | command: bash /tmp/install_script.sh 14 | ``` 15 | ### Correct code 16 | 17 | ``` 18 | - name: Download sample app installation script. 19 | get_url: 20 | url: https://example.com/path/install_script.sh 21 | dest: /tmp/install_script.sh 22 | 23 | - name: Install sample app. 24 | command: bash /tmp/install_script.sh 25 | ``` -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R104_unauthorized_download_src.md: -------------------------------------------------------------------------------- 1 | ## unauthorized download source 2 | The `unauthorized download source` rule checks whether a task download a source from an authorized location. 3 | Authorized locations can be defined using the allow url list and deny url list. 4 | 5 | ### Problematic code 6 | 7 | ``` 8 | # allow_url_list = ["https://valid*", "https://myurl*"] 9 | 10 | - name: Download sample app installation script. 11 | get_url: 12 | url: https://invalid.example.com/path/install_script.sh 13 | dest: /tmp/install_script.sh 14 | ``` 15 | ### Correct code 16 | 17 | ``` 18 | # allow_url_list = ["https://valid*", "https://myurl*"] 19 | 20 | - name: Download sample app installation script. 21 | get_url: 22 | url: https://valid.example.com/path/install_script.sh 23 | dest: /tmp/install_script.sh 24 | ``` -------------------------------------------------------------------------------- /ansible_risk_insight/batch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dir=$1 4 | outdir=$2 5 | resume_role_name=$3 6 | 7 | echo "execute serial_resolver.py for all roles in \"$dir\"" 8 | 9 | role_dirs=$(ls -1 -d $dir/*/) 10 | 11 | start="true" 12 | if [[ $resume_role_name != "" ]]; then 13 | start="false" 14 | fi 15 | 16 | num=$(echo -e "$role_dirs" | wc -l | sed 's/ //g') 17 | i=1 18 | 19 | IFS=$'\n' 20 | for role_dir in $role_dirs; do 21 | basename=$(basename $role_dir) 22 | # echo $basename 23 | if [[ $resume_role_name == $basename ]];then 24 | start="true" 25 | fi 26 | if [[ $start != "true" ]]; then 27 | echo "[$i/$num] $basename skipped." 28 | i=$((i+1)) 29 | continue 30 | fi 31 | output="$outdir/$basename.json" 32 | echo "[$i/$num] $basename" 33 | python serial_resolver.py -r $role_dir -o $output 34 | i=$((i+1)) 35 | done 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /test/testdata/files/test_line_number.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Copy Apache configuration file over and restart httpd 3 | 4 | hosts: all 5 | tasks: 6 | - name: Copy Apache configuration file over 7 | copy: 8 | src: /etc/httpd/conf/httpd.conf 9 | dest: /etc/httpd/conf/httpd.conf.bak 10 | remote_src: true 11 | owner: root 12 | group: root 13 | mode: "0644" 14 | - name: Restart httpd 15 | service: 16 | name: httpd 17 | state: restarted 18 | notify: Restart httpd 19 | handlers: 20 | - name: Restart httpd 21 | service: 22 | name: httpd 23 | state: restarted 24 | - name: aaa 25 | hosts: all 26 | tasks: 27 | # identical task to the last task in the previous play 28 | # line_num should be [28, 32], not [13, 17] 29 | - name: Restart httpd 30 | service: 31 | name: httpd 32 | state: restarted 33 | notify: Restart httpd 34 | ... 35 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: pytest 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - '**' 8 | - '!**.md' 9 | - '!doc/**' 10 | - '!**.txt' 11 | - '!LICENSE' 12 | - 'test/**' 13 | branches: ['main', 'release-*'] 14 | pull_request: 15 | 16 | jobs: 17 | pytest: 18 | name: Run tests with pytest 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | python-version: [3.11] 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v2 26 | - name: Set up Python ${{ matrix.python-version }} 27 | uses: actions/setup-python@v1 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | - name: Install ARI 31 | run: pip install -e . 32 | - name: Install pytest 33 | run: pip install pytest 34 | - name: Run Tests 35 | env: 36 | ARI_LOG_LEVEL: debug 37 | run: pytest test -s 38 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Lint 4 | 5 | on: 6 | workflow_dispatch: 7 | push: 8 | paths: 9 | - '**' 10 | - '!**.md' 11 | - '!doc/**' 12 | - '!**.txt' 13 | - '!LICENSE' 14 | - 'test/**' 15 | branches: ['main', 'release-*'] 16 | pull_request: 17 | 18 | jobs: 19 | flake8_py3: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Setup Python 23 | uses: actions/setup-python@v1 24 | with: 25 | python-version: '3.9' 26 | architecture: x64 27 | - name: Checkout 28 | uses: actions/checkout@v2 29 | - name: Install flake8 30 | run: pip install flake8 31 | - name: Run flake8 32 | uses: suo/flake8-github-action@releases/v1 33 | with: 34 | checkName: 'flake8_py3' # NOTE: this needs to be the same as the job name 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /test/testdata/projects/my.collection/MANIFEST.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection_info": { 3 | "description": "your collection description", 4 | "repository": "http://example.com/repository", 5 | "tags": [], 6 | "dependencies": { 7 | "google.cloud": ">=1.0.2" 8 | }, 9 | "authors": [ 10 | "your name " 11 | ], 12 | "issues": "http://example.com/issue/tracker", 13 | "license": [ 14 | "GPL-2.0-or-later" 15 | ], 16 | "documentation": "http://docs.example.com", 17 | "namespace": "my", 18 | "name": "collection", 19 | "version": "1.2.3", 20 | "readme": "README.md", 21 | "license_file": null, 22 | "homepage": "http://example.com" 23 | }, 24 | "file_manifest_file": { 25 | "format": 1, 26 | "ftype": "file", 27 | "chksum_sha256": "12dc56b5d698075ee97a2e66e04b71064c1b36ddd7276c4f10666bae10e1cade", 28 | "name": "FILES.json", 29 | "chksum_type": "sha256" 30 | }, 31 | "format": 1 32 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | For contributing to this repository, please first submit an issue via an [issue](https://github.com/ansible/ansible-risk-insight/issues). 4 | All the related PRs should refer to this issue. 5 | 6 | ## Pull Request Process 7 | 8 | 1. Create an [issue](https://github.com/ansible/ansible-risk-insight/issues). 9 | 2. Fork the ansible-risk-insight repository to your own github account and clone it locally. 10 | 3. Apply your changes to the local codes. 11 | 4. Update the README.md and some other documents if needed, especially if you change any interface such as CLI parameters, environment variables and data models. 12 | 5. Write your commit message to describe your changes concisely. 13 | 6. Submit a pull request with your sign-off signature like `Signed-off-by: XXXX xxxx@example.com` 14 | 7. Ensure that CI passes, if it fails, fix the failures. 15 | 8. Every pull request requires a review before merging. 16 | 9. If your pull request consists of more than one commit, your commits will be squashed when the PR is merged. -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "setuptools-scm"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "ansible-risk-insight" 7 | description = "My package description" 8 | readme = "README.rst" 9 | requires-python = ">=3.7" 10 | keywords = ["one", "two"] 11 | license = {text = "Apache License 2.0"} 12 | classifiers = [ 13 | "Programming Language :: Python :: 3", 14 | ] 15 | dependencies = [ 16 | "gitdb", 17 | "joblib", 18 | "jsonpickle", 19 | "PyYAML", 20 | "smmap", 21 | "tabulate", 22 | "requests", 23 | "ruamel.yaml", 24 | "filelock", 25 | "rapidfuzz", 26 | ] 27 | dynamic = ["version"] 28 | 29 | [tool.setuptools.dynamic] 30 | version = {attr = "ansible_risk_insight._version.__version__"} 31 | 32 | [project.scripts] 33 | ansible-risk-insight = "ansible_risk_insight:main" 34 | ari = "ansible_risk_insight:main" 35 | 36 | [tool.black] 37 | line-length = 150 38 | include = '\.pyi?$' 39 | exclude = ''' 40 | /( 41 | \.git 42 | | \.hg 43 | | \.mypy_cache 44 | | \.tox 45 | | \.venv 46 | | _build 47 | | buck-out 48 | | build 49 | | dist 50 | )/ 51 | ''' 52 | 53 | [tool.flake8] 54 | ignore = "E203, W503," 55 | max-line-length = 150 56 | -------------------------------------------------------------------------------- /example/readme.md: -------------------------------------------------------------------------------- 1 | This sample shows how to call ARI from your code. 2 | 3 | ``` 4 | example 5 | ├── playbooks 6 | │ └── sample_playbook.yml # sample playbook file 7 | ├── readme.md #this document 8 | ├── rules 9 | │ └── sample_rule.py # sample rule 10 | ├── sample.py # sample code 11 | ``` 12 | 13 | For the preparation, see the document in ARI repositogy. Here is the short path for minimal preparation. 14 | 15 | ``` 16 | git clone git@github.com:ansible/ansible-risk-insight.git 17 | cd ansible-risk-insight 18 | 19 | # install ARI 20 | pip install -e . 21 | 22 | # list of collection crawled for Knowledge Base (KB) 23 | cat << EOS > /tmp/ram_input_list.txt 24 | collection amazon.aws 25 | collection azure.azcollection 26 | collection google.cloud 27 | collection arista.eos 28 | collection junipernetworks.junos 29 | collection containers.podman 30 | collection ansible.builtin 31 | collection community.general 32 | collection ansible.posix 33 | collection arista.avd 34 | EOS 35 | 36 | # prepare ARI KB (created under /tmp/ari-data) 37 | ari ram generate -f /tmp/ram_input_list.txt 38 | ``` 39 | 40 | You can run the sample by 41 | 42 | ``` 43 | python example/sample.py 44 | ``` 45 | 46 | See the document [here](../docs/customize_rules.md) to add your own custom rules. 47 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/module_annotator_base.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import TaskCall 19 | from ansible_risk_insight.annotators.annotator_base import Annotator, AnnotatorResult 20 | 21 | 22 | class ModuleAnnotator(Annotator): 23 | type: str = "module_annotation" 24 | fqcn: str = "" 25 | 26 | def run(self, task: TaskCall) -> AnnotatorResult: 27 | raise ValueError("this is a base class method") 28 | 29 | 30 | @dataclass 31 | class ModuleAnnotatorResult(AnnotatorResult): 32 | pass 33 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible_builtin.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from ansible_risk_insight.models import TaskCall 18 | from ansible_risk_insight.annotators.risk_annotator_base import RiskAnnotator 19 | 20 | 21 | class AnsibleBuiltinRiskAnnotator(RiskAnnotator): 22 | name: str = "ansible.builtin" 23 | enabled: bool = True 24 | 25 | def match(self, task: TaskCall) -> bool: 26 | resolved_name = task.spec.resolved_name 27 | return resolved_name.startswith("ansible.builtin.") 28 | 29 | # embed "analyzed_data" field in Task 30 | def run(self, task: TaskCall): 31 | if not self.match(task): 32 | return [] 33 | 34 | return self.run_module_annotators("ansible.builtin", task) 35 | -------------------------------------------------------------------------------- /ansible_risk_insight/key_test.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 json 18 | from keyutil import get_obj_info_by_key 19 | 20 | 21 | if __name__ == "__main__": 22 | keys = [ 23 | "task role:geerlingguy.gitlab#taskfile:tasks/main.yml#task:[0]", 24 | "taskfile role:geerlingguy.gitlab#taskfile:tasks/main.yml", 25 | "role role:geerlingguy.gitlab", 26 | "play collection:debops.debops#playbook:playbooks/virt/dnsmasq-persistent_paths.yml#play:[0]", 27 | "playbook" " collection:debops.debops#playbook:playbooks/sys/cryptsetup-plain.yml", 28 | "module collection:debops.debops#module:debops.debops.apache2_module", 29 | ] 30 | for k in keys: 31 | info = get_obj_info_by_key(k) 32 | print(json.dumps(info, indent=2)) 33 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/git.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from typing import List 18 | from ansible_risk_insight.models import Annotation, RiskAnnotation, TaskCall, DefaultRiskType, InboundTransferDetail 19 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 20 | 21 | 22 | class GetURLAnnotator(ModuleAnnotator): 23 | fqcn: str = "ansible.builtin.git" 24 | enabled: bool = True 25 | 26 | def run(self, task: TaskCall) -> List[Annotation]: 27 | src = task.args.get("repo") 28 | dest = task.args.get("dest") 29 | 30 | annotation = RiskAnnotation.init(risk_type=DefaultRiskType.INBOUND, detail=InboundTransferDetail(_src_arg=src, _dest_arg=dest)) 31 | return ModuleAnnotatorResult(annotations=[annotation]) 32 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | site_name: Ansible Risk Insight Documentation 3 | site_url: https://ansible-risk-insight.readthedocs.io/ 4 | repo_url: https://github.com/ansible/ansible-risk-insight 5 | copyright: Copyright © 2023 Red Hat, Inc. 6 | docs_dir: docs 7 | # strict: true 8 | 9 | theme: 10 | name: readthedocs 11 | features: 12 | - content.code.copy 13 | - content.action.edit 14 | - navigation.expand 15 | - navigation.sections 16 | - navigation.instant 17 | - navigation.indexes 18 | - navigation.tracking 19 | - toc.integrate 20 | 21 | nav: 22 | - User Guide: 23 | - home: index.md 24 | - installing.md 25 | - customize_rules.md 26 | - Rules: 27 | - rules/R101_command_exec.md 28 | - rules/R102_command_instead_of_shell.md 29 | - rules/R103_download_exec.md 30 | - rules/R104_unauthorized_download_src.md 31 | - rules/R105_outbound_transfer.md 32 | - rules/R106_inbound_transfer.md 33 | - rules/R107_pkg_install_with_insecure_option.md 34 | - rules/R108_privilege_escalation.md 35 | - rules/R109_key_config_change.md 36 | - rules/R110_non_builtin_use.md 37 | - rules/R111_parameterized_import_role.md 38 | - rules/R112_parameterized_import_taskfile.md 39 | - rules/R113_parameterized_pkg_install.md 40 | - rules/R114_file_change.md 41 | - rules/R115_file_deletion.md 42 | - rules/R116_insecure_file_permission.md 43 | - rules/R301_non_fqcn_use.md 44 | 45 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/get_url.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from typing import List 18 | from ansible_risk_insight.models import Annotation, RiskAnnotation, TaskCall, DefaultRiskType, InboundTransferDetail 19 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 20 | 21 | 22 | class GetURLAnnotator(ModuleAnnotator): 23 | fqcn: str = "ansible.builtin.get_url" 24 | enabled: bool = True 25 | 26 | def run(self, task: TaskCall) -> List[Annotation]: 27 | src = task.args.get("url") 28 | dest = task.args.get("dest") 29 | 30 | annotation = RiskAnnotation.init(risk_type=DefaultRiskType.INBOUND, detail=InboundTransferDetail(_src_arg=src, _dest_arg=dest)) 31 | return ModuleAnnotatorResult(annotations=[annotation]) 32 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/subversion.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from typing import List 18 | from ansible_risk_insight.models import Annotation, RiskAnnotation, TaskCall, DefaultRiskType, InboundTransferDetail 19 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 20 | 21 | 22 | class SubversionAnnotator(ModuleAnnotator): 23 | fqcn: str = "ansible.builtin.subversion" 24 | enabled: bool = True 25 | 26 | def run(self, task: TaskCall) -> List[Annotation]: 27 | src = task.args.get("repo") 28 | dest = task.args.get("dest") 29 | annotation = RiskAnnotation.init(risk_type=DefaultRiskType.INBOUND, detail=InboundTransferDetail(_src_arg=src, _dest_arg=dest)) 30 | return ModuleAnnotatorResult(annotations=[annotation]) 31 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/pip.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from typing import List 18 | from ansible_risk_insight.models import Annotation, RiskAnnotation, TaskCall, DefaultRiskType, PackageInstallDetail 19 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 20 | 21 | 22 | class PipAnnotator(ModuleAnnotator): 23 | fqcn: str = "ansible.builtin.pip" 24 | enabled: bool = True 25 | 26 | def run(self, task: TaskCall) -> List[Annotation]: 27 | pkg = task.args.get("name") 28 | if pkg is None: 29 | pkg = task.args.get("requirements") 30 | 31 | annotation = RiskAnnotation.init(risk_type=DefaultRiskType.PACKAGE_INSTALL, detail=PackageInstallDetail(_pkg_arg=pkg)) 32 | return ModuleAnnotatorResult(annotations=[annotation]) 33 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/rpm_key.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from typing import List 18 | from ansible_risk_insight.models import Annotation, RiskAnnotation, TaskCall, DefaultRiskType, KeyConfigChangeDetail 19 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 20 | 21 | 22 | class RpmKeyAnnotator(ModuleAnnotator): 23 | fqcn: str = "ansible.builtin.rpm_key" 24 | enabled: bool = True 25 | 26 | def run(self, task: TaskCall) -> List[Annotation]: 27 | key = task.args.get("key") 28 | state = task.args.get("state") 29 | 30 | annotation = RiskAnnotation.init(risk_type=DefaultRiskType.CONFIG_CHANGE, 31 | detail=KeyConfigChangeDetail(_key_arg=key, _state_arg=state)) 32 | return ModuleAnnotatorResult(annotations=[annotation]) 33 | -------------------------------------------------------------------------------- /example/rules/sample_rule.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from ansible_risk_insight.models import ( 4 | AnsibleRunContext, 5 | RunTargetType, 6 | Rule, 7 | RuleResult, 8 | Severity, 9 | ) 10 | 11 | 12 | @dataclass 13 | class SampleRule(Rule): 14 | # rule definition 15 | rule_id: str = "Sample101" 16 | description: str = "echo task block" 17 | enabled: bool = True 18 | name: str = "EchoTaskContent" 19 | version: str = "v0.0.1" 20 | severity: Severity = Severity.NONE 21 | 22 | def match(self, ctx: AnsibleRunContext) -> bool: 23 | # specify targets to be checked 24 | return ctx.current.type == RunTargetType.Task 25 | 26 | def process(self, ctx: AnsibleRunContext): 27 | # implement check logic 28 | task = ctx.current # get current object of the context 29 | 30 | correct_fqcn = task.get_annotation(key="module.correct_fqcn") 31 | need_correction = task.get_annotation(key="module.need_correction") 32 | 33 | verdict = True # indicates the rule is matched or not. 34 | detail = {} 35 | task_content = task.content # get the task content 36 | task_block = task_content.yaml() # convert to yaml format by yaml() 37 | detail["task_block"] = task_block 38 | # put the data into rule result 39 | detail["correct_fqcn"] = correct_fqcn 40 | detail["need_correction"] = need_correction 41 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 42 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/apt.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from typing import List 18 | from ansible_risk_insight.models import Annotation, RiskAnnotation, TaskCall, DefaultRiskType, PackageInstallDetail 19 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 20 | 21 | 22 | class AptAnnotator(ModuleAnnotator): 23 | fqcn: str = "ansible.builtin.apt" 24 | enabled: bool = True 25 | 26 | def run(self, task: TaskCall) -> List[Annotation]: 27 | pkg = task.args.get("name") 28 | if pkg is None: 29 | pkg = task.args.get("pkg") 30 | if pkg is None: 31 | pkg = task.args.get("deb") 32 | 33 | annotation = RiskAnnotation.init(risk_type=DefaultRiskType.PACKAGE_INSTALL, detail=PackageInstallDetail(_pkg_arg=pkg)) 34 | return ModuleAnnotatorResult(annotations=[annotation]) 35 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/raw.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from ansible_risk_insight.models import RiskAnnotation, TaskCall, DefaultRiskType, CommandExecDetail 18 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 19 | 20 | 21 | class RawAnnotator(ModuleAnnotator): 22 | fqcn: str = "ansible.builtin.raw" 23 | enabled: bool = True 24 | 25 | def run(self, task: TaskCall) -> ModuleAnnotatorResult: 26 | cmd = task.args.get("") 27 | if cmd is None: 28 | cmd = task.args.get("cmd") 29 | if cmd is None: 30 | cmd = task.args.get("argv") 31 | 32 | annotation = RiskAnnotation.init( 33 | risk_type=DefaultRiskType.CMD_EXEC, 34 | detail=CommandExecDetail(command=cmd), 35 | ) 36 | return ModuleAnnotatorResult(annotations=[annotation]) 37 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/shell.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from ansible_risk_insight.models import RiskAnnotation, TaskCall, DefaultRiskType, CommandExecDetail 18 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 19 | 20 | 21 | class ShellAnnotator(ModuleAnnotator): 22 | fqcn: str = "ansible.builtin.shell" 23 | enabled: bool = True 24 | 25 | def run(self, task: TaskCall) -> ModuleAnnotatorResult: 26 | cmd = task.args.get("") 27 | if cmd is None: 28 | cmd = task.args.get("cmd") 29 | if cmd is None: 30 | cmd = task.args.get("argv") 31 | 32 | annotation = RiskAnnotation.init( 33 | risk_type=DefaultRiskType.CMD_EXEC, 34 | detail=CommandExecDetail(command=cmd), 35 | ) 36 | return ModuleAnnotatorResult(annotations=[annotation]) 37 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/command.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from ansible_risk_insight.models import RiskAnnotation, TaskCall, DefaultRiskType, CommandExecDetail 18 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 19 | 20 | 21 | class CommandAnnotator(ModuleAnnotator): 22 | fqcn: str = "ansible.builtin.command" 23 | enabled: bool = True 24 | 25 | def run(self, task: TaskCall) -> ModuleAnnotatorResult: 26 | cmd = task.args.get("") 27 | if cmd is None: 28 | cmd = task.args.get("cmd") 29 | if cmd is None: 30 | cmd = task.args.get("argv") 31 | 32 | annotation = RiskAnnotation.init( 33 | risk_type=DefaultRiskType.CMD_EXEC, 34 | detail=CommandExecDetail(command=cmd), 35 | ) 36 | return ModuleAnnotatorResult(annotations=[annotation]) 37 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/script.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from ansible_risk_insight.models import RiskAnnotation, TaskCall, DefaultRiskType, CommandExecDetail 18 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 19 | 20 | 21 | class ScriptAnnotator(ModuleAnnotator): 22 | fqcn: str = "ansible.builtin.script" 23 | enabled: bool = True 24 | 25 | def run(self, task: TaskCall) -> ModuleAnnotatorResult: 26 | cmd = task.args.get("") 27 | if cmd is None: 28 | cmd = task.args.get("cmd") 29 | if cmd is None: 30 | cmd = task.args.get("argv") 31 | 32 | annotation = RiskAnnotation.init( 33 | risk_type=DefaultRiskType.CMD_EXEC, 34 | detail=CommandExecDetail(command=cmd), 35 | ) 36 | return ModuleAnnotatorResult(annotations=[annotation]) 37 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/assemble.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from typing import List 18 | from ansible_risk_insight.models import Annotation, RiskAnnotation, TaskCall, DefaultRiskType, FileChangeDetail 19 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 20 | 21 | 22 | class AssembleAnnotator(ModuleAnnotator): 23 | fqcn: str = "ansible.builtin.assemble" 24 | enabled: bool = True 25 | 26 | def run(self, task: TaskCall) -> List[Annotation]: 27 | path = task.args.get("dest") 28 | src = task.args.get("src") 29 | unsafe_writes = task.args.get("unsafe_writes") 30 | 31 | annotation = RiskAnnotation.init(risk_type=DefaultRiskType.FILE_CHANGE, 32 | detail=FileChangeDetail(_path_arg=path, _src_arg=src, _unsafe_write_arg=unsafe_writes)) 33 | return ModuleAnnotatorResult(annotations=[annotation]) 34 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/replace.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from typing import List 18 | from ansible_risk_insight.models import Annotation, RiskAnnotation, TaskCall, DefaultRiskType, FileChangeDetail 19 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 20 | 21 | 22 | class ReplaceAnnotator(ModuleAnnotator): 23 | fqcn: str = "ansible.builtin.replace" 24 | enabled: bool = True 25 | 26 | def run(self, task: TaskCall) -> List[Annotation]: 27 | path = task.args.get("path") 28 | mode = task.args.get("mode") 29 | unsafe_writes = task.args.get("unsafe_writes") 30 | 31 | annotation = RiskAnnotation.init(risk_type=DefaultRiskType.FILE_CHANGE, 32 | detail=FileChangeDetail(_path_arg=path, _mode_arg=mode, _unsafe_write_arg=unsafe_writes)) 33 | return ModuleAnnotatorResult(annotations=[annotation]) 34 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/annotator_base.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from typing import List 19 | from ansible_risk_insight.models import TaskCall, AnsibleRunContext, Annotation 20 | 21 | 22 | class Annotator(object): 23 | type: str = "" 24 | context: AnsibleRunContext = None 25 | 26 | def __init__(self, context: AnsibleRunContext = None): 27 | if context: 28 | self.context = context 29 | 30 | def run(self, task: TaskCall): 31 | raise ValueError("this is a base class method") 32 | 33 | 34 | @dataclass 35 | class AnnotatorResult(object): 36 | annotations: List[Annotation] = None 37 | data: any = None 38 | 39 | def print(self): 40 | raise ValueError("this is a base class method") 41 | 42 | def to_json(self): 43 | raise ValueError("this is a base class method") 44 | 45 | def error(self): 46 | raise ValueError("this is a base class method") 47 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/blockinfile.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from typing import List 18 | from ansible_risk_insight.models import Annotation, RiskAnnotation, TaskCall, DefaultRiskType, FileChangeDetail 19 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 20 | 21 | 22 | class BlockinfileAnnotator(ModuleAnnotator): 23 | fqcn: str = "ansible.builtin.blockinfile" 24 | enabled: bool = True 25 | 26 | def run(self, task: TaskCall) -> List[Annotation]: 27 | path = task.args.get("path") 28 | unsafe_writes = task.args.get("unsafe_writes") 29 | state = task.args.get("state") 30 | 31 | annotation = RiskAnnotation.init(risk_type=DefaultRiskType.FILE_CHANGE, 32 | detail=FileChangeDetail(_path_arg=path, _state_arg=state, _unsafe_write_arg=unsafe_writes)) 33 | return ModuleAnnotatorResult(annotations=[annotation]) 34 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/expect.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from ansible_risk_insight.models import RiskAnnotation, TaskCall, DefaultRiskType, CommandExecDetail 18 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 19 | 20 | 21 | class RawAnnotator(ModuleAnnotator): 22 | fqcn: str = "ansible.builtin.raw" 23 | enabled: bool = True 24 | 25 | def run(self, task: TaskCall) -> ModuleAnnotatorResult: 26 | cmd = task.args.get("") 27 | if cmd is None: 28 | cmd = task.args.get("command") 29 | if cmd is None: 30 | cmd = task.args.get("cmd") 31 | if cmd is None: 32 | cmd = task.args.get("argv") 33 | 34 | annotation = RiskAnnotation.init( 35 | risk_type=DefaultRiskType.CMD_EXEC, 36 | detail=CommandExecDetail(command=cmd), 37 | ) 38 | return ModuleAnnotatorResult(annotations=[annotation]) 39 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R303_task_without_name.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | Rule, 23 | Severity, 24 | RuleTag as Tag, 25 | RuleResult, 26 | ) 27 | 28 | 29 | @dataclass 30 | class TaskWithoutNameRule(Rule): 31 | rule_id: str = "R303" 32 | description: str = "A task without name is found" 33 | enabled: bool = True 34 | name: str = "TaskWithoutName" 35 | version: str = "v0.0.1" 36 | severity: Severity = Severity.LOW 37 | tags: tuple = Tag.DEPENDENCY 38 | 39 | def match(self, ctx: AnsibleRunContext) -> bool: 40 | return ctx.current.type == RunTargetType.Task 41 | 42 | def process(self, ctx: AnsibleRunContext): 43 | task = ctx.current 44 | 45 | verdict = not task.spec.name 46 | 47 | return RuleResult(verdict=verdict, file=task.file_info(), rule=self.get_metadata()) 48 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/dnf.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from typing import List 18 | from ansible_risk_insight.models import Annotation, RiskAnnotation, TaskCall, DefaultRiskType, PackageInstallDetail 19 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 20 | 21 | 22 | class DnfAnnotator(ModuleAnnotator): 23 | fqcn: str = "ansible.builtin.dnf" 24 | enabled: bool = True 25 | 26 | def run(self, task: TaskCall) -> List[Annotation]: 27 | pkg = task.args.get("name") 28 | allow_downgrade = task.args.get("allow_downgrade") 29 | validate_certs = task.args.get("validate_certs") 30 | 31 | annotation = RiskAnnotation.init(risk_type=DefaultRiskType.PACKAGE_INSTALL, detail=PackageInstallDetail(_pkg_arg=pkg, 32 | _validate_certs_arg=validate_certs, _allow_downgrade_arg=allow_downgrade)) 33 | return ModuleAnnotatorResult(annotations=[annotation]) 34 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/yum.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from typing import List 18 | from ansible_risk_insight.models import Annotation, RiskAnnotation, TaskCall, DefaultRiskType, PackageInstallDetail 19 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 20 | 21 | 22 | class YumAnnotator(ModuleAnnotator): 23 | fqcn: str = "ansible.builtin.yum" 24 | enabled: bool = True 25 | 26 | def run(self, task: TaskCall) -> List[Annotation]: 27 | pkg = task.args.get("name") 28 | allow_downgrade = task.args.get("allow_downgrade") 29 | validate_certs = task.args.get("validate_certs") 30 | 31 | annotation = RiskAnnotation.init(risk_type=DefaultRiskType.PACKAGE_INSTALL, detail=PackageInstallDetail(_pkg_arg=pkg, 32 | _validate_certs_arg=validate_certs, _allow_downgrade_arg=allow_downgrade)) 33 | return ModuleAnnotatorResult(annotations=[annotation]) 34 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R302_role_without_metadata.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import ( 19 | AnsibleRunContext, 20 | RunTargetType, 21 | Rule, 22 | Severity, 23 | RuleTag as Tag, 24 | RuleResult, 25 | ) 26 | 27 | 28 | @dataclass 29 | class RoleWithoutMetadataRule(Rule): 30 | rule_id: str = "R302" 31 | description: str = "A role without metadata is used" 32 | enabled: bool = True 33 | name: str = "RoleWithoutMetadata" 34 | version: str = "v0.0.1" 35 | severity: Severity = Severity.LOW 36 | tags: tuple = Tag.DEPENDENCY 37 | 38 | def match(self, ctx: AnsibleRunContext) -> bool: 39 | return ctx.current.type == RunTargetType.Role 40 | 41 | def process(self, ctx: AnsibleRunContext): 42 | role = ctx.current 43 | 44 | verdict = not role.spec.metadata 45 | 46 | return RuleResult(verdict=verdict, file=role.file_info(), rule=self.get_metadata()) 47 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/file.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from typing import List 18 | from ansible_risk_insight.models import Annotation, RiskAnnotation, TaskCall, DefaultRiskType, FileChangeDetail 19 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 20 | 21 | 22 | class FileAnnotator(ModuleAnnotator): 23 | fqcn: str = "ansible.builtin.file" 24 | enabled: bool = True 25 | 26 | def run(self, task: TaskCall) -> List[Annotation]: 27 | path = task.args.get("path") 28 | mode = task.args.get("mode") 29 | unsafe_writes = task.args.get("unsafe_writes") 30 | state = task.args.get("state") 31 | 32 | annotation = RiskAnnotation.init(risk_type=DefaultRiskType.FILE_CHANGE, 33 | detail=FileChangeDetail(_path_arg=path, _state_arg=state, _mode_arg=mode, _unsafe_write_arg=unsafe_writes)) 34 | return ModuleAnnotatorResult(annotations=[annotation]) 35 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/template.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from typing import List 18 | from ansible_risk_insight.models import Annotation, RiskAnnotation, TaskCall, DefaultRiskType, FileChangeDetail 19 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 20 | 21 | 22 | class TemplateAnnotator(ModuleAnnotator): 23 | fqcn: str = "ansible.builtin.template" 24 | enabled: bool = True 25 | 26 | def run(self, task: TaskCall) -> List[Annotation]: 27 | path = task.args.get("dest") 28 | src = task.args.get("src") 29 | mode = task.args.get("mode") 30 | unsafe_writes = task.args.get("unsafe_writes") 31 | 32 | annotation = RiskAnnotation.init(risk_type=DefaultRiskType.FILE_CHANGE, 33 | detail=FileChangeDetail(_path_arg=path, _src_arg=src, _mode_arg=mode, _unsafe_write_arg=unsafe_writes)) 34 | return ModuleAnnotatorResult(annotations=[annotation]) 35 | -------------------------------------------------------------------------------- /ansible_risk_insight/cli/ram/list.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | 19 | from ...scanner import config 20 | from ...risk_assessment_model import RAMClient 21 | from ...utils import show_all_ram_metadata 22 | 23 | 24 | class RAMListCLI: 25 | args = None 26 | 27 | def __init__(self): 28 | parser = argparse.ArgumentParser(description="TODO") 29 | parser.add_argument("target_type", help="content type", choices={"ram"}) 30 | parser.add_argument("action", help="action for RAM command or target_name of search action") 31 | args = parser.parse_args() 32 | self.args = args 33 | 34 | def run(self): 35 | args = self.args 36 | action = args.action 37 | if action != "list": 38 | raise ValueError('RAMListCLI cannot be executed without "list" action') 39 | 40 | ram_client = RAMClient(root_dir=config.data_dir) 41 | 42 | all_ram_meta = ram_client.list_all_ram_metadata() 43 | show_all_ram_metadata(all_ram_meta) 44 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/lineinfile.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from typing import List 18 | from ansible_risk_insight.models import Annotation, RiskAnnotation, TaskCall, DefaultRiskType, FileChangeDetail 19 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 20 | 21 | 22 | class LineInFileAnnotator(ModuleAnnotator): 23 | fqcn: str = "ansible.builtin.lineinfile" 24 | enabled: bool = True 25 | 26 | def run(self, task: TaskCall) -> List[Annotation]: 27 | path = task.args.get("path") 28 | mode = task.args.get("mode") 29 | unsafe_writes = task.args.get("unsafe_writes") 30 | state = task.args.get("state") 31 | 32 | annotation = RiskAnnotation.init(risk_type=DefaultRiskType.FILE_CHANGE, 33 | detail=FileChangeDetail(_path_arg=path, _state_arg=state, _mode_arg=mode, _unsafe_write_arg=unsafe_writes)) 34 | return ModuleAnnotatorResult(annotations=[annotation]) 35 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/uri.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from ansible_risk_insight.models import RiskAnnotation, TaskCall, DefaultRiskType, OutboundTransferDetail 18 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 19 | 20 | 21 | class URIAnnotator(ModuleAnnotator): 22 | fqcn: str = "ansible.builtin.uri" 23 | enabled: bool = True 24 | 25 | def run(self, task: TaskCall) -> ModuleAnnotatorResult: 26 | method = task.args.get("method") 27 | 28 | annotations = [] 29 | if method in ["PUT", "POST", "PATCH"]: 30 | url = task.args.get("url") 31 | body = task.args.get("body") 32 | annotation = RiskAnnotation.init( 33 | risk_type=DefaultRiskType.OUTBOUND, 34 | detail=OutboundTransferDetail(_dest_arg=url, _src_arg=body), 35 | ) 36 | annotations.append(annotation) 37 | return ModuleAnnotatorResult(annotations=annotations) 38 | -------------------------------------------------------------------------------- /data-struct.txt: -------------------------------------------------------------------------------- 1 | ------ 2 | [external dependencies installed by ansible galaxy] 3 | role_repo 4 | - roles (list of role_ids) 5 | - modules (list of module_ids) 6 | * role_repo_id 7 | 8 | collection_repo .... a collection 9 | * collection_id 10 | 11 | ## xxxx_repoはansible_galaxy install後のdirectoryをinputにロードされる 12 | (loaded_role_repos, loaded_collection_repos) 13 | 14 | --------- 15 | 16 | [scm-repo] 17 | scm-repo 18 | - playbooks 19 | - roles 20 | - modules# 21 | - role_repos (defined in requirement.yml) 22 | - collection_repos (defined in requirement.yml) 23 | 24 | def defined_modules() 25 | return xxx_modules 26 | 27 | def get_playbooks 28 | 29 | ## scm_repoはexternal dependenciesのロードが済んでいる前提でロードされる 30 | 31 | scm-repo.init(scm-repo-dir, loaded_role_repos, loaded_collection_repos) 32 | 33 | scm-repo.dump() ----> json out 34 | 35 | ------ 36 | class list 37 | 38 | a playbook 39 | - tasks (list of task_ids) 40 | * playbook_id 41 | * source_path 42 | 43 | a role 44 | def init(role_dir): 45 | 46 | - tasks (list of task_ids) 47 | - modules (list of module_ids) 48 | * name 49 | * role_id 50 | * defined_in: debops/debops/tree/master/ansible/roles/apt 51 | * source: collection:debops.debops 52 | 53 | a collection 54 | - collection_id 55 | - playbooks (list of playbook_ids) 56 | - roles (list of role_ids) 57 | - modules (list of module_ids) 58 | 59 | a task 60 | - module: ansible.builtin.apt 61 | * task_id 62 | * defined_in: ./tasks/main.yml#L21-L28 63 | * parameters 64 | * used_in: pointer to role (e.g. role_id) 65 | 66 | a module 67 | * module_id 68 | * fqcn: ansible.builtin.apt 69 | * defined_in: path to plugin file 70 | * collection: ansible.builtin 71 | * module_category: package_management -------------------------------------------------------------------------------- /ansible_risk_insight/rules/sample_rule.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | Rule, 23 | Severity, 24 | RuleResult, 25 | ) 26 | 27 | 28 | @dataclass 29 | class SampleRule(Rule): 30 | rule_id: str = "Sample101" 31 | description: str = "echo task block" 32 | enabled: bool = False 33 | name: str = "EchoTaskContent" 34 | version: str = "v0.0.1" 35 | severity: Severity = Severity.NONE 36 | tags: tuple = "sample" 37 | 38 | def match(self, ctx: AnsibleRunContext) -> bool: 39 | # specify targets to be checked 40 | return ctx.current.type == RunTargetType.Task 41 | 42 | def process(self, ctx: AnsibleRunContext): 43 | task = ctx.current 44 | 45 | verdict = True 46 | detail = {} 47 | task_block = task.content.yaml() 48 | detail["task_block"] = task_block 49 | 50 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 51 | -------------------------------------------------------------------------------- /ansible_risk_insight/builtin-modules.txt: -------------------------------------------------------------------------------- 1 | add_host 2 | apt 3 | apt_key 4 | apt_repository 5 | assemble 6 | assert 7 | async_status 8 | blockinfile 9 | command 10 | copy 11 | cron 12 | debconf 13 | debug 14 | dnf 15 | dpkg_selections 16 | expect 17 | fail 18 | fetch 19 | file 20 | find 21 | gather_facts 22 | get_url 23 | getent 24 | git 25 | group 26 | group_by 27 | hostname 28 | import_playbook 29 | import_role 30 | import_tasks 31 | include 32 | include_role 33 | include_tasks 34 | include_vars 35 | iptables 36 | known_hosts 37 | lineinfile 38 | meta 39 | package 40 | package_facts 41 | pause 42 | ping 43 | pip 44 | raw 45 | reboot 46 | replace 47 | rpm_key 48 | script 49 | service 50 | service_facts 51 | set_fact 52 | set_stats 53 | setup 54 | shell 55 | slurp 56 | stat 57 | subversion 58 | systemd 59 | sysvinit 60 | tempfile 61 | template 62 | unarchive 63 | uri 64 | user 65 | validate_argument_spec 66 | wait_for 67 | wait_for_connection 68 | yum 69 | yum_repository 70 | runas 71 | su 72 | sudo 73 | jsonfile 74 | memory 75 | default 76 | junit 77 | minimal 78 | oneline 79 | tree 80 | local 81 | paramiko_ssh 82 | psrp 83 | ssh 84 | winrm 85 | advanced_host_list 86 | auto 87 | constructed 88 | generator 89 | host_list 90 | ini 91 | script 92 | toml 93 | yaml 94 | config 95 | csvfile 96 | dict 97 | env 98 | file 99 | fileglob 100 | first_found 101 | indexed_items 102 | ini 103 | inventory_hostnames 104 | items 105 | lines 106 | list 107 | nested 108 | password 109 | pipe 110 | random_choice 111 | sequence 112 | subelements 113 | template 114 | together 115 | unvault 116 | url 117 | varnames 118 | cmd 119 | powershell 120 | sh 121 | debug 122 | free 123 | host_pinned 124 | linear 125 | host_group_vars -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R108_privilege_escalation.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | Rule, 23 | Severity, 24 | RuleTag as Tag, 25 | RuleResult, 26 | ) 27 | 28 | 29 | @dataclass 30 | class PrivilegeEscalationRule(Rule): 31 | rule_id: str = "R108" 32 | description: str = "Privilege escalation is found" 33 | enabled: bool = True 34 | name: str = "PrivilegeEscalation" 35 | version: str = "v0.0.1" 36 | severity: Severity = Severity.HIGH 37 | tags: tuple = Tag.SYSTEM 38 | 39 | def match(self, ctx: AnsibleRunContext) -> bool: 40 | return ctx.current.type == RunTargetType.Task 41 | 42 | def process(self, ctx: AnsibleRunContext): 43 | task = ctx.current 44 | 45 | verdict = task.become and task.become.enabled 46 | detail = {} 47 | if verdict: 48 | detail = task.become.__dict__ 49 | 50 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 51 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/apt_key.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from typing import List 18 | from ansible_risk_insight.models import Annotation, RiskAnnotation, TaskCall, DefaultRiskType, KeyConfigChangeDetail 19 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 20 | 21 | 22 | class AptKeyAnnotator(ModuleAnnotator): 23 | fqcn: str = "ansible.builtin.apt_key" 24 | enabled: bool = True 25 | 26 | def run(self, task: TaskCall) -> List[Annotation]: 27 | # id = task.args.get("id") 28 | key = None 29 | if key is None: 30 | key = task.args.get("url") 31 | if key is None: 32 | key = task.args.get("data") 33 | if key is None: 34 | key = task.args.get("keyserver") 35 | 36 | state = task.args.get("state") 37 | 38 | annotation = RiskAnnotation.init(risk_type=DefaultRiskType.CONFIG_CHANGE, 39 | detail=KeyConfigChangeDetail(_key_arg=key, _state_arg=state)) 40 | return ModuleAnnotatorResult(annotations=[annotation]) 41 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R304_unresolved_module.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import ( 19 | AnsibleRunContext, 20 | RunTargetType, 21 | ExecutableType as ActionType, 22 | Rule, 23 | Severity, 24 | RuleTag as Tag, 25 | RuleResult, 26 | ) 27 | 28 | 29 | @dataclass 30 | class UnresolvedModuleRule(Rule): 31 | rule_id: str = "R304" 32 | description: str = "Unresolved module is found" 33 | enabled: bool = True 34 | name: str = "UnresolvedModule" 35 | version: str = "v0.0.1" 36 | severity: Severity = Severity.LOW 37 | tags: tuple = Tag.DEPENDENCY 38 | 39 | def match(self, ctx: AnsibleRunContext) -> bool: 40 | return ctx.current.type == RunTargetType.Task 41 | 42 | def process(self, ctx: AnsibleRunContext): 43 | task = ctx.current 44 | 45 | verdict = task.action_type == ActionType.MODULE_TYPE and task.spec.action and not task.resolved_action 46 | detail = { 47 | "module": task.spec.action, 48 | } 49 | 50 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 51 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R402_list_all_used_variables.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | Rule, 23 | Severity, 24 | RuleTag as Tag, 25 | RuleResult, 26 | ) 27 | 28 | 29 | @dataclass 30 | class ListAllUsedVariablesRule(Rule): 31 | rule_id: str = "R402" 32 | description: str = "Listing all used variables" 33 | enabled: bool = True 34 | name: str = "ListAllUsedVariables" 35 | version: str = "v0.0.1" 36 | severity: Severity = Severity.NONE 37 | tags: tuple = Tag.VARIABLE 38 | 39 | def match(self, ctx: AnsibleRunContext) -> bool: 40 | return ctx.current.type == RunTargetType.Task 41 | 42 | def process(self, ctx: AnsibleRunContext): 43 | task = ctx.current 44 | 45 | verdict = False 46 | detail = {} 47 | if ctx.is_end(task): 48 | 49 | verdict = True 50 | detail["metadata"] = ctx.info 51 | detail["variables"] = list(task.variable_use.keys()) 52 | 53 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 54 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R110_non_builtin_use.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import ( 19 | AnsibleRunContext, 20 | RunTargetType, 21 | ExecutableType as ActionType, 22 | Rule, 23 | Severity, 24 | RuleTag as Tag, 25 | RuleResult, 26 | ) 27 | 28 | 29 | @dataclass 30 | class NonBuiltinUseRule(Rule): 31 | rule_id: str = "R110" 32 | description: str = "Non-builtin module is used" 33 | enabled: bool = True 34 | name: str = "NonBuiltinUse" 35 | version: str = "v0.0.1" 36 | severity: Severity = Severity.VERY_LOW 37 | tags: tuple = Tag.DEPENDENCY 38 | 39 | def match(self, ctx: AnsibleRunContext) -> bool: 40 | return ctx.current.type == RunTargetType.Task 41 | 42 | def process(self, ctx: AnsibleRunContext): 43 | task = ctx.current 44 | 45 | verdict = task.action_type == ActionType.MODULE_TYPE and task.resolved_action and not task.resolved_action.startswith("ansible.builtin.") 46 | 47 | detail = { 48 | "fqcn": task.resolved_name, 49 | } 50 | 51 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 52 | -------------------------------------------------------------------------------- /ansible_risk_insight/cli/ram/release.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | 19 | from ...scanner import config 20 | from ...risk_assessment_model import RAMClient 21 | 22 | 23 | class RAMReleaseCLI: 24 | args = None 25 | 26 | def __init__(self): 27 | parser = argparse.ArgumentParser(description="TODO") 28 | parser.add_argument("target_type", help="content type", choices={"ram"}) 29 | parser.add_argument("action", help="action for RAM command or target_name of search action") 30 | parser.add_argument("-o", "--outfile", help="if execute release action, specify tar.gz file to store KB files") 31 | args = parser.parse_args() 32 | self.args = args 33 | 34 | def run(self): 35 | args = self.args 36 | action = args.action 37 | if action != "release": 38 | raise ValueError('RAMReleaseCLI cannot be executed without "release" action') 39 | 40 | if not args.outfile: 41 | raise ValueError(' "release" action cannot be executed without `--outfile` option. Please set "tar.gz" file name to export KB files.') 42 | 43 | ram_client = RAMClient(root_dir=config.data_dir) 44 | ram_client.release(args.outfile) 45 | -------------------------------------------------------------------------------- /ansible_risk_insight/cli/ram/diff.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | 19 | from ...scanner import config 20 | from ...risk_assessment_model import RAMClient 21 | from ...utils import show_diffs 22 | 23 | 24 | class RAMDiffCLI: 25 | args = None 26 | 27 | def __init__(self): 28 | parser = argparse.ArgumentParser(description="TODO") 29 | parser.add_argument("target_type", help="content type", choices={"ram"}) 30 | parser.add_argument("action", help="action for RAM command or target_name of search action") 31 | parser.add_argument("target_name", help="target_name for the action") 32 | parser.add_argument("version1", help="version string of the target") 33 | parser.add_argument("version2", help="version string compared") 34 | args = parser.parse_args() 35 | self.args = args 36 | 37 | def run(self): 38 | args = self.args 39 | action = args.action 40 | if action != "diff": 41 | raise ValueError('RAMDiffCLI cannot be executed without "diff" action') 42 | 43 | ram_client = RAMClient(root_dir=config.data_dir) 44 | diffs = ram_client.diff(args.target_name, args.version1, args.version2) 45 | show_diffs(diffs) 46 | -------------------------------------------------------------------------------- /ansible_risk_insight/logger.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 sys 18 | import logging 19 | 20 | 21 | _logger = None 22 | 23 | log_level_map = { 24 | "error": logging.ERROR, 25 | "warning": logging.WARNING, 26 | "info": logging.INFO, 27 | "debug": logging.DEBUG, 28 | } 29 | 30 | 31 | def set_logger_channel(channel: str = ""): 32 | global _logger 33 | _logger = logging.getLogger(channel) 34 | handler = logging.StreamHandler(sys.stdout) 35 | # default formatter 36 | formatter = logging.Formatter("%(levelname)s:%(name)s:%(message)s") 37 | handler.setFormatter(formatter) 38 | _logger.addHandler(handler) 39 | 40 | 41 | def set_log_level(level_str: str = "info"): 42 | global _logger 43 | level = log_level_map.get(level_str.lower(), None) 44 | _logger.setLevel(level) 45 | 46 | 47 | def exception(*args, **kwargs): 48 | _logger.exception(*args, **kwargs) 49 | 50 | 51 | def error(*args, **kwargs): 52 | _logger.error(*args, **kwargs) 53 | 54 | 55 | def warning(*args, **kwargs): 56 | _logger.warning(*args, **kwargs) 57 | 58 | 59 | def info(*args, **kwargs): 60 | _logger.info(*args, **kwargs) 61 | 62 | 63 | def debug(*args, **kwargs): 64 | _logger.debug(*args, **kwargs) 65 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R116_insecure_file_permission.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | DefaultRiskType as RiskType, 23 | AnnotationCondition, 24 | Rule, 25 | Severity, 26 | RuleTag as Tag, 27 | RuleResult, 28 | ) 29 | 30 | 31 | @dataclass 32 | class FilePermissionRule(Rule): 33 | rule_id: str = "R116" 34 | description: str = "File permission is not secure." 35 | enabled: bool = False 36 | name: str = "FilePermissionRule" 37 | version: str = "v0.0.1" 38 | severity: Severity = Severity.MEDIUM 39 | tags: tuple = Tag.SYSTEM 40 | 41 | def match(self, ctx: AnsibleRunContext) -> bool: 42 | return ctx.current.type == RunTargetType.Task 43 | 44 | def process(self, ctx: AnsibleRunContext): 45 | task = ctx.current 46 | 47 | # define a condition for this rule here 48 | ac = AnnotationCondition().risk_type(RiskType.FILE_CHANGE).attr("is_insecure_permissions", True) 49 | verdict = task.has_annotation_by_condition(ac) 50 | 51 | return RuleResult(verdict=verdict, file=task.file_info(), rule=self.get_metadata()) 52 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R117_external_role.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import ( 19 | AnsibleRunContext, 20 | RunTargetType, 21 | Rule, 22 | Severity, 23 | RuleTag as Tag, 24 | RuleResult, 25 | ) 26 | 27 | 28 | @dataclass 29 | class ExternalRoleRuleResult(RuleResult): 30 | pass 31 | 32 | 33 | @dataclass 34 | class ExternalRoleRule(Rule): 35 | rule_id: str = "R117" 36 | description: str = "An external role is used" 37 | enabled: bool = True 38 | name: str = "ExternalRole" 39 | version: str = "v0.0.1" 40 | severity: Severity = Severity.VERY_LOW 41 | tags: tuple = Tag.DEPENDENCY 42 | result_type: type = ExternalRoleRuleResult 43 | 44 | def match(self, ctx: AnsibleRunContext) -> bool: 45 | return ctx.current.type == RunTargetType.Role 46 | 47 | def process(self, ctx: AnsibleRunContext): 48 | role = ctx.current 49 | 50 | verdict = ( 51 | not ctx.is_begin(role) and role.spec.metadata and isinstance(role.spec.metadata, dict) and role.spec.metadata.get("galaxy_info", None) 52 | ) 53 | 54 | return RuleResult(verdict=verdict, file=role.file_info(), rule=self.get_metadata()) 55 | -------------------------------------------------------------------------------- /ansible_risk_insight/cli/ram/search.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | 19 | from ...scanner import config 20 | from ...risk_assessment_model import RAMClient 21 | from ...utils import split_name_and_version 22 | 23 | 24 | class RAMSearchCLI: 25 | args = None 26 | 27 | def __init__(self): 28 | parser = argparse.ArgumentParser(description="TODO") 29 | parser.add_argument("target_type", help="content type", choices={"ram"}) 30 | parser.add_argument("action", help="action for RAM command or target_name of search action") 31 | parser.add_argument("target_name", help="target_name for the action") 32 | args = parser.parse_args() 33 | self.args = args 34 | 35 | def run(self): 36 | args = self.args 37 | action = args.action 38 | target_name = args.target_name 39 | if action != "search": 40 | raise ValueError('RAMSearchCLI cannot be executed without "search" action') 41 | 42 | ram_client = RAMClient(root_dir=config.data_dir) 43 | 44 | target_name, target_version = split_name_and_version(target_name) 45 | findings = ram_client.search_findings(target_name, target_version) 46 | if findings: 47 | print(findings.summary_txt) 48 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R102_command_instead_of_shell.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | ExecutableType as ActionType, 23 | Rule, 24 | Severity, 25 | RuleTag as Tag, 26 | RuleResult, 27 | ) 28 | 29 | 30 | @dataclass 31 | class UseShellRule(Rule): 32 | rule_id: str = "R102" 33 | description: str = "Use 'command' module instead of 'shell' " 34 | enabled: bool = True 35 | name: str = "UseShellRule" 36 | version: str = "v0.0.1" 37 | severity: Severity = Severity.VERY_LOW 38 | tags: tuple = Tag.COMMAND 39 | 40 | def match(self, ctx: AnsibleRunContext) -> bool: 41 | return ctx.current.type == RunTargetType.Task 42 | 43 | def process(self, ctx: AnsibleRunContext): 44 | task = ctx.current 45 | 46 | # define a condition for this rule here 47 | verdict = ( 48 | task.action_type == ActionType.MODULE_TYPE 49 | and task.spec.action 50 | and task.resolved_action 51 | and task.resolved_action == "ansible.builtin.shell" 52 | ) 53 | 54 | return RuleResult(verdict=verdict, file=task.file_info(), rule=self.get_metadata()) 55 | -------------------------------------------------------------------------------- /example/sample.py: -------------------------------------------------------------------------------- 1 | import json 2 | from ansible_risk_insight import ARIScanner, Config 3 | import os 4 | 5 | if __name__ == "__main__": 6 | playbook_path = os.path.join(os.path.dirname(__file__), "playbooks/sample_playbook.yml") 7 | rule_dir = os.path.join(os.path.dirname(__file__), "rules") 8 | task_name = "Create a cloud instance" 9 | rule_id = "Sample101" 10 | 11 | ariScanner = ARIScanner( 12 | Config( 13 | rules_dir=rule_dir, 14 | data_dir="/tmp/ari-data", 15 | rules=[ 16 | "P001", # need for module annotation 17 | "P002", # need for module annotation 18 | "P003", # need for module annotation 19 | "P004", # need for module annotation 20 | rule_id, 21 | ], 22 | ), 23 | silent=True, 24 | ) 25 | 26 | if not playbook_path: 27 | print("please input file path to scan") 28 | else: 29 | result = ariScanner.evaluate( 30 | type="playbook", 31 | path=playbook_path, 32 | ) 33 | 34 | playbook = result.playbook(path=playbook_path) 35 | if not playbook: 36 | raise ValueError("the playbook was not found") 37 | 38 | # to get all tasks 39 | # tasks = playbook.tasks() 40 | 41 | # to get a task with specific name 42 | task = playbook.task(task_name) 43 | if not task: 44 | raise ValueError("No task was found") 45 | 46 | rule_result = task.find_result(rule_id=rule_id) 47 | # rule_result = task.find_result(rule_id="P001") 48 | 49 | if not rule_result: 50 | raise ValueError("the rule result was not found") 51 | 52 | if rule_result.error: 53 | raise ValueError(f"the rule could not be evaluated: {rule_result.error}") 54 | 55 | detail_dict = rule_result.get_detail() 56 | print(json.dumps(detail_dict, indent=2)) 57 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R205_unnecessary_include_vars.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | ExecutableType as ActionType, 23 | Rule, 24 | Severity, 25 | RuleTag as Tag, 26 | RuleResult, 27 | ) 28 | 29 | 30 | @dataclass 31 | class UnnecessaryIncludeVarsRule(Rule): 32 | rule_id: str = "R205" 33 | description: str = "include_vars is used without any condition" 34 | enabled: bool = True 35 | name: str = "UnnecessaryIncludeVars" 36 | version: str = "v0.0.1" 37 | severity: Severity = Severity.VERY_LOW 38 | tags: tuple = Tag.VARIABLE 39 | 40 | def match(self, ctx: AnsibleRunContext) -> bool: 41 | return ctx.current.type == RunTargetType.Task 42 | 43 | def process(self, ctx: AnsibleRunContext): 44 | task = ctx.current 45 | 46 | verdict = ( 47 | task.action_type == ActionType.MODULE_TYPE 48 | and task.resolved_action 49 | and task.resolved_action == "ansible.builtin.include_vars" 50 | and not task.spec.tags 51 | and not task.spec.when 52 | ) 53 | 54 | return RuleResult(verdict=verdict, file=task.file_info(), rule=self.get_metadata()) 55 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R305_unresolved_role.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import ( 19 | AnsibleRunContext, 20 | RunTargetType, 21 | ExecutableType as ActionType, 22 | Rule, 23 | Severity, 24 | RuleTag as Tag, 25 | RuleResult, 26 | ) 27 | 28 | 29 | @dataclass 30 | class UnresolvedRoleRuleResult(RuleResult): 31 | pass 32 | 33 | 34 | @dataclass 35 | class UnresolvedRoleRule(Rule): 36 | rule_id: str = "R305" 37 | description: str = "Unresolved role is found" 38 | enabled: bool = True 39 | name: str = "UnresolvedRole" 40 | version: str = "v0.0.1" 41 | severity: Severity = Severity.LOW 42 | tags: tuple = Tag.DEPENDENCY 43 | result_type: type = UnresolvedRoleRuleResult 44 | 45 | def match(self, ctx: AnsibleRunContext) -> bool: 46 | return ctx.current.type == RunTargetType.Task 47 | 48 | def process(self, ctx: AnsibleRunContext): 49 | task = ctx.current 50 | 51 | verdict = task.action_type == ActionType.ROLE_TYPE and task.spec.action and not task.resolved_action 52 | detail = { 53 | "role": task.spec.action, 54 | } 55 | 56 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 57 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R111_parameterized_import_role.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import ( 19 | AnsibleRunContext, 20 | RunTargetType, 21 | ExecutableType as ActionType, 22 | Rule, 23 | Severity, 24 | RuleTag as Tag, 25 | RuleResult, 26 | ) 27 | 28 | 29 | @dataclass 30 | class ParameterizedImportRoleRule(Rule): 31 | rule_id: str = "R111" 32 | description: str = "Import/include a parameterized name of role" 33 | enabled: bool = True 34 | name: str = "ParameterizedImportRole" 35 | version: str = "v0.0.1" 36 | severity: Severity = Severity.HIGH 37 | tags: tuple = Tag.DEPENDENCY 38 | 39 | def match(self, ctx: AnsibleRunContext) -> bool: 40 | return ctx.current.type == RunTargetType.Task 41 | 42 | def process(self, ctx: AnsibleRunContext): 43 | task = ctx.current 44 | 45 | role_ref_arg = task.args.get("name") 46 | verdict = task.action_type == ActionType.ROLE_TYPE and role_ref_arg and role_ref_arg.is_mutable 47 | role_ref = role_ref_arg.raw if role_ref_arg else None 48 | detail = { 49 | "role": role_ref, 50 | } 51 | 52 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 53 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R109_key_config_change.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import ( 19 | AnsibleRunContext, 20 | RunTargetType, 21 | DefaultRiskType as RiskType, 22 | AnnotationCondition, 23 | Rule, 24 | Severity, 25 | RuleTag as Tag, 26 | RuleResult, 27 | ) 28 | 29 | 30 | @dataclass 31 | class KeyConfigChangeRule(Rule): 32 | rule_id: str = "R109" 33 | description: str = "Key configuration is changed" 34 | enabled: bool = True 35 | name: str = "ConfigChange" 36 | version: str = "v0.0.1" 37 | severity: Severity = Severity.LOW 38 | tags: tuple = Tag.SYSTEM 39 | 40 | def match(self, ctx: AnsibleRunContext) -> bool: 41 | return ctx.current.type == RunTargetType.Task 42 | 43 | def process(self, ctx: AnsibleRunContext): 44 | task = ctx.current 45 | 46 | ac = AnnotationCondition().risk_type(RiskType.CONFIG_CHANGE).attr("is_mutable_key", True) 47 | verdict = task.has_annotation_by_condition(ac) 48 | 49 | detail = {} 50 | if verdict: 51 | anno = task.get_annotation_by_condition(ac) 52 | if anno: 53 | detail["key"] = anno.key 54 | 55 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 56 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R306_undefined_variable.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | VariableType, 23 | Rule, 24 | Severity, 25 | RuleTag as Tag, 26 | RuleResult, 27 | ) 28 | 29 | 30 | @dataclass 31 | class UndefinedVariableRule(Rule): 32 | rule_id: str = "R306" 33 | description: str = "Undefined variable is found" 34 | enabled: bool = True 35 | name: str = "UndefinedVariable" 36 | version: str = "v0.0.1" 37 | severity: Severity = Severity.LOW 38 | tags: tuple = Tag.VARIABLE 39 | 40 | def match(self, ctx: AnsibleRunContext) -> bool: 41 | return ctx.current.type == RunTargetType.Task 42 | 43 | def process(self, ctx: AnsibleRunContext): 44 | task = ctx.current 45 | 46 | verdict = False 47 | detail = {} 48 | for v_name in task.variable_use: 49 | v = task.variable_use[v_name] 50 | if v and v[-1].type == VariableType.Unknown: 51 | verdict = True 52 | current = detail.get("undefined_variables", []) 53 | current.append(v_name) 54 | detail["undefined_variables"] = current 55 | 56 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 57 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R101_command_exec.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import DefaultRiskType as RiskType 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | AnnotationCondition, 23 | Rule, 24 | Severity, 25 | RuleTag as Tag, 26 | RuleResult, 27 | ) 28 | 29 | 30 | @dataclass 31 | class CommandExecRule(Rule): 32 | rule_id: str = "R101" 33 | description: str = "A parameterized command execution found" 34 | enabled: bool = True 35 | name: str = "CommandExec" 36 | version: str = "v0.0.1" 37 | severity: Severity = Severity.LOW 38 | tags: tuple = Tag.COMMAND 39 | 40 | def match(self, ctx: AnsibleRunContext) -> bool: 41 | return ctx.current.type == RunTargetType.Task 42 | 43 | def process(self, ctx: AnsibleRunContext): 44 | task = ctx.current 45 | 46 | ac = AnnotationCondition().risk_type(RiskType.CMD_EXEC).attr("is_mutable_cmd", True) 47 | verdict = task.has_annotation_by_condition(ac) 48 | 49 | detail = {} 50 | if verdict: 51 | anno = task.get_annotation_by_condition(ac) 52 | if anno: 53 | detail["cmd"] = anno.command.raw 54 | 55 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 56 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R301_non_fqcn_use.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import ( 19 | AnsibleRunContext, 20 | RunTargetType, 21 | ExecutableType as ActionType, 22 | Rule, 23 | Severity, 24 | RuleTag as Tag, 25 | RuleResult, 26 | ) 27 | 28 | 29 | @dataclass 30 | class NonFQCNUseRule(Rule): 31 | rule_id: str = "R301" 32 | description: str = "A task with a short module name is found" 33 | enabled: bool = True 34 | name: str = "NonFQCNUse" 35 | version: str = "v0.0.1" 36 | severity: Severity = Severity.VERY_LOW 37 | tags: tuple = Tag.DEPENDENCY 38 | 39 | def match(self, ctx: AnsibleRunContext) -> bool: 40 | return ctx.current.type == RunTargetType.Task 41 | 42 | def process(self, ctx: AnsibleRunContext): 43 | task = ctx.current 44 | 45 | verdict = ( 46 | task.action_type == ActionType.MODULE_TYPE 47 | and task.spec.action 48 | and task.resolved_action 49 | and task.spec.action != task.resolved_action 50 | and not task.resolved_action.startswith("ansible.builtin.") 51 | ) 52 | detail = { 53 | "module": task.spec.action, 54 | "fqcn": task.resolved_name, 55 | } 56 | 57 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 58 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R105_outbound_transfer.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import ( 19 | AnsibleRunContext, 20 | RunTargetType, 21 | DefaultRiskType as RiskType, 22 | AnnotationCondition, 23 | Rule, 24 | Severity, 25 | RuleTag as Tag, 26 | RuleResult, 27 | ) 28 | 29 | 30 | @dataclass 31 | class InboundTransferRule(Rule): 32 | rule_id: str = "R105" 33 | description: str = "An outbound network transfer to a parameterized URL is found" 34 | enabled: bool = True 35 | name: str = "OutboundTransfer" 36 | version: str = "v0.0.1" 37 | severity: Severity = Severity.MEDIUM 38 | tags: tuple = Tag.NETWORK 39 | 40 | def match(self, ctx: AnsibleRunContext) -> bool: 41 | return ctx.current.type == RunTargetType.Task 42 | 43 | def process(self, ctx: AnsibleRunContext): 44 | task = ctx.current 45 | 46 | ac = AnnotationCondition().risk_type(RiskType.OUTBOUND).attr("is_mutable_dest", True) 47 | verdict = task.has_annotation_by_condition(ac) 48 | 49 | detail = {} 50 | if verdict: 51 | anno = task.get_annotation_by_condition(ac) 52 | if anno: 53 | detail["from"] = anno.src.value 54 | detail["to"] = anno.dest.value 55 | 56 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 57 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R115_file_deletion.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | DefaultRiskType as RiskType, 23 | AnnotationCondition, 24 | Rule, 25 | Severity, 26 | RuleTag as Tag, 27 | RuleResult, 28 | ) 29 | 30 | 31 | @dataclass 32 | class FileDeletionRule(Rule): 33 | rule_id: str = "R115" 34 | description: str = "File deletion found. Directories will be recursively deleted." 35 | enabled: bool = False 36 | name: str = "FileDeletionRule" 37 | version: str = "v0.0.1" 38 | severity: Severity = Severity.LOW 39 | tags: tuple = Tag.SYSTEM 40 | 41 | def match(self, ctx: AnsibleRunContext) -> bool: 42 | return ctx.current.type == RunTargetType.Task 43 | 44 | def process(self, ctx: AnsibleRunContext): 45 | task = ctx.current 46 | 47 | # define a condition for this rule here 48 | ac = AnnotationCondition().risk_type(RiskType.FILE_CHANGE).attr("is_delete", True).attr("is_mutable_path", True) 49 | verdict = task.has_annotation_by_condition(ac) 50 | 51 | detail = {} 52 | if verdict: 53 | anno = task.get_annotation_by_condition(ac) 54 | if anno: 55 | detail["path"] = anno.path.value 56 | 57 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 58 | -------------------------------------------------------------------------------- /ansible_risk_insight/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 sys 18 | from .cli import ARICLI 19 | from .cli.ram import RAMCLI 20 | from ansible_risk_insight.scanner import ARIScanner, Config 21 | 22 | ari_actions = ["project", "playbook", "collection", "role", "taskfile"] 23 | ram_actions = ["ram"] 24 | 25 | all_actions = ari_actions + ram_actions 26 | 27 | 28 | def main(): 29 | if len(sys.argv) == 1: 30 | 31 | print("Please specify one of the following operations of ari.") 32 | print("[operations]") 33 | print(" playbook scan a playbook (e.g. `ari playbook path/to/playbook.yml` )") 34 | print(" collection scan a collection (e.g. `ari collection collection.name` )") 35 | print(" role scan a role (e.g. `ari role role.name` )") 36 | print(" project scan a project (e.g. `ari project path/to/project`)") 37 | print(" taskfile scan a taskfile (e.g. `ari taskfile path/to/taskfile.yml`)") 38 | print(" ram operate the backend data (e.g. `ari ram generate -f input.txt`)") 39 | sys.exit() 40 | 41 | action = sys.argv[1] 42 | 43 | if action in ari_actions: 44 | cli = ARICLI() 45 | cli.run() 46 | elif action == "ram": 47 | cli = RAMCLI() 48 | cli.run() 49 | else: 50 | print(f"The action {action} is not supported!", file=sys.stderr) 51 | sys.exit(1) 52 | 53 | 54 | __all__ = ["ARIScanner", "Config", "models"] 55 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R113_parameterized_pkg_install.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import ( 19 | AnsibleRunContext, 20 | RunTargetType, 21 | DefaultRiskType as RiskType, 22 | AnnotationCondition, 23 | Rule, 24 | Severity, 25 | RuleTag as Tag, 26 | RuleResult, 27 | ) 28 | 29 | 30 | @dataclass 31 | class PkgInstallRuleResult(RuleResult): 32 | pass 33 | 34 | 35 | @dataclass 36 | class PkgInstallRule(Rule): 37 | rule_id: str = "R113" 38 | description: str = "A parameterized pkg installation is found" 39 | enabled: bool = True 40 | name: str = "PkgInstall" 41 | version: str = "v0.0.1" 42 | severity: Severity = Severity.MEDIUM 43 | tags: tuple = Tag.PACKAGE 44 | result_type: type = PkgInstallRuleResult 45 | 46 | def match(self, ctx: AnsibleRunContext) -> bool: 47 | return ctx.current.type == RunTargetType.Task 48 | 49 | def process(self, ctx: AnsibleRunContext): 50 | task = ctx.current 51 | 52 | ac = AnnotationCondition().risk_type(RiskType.PACKAGE_INSTALL).attr("is_mutable_pkg", True) 53 | verdict = task.has_annotation_by_condition(ac) 54 | 55 | detail = {} 56 | if verdict: 57 | anno = task.get_annotation_by_condition(ac) 58 | if anno: 59 | detail["pkg"] = anno.pkg 60 | 61 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 62 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R201_changed_data_dependence.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | Rule, 23 | Severity, 24 | RuleTag as Tag, 25 | RuleResult, 26 | ) 27 | 28 | 29 | @dataclass 30 | class ChangedDataDependenceRule(Rule): 31 | rule_id: str = "R201" 32 | description: str = "A variable is re-defined" 33 | enabled: bool = True 34 | name: str = "ChangedDataDependence" 35 | version: str = "v0.0.1" 36 | severity: Severity = Severity.VERY_LOW 37 | tags: tuple = Tag.VARIABLE 38 | 39 | def match(self, ctx: AnsibleRunContext) -> bool: 40 | return ctx.current.type == RunTargetType.Task 41 | 42 | def process(self, ctx: AnsibleRunContext): 43 | task = ctx.current 44 | 45 | verdict = False 46 | detail = {"variables": []} 47 | if task.spec.defined_vars: 48 | for v in task.spec.defined_vars: 49 | all_definitions = task.variable_set.get(v, []) 50 | if len(all_definitions) > 1: 51 | detail["variables"].append( 52 | { 53 | "name": v, 54 | "defined_by": [d.setter for d in all_definitions], 55 | } 56 | ) 57 | verdict = True 58 | 59 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 60 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R401_list_all_inbound_src.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | DefaultRiskType as RiskType, 23 | AnnotationCondition, 24 | Rule, 25 | Severity, 26 | RuleTag as Tag, 27 | RuleResult, 28 | ) 29 | 30 | 31 | @dataclass 32 | class ListAllInboundSrcRule(Rule): 33 | rule_id: str = "R401" 34 | description: str = "List all inbound sources" 35 | enabled: bool = True 36 | name: str = "ListAllInboundSrcRule" 37 | version: str = "v0.0.1" 38 | severity: Severity = Severity.VERY_LOW 39 | tags: tuple = Tag.DEBUG 40 | 41 | def match(self, ctx: AnsibleRunContext) -> bool: 42 | return ctx.current.type == RunTargetType.Task 43 | 44 | def process(self, ctx: AnsibleRunContext): 45 | task = ctx.current 46 | 47 | ac = AnnotationCondition().risk_type(RiskType.INBOUND) 48 | verdict = False 49 | detail = {} 50 | src_list = [] 51 | if ctx.is_end(task): 52 | tasks = ctx.search(ac) 53 | for t in tasks: 54 | anno = t.get_annotation_by_condition(ac) 55 | if anno: 56 | src_list.append(anno.src.value) 57 | if len(src_list) > 0: 58 | verdict = True 59 | detail["inbound_src"] = src_list 60 | 61 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 62 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R404_show_variables.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | VariableDict, 22 | RunTargetType, 23 | Rule, 24 | Severity, 25 | RuleTag as Tag, 26 | RuleResult, 27 | ) 28 | 29 | 30 | @dataclass 31 | class ShowVariablesRule(Rule): 32 | rule_id: str = "R404" 33 | description: str = "Show all variables" 34 | enabled: bool = False 35 | name: str = "ShowVariables" 36 | version: str = "v0.0.1" 37 | severity: Severity = Severity.NONE 38 | tags: tuple = Tag.VARIABLE 39 | 40 | def match(self, ctx: AnsibleRunContext) -> bool: 41 | return ctx.current.type == RunTargetType.Task 42 | 43 | def process(self, ctx: AnsibleRunContext): 44 | task = ctx.current 45 | 46 | verdict = True 47 | detail = {"variables": task.variable_set} 48 | 49 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 50 | 51 | def print(self, result: RuleResult): 52 | variables = result.detail["variables"] 53 | var_table = "None" 54 | if variables: 55 | var_table = "\n" + VariableDict.print_table(variables) 56 | output = f"ruleID={self.rule_id}, \ 57 | severity={self.severity}, \ 58 | description={self.description}, \ 59 | verdict={result.verdict}, \ 60 | file={result.file}, \ 61 | variables={var_table}\n" 62 | return output 63 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R106_inbound_transfer.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import ( 19 | AnsibleRunContext, 20 | RunTargetType, 21 | DefaultRiskType as RiskType, 22 | AnnotationCondition, 23 | Rule, 24 | Severity, 25 | RuleTag as Tag, 26 | RuleResult, 27 | ) 28 | 29 | 30 | @dataclass 31 | class InboundRuleResult(RuleResult): 32 | pass 33 | 34 | 35 | @dataclass 36 | class InboundTransferRule(Rule): 37 | rule_id: str = "R101" 38 | description: str = "A inbound network transfer from a parameterized source is found" 39 | enabled: bool = True 40 | name: str = "InboundTransfer" 41 | version: str = "v0.0.1" 42 | severity: Severity = Severity.MEDIUM 43 | tags: tuple = Tag.NETWORK 44 | result_type: type = InboundRuleResult 45 | 46 | def match(self, ctx: AnsibleRunContext) -> bool: 47 | return ctx.current.type == RunTargetType.Task 48 | 49 | def process(self, ctx: AnsibleRunContext): 50 | task = ctx.current 51 | 52 | ac = AnnotationCondition().risk_type(RiskType.INBOUND).attr("is_mutable_src", True) 53 | verdict = task.has_annotation_by_condition(ac) 54 | 55 | detail = {} 56 | if verdict: 57 | anno = task.get_annotation_by_condition(ac) 58 | if anno: 59 | detail["from"] = anno.src.value 60 | detail["to"] = anno.dest.value 61 | 62 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 63 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R112_parameterized_import_taskfile.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import ( 19 | AnsibleRunContext, 20 | RunTargetType, 21 | ExecutableType as ActionType, 22 | Rule, 23 | Severity, 24 | RuleTag as Tag, 25 | RuleResult, 26 | ) 27 | 28 | 29 | @dataclass 30 | class ParameterizedImportTaskfileRule(Rule): 31 | rule_id: str = "R112" 32 | description: str = "Import/include a parameterized name of taskfile" 33 | enabled: bool = True 34 | name: str = "ParameterizedImportTaskfile" 35 | version: str = "v0.0.1" 36 | severity: Severity = Severity.MEDIUM 37 | tags: tuple = Tag.DEPENDENCY 38 | 39 | def match(self, ctx: AnsibleRunContext) -> bool: 40 | return ctx.current.type == RunTargetType.Task 41 | 42 | def process(self, ctx: AnsibleRunContext): 43 | task = ctx.current 44 | 45 | # import_tasks: xxx.yml 46 | # or 47 | # import_tasks: 48 | # file: yyy.yml 49 | 50 | taskfile_ref_arg = task.args.get("file") 51 | if not taskfile_ref_arg: 52 | taskfile_ref_arg = task.args 53 | 54 | verdict = task.action_type == ActionType.TASKFILE_TYPE and taskfile_ref_arg and taskfile_ref_arg.is_mutable 55 | taskfile_ref = taskfile_ref_arg.raw if taskfile_ref_arg else None 56 | detail = { 57 | "taskfile": taskfile_ref, 58 | } 59 | 60 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 61 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R501_dependency_suggestion.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | Rule, 23 | Severity, 24 | RuleTag as Tag, 25 | RuleResult, 26 | ) 27 | 28 | 29 | @dataclass 30 | class DependencySuggestionRule(Rule): 31 | rule_id: str = "R501" 32 | description: str = "Suggest dependencies for unresolved modules/roles" 33 | enabled: bool = True 34 | name: str = "DependencySuggestion" 35 | version: str = "v0.0.1" 36 | severity: Severity = Severity.NONE 37 | tags: tuple = Tag.DEPENDENCY 38 | 39 | def match(self, ctx: AnsibleRunContext) -> bool: 40 | return ctx.current.type == RunTargetType.Task 41 | 42 | def process(self, ctx: AnsibleRunContext): 43 | task = ctx.current 44 | 45 | verdict = False 46 | detail = {} 47 | if task.spec.possible_candidates: 48 | verdict = True 49 | detail["type"] = task.spec.executable_type.lower() 50 | detail["fqcn"] = task.spec.possible_candidates[0][0] 51 | req_info = task.spec.possible_candidates[0][1] 52 | detail["suggestion"] = {} 53 | detail["suggestion"]["type"] = req_info.get("type", "") 54 | detail["suggestion"]["name"] = req_info.get("name", "") 55 | detail["suggestion"]["version"] = req_info.get("version", "") 56 | 57 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 58 | -------------------------------------------------------------------------------- /ansible_risk_insight/cli/ram/update.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | 19 | from ...ram_generator import RiskAssessmentModelGenerator as RAMGenerator 20 | 21 | 22 | class RAMUpdateCLI: 23 | args = None 24 | 25 | def __init__(self): 26 | parser = argparse.ArgumentParser(description="TODO") 27 | parser.add_argument("target_type", help="content type", choices={"ram"}) 28 | parser.add_argument("action", help="action for RAM command or target_name of search action") 29 | parser.add_argument("-f", "--file", help='target list like "collection community.general"') 30 | parser.add_argument("-r", "--resume", help="line number to resume scanning") 31 | args = parser.parse_args() 32 | self.args = args 33 | 34 | def run(self): 35 | args = self.args 36 | action = args.action 37 | if action != "update": 38 | raise ValueError('RAMUpdateCLI cannot be executed without "update" action') 39 | 40 | target_list = [] 41 | with open(args.file, "r") as file: 42 | for line in file: 43 | parts = line.replace("\n", "").split(" ") 44 | if len(parts) != 2: 45 | raise ValueError('target list file must be lines of " " such as "collection community.general"') 46 | target_list.append((parts[0], parts[1])) 47 | 48 | resume = -1 49 | if args.resume: 50 | resume = int(args.resume) 51 | ram_generator = RAMGenerator(target_list, resume, update=True) 52 | ram_generator.run() 53 | -------------------------------------------------------------------------------- /ansible_risk_insight/cli/ram/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 sys 18 | 19 | from .search import RAMSearchCLI 20 | from .list import RAMListCLI 21 | from .diff import RAMDiffCLI 22 | from .generate import RAMGenerateCLI 23 | from .update import RAMUpdateCLI 24 | from .release import RAMReleaseCLI 25 | 26 | 27 | ram_actions = ["search", "list", "diff", "generate", "update", "release"] 28 | 29 | 30 | class RAMCLI: 31 | _cli = None 32 | 33 | def __init__(self): 34 | 35 | args = sys.argv 36 | if len(args) > 2: 37 | action = args[2] 38 | # "search" can be abbreviated 39 | if action not in ram_actions: 40 | action = "search" 41 | target_name = sys.argv[2] 42 | sys.argv[2] = action 43 | sys.argv.insert(3, target_name) 44 | 45 | if action == "search": 46 | self._cli = RAMSearchCLI() 47 | elif action == "list": 48 | self._cli = RAMListCLI() 49 | elif action == "diff": 50 | self._cli = RAMDiffCLI() 51 | elif action == "generate": 52 | self._cli = RAMGenerateCLI() 53 | elif action == "update": 54 | self._cli = RAMUpdateCLI() 55 | elif action == "release": 56 | self._cli = RAMReleaseCLI() 57 | else: 58 | raise ValueError(f"The action {action} is not supported") 59 | else: 60 | raise ValueError(f"An action must be specified; {ram_actions}") 61 | 62 | def run(self): 63 | if self._cli: 64 | self._cli.run() 65 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R107_pkg_install_with_insecure_option.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import ( 19 | AnsibleRunContext, 20 | RunTargetType, 21 | DefaultRiskType as RiskType, 22 | AnnotationCondition, 23 | Rule, 24 | Severity, 25 | RuleTag as Tag, 26 | RuleResult, 27 | ) 28 | 29 | 30 | @dataclass 31 | class InsecurePkgInstallRule(Rule): 32 | rule_id: str = "R107" 33 | description: str = "A package installation with insecure option is found" 34 | enabled: bool = True 35 | name: str = "InsecurePkgInstall" 36 | version: str = "v0.0.1" 37 | severity: Severity = Severity.HIGH 38 | tags: tuple = Tag.PACKAGE 39 | 40 | def match(self, ctx: AnsibleRunContext) -> bool: 41 | return ctx.current.type == RunTargetType.Task 42 | 43 | def process(self, ctx: AnsibleRunContext): 44 | task = ctx.current 45 | 46 | ac = AnnotationCondition().risk_type(RiskType.PACKAGE_INSTALL).attr("disable_validate_certs", True) 47 | ac2 = AnnotationCondition().risk_type(RiskType.PACKAGE_INSTALL).attr("allow_downgrade", True) 48 | verdict = task.has_annotation_by_condition(ac) or task.has_annotation_by_condition(ac2) 49 | 50 | detail = {} 51 | if verdict: 52 | anno = task.get_annotation_by_condition(ac) 53 | if anno: 54 | detail["pkg"] = anno.pkg 55 | anno2 = task.get_annotation_by_condition(ac2) 56 | if anno2: 57 | detail["pkg"] = anno2.pkg 58 | 59 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 60 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R114_file_change.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import ( 19 | AnsibleRunContext, 20 | RunTargetType, 21 | DefaultRiskType as RiskType, 22 | AnnotationCondition, 23 | Rule, 24 | Severity, 25 | RuleTag as Tag, 26 | RuleResult, 27 | ) 28 | 29 | 30 | @dataclass 31 | class FileChangeRule(Rule): 32 | rule_id: str = "R114" 33 | description: str = "Parameterized file change is found" 34 | enabled: bool = True 35 | name: str = "ConfigChange" 36 | version: str = "v0.0.1" 37 | severity: Severity = Severity.LOW 38 | tags: tuple = Tag.SYSTEM 39 | 40 | def match(self, ctx: AnsibleRunContext) -> bool: 41 | return ctx.current.type == RunTargetType.Task 42 | 43 | def process(self, ctx: AnsibleRunContext): 44 | task = ctx.current 45 | 46 | ac = AnnotationCondition().risk_type(RiskType.FILE_CHANGE).attr("is_mutable_path", True) 47 | ac2 = AnnotationCondition().risk_type(RiskType.FILE_CHANGE).attr("is_mutable_src", True) 48 | verdict = False 49 | detail = {} 50 | if task.has_annotation_by_condition(ac): 51 | verdict = True 52 | anno = task.get_annotation_by_condition(ac) 53 | if anno: 54 | detail["path"] = anno.path.value 55 | 56 | if task.has_annotation_by_condition(ac2): 57 | verdict = True 58 | anno = task.get_annotation_by_condition(ac2) 59 | if anno: 60 | detail["src"] = anno.src.value 61 | 62 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 63 | -------------------------------------------------------------------------------- /docs/annotation.md: -------------------------------------------------------------------------------- 1 | ## Annotation 2 | 3 | ARI provides some default annotations. 4 | Annotations are attached to each node so that it can be retrieved from each context. 5 | From your rule, you can get an annotation by defining annotation `key` to `get_annotation` method. 6 | ```python 7 | def process(self, ctx: AnsibleRunContext): 8 | task = ctx.current 9 | # getting resolved fqcn data from annotation 10 | resolved_fqcn = task.get_annotation(key="module.resolved_fqcn") 11 | ``` 12 | 13 | The default annotations are shown in the tables. 14 | 15 | 1. annotations about module 16 | 17 | | key | value | 18 | |--- |--- | 19 | | module.wrong_module_name | non-existing module name | 20 | | module.suggested_fqcn | inferred FQCN | 21 | | module.resolved_fqcn | resolved FQCN from dependency list | 22 | | module.not_exist | true if non-existing module name is used | 23 | | module.correct_fqcn | best guess for FQCN | 24 | | module.need_correction | true if module name should be replaced with correct_fqcn | 25 | | module.suggested_dependency | inferred dependencies | 26 | 27 | 28 | 29 | 2. annotations about module argument key 30 | 31 | | key | value | 32 | |--- |--- | 33 | | module.wrong_arg_keys | non-existing arg keys for the module | 34 | | module.available_arg_keys | supported arg keys for the module | 35 | | module.required_arg_keys | required arg keys for the module | 36 | | module.missing_required_arg_keys | missing required arg keys for the module | 37 | | module.available_args | detail information for the module (derived via ansible-doc) | 38 | | module.used_alias_and_real_keys | arg keys using alias | 39 | 40 | 41 | 3. annotations about module argument value 42 | 43 | | key | value | 44 | |--- |--- | 45 | | module.wrong_arg_values | arg values using wrong format, expected format is also included| 46 | | module.undefined_values | undefined arg values | 47 | | module.unknown_type_values | parameterized arg values (= cannot determine the value type) | 48 | 49 | 50 | 51 | 4. annotations about variable 52 | 53 | | key | value | 54 | |--- |--- | 55 | | variable.undefined_vars | undefined variables used in the task | 56 | | variable.unnecessary_loop_vars | unnecessary loop vars (e.g. “item”) used outside of the loop | 57 | | variable.unknown_name_vars | undefined var & the var name is different from the key name | 58 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R202_unconditional_override.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | Rule, 23 | Severity, 24 | RuleTag as Tag, 25 | RuleResult, 26 | ) 27 | 28 | 29 | @dataclass 30 | class UnconditionalOverrideRule(Rule): 31 | rule_id: str = "R202" 32 | description: str = "A variable is re-defined without any conditions" 33 | enabled: bool = True 34 | name: str = "UnconditionalOverride" 35 | version: str = "v0.0.1" 36 | severity: Severity = Severity.VERY_LOW 37 | tags: tuple = Tag.VARIABLE 38 | 39 | def match(self, ctx: AnsibleRunContext) -> bool: 40 | return ctx.current.type == RunTargetType.Task 41 | 42 | def process(self, ctx: AnsibleRunContext): 43 | task = ctx.current 44 | 45 | verdict = False 46 | detail = {"variables": []} 47 | if not task.spec.tags and not task.spec.when: 48 | if task.spec.defined_vars: 49 | for v in task.spec.defined_vars: 50 | all_definitions = task.variable_set.get(v, []) 51 | if len(all_definitions) > 1: 52 | detail["variables"].append( 53 | { 54 | "name": v, 55 | "defined_by": [d.setter for d in all_definitions], 56 | "type": [d.type for d in all_definitions], 57 | } 58 | ) 59 | verdict = True 60 | 61 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 62 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R203_unused_override.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | Rule, 23 | Severity, 24 | RuleTag as Tag, 25 | RuleResult, 26 | ) 27 | 28 | 29 | @dataclass 30 | class UnusedOverrideRule(Rule): 31 | rule_id: str = "R203" 32 | description: str = "A variable is not successfully re-defined because of low precedence" 33 | enabled: bool = True 34 | name: str = "UnusedOverride" 35 | version: str = "v0.0.1" 36 | severity: Severity = Severity.VERY_LOW 37 | tags: tuple = Tag.VARIABLE 38 | 39 | def match(self, ctx: AnsibleRunContext) -> bool: 40 | return ctx.current.type == RunTargetType.Task 41 | 42 | def process(self, ctx: AnsibleRunContext): 43 | task = ctx.current 44 | 45 | verdict = False 46 | detail = {"variables": []} 47 | if task.spec.defined_vars: 48 | for v in task.spec.defined_vars: 49 | all_definitions = task.variable_set.get(v, []) 50 | if len(all_definitions) > 1: 51 | prev_prec = all_definitions[-2].type 52 | new_prec = all_definitions[-1].type 53 | if new_prec < prev_prec: 54 | detail["variables"].append( 55 | { 56 | "name": v, 57 | "prev_precedence": prev_prec, 58 | "new_precedence": new_prec, 59 | } 60 | ) 61 | verdict = True 62 | 63 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 64 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/ansible.builtin/unarchive.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 | from typing import List 18 | from ansible_risk_insight.models import Annotation, RiskAnnotation, TaskCall, DefaultRiskType, InboundTransferDetail 19 | from ansible_risk_insight.utils import parse_bool 20 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 21 | 22 | 23 | class UnarchiveAnnotator(ModuleAnnotator): 24 | fqcn: str = "ansible.builtin.unarchive" 25 | enabled: bool = True 26 | 27 | def run(self, task: TaskCall) -> List[Annotation]: 28 | src = task.args.get("src") # required 29 | dest = task.args.get("dest") # required 30 | remote_src = task.args.get("remote_src") 31 | 32 | is_remote_src = False 33 | if remote_src: 34 | 35 | if isinstance(remote_src.raw, str) or isinstance(remote_src.raw, bool): 36 | try: 37 | is_remote_src = parse_bool(remote_src.raw) 38 | except Exception: 39 | pass 40 | if not is_remote_src and (isinstance(remote_src.templated, str) or isinstance(remote_src.templated, bool)): 41 | try: 42 | is_remote_src = parse_bool(remote_src.templated) 43 | except Exception: 44 | pass 45 | 46 | url_sep = "://" 47 | is_download = False 48 | if is_remote_src and (url_sep in src.raw or url_sep in src.templated): 49 | is_download = True 50 | 51 | if not is_download: 52 | return None 53 | 54 | annotation = RiskAnnotation.init(risk_type=DefaultRiskType.INBOUND, detail=InboundTransferDetail(_src_arg=src, _dest_arg=dest)) 55 | return ModuleAnnotatorResult(annotations=[annotation]) 56 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R204_unnecessary_set_fact.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | ExecutableType as ActionType, 23 | Rule, 24 | Severity, 25 | RuleTag as Tag, 26 | RuleResult, 27 | ) 28 | 29 | 30 | @dataclass 31 | class UnnecessarySetFactRule(Rule): 32 | rule_id: str = "R204" 33 | description: str = "set_fact is used without random filter" 34 | enabled: bool = True 35 | name: str = "UnnecessarySetFact" 36 | version: str = "v0.0.1" 37 | severity: Severity = Severity.VERY_LOW 38 | tags: tuple = Tag.VARIABLE 39 | 40 | def match(self, ctx: AnsibleRunContext) -> bool: 41 | return ctx.current.type == RunTargetType.Task 42 | 43 | def process(self, ctx: AnsibleRunContext): 44 | task = ctx.current 45 | 46 | args = task.args.raw 47 | is_impure = False 48 | detail = {} 49 | if isinstance(args, str): 50 | is_impure = "random" in args 51 | detail["impure_args"] = args 52 | elif isinstance(args, dict): 53 | for v in args.values(): 54 | if isinstance(v, str) and "random" in v: 55 | is_impure = True 56 | current = detail.get("impure_args", []) 57 | if not current: 58 | current = [] 59 | detail["impure_args"] = current.append(v) 60 | 61 | verdict = ( 62 | task.action_type == ActionType.MODULE_TYPE and task.resolved_action and task.resolved_action == "ansible.builtin.set_fact" and is_impure 63 | ) 64 | 65 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /ansible_risk_insight/yaml.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2023 IBM Corp. All rights reserved. 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 | # https://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 io 18 | from contextvars import ContextVar 19 | from ruamel.yaml import YAML 20 | from ruamel.yaml.emitter import EmitterError 21 | 22 | 23 | _yaml: ContextVar[YAML] = ContextVar("yaml") 24 | 25 | 26 | def _set_yaml(force=False): 27 | if not _yaml.get(None) or force: 28 | yaml = YAML(typ="rt", pure=True) 29 | yaml.default_flow_style = False 30 | yaml.preserve_quotes = True 31 | yaml.allow_duplicate_keys = True 32 | yaml.width = 1024 33 | _yaml.set(yaml) 34 | 35 | 36 | def config(**kwargs): 37 | _set_yaml() 38 | yaml = _yaml.get() 39 | for key, value in kwargs.items(): 40 | setattr(yaml, key, value) 41 | _yaml.set(yaml) 42 | 43 | 44 | def indent(**kwargs): 45 | _set_yaml() 46 | yaml = _yaml.get() 47 | yaml.indent(**kwargs) 48 | _yaml.set(yaml) 49 | 50 | 51 | def load(stream: any): 52 | _set_yaml() 53 | yaml = _yaml.get() 54 | return yaml.load(stream) 55 | 56 | 57 | # `ruamel.yaml` has a bug around multi-threading, and its YAML() instance could be broken 58 | # while concurrent dump() operation. So we try retrying if the specific error occurs. 59 | # Bug details: https://sourceforge.net/p/ruamel-yaml/tickets/367/ 60 | def dump(data: any): 61 | _set_yaml() 62 | retry = 2 63 | err = None 64 | result = None 65 | for i in range(retry): 66 | try: 67 | yaml = _yaml.get() 68 | output = io.StringIO() 69 | yaml.dump(data, output) 70 | result = output.getvalue() 71 | except EmitterError as exc: 72 | err = exc 73 | except Exception: 74 | raise 75 | if err: 76 | if i < retry - 1: 77 | _set_yaml(force=True) 78 | err = None 79 | else: 80 | raise err 81 | else: 82 | break 83 | return result 84 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/sample_custom_annotator.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from typing import List 18 | from ansible_risk_insight.models import TaskCall, Annotation, RiskAnnotation, DefaultRiskType 19 | from ansible_risk_insight.annotators.variable_resolver import VariableAnnotation 20 | from ansible_risk_insight.annotators.risk_annotator_base import RiskAnnotator 21 | 22 | 23 | class SampleCustomAnnotator(RiskAnnotator): 24 | name: str = "sample" 25 | enabled: bool = False 26 | 27 | # whether this task should be analyzed by this or not 28 | def match(self, taskcall: TaskCall) -> bool: 29 | # resolved_name = taskcall.resolved_name 30 | # return resolved_name.startswith("sample.custom.") 31 | return False 32 | 33 | # extract analyzed_data from task and embed it 34 | def run(self, task: TaskCall) -> List[Annotation]: 35 | resolved_name = task.spec.resolved_name 36 | options = task.spec.module_options 37 | var_annos = task.get_annotation_by_type(VariableAnnotation.type) 38 | var_anno = var_annos[0] if len(var_annos) > 0 else VariableAnnotation() 39 | resolved_options = var_anno.resolved_module_options 40 | 41 | annotations = [] 42 | # example of package_install 43 | if resolved_name == "sample.custom.homebrew": 44 | res = RiskAnnotation(type=self.type, category=DefaultRiskType.PACKAGE_INSTALL) 45 | res.data = self.homebrew(options) 46 | for ro in resolved_options: 47 | res.resolved_data.append(self.homebrew(ro)) 48 | annotations.append(res) 49 | return annotations 50 | 51 | def homebrew(self, options): 52 | data = {} 53 | if type(options) is not dict: 54 | return data 55 | if "name" in options: 56 | data["pkg"] = options["name"] 57 | if "state" in options and options["state"] == "absent": 58 | data["delete"] = True 59 | return data 60 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R104_unauthorized_download_src.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 re 18 | from dataclasses import dataclass 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | DefaultRiskType as RiskType, 23 | AnnotationCondition, 24 | Rule, 25 | Severity, 26 | RuleTag as Tag, 27 | RuleResult, 28 | ) 29 | 30 | 31 | allow_url_list = ["https://*"] 32 | 33 | deny_url_list = ["http://*"] 34 | 35 | 36 | @dataclass 37 | class InvalidDownloadSourceRule(Rule): 38 | rule_id: str = "R104" 39 | description: str = "A network transfer from unauthorized source is found." 40 | enabled: bool = True 41 | name: str = "InvalidDownloadSource" 42 | version: str = "v0.0.1" 43 | severity: Severity = Severity.HIGH 44 | tags: tuple = Tag.NETWORK 45 | 46 | def match(self, ctx: AnsibleRunContext) -> bool: 47 | return ctx.current.type == RunTargetType.Task 48 | 49 | def process(self, ctx: AnsibleRunContext): 50 | task = ctx.current 51 | 52 | ac = AnnotationCondition().risk_type(RiskType.INBOUND) 53 | 54 | verdict = False 55 | detail = {} 56 | 57 | anno = task.get_annotation_by_condition(ac) 58 | if anno: 59 | if not self.is_allowed_url(anno.src.value, allow_url_list, deny_url_list): 60 | verdict = True 61 | detail["invalid_src"] = anno.src.value 62 | 63 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 64 | 65 | def is_allowed_url(self, src, allow_list, deny_list): 66 | matched = True 67 | if len(allow_list) > 0: 68 | matched = False 69 | for a in allow_list: 70 | res = re.match(a, src) 71 | if res: 72 | matched = True 73 | elif len(deny_list) > 0: 74 | for d in deny_list: 75 | res = re.match(d, src) 76 | if res: 77 | matched = False 78 | return matched 79 | -------------------------------------------------------------------------------- /test/testdata/inline_replace_data/block_and_when_play.yml: -------------------------------------------------------------------------------- 1 | - hosts: myhosts 2 | connection: local 3 | gather_facts: false 4 | tasks: 5 | 6 | - name: Validate server authentication input provided by user 7 | when: 8 | - (username is not defined or password is not defined) and (cert_file is not defined 9 | or key_file is not defined) and (auth_token is not defined) 10 | ansible.builtin.fail: 11 | msg: username/password or cert_file/key_file or auth_token is mandatory 12 | 13 | - name: Fail when more than one valid authentication method is provided 14 | when: 15 | - ((username is defined or password is defined) and (cert_file is defined or key_file 16 | is defined) and auth_token is defined) or ((username is defined or password 17 | is defined) and (cert_file is defined or key_file is defined)) or ((username 18 | is defined or password is defined) and auth_token is defined) or ((cert_file 19 | is defined or key_file is defined) and auth_token is defined) 20 | ansible.builtin.fail: 21 | msg: Only one authentication method is allowed. Provide either username/password 22 | or cert_file/key_file or auth_token. 23 | 24 | - name: Get physical network adapter details when username and password are defined 25 | block: 26 | 27 | - ilo_network: 28 | category: Systems 29 | command: GetNetworkAdapters 30 | baseuri: '{{ baseuri }}' 31 | username: '{{ username }}' 32 | password: '{{ password }}' 33 | register: network_adapter_details 34 | 35 | - name: Physical network adapter details in the server 36 | ansible.builtin.debug: 37 | msg: '{{ network_adapter_details }}' 38 | 39 | when: username is defined and password is defined 40 | 41 | - name: Get physical network adapter details when cert_file and key_file are defined 42 | block: 43 | 44 | - ilo_network: 45 | category: Systems 46 | command: GetNetworkAdapters 47 | baseuri: '{{ baseuri }}' 48 | cert_file: '{{ cert_file }}' 49 | key_file: '{{ key_file }}' 50 | register: network_adapter_details 51 | 52 | - name: Physical network adapter details present in the server 53 | ansible.builtin.debug: 54 | msg: '{{ network_adapter_details }}' 55 | 56 | when: cert_file is defined and key_file is defined 57 | 58 | - name: Get physical network adapter details when auth_token is defined 59 | block: 60 | 61 | - ilo_network: 62 | category: Systems 63 | command: GetNetworkAdapters 64 | baseuri: '{{ baseuri }}' 65 | auth_token: '{{ auth_token }}' 66 | register: network_adapter_details 67 | 68 | - name: Physical network adapter details in the server 69 | ansible.builtin.debug: 70 | msg: '{{ network_adapter_details }}' 71 | when: auth_token is defined 72 | -------------------------------------------------------------------------------- /test/testdata/inline_replace_data/block_and_when_play_fixed.yml: -------------------------------------------------------------------------------- 1 | - hosts: myhosts 2 | connection: local 3 | gather_facts: false 4 | tasks: 5 | 6 | - name: Validate server authentication input provided by user 7 | when: 8 | - (username is not defined or password is not defined) and (cert_file is not defined 9 | or key_file is not defined) and (auth_token is not defined) 10 | ansible.builtin.fail: 11 | msg: username/password or cert_file/key_file or auth_token is mandatory 12 | 13 | - name: Fail when more than one valid authentication method is provided 14 | when: 15 | - ((username is defined or password is defined) and (cert_file is defined or key_file 16 | is defined) and auth_token is defined) or ((username is defined or password 17 | is defined) and (cert_file is defined or key_file is defined)) or ((username 18 | is defined or password is defined) and auth_token is defined) or ((cert_file 19 | is defined or key_file is defined) and auth_token is defined) 20 | ansible.builtin.fail: 21 | msg: Only one authentication method is allowed. Provide either username/password 22 | or cert_file/key_file or auth_token. 23 | 24 | - name: Get physical network adapter details when username and password are defined 25 | block: 26 | 27 | - ilo_network: 28 | category: Systems 29 | command: GetNetworkAdapters 30 | baseuri: '{{ baseuri }}' 31 | username: '{{ username }}' 32 | password: '{{ password }}' 33 | register: network_adapter_details 34 | 35 | - name: Physical network adapter details in the server 36 | ansible.builtin.debug: 37 | msg: '{{ network_adapter_details }}' 38 | 39 | when: username is defined and password is defined 40 | 41 | - name: Get physical network adapter details when cert_file and key_file are defined 42 | block: 43 | 44 | - ilo_network: 45 | category: Systems 46 | command: GetNetworkAdapters 47 | baseuri: '{{ baseuri }}' 48 | cert_file: '{{ cert_file }}' 49 | key_file: '{{ key_file }}' 50 | register: network_adapter_details 51 | 52 | - name: Physical network adapter details present in the server 53 | ansible.builtin.debug: 54 | msg: '{{ network_adapter_details }}' 55 | 56 | when: cert_file is defined and key_file is defined 57 | 58 | - name: Get physical network adapter details when auth_token is defined 59 | block: 60 | 61 | - ilo_network: 62 | category: Systems 63 | command: GetNetworkAdapters 64 | baseuri: '{{ baseuri }}' 65 | auth_token: '{{ auth_token }}' 66 | register: network_adapter_details 67 | 68 | - name: Physical network adapter details in the server 69 | ansible.builtin.debug: 70 | msg: '{{ network_adapter_details }}' 71 | when: auth_token is defined 72 | -------------------------------------------------------------------------------- /ansible_risk_insight/ansible_variables.txt: -------------------------------------------------------------------------------- 1 | ansible_check_mode 2 | ansible_config_file 3 | ansible_dependent_role_names 4 | ansible_diff_mode 5 | ansible_forks 6 | ansible_inventory_sources 7 | ansible_limit 8 | ansible_loop 9 | ansible_loop_var 10 | ansible_index_var 11 | ansible_parent_role_names 12 | ansible_parent_role_paths 13 | ansible_play_batch 14 | ansible_play_hosts 15 | ansible_play_hosts_all 16 | ansible_play_role_names 17 | ansible_playbook_python 18 | ansible_role_names 19 | ansible_role_name 20 | ansible_collection_name 21 | ansible_run_tags 22 | ansible_search_path 23 | ansible_skip_tags 24 | ansible_verbosity 25 | ansible_version 26 | group_names 27 | groups 28 | hostvars 29 | inventory_hostname 30 | inventory_hostname_short 31 | inventory_dir 32 | inventory_file 33 | omit 34 | play_hosts 35 | ansible_play_name 36 | playbook_dir 37 | role_name 38 | role_names 39 | role_path 40 | ansible_facts 41 | ansible_local 42 | ansible_become_user 43 | ansible_connection 44 | ansible_host 45 | ansible_python_interpreter 46 | ansible_user 47 | ansible_all_ipv4_addresses 48 | ansible_all_ipv6_addresses 49 | ansible_ap1 50 | ansible_apparmor 51 | ansible_architecture 52 | ansible_awdl0 53 | ansible_bridge0 54 | ansible_date_time 55 | ansible_default_ipv4 56 | ansible_default_ipv6 57 | ansible_distribution 58 | ansible_distribution_major_version 59 | ansible_distribution_release 60 | ansible_distribution_version 61 | ansible_dns 62 | ansible_domain 63 | ansible_effective_group_id 64 | ansible_effective_user_id 65 | ansible_en0 66 | ansible_en1 67 | ansible_en2 68 | ansible_en3 69 | ansible_en4 70 | ansible_en5 71 | ansible_env 72 | ansible_fibre_channel_wwn 73 | ansible_fips 74 | ansible_fqdn 75 | ansible_gif0 76 | ansible_hostname 77 | ansible_hostnqn 78 | ansible_interfaces 79 | ansible_is_chroot 80 | ansible_iscsi_iqn 81 | ansible_kernel 82 | ansible_kernel_version 83 | ansible_llw0 84 | ansible_lo0 85 | ansible_local 86 | ansible_lsb 87 | ansible_machine 88 | ansible_memfree_mb 89 | ansible_memtotal_mb 90 | ansible_model 91 | ansible_nodename 92 | ansible_os_family 93 | ansible_osrevision 94 | ansible_osversion 95 | ansible_pkg_mgr 96 | ansible_processor 97 | ansible_processor_cores 98 | ansible_processor_vcpus 99 | ansible_product_name 100 | ansible_python 101 | ansible_python_version 102 | ansible_real_group_id 103 | ansible_real_user_id 104 | ansible_selinux 105 | ansible_selinux_python_present 106 | ansible_service_mgr 107 | ansible_stf0 108 | ansible_system 109 | ansible_uptime_seconds 110 | ansible_user_dir 111 | ansible_user_gecos 112 | ansible_user_gid 113 | ansible_user_id 114 | ansible_user_shell 115 | ansible_user_uid 116 | ansible_userspace_architecture 117 | ansible_userspace_bits 118 | ansible_utun0 119 | ansible_utun1 120 | ansible_utun2 121 | ansible_virtualization_role 122 | ansible_virtualization_tech_guest 123 | ansible_virtualization_tech_host 124 | ansible_virtualization_type 125 | gather_subset 126 | module_setup 127 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/R103_download_exec.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import ( 19 | AnsibleRunContext, 20 | RunTargetType, 21 | DefaultRiskType as RiskType, 22 | AnnotationCondition, 23 | Rule, 24 | Severity, 25 | RuleTag as Tag, 26 | RuleResult, 27 | ) 28 | 29 | 30 | @dataclass 31 | class DownloadExecRule(Rule): 32 | rule_id: str = "R103" 33 | description: str = "A downloaded file from parameterized source is executed" 34 | enabled: bool = True 35 | name: str = "Download & Exec" 36 | version: str = "v0.0.1" 37 | severity: Severity = Severity.HIGH 38 | tags: tuple = (Tag.NETWORK, Tag.COMMAND) 39 | precedence: int = 11 40 | 41 | def match(self, ctx: AnsibleRunContext) -> bool: 42 | return ctx.current.type == RunTargetType.Task 43 | 44 | def process(self, ctx: AnsibleRunContext): 45 | task = ctx.current 46 | 47 | verdict = False 48 | detail = {} 49 | 50 | ac = AnnotationCondition().risk_type(RiskType.CMD_EXEC) 51 | if task.has_annotation_by_condition(ac): 52 | cmd_an = task.get_annotation_by_condition(ac) 53 | if cmd_an: 54 | detail["command"] = cmd_an.command.raw 55 | 56 | ac2 = AnnotationCondition().risk_type(RiskType.INBOUND).attr("is_mutable_src", True) 57 | inbound_tasks = ctx.before(task).search(ac2) 58 | for inbound_task in inbound_tasks: 59 | inbound_an = inbound_task.get_annotation_by_condition(ac2) 60 | if not inbound_an: 61 | continue 62 | detail["src"] = inbound_an.src.value 63 | 64 | # check if any of the exec_files are inside the download location 65 | # if so, the downloaded file is executed, so we report it 66 | download_location = inbound_an.dest 67 | executed_files = cmd_an.exec_files 68 | matched_files = [f for f in executed_files if f.is_inside(download_location)] 69 | if matched_files: 70 | detail["executed_file"] = [f.value for f in matched_files] 71 | verdict = True 72 | 73 | return RuleResult(verdict=verdict, detail=detail, file=task.file_info(), rule=self.get_metadata()) 74 | -------------------------------------------------------------------------------- /ansible_risk_insight/findings.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 os 18 | from copy import deepcopy 19 | from dataclasses import dataclass, field 20 | import jsonpickle 21 | from .utils import ( 22 | lock_file, 23 | unlock_file, 24 | remove_lock_file, 25 | ) 26 | 27 | 28 | @dataclass 29 | class Findings: 30 | metadata: dict = field(default_factory=dict) 31 | dependencies: list = field(default_factory=list) 32 | 33 | root_definitions: dict = field(default_factory=dict) 34 | ext_definitions: dict = field(default_factory=dict) 35 | extra_requirements: list = field(default_factory=list) 36 | resolve_failures: dict = field(default_factory=dict) 37 | 38 | prm: dict = field(default_factory=dict) 39 | report: dict = field(default_factory=dict) 40 | 41 | summary_txt: str = "" 42 | scan_time: str = "" 43 | 44 | def simple(self): 45 | d = self.report.copy() 46 | d["metadata"] = self.metadata 47 | d["dependencies"] = self.dependencies 48 | return d 49 | 50 | def dump(self, fpath=""): 51 | f = deepcopy(self) 52 | # omit report and summary_txt when the findings are saved 53 | # to reduce unnecessary file write 54 | f.report = {} 55 | f.summary_txt = "" 56 | json_str = jsonpickle.encode(f, make_refs=False) 57 | if fpath: 58 | lock = lock_file(fpath) 59 | try: 60 | with open(fpath, "w") as file: 61 | file.write(json_str) 62 | finally: 63 | unlock_file(lock) 64 | remove_lock_file(lock) 65 | return json_str 66 | 67 | def save_rule_result(self, fpath=""): 68 | json_str = jsonpickle.encode(self.report.get("ari_result", {}), make_refs=False, unpicklable=False) 69 | if fpath: 70 | rule_result_dir = os.path.dirname(fpath) 71 | if not os.path.exists(rule_result_dir): 72 | os.makedirs(rule_result_dir, exist_ok=True) 73 | with open(fpath, "w") as file: 74 | file.write(json_str) 75 | return json_str 76 | 77 | @staticmethod 78 | def load(fpath="", json_str=""): 79 | if fpath: 80 | with open(fpath, "r") as file: 81 | json_str = file.read() 82 | findings = jsonpickle.decode(json_str) 83 | return findings 84 | -------------------------------------------------------------------------------- /ansible_risk_insight/annotators/risk_annotator_base.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | from ansible_risk_insight.models import TaskCall, RiskAnnotation 19 | from ansible_risk_insight.utils import load_classes_in_dir 20 | from ansible_risk_insight.annotators.annotator_base import Annotator, AnnotatorResult 21 | from ansible_risk_insight.annotators.module_annotator_base import ModuleAnnotator, ModuleAnnotatorResult 22 | 23 | 24 | class RiskAnnotator(Annotator): 25 | type: str = RiskAnnotation.type 26 | name: str = "" 27 | enabled: bool = False 28 | 29 | module_annotator_cache: dict = {} 30 | 31 | def match(self, task: TaskCall) -> bool: 32 | raise ValueError("this is a base class method") 33 | 34 | def run(self, task: TaskCall): 35 | raise ValueError("this is a base class method") 36 | 37 | def load_module_annotators(self, dir_path: str): 38 | if dir_path in self.module_annotator_cache: 39 | return self.module_annotator_cache[dir_path] 40 | 41 | annotator_classes, _ = load_classes_in_dir(dir_path, ModuleAnnotator, __file__) 42 | module_annotators = [] 43 | for a_c in annotator_classes: 44 | annotator = a_c(context=self.context) 45 | module_annotators.append(annotator) 46 | if module_annotators: 47 | self.module_annotator_cache[dir_path] = module_annotators 48 | return module_annotators 49 | 50 | def run_module_annotators(self, dir_path: str, task: TaskCall) -> ModuleAnnotatorResult: 51 | if not dir_path: 52 | return [] 53 | 54 | resolved_name = task.spec.resolved_name 55 | module_annotators = self.load_module_annotators(dir_path) 56 | 57 | # TODO: need to consider annotator precedence 58 | 59 | annotations = [] 60 | 61 | for annotator in module_annotators: 62 | if not isinstance(annotator, ModuleAnnotator): 63 | continue 64 | if not annotator.fqcn: 65 | continue 66 | if resolved_name != annotator.fqcn: 67 | continue 68 | 69 | result = annotator.run(task) 70 | if not result: 71 | continue 72 | 73 | if result.annotations: 74 | annotations.extend(result.annotations) 75 | if annotations: 76 | return ModuleAnnotatorResult(annotations=annotations) 77 | return None 78 | 79 | 80 | @dataclass 81 | class RiskAnnotatorResult(AnnotatorResult): 82 | pass 83 | -------------------------------------------------------------------------------- /ansible_risk_insight/cli/ram/generate.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | 19 | from ...ram_generator import RiskAssessmentModelGenerator as RAMGenerator 20 | 21 | 22 | class RAMGenerateCLI: 23 | args = None 24 | 25 | def __init__(self): 26 | parser = argparse.ArgumentParser(description="TODO") 27 | parser.add_argument("target_type", help="content type", choices={"ram"}) 28 | parser.add_argument("action", help="action for RAM command or target_name of search action") 29 | parser.add_argument("-f", "--file", help='target list like "collection community.general"') 30 | parser.add_argument("-r", "--resume", help="line number to resume scanning") 31 | parser.add_argument("--serial", action="store_true", help="if True, do not parallelize ram generation") 32 | parser.add_argument("--no-module-spec", action="store_true", help="if True, ansible-doc is not used") 33 | parser.add_argument("--download-only", action="store_true", help="if True, just download the content") 34 | parser.add_argument("--include-tests", action="store_true", help='if true, load test contents in "tests/integration/targets"') 35 | parser.add_argument("--no-retry", action="store_true", help="if True, not retry failed items.") 36 | parser.add_argument("-o", "--out-dir", help="output directory for the rule evaluation result") 37 | args = parser.parse_args() 38 | self.args = args 39 | 40 | def run(self): 41 | args = self.args 42 | action = args.action 43 | if action != "generate": 44 | raise ValueError('RAMGenerateCLI cannot be executed without "generate" action') 45 | 46 | target_list = [] 47 | with open(args.file, "r") as file: 48 | for line in file: 49 | parts = line.replace("\n", "").split(" ") 50 | if len(parts) != 2: 51 | raise ValueError('target list file must be lines of " " such as "collection community.general"') 52 | target_list.append((parts[0], parts[1])) 53 | 54 | resume = -1 55 | if args.resume: 56 | resume = int(args.resume) 57 | 58 | parallel = True 59 | if args.serial: 60 | parallel = False 61 | 62 | ram_generator = RAMGenerator( 63 | target_list=target_list, 64 | resume=resume, 65 | parallel=parallel, 66 | download_only=args.download_only, 67 | include_test_contents=args.include_tests, 68 | out_dir=args.out_dir, 69 | no_module_spec=args.no_module_spec, 70 | no_retry=args.no_retry, 71 | ) 72 | ram_generator.run() 73 | -------------------------------------------------------------------------------- /ansible_risk_insight/awx_utils.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 codecs 18 | import os 19 | import re 20 | 21 | 22 | valid_playbook_re = re.compile(r"^\s*?-?\s*?(?:hosts|include|import_playbook):\s*?.*?$") 23 | 24 | 25 | # this method is based on awx code 26 | # awx/main/utils/ansible.py#L42-L64 in ansible/awx 27 | def could_be_playbook(fpath): 28 | basename, ext = os.path.splitext(fpath) 29 | if ext not in [".yml", ".yaml"]: 30 | return False 31 | # Filter files that do not have either hosts or top-level 32 | # includes. Use regex to allow files with invalid YAML to 33 | # show up. 34 | matched = False 35 | try: 36 | with codecs.open(fpath, "r", encoding="utf-8", errors="ignore") as f: 37 | for n, line in enumerate(f): 38 | if valid_playbook_re.match(line): 39 | matched = True 40 | break 41 | # Any YAML file can also be encrypted with vault; 42 | # allow these to be used as the main playbook. 43 | elif n == 0 and line.startswith("$ANSIBLE_VAULT;"): 44 | matched = True 45 | break 46 | except IOError: 47 | return False 48 | return matched 49 | 50 | 51 | # this method is based on awx code 52 | # awx/main/models/projects.py#L206-L217 in ansible/awx 53 | def search_playbooks(root_path): 54 | results = [] 55 | if root_path and os.path.exists(root_path): 56 | for dirpath, dirnames, filenames in os.walk(root_path, followlinks=False): 57 | if skip_directory(dirpath): 58 | continue 59 | for filename in filenames: 60 | fpath = os.path.join(dirpath, filename) 61 | if could_be_playbook(fpath): 62 | results.append(fpath) 63 | return sorted(results, key=lambda x: x.lower()) 64 | 65 | 66 | # this method is based on awx code 67 | # awx/main/utils/ansible.py#L24-L39 in ansible/awx 68 | def skip_directory(relative_directory_path): 69 | path_elements = relative_directory_path.split(os.sep) 70 | # Exclude files in a roles subdirectory. 71 | if "roles" in path_elements: 72 | return True 73 | # Filter files in a tasks subdirectory. 74 | if "tasks" in path_elements: 75 | return True 76 | # Filter files in a molecule subdirectory. 77 | if "molecule" in path_elements: 78 | return True 79 | # Filter files in a tests/integration subdirectory. 80 | if "tests" in path_elements and "integration" in path_elements: 81 | return True 82 | for element in path_elements: 83 | # Do not include dot files or dirs 84 | if element.startswith("."): 85 | return True 86 | # Exclude anything inside of group or host vars directories 87 | if "group_vars" in path_elements or "host_vars" in path_elements: 88 | return True 89 | return False 90 | -------------------------------------------------------------------------------- /ansible_risk_insight/rules/P004_variable_validation.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 | from dataclasses import dataclass 18 | 19 | from ansible_risk_insight.models import ( 20 | AnsibleRunContext, 21 | RunTargetType, 22 | Rule, 23 | Severity, 24 | RuleTag as Tag, 25 | VariableType, 26 | ArgumentsType, 27 | ) 28 | 29 | 30 | def is_loop_var(value, task): 31 | # `item` or alternative loop variable (if any) should not be replaced to avoid breaking loop 32 | skip_variables = [] 33 | if task.spec.loop and isinstance(task.spec.loop, dict): 34 | skip_variables.extend(list(task.spec.loop.keys())) 35 | 36 | _v = value.replace(" ", "") 37 | 38 | for var in skip_variables: 39 | for _prefix in ["}}", "|", "."]: 40 | pattern = "{{" + var + _prefix 41 | if pattern in _v: 42 | return True 43 | return False 44 | 45 | 46 | @dataclass 47 | class VariableValidationRule(Rule): 48 | rule_id: str = "P004" 49 | description: str = "Validate variables and set annotations" 50 | enabled: bool = True 51 | name: str = "VariableValidation" 52 | version: str = "v0.0.1" 53 | severity: Severity = Severity.NONE 54 | tags: tuple = Tag.QUALITY 55 | precedence: int = 0 56 | 57 | def match(self, ctx: AnsibleRunContext) -> bool: 58 | return ctx.current.type == RunTargetType.Task 59 | 60 | def process(self, ctx: AnsibleRunContext): 61 | task = ctx.current 62 | 63 | undefined_variables = [] 64 | unknown_name_vars = [] 65 | unnecessary_loop = [] 66 | task_arg_keys = [] 67 | if task.args.type == ArgumentsType.DICT: 68 | task_arg_keys = list(task.args.raw.keys()) 69 | 70 | registered_vars = [] 71 | for v_name in task.variable_set: 72 | v = task.variable_set[v_name] 73 | if v and v[-1].type == VariableType.RegisteredVars: 74 | registered_vars.append(v_name) 75 | 76 | for v_name in task.variable_use: 77 | first_v_name = v_name.split(".")[0] 78 | # skip registered vars 79 | if first_v_name in registered_vars: 80 | continue 81 | 82 | v = task.variable_use[v_name] 83 | if v and v[-1].type == VariableType.Unknown: 84 | if v_name not in undefined_variables: 85 | undefined_variables.append(v_name) 86 | if v_name not in unknown_name_vars and v_name not in task_arg_keys: 87 | unknown_name_vars.append(v_name) 88 | if v_name not in unnecessary_loop: 89 | v_str = "{{ " + v_name + " }}" 90 | if not is_loop_var(v_str, task): 91 | unnecessary_loop.append({"name": v_name, "suggested": v_name.replace("item.", "")}) 92 | 93 | task.set_annotation("variable.undefined_vars", undefined_variables, rule_id=self.rule_id) 94 | task.set_annotation("variable.unknown_name_vars", unknown_name_vars, rule_id=self.rule_id) 95 | task.set_annotation("variable.unnecessary_loop_vars", unnecessary_loop, rule_id=self.rule_id) 96 | 97 | return None 98 | -------------------------------------------------------------------------------- /test/test_inline_replace.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2024 RedHat. All rights reserved. 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 | # https://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 | from ansible_risk_insight.finder import update_the_yaml_target 18 | 19 | 20 | def test_inline_replace_for_block_and_when(): 21 | file_path = "test/testdata/inline_replace_data/block_and_when_play.yml" 22 | file_path_out = "test/testdata/inline_replace_data/block_and_when_play_fixed.yml" 23 | line_number = [ 24 | "L6-11", 25 | "L12-20", 26 | "L23-30", 27 | "L31-34", 28 | "L39-46", 29 | "L47-50", 30 | "L55-61", 31 | "L62-65" 32 | ] 33 | new_content = [ 34 | '''- name: Validate server authentication input provided by user\n when:\n 35 | - (username is not defined or password is not defined) and (cert_file is not defined or key_file is not defined) 36 | and (auth_token is not defined)\n ansible.builtin.fail:\n msg: "username/password or cert_file/key_file or auth_token 37 | is mandatory"\n''', 38 | '''- name: Fail when more than one valid authentication method is provided\n when:\n 39 | - ((username is defined or password is defined) and (cert_file is defined or key_file is defined) and 40 | auth_token is defined) or ((username is defined or password is defined) and (cert_file is defined or key_file is defined)) 41 | or ((username is defined or password is defined) and auth_token is defined) or ((cert_file is defined or key_file is defined) and 42 | auth_token is defined)\n ansible.builtin.fail:\n msg: "Only one authentication method is allowed. 43 | Provide either username/password or cert_file/key_file or auth_token."\n''', 44 | ''' - ilo_network:\n category: Systems\n command: GetNetworkAdapters\n 45 | baseuri: "{{ baseuri }}"\n username: "{{ username }}"\n password: "{{ password }}"\n 46 | register: network_adapter_details\n''', 47 | '- name: Physical network adapter details in the server\n ansible.builtin.debug:\n msg: "{{ network_adapter_details }}"\n', 48 | ''' - ilo_network:\n category: Systems\n command: GetNetworkAdapters\n 49 | baseuri: "{{ baseuri }}"\n cert_file: "{{ cert_file }}"\n key_file: "{{ key_file }}"\n 50 | register: network_adapter_details\n''', 51 | '- name: Physical network adapter details present in the server\n ansible.builtin.debug:\n msg: "{{ network_adapter_details }}"\n', 52 | ''' - ilo_network:\n category: Systems\n command: GetNetworkAdapters\n 53 | baseuri: "{{ baseuri }}"\n auth_token: "{{ auth_token }}"\n register: network_adapter_details\n''', 54 | '- name: Physical network adapter details in the server\n ansible.builtin.debug:\n msg: "{{ network_adapter_details }}"\n' 55 | ] 56 | 57 | update_the_yaml_target(file_path, line_number, new_content) 58 | with open(file_path, 'r') as file: 59 | data = file.read() 60 | with open(file_path_out, 'r') as file: 61 | data_fixed = file.read() 62 | 63 | assert data == data_fixed 64 | -------------------------------------------------------------------------------- /test/test_scanner.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 pytest 18 | 19 | from ansible_risk_insight.scanner import ARIScanner, config 20 | from ansible_risk_insight.rules.R103_download_exec import DownloadExecRule 21 | 22 | 23 | @pytest.mark.parametrize("type, name", [("project", "test/testdata/projects/my.collection")]) 24 | def test_scanner_with_project(type, name): 25 | ari_result, _ = _scan(type, name) 26 | assert ari_result 27 | role_result = ari_result.role(name="my.collection.sample-role-1") 28 | assert role_result 29 | task_result = role_result.task(name="Gcloud | Archive | Install into Path") 30 | assert task_result 31 | result = task_result.find_result(rule_id=DownloadExecRule.rule_id) 32 | assert result 33 | assert result.verdict 34 | assert result.detail["executed_file"] 35 | assert result.detail["executed_file"][0] == "{{ gcloud_archive_path }}/install.sh" 36 | 37 | 38 | @pytest.mark.parametrize("type, name", [("collection", "community.mongodb")]) 39 | def test_scanner_with_collection(type, name): 40 | _, scandata = _scan(type, name) 41 | dep_names = [dep.get("name", "") for dep in scandata.findings.dependencies] 42 | assert len(dep_names) == 2 43 | assert "community.general" in dep_names 44 | assert "ansible.posix" in dep_names 45 | 46 | 47 | @pytest.mark.parametrize("type, name", [("role", "test/testdata/roles/test_role")]) 48 | def test_scanner_with_role(type, name): 49 | ari_result, _ = _scan(type, name) 50 | assert ari_result 51 | role_result = ari_result.role(name="test_role") 52 | assert role_result 53 | task_result = role_result.task(name="execute the downloaded file") 54 | assert task_result 55 | result = task_result.find_result(rule_id=DownloadExecRule.rule_id) 56 | assert result 57 | assert result.verdict 58 | assert result.detail["executed_file"] 59 | assert result.detail["executed_file"][0] == "/etc/install.sh" 60 | 61 | 62 | @pytest.mark.parametrize( 63 | "type, name, expected_line_numbers", 64 | [ 65 | ("playbook", "test/testdata/files/test_line_number.yml", [[6, 13], [14, 18], [20, 23], [29, 33]]), 66 | ("playbook", "test/testdata/files/test_line_number2.yml", [[12, 15], [16, 17]]), 67 | ], 68 | ) 69 | def test_scanner_line_number_detection(type, name, expected_line_numbers): 70 | ari_result, _ = _scan(type=type, name=name, playbook_only=True) 71 | assert ari_result 72 | playbook_result = ari_result.playbook(path=name) 73 | assert playbook_result 74 | task_results = playbook_result.tasks() 75 | for i, task_result in enumerate(task_results.nodes): 76 | assert task_result.node.spec.line_num_in_file 77 | detected = task_result.node.spec.line_num_in_file 78 | assert len(detected) == 2 79 | expected = expected_line_numbers[i] 80 | assert detected == expected 81 | 82 | 83 | def _scan(type, name, **kwargs): 84 | if not kwargs: 85 | kwargs = {} 86 | kwargs["type"] = type 87 | kwargs["name"] = name 88 | 89 | s = ARIScanner( 90 | root_dir=config.data_dir, 91 | use_ansible_doc=False, 92 | read_ram=False, 93 | write_ram=False, 94 | ) 95 | ari_result = s.evaluate(**kwargs) 96 | scandata = s.get_last_scandata() 97 | return ari_result, scandata 98 | -------------------------------------------------------------------------------- /ansible_risk_insight/analyzer.py: -------------------------------------------------------------------------------- 1 | # -*- mode:python; coding:utf-8 -*- 2 | 3 | # Copyright (c) 2022 IBM Corp. All rights reserved. 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 | # https://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 json 19 | from typing import List 20 | from ansible_risk_insight.annotators.risk_annotator_base import RiskAnnotator 21 | import ansible_risk_insight.logger as logger 22 | from .models import TaskCallsInTree, AnsibleRunContext 23 | from .utils import load_classes_in_dir 24 | 25 | 26 | annotator_cache = [] 27 | 28 | 29 | def load_annotators(ctx: AnsibleRunContext = None): 30 | global annotator_cache 31 | 32 | if annotator_cache: 33 | return annotator_cache 34 | 35 | _annotator_classes, _ = load_classes_in_dir("annotators", RiskAnnotator, __file__) 36 | _annotators = [] 37 | for a in _annotator_classes: 38 | try: 39 | _annotator = a(context=ctx) 40 | _annotators.append(_annotator) 41 | except Exception: 42 | raise ValueError(f"failed to load an annotator: {a}") 43 | annotator_cache = _annotators 44 | return _annotators 45 | 46 | 47 | def load_taskcalls_in_trees(path: str) -> List[TaskCallsInTree]: 48 | taskcalls_in_trees = [] 49 | try: 50 | with open(path, "r") as file: 51 | for line in file: 52 | taskcalls_in_tree = TaskCallsInTree.from_json(line) 53 | taskcalls_in_trees.append(taskcalls_in_tree) 54 | except Exception as e: 55 | raise ValueError("failed to load the json file {} {}".format(path, e)) 56 | return taskcalls_in_trees 57 | 58 | 59 | def analyze(contexts: List[AnsibleRunContext]): 60 | num = len(contexts) 61 | for i, ctx in enumerate(contexts): 62 | if not isinstance(ctx, AnsibleRunContext): 63 | continue 64 | for j, t in enumerate(ctx.tasks): 65 | annotator = None 66 | _annotators = load_annotators(ctx) 67 | for ax in _annotators: 68 | if not ax.enabled: 69 | continue 70 | if ax.match(task=t): 71 | annotator = ax 72 | break 73 | if annotator is None: 74 | continue 75 | result = annotator.run(task=t) 76 | if not result: 77 | continue 78 | if result.annotations: 79 | t.annotations.extend(result.annotations) 80 | logger.debug("analyze() {}/{} done".format(i + 1, num)) 81 | return contexts 82 | 83 | 84 | def main(): 85 | parser = argparse.ArgumentParser( 86 | prog="analyze.py", 87 | description="analyze tasks", 88 | epilog="end", 89 | add_help=True, 90 | ) 91 | 92 | parser.add_argument( 93 | "-i", 94 | "--input", 95 | default="", 96 | help="path to the input json (taskcalls_in_trees.json)", 97 | ) 98 | parser.add_argument("-o", "--output", default="", help="path to the output json") 99 | 100 | args = parser.parse_args() 101 | 102 | taskcalls_in_trees = load_taskcalls_in_trees(args.input) 103 | taskcalls_in_trees = analyze(taskcalls_in_trees) 104 | 105 | if args.output != "": 106 | lines = [json.dumps(single_tree_data) for single_tree_data in taskcalls_in_trees] 107 | with open(args.output, mode="wt") as file: 108 | file.write("\n".join(lines)) 109 | 110 | 111 | if __name__ == "__main__": 112 | main() 113 | --------------------------------------------------------------------------------