Repository: ansible/ansible-lint

Issue:
Error with Vault single-variable encryption
# Issue Type
- Bug report

# Ansible and Ansible Lint details

```
ansible-ansible 2.3.0.0
config file = /home/jldefer/ansible/ansible.cfg
configured module search path = [u'/usr/share/ansible']
python version = 2.7.13 (default, Jan 12 2017, 17:59:37) [GCC 6.3.1 20161221 (Red Hat 6.3.1-1)]

ansible-lint 3.4.12
```

- ansible installation method: pip
- ansible-lint installation method: pip

# Desired Behaviour

No error

# Actual Behaviour (Bug report only)

When run against new vault string encryption (https://docs.ansible.com/ansible/playbooks_vault.html#single-encrypted-variable)  files it fails with a YAML syntax error:
```
$ ansible-lint test.yml 
Syntax Error while loading YAML.


The error appears to have been in '/home/user/test.yml': line 2, column 15, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:

---
vault_string: !vault |
              ^ here
```
test.yml:
```---
vault_string: !vault |
              $ANSIBLE_VAULT;1.1;AES256
              646637383932633933266643636373936936373964366536636639643136646530666531663365
              64653765356234396336133343937386276664353861640a303730366362366266303864346235
plain_string: test
```


Discussion:
I'll need to look into this works in Ansible. If anyone already knows and wants to fix this, just let me know.
@willthames I added a fix upon your `minimal-vault` branch in order to use it here: https://github.com/kouk/ansible-lint/tree/minimal-vault
feel free to use it.
if I find time I would also like to work on #115. Using ansible 2.3 here.  

Resolving patch:
diff --git a/lib/ansiblelint/rules/OctalPermissionsRule.py b/lib/ansiblelint/rules/OctalPermissionsRule.py
--- a/lib/ansiblelint/rules/OctalPermissionsRule.py
+++ b/lib/ansiblelint/rules/OctalPermissionsRule.py
@@ -37,25 +37,36 @@ class OctalPermissionsRule(AnsibleLintRule):
     mode_regex = re.compile(r'^\s*[0-9]+\s*$')
     valid_mode_regex = re.compile(r'^\s*0[0-7]{3,4}\s*$')
 
+    def is_invalid_permission(self, mode):
+        # sensible file permission modes don't
+        # have write bit set when read bit is
+        # not set and don't have execute bit set
+        # when user execute bit is not set.
+        # also, user permissions are more generous than
+        # group permissions and user and group permissions
+        # are more generous than world permissions
+
+        other_write_without_read = (mode % 8 and mode % 8 < 4 and
+                                    not (mode % 8 == 1 and (mode >> 6) % 2 == 1))
+        group_write_without_read = ((mode >> 3) % 8 and (mode >> 3) % 8 < 4 and
+                                    not ((mode >> 3) % 8 == 1 and (mode >> 6) % 2 == 1))
+        user_write_without_read = ((mode >> 6) % 8 and (mode >> 6) % 8 < 4 and
+                                   not (mode >> 6) % 8 == 1)
+        other_more_generous_than_group = mode % 8 > (mode >> 3) % 8
+        other_more_generous_than_user = mode % 8 > (mode >> 6) % 8
+        group_more_generous_than_user = (mode >> 3) % 8 > (mode >> 6) % 8
+
+        return (other_write_without_read or
+                group_write_without_read or
+                user_write_without_read or
+                other_more_generous_than_group or
+                other_more_generous_than_user or
+                group_more_generous_than_user)
+
     def matchtask(self, file, task):
         if task["action"]["__ansible_module__"] in self._modules:
             mode = task['action'].get('mode', None)
             if isinstance(mode, six.string_types) and self.mode_regex.match(mode):
                 return not self.valid_mode_regex.match(mode)
             if isinstance(mode, int):
-                # sensible file permission modes don't
-                # have write bit set when read bit is
-                # not set and don't have execute bit set
-                # when user execute bit is not set.
-                # also, user permissions are more generous than
-                # group permissions and user and group permissions
-                # are more generous than world permissions
-
-                result = (mode % 8 and mode % 8 < 4 and not (mode % 8 == 1 and (mode >> 6) % 2 == 1) or
-                          (mode >> 3) % 8 and (mode >> 3) % 8 < 4 and not ((mode >> 3) % 8 == 1 and (mode >> 6) % 2 == 1) or
-                          (mode >> 6) % 8 and (mode >> 6) % 8 < 4 and not (mode >> 6) % 8 == 1 or
-                          mode & 8 < (mode << 3) & 8 or
-                          mode & 8 < (mode << 6) & 8 or
-                          (mode << 3) & 8 < (mode << 6) & 8)
-
-                return result
+                return self.is_invalid_permission(mode)
diff --git a/lib/ansiblelint/utils.py b/lib/ansiblelint/utils.py
--- a/lib/ansiblelint/utils.py
+++ b/lib/ansiblelint/utils.py
@@ -49,11 +49,20 @@
     from ansible.parsing.dataloader import DataLoader
     from ansible.template import Templar
     from ansible.parsing.mod_args import ModuleArgsParser
+    from ansible.parsing.yaml.constructor import AnsibleConstructor
+    from ansible.parsing.yaml.loader import AnsibleLoader
     from ansible.errors import AnsibleParserError
     ANSIBLE_VERSION = 2
 
+    # ansible-lint doesn't need/want to know about encrypted secrets, but it needs
+    # Ansible 2.3+ allows encrypted secrets within yaml files, so we pass a string
+    # as the password to enable such yaml files to be opened and parsed successfully.
+    DEFAULT_VAULT_PASSWORD = 'x'
+
     def parse_yaml_from_file(filepath):
         dl = DataLoader()
+        if hasattr(dl, 'set_vault_password'):
+            dl.set_vault_password(DEFAULT_VAULT_PASSWORD)
         return dl.load_from_file(filepath)
 
     def path_dwim(basedir, given):
@@ -66,6 +75,7 @@ def ansible_template(basedir, varname, templatevars, **kwargs):
         dl.set_basedir(basedir)
         templar = Templar(dl, variables=templatevars)
         return templar.template(varname, **kwargs)
+
     try:
         from ansible.plugins import module_loader
     except ImportError:
@@ -529,13 +539,26 @@ def compose_node(parent, index):
         return node
 
     def construct_mapping(node, deep=False):
-        mapping = Constructor.construct_mapping(loader, node, deep=deep)
-        mapping[LINE_NUMBER_KEY] = node.__line__
+        if ANSIBLE_VERSION < 2:
+            mapping = Constructor.construct_mapping(loader, node, deep=deep)
+        else:
+            mapping = AnsibleConstructor.construct_mapping(loader, node, deep=deep)
+        if hasattr(node, '__line__'):
+            mapping[LINE_NUMBER_KEY] = node.__line__
+        else:
+            mapping[LINE_NUMBER_KEY] = mapping._line_number
         mapping[FILENAME_KEY] = filename
         return mapping
 
     try:
-        loader = yaml.Loader(data)
+        if ANSIBLE_VERSION < 2:
+            loader = yaml.Loader(data)
+        else:
+            import inspect
+            kwargs = {}
+            if 'vault_password' in inspect.getargspec(AnsibleLoader.__init__).args:
+                kwargs['vault_password'] = DEFAULT_VAULT_PASSWORD
+            loader = AnsibleLoader(data, **kwargs)
         loader.compose_node = compose_node
         loader.construct_mapping = construct_mapping
         data = loader.get_single_data()
diff --git a/lib/ansiblelint/version.py b/lib/ansiblelint/version.py
--- a/lib/ansiblelint/version.py
+++ b/lib/ansiblelint/version.py
@@ -1 +1 @@
-__version__ = '3.4.21'
+__version__ = '3.5.0-rc1'


Test patch:
diff --git a/test/TestOctalPermissions.py b/test/TestOctalPermissions.py
--- a/test/TestOctalPermissions.py
+++ b/test/TestOctalPermissions.py
@@ -4,10 +4,21 @@
 
 
 class TestOctalPermissionsRuleWithFile(unittest.TestCase):
+
     collection = RulesCollection()
+    VALID_MODES = [0o777, 0o775, 0o770, 0o755, 0o750, 0o711, 0o710, 0o700,
+                   0o666, 0o664, 0o660, 0o644, 0o640, 0o600,
+                   0o555, 0o551, 0o550, 0o511, 0o510, 0o500,
+                   0o444, 0o440, 0o400]
+
+    INVALID_MODES = [777, 775, 770, 755, 750, 711, 710, 700,
+                     666, 664, 660, 644, 640, 622, 620, 600,
+                     555, 551, 550,  # 511 == 0o777, 510 == 0o776, 500 == 0o764
+                     444, 440, 400]
 
     def setUp(self):
-        self.collection.register(OctalPermissionsRule())
+        self.rule = OctalPermissionsRule()
+        self.collection.register(self.rule)
 
     def test_file_positive(self):
         success = 'test/octalpermissions-success.yml'
@@ -19,3 +30,13 @@ def test_file_negative(self):
         bad_runner = Runner(self.collection, failure, [], [], [])
         errs = bad_runner.run()
         self.assertEqual(5, len(errs))
+
+    def test_valid_modes(self):
+        for mode in self.VALID_MODES:
+            self.assertFalse(self.rule.is_invalid_permission(mode),
+                             msg="0o%o should be a valid mode" % mode)
+
+    def test_invalid_modes(self):
+        for mode in self.INVALID_MODES:
+            self.assertTrue(self.rule.is_invalid_permission(mode),
+                            msg="%d should be an invalid mode" % mode)
diff --git a/test/TestRunner.py b/test/TestRunner.py
--- a/test/TestRunner.py
+++ b/test/TestRunner.py
@@ -91,6 +91,17 @@ def test_runner_empty_tags_count(self):
         runner = Runner(self.rules, filename, [], [], [])
         assert (len(runner.run()) == 0)
 
+    def test_runner_encrypted_secrets(self):
+        from pkg_resources import parse_version
+        import ansible
+        # Only run this test for ansible 2.3+
+        # It checks ansible-lint's parsing of yaml files that contain
+        # encrypted secrets.
+        if parse_version(ansible.__version__) >= parse_version('2.3'):
+            filename = 'test/contains_secrets.yml'
+            runner = Runner(self.rules, filename, [], [], [])
+            assert (len(runner.run()) == 0)
+
     def test_dir_with_trailing_slash(self):
         filename = 'test/'
         runner = Runner(self.rules, filename, [], [], [])
diff --git a/test/contains_secrets.yml b/test/contains_secrets.yml
new file mode 100644
--- /dev/null
+++ b/test/contains_secrets.yml
@@ -0,0 +1,14 @@
+- hosts: localhost
+  vars:
+    plain: hello123
+    # just 'hello123' encrypted with 'letmein' for test purposes
+    secret: !vault |
+          $ANSIBLE_VAULT;1.1;AES256
+          63346434613163653866303630313238626164313961613935373137323639636333393338386232
+          3735313061316666343839343665383036623237353263310a623639336530383433343833653138
+          30393032393534316164613834393864616566646164363830316664623636643731383164376163
+          3736653037356435310a303533383533353739323834343637366438633766666163656330343631
+          3066
+  tasks:
+  - name: just a debug task
+    debug: msg="hello world"