Repository: PyCQA/bandit
Pull request #618 (2020-06-02 04:20:02)

Add the column offset to the issue model
This proposed change propagates the `col_offset` of the offending node in the AST to the `Issue` data model.

Just as you can report line number this allows reporting of the column offset as well.

Column offset reporting is a standard feature on many linters, also I've noticed the way that bandit is used by linter integrations like VSCode could benefit having the col offset of the reported function call, import etc.
In some cases there are two issues on a single line and this would indicate which is belongs to which report.

I noticed the custom formatter didn't have any unit tests, so I added one for this change.

Example output below:

```
python -m bandit examples/ -f custom --msg-template '{line}:{col} - {severity} - {test_id}:{msg}' -r                                                                                                                        col_offset ⍟1
[main]  INFO    profile include tests: None
[main]  INFO    profile exclude tests: None
[main]  INFO    cli include tests: None
[main]  INFO    cli exclude tests: None
[main]  INFO    running on Python 2.7.15
85 [0.. [manager]       ERROR   Exception occurred when executing tests against examples/nonsense2.py. Run "bandit --debug examples/nonsense2.py" to see the full traceback.
50.. ]
1:0 - LOW - B101:Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
4:8 - MEDIUM - B104:Possible binding to all interfaces.
6:7 - MEDIUM - B305:Use of insecure cipher mode cryptography.hazmat.primitives.ciphers.modes.ECB.
1:0 - HIGH - B413:The pyCrypto library and its module ARC2 are no longer actively maintained and have been deprecated. Consider using pyca/cryptography library.
2:0 - HIGH - B413:The pyCrypto library and its module ARC4 are no longer actively maintained and have been deprecated. Consider using pyca/cryptography library.
3:0 - HIGH - B413:The pyCrypto library and its module Blowfish are no longer actively maintained and have been deprecated. Consider using pyca/cryptography library.
4:0 - HIGH - B413:The pyCrypto library and its module DES are no longer actively maintained and have been deprecated. Consider using pyca/cryptography library.
5:0 - HIGH - B413:The pyCrypto library and its module XOR are no longer actively maintained and have been deprecated. Consider using pyca/cryptography library.
11:0 - HIGH - B413:The pyCrypto library and its module SHA are no longer actively maintained and have been deprecated. Consider using pyca/cryptography library.
12:0 - HIGH - B413:The pyCrypto library and its module Random are no longer actively maintained and have been deprecated. Consider using pyca/cryptography library.
13:0 - HIGH - B413:The pyCrypto library and its module Counter are no longer actively maintained and have been deprecated. Consider using pyca/cryptography library.
22:9 - HIGH - B304:Use of insecure cipher Crypto.Cipher.ARC2.new. Replace with a known secure cipher such as AES.
24:9 - HIGH - B304:Use of insecure cipher Cryptodome.Cipher.ARC2.new. Replace with a known secure cipher such as AES.
29:10 - MEDIUM - B303:Use of insecure MD2, MD4, MD5, or SHA1 hash function.
30:9 - HIGH - B304:Use of insecure cipher Crypto.Cipher.ARC4.new. Replace with a known secure cipher such as AES.
32:9 - HIGH - B304:Use of insecure cipher Cryptodome.Cipher.ARC4.new. Replace with a known secure cipher such as AES.
42:9 - HIGH - B304:Use of insecure cipher Crypto.Cipher.Blowfish.new. Replace with a known secure cipher such as AES.
45:9 - HIGH - B304:Use of insecure cipher Cryptodome.Cipher.Blowfish.new. Replace with a known secure cipher such as AES.
52:9 - HIGH - B304:Use of insecure cipher Crypto.Cipher.DES.new. Replace with a known secure cipher such as AES.
56:9 - HIGH - B304:Use of insecure cipher Cryptodome.Cipher.DES.new. Replace with a known secure cipher such as AES.
61:9 - HIGH - B304:Use of insecure cipher Crypto.Cipher.XOR.new. Replace with a known secure cipher such as AES.
63:9 - HIGH - B304:Use of insecure cipher Cryptodome.Cipher.XOR.new. Replace with a known secure cipher such as AES.
66:16 - HIGH - B304:Use of insecure cipher cryptography.hazmat.primitives.ciphers.algorithms.ARC4. Replace with a known secure cipher such as AES.
70:16 - HIGH - B304:Use of insecure cipher cryptography.hazmat.primitives.ciphers.algorithms.Blowfish. Replace with a known secure cipher such as AES.
74:16 - HIGH - B304:Use of insecure cipher cryptography.hazmat.primitives.ciphers.algorithms.IDEA. Replace with a known secure cipher such as AES.
2:0 - HIGH - B413:The pyCrypto library and its module MD2 are no longer actively maintained and have been deprecated. Consider using pyca/cryptography library.
3:0 - HIGH - B413:The pyCrypto library and its module MD4 are no longer actively maintained and have been deprecated. Consider using pyca/cryptography library.
4:0 - HIGH - B413:The pyCrypto library and its module MD5 are no longer actively maintained and have been deprecated. Consider using pyca/cryptography library.
5:0 - HIGH - B413:The pyCrypto library and its module SHA are no longer actively maintained and have been deprecated. Consider using pyca/cryptography library.
12:0 - MEDIUM - B303:Use of insecure MD2, MD4, MD5, or SHA1 hash function.
13:0 - MEDIUM - B303:Use of insecure MD2, MD4, MD5, or SHA1 hash function.
15:18 - MEDIUM - B303:Use of insecure MD2, MD4, MD5, or SHA1 hash function.
17:6 - MEDIUM - B303:Use of insecure MD2, MD4, MD5, or SHA1 hash function.
19:0 - MEDIUM - B303:Use of insecure MD2, MD4, MD5, or SHA1 hash function.
21:0 - MEDIUM - B303:Use of insecure MD2, MD4, MD5, or SHA1 hash function.
22:0 - MEDIUM - B303:Use of insecure MD2, MD4, MD5, or SHA1 hash function.
23:0 - MEDIUM - B303:Use of insecure MD2, MD4, MD5, or SHA1 hash function.
24:0 - MEDIUM - B303:Use of insecure MD2, MD4, MD5, or SHA1 hash function.
26:0 - MEDIUM - B303:Use of insecure MD2, MD4, MD5, or SHA1 hash function.
27:0 - MEDIUM - B303:Use of insecure MD2, MD4, MD5, or SHA1 hash function.
28:0 - MEDIUM - B303:Use of insecure MD2, MD4, MD5, or SHA1 hash function.
29:0 - MEDIUM - B303:Use of insecure MD2, MD4, MD5, or SHA1 hash function.
31:0 - MEDIUM - B303:Use of insecure MD2, MD4, MD5, or SHA1 hash function.
32:0 - MEDIUM - B303:Use of insecure MD2, MD4, MD5, or SHA1 hash function.
1:0 - LOW - B403:Consider possible security implications associated with dill module.
6:6 - MEDIUM - B301:Pickle and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue.
11:6 - MEDIUM - B301:Pickle and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue.
```

tonybaloney: > Is test_custom.py part of the column offset? If not, I suggest moving to a separate PR..

Yes, it's the test for the change. 
tonybaloney: @ericwb changes updated as requested

Patch:
diff --git a/README.rst b/README.rst
index 616808d..8269658 100644
--- a/README.rst
+++ b/README.rst
@@ -166,7 +166,7 @@ Usage::
 
     Available tags:
 
-        {abspath}, {relpath}, {line},  {test_id},
+        {abspath}, {relpath}, {line}, {col}, {test_id},
         {severity}, {msg}, {confidence}, {range}
 
     Example usage:
diff --git a/bandit/cli/main.py b/bandit/cli/main.py
index 0f71f88..f724b08 100644
--- a/bandit/cli/main.py
+++ b/bandit/cli/main.py
@@ -268,7 +268,7 @@ def main():
 
     Available tags:
 
-        {abspath}, {relpath}, {line},  {test_id},
+        {abspath}, {relpath}, {line}, {col}, {test_id},
         {severity}, {msg}, {confidence}, {range}
 
     Example usage:
diff --git a/bandit/core/issue.py b/bandit/core/issue.py
index 61394db..2f9f779 100644
--- a/bandit/core/issue.py
+++ b/bandit/core/issue.py
@@ -16,7 +16,7 @@ from bandit.core import constants
 
 class Issue(object):
     def __init__(self, severity, confidence=constants.CONFIDENCE_DEFAULT,
-                 text="", ident=None, lineno=None, test_id=""):
+                 text="", ident=None, lineno=None, test_id="", col_offset=0):
         self.severity = severity
         self.confidence = confidence
         if isinstance(text, bytes):
@@ -27,6 +27,7 @@ class Issue(object):
         self.test = ""
         self.test_id = test_id
         self.lineno = lineno
+        self.col_offset = col_offset
         self.linerange = []
 
     def __str__(self):
@@ -105,6 +106,7 @@ class Issue(object):
             'issue_text': self.text.encode('utf-8').decode('utf-8'),
             'line_number': self.lineno,
             'line_range': self.linerange,
+            'col_offset': self.col_offset
             }
 
         if with_code:
@@ -121,6 +123,7 @@ class Issue(object):
         self.test_id = data["test_id"]
         self.lineno = data["line_number"]
         self.linerange = data["line_range"]
+        self.col_offset = data.get("col_offset", 0)
 
 
 def issue_from_dict(data):
diff --git a/bandit/core/node_visitor.py b/bandit/core/node_visitor.py
index 5105d51..eba93e0 100644
--- a/bandit/core/node_visitor.py
+++ b/bandit/core/node_visitor.py
@@ -205,6 +205,8 @@ class BanditNodeVisitor(object):
                 LOG.debug("skipped, nosec")
                 self.metrics.note_nosec()
                 return False
+        if hasattr(node, 'col_offset'):
+            self.context['col_offset'] = node.col_offset
 
         self.context['node'] = node
         self.context['linerange'] = b_utils.linerange_fix(node)
diff --git a/bandit/formatters/custom.py b/bandit/formatters/custom.py
index eb39a9c..17740c5 100644
--- a/bandit/formatters/custom.py
+++ b/bandit/formatters/custom.py
@@ -74,6 +74,7 @@ def report(manager, fileobj, sev_level, conf_level, template=None):
         'abspath': lambda issue: os.path.abspath(issue.fname),
         'relpath': lambda issue: os.path.relpath(issue.fname),
         'line': lambda issue: issue.lineno,
+        'col': lambda issue: issue.col_offset,
         'test_id': lambda issue: issue.test_id,
         'severity': lambda issue: issue.severity,
         'msg': lambda issue: issue.text,


Test patch:
diff --git a/bandit/core/tester.py b/bandit/core/tester.py
index 8a8b4eb..fc46655 100644
--- a/bandit/core/tester.py
+++ b/bandit/core/tester.py
@@ -66,6 +66,7 @@ class BanditTester(object):
                     if result.lineno is None:
                         result.lineno = temp_context['lineno']
                     result.linerange = temp_context['linerange']
+                    result.col_offset = temp_context['col_offset']
                     result.test = name
                     if result.test_id == "":
                         result.test_id = test._test_id
diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py
index be0f99c..f8b373b 100644
--- a/tests/functional/test_functional.py
+++ b/tests/functional/test_functional.py
@@ -757,6 +757,7 @@ class FunctionalTests(testtools.TestCase):
               "issue_severity": "HIGH",
               "issue_text": "%s",
               "line_number": 10,
+              "col_offset": 0,
               "line_range": [
                 10
               ],
diff --git a/tests/unit/formatters/test_custom.py b/tests/unit/formatters/test_custom.py
new file mode 100644
index 0000000..b71b8c3
--- /dev/null
+++ b/tests/unit/formatters/test_custom.py
@@ -0,0 +1,55 @@
+# SPDX-License-Identifier: Apache-2.0
+
+import csv
+import tempfile
+
+import six
+import testtools
+
+import bandit
+from bandit.core import config
+from bandit.core import issue
+from bandit.core import manager
+from bandit.formatters import custom
+
+
+class CustomFormatterTests(testtools.TestCase):
+
+    def setUp(self):
+        super(CustomFormatterTests, self).setUp()
+        conf = config.BanditConfig()
+        self.manager = manager.BanditManager(conf, 'custom')
+        (tmp_fd, self.tmp_fname) = tempfile.mkstemp()
+        self.context = {'filename': self.tmp_fname,
+                        'lineno': 4,
+                        'linerange': [4],
+                        'col_offset': 30}
+        self.check_name = 'hardcoded_bind_all_interfaces'
+        self.issue = issue.Issue(bandit.MEDIUM, bandit.MEDIUM,
+                                 'Possible binding to all interfaces.')
+        self.manager.out_file = self.tmp_fname
+
+        self.issue.fname = self.context['filename']
+        self.issue.lineno = self.context['lineno']
+        self.issue.linerange = self.context['linerange']
+        self.issue.col_offset = self.context['col_offset']
+        self.issue.test = self.check_name
+
+        self.manager.results.append(self.issue)
+
+    def test_report(self):
+        with open(self.tmp_fname, 'w') as tmp_file:
+            custom.report(
+                self.manager, tmp_file, self.issue.severity,
+                self.issue.confidence,
+                template="{line},{col},{severity},{msg}")
+
+        with open(self.tmp_fname) as f:
+            reader = csv.DictReader(f, ['line', 'col', 'severity', 'message'])
+            data = six.next(reader)
+            self.assertEqual(six.text_type(self.context['lineno']),
+                             data['line'])
+            self.assertEqual(six.text_type(self.context['col_offset']),
+                             data['col'])
+            self.assertEqual(self.issue.severity, data['severity'])
+            self.assertEqual(self.issue.text, data['message'])