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
1diff --git a/audits/shellcheck-json.sh b/audits/shellcheck-json.sh
2new file mode 100755
3index 0000000..b061d08
4--- /dev/null
5+++ b/audits/shellcheck-json.sh
6@@ -0,0 +1,2 @@
7+#!/bin/bash
8+shellcheck -f json1 $(find . -type f -exec file {} \; | awk -F: '{if ($0 ~ /shell script/) print $1;}' )
9diff --git a/audits/uaudit b/audits/uaudit
10index f9430df..ed65c43 100755
11--- a/audits/uaudit
12+++ b/audits/uaudit
13@@ -50,11 +50,15 @@ class StaticAnalysisTool(object):
14 name: str,
15 source: StaticAnalysisToolSource=StaticAnalysisToolSource.SNAP,
16 cmd: list=list(),
17+ cmd_json: list=list(),
18+ stderr: bool=True,
19 emacs_vars: str="",
20 summary: list=list(["cat", OUTPUT_FILE])):
21 self.name = name
22 self._source = source
23 self._cmd = cmd if len(cmd) > 0 else [self.name, '.']
24+ self._cmd_json = cmd_json
25+ self._stderr = stderr
26 self.header = "# -*- mode: compilation; default-directory: \"$PWD\"; %s -*-\n" % emacs_vars
27 self._summary = summary
28
29@@ -65,55 +69,87 @@ class StaticAnalysisTool(object):
30 def exec_cmd(self) -> list:
31 return self._cmd
32
33+ def exec_cmd_json(self) -> list:
34+ return self._cmd_json
35+
36 def summary_cmd(self, out) -> list:
37 return [out if i==OUTPUT_FILE else i for i in self._summary]
38
39 static_analysis_tools = [
40- StaticAnalysisTool("cppcheck",
41- # the ignore path for the .pc directory may not
42- # strictly be necessary
43- cmd=['cppcheck', '--max-configs=15', '-j 8', '-q', '-i', '.pc', '.'],
44- summary=['grep', '-c', '^[a-z]', OUTPUT_FILE],
45- source=StaticAnalysisToolSource.DEB),
46- StaticAnalysisTool("bandit",
47- # output in a format which we can easily read (-f
48- # custom) and which doesn't truncate results (ie -n
49- # -1)
50- # https://github.com/PyCQA/bandit/issues/371#issuecomment-413704112
51- #
52- # arguments to -x (excluded paths) have to be relative or absolute
53- # paths so ./.pc rather than just .pc
54- # https://github.com/PyCQA/bandit/issues/488#issuecomment-583144793
55- cmd=['bandit', '-r', '-n', '-1', '-x', './.pc', '-f', 'custom', '.'],
56- summary=['grep', '-c', '^/', OUTPUT_FILE]),
57- StaticAnalysisTool("brakeman",
58- # output in text format without the pager and turn
59- # on all warnings
60- cmd=['brakeman', '--force', '--all', '--quiet', '--no-pager',
61- '-o', '/dev/stdout', '.'],
62- emacs_vars=("eval: (defun brakeman-backward-search-filename ()" +
63- " (save-match-data " +
64- " (save-excursion " +
65- " (when (re-search-backward \"^File: \\\\(.*\\\\)$\" (point-min) t)" +
66- " (list (match-string-no-properties 1)))))); " +
67- "eval: (setq-local compilation-error-regexp-alist " +
68- " '((\"^Line: \\\\([[:digit:]]+\\\\)$\" brakeman-backward-search-filename 1)));"),
69- summary=['grep', '-c', '^Message:', OUTPUT_FILE]),
70- StaticAnalysisTool("flawfinder",
71- summary=['grep', '-c', '^[a-z]', OUTPUT_FILE]),
72- StaticAnalysisTool("shellcheck",
73- cmd=["shellcheck.sh"],
74- summary=['grep', '-c', '^\\./', OUTPUT_FILE]),
75- StaticAnalysisTool("gosec",
76- cmd=['gosec', '-quiet', '-out', '/dev/stdout', './...'],
77- emacs_vars=("eval: (setq-local compilation-error-regexp-alist " +
78- " '((\"^\\\\[\\\\(.*\\\\):\\\\([[:digit:]]+\\\\)\\\\].*$\" 1 2)));"),
79- summary=['grep', '-c', '\\[/.*\\]', OUTPUT_FILE]),
80- StaticAnalysisTool("scc",
81- cmd=['scc', '--no-cocomo', '--ci', '--exclude-dir', '.git,.hg,.svn,.pc', '.'],
82- summary=["sed", "/^Processed/q", OUTPUT_FILE]),
83+ StaticAnalysisTool(
84+ "cppcheck",
85+ # the ignore path for the .pc directory may not
86+ # strictly be necessary
87+ cmd=["cppcheck", "--max-configs=15", "-j 8", "-q", "-i", ".pc", "."],
88+ # json not yet supported
89+ # https://sourceforge.net/p/cppcheck/discussion/development/thread/11df29af5b/?limit=25
90+ summary=["grep", "-c", "^[a-z]", OUTPUT_FILE],
91+ source=StaticAnalysisToolSource.DEB,
92+ ),
93+ StaticAnalysisTool(
94+ "bandit",
95+ # output in a format which we can easily read (-f
96+ # custom) and which doesn't truncate results (ie -n
97+ # -1)
98+ # https://github.com/PyCQA/bandit/issues/371#issuecomment-413704112
99+ #
100+ # arguments to -x (excluded paths) have to be relative or absolute
101+ # paths so ./.pc rather than just .pc
102+ # https://github.com/PyCQA/bandit/issues/488#issuecomment-583144793
103+ cmd=["bandit", "-r", "-n", "-1", "-x", "./.pc", "-f", "custom", "."],
104+ cmd_json=["bandit", "-r", "-n", "-1", "-x", "./.pc", "-f", "json", "."],
105+ stderr=False,
106+ summary=["grep", "-c", "^/", OUTPUT_FILE],
107+ ),
108+ StaticAnalysisTool(
109+ "brakeman",
110+ # output in text format without the pager and turn
111+ # on all warnings
112+ cmd=["brakeman", "--force", "--all", "--quiet", "--no-pager", "-o", "/dev/stdout"],
113+ cmd_json=["brakeman", "--force", "--all", "--quiet", "--no-pager", "-o", "/dev/stdout", "-f", "json"],
114+ emacs_vars=(
115+ "eval: (defun brakeman-backward-search-filename ()"
116+ + " (save-match-data "
117+ + " (save-excursion "
118+ + ' (when (re-search-backward "^File: \\\\(.*\\\\)$" (point-min) t)'
119+ + " (list (match-string-no-properties 1)))))); "
120+ + "eval: (setq-local compilation-error-regexp-alist "
121+ + ' \'(("^Line: \\\\([[:digit:]]+\\\\)$" brakeman-backward-search-filename 1)));'
122+ ),
123+ summary=["grep", "-c", "^Message:", OUTPUT_FILE],
124+ ),
125+ StaticAnalysisTool(
126+ "flawfinder",
127+ # nb: using sarif style json
128+ cmd_json=["flawfinder", "--sarif", "./"],
129+ summary=["grep", "-c", "^[a-z]", OUTPUT_FILE],
130+ ),
131+ StaticAnalysisTool(
132+ "shellcheck",
133+ cmd=["shellcheck.sh"],
134+ cmd_json=["shellcheck-json.sh"],
135+ summary=["grep", "-c", "^\\./", OUTPUT_FILE],
136+ ),
137+ StaticAnalysisTool(
138+ "gosec",
139+ cmd=["gosec", "-quiet", "-out", "/dev/stdout", "./..."],
140+ # nb: using sarif style json
141+ cmd_json=["gosec", "-quiet", "-out", "/dev/stdout", "-fmt=sarif", "./..."],
142+ emacs_vars=(
143+ "eval: (setq-local compilation-error-regexp-alist "
144+ + ' \'(("^\\\\[\\\\(.*\\\\):\\\\([[:digit:]]+\\\\)\\\\].*$" 1 2)));'
145+ ),
146+ summary=["grep", "-c", "\\[/.*\\]", OUTPUT_FILE],
147+ ),
148+ StaticAnalysisTool(
149+ "scc",
150+ cmd=["scc", "--no-cocomo", "--ci", "--exclude-dir", ".git,.hg,.svn,.pc", "."],
151+ cmd_json=["scc", "--format", "json", "--exclude-dir", ".git,.hg,.svn,.pc", "."],
152+ summary=["sed", "/^Processed/q", OUTPUT_FILE],
153+ ),
154 ]
155
156+
157 #
158 # Utility functions
159 #
160@@ -169,12 +205,16 @@ def debug(out):
161 pass
162
163
164-def cmd(command):
165+def cmd(command, stderr = True):
166 '''Try to execute the given command.'''
167 debug(command)
168 try:
169- sp = subprocess.Popen(command, stdout=subprocess.PIPE,
170- stderr=subprocess.STDOUT)
171+ if stderr:
172+ sp = subprocess.Popen(command, stdout=subprocess.PIPE,
173+ stderr=subprocess.STDOUT)
174+ else:
175+ sp = subprocess.Popen(command, stdout=subprocess.PIPE,
176+ stderr=subprocess.DEVNULL)
177 except OSError as ex:
178 return [127, str(ex)]
179
180@@ -541,6 +581,35 @@ def audit_packaging(audit_dir):
181 write_file(out_fn, out)
182 msg("Package audit: %s" % (out_fn_rel))
183
184+def run_static_analysis(tool, output_type):
185+ out_fn = os.path.join(audit_dir, tool.name.lower() + "." + output_type)
186+ out_fn_rel = '/'.join(out_fn.split('/')[-2:])
187+ if os.path.exists(out_fn):
188+ warn("Skipping %s. '%s' already exists" % (tool.name, out_fn))
189+ else:
190+ if output_type == "txt":
191+ rc, out = cmd(tool.exec_cmd())
192+ out = tool.header.replace("$PWD", os.getcwd()) + out
193+ elif output_type == "json":
194+ if tool._stderr:
195+ rc, out = cmd(tool.exec_cmd_json())
196+ else:
197+ rc, out = cmd(tool.exec_cmd_json(), False)
198+ else:
199+ error(f"Unknown output_type ({output_type}) while running {tool.name})")
200+ # TODO: gosec exits 1 if no go files and actual output not saved
201+ if rc != 0:
202+ # note: above loop uses error instead of warn
203+ warn(f"Attempting to run {tool.name} exited with: {rc}")
204+ write_file(out_fn, out)
205+ msg(f"Static analysis ({tool.name}): {out_fn_rel}")
206+
207+ rc, summary = cmd(tool.summary_cmd(out_fn))
208+ # strip any trailing newline
209+ if summary[-1] == "\n":
210+ summary = summary[:-1]
211+ details[tool.name] = summary
212+
213
214 def audit_code(audit_dir, details, disable_coverity=False):
215 '''Audit code'''
216@@ -581,26 +650,9 @@ def audit_code(audit_dir, details, disable_coverity=False):
217 msg("Code audit (%s): %s" % (i, out_fn_rel))
218
219 for tool in static_analysis_tools:
220- out_fn = os.path.join(audit_dir, tool.name.lower() + ".txt")
221- out_fn_rel = '/'.join(out_fn.split('/')[-2:])
222- if os.path.exists(out_fn):
223- warn("Skipping %s. '%s' already exists" % (tool.name, out_fn))
224- else:
225- rc, out = cmd(tool.exec_cmd())
226- if rc != 0:
227- # note: above loop uses error instead of warn
228- warn(f"Attempting to run {tool.name} exited with: {rc}")
229- # TODO: gosec exits 1 if no go files and actual output not saved
230- out = tool.header.replace("$PWD", os.getcwd()) + out
231- write_file(out_fn, out)
232- msg(f"Static analysis ({tool.name}): {out_fn_rel}")
233-
234- rc, summary = cmd(tool.summary_cmd(out_fn))
235- # strip any trailing newline
236- if summary[-1] == "\n":
237- summary = summary[:-1]
238- details[tool.name] = summary
239-
240+ run_static_analysis(tool, "txt")
241+ if tool._cmd_json:
242+ run_static_analysis(tool, "json")
243
244 if not disable_coverity:
245 # perform coverity analysis if the directory exists (assume in this

Subscribers

People subscribed via source and target branches