Merge pull request #1387 from pikasTech/fix/1385-wbc003-findings
修复 Web 哨兵 WBC-003 遮盖 analyzer findings
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user