Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 33 additions & 11 deletions sdk/typescript/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2659,7 +2659,14 @@ async function runScan(
).length;
const incomplete = result.coverage.completeness !== "complete";
progress?.stage("Scan complete");
printScanSummary(result, progress, errorOutput);
printScanSummary(
result,
progress,
errorOutput,
progress?.interactive === true &&
dependencies.environment["NO_COLOR"] === undefined &&
dependencies.environment["TERM"] !== "dumb",
);
if (incomplete) {
errorOutput.write(
threshold === undefined
Expand Down Expand Up @@ -2730,7 +2737,10 @@ function printScanSummary(
result: ScanResult,
progress: Progress | null,
errorOutput: Writable,
color: boolean,
): void {
const paint = (value: string, code: number | string): string =>
color ? `\u001B[${code}m${value}\u001B[0m` : value;
const severities = new Map<SeverityLevel, number>();
for (const finding of result.findings.findings) {
severities.set(
Expand All @@ -2744,9 +2754,6 @@ function printScanSummary(
})
.filter((value): value is string => value !== null)
.join(", ");
errorOutput.write(
`codex-security: Findings: ${result.findings.findings.length}${severitySummary === "" ? "" : ` (${severitySummary})`}. Coverage: ${result.coverage.completeness}.\n`,
);

const started = Date.parse(result.manifest.scan.startedAt);
const completed = Date.parse(result.manifest.scan.completedAt);
Expand All @@ -2756,22 +2763,37 @@ function printScanSummary(
completed >= started
? Math.floor((completed - started) / 1_000)
: progress?.elapsedSeconds ?? 0;
errorOutput.write(`codex-security: Elapsed: ${elapsed}s.\n`);
const duration =
elapsed < 60
? `${elapsed}s`
: `${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
const findingCount = result.findings.findings.length;
const findingColor =
findingCount === 0
? 32
: severities.has("critical") || severities.has("high")
? 31
: severities.has("medium")
? 33
: 36;
errorOutput.write(
`\n ${paint("REPORT", "1;36")} ${paint(cliErrorMessage(result.reportPath), 4)}\n\n` +
` ${paint("FINDINGS", 1)} ${paint(`${findingCount}${severitySummary === "" ? "" : ` (${severitySummary})`}`, findingColor)}\n` +
` ${paint("COVERAGE", 1)} ${result.coverage.completeness}\n` +
` ${paint("ELAPSED", 1)} ${duration}\n`,
);

const tokenSummary = formatTokenUsage(result.turnResult.usage);
if (tokenSummary !== null) {
errorOutput.write(`codex-security: Tokens: ${tokenSummary}.\n`);
errorOutput.write(` ${paint("TOKENS", 1)} ${tokenSummary}\n`);
}
if (result.cost !== null) {
errorOutput.write(
`codex-security: Estimated cost: ${formatUsd(result.cost.estimatedUsd)} USD.\n`,
` ${paint("COST", 1)} ${formatUsd(result.cost.estimatedUsd)}\n`,
);
}
errorOutput.write(
`codex-security: Report: ${cliErrorMessage(result.reportPath)}\n`,
);
errorOutput.write(
`codex-security: Results: ${cliErrorMessage(result.scanDir)}\n`,
` ${paint("RESULTS", 1)} ${cliErrorMessage(result.scanDir)}\n`,
);
}

Expand Down
67 changes: 52 additions & 15 deletions sdk/typescript/tests-ts/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2094,6 +2094,7 @@ describe("CLI", () => {
cached_input_tokens: 200,
output_tokens: 30,
});
result.manifest.scan.completedAt = "2026-01-01T00:06:37Z";

expect(
await main(
Expand All @@ -2106,18 +2107,53 @@ describe("CLI", () => {
expect(stdout.text()).toBe("");
expect(stderr.text()).toContain("Scan complete");
expect(stderr.text()).toContain(
"Findings: 1 (1 high). Coverage: complete.",
);
expect(stderr.text()).toContain("Elapsed: 1s.");
expect(stderr.text()).toContain(
"Tokens: 1,250 input, 200 cached, 30 output.",
[
` REPORT ${result.reportPath}`,
"",
" FINDINGS 1 (1 high)",
" COVERAGE complete",
" ELAPSED 6m 37s",
" TOKENS 1,250 input, 200 cached, 30 output",
" COST $0.00625",
" RESULTS /tmp/scan",
].join("\n"),
);
expect(stderr.text()).toContain("Estimated cost: $0.00625 USD.");
expect(stderr.text()).toContain(`Report: ${result.reportPath}`);
expect(stderr.text()).toContain("Results: /tmp/scan");
expect(stderr.text()).not.toContain("codex-security:");
expect(stderr.text()).not.toContain("Next:");
});

test("styles terminal scan summaries and respects color settings", async () => {
for (const [environment, color] of [
[{}, true],
[{ NO_COLOR: "1" }, false],
[{ TERM: "dumb" }, false],
] as const) {
const stdout = capture();
const stderr = capture(true);
const result = fakeResult(["medium"]);

expect(
await main(
["scan"],
stdout.stream,
stderr.stream,
dependencies({ environment, result }),
),
).toBe(0);

if (color) {
expect(stderr.text()).toContain("\u001B[1;36mREPORT\u001B[0m");
expect(stderr.text()).toContain(
`\u001B[4m${result.reportPath}\u001B[0m`,
);
expect(stderr.text()).toContain("\u001B[33m1 (1 medium)\u001B[0m");
} else {
expect(stderr.text()).toContain(` REPORT ${result.reportPath}`);
expect(stderr.text()).not.toContain("\u001B[1;36mREPORT");
}
}
});

test("prints complete scan results only when explicitly requested", async () => {
for (const [arguments_, marker] of [
[["--json"], '"manifest"'],
Expand Down Expand Up @@ -2337,15 +2373,16 @@ describe("CLI", () => {
).toBe(0);
expect(JSON.parse(stdout.text())).toEqual(result.toJSON());
expect(stderr.text()).toContain(
"Findings: 4 (1 critical, 2 high, 1 informational). Coverage: complete.",
"FINDINGS 4 (1 critical, 2 high, 1 informational)",
);
expect(stderr.text()).toContain("Elapsed: 1s.");
expect(stderr.text()).toContain("COVERAGE complete");
expect(stderr.text()).toContain("ELAPSED 1s");
expect(stderr.text()).toContain(
"Tokens: 1,250 input, 200 cached, 30 output.",
"TOKENS 1,250 input, 200 cached, 30 output",
);
expect(stderr.text()).toContain("Estimated cost: $0.00625 USD.");
expect(stderr.text()).toContain(`Report: ${result.reportPath}`);
expect(stderr.text()).toContain("Results: /tmp/scan");
expect(stderr.text()).toContain("COST $0.00625");
expect(stderr.text()).toContain(`REPORT ${result.reportPath}`);
expect(stderr.text()).toContain("RESULTS /tmp/scan");
expect(stderr.text()).not.toContain("Next:");
});

Expand Down Expand Up @@ -2476,7 +2513,7 @@ describe("CLI", () => {
"Worker delegation unavailable during file review; continuing without delegated workers.",
);
expect(stdout.text()).toBe("");
expect(stderr.text()).toContain("Findings: 0. Coverage: complete.");
expect(stderr.text()).toContain("FINDINGS 0\n COVERAGE complete");
});

test("validates a dry run without starting a scan", async () => {
Expand Down
Loading