large image scans no longer fail when the scanner log rotates
This commit is contained in:
3 files changed
+89
-5
No files matched your search
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Pure, side-effect-free helpers for handling scanner (Trivy/Grype) stdout. Kept out of
|
||||
* scanner.ts so they can be unit-tested without pulling in docker/db (which break bun test).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bound a large scanner output for logging: keep a head and tail (default 2 KB each) with a
|
||||
* marker for the elided middle, so a failed parse of a multi-MB report never floods stderr /
|
||||
* the log shipper (#1496). Small outputs pass through unchanged.
|
||||
*/
|
||||
export function truncateForLog(output: string, keep = 2048): string {
|
||||
if (output.length <= keep * 2) return output;
|
||||
const elided = output.length - keep * 2;
|
||||
return `${output.slice(0, keep)}\n...[${elided} bytes elided]...\n${output.slice(-keep)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify a scanner output that failed to parse as JSON, to surface a useful error.
|
||||
* - 'truncated': a large buffer that does NOT start with `{` is a mid-document fragment - the
|
||||
* head of the JSON was lost to container-log rotation (#1496).
|
||||
* - 'cli-error': a short non-JSON output is a genuine error message printed by the scanner CLI.
|
||||
* - 'unknown': anything else (e.g. starts with `{` but is still unparseable).
|
||||
*/
|
||||
export function classifyUnparseableOutput(output: string): 'truncated' | 'cli-error' | 'unknown' {
|
||||
const trimmed = output.trimStart();
|
||||
if (output.length > 100_000 && !trimmed.startsWith('{')) return 'truncated';
|
||||
const firstLine = trimmed.split('\n', 1)[0].trim();
|
||||
if (firstLine && !firstLine.startsWith('{')) return 'cli-error';
|
||||
return 'unknown';
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { getEnvironment, getEnvSetting, getSetting } from './db';
|
||||
import { sendEventNotification } from './notifications';
|
||||
import { detectRemoteSocketPath } from './scanner-socket-detect';
|
||||
import { truncateForLog, classifyUnparseableOutput } from './scanner-output-core';
|
||||
import {
|
||||
getHostDockerSocket,
|
||||
getHostDataDir,
|
||||
@@ -546,11 +547,17 @@ function parseTrivyOutput(output: string): { vulnerabilities: Vulnerability[]; s
|
||||
console.error('[Trivy] Failed to parse output:', errorMsg);
|
||||
console.error('[Trivy] Output length:', output.length);
|
||||
console.error('[Trivy] First 32 bytes (hex):', Buffer.from(output.slice(0, 32)).toString('hex'));
|
||||
console.error('[Trivy] Full output:', output);
|
||||
// Check if output looks like an error message from trivy
|
||||
const firstLine = output.split('\n')[0].trim();
|
||||
if (firstLine && !firstLine.startsWith('{')) {
|
||||
throw new Error(`Scanner output error: ${firstLine}`);
|
||||
// Never dump the whole report: a large truncated report is ~13 MB and this ran
|
||||
// on every failed scan (#1496). Log only a bounded head + tail for diagnosis.
|
||||
console.error('[Trivy] Output (head/tail):', truncateForLog(output));
|
||||
|
||||
const kind = classifyUnparseableOutput(output);
|
||||
if (kind === 'truncated') {
|
||||
// The head of the JSON was lost to container-log rotation the helper now disables (#1496).
|
||||
throw new Error('Scanner output truncated (container log rotation?) - the JSON report was too large to read back in full');
|
||||
}
|
||||
if (kind === 'cli-error') {
|
||||
throw new Error(`Scanner output error: ${output.trimStart().split('\n', 1)[0].trim()}`);
|
||||
}
|
||||
throw new Error('Failed to parse scanner output - ensure CLI args include "--format json"');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { truncateForLog, classifyUnparseableOutput } from '../../src/lib/server/scanner-output-core';
|
||||
|
||||
describe('truncateForLog', () => {
|
||||
test('short output passes through unchanged', () => {
|
||||
const s = 'a small error line';
|
||||
expect(truncateForLog(s)).toBe(s);
|
||||
});
|
||||
test('output at exactly 2*keep is unchanged', () => {
|
||||
const s = 'x'.repeat(20);
|
||||
expect(truncateForLog(s, 10)).toBe(s);
|
||||
});
|
||||
test('large output is bounded to head + tail with an elision marker', () => {
|
||||
const s = 'H'.repeat(50) + 'M'.repeat(1000) + 'T'.repeat(50);
|
||||
const out = truncateForLog(s, 10);
|
||||
expect(out.startsWith('HHHHHHHHHH')).toBe(true);
|
||||
expect(out.endsWith('TTTTTTTTTT')).toBe(true);
|
||||
expect(out).toContain('bytes elided');
|
||||
// only head(10) + tail(10) + marker - far shorter than the 1100-char input
|
||||
expect(out.length).toBeLessThan(100);
|
||||
// the elided count is the middle
|
||||
expect(out).toContain(`[${s.length - 20} bytes elided]`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyUnparseableOutput', () => {
|
||||
test('a large buffer not starting with { is truncated (log rotation)', () => {
|
||||
// mimic the #1496 fragment: starts mid-JSON (leading spaces then a value)
|
||||
const frag = ' "https://ubuntu.com/security/notices/USN-1"\n' + 'x'.repeat(200_000);
|
||||
expect(classifyUnparseableOutput(frag)).toBe('truncated');
|
||||
});
|
||||
test('a short non-JSON line is a CLI error message', () => {
|
||||
expect(classifyUnparseableOutput('FATAL failed to download vulnerability DB')).toBe('cli-error');
|
||||
});
|
||||
test('leading whitespace before { is not misread as an error', () => {
|
||||
// a genuine (if unparseable-later) JSON doc that does start with { after trim
|
||||
expect(classifyUnparseableOutput(' \n { "SchemaVersion": 2 ')).toBe('unknown');
|
||||
});
|
||||
test('a large fragment that happens to start with { is not "truncated"', () => {
|
||||
// only a NON-brace lead signals a lost head; a brace-led large blob is "unknown"
|
||||
const big = '{' + 'x'.repeat(200_000);
|
||||
expect(classifyUnparseableOutput(big)).toBe('unknown');
|
||||
});
|
||||
test('empty output is unknown, not a cli-error', () => {
|
||||
expect(classifyUnparseableOutput('')).toBe('unknown');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user