Skip to content

fix(scan report): render the default report as plain text instead of an object dump - #1469

Merged
John-David Dalton (jdalton) merged 1 commit into
mainfrom
jdalton/ask-302-pr-comment-output-readability
Aug 4, 2026
Merged

fix(scan report): render the default report as plain text instead of an object dump#1469
John-David Dalton (jdalton) merged 1 commit into
mainfrom
jdalton/ask-302-pr-comment-output-readability

Conversation

@jdalton

@jdalton John-David Dalton (jdalton) commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

When you run socket scan report in a CI/CD pipeline, the report is currently printed as a raw JavaScript object dump. Nested Map objects show up literally, like Map(1) { 'npm' => Map(1) { ... } }, which is hard to read in a log viewer and looks nothing like the report the Socket GitHub App posts on a pull request.

This change replaces that dump with a plain-text report that has labelled sections and space-padded columns, so a reader can scan it top to bottom in a build log.

Only the default text output changes. The --json, --markdown, and --short outputs are untouched, so anything that parses this command keeps working exactly as before.

Refs ASK-302.

What the output looked like before, and what it looks like now — a raw object dump became a labelled report

Before, the default text path called logger.dir(), which is Node's object inspector. For a report with two alerts it printed something close to this:

{
  healthy: false,
  orgSlug: 'acme-inc',
  scanId: 'a1b2c3d4',
  alerts: Map(1) {
    'npm' => Map(2) {
      'acme-widget' => Map(1) { '1.2.3' => { type: 'envVars', policy: 'error', ... } }
    }
  },
  options: { fold: 'none', reportLevel: 'error' }
}

After, the same report prints as:

Socket scan policy report

Health status
  VIOLATES one or more policies set to the "error" level.

Settings
  Organization:            acme-inc
  Scan ID:                 a1b2c3d4
  Alert folding:           none
  Minimum policy level:    error
  Include license alerts:  no

Alerts
  2 alerts with a policy set to at least "error".

  POLICY  ALERT TYPE  PACKAGE           INTRODUCED BY  MANIFEST FILE
  ------  ----------  ----------------  -------------  -------------
  error   envVars     acme-widget       1.2.3          package.json
    https://socket.dev/npm/package/acme-widget
  warn    telemetry   acme-plugin-node  4.0.1          package.json
    https://socket.dev/npm/package/acme-plugin-node
Why the output is plain ASCII with no colour — a CI log is not a terminal

A build log is a stream of monospaced lines that may be piped to a file, replayed later without a terminal attached, or ingested by a log aggregator. Three decisions follow from that:

Decision Reason
No colour codes An escape code that a log viewer does not interpret shows up as literal noise such as [32m.
No box-drawing characters Characters outside plain ASCII can render as replacement boxes depending on the viewer's font and encoding.
Each alert's URL on its own indented line URLs are long. Putting one in a table column would stretch every row far past a readable width.

The columns are padded with ordinary spaces, so the table still lines up vertically wherever a monospaced font is used, which is every CI log viewer.

How the change is structured — one shared flattener now feeds both renderers

logger.dir appeared exactly once in the whole CLI, at this one call site. Every other output path already builds a formatted string and passes it to logger.log, so this brings the last outlier in line with the rest of the codebase.

The new toPlainTextReport sits alongside the existing toJsonReport and toMarkdownReport. All three now read their rows from one new flattenReportAlerts helper, which walks the nested ecosystem, package, and version maps and returns one flat row per alert. Previously the markdown renderer did that walk inline. Sharing it means the text and markdown reports cannot drift apart in what they show.

Two small helpers do the layout: formatLabelledPairs pads the Label: prefixes in the settings block, and formatAlertTable pads the alert columns and places each URL on its own line.

Verification — 10 new tests, each one proven able to fail

The new tests assert specific properties rather than snapshotting the whole blob, so each assertion states what the contract actually is: no ANSI escapes, no characters outside printable ASCII, no raw object dump, labelled settings, health status in words, aligned columns, and a bounded line width.

Every new assertion was mutation-checked. I broke the implementation, confirmed a named test went red, then restored it:

Mutation Test that went red
Wrapped the report title in an ANSI colour code emits no ANSI escape codes and emits only printable ASCII, so no glyph can mangle in a log viewer
Removed the column padding from formatAlertTable aligns the alert columns so the table scans vertically
Restored the old logger.dir call at the call site should handle successful result with healthy report

The alignment test caught a real problem in its own first draft. My initial version asserted only that one character position was not a space, which still passed with the padding removed. That is a test that cannot fail for a real reason, so I rewrote it to assert that each cell begins at exactly the offset its column header begins at. The rewritten version fails when the padding is removed, as shown above.

Ran:

  • pnpm --filter @socketsecurity/cli run test:unit test/unit/commands/scan/ — exit 1. 828 passed, 6 failed. All 6 failures are in perform-reachability-analysis.test.mts and perform-reachability-analysis-coana.test.mts, and I confirmed the identical 6 fail on a clean default branch before making any change, so they are pre-existing and unrelated to this work.
  • pnpm --filter @socketsecurity/cli run test:unit test/unit/commands/scan/output-scan-report-text.test.mts — exit 0. 10 passed.
  • pnpm --filter @socketsecurity/cli run test:unit test/unit/commands/scan/output-scan-report.test.mts — exit 0. 12 passed.
  • pnpm run lint — exit 0, "Lint passed" on the changed files.
  • pnpm --filter @socketsecurity/cli run type — exit 0.
  • pnpm run build:cli — exit 0.

Did not run:

  • The full pnpm run check --all suite as a gate. I did run it once on a clean default branch to record a baseline, where it exits 1 with 7 failing checks that have nothing to do with this change. I compared against that baseline rather than treating it as a pass or fail signal.
  • The end-to-end and integration suites, which need network access and a published build.

Note

Low Risk
CLI presentation-only change for default text output; structured outputs and exit-code behavior are preserved and covered by updated and new unit tests.

Overview
The default socket scan report text output no longer uses logger.dir() on nested Map structures. It now prints a plain-text policy report via toPlainTextReport: health status, aligned settings labels, and a space-padded alert table with URLs on separate indented lines (ASCII-only, no ANSI).

Shared flattening: flattenReportAlerts walks ecosystem/package/version maps into ReportAlertRow rows; markdown (toMarkdownReport) now uses the same helper instead of duplicating the walk, so text and markdown stay aligned.

--json, --markdown, and --short paths are unchanged. New unit tests cover the plain-text contract (no object dumps, column alignment, line width).

Reviewed by Cursor Bugbot for commit fb24f7a. Configure here.

@jdalton
John-David Dalton (jdalton) force-pushed the jdalton/ask-302-pr-comment-output-readability branch from 04a0c12 to fb24f7a Compare August 3, 2026 19:09
@jdalton

Copy link
Copy Markdown
Collaborator Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit fb24f7a. Configure here.

@jdalton
John-David Dalton (jdalton) force-pushed the jdalton/ask-302-pr-comment-output-readability branch from fb24f7a to a020e66 Compare August 4, 2026 14:45
@jdalton

Copy link
Copy Markdown
Collaborator Author

[agent] Both red checks are pre-existing on main, not caused by this branch.

Main at 0bd8b9e, which is the exact base of this PR, fails the identical set in run https://github.com/SocketDev/socket-cli/actions/runs/30843921672 — the same 4 checks, the same 129 warnings and 18 errors, and the same 1 test.

Check, 4 failures, all repo-wide:

  • lint: 18 max-comment-block-lines errors across 10 files. The only one in a file either open PR touches is cmd-manifest-cdxgen.mts, and that is the pre-existing cdxgen --help dump which sits verbatim on main; PR 1470 only shifted its line number from 44 to 49.
  • commits-have-no-ai-attribution: the CI checkout is a shallow clone, so the scan refuses to report a pass it cannot verify. Wants fetch-depth 0, or --unpushed, in the workflow.
  • check-registrations-resolve: scripts/fleet/check/tests-read-canonical-sources.mts is a check file that no runner invokes.
  • claude-md-repo-section-is-a-bullet-index: CLAUDE.md:151 is a 230-char bullet, over the 200 cap.

Test, 1 failure, and this one is a real bug worth its own fix:

packages/cli/test/unit/commands/scan/perform-reachability-analysis-coana.test.mts reads the options bag at the wrong argument index. spawnCoanaDlx is declared as (args, options, spawnExtra) at src/util/dlx/spawn-coana.mts:53, and perform-reachability-analysis.mts:288 calls it with two arguments, so mock.calls[0][2] is undefined and reading opts.stdio throws a TypeError. Lines 315 and 327 should read mock.calls[0][1]. Run locally, both tests in that describe block fail for this reason; CI reports one because the two land in different shards. So the test has never actually asserted the stdio routing, it threw instead. Introduced by bd3f4a9, the port of #1371.

None of that is fixable from inside this PR without unrelated churn, so I left it alone.

The default `socket scan report` output went through `logger.dir()`,
Node's object inspector. In a CI log that printed a raw JavaScript
object with nested maps rendered literally, like

  alerts: Map(1) { 'npm' => Map(2) { 'acme-widget' => Map(1) { ... } } }

which is hard to read in a log viewer and looks nothing like the report
the Socket GitHub App posts on a pull request.

The default text path now renders a plain-text report with labelled
sections and space-padded columns. It stays ASCII-only with no colour
and no box-drawing, because a build log may be piped to a file,
replayed without a TTY, or ingested by a log aggregator, where an
uninterpreted escape code shows up as literal noise and a non-ASCII
glyph can render as a replacement box. Each alert's URL goes on its own
indented line so a long URL cannot stretch every row past a readable
width.

`logger.dir` appeared exactly once in the whole CLI, at this call site;
every other output path already builds a string and passes it to
`logger.log`, so this brings the last outlier into line.

The new `toPlainTextReport` sits beside `toJsonReport` and
`toMarkdownReport`, and all three now read their rows from one new
`flattenReportAlerts` helper. The markdown renderer previously did that
nested-map walk inline, so sharing it means the two formats cannot
drift apart in what they show.

Only the default text output changes. The `--json`, `--markdown`, and
`--short` outputs are untouched, so anything parsing this command keeps
working as before.

Refs ASK-302.
@jdalton
John-David Dalton (jdalton) force-pushed the jdalton/ask-302-pr-comment-output-readability branch from a020e66 to 831fd78 Compare August 4, 2026 14:59
@jdalton
John-David Dalton (jdalton) merged commit 03ad68a into main Aug 4, 2026
4 of 6 checks passed
@jdalton
John-David Dalton (jdalton) deleted the jdalton/ask-302-pr-comment-output-readability branch August 4, 2026 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant