codex-plugin-doctor 1.24.0 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/core/validate-plugin.js +6 -4
- package/dist/domain/types.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/generic-mcp-doctor.js +8 -2
- package/dist/reporting/finding-fingerprint.d.ts +5 -0
- package/dist/reporting/finding-fingerprint.js +50 -0
- package/dist/reporting/render-markdown-report.js +5 -0
- package/dist/reporting/render-sarif-report.js +7 -0
- package/dist/reporting/render-text-report.js +5 -0
- package/dist/security/security-audit.js +11 -12
- package/dist/security/trust-score.js +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -314,7 +314,7 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
|
314
314
|
|
|
315
315
|
`init [path] --template ...` creates targeted starter packages. `skill-only` is the default minimal skill package, `mcp-stdio` adds a local stdio MCP config and mock server, `mcp-http` scaffolds a streamable HTTP MCP config, and `full-runtime` generates a stdio sample that passes the runtime protocol probes.
|
|
316
316
|
|
|
317
|
-
`security <path>` renders a focused package security scorecard. It reuses the existing package security findings, then adds deeper MCP command-surface checks for shell wrappers, encoded shell payloads, remote pipe-to-shell startup patterns, `cwd` values outside the plugin root, and plain HTTP URLs. Security findings include structured evidence where available, such as server names, config paths, env keys, arg flags, and escaped paths. Use `--json` for automation or `--scorecard` for a compact status view.
|
|
317
|
+
`security <path>` renders a focused package security scorecard. It reuses the existing package security findings, then adds deeper MCP command-surface checks for shell wrappers, encoded shell payloads, remote pipe-to-shell startup patterns, `cwd` values outside the plugin root, and plain HTTP URLs. Security findings include structured evidence where available, such as server names, config paths, env keys, arg flags, and escaped paths. Findings also include deterministic SHA-256 fingerprints derived from the rule ID and canonical evidence, with package paths normalized for cross-machine stability. Use `--json` for automation or `--scorecard` for a compact status view.
|
|
318
318
|
|
|
319
319
|
`compat --client claude-desktop` checks whether the MCP package can be added to the local Claude Desktop setup. On Windows it looks for `%APPDATA%\Claude\claude_desktop_config.json`; on macOS it looks for `~/Library/Application Support/Claude/claude_desktop_config.json`. A valid existing config returns `PASS`, a missing Claude Desktop install returns `WARN`, and a malformed local config returns `FAIL` so you do not add new servers into a broken config file. If the package server name already exists in Claude Desktop, the command returns `WARN` with the duplicate server name. Add `--install-preview` to print the JSON snippet that should be merged into `claude_desktop_config.json`; it does not modify files. Use `--apply --backup` only when you want the CLI to create a timestamped backup and merge the server config. Apply mode refuses to overwrite duplicate server names.
|
|
320
320
|
|
|
@@ -370,9 +370,9 @@ jobs:
|
|
|
370
370
|
runs-on: ubuntu-latest
|
|
371
371
|
steps:
|
|
372
372
|
- uses: actions/checkout@v5
|
|
373
|
-
- uses: Esquetta/CodexPluginDoctor@v1.
|
|
373
|
+
- uses: Esquetta/CodexPluginDoctor@v1.25.0
|
|
374
374
|
with:
|
|
375
|
-
version: "1.
|
|
375
|
+
version: "1.25.0"
|
|
376
376
|
path: .
|
|
377
377
|
runtime: "true"
|
|
378
378
|
policy: codex-publish
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { withFindingFingerprints } from "../reporting/finding-fingerprint.js";
|
|
3
4
|
import { discoverPackage } from "./discover-package.js";
|
|
4
5
|
import { probeRuntime } from "./runtime-probe.js";
|
|
5
6
|
function buildFailure(id, message, impact, suggestedFix, evidence) {
|
|
@@ -358,7 +359,7 @@ export async function validatePlugin(targetPath, options = {}) {
|
|
|
358
359
|
targetPath: rootPath,
|
|
359
360
|
status: "fail",
|
|
360
361
|
exitCode: 1,
|
|
361
|
-
findings: [await buildMissingManifestFailure(rootPath)]
|
|
362
|
+
findings: withFindingFingerprints([await buildMissingManifestFailure(rootPath)], rootPath)
|
|
362
363
|
};
|
|
363
364
|
}
|
|
364
365
|
const runtimeResult = options.runtime
|
|
@@ -373,8 +374,9 @@ export async function validatePlugin(targetPath, options = {}) {
|
|
|
373
374
|
...(await validateMcpConfig(discoveredPackage)),
|
|
374
375
|
...(runtimeResult ? runtimeResult.findings : [])
|
|
375
376
|
];
|
|
376
|
-
const
|
|
377
|
-
const
|
|
377
|
+
const fingerprintedFindings = withFindingFingerprints(findings, discoveredPackage.rootPath);
|
|
378
|
+
const hasFailures = fingerprintedFindings.some((finding) => finding.severity === "fail");
|
|
379
|
+
const hasWarnings = fingerprintedFindings.some((finding) => finding.severity === "warn");
|
|
378
380
|
if (!hasFailures && !hasWarnings) {
|
|
379
381
|
return {
|
|
380
382
|
targetPath: discoveredPackage.rootPath,
|
|
@@ -388,7 +390,7 @@ export async function validatePlugin(targetPath, options = {}) {
|
|
|
388
390
|
targetPath: discoveredPackage.rootPath,
|
|
389
391
|
status: hasFailures ? "fail" : "warn",
|
|
390
392
|
exitCode: hasFailures ? 1 : 0,
|
|
391
|
-
findings,
|
|
393
|
+
findings: fingerprintedFindings,
|
|
392
394
|
...(runtimeResult ? { runtimeScorecard: runtimeResult.scorecard } : {})
|
|
393
395
|
};
|
|
394
396
|
}
|
package/dist/domain/types.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CheckOptions, CheckResult } from "./domain/types.js";
|
|
2
|
+
export { buildFindingFingerprint, formatFindingFingerprintLine, withFindingFingerprint, withFindingFingerprints } from "./reporting/finding-fingerprint.js";
|
|
2
3
|
export { buildSecurityAudit, renderSecurityAuditJson, renderSecurityScorecard, type SecurityAudit } from "./security/security-audit.js";
|
|
3
4
|
export { buildTrustScore, renderTrustScore, renderTrustScoreJson, type BuildTrustScoreOptions, type TrustScoreReport } from "./security/trust-score.js";
|
|
4
5
|
export { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson, type DoctorSnapshot } from "./core/doctor-snapshot.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validatePlugin } from "./core/validate-plugin.js";
|
|
2
|
+
export { buildFindingFingerprint, formatFindingFingerprintLine, withFindingFingerprint, withFindingFingerprints } from "./reporting/finding-fingerprint.js";
|
|
2
3
|
export { buildSecurityAudit, renderSecurityAuditJson, renderSecurityScorecard } from "./security/security-audit.js";
|
|
3
4
|
export { buildTrustScore, renderTrustScore, renderTrustScoreJson } from "./security/trust-score.js";
|
|
4
5
|
export { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson } from "./core/doctor-snapshot.js";
|
|
@@ -2,6 +2,7 @@ import { stat } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { buildCompatibilityMatrix, readMcpConfigPath } from "../compatibility/compatibility-matrix.js";
|
|
4
4
|
import { readJsonFile } from "../core/read-json-file.js";
|
|
5
|
+
import { formatFindingFingerprintLine, withFindingFingerprints } from "../reporting/finding-fingerprint.js";
|
|
5
6
|
import { auditMcpServerConfig, buildSecurityAuditFromFindings } from "../security/security-audit.js";
|
|
6
7
|
function buildFinding(severity, id, message, impact, suggestedFix) {
|
|
7
8
|
return {
|
|
@@ -110,14 +111,15 @@ export async function buildGenericMcpDoctor(targetPath, environment = {}) {
|
|
|
110
111
|
const security = buildSecurityAuditFromFindings(rootPath, mcpConfigPath && parsedConfig !== null
|
|
111
112
|
? auditMcpServerConfig(rootPath, parsedConfig)
|
|
112
113
|
: []);
|
|
113
|
-
const
|
|
114
|
+
const fingerprintedStaticFindings = withFindingFingerprints(staticFindings, rootPath);
|
|
115
|
+
const status = mergeReportStatus(fingerprintedStaticFindings, security);
|
|
114
116
|
return {
|
|
115
117
|
targetPath: rootPath,
|
|
116
118
|
status,
|
|
117
119
|
exitCode: status === "fail" ? 1 : 0,
|
|
118
120
|
mcpConfigPath,
|
|
119
121
|
serverCount,
|
|
120
|
-
findings: [...
|
|
122
|
+
findings: [...fingerprintedStaticFindings, ...security.findings],
|
|
121
123
|
security,
|
|
122
124
|
compatibility
|
|
123
125
|
};
|
|
@@ -159,6 +161,10 @@ export function renderGenericMcpDoctor(report) {
|
|
|
159
161
|
lines.push(` Message: ${finding.message}`);
|
|
160
162
|
lines.push(` Impact: ${finding.impact}`);
|
|
161
163
|
lines.push(` Suggested fix: ${finding.suggestedFix}`);
|
|
164
|
+
const fingerprint = formatFindingFingerprintLine(finding);
|
|
165
|
+
if (fingerprint) {
|
|
166
|
+
lines.push(` Fingerprint: ${fingerprint}`);
|
|
167
|
+
}
|
|
162
168
|
}
|
|
163
169
|
};
|
|
164
170
|
appendFindings("Failures", failures, "x");
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Finding } from "../domain/types.js";
|
|
2
|
+
export declare function buildFindingFingerprint(finding: Finding, rootPath: string): string;
|
|
3
|
+
export declare function withFindingFingerprint(finding: Finding, rootPath: string): Finding;
|
|
4
|
+
export declare function withFindingFingerprints(findings: Finding[], rootPath: string): Finding[];
|
|
5
|
+
export declare function formatFindingFingerprintLine(finding: Finding): string | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const fingerprintVersion = 1;
|
|
4
|
+
function isPathEvidenceKey(key) {
|
|
5
|
+
return key.toLowerCase().endsWith("path");
|
|
6
|
+
}
|
|
7
|
+
function normalizePathEvidence(value, rootPath) {
|
|
8
|
+
const windowsPath = /^[a-z]:[\\/]/i.test(value);
|
|
9
|
+
const windowsRoot = /^[a-z]:[\\/]/i.test(rootPath);
|
|
10
|
+
if (windowsPath && windowsRoot) {
|
|
11
|
+
return path.win32.relative(rootPath, value).replaceAll("\\", "/") || ".";
|
|
12
|
+
}
|
|
13
|
+
if (value.startsWith("/") && rootPath.startsWith("/")) {
|
|
14
|
+
return path.posix.relative(rootPath, value) || ".";
|
|
15
|
+
}
|
|
16
|
+
return value.replaceAll("\\", "/");
|
|
17
|
+
}
|
|
18
|
+
function canonicalizeEvidenceValue(key, value, rootPath) {
|
|
19
|
+
return typeof value === "string" && isPathEvidenceKey(key)
|
|
20
|
+
? normalizePathEvidence(value, rootPath)
|
|
21
|
+
: value;
|
|
22
|
+
}
|
|
23
|
+
function canonicalizeEvidence(evidence, rootPath) {
|
|
24
|
+
return Object.fromEntries(Object.entries(evidence ?? {})
|
|
25
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
26
|
+
.map(([key, value]) => [
|
|
27
|
+
key,
|
|
28
|
+
canonicalizeEvidenceValue(key, value, rootPath)
|
|
29
|
+
]));
|
|
30
|
+
}
|
|
31
|
+
export function buildFindingFingerprint(finding, rootPath) {
|
|
32
|
+
const payload = JSON.stringify({
|
|
33
|
+
version: fingerprintVersion,
|
|
34
|
+
ruleId: finding.id,
|
|
35
|
+
evidence: canonicalizeEvidence(finding.evidence, rootPath)
|
|
36
|
+
});
|
|
37
|
+
return createHash("sha256").update(payload).digest("hex");
|
|
38
|
+
}
|
|
39
|
+
export function withFindingFingerprint(finding, rootPath) {
|
|
40
|
+
return {
|
|
41
|
+
...finding,
|
|
42
|
+
fingerprint: buildFindingFingerprint(finding, rootPath)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function withFindingFingerprints(findings, rootPath) {
|
|
46
|
+
return findings.map((finding) => withFindingFingerprint(finding, rootPath));
|
|
47
|
+
}
|
|
48
|
+
export function formatFindingFingerprintLine(finding) {
|
|
49
|
+
return finding.fingerprint ?? null;
|
|
50
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatFindingEvidenceLine } from "./format-finding-evidence.js";
|
|
2
|
+
import { formatFindingFingerprintLine } from "./finding-fingerprint.js";
|
|
2
3
|
export function buildMarkdownReport(result, options) {
|
|
3
4
|
const failCount = result.findings.filter((finding) => finding.severity === "fail").length;
|
|
4
5
|
const warnCount = result.findings.filter((finding) => finding.severity === "warn").length;
|
|
@@ -39,6 +40,10 @@ export function buildMarkdownReport(result, options) {
|
|
|
39
40
|
lines.push(`- Message: ${finding.message}`);
|
|
40
41
|
lines.push(`- Impact: ${finding.impact}`);
|
|
41
42
|
lines.push(`- Suggested fix: ${finding.suggestedFix}`);
|
|
43
|
+
const fingerprint = formatFindingFingerprintLine(finding);
|
|
44
|
+
if (fingerprint) {
|
|
45
|
+
lines.push(`- Fingerprint: \`${fingerprint}\``);
|
|
46
|
+
}
|
|
42
47
|
const evidence = formatFindingEvidenceLine(finding);
|
|
43
48
|
if (evidence) {
|
|
44
49
|
lines.push(`- Evidence: ${evidence}`);
|
|
@@ -47,6 +47,13 @@ export function renderSarifReport(result) {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
],
|
|
50
|
+
...(finding.fingerprint
|
|
51
|
+
? {
|
|
52
|
+
partialFingerprints: {
|
|
53
|
+
"codexPluginDoctor/v1": finding.fingerprint
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
: {}),
|
|
50
57
|
...(finding.evidence ? { properties: { evidence: finding.evidence } } : {})
|
|
51
58
|
}))
|
|
52
59
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { findRuleDefinition } from "../rules/rule-catalog.js";
|
|
2
2
|
import { formatFindingEvidenceLine } from "./format-finding-evidence.js";
|
|
3
|
+
import { formatFindingFingerprintLine } from "./finding-fingerprint.js";
|
|
3
4
|
function getCounts(result) {
|
|
4
5
|
const failCount = result.findings.filter((finding) => finding.severity === "fail").length;
|
|
5
6
|
const warnCount = result.findings.filter((finding) => finding.severity === "warn").length;
|
|
@@ -61,6 +62,10 @@ export function renderTextReport(result, options = {}) {
|
|
|
61
62
|
lines.push(` Message: ${finding.message}`);
|
|
62
63
|
lines.push(` Impact: ${finding.impact}`);
|
|
63
64
|
lines.push(` Suggested fix: ${finding.suggestedFix}`);
|
|
65
|
+
const fingerprint = formatFindingFingerprintLine(finding);
|
|
66
|
+
if (fingerprint) {
|
|
67
|
+
lines.push(` Fingerprint: ${fingerprint}`);
|
|
68
|
+
}
|
|
64
69
|
const evidence = formatFindingEvidenceLine(finding);
|
|
65
70
|
if (evidence) {
|
|
66
71
|
lines.push(` Evidence: ${evidence}`);
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { discoverPackage } from "../core/discover-package.js";
|
|
4
4
|
import { readJsonFile } from "../core/read-json-file.js";
|
|
5
5
|
import { validatePlugin } from "../core/validate-plugin.js";
|
|
6
|
+
import { formatFindingFingerprintLine, withFindingFingerprints } from "../reporting/finding-fingerprint.js";
|
|
6
7
|
import { formatFindingEvidenceLine } from "../reporting/format-finding-evidence.js";
|
|
7
8
|
function buildFinding(severity, id, message, impact, suggestedFix, evidence) {
|
|
8
9
|
return {
|
|
@@ -310,19 +311,20 @@ function scoreSecurityAudit(findingCounts) {
|
|
|
310
311
|
return Math.max(0, 100 - (findingCounts.fail * 35) - (findingCounts.warn * 10));
|
|
311
312
|
}
|
|
312
313
|
export function buildSecurityAuditFromFindings(targetPath, findings) {
|
|
313
|
-
const
|
|
314
|
-
const
|
|
314
|
+
const rootPath = path.resolve(targetPath);
|
|
315
|
+
const fingerprintedFindings = withFindingFingerprints(dedupeFindings(findings), rootPath);
|
|
316
|
+
const findingCounts = buildFindingCounts(fingerprintedFindings);
|
|
315
317
|
const status = findingCounts.fail > 0
|
|
316
318
|
? "fail"
|
|
317
319
|
: findingCounts.warn > 0
|
|
318
320
|
? "warn"
|
|
319
321
|
: "pass";
|
|
320
322
|
return {
|
|
321
|
-
targetPath:
|
|
323
|
+
targetPath: rootPath,
|
|
322
324
|
status,
|
|
323
325
|
score: scoreSecurityAudit(findingCounts),
|
|
324
326
|
findingCounts,
|
|
325
|
-
findings:
|
|
327
|
+
findings: fingerprintedFindings
|
|
326
328
|
};
|
|
327
329
|
}
|
|
328
330
|
export async function buildSecurityAudit(targetPath) {
|
|
@@ -331,14 +333,7 @@ export async function buildSecurityAudit(targetPath) {
|
|
|
331
333
|
const findings = [
|
|
332
334
|
buildFinding("fail", "plugin.security.audit_unavailable", "The target directory is missing `.codex-plugin/plugin.json`, so the package security audit cannot run.", "Without a Codex plugin manifest, the audit cannot resolve packaged skills or MCP server configuration safely.", "Run the audit against a Codex plugin package root.", { targetPath: path.resolve(targetPath) })
|
|
333
335
|
];
|
|
334
|
-
|
|
335
|
-
return {
|
|
336
|
-
targetPath: path.resolve(targetPath),
|
|
337
|
-
status: "fail",
|
|
338
|
-
score: scoreSecurityAudit(findingCounts),
|
|
339
|
-
findingCounts,
|
|
340
|
-
findings
|
|
341
|
-
};
|
|
336
|
+
return buildSecurityAuditFromFindings(targetPath, findings);
|
|
342
337
|
}
|
|
343
338
|
const validationResult = await validatePlugin(discoveredPackage.rootPath);
|
|
344
339
|
const validationSecurityFindings = validationResult.findings.filter((finding) => finding.id.startsWith("plugin.security."));
|
|
@@ -382,6 +377,10 @@ export function renderSecurityScorecard(audit, options = {}) {
|
|
|
382
377
|
lines.push(` Message: ${finding.message}`);
|
|
383
378
|
lines.push(` Impact: ${finding.impact}`);
|
|
384
379
|
lines.push(` Suggested fix: ${finding.suggestedFix}`);
|
|
380
|
+
const fingerprint = formatFindingFingerprintLine(finding);
|
|
381
|
+
if (fingerprint) {
|
|
382
|
+
lines.push(` Fingerprint: ${fingerprint}`);
|
|
383
|
+
}
|
|
385
384
|
const evidence = formatFindingEvidenceLine(finding);
|
|
386
385
|
if (evidence) {
|
|
387
386
|
lines.push(` Evidence: ${evidence}`);
|
|
@@ -2,6 +2,7 @@ import { readFile, stat } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { discoverPackage } from "../core/discover-package.js";
|
|
4
4
|
import { parseJsonText } from "../core/read-json-file.js";
|
|
5
|
+
import { formatFindingFingerprintLine, withFindingFingerprints } from "../reporting/finding-fingerprint.js";
|
|
5
6
|
import { buildSecurityAudit } from "./security-audit.js";
|
|
6
7
|
const lifecycleScripts = new Set([
|
|
7
8
|
"preinstall",
|
|
@@ -130,11 +131,11 @@ export async function buildTrustScore(targetPath, options = {}) {
|
|
|
130
131
|
: await discoverPackage(rootPath)
|
|
131
132
|
? await buildSecurityAudit(rootPath)
|
|
132
133
|
: null;
|
|
133
|
-
const findings = dedupeFindings([
|
|
134
|
+
const findings = withFindingFingerprints(dedupeFindings([
|
|
134
135
|
...scriptAudit.findings,
|
|
135
136
|
...dependencyAudit.findings,
|
|
136
137
|
...(securityAudit?.findings ?? [])
|
|
137
|
-
]);
|
|
138
|
+
]), rootPath);
|
|
138
139
|
const fail = findings.filter((finding) => finding.severity === "fail").length;
|
|
139
140
|
const warn = findings.filter((finding) => finding.severity === "warn").length;
|
|
140
141
|
const score = scoreFindings(findings);
|
|
@@ -189,6 +190,10 @@ export function renderTrustScore(report) {
|
|
|
189
190
|
lines.push(` Message: ${finding.message}`);
|
|
190
191
|
lines.push(` Impact: ${finding.impact}`);
|
|
191
192
|
lines.push(` Suggested fix: ${finding.suggestedFix}`);
|
|
193
|
+
const fingerprint = formatFindingFingerprintLine(finding);
|
|
194
|
+
if (fingerprint) {
|
|
195
|
+
lines.push(` Fingerprint: ${fingerprint}`);
|
|
196
|
+
}
|
|
192
197
|
}
|
|
193
198
|
};
|
|
194
199
|
appendSection("Failures", report.findings.filter((finding) => finding.severity === "fail"), "x");
|
package/package.json
CHANGED