Merge ubuntu-security-tools:json-output into ubuntu-security-tools:master

Proposed by Mark Esler
Status: Rejected
Rejected by: Mark Esler
Proposed branch: ubuntu-security-tools:json-output
Merge into: ubuntu-security-tools:master
Diff against target: 245 lines (+120/-66)
2 files modified
audits/shellcheck-json.sh (+2/-0)
audits/uaudit (+118/-66)
Reviewer Review Type Date Requested Status
Alex Murray Needs Fixing
Review via email: mp+431459@code.launchpad.net

This proposal has been superseded by a proposal from 2022-10-14.

Commit message

extends StaticAnalysisTool to allow optional JSON output

Description of the change

Primarily extends the StaticAnalysisTool class. StaticAnalysisTool.cmd_json sets command to generate JSON output. Also adds StaticAnalysisTool.stderr to set if stderr is allowed in output.

bandit and scc have cmd_json examples. Most other tools support JSON output.

To post a comment you must log in.
Revision history for this message
Alex Murray (alexmurray) wrote :

Since both tools support `--format json` why not have a supports_format_json boolean parameter which defaults to False but when set means the tools get run twice (with and without the --format json parameters) and then we can always generated both json and text output? Then you don't have to add the --json top level command-line flag.

review: Needs Fixing
ubuntu-security-tools:json-output updated
aca84bd... by Mark Esler

uaudit: add json output to all supported static_analysis_tools and clean formatting

Revision history for this message
Mark Esler (eslerm) wrote (last edit ):

@alexmurray thanks for the review and suggestions.

I added cmd_json for all the tools we use that support json output. Not all of them use `--format json`.

It would be nice to only maintain a single or base command per tool, but I don't believe that is possible. I was hoping we could splice json specific arguments into StaticAnalysisTool.cmd between element 0 and 1, but shellcheck and scc throw a wrench into that.

I reformatted static_analysis_tools to help make it easier to read and lower technical debt.

ubuntu-security-tools:json-output updated
fb4bd69... by Mark Esler

uaudit: force json output when supported

Revision history for this message
Mark Esler (eslerm) wrote :

@alexmurray I removed `--json` so that every tool that supports json output runs StaticAnalysisTool.cmd_json

Revision history for this message
Alex Murray (alexmurray) wrote :

Thanks @eslerm - however I would really like to avoid having to duplicate the arguments between cmd and cmd_json - hence my suggestion for a single boolean above. If we have these duplicated then it will only be a matter of time before they start to diverge - so better to have a single source of truth and then a way to make this do json IMO.

If that can't work, how about a `json_flags` argument or similar which would be a list of command-line options to use to have the tool output json instead - and then you could update the existing shellcheck.sh to look for these and DTRT so we don't have to have two shellcheck wrapper scripts as well.

Thoughts?

review: Needs Fixing
Revision history for this message
Mark Esler (eslerm) wrote :

I want to deduplicate arguments, but I do not see an elegant way to do so.

In my previous commit, I attempted ~`json_flags` but abandoned that approach. In addition to adding arguments, `json_flags` would need to remove arguments from StaticAnalysisTool.cmd which requires logic about each tool--unless we also add ~`cmd_flags`. Using a ~`json_flags` approach would need something like:
```
+ cmd_base=["scc", "--exclude-dir", ".git,.hg,.svn,.pc"],
+ cmd_txt=["--no-cocomo", "--ci"],
+ cmd_json=["--format", "json"],
+ cmd_end=["."],
```
(Hopefully no other positional arguments come along!)

Revision history for this message
Alex Murray (alexmurray) wrote :

What about a output_formats parameter which takes a dict of output format names along with their command-line arguments to make that happen? Then depending on which output format is specified to uaudit we use that, and fallback to a "txt" one if the specified one is not found?

Unmerged commits

fb4bd69... by Mark Esler

uaudit: force json output when supported

aca84bd... by Mark Esler

uaudit: add json output to all supported static_analysis_tools and clean formatting

421d63c... by Mark Esler

uaudit: add ignored directories for json commands

c82a8df... by Mark Esler

uaudit: add cmd_json to StaticAnalysisTool

585f6d6... by Mark Esler

uaudit add json bandit

63276e0... by Mark Esler

uaudit make cmd and tool stderr optional

5174f91... by Mark Esler

init additional json output mode

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/audits/shellcheck-json.sh b/audits/shellcheck-json.sh
0new file mode 1007550new file mode 100755
index 0000000..b061d08
--- /dev/null
+++ b/audits/shellcheck-json.sh
@@ -0,0 +1,2 @@
1#!/bin/bash
2shellcheck -f json1 $(find . -type f -exec file {} \; | awk -F: '{if ($0 ~ /shell script/) print $1;}' )
diff --git a/audits/uaudit b/audits/uaudit
index f9430df..ed65c43 100755
--- a/audits/uaudit
+++ b/audits/uaudit
@@ -50,11 +50,15 @@ class StaticAnalysisTool(object):
50 name: str,50 name: str,
51 source: StaticAnalysisToolSource=StaticAnalysisToolSource.SNAP,51 source: StaticAnalysisToolSource=StaticAnalysisToolSource.SNAP,
52 cmd: list=list(),52 cmd: list=list(),
53 cmd_json: list=list(),
54 stderr: bool=True,
53 emacs_vars: str="",55 emacs_vars: str="",
54 summary: list=list(["cat", OUTPUT_FILE])):56 summary: list=list(["cat", OUTPUT_FILE])):
55 self.name = name57 self.name = name
56 self._source = source58 self._source = source
57 self._cmd = cmd if len(cmd) > 0 else [self.name, '.']59 self._cmd = cmd if len(cmd) > 0 else [self.name, '.']
60 self._cmd_json = cmd_json
61 self._stderr = stderr
58 self.header = "# -*- mode: compilation; default-directory: \"$PWD\"; %s -*-\n" % emacs_vars62 self.header = "# -*- mode: compilation; default-directory: \"$PWD\"; %s -*-\n" % emacs_vars
59 self._summary = summary63 self._summary = summary
6064
@@ -65,55 +69,87 @@ class StaticAnalysisTool(object):
65 def exec_cmd(self) -> list:69 def exec_cmd(self) -> list:
66 return self._cmd70 return self._cmd
6771
72 def exec_cmd_json(self) -> list:
73 return self._cmd_json
74
68 def summary_cmd(self, out) -> list:75 def summary_cmd(self, out) -> list:
69 return [out if i==OUTPUT_FILE else i for i in self._summary]76 return [out if i==OUTPUT_FILE else i for i in self._summary]
7077
71static_analysis_tools = [78static_analysis_tools = [
72 StaticAnalysisTool("cppcheck",79 StaticAnalysisTool(
73 # the ignore path for the .pc directory may not80 "cppcheck",
74 # strictly be necessary81 # the ignore path for the .pc directory may not
75 cmd=['cppcheck', '--max-configs=15', '-j 8', '-q', '-i', '.pc', '.'],82 # strictly be necessary
76 summary=['grep', '-c', '^[a-z]', OUTPUT_FILE],83 cmd=["cppcheck", "--max-configs=15", "-j 8", "-q", "-i", ".pc", "."],
77 source=StaticAnalysisToolSource.DEB),84 # json not yet supported
78 StaticAnalysisTool("bandit",85 # https://sourceforge.net/p/cppcheck/discussion/development/thread/11df29af5b/?limit=25
79 # output in a format which we can easily read (-f86 summary=["grep", "-c", "^[a-z]", OUTPUT_FILE],
80 # custom) and which doesn't truncate results (ie -n87 source=StaticAnalysisToolSource.DEB,
81 # -1)88 ),
82 # https://github.com/PyCQA/bandit/issues/371#issuecomment-41370411289 StaticAnalysisTool(
83 #90 "bandit",
84 # arguments to -x (excluded paths) have to be relative or absolute91 # output in a format which we can easily read (-f
85 # paths so ./.pc rather than just .pc92 # custom) and which doesn't truncate results (ie -n
86 # https://github.com/PyCQA/bandit/issues/488#issuecomment-58314479393 # -1)
87 cmd=['bandit', '-r', '-n', '-1', '-x', './.pc', '-f', 'custom', '.'],94 # https://github.com/PyCQA/bandit/issues/371#issuecomment-413704112
88 summary=['grep', '-c', '^/', OUTPUT_FILE]),95 #
89 StaticAnalysisTool("brakeman",96 # arguments to -x (excluded paths) have to be relative or absolute
90 # output in text format without the pager and turn97 # paths so ./.pc rather than just .pc
91 # on all warnings98 # https://github.com/PyCQA/bandit/issues/488#issuecomment-583144793
92 cmd=['brakeman', '--force', '--all', '--quiet', '--no-pager',99 cmd=["bandit", "-r", "-n", "-1", "-x", "./.pc", "-f", "custom", "."],
93 '-o', '/dev/stdout', '.'],100 cmd_json=["bandit", "-r", "-n", "-1", "-x", "./.pc", "-f", "json", "."],
94 emacs_vars=("eval: (defun brakeman-backward-search-filename ()" +101 stderr=False,
95 " (save-match-data " +102 summary=["grep", "-c", "^/", OUTPUT_FILE],
96 " (save-excursion " +103 ),
97 " (when (re-search-backward \"^File: \\\\(.*\\\\)$\" (point-min) t)" +104 StaticAnalysisTool(
98 " (list (match-string-no-properties 1)))))); " +105 "brakeman",
99 "eval: (setq-local compilation-error-regexp-alist " +106 # output in text format without the pager and turn
100 " '((\"^Line: \\\\([[:digit:]]+\\\\)$\" brakeman-backward-search-filename 1)));"),107 # on all warnings
101 summary=['grep', '-c', '^Message:', OUTPUT_FILE]),108 cmd=["brakeman", "--force", "--all", "--quiet", "--no-pager", "-o", "/dev/stdout"],
102 StaticAnalysisTool("flawfinder",109 cmd_json=["brakeman", "--force", "--all", "--quiet", "--no-pager", "-o", "/dev/stdout", "-f", "json"],
103 summary=['grep', '-c', '^[a-z]', OUTPUT_FILE]),110 emacs_vars=(
104 StaticAnalysisTool("shellcheck",111 "eval: (defun brakeman-backward-search-filename ()"
105 cmd=["shellcheck.sh"],112 + " (save-match-data "
106 summary=['grep', '-c', '^\\./', OUTPUT_FILE]),113 + " (save-excursion "
107 StaticAnalysisTool("gosec",114 + ' (when (re-search-backward "^File: \\\\(.*\\\\)$" (point-min) t)'
108 cmd=['gosec', '-quiet', '-out', '/dev/stdout', './...'],115 + " (list (match-string-no-properties 1)))))); "
109 emacs_vars=("eval: (setq-local compilation-error-regexp-alist " +116 + "eval: (setq-local compilation-error-regexp-alist "
110 " '((\"^\\\\[\\\\(.*\\\\):\\\\([[:digit:]]+\\\\)\\\\].*$\" 1 2)));"),117 + ' \'(("^Line: \\\\([[:digit:]]+\\\\)$" brakeman-backward-search-filename 1)));'
111 summary=['grep', '-c', '\\[/.*\\]', OUTPUT_FILE]),118 ),
112 StaticAnalysisTool("scc",119 summary=["grep", "-c", "^Message:", OUTPUT_FILE],
113 cmd=['scc', '--no-cocomo', '--ci', '--exclude-dir', '.git,.hg,.svn,.pc', '.'],120 ),
114 summary=["sed", "/^Processed/q", OUTPUT_FILE]),121 StaticAnalysisTool(
122 "flawfinder",
123 # nb: using sarif style json
124 cmd_json=["flawfinder", "--sarif", "./"],
125 summary=["grep", "-c", "^[a-z]", OUTPUT_FILE],
126 ),
127 StaticAnalysisTool(
128 "shellcheck",
129 cmd=["shellcheck.sh"],
130 cmd_json=["shellcheck-json.sh"],
131 summary=["grep", "-c", "^\\./", OUTPUT_FILE],
132 ),
133 StaticAnalysisTool(
134 "gosec",
135 cmd=["gosec", "-quiet", "-out", "/dev/stdout", "./..."],
136 # nb: using sarif style json
137 cmd_json=["gosec", "-quiet", "-out", "/dev/stdout", "-fmt=sarif", "./..."],
138 emacs_vars=(
139 "eval: (setq-local compilation-error-regexp-alist "
140 + ' \'(("^\\\\[\\\\(.*\\\\):\\\\([[:digit:]]+\\\\)\\\\].*$" 1 2)));'
141 ),
142 summary=["grep", "-c", "\\[/.*\\]", OUTPUT_FILE],
143 ),
144 StaticAnalysisTool(
145 "scc",
146 cmd=["scc", "--no-cocomo", "--ci", "--exclude-dir", ".git,.hg,.svn,.pc", "."],
147 cmd_json=["scc", "--format", "json", "--exclude-dir", ".git,.hg,.svn,.pc", "."],
148 summary=["sed", "/^Processed/q", OUTPUT_FILE],
149 ),
115]150]
116151
152
117#153#
118# Utility functions154# Utility functions
119#155#
@@ -169,12 +205,16 @@ def debug(out):
169 pass205 pass
170206
171207
172def cmd(command):208def cmd(command, stderr = True):
173 '''Try to execute the given command.'''209 '''Try to execute the given command.'''
174 debug(command)210 debug(command)
175 try:211 try:
176 sp = subprocess.Popen(command, stdout=subprocess.PIPE,212 if stderr:
177 stderr=subprocess.STDOUT)213 sp = subprocess.Popen(command, stdout=subprocess.PIPE,
214 stderr=subprocess.STDOUT)
215 else:
216 sp = subprocess.Popen(command, stdout=subprocess.PIPE,
217 stderr=subprocess.DEVNULL)
178 except OSError as ex:218 except OSError as ex:
179 return [127, str(ex)]219 return [127, str(ex)]
180220
@@ -541,6 +581,35 @@ def audit_packaging(audit_dir):
541 write_file(out_fn, out)581 write_file(out_fn, out)
542 msg("Package audit: %s" % (out_fn_rel))582 msg("Package audit: %s" % (out_fn_rel))
543583
584def run_static_analysis(tool, output_type):
585 out_fn = os.path.join(audit_dir, tool.name.lower() + "." + output_type)
586 out_fn_rel = '/'.join(out_fn.split('/')[-2:])
587 if os.path.exists(out_fn):
588 warn("Skipping %s. '%s' already exists" % (tool.name, out_fn))
589 else:
590 if output_type == "txt":
591 rc, out = cmd(tool.exec_cmd())
592 out = tool.header.replace("$PWD", os.getcwd()) + out
593 elif output_type == "json":
594 if tool._stderr:
595 rc, out = cmd(tool.exec_cmd_json())
596 else:
597 rc, out = cmd(tool.exec_cmd_json(), False)
598 else:
599 error(f"Unknown output_type ({output_type}) while running {tool.name})")
600 # TODO: gosec exits 1 if no go files and actual output not saved
601 if rc != 0:
602 # note: above loop uses error instead of warn
603 warn(f"Attempting to run {tool.name} exited with: {rc}")
604 write_file(out_fn, out)
605 msg(f"Static analysis ({tool.name}): {out_fn_rel}")
606
607 rc, summary = cmd(tool.summary_cmd(out_fn))
608 # strip any trailing newline
609 if summary[-1] == "\n":
610 summary = summary[:-1]
611 details[tool.name] = summary
612
544613
545def audit_code(audit_dir, details, disable_coverity=False):614def audit_code(audit_dir, details, disable_coverity=False):
546 '''Audit code'''615 '''Audit code'''
@@ -581,26 +650,9 @@ def audit_code(audit_dir, details, disable_coverity=False):
581 msg("Code audit (%s): %s" % (i, out_fn_rel))650 msg("Code audit (%s): %s" % (i, out_fn_rel))
582651
583 for tool in static_analysis_tools:652 for tool in static_analysis_tools:
584 out_fn = os.path.join(audit_dir, tool.name.lower() + ".txt")653 run_static_analysis(tool, "txt")
585 out_fn_rel = '/'.join(out_fn.split('/')[-2:])654 if tool._cmd_json:
586 if os.path.exists(out_fn):655 run_static_analysis(tool, "json")
587 warn("Skipping %s. '%s' already exists" % (tool.name, out_fn))
588 else:
589 rc, out = cmd(tool.exec_cmd())
590 if rc != 0:
591 # note: above loop uses error instead of warn
592 warn(f"Attempting to run {tool.name} exited with: {rc}")
593 # TODO: gosec exits 1 if no go files and actual output not saved
594 out = tool.header.replace("$PWD", os.getcwd()) + out
595 write_file(out_fn, out)
596 msg(f"Static analysis ({tool.name}): {out_fn_rel}")
597
598 rc, summary = cmd(tool.summary_cmd(out_fn))
599 # strip any trailing newline
600 if summary[-1] == "\n":
601 summary = summary[:-1]
602 details[tool.name] = summary
603
604656
605 if not disable_coverity:657 if not disable_coverity:
606 # perform coverity analysis if the directory exists (assume in this658 # perform coverity analysis if the directory exists (assume in this

Subscribers

People subscribed via source and target branches