Repository: PyCQA/bandit
Pull request #507 (2019-06-24 23:32:00)

get_url returns different urls calling twice (bug #506)
* Describe the bug
The function get_url overwrite the info['id'] stored on extension_loader.MANAGER.blacklist_by_id

* Steps to reproduce the behavior:
```
In [1]: from bandit.core.docs_utils import get_url                                                                                                                                                                                                                      
In [2]: get_url('B304')                                                                                                                                                                                                                                                 
Out[2]: 'https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html#b304-b305-ciphers-and-modes'
In [3]: get_url('B304')                                                                                                                                                                                                                                                 
Out[3]: 'https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b304-b305-ciphers-and-modes'
```
* Expected behavior
Call to bid with same bid must return same url


Patch:
diff --git a/.travis.yml b/.travis.yml
index b8bc58d..064ad10 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -31,12 +31,6 @@ matrix:
       sudo: true
     - python: pypy
       env: TOXENV=pypy
-  allow_failures:
-    - python: 3.8-dev
-      env: TOXENV=py38
-      dist: xenial
-      sudo: true
-
 notifications:
   email:
     - lhinds@protonmail.com
diff --git a/bandit/core/docs_utils.py b/bandit/core/docs_utils.py
index c26ae90..053584d 100644
--- a/bandit/core/docs_utils.py
+++ b/bandit/core/docs_utils.py
@@ -37,10 +37,12 @@ def get_url(bid):
         if info['id'].startswith('B3'):  # B3XX
             # Some of the links are combined, so we have exception cases
             if info['id'] in ['B304', 'B305']:
+                info = info.copy()
                 info['id'] = 'b304-b305'
                 info['name'] = 'ciphers-and-modes'
             elif info['id'] in ['B313', 'B314', 'B315', 'B316', 'B317',
                                 'B318', 'B319', 'B320']:
+                info = info.copy()
                 info['id'] = 'b313-b320'
             ext = template.format(
                 kind='calls', id=info['id'], name=info['name'])
diff --git a/bandit/core/node_visitor.py b/bandit/core/node_visitor.py
index 1f7608e..172eebc 100644
--- a/bandit/core/node_visitor.py
+++ b/bandit/core/node_visitor.py
@@ -152,6 +152,23 @@ class BanditNodeVisitor(object):
             self.context['name'] = nodename.name
         self.update_scores(self.tester.run_tests(self.context, 'ImportFrom'))
 
+    def visit_Constant(self, node):
+        '''Visitor for AST Constant nodes
+
+        call the appropriate method for the node type.
+        this maintains compatibility with <3.6 and 3.8+
+
+        This code is heavily influenced by Anthony Sottile (@asottile) here:
+        https://bugs.python.org/msg342486
+
+        :param node: The node that is being inspected
+        :return: -
+        '''
+        if isinstance(node.value, str):
+            self.visit_Str(node)
+        elif isinstance(node.value, bytes):
+            self.visit_Bytes(node)
+
     def visit_Str(self, node):
         '''Visitor for AST String nodes
 
@@ -227,7 +244,7 @@ class BanditNodeVisitor(object):
 
         # HACK(tkelsey): this is needed to clean up post-recursion stuff that
         # gets setup in the visit methods for these node types.
-        if isinstance(node, ast.FunctionDef) or isinstance(node, ast.ClassDef):
+        if isinstance(node, (ast.FunctionDef, ast.ClassDef)):
             self.namespace = b_utils.namespace_path_split(self.namespace)[0]
 
     def generic_visit(self, node):
@@ -238,10 +255,10 @@ class BanditNodeVisitor(object):
                 for idx, item in enumerate(value):
                     if isinstance(item, ast.AST):
                         if idx < max_idx:
-                            setattr(item, '_bandit_sibling', value[idx + 1])
+                            item._bandit_sibling = value[idx + 1]
                         else:
-                            setattr(item, '_bandit_sibling', None)
-                        setattr(item, '_bandit_parent', node)
+                            item._bandit_sibling = None
+                        item._bandit_parent = node
 
                         if self.pre_visit(item):
                             self.visit(item)
@@ -249,9 +266,8 @@ class BanditNodeVisitor(object):
                             self.post_visit(item)
 
             elif isinstance(value, ast.AST):
-                setattr(value, '_bandit_sibling', None)
-                setattr(value, '_bandit_parent', node)
-
+                value._bandit_sibling = None
+                value._bandit_parent = node
                 if self.pre_visit(value):
                     self.visit(value)
                     self.generic_visit(value)
diff --git a/bandit/core/utils.py b/bandit/core/utils.py
index 694d322..ab031ab 100644
--- a/bandit/core/utils.py
+++ b/bandit/core/utils.py
@@ -14,7 +14,6 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
-import _ast
 import ast
 import logging
 import os.path
@@ -46,11 +45,11 @@ def _get_attr_qual_name(node, aliases):
     :param aliases: Import aliases dictionary
     :returns: Qualified name referred to by the attribute or name.
     '''
-    if isinstance(node, _ast.Name):
+    if isinstance(node, ast.Name):
         if node.id in aliases:
             return aliases[node.id]
         return node.id
-    elif isinstance(node, _ast.Attribute):
+    elif isinstance(node, ast.Attribute):
         name = '%s.%s' % (_get_attr_qual_name(node.value, aliases), node.attr)
         if name in aliases:
             return aliases[name]
@@ -60,11 +59,11 @@ def _get_attr_qual_name(node, aliases):
 
 
 def get_call_name(node, aliases):
-    if isinstance(node.func, _ast.Name):
+    if isinstance(node.func, ast.Name):
         if deepgetattr(node, 'func.id') in aliases:
             return aliases[deepgetattr(node, 'func.id')]
         return deepgetattr(node, 'func.id')
-    elif isinstance(node.func, _ast.Attribute):
+    elif isinstance(node.func, ast.Attribute):
         return _get_attr_qual_name(node.func, aliases)
     else:
         return ""
@@ -76,7 +75,7 @@ def get_func_name(node):
 
 def get_qual_attr(node, aliases):
     prefix = ""
-    if isinstance(node, _ast.Attribute):
+    if isinstance(node, ast.Attribute):
         try:
             val = deepgetattr(node, 'value.id')
             if val in aliases:
@@ -211,7 +210,7 @@ def linerange(node):
     for key in strip.keys():
         if hasattr(node, key):
             strip[key] = getattr(node, key)
-            setattr(node, key, [])
+            node.key = []
 
     lines_min = 9999999999
     lines_max = -1
@@ -222,7 +221,7 @@ def linerange(node):
 
     for key in strip.keys():
         if strip[key] is not None:
-            setattr(node, key, strip[key])
+            node.key = strip[key]
 
     if lines_max > -1:
         return list(range(lines_min, lines_max + 1))


Test patch:
diff --git a/bandit/core/test_set.py b/bandit/core/test_set.py
index 9addfb9..bbf7ef5 100644
--- a/bandit/core/test_set.py
+++ b/bandit/core/test_set.py
@@ -93,9 +93,10 @@ class BanditTestSet(object):
         # this dresses up the blacklist to look like a plugin, but
         # the '_checks' data comes from the blacklist information.
         # the '_config' is the filtered blacklist data set.
-        setattr(blacklisting.blacklist, "_test_id", 'B001')
-        setattr(blacklisting.blacklist, "_checks", blacklist.keys())
-        setattr(blacklisting.blacklist, "_config", blacklist)
+        blacklisting.blacklist._test_id = "B001"
+        blacklisting.blacklist._checks = blacklist.keys()
+        blacklisting.blacklist._config = blacklist
+
         return [Wrapper('blacklist', blacklisting.blacklist)]
 
     def _load_tests(self, config, plugins):
diff --git a/tests/unit/core/test_docs_util.py b/tests/unit/core/test_docs_util.py
new file mode 100644
index 0000000..a4d6cd7
--- /dev/null
+++ b/tests/unit/core/test_docs_util.py
@@ -0,0 +1,37 @@
+# Copyright 2019 Victor Torre
+#
+# 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.
+
+import testtools
+
+from bandit.core.docs_utils import BASE_URL
+from bandit.core.docs_utils import get_url
+
+
+class DocsUtilTests(testtools.TestCase):
+    '''This set of tests exercises bandit.core.docs_util functions.'''
+
+    def test_overwrite_bib_info(self):
+        expected_url = BASE_URL + ("blacklists/blacklist_calls.html"
+                                   "#b304-b305-ciphers-and-modes")
+        self.assertEqual(get_url('B304'), get_url('B305'))
+        self.assertEqual(expected_url, get_url('B304'))
+
+    def test_plugin_call_bib(self):
+        expected_url = BASE_URL + "plugins/b101_assert_used.html"
+        self.assertEqual(expected_url, get_url('B101'))
+
+    def test_import_call_bib(self):
+        expected_url = BASE_URL + ("blacklists/blacklist_imports.html"
+                                   "#b413-import-pycrypto")
+        self.assertEqual(expected_url, get_url('B413'))