From edeabc3faf638716842a562bc63e0e72c7937281 Mon Sep 17 00:00:00 2001 From: Tim McCormack <tmccormack@edx.org> Date: Fri, 11 Dec 2020 12:34:04 +0000 Subject: [PATCH] Add --summary-format=json option to XSS linter (#25851) This will simplify updating of the linter thresholds file after XSS linter violations are addressed. --- scripts/xsslint/tests/test_main.py | 35 +++++++++++++++++++++ scripts/xsslint/xsslint/main.py | 6 ++++ scripts/xsslint/xsslint/reporting.py | 47 ++++++++++++++++++++++------ 3 files changed, 78 insertions(+), 10 deletions(-) diff --git a/scripts/xsslint/tests/test_main.py b/scripts/xsslint/tests/test_main.py index d437441f03b..3e026b62abc 100644 --- a/scripts/xsslint/tests/test_main.py +++ b/scripts/xsslint/tests/test_main.py @@ -4,6 +4,7 @@ Tests for main.py """ +import json import re from six import StringIO from unittest import TestCase @@ -69,6 +70,7 @@ class TestXSSLinter(TestCase): 'list_files': False, 'verbose': False, 'rule_totals': False, + 'summary_format': 'eslint', 'skip_dirs': () }, summary_results=self.summary_results, @@ -107,6 +109,7 @@ class TestXSSLinter(TestCase): 'list_files': False, 'verbose': True, 'rule_totals': False, + 'summary_format': 'eslint', 'skip_dirs': () }, summary_results=self.summary_results, @@ -139,6 +142,7 @@ class TestXSSLinter(TestCase): 'list_files': False, 'verbose': False, 'rule_totals': True, + 'summary_format': 'eslint', 'skip_dirs': () }, summary_results=self.summary_results, @@ -153,6 +157,36 @@ class TestXSSLinter(TestCase): self.assertIsNotNone(re.search(r'{}:\s*{} violations'.format(self.ruleset.python_wrap_html.rule_id, 1), output)) self.assertIsNotNone(re.search(r'{} violations total'.format(5), output)) + def test_lint_with_json_output(self): + """ + Tests the top-level linting with JSON summary format. + """ + _lint( + 'scripts/xsslint/tests/templates', + template_linters=self.template_linters, + options={ + 'list_files': False, + 'verbose': False, + 'rule_totals': True, + 'summary_format': 'json', + 'skip_dirs': () + }, + summary_results=self.summary_results, + out=self.out, + ) + + output = self.out.getvalue() + self.assertIsNotNone(re.search(r'test\.py.*{}'.format(self.ruleset.python_wrap_html.rule_id), output)) + + # Find something that looks like pretty-printed JSON + json_match = re.search(r'\n\{.*\n\}', output, re.DOTALL) + self.assertIsNotNone(json_match) + data = json.loads(json_match.group()) + # Check for rule counts (including zero-instance ones) and total + self.assertEqual(1, data['rules']['javascript-concat-html']) + self.assertEqual(0, data['rules']['python-concat-html']) + self.assertEqual(5, data['total']) + def test_lint_with_list_files(self): """ Tests the top-level linting with list files option. @@ -164,6 +198,7 @@ class TestXSSLinter(TestCase): 'list_files': True, 'verbose': False, 'rule_totals': False, + 'summary_format': 'eslint', 'skip_dirs': () }, summary_results=self.summary_results, diff --git a/scripts/xsslint/xsslint/main.py b/scripts/xsslint/xsslint/main.py index 5f8b9c65efd..1eefc690953 100644 --- a/scripts/xsslint/xsslint/main.py +++ b/scripts/xsslint/xsslint/main.py @@ -151,6 +151,11 @@ def main(): '--rule-totals', dest='rule_totals', action='store_true', help='Display the totals for each rule.' ) + parser.add_argument( + '--summary-format', dest='summary_format', + choices=['eslint', 'json'], default='eslint', + help='Choose the display format for the summary.' + ) parser.add_argument( '--verbose', dest='verbose', action='store_true', help='Print multiple lines where possible for additional context of violations.' @@ -166,6 +171,7 @@ def main(): options = { 'list_files': args.list_files, 'rule_totals': args.rule_totals, + 'summary_format': args.summary_format, 'verbose': args.verbose, 'skip_dirs': getattr(config, 'SKIP_DIRS', ()) } diff --git a/scripts/xsslint/xsslint/reporting.py b/scripts/xsslint/xsslint/reporting.py index b50fe76d905..a1690e58d5c 100644 --- a/scripts/xsslint/xsslint/reporting.py +++ b/scripts/xsslint/xsslint/reporting.py @@ -3,6 +3,7 @@ Utility classes for reporting linter results. """ +import json import os import re @@ -275,17 +276,43 @@ class SummaryResults(object): """ if options['list_files'] is False: - if options['rule_totals']: - max_rule_id_len = max(len(rule_id) for rule_id in self.totals_by_rule) - print("", file=out) - for rule_id in sorted(self.totals_by_rule.keys()): - padding = " " * (max_rule_id_len - len(rule_id)) - print("{}: {}{} violations".format(rule_id, padding, self.totals_by_rule[rule_id]), file=out) - print("", file=out) - - # matches output of eslint for simplicity + if options['summary_format'] == 'json': + self._print_json_format(options, out) + else: + self._print_eslint_format(options, out) + + def _print_eslint_format(self, options, out): + """ + Implementation of print_results with eslint-style output. + """ + if options['rule_totals']: + max_rule_id_len = max(len(rule_id) for rule_id in self.totals_by_rule) print("", file=out) - print("{} violations total".format(self.total_violations), file=out) + for rule_id in sorted(self.totals_by_rule.keys()): + padding = " " * (max_rule_id_len - len(rule_id)) + print("{}: {}{} violations".format(rule_id, padding, self.totals_by_rule[rule_id]), file=out) + print("", file=out) + + # matches output of eslint for simplicity + print("", file=out) + print("{} violations total".format(self.total_violations), file=out) + + def _print_json_format(self, options, out): + """ + Implementation of print_results with JSON output. + """ + print("", file=out) + print("Violation counts:", file=out) + data = {'rules': self.totals_by_rule} + if options['rule_totals']: + data['total'] = self.total_violations + json.dump(data, fp=out, indent=4, sort_keys=True) + print("", file=out) + print( + "If you've fixed some XSS issues and these numbers have gone down, " + "you can use this to update scripts/xsslint_thresholds.json", + file=out + ) class FileResults(object): -- GitLab