summaryrefslogtreecommitdiffstats
path: root/src/ansiblelint/rules/no_log_password.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/ansiblelint/rules/no_log_password.py')
-rw-r--r--src/ansiblelint/rules/no_log_password.py306
1 files changed, 306 insertions, 0 deletions
diff --git a/src/ansiblelint/rules/no_log_password.py b/src/ansiblelint/rules/no_log_password.py
new file mode 100644
index 0000000..7cc7439
--- /dev/null
+++ b/src/ansiblelint/rules/no_log_password.py
@@ -0,0 +1,306 @@
+# Copyright 2018, Rackspace US, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""NoLogPasswordsRule used with ansible-lint."""
+from __future__ import annotations
+
+import sys
+from typing import TYPE_CHECKING
+
+from ansiblelint.rules import AnsibleLintRule
+from ansiblelint.utils import Task, convert_to_boolean
+
+if TYPE_CHECKING:
+ from ansiblelint.file_utils import Lintable
+
+
+class NoLogPasswordsRule(AnsibleLintRule):
+ """Password should not be logged."""
+
+ id = "no-log-password"
+ description = (
+ "When passing password argument you should have no_log configured "
+ "to a non False value to avoid accidental leaking of secrets."
+ )
+ severity = "LOW"
+ tags = ["opt-in", "security", "experimental"]
+ version_added = "v5.0.9"
+
+ def matchtask(
+ self,
+ task: Task,
+ file: Lintable | None = None,
+ ) -> bool | str:
+ if task["action"]["__ansible_module_original__"] == "ansible.builtin.user" and (
+ task["action"].get("password_lock") and not task["action"].get("password")
+ ):
+ has_password = False
+ else:
+ for param in task["action"]:
+ if "password" in param:
+ has_password = True
+ break
+ else:
+ has_password = False
+
+ has_loop = [key for key in task if key.startswith("with_") or key == "loop"]
+ # No no_log and no_log: False behave the same way
+ # and should return a failure (return True), so we
+ # need to invert the boolean
+ no_log = task.get("no_log", False)
+
+ if (
+ isinstance(no_log, str)
+ and no_log.startswith("{{")
+ and no_log.endswith("}}")
+ ):
+ # we cannot really evaluate jinja expressions
+ return False
+
+ return bool(
+ has_password and not convert_to_boolean(no_log) and len(has_loop) > 0,
+ )
+
+
+if "pytest" in sys.modules:
+ import pytest
+
+ if TYPE_CHECKING:
+ from ansiblelint.testing import RunFromText # pylint: disable=ungrouped-imports
+
+ NO_LOG_UNUSED = """
+- name: Test
+ hosts: all
+ tasks:
+ - name: Succeed when no_log is not used but no loop present
+ ansible.builtin.user:
+ name: john_doe
+ password: "wow"
+ state: absent
+"""
+
+ NO_LOG_FALSE = """
+- hosts: all
+ tasks:
+ - name: Use of jinja for no_log is valid
+ user:
+ name: john_doe
+ user_password: "{{ item }}"
+ state: absent
+ no_log: "{{ False }}"
+ - name: Fail when no_log is set to False
+ user:
+ name: john_doe
+ user_password: "{{ item }}"
+ state: absent
+ with_items:
+ - wow
+ - now
+ no_log: False
+ - name: Fail when no_log is set to False
+ ansible.builtin.user:
+ name: john_doe
+ user_password: "{{ item }}"
+ state: absent
+ with_items:
+ - wow
+ - now
+ no_log: False
+"""
+
+ NO_LOG_NO = """
+- hosts: all
+ tasks:
+ - name: Fail when no_log is set to no
+ user:
+ name: john_doe
+ password: "{{ item }}"
+ state: absent
+ no_log: no
+ loop:
+ - wow
+ - now
+"""
+
+ PASSWORD_WITH_LOCK = """
+- hosts: all
+ tasks:
+ - name: Fail when password is set and password_lock is true
+ user:
+ name: "{{ item }}"
+ password: "wow"
+ password_lock: true
+ with_random_choice:
+ - ansible
+ - lint
+"""
+
+ NO_LOG_YES = """
+- hosts: all
+ tasks:
+ - name: Succeed when no_log is set to yes
+ with_list:
+ - name: user
+ password: wow
+ - password: now
+ name: ansible
+ user:
+ name: "{{ item.name }}"
+ password: "{{ item.password }}"
+ state: absent
+ no_log: yes
+"""
+
+ NO_LOG_TRUE = """
+- hosts: all
+ tasks:
+ - name: Succeed when no_log is set to True
+ user:
+ name: john_doe
+ user_password: "{{ item }}"
+ state: absent
+ no_log: True
+ loop:
+ - wow
+ - now
+"""
+
+ PASSWORD_LOCK_YES = """
+- hosts: all
+ tasks:
+ - name: Succeed when only password locking account
+ user:
+ name: "{{ item }}"
+ password_lock: yes
+ # user_password: "this is a comment, not a password"
+ with_list:
+ - ansible
+ - lint
+"""
+
+ PASSWORD_LOCK_YES_BUT_NO_PASSWORD = """
+- hosts: all
+ tasks:
+ - name: Succeed when only password locking account
+ ansible.builtin.user:
+ name: "{{ item }}"
+ password_lock: yes
+ # user_password: "this is a comment, not a password"
+ with_list:
+ - ansible
+ - lint
+"""
+
+ PASSWORD_LOCK_FALSE = """
+- hosts: all
+ tasks:
+ - name: Succeed when password_lock is false and password is not used
+ user:
+ name: lint
+ password_lock: False
+"""
+
+ @pytest.mark.parametrize(
+ "rule_runner",
+ (NoLogPasswordsRule,),
+ indirect=["rule_runner"],
+ )
+ def test_no_log_unused(rule_runner: RunFromText) -> None:
+ """The task does not use no_log but also no loop."""
+ results = rule_runner.run_playbook(NO_LOG_UNUSED)
+ assert len(results) == 0
+
+ @pytest.mark.parametrize(
+ "rule_runner",
+ (NoLogPasswordsRule,),
+ indirect=["rule_runner"],
+ )
+ def test_no_log_false(rule_runner: RunFromText) -> None:
+ """The task sets no_log to false."""
+ results = rule_runner.run_playbook(NO_LOG_FALSE)
+ assert len(results) == 2
+ for result in results:
+ assert result.rule.id == "no-log-password"
+
+ @pytest.mark.parametrize(
+ "rule_runner",
+ (NoLogPasswordsRule,),
+ indirect=["rule_runner"],
+ )
+ def test_no_log_no(rule_runner: RunFromText) -> None:
+ """The task sets no_log to no."""
+ results = rule_runner.run_playbook(NO_LOG_NO)
+ assert len(results) == 1
+ assert results[0].rule.id == "no-log-password"
+
+ @pytest.mark.parametrize(
+ "rule_runner",
+ (NoLogPasswordsRule,),
+ indirect=["rule_runner"],
+ )
+ def test_password_with_lock(rule_runner: RunFromText) -> None:
+ """The task sets a password but also lock the user."""
+ results = rule_runner.run_playbook(PASSWORD_WITH_LOCK)
+ assert len(results) == 1
+ assert results[0].rule.id == "no-log-password"
+
+ @pytest.mark.parametrize(
+ "rule_runner",
+ (NoLogPasswordsRule,),
+ indirect=["rule_runner"],
+ )
+ def test_no_log_yes(rule_runner: RunFromText) -> None:
+ """The task sets no_log to yes."""
+ results = rule_runner.run_playbook(NO_LOG_YES)
+ assert len(results) == 0
+
+ @pytest.mark.parametrize(
+ "rule_runner",
+ (NoLogPasswordsRule,),
+ indirect=["rule_runner"],
+ )
+ def test_no_log_true(rule_runner: RunFromText) -> None:
+ """The task sets no_log to true."""
+ results = rule_runner.run_playbook(NO_LOG_TRUE)
+ assert len(results) == 0
+
+ @pytest.mark.parametrize(
+ "rule_runner",
+ (NoLogPasswordsRule,),
+ indirect=["rule_runner"],
+ )
+ def test_no_log_password_lock_yes(rule_runner: RunFromText) -> None:
+ """The task only locks the user."""
+ results = rule_runner.run_playbook(PASSWORD_LOCK_YES)
+ assert len(results) == 0
+
+ @pytest.mark.parametrize(
+ "rule_runner",
+ (NoLogPasswordsRule,),
+ indirect=["rule_runner"],
+ )
+ def test_no_log_password_lock_yes_but_no_password(rule_runner: RunFromText) -> None:
+ """The task only locks the user."""
+ results = rule_runner.run_playbook(PASSWORD_LOCK_YES_BUT_NO_PASSWORD)
+ assert len(results) == 0
+
+ @pytest.mark.parametrize(
+ "rule_runner",
+ (NoLogPasswordsRule,),
+ indirect=["rule_runner"],
+ )
+ def test_password_lock_false(rule_runner: RunFromText) -> None:
+ """The task does not actually lock the user."""
+ results = rule_runner.run_playbook(PASSWORD_LOCK_FALSE)
+ assert len(results) == 0