fix(reports): sanitize OpenClaw target paths
This commit is contained in:
parent
e9e4b6704c
commit
332706b014
@ -2,6 +2,11 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
|
||||
- Sanitize absolute target OpenClaw paths from generated report artifacts and JSON CLI output.
|
||||
- Normalize the dependency-install inspector finding title to use isolated-workspace wording.
|
||||
|
||||
## 0.3.3 - 2026-04-28
|
||||
|
||||
### Fixed
|
||||
|
||||
@ -170,6 +170,7 @@ export {
|
||||
classifyCompatRecordCoverage,
|
||||
renderMarkdownReport,
|
||||
renderTextSummary,
|
||||
sanitizeReportArtifact,
|
||||
writeCompatibilityReport,
|
||||
writeReport,
|
||||
} from "./report.js";
|
||||
|
||||
@ -3,6 +3,7 @@ import path from "node:path";
|
||||
import {
|
||||
loadPluginConfig,
|
||||
renderTextSummary,
|
||||
sanitizeReportArtifact,
|
||||
runPluginCheck,
|
||||
} from "./index.js";
|
||||
import {
|
||||
@ -90,7 +91,7 @@ async function runCheck(commandArgs) {
|
||||
});
|
||||
|
||||
if (json) {
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
console.log(JSON.stringify(sanitizeReportArtifact(report), null, 2));
|
||||
} else {
|
||||
console.log(renderTextSummary(report, { artifacts: paths }));
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { renderPaddedMarkdownTable } from "./artifacts.js";
|
||||
import { sanitizeReportArtifact } from "./report-sanitizer.js";
|
||||
|
||||
const defaultSeverityLabels = {
|
||||
P0: "P0",
|
||||
@ -8,6 +9,7 @@ const defaultSeverityLabels = {
|
||||
};
|
||||
|
||||
export function renderCompatibilityMarkdownReport(report, options = {}) {
|
||||
report = sanitizeReportArtifact(report, options);
|
||||
return [
|
||||
`# ${options.title ?? "OpenClaw Plugin Compatibility Report"}`,
|
||||
"",
|
||||
@ -127,6 +129,7 @@ export function renderCompatibilityMarkdownReport(report, options = {}) {
|
||||
}
|
||||
|
||||
export function renderCompatibilityIssuesReport(report, options = {}) {
|
||||
report = sanitizeReportArtifact(report, options);
|
||||
return [
|
||||
`# ${options.title ?? "OpenClaw Plugin Issue Findings"}`,
|
||||
"",
|
||||
|
||||
@ -53,11 +53,13 @@ export const staticInspection = Object.freeze({
|
||||
export const reports = Object.freeze({
|
||||
renderMarkdown: reportApi.renderMarkdownReport,
|
||||
renderTextSummary: pluginApi.renderTextSummary,
|
||||
sanitizeArtifact: reportApi.sanitizeReportArtifact,
|
||||
write: reportApi.writeReport,
|
||||
issueId: issuesApi.issueId,
|
||||
classifyIssueFinding: issuesApi.classifyIssueFinding,
|
||||
knownIssueCodes: issuesApi.knownIssueCodes,
|
||||
openClawTargetPathCandidates: openClawTargetApi.openClawTargetPathCandidates,
|
||||
readOpenClawTargetSurface: openClawTargetApi.readOpenClawTargetSurface,
|
||||
});
|
||||
|
||||
export const contracts = Object.freeze({
|
||||
@ -200,7 +202,7 @@ export {
|
||||
} from "./import-loop-profile.js";
|
||||
export { classifyIssueFinding, issueId, knownIssueCodes } from "./issues.js";
|
||||
export { inspectFixtureSet, inspectPlugin, inspectSourceText } from "./inspector.js";
|
||||
export { openClawTargetPathCandidates } from "./openclaw-target.js";
|
||||
export { openClawTargetPathCandidates, readOpenClawTargetSurface } from "./openclaw-target.js";
|
||||
export {
|
||||
buildProfileDiff,
|
||||
defaultProfileDiffOptions,
|
||||
@ -216,7 +218,7 @@ export {
|
||||
validateRefDiff,
|
||||
writeRefDiff,
|
||||
} from "./ref-diff.js";
|
||||
export { renderMarkdownReport, writeReport } from "./report.js";
|
||||
export { renderMarkdownReport, sanitizeReportArtifact, writeReport } from "./report.js";
|
||||
export {
|
||||
buildRuntimeProfile,
|
||||
defaultRuntimeProfileCommands,
|
||||
|
||||
@ -119,7 +119,7 @@ export const issueMetadataByCode = {
|
||||
severity: "P2",
|
||||
owner: "inspector",
|
||||
decision: "inspector-follow-up",
|
||||
title: "cold import requires isolated dependency installation",
|
||||
title: "cold import requires dependency installation in an isolated workspace",
|
||||
},
|
||||
"package-entrypoint-missing": {
|
||||
severity: "P1",
|
||||
|
||||
42
src/report-sanitizer.js
Normal file
42
src/report-sanitizer.js
Normal file
@ -0,0 +1,42 @@
|
||||
import path from "node:path";
|
||||
|
||||
export function sanitizeReportArtifact(report, options = {}) {
|
||||
const sensitivePaths = sensitiveOpenClawPaths(report);
|
||||
if (sensitivePaths.length === 0) {
|
||||
return report;
|
||||
}
|
||||
const placeholder = options.openclawPathPlaceholder ?? "<OPENCLAW_PATH>";
|
||||
return sanitizeValue(report, sensitivePaths, placeholder);
|
||||
}
|
||||
|
||||
function sensitiveOpenClawPaths(report) {
|
||||
const targetOpenClaw = report?.targetOpenClaw;
|
||||
return unique(
|
||||
[targetOpenClaw?.configuredPath, ...(targetOpenClaw?.searchedPaths ?? [])]
|
||||
.filter((value) => typeof value === "string" && isAbsolutePath(value))
|
||||
.sort((left, right) => right.length - left.length),
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeValue(value, sensitivePaths, placeholder) {
|
||||
if (typeof value === "string") {
|
||||
return sensitivePaths.reduce((result, sensitivePath) => result.replaceAll(sensitivePath, placeholder), value);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => sanitizeValue(item, sensitivePaths, placeholder));
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, entryValue]) => [key, sanitizeValue(entryValue, sensitivePaths, placeholder)]),
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function isAbsolutePath(value) {
|
||||
return path.isAbsolute(value) || /^[A-Za-z]:[\\/]/u.test(value) || value.startsWith("\\\\");
|
||||
}
|
||||
|
||||
function unique(values) {
|
||||
return [...new Set(values)];
|
||||
}
|
||||
@ -4,6 +4,7 @@ import { renderCompatibilityIssuesReport, renderCompatibilityMarkdownReport } fr
|
||||
import { buildContractProbes } from "./contract-probes.js";
|
||||
import { classifyCompatibilityFixture } from "./fixture-summary.js";
|
||||
import { buildIssues, summarizeIssueClasses } from "./issues.js";
|
||||
import { sanitizeReportArtifact } from "./report-sanitizer.js";
|
||||
|
||||
export function buildReport({ config, inspections, failures = [], generatedAt = "deterministic" }) {
|
||||
const inspectionById = new Map(inspections.map((inspection) => [inspection.id, inspection]));
|
||||
@ -233,12 +234,13 @@ export async function writeReport(report, options = {}) {
|
||||
const basename = options.basename ?? "plugin-inspector-report";
|
||||
const jsonPath = path.join(outDir, `${basename}.json`);
|
||||
const markdownPath = path.join(outDir, `${basename}.md`);
|
||||
const artifactReport = sanitizeReportArtifact(report, options);
|
||||
|
||||
return writeJsonMarkdownArtifacts({
|
||||
jsonPath,
|
||||
markdownPath,
|
||||
json: report,
|
||||
markdown: renderMarkdownReport(report),
|
||||
json: artifactReport,
|
||||
markdown: renderMarkdownReport(artifactReport),
|
||||
check: options.check,
|
||||
});
|
||||
}
|
||||
@ -249,6 +251,7 @@ export async function writeCompatibilityReport(report, options = {}) {
|
||||
const jsonPath = options.jsonPath ?? path.join(outDir, `${basename}.json`);
|
||||
const markdownPath = options.markdownPath ?? path.join(outDir, `${basename}.md`);
|
||||
const issuesPath = options.issuesPath ?? path.join(outDir, options.issuesBasename ?? "plugin-inspector-issues.md");
|
||||
const artifactReport = sanitizeReportArtifact(report, options);
|
||||
const markdownOptions = compatibilityRenderOptions(options, {
|
||||
title: options.markdownTitle ?? options.title,
|
||||
...options.markdownOptions,
|
||||
@ -260,14 +263,16 @@ export async function writeCompatibilityReport(report, options = {}) {
|
||||
|
||||
return writeArtifacts(
|
||||
[
|
||||
{ name: "jsonPath", path: jsonPath, json: report },
|
||||
{ name: "markdownPath", path: markdownPath, markdown: renderCompatibilityMarkdownReport(report, markdownOptions) },
|
||||
{ name: "issuesPath", path: issuesPath, markdown: renderCompatibilityIssuesReport(report, issuesOptions) },
|
||||
{ name: "jsonPath", path: jsonPath, json: artifactReport },
|
||||
{ name: "markdownPath", path: markdownPath, markdown: renderCompatibilityMarkdownReport(artifactReport, markdownOptions) },
|
||||
{ name: "issuesPath", path: issuesPath, markdown: renderCompatibilityIssuesReport(artifactReport, issuesOptions) },
|
||||
],
|
||||
{ check: options.check },
|
||||
);
|
||||
}
|
||||
|
||||
export { sanitizeReportArtifact };
|
||||
|
||||
function compatibilityRenderOptions(options, overrides) {
|
||||
const renderOptions = {
|
||||
formatEvidence: options.formatEvidence,
|
||||
|
||||
@ -111,6 +111,8 @@ test("public API exposes grouped facades for common workflows", () => {
|
||||
assert.equal(fixtureSuites.runReport, runFixtureSetReport);
|
||||
assert.equal(staticInspection.inspectSourceText, inspectSourceText);
|
||||
assert.equal(reports.renderMarkdown, renderMarkdownReport);
|
||||
assert.equal(typeof reports.sanitizeArtifact, "function");
|
||||
assert.equal(typeof reports.readOpenClawTargetSurface, "function");
|
||||
assert.equal(contracts.buildCapture, buildContractCapture);
|
||||
assert.equal(contracts.validateCoverage, validateContractCoverage);
|
||||
assert.equal(ci.buildSummary, buildCiSummary);
|
||||
|
||||
@ -87,6 +87,25 @@ test("check command can target a plugin root and use runtime aliases", async ()
|
||||
assert.equal(capture.summary.capturedCount, 1);
|
||||
});
|
||||
|
||||
test("check command sanitizes absolute OpenClaw paths in JSON output and artifacts", async () => {
|
||||
const rootDir = await createCliPluginRoot("plugin-inspector-cli-sanitize-");
|
||||
const openclawPath = await createTargetOpenClaw(rootDir);
|
||||
const cliPath = path.resolve("src/cli.js");
|
||||
|
||||
const { stdout } = await execFileAsync(
|
||||
process.execPath,
|
||||
[cliPath, "check", "--out", "reports", "--openclaw", openclawPath, "--json"],
|
||||
{ cwd: rootDir },
|
||||
);
|
||||
const output = JSON.parse(stdout);
|
||||
const artifact = JSON.parse(await readFile(path.join(rootDir, "reports", "plugin-inspector-report.json"), "utf8"));
|
||||
|
||||
assert.equal(output.targetOpenClaw.configuredPath, "<OPENCLAW_PATH>");
|
||||
assert.equal(artifact.targetOpenClaw.configuredPath, "<OPENCLAW_PATH>");
|
||||
assert.deepEqual(artifact.targetOpenClaw.searchedPaths, ["<OPENCLAW_PATH>"]);
|
||||
assert.doesNotMatch(stdout, new RegExp(escapeRegExp(openclawPath)));
|
||||
});
|
||||
|
||||
test("inspect command runs from a plugin root and can write CI outputs", async () => {
|
||||
const rootDir = await createCliPluginRoot("plugin-inspector-cli-inspect-");
|
||||
const cliPath = path.resolve("src/cli.js");
|
||||
@ -318,3 +337,18 @@ async function createCliPluginRoot(prefix) {
|
||||
);
|
||||
return rootDir;
|
||||
}
|
||||
|
||||
async function createTargetOpenClaw(rootDir) {
|
||||
const openclawPath = path.join(rootDir, "target-openclaw");
|
||||
await mkdir(path.join(openclawPath, "src/plugins/compat"), { recursive: true });
|
||||
await writeFile(
|
||||
path.join(openclawPath, "src/plugins/compat/registry.ts"),
|
||||
'export const records = [{ code: "legacy-root-sdk-import", status: "deprecated" }];\n',
|
||||
"utf8",
|
||||
);
|
||||
return openclawPath;
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
buildSarifReport,
|
||||
buildCompatibilityReport,
|
||||
buildCompatibilityFixtureReport,
|
||||
buildIssues,
|
||||
classifyCompatibilityFixture,
|
||||
classifyCompatRecordCoverage,
|
||||
classifyPackageContracts,
|
||||
@ -20,6 +21,7 @@ import {
|
||||
renderMarkdownTable,
|
||||
renderTextSummary,
|
||||
writeArtifacts,
|
||||
writeCompatibilityReport,
|
||||
writeCiOutputArtifacts,
|
||||
writeReport,
|
||||
} from "../src/advanced.js";
|
||||
@ -185,6 +187,91 @@ test("compatibility report renderer supports issue metadata and evidence links",
|
||||
assert.match(issues, /\[linked\]\(plugins\/sample\/src\/index\.ts:1\)/);
|
||||
});
|
||||
|
||||
test("compatibility report artifacts sanitize absolute OpenClaw target paths", async () => {
|
||||
const outDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-sanitized-report-"));
|
||||
const absoluteOpenClawPath = path.join(outDir, "openclaw");
|
||||
const report = {
|
||||
generatedAt: "test",
|
||||
status: "pass",
|
||||
targetOpenClaw: {
|
||||
status: "ok",
|
||||
configuredPath: absoluteOpenClawPath,
|
||||
searchedPaths: [absoluteOpenClawPath],
|
||||
compatRecords: [],
|
||||
compatRecordStatuses: {},
|
||||
},
|
||||
summary: {
|
||||
fixtureCount: 1,
|
||||
highPriorityFixtures: 1,
|
||||
breakageCount: 0,
|
||||
warningCount: 0,
|
||||
suggestionCount: 0,
|
||||
decisionCount: 0,
|
||||
issueCount: 1,
|
||||
p0IssueCount: 0,
|
||||
p1IssueCount: 0,
|
||||
liveIssueCount: 0,
|
||||
liveP0IssueCount: 0,
|
||||
compatGapCount: 0,
|
||||
deprecationWarningCount: 0,
|
||||
inspectorGapCount: 1,
|
||||
upstreamIssueCount: 0,
|
||||
fixtureRegressionCount: 0,
|
||||
contractProbeCount: 0,
|
||||
},
|
||||
fixtures: [
|
||||
{
|
||||
id: "sample-plugin",
|
||||
priority: "high",
|
||||
seams: ["native-tool"],
|
||||
hooks: [],
|
||||
registrations: [],
|
||||
manifestContracts: [],
|
||||
},
|
||||
],
|
||||
breakages: [],
|
||||
warnings: [],
|
||||
suggestions: [],
|
||||
issues: [
|
||||
{
|
||||
fixture: "sample-plugin",
|
||||
code: "package-dependency-install-required",
|
||||
issueClass: "inspector-gap",
|
||||
decision: "inspector-follow-up",
|
||||
severity: "P2",
|
||||
title: `sample-plugin: path ${absoluteOpenClawPath}`,
|
||||
status: "open",
|
||||
compatStatus: "none",
|
||||
live: false,
|
||||
evidence: [absoluteOpenClawPath],
|
||||
},
|
||||
],
|
||||
contractProbes: [],
|
||||
logs: [],
|
||||
decisions: [],
|
||||
};
|
||||
|
||||
const markdown = renderCompatibilityMarkdownReport(report);
|
||||
const issues = renderCompatibilityIssuesReport(report);
|
||||
const paths = await writeCompatibilityReport(report, {
|
||||
jsonPath: path.join(outDir, "report.json"),
|
||||
markdownPath: path.join(outDir, "report.md"),
|
||||
issuesPath: path.join(outDir, "issues.md"),
|
||||
});
|
||||
const artifact = JSON.parse(await readFile(paths.jsonPath, "utf8"));
|
||||
|
||||
assert.equal(report.targetOpenClaw.configuredPath, absoluteOpenClawPath);
|
||||
assert.equal(artifact.targetOpenClaw.configuredPath, "<OPENCLAW_PATH>");
|
||||
assert.deepEqual(artifact.targetOpenClaw.searchedPaths, ["<OPENCLAW_PATH>"]);
|
||||
assert.equal(artifact.issues[0].evidence[0], "<OPENCLAW_PATH>");
|
||||
assert.equal(artifact.issues[0].title, "sample-plugin: path <OPENCLAW_PATH>");
|
||||
assert.doesNotMatch(markdown, new RegExp(escapeRegExp(absoluteOpenClawPath)));
|
||||
assert.doesNotMatch(issues, new RegExp(escapeRegExp(absoluteOpenClawPath)));
|
||||
assert.match(markdown, /<OPENCLAW_PATH>/);
|
||||
assert.match(await readFile(paths.markdownPath, "utf8"), /<OPENCLAW_PATH>/);
|
||||
assert.match(await readFile(paths.issuesPath, "utf8"), /<OPENCLAW_PATH>/);
|
||||
});
|
||||
|
||||
test("compatibility report assembly classifies fixtures, issues, probes, and compat records", async () => {
|
||||
const report = await buildCompatibilityReport({
|
||||
generatedAt: "test",
|
||||
@ -401,6 +488,18 @@ test("package contract classifier reports install and entrypoint blockers", () =
|
||||
assert.ok(result.suggestions.some((finding) => finding.code === "package-build-artifact-entrypoint"));
|
||||
assert.ok(result.suggestions.some((finding) => finding.code === "package-dependency-install-required"));
|
||||
assert.ok(result.decisions.some((decision) => decision.seam === "cold-import"));
|
||||
|
||||
const issues = buildIssues({
|
||||
suggestions: result.suggestions,
|
||||
targetOpenClaw: { status: "ok", compatRecordStatuses: {} },
|
||||
});
|
||||
assert.ok(
|
||||
issues.some(
|
||||
(issue) =>
|
||||
issue.code === "package-dependency-install-required" &&
|
||||
issue.title === "fixture: cold import requires dependency installation in an isolated workspace",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("target OpenClaw coverage classifier reports missing public surface", () => {
|
||||
@ -585,3 +684,7 @@ test("markdown table helper supports padded empty-table reports", () => {
|
||||
);
|
||||
assert.equal(renderMarkdownTable([], ["Name"], { empty: "_none_" }), "_none_");
|
||||
});
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user