Summary
When --level is set to anything above info, JSON output is discarded entirely and the program prints only an empty line. The exit code is still correct, but nothing parseable reaches STDOUT.
Version
- ssh-audit
v3.9.1-dev (commit 111399e), also present in the current release 3.9.0
- Python:
3.14.6
Steps to reproduce
$ ./ssh-audit.py --level=warn -j localhost:22
Expected behaviour
A JSON document on STDOUT. --level either filters the structure or is ignored, but the document is emitted either way.
Actual behaviour
A single empty line. Exit code is still set correctly (2 for warnings, 3 for failures), so the audit itself runs fine, only the output is lost.
Both of these work as expected:
$ ./ssh-audit.py -j localhost:22 # JSON output, fine
$ ./ssh-audit.py --level=warn localhost:22 # filtered text output, fine
Root cause
The JSON document is emitted through the severity-filtered output buffer, at info level.
src/ssh_audit/ssh_audit.py:
if aconf.json:
out.reset()
# Build & write the JSON struct.
out.info(json.dumps(build_struct(...), indent=..., sort_keys=True))
OutputBuffer._print() in src/ssh_audit/outputbuffer.py drops anything below the configured minimum level:
if (always_print is False) and (self.get_level(level) < self.__level):
return
--level=warn sets out.level = 'warn' (__level == 1) at ssh_audit.py:1154. The JSON is written at info (0), so 0 < 1 and the entire document is dropped. The buffer is then empty and out.write() executes print(""), which is the blank line.
This reproduces without needing a server:
>>> import sys
>>> sys.path.insert(0, 'src')
>>> from ssh_audit.outputbuffer import OutputBuffer
>>> out = OutputBuffer()
>>> out.level = 'warn'
>>> out.info('{"json": "payload"}')
<ssh_audit.outputbuffer.OutputBuffer object at 0x7f03e3b90c20>
>>> print(repr(out.get_buffer()))
''
Suggested fix
out.info() already accepts always_print for this case.
--- a/src/ssh_audit/ssh_audit.py
+++ b/src/ssh_audit/ssh_audit.py
@@ audit()
- out.info(json.dumps(build_struct(...), indent=4 if aconf.json_print_indent else None, sort_keys=True))
+ out.info(json.dumps(build_struct(...), indent=4 if aconf.json_print_indent else None, sort_keys=True), always_print=True)
@@ gex modulus output
- out.info(json.dumps(json_struct, indent=4 if aconf.json_print_indent else None, sort_keys=True))
+ out.info(json.dumps(json_struct, indent=4 if aconf.json_print_indent else None, sort_keys=True), always_print=True)
This fix is needed regardless of what is decided about the design question below: filtering inside build_struct() alone would not help, since the emission would still pass through out.info() and still be dropped by _print().
Rejecting --level/-l + --json/-j at the argument parser would also resolve the silent-failure aspect, but it could break existing scripts.
Design question: should --level filter the JSON?
Once the output is no longer swallowed, there are two reasonable behaviours.
Option A: --level remains a no-op for JSON. The document always contains the complete inventory; consumers filter downstream:
./ssh-audit.py -j localhost:22 | jq '{kex, key, enc, mac} | map_values(map(select(.notes.fail or .notes.warn)))'
This has zero implementation cost beyond the fix above, however leaves a silently inert flag (a one-line note on STDERR (so jq pipelines aren't broken) would make that discoverable).
Option B: filter build_struct() output by level. --level means the same thing in every output mode.
Consistent flag semantics (expected when using --level) and does not require use of external tools.
If Option B is preferred, one detail worth deciding: whether recommendations (which carries its own critical / warning / informational levels) should be filtered by the same threshold, or left intact.
I'm happy to submit a PR for whichever you prefer.
Summary
When
--levelis set to anything aboveinfo, JSON output is discarded entirely and the program prints only an empty line. The exit code is still correct, but nothing parseable reaches STDOUT.Version
v3.9.1-dev(commit111399e), also present in the current release3.9.03.14.6Steps to reproduce
Expected behaviour
A JSON document on STDOUT.
--leveleither filters the structure or is ignored, but the document is emitted either way.Actual behaviour
A single empty line. Exit code is still set correctly (
2for warnings,3for failures), so the audit itself runs fine, only the output is lost.Both of these work as expected:
Root cause
The JSON document is emitted through the severity-filtered output buffer, at
infolevel.src/ssh_audit/ssh_audit.py:OutputBuffer._print()insrc/ssh_audit/outputbuffer.pydrops anything below the configured minimum level:--level=warnsetsout.level = 'warn'(__level == 1) atssh_audit.py:1154. The JSON is written atinfo(0), so0 < 1and the entire document is dropped. The buffer is then empty andout.write()executesprint(""), which is the blank line.This reproduces without needing a server:
Suggested fix
out.info()already acceptsalways_printfor this case.This fix is needed regardless of what is decided about the design question below: filtering inside
build_struct()alone would not help, since the emission would still pass throughout.info()and still be dropped by_print().Rejecting
--level/-l+--json/-jat the argument parser would also resolve the silent-failure aspect, but it could break existing scripts.Design question: should
--levelfilter the JSON?Once the output is no longer swallowed, there are two reasonable behaviours.
Option A:
--levelremains a no-op for JSON. The document always contains the complete inventory; consumers filter downstream:This has zero implementation cost beyond the fix above, however leaves a silently inert flag (a one-line note on STDERR (so
jqpipelines aren't broken) would make that discoverable).Option B: filter
build_struct()output by level.--levelmeans the same thing in every output mode.Consistent flag semantics (expected when using
--level) and does not require use of external tools.If Option B is preferred, one detail worth deciding: whether
recommendations(which carries its owncritical/warning/informationallevels) should be filtered by the same threshold, or left intact.I'm happy to submit a PR for whichever you prefer.