Merge pull request #1387 from pikasTech/fix/1385-wbc003-findings

修复 Web 哨兵 WBC-003 遮盖 analyzer findings
This commit is contained in:
Lyon
2026-07-01 17:06:29 +08:00
committed by GitHub
3 changed files with 232 additions and 20 deletions
@@ -318,10 +318,14 @@ export function runSentinelQuickVerify(state: SentinelCicdState, reason: string,
const controlFindings = quickVerifyControlFindings(null, promptIndex, turnSummary, traceFrame);
const artifactSummaryRecord = record(artifactSummary);
const artifactFindings = Array.isArray(artifactSummaryRecord.findings) ? artifactSummaryRecord.findings.map(record) : [];
const visibilityFindings = quickVerifyAnalysisVisibilityFindings(analysis, artifactSummary);
const cleanupStep = stopQuickVerifyObserver(state, observerId, "observe-stop-after-terminal");
const cleanupFindings = quickVerifyCleanupFindings(cleanupStep);
const findings = mergeFindingRecords(
mergeFindingRecords(artifactFindings, controlFindings),
mergeFindingRecords(
mergeFindingRecords(artifactFindings, controlFindings),
visibilityFindings,
),
cleanupFindings,
);
const blockingFindings = findings.filter(isQuickVerifyBlockingFinding);
@@ -352,7 +356,7 @@ export function runSentinelQuickVerify(state: SentinelCicdState, reason: string,
steps: [...steps, cleanupStep],
analysis: artifactSummary,
views: {
summary: { renderedText: renderQuickVerifySummary({ runId, scenarioId, observerId, artifactSummary, steps, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl") }) },
summary: { renderedText: renderQuickVerifySummary({ runId, scenarioId, observerId, artifactSummary, findings, findingCount: findings.length, steps, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl") }) },
"auth-session-switch-summary": { renderedText: renderAuthSessionSwitchQuickVerifySummary({ runId, scenarioId, observerId, artifactSummary, steps, findings, accountEnv: accountEnv.summary, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl") }) },
"turn-summary": { renderedText: typeof turnSummary.renderedText === "string" ? turnSummary.renderedText : null, ok: turnSummary.ok },
"trace-frame": { renderedText: typeof traceFrame.renderedText === "string" ? traceFrame.renderedText : null, ok: traceFrame.ok },
@@ -513,7 +517,11 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: {
const controlFindings = quickVerifyControlFindings(input.failure, input.promptIndex, turnSummary, traceFrame);
const artifactSummaryRecord = record(artifactSummary);
const artifactFindings = Array.isArray(artifactSummaryRecord.findings) ? artifactSummaryRecord.findings.map(record) : [];
const findings = mergeFindingRecords(artifactFindings, controlFindings);
const visibilityFindings = quickVerifyAnalysisVisibilityFindings(analysis, artifactSummary);
const findings = mergeFindingRecords(
mergeFindingRecords(artifactFindings, controlFindings),
visibilityFindings,
);
const blockingFindings = findings.filter(isQuickVerifyBlockingFinding);
const recoveredWaitFailure = durableBusinessTurn
&& isRecoverableQuickVerifyWaitFailure(input.failure)
@@ -539,7 +547,7 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: {
steps: [...input.steps, ...cleanupSteps],
analysis: artifactSummary,
views: {
summary: { renderedText: renderQuickVerifySummary({ runId: input.runId, scenarioId: input.scenarioId, observerId: input.observerId, artifactSummary, steps: input.steps, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl") }) },
summary: { renderedText: renderQuickVerifySummary({ runId: input.runId, scenarioId: input.scenarioId, observerId: input.observerId, artifactSummary, findings, findingCount: findings.length, steps: input.steps, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl") }) },
"auth-session-switch-summary": { renderedText: renderAuthSessionSwitchQuickVerifySummary({ runId: input.runId, scenarioId: input.scenarioId, observerId: input.observerId, artifactSummary, steps: input.steps, findings, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl") }) },
"turn-summary": { renderedText: typeof turnSummary.renderedText === "string" ? turnSummary.renderedText : null, ok: turnSummary.ok },
"trace-frame": { renderedText: typeof traceFrame.renderedText === "string" ? traceFrame.renderedText : null, ok: traceFrame.ok },
@@ -580,6 +588,26 @@ function quickVerifyCleanupFindings(cleanupStep: Record<string, unknown>): Recor
}];
}
function quickVerifyAnalysisVisibilityFindings(analysis: ChildCliResult, artifactSummary: Record<string, unknown>): Record<string, unknown>[] {
if (analysis.ok !== true) return [];
if (stringAtNullable(artifactSummary, "reportJsonSha256") !== null) return [];
const result = record(analysis.result);
const stdout = stringAtNullable(result, "stdoutPreview") ?? stringAtNullable(result, "stdoutTail");
const reportPathMatch = stdout === null ? null : stdout.match(/analysis\/report\.(?:json|md)/u);
return [{
id: "quick-verify-analysis-summary-unreadable",
severity: "red",
count: 1,
summary: "quick verify analyze exited successfully, but sentinel could not read analysis/report.json before recording the run.",
rootCause: "The report index would otherwise contain only control blockers such as WBC-003 and hide analyzer red/amber findings.",
rootCauseStatus: "confirmed",
rootCauseConfidence: "high",
evidenceSummary: `analyze ok=true reportJsonSha256=missing stdoutMentionsReport=${reportPathMatch === null ? "false" : "true"}`,
nextAction: "Read stateDir/analysis/report.json again or rehydrate the report from stateDir before trusting WBC-003 as the only visible finding.",
valuesRedacted: true,
}];
}
function callSentinelService(state: SentinelCicdState, method: "GET" | "POST", pathWithQuery: string, body: Record<string, unknown> | null, timeoutSeconds: number): Record<string, unknown> {
const namespace = stringAt(state.runtime, "namespace");
const serviceName = stringAt(state.runtime, "serviceName");
@@ -1540,6 +1568,7 @@ function isQuickVerifyBlockingFinding(item: Record<string, unknown>): boolean {
if (id === "observer-command-failed") return observerCommandFailureBlocks(item);
return [
"quick-verify-no-business-turn",
"quick-verify-analysis-summary-unreadable",
"quick-verify-command-sequence-failed",
"quick-verify-observer-start-failed",
"quick-verify-account-secret-missing",
@@ -1798,12 +1827,17 @@ function compactCommandWithTail(result: CommandResult): CompactCommandResult & {
function renderQuickVerifySummary(input: Record<string, unknown>): string {
const artifact = record(input.artifactSummary);
const findings = Array.isArray(artifact.findings) ? artifact.findings.map(record).slice(0, 8) : [];
const findings = Array.isArray(input.findings)
? input.findings.map(record).slice(0, 8)
: Array.isArray(artifact.findings)
? artifact.findings.map(record).slice(0, 8)
: [];
const findingCount = numberAtNullable(input, "findingCount") ?? numberAtNullable(artifact, "findingCount") ?? findings.length;
return [
"Web Probe Sentinel Quick Verify",
"=======================================================",
`run=${input.runId ?? "-"} scenario=${input.scenarioId ?? "-"} observer=${input.observerId ?? "-"}`,
`report=${artifact.reportJsonSha256 ?? "-"} artifacts=${artifact.artifactCount ?? "-"} findings=${artifact.findingCount ?? findings.length}`,
`report=${artifact.reportJsonSha256 ?? "-"} artifacts=${artifact.artifactCount ?? "-"} findings=${findingCount}`,
`publicOrigin=${input.publicOrigin ?? "-"}`,
"",
"Findings",
+65 -2
View File
@@ -242,6 +242,11 @@ export function runSentinelReport(state: SentinelCicdState, options: Extract<Web
const payload = compactSentinelReportRawPayload(state, body, report, artifactSummary);
return rendered(report.ok && body.ok !== false, command, renderSentinelReportSummary(payload, state));
}
if (options.view === "findings") {
const artifactSummary = readSentinelReportArtifactSummary(state, body, Math.min(options.timeoutSeconds, 55));
const payload = compactSentinelReportRawPayload(state, body, report, artifactSummary);
return rendered(report.ok && body.ok !== false, command, renderSentinelReportFindings(payload));
}
const renderedText = typeof body.renderedText === "string" ? body.renderedText : renderReportResult({ command, node: state.spec.nodeId, lane: state.spec.lane, report, valuesRedacted: true });
return rendered(report.ok && body.ok !== false, command, renderedText);
}
@@ -256,6 +261,10 @@ function compactSentinelReportRawPayload(
const artifact = record(artifactSummary);
const findings = Array.isArray(body.findings) ? body.findings.map(record) : [];
const artifactFindings = Array.isArray(artifact.findings) ? artifact.findings.map(record) : [];
const visibleFindings = mergeSentinelReportFindings(findings, artifactFindings);
const storedFindingCount = numberAtNullable(run, "finding_count") ?? numberAtNullable(run, "findingCount") ?? findings.length;
const artifactFindingCount = numberAtNullable(artifact, "findingCount") ?? artifactFindings.length;
const visibleFindingCount = Math.max(storedFindingCount, artifactFindingCount, visibleFindings.length);
const rootCauseSignalFindings = artifactFindings
.filter((item) => Object.keys(record(item.rootCauseSignals)).length > 0)
.slice(0, 8)
@@ -279,7 +288,9 @@ function compactSentinelReportRawPayload(
observerId: run.observer_id ?? run.observerId ?? null,
stateDir: run.state_dir ?? run.stateDir ?? null,
reportJsonSha256: run.report_json_sha256 ?? run.reportJsonSha256 ?? artifact.reportJsonSha256 ?? null,
findingCount: run.finding_count ?? run.findingCount ?? findings.length,
findingCount: visibleFindingCount,
storedFindingCount,
artifactFindingCount,
artifactCount: run.artifact_count ?? run.artifactCount ?? artifact.artifactCount ?? null,
durationMinutes: run.durationMinutes ?? run.runDurationMinutes ?? record(run.timing).durationMinutes ?? null,
runDurationMinutes: run.runDurationMinutes ?? run.durationMinutes ?? record(run.timing).durationMinutes ?? null,
@@ -292,13 +303,15 @@ function compactSentinelReportRawPayload(
valuesRedacted: true,
},
summary: pickFields(record(body.summary), ["reason", "status", "businessStatus", "failure", "valuesRedacted"]),
findings: findings.slice(0, 12).map(compactSentinelReportFinding),
findings: visibleFindings.slice(0, 12).map(compactSentinelReportFinding),
artifactSummary: Object.keys(artifact).length === 0 ? null : {
ok: artifact.ok === true,
reportOk: artifact.reportOk === true ? true : artifact.reportOk === false ? false : null,
reportJsonPath: artifact.reportJsonPath ?? null,
reportJsonSha256: artifact.reportJsonSha256 ?? null,
reportMdSha256: artifact.reportMdSha256 ?? null,
findingCount: artifactFindingCount,
findings: artifactFindings.slice(0, 12).map(compactSentinelReportFinding),
screenshot: record(artifact.screenshot),
counts: record(artifact.counts),
analysisWindow: compactSentinelAnalysisWindow(artifact.analysisWindow),
@@ -315,6 +328,27 @@ function compactSentinelReportRawPayload(
};
}
function mergeSentinelReportFindings(primary: readonly Record<string, unknown>[], artifact: readonly Record<string, unknown>[]): Record<string, unknown>[] {
const merged: Record<string, unknown>[] = [];
const seen = new Set<string>();
for (const item of [...primary, ...artifact]) {
const id = sentinelReportFindingIdentity(item);
const key = `${id ?? JSON.stringify(item).slice(0, 160)}:${stringAtNullable(item, "severity") ?? stringAtNullable(item, "level") ?? ""}`;
if (seen.has(key)) continue;
seen.add(key);
merged.push(item);
}
return merged;
}
function sentinelReportFindingIdentity(item: Record<string, unknown>): string | null {
return stringAtNullable(item, "finding_id")
?? stringAtNullable(item, "findingId")
?? stringAtNullable(item, "id")
?? stringAtNullable(item, "kind")
?? stringAtNullable(item, "code");
}
function compactSentinelReportFinding(value: Record<string, unknown>): Record<string, unknown> {
const result: Record<string, unknown> = {
id: stringAtNullable(value, "finding_id") ?? stringAtNullable(value, "findingId") ?? stringAtNullable(value, "id") ?? stringAtNullable(value, "kind") ?? stringAtNullable(value, "code"),
@@ -415,6 +449,35 @@ function renderSentinelReportSummary(payload: Record<string, unknown>, state: Se
].join("\n");
}
function renderSentinelReportFindings(payload: Record<string, unknown>): string {
const run = record(payload.run);
const artifactSummary = record(payload.artifactSummary);
const findings = Array.isArray(payload.findings) ? payload.findings.map(record) : [];
const reportSha = stringAtNullable(run, "reportJsonSha256") ?? stringAtNullable(artifactSummary, "reportJsonSha256");
return [
"Web Probe Sentinel Findings",
"=======================================================",
`run=${run.id ?? "-"} report=${reportSha ?? "-"} findings=${run.findingCount ?? findings.length}`,
findings.length === 0 ? "-" : findings.map(formatSentinelReportFindingLine).join("\n"),
].join("\n");
}
function formatSentinelReportFindingLine(item: Record<string, unknown>): string {
const check = record(item.check);
const code = stringAtNullable(check, "code") ?? stringAtNullable(item, "id") ?? "-";
const title = stringAtNullable(check, "titleZh") ?? "";
const summary = reportText(item.summary, 180) ?? "";
const rootCause = reportText(item.rootCause, 180);
const evidence = reportText(item.evidenceSummary, 180);
const nextAction = reportText(item.nextAction, 180);
return [
`${item.severity ?? "-"} ${code} ${title} count=${item.count ?? "-"} ${summary}`.trim(),
rootCause === null ? null : `rootCause=${rootCause}`,
evidence === null ? null : `evidence=${evidence}`,
nextAction === null ? null : `next=${nextAction}`,
].filter((part): part is string => part !== null && part.length > 0).join(" | ");
}
function compactRootCauseSignals(value: unknown): Record<string, unknown> | null {
const item = record(value);
const keys = [
+127 -12
View File
@@ -916,8 +916,10 @@ function dashboardRunDetail(config: WebProbeSentinelServiceConfig, db: Database,
const row = readRunRow(config, db, runId);
if (row === null) return { ok: false, error: "run-not-found", runId, valuesRedacted: true };
const stored = readMetadata(db, `run.report.${runId}`) ?? {};
const findings = findingsForRun(config, db, runId, dashboardMaxPageSize(config));
const artifactSummary = readAnalysisArtifactSummaryForRun(config, row, dashboardMaxPageSize(config));
const findings = visibleFindingsForRun(config, db, row, dashboardMaxPageSize(config), artifactSummary);
const views = record(stored.views);
const reportJsonSha256 = stringOrNull(row.report_json_sha256) ?? stringOrNull(artifactSummary.reportJsonSha256);
return {
ok: true,
contractVersion: DASHBOARD_CONTRACT_VERSION,
@@ -929,7 +931,8 @@ function dashboardRunDetail(config: WebProbeSentinelServiceConfig, db: Database,
viewsAvailable: Object.keys(views),
artifacts: {
artifactCount: numberOr(row.artifact_count, 0),
reportJsonSha256: stringOrNull(row.report_json_sha256),
reportJsonSha256,
analysisReport: artifactSummary,
screenshot: compactArtifactRef(stored.screenshot),
publicOrigin: stringOrNull(stored.publicOrigin),
},
@@ -1264,14 +1267,17 @@ function shortDashboardText(value: string, max: number): string {
function dashboardRunSummary(config: WebProbeSentinelServiceConfig, db: Database, row: Record<string, unknown>): Record<string, unknown> {
const id = stringOrNull(row.id);
const severityCounts = id === null ? {} : severityCountsForRun(config, db, id);
const artifactSummary = readAnalysisArtifactSummaryForRun(config, row, 50);
const findings = visibleFindingsForRun(config, db, row, 50, artifactSummary);
const severityCounts = checkLevelCounts(config, findings);
const maxSeverity = maxSeverityFromCounts(severityCounts);
const findingTypeCount = numberOr(row.finding_count, 0);
const findingTypeCount = visibleFindingTypeCount(row, findings, artifactSummary);
const findingSampleCount = Object.values(severityCounts).reduce((sum, value) => sum + numberOr(value, 0), 0);
const findingAlertSampleCount = alertSeveritySampleCount(severityCounts);
const stored = id === null ? {} : record(readMetadata(db, `run.report.${id}`));
const scenarioId = stringOrNull(row.scenario_id);
const timing = dashboardRunTiming(config, row, stored);
const reportJsonSha256 = stringOrNull(row.report_json_sha256) ?? stringOrNull(artifactSummary.reportJsonSha256);
return {
id,
runId: id,
@@ -1285,8 +1291,8 @@ function dashboardRunSummary(config: WebProbeSentinelServiceConfig, db: Database
observerId: stringOrNull(row.observer_id),
state_dir: stringOrNull(row.state_dir),
stateDir: stringOrNull(row.state_dir),
report_json_sha256: stringOrNull(row.report_json_sha256),
reportJsonSha256: stringOrNull(row.report_json_sha256),
report_json_sha256: reportJsonSha256,
reportJsonSha256,
finding_count: findingTypeCount,
findingCount: findingTypeCount,
findingTypeCount,
@@ -1511,6 +1517,98 @@ function findingsForRun(config: WebProbeSentinelServiceConfig, db: Database, run
return rows.map((row) => enrichFindingRowWithStoredDetail(config, db, runId, row));
}
function visibleFindingsForRun(
config: WebProbeSentinelServiceConfig,
db: Database,
row: Record<string, unknown>,
limit: number,
artifactSummary: Record<string, unknown> = readAnalysisArtifactSummaryForRun(config, row, limit),
): readonly Record<string, unknown>[] {
const runId = stringOrNull(row.id);
const storedFindings = runId === null ? [] : findingsForRun(config, db, runId, limit);
const artifactFindings = arrayRecords(artifactSummary.findings)
.slice(0, limit)
.map((item) => enrichFindingWithCheck(config, compactStoredFinding(item)));
return mergeVisibleFindings(storedFindings, artifactFindings, limit);
}
function mergeVisibleFindings(primary: readonly Record<string, unknown>[], artifact: readonly Record<string, unknown>[], limit: number): readonly Record<string, unknown>[] {
const merged: Record<string, unknown>[] = [];
const seen = new Set<string>();
for (const item of [...primary, ...artifact]) {
const key = `${findingIdentity(item) ?? JSON.stringify(item).slice(0, 160)}:${stringOrNull(item.severity) ?? stringOrNull(item.level) ?? ""}`;
if (seen.has(key)) continue;
seen.add(key);
merged.push(item);
if (merged.length >= limit) break;
}
return merged;
}
function findingIdentity(item: Record<string, unknown>): string | null {
return stringOrNull(item.finding_id)
?? stringOrNull(item.findingId)
?? stringOrNull(item.id)
?? stringOrNull(item.kind)
?? stringOrNull(item.code);
}
function visibleFindingTypeCount(row: Record<string, unknown>, findings: readonly Record<string, unknown>[], artifactSummary: Record<string, unknown>): number {
return Math.max(
numberOr(row.finding_count, 0),
numberOr(artifactSummary.findingCount, 0),
findings.length,
);
}
function readAnalysisArtifactSummaryForRun(config: WebProbeSentinelServiceConfig, row: Record<string, unknown>, limit: number): Record<string, unknown> {
const stateDir = stringOrNull(row.state_dir) ?? stringOrNull(row.stateDir);
if (stateDir === null) return { ok: false, reason: "state-dir-missing", findings: [], valuesRedacted: true };
if (!isSafeDashboardStateDir(stateDir)) return { ok: false, reason: "unsafe-state-dir", stateDir, findings: [], valuesRedacted: true };
const reportJsonPath = join(config.stateRoot, stateDir, "analysis", "report.json");
const reportMdPath = join(config.stateRoot, stateDir, "analysis", "report.md");
const jsonBuffer = readFileBuffer(reportJsonPath);
const mdBuffer = readFileBuffer(reportMdPath);
let parsed: unknown = null;
if (jsonBuffer !== null) {
try {
parsed = JSON.parse(jsonBuffer.toString("utf8")) as unknown;
} catch {
parsed = null;
}
}
const report = record(parsed);
const reportFindings = arrayRecords(report.findings);
const archiveFindings = arrayRecords(record(report.archiveSummary).redFindings);
const findings = (reportFindings.length > 0 ? reportFindings : archiveFindings)
.slice(0, limit)
.map(compactStoredFinding);
return {
ok: parsed !== null,
reason: parsed === null ? "analysis-report-json-missing-or-invalid" : null,
stateDir,
reportJsonPath,
reportJsonSha256: sha256Buffer(jsonBuffer),
reportMdSha256: sha256Buffer(mdBuffer),
findingCount: numberOr(report.findingCount, findings.length),
findings,
counts: record(report.counts),
valuesRedacted: true,
};
}
function readFileBuffer(path: string): Buffer | null {
try {
return readFileSync(path);
} catch {
return null;
}
}
function sha256Buffer(value: Buffer | null): string | null {
return value === null ? null : `sha256:${createHash("sha256").update(value).digest("hex")}`;
}
function enrichFindingRowWithStoredDetail(config: WebProbeSentinelServiceConfig, db: Database, runId: string, row: Record<string, unknown>): Record<string, unknown> {
const detail = storedFindingDetailForRow(db, row, runId);
return enrichFindingWithCheck(config, {
@@ -1984,10 +2082,17 @@ function reportRunView(config: WebProbeSentinelServiceConfig, db: Database, view
const selectedRunId = stringOrNull(row.id);
if (selectedRunId === null) return { ok: false, error: "report-run-id-missing", view, valuesRedacted: true };
const stored = readMetadata(db, `run.report.${selectedRunId}`) ?? {};
const findings = findingsForRun(config, db, selectedRunId, 50);
const artifactSummary = readAnalysisArtifactSummaryForRun(config, row, 50);
const findings = visibleFindingsForRun(config, db, row, 50, artifactSummary);
const views = record(stored.views);
const storedView = record(views[view]);
const renderedText = view === "findings" ? renderStoredFindings(row, findings) : typeof storedView.renderedText === "string" ? storedView.renderedText : view === "summary" ? renderStoredSummary(config, row, stored, findings) : null;
const renderedText = view === "findings"
? renderStoredFindings(row, findings, artifactSummary)
: view === "summary"
? renderStoredSummary(config, row, stored, findings, artifactSummary)
: typeof storedView.renderedText === "string"
? storedView.renderedText
: null;
if (renderedText === null) {
return { ok: false, error: "report-view-not-indexed", runId: selectedRunId, view, availableViews: Object.keys(views), valuesRedacted: true };
}
@@ -1997,6 +2102,7 @@ function reportRunView(config: WebProbeSentinelServiceConfig, db: Database, view
run: dashboardRunSummary(config, db, row),
summary: record(stored.summary),
findings,
artifactSummary,
renderedText,
valuesRedacted: true,
};
@@ -2033,15 +2139,23 @@ function formatStoredFindingTiming(item: Record<string, unknown>): string | null
return `timing=${pieces.join(" ")}`;
}
function renderStoredSummary(config: WebProbeSentinelServiceConfig, row: Record<string, unknown>, stored: Record<string, unknown>, findings: readonly Record<string, unknown>[]): string {
function renderStoredSummary(
config: WebProbeSentinelServiceConfig,
row: Record<string, unknown>,
stored: Record<string, unknown>,
findings: readonly Record<string, unknown>[],
artifactSummary: Record<string, unknown>,
): string {
const summary = record(stored.summary);
const timing = dashboardRunTiming(config, row, stored);
const reportSha = stringOrNull(row.report_json_sha256) ?? stringOrNull(artifactSummary.reportJsonSha256);
const findingCount = visibleFindingTypeCount(row, findings, artifactSummary);
return [
"Web Probe Sentinel Report",
"=======================================================",
`run=${stringOrNull(row.id) ?? "-"} scenario=${stringOrNull(row.scenario_id) ?? "-"} status=${stringOrNull(row.status) ?? "-"}`,
`observer=${stringOrNull(row.observer_id) ?? "-"} stateDir=${stringOrNull(row.state_dir) ?? "-"}`,
`report=${stringOrNull(row.report_json_sha256) ?? "-"} artifacts=${String(row.artifact_count ?? 0)} findings=${String(row.finding_count ?? findings.length)}`,
`report=${reportSha ?? "-"} artifacts=${String(row.artifact_count ?? 0)} findings=${String(findingCount)}`,
`timing=${formatRunTimingSummary(timing)}`,
`publicOrigin=${stringOrNull(stored.publicOrigin) ?? "-"}`,
`analysisWindow=${formatStoredAnalysisWindow(summary.analysisWindow)}`,
@@ -2051,11 +2165,12 @@ function renderStoredSummary(config: WebProbeSentinelServiceConfig, row: Record<
].join("\n");
}
function renderStoredFindings(row: Record<string, unknown>, findings: readonly Record<string, unknown>[]): string {
function renderStoredFindings(row: Record<string, unknown>, findings: readonly Record<string, unknown>[], artifactSummary: Record<string, unknown>): string {
const reportSha = stringOrNull(row.report_json_sha256) ?? stringOrNull(artifactSummary.reportJsonSha256);
return [
"Web Probe Sentinel Findings",
"=======================================================",
`run=${stringOrNull(row.id) ?? "-"} report=${stringOrNull(row.report_json_sha256) ?? "-"}`,
`run=${stringOrNull(row.id) ?? "-"} report=${reportSha ?? "-"} findings=${String(visibleFindingTypeCount(row, findings, artifactSummary))}`,
findings.length === 0 ? "-" : findings.map(formatStoredFindingLine).join("\n"),
].join("\n");
}