codex-plugin-doctor 0.13.0 → 0.15.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 +10 -2
- package/dist/audit/ecosystem-audit-cache.d.ts +15 -0
- package/dist/audit/ecosystem-audit-cache.js +78 -0
- package/dist/audit/ecosystem-audit.d.ts +8 -0
- package/dist/audit/ecosystem-audit.js +63 -6
- package/dist/core/doctor-export-bundle.d.ts +4 -4
- package/dist/core/doctor-export-bundle.js +3 -30
- package/dist/core/doctor-recommendations.d.ts +2 -2
- package/dist/core/doctor-recommendations.js +5 -135
- package/dist/core/npm-package-doctor.d.ts +33 -0
- package/dist/core/npm-package-doctor.js +173 -0
- package/dist/core/package-analysis.d.ts +28 -0
- package/dist/core/package-analysis.js +180 -0
- package/dist/core/performance-report.d.ts +37 -0
- package/dist/core/performance-report.js +141 -0
- package/dist/core/risk-diff.d.ts +33 -0
- package/dist/core/risk-diff.js +102 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -0
- package/dist/rules/rule-catalog.js +9 -0
- package/dist/run-cli.js +113 -3
- package/dist/security/security-audit.js +59 -1
- package/dist/security/trust-score.d.ts +5 -1
- package/dist/security/trust-score.js +6 -5
- package/package.json +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { buildPackageAnalysis } from "./package-analysis.js";
|
|
2
|
+
function findingKey(finding) {
|
|
3
|
+
return `${finding.category}\n${finding.id}\n${finding.message}`;
|
|
4
|
+
}
|
|
5
|
+
function collectComparableFindings(analysis) {
|
|
6
|
+
const findings = [
|
|
7
|
+
...analysis.validation.findings.map((finding) => ({
|
|
8
|
+
...finding,
|
|
9
|
+
category: "validation"
|
|
10
|
+
})),
|
|
11
|
+
...analysis.security.findings.map((finding) => ({
|
|
12
|
+
...finding,
|
|
13
|
+
category: "security"
|
|
14
|
+
}))
|
|
15
|
+
];
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
return findings.filter((finding) => {
|
|
18
|
+
const key = findingKey(finding);
|
|
19
|
+
if (seen.has(key)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
seen.add(key);
|
|
23
|
+
return true;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function compareFindings(beforeFindings, afterFindings) {
|
|
27
|
+
const beforeKeys = new Set(beforeFindings.map(findingKey));
|
|
28
|
+
const afterKeys = new Set(afterFindings.map(findingKey));
|
|
29
|
+
return {
|
|
30
|
+
newFindings: afterFindings.filter((finding) => !beforeKeys.has(findingKey(finding))),
|
|
31
|
+
resolvedFindings: beforeFindings.filter((finding) => !afterKeys.has(findingKey(finding)))
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export async function buildDoctorRiskDiffReport(beforePath, afterPath, options = {}) {
|
|
35
|
+
const [beforeAnalysis, afterAnalysis] = await Promise.all([
|
|
36
|
+
buildPackageAnalysis(beforePath, { environment: options.environment }),
|
|
37
|
+
buildPackageAnalysis(afterPath, { environment: options.environment })
|
|
38
|
+
]);
|
|
39
|
+
const { newFindings, resolvedFindings } = compareFindings(collectComparableFindings(beforeAnalysis), collectComparableFindings(afterAnalysis));
|
|
40
|
+
const trustScoreDelta = afterAnalysis.trust.score - beforeAnalysis.trust.score;
|
|
41
|
+
const hasNewFailure = newFindings.some((finding) => finding.severity === "fail");
|
|
42
|
+
const hasNewWarning = newFindings.some((finding) => finding.severity === "warn");
|
|
43
|
+
const riskIncreased = hasNewFailure || trustScoreDelta < 0;
|
|
44
|
+
const status = riskIncreased
|
|
45
|
+
? "fail"
|
|
46
|
+
: hasNewWarning
|
|
47
|
+
? "warn"
|
|
48
|
+
: "pass";
|
|
49
|
+
return {
|
|
50
|
+
schemaVersion: "1.0.0",
|
|
51
|
+
generatedAt: new Date().toISOString(),
|
|
52
|
+
kind: "doctor.risk.diff",
|
|
53
|
+
beforePath: beforeAnalysis.targetPath,
|
|
54
|
+
afterPath: afterAnalysis.targetPath,
|
|
55
|
+
status,
|
|
56
|
+
exitCode: status === "fail" ? 1 : 0,
|
|
57
|
+
summary: {
|
|
58
|
+
riskIncreased,
|
|
59
|
+
newFindings: newFindings.length,
|
|
60
|
+
resolvedFindings: resolvedFindings.length,
|
|
61
|
+
trustScoreBefore: beforeAnalysis.trust.score,
|
|
62
|
+
trustScoreAfter: afterAnalysis.trust.score,
|
|
63
|
+
trustScoreDelta
|
|
64
|
+
},
|
|
65
|
+
newFindings,
|
|
66
|
+
resolvedFindings
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function renderDoctorRiskDiffReportJson(report) {
|
|
70
|
+
return JSON.stringify(report, null, 2);
|
|
71
|
+
}
|
|
72
|
+
export function renderDoctorRiskDiffReport(report, options = {}) {
|
|
73
|
+
const lines = [
|
|
74
|
+
"Doctor Risk Diff",
|
|
75
|
+
"================",
|
|
76
|
+
`Status: ${report.status.toUpperCase()}`,
|
|
77
|
+
`Before: ${report.beforePath}`,
|
|
78
|
+
`After: ${report.afterPath}`,
|
|
79
|
+
`Risk increased: ${report.summary.riskIncreased ? "yes" : "no"}`,
|
|
80
|
+
`Trust score delta: ${report.summary.trustScoreDelta}`,
|
|
81
|
+
`New findings: ${report.summary.newFindings}`,
|
|
82
|
+
`Resolved findings: ${report.summary.resolvedFindings}`
|
|
83
|
+
];
|
|
84
|
+
if (options.outputPath) {
|
|
85
|
+
lines.push(`Output: ${options.outputPath}`);
|
|
86
|
+
}
|
|
87
|
+
if (report.newFindings.length > 0) {
|
|
88
|
+
lines.push("", "New Findings", "------------");
|
|
89
|
+
for (const finding of report.newFindings) {
|
|
90
|
+
lines.push(`[${finding.severity.toUpperCase()}] ${finding.category}: ${finding.id}`);
|
|
91
|
+
lines.push(` Message: ${finding.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (report.resolvedFindings.length > 0) {
|
|
95
|
+
lines.push("", "Resolved Findings", "-----------------");
|
|
96
|
+
for (const finding of report.resolvedFindings) {
|
|
97
|
+
lines.push(`[${finding.severity.toUpperCase()}] ${finding.category}: ${finding.id}`);
|
|
98
|
+
lines.push(` Message: ${finding.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { CheckOptions, CheckResult } from "./domain/types.js";
|
|
2
2
|
export { buildSecurityAudit, renderSecurityAuditJson, renderSecurityScorecard, type SecurityAudit } from "./security/security-audit.js";
|
|
3
|
-
export { buildTrustScore, renderTrustScore, renderTrustScoreJson, type TrustScoreReport } from "./security/trust-score.js";
|
|
3
|
+
export { buildTrustScore, renderTrustScore, renderTrustScoreJson, type BuildTrustScoreOptions, type TrustScoreReport } from "./security/trust-score.js";
|
|
4
4
|
export { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson, type DoctorSnapshot } from "./core/doctor-snapshot.js";
|
|
5
5
|
export { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson, type DoctorRecommendationAction, type DoctorRecommendationsReport } from "./core/doctor-recommendations.js";
|
|
6
6
|
export { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson, type DoctorExportBundle } from "./core/doctor-export-bundle.js";
|
|
7
|
+
export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis, type PackageAnalysis, type PackageAnalysisOptions, type PackageAnalysisStage, type PackageAnalysisTiming } from "./core/package-analysis.js";
|
|
8
|
+
export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson, type BuildDoctorPerformanceReportOptions, type DoctorPerformanceReport, type DoctorPerformanceStage, type DoctorPerformanceStageName } from "./core/performance-report.js";
|
|
9
|
+
export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson, type BuildDoctorNpmPackageReportOptions, type DoctorNpmPackageReport } from "./core/npm-package-doctor.js";
|
|
10
|
+
export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson, type BuildDoctorRiskDiffReportOptions, type DoctorRiskDiffReport, type RiskDiffFinding, type RiskFindingCategory } from "./core/risk-diff.js";
|
|
7
11
|
export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson, type EcosystemAuditReport } from "./audit/ecosystem-audit.js";
|
|
8
12
|
export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames, type PolicyPackName } from "./policy/policy-packs.js";
|
|
9
13
|
export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson, type GenericMcpDoctorReport } from "./mcp/generic-mcp-doctor.js";
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,10 @@ export { buildTrustScore, renderTrustScore, renderTrustScoreJson } from "./secur
|
|
|
4
4
|
export { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson } from "./core/doctor-snapshot.js";
|
|
5
5
|
export { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson } from "./core/doctor-recommendations.js";
|
|
6
6
|
export { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson } from "./core/doctor-export-bundle.js";
|
|
7
|
+
export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./core/package-analysis.js";
|
|
8
|
+
export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
|
|
9
|
+
export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
|
|
10
|
+
export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
|
|
7
11
|
export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson } from "./audit/ecosystem-audit.js";
|
|
8
12
|
export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames } from "./policy/policy-packs.js";
|
|
9
13
|
export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson } from "./mcp/generic-mcp-doctor.js";
|
|
@@ -215,6 +215,15 @@ export const ruleCatalog = [
|
|
|
215
215
|
fix: "Use HTTPS for remote MCP servers; reserve HTTP for explicit localhost development endpoints.",
|
|
216
216
|
example: '{ "url": "https://example.com/mcp" }'
|
|
217
217
|
},
|
|
218
|
+
{
|
|
219
|
+
id: "plugin.security.prompt_injection_text",
|
|
220
|
+
category: "security",
|
|
221
|
+
defaultSeverity: "fail",
|
|
222
|
+
summary: "Packaged text contains prompt-injection or secret-exfiltration instructions.",
|
|
223
|
+
why: "Poisoned tool, prompt, resource, or skill text can instruct an agent to ignore higher-priority instructions or leak secrets when loaded into context.",
|
|
224
|
+
fix: "Remove hidden override or exfiltration instructions and keep descriptions scoped to legitimate behavior.",
|
|
225
|
+
example: "Keep SKILL.md, prompt, resource, and tool descriptions direct and user-facing."
|
|
226
|
+
},
|
|
218
227
|
{
|
|
219
228
|
id: "plugin.runtime.exited_early",
|
|
220
229
|
category: "runtime",
|
package/dist/run-cli.js
CHANGED
|
@@ -16,6 +16,9 @@ import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
|
|
|
16
16
|
import { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson } from "./core/doctor-snapshot.js";
|
|
17
17
|
import { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson } from "./core/doctor-recommendations.js";
|
|
18
18
|
import { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson } from "./core/doctor-export-bundle.js";
|
|
19
|
+
import { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
|
|
20
|
+
import { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
|
|
21
|
+
import { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
|
|
19
22
|
import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlanJsonReport, renderFixPlan } from "./core/fix-plan.js";
|
|
20
23
|
import { renderClientDoctor, renderEnvironmentDoctor, renderEnvironmentDoctorJson } from "./core/environment-doctor.js";
|
|
21
24
|
import { initCiWorkflow } from "./core/init-ci.js";
|
|
@@ -61,7 +64,7 @@ const defaultIo = {
|
|
|
61
64
|
}
|
|
62
65
|
};
|
|
63
66
|
function printUsage(io) {
|
|
64
|
-
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor doctor [recommend <path>|trust <path>|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
|
|
67
|
+
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor doctor [npm <package>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path>|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
|
|
65
68
|
}
|
|
66
69
|
function renderInstalledPlugins(plugins) {
|
|
67
70
|
const lines = [
|
|
@@ -226,6 +229,68 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
226
229
|
io.writeStdout(renderedReport);
|
|
227
230
|
return report.exitCode;
|
|
228
231
|
}
|
|
232
|
+
if (maybePath === "npm") {
|
|
233
|
+
const packageSpec = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
234
|
+
? remainingArgs[0]
|
|
235
|
+
: null;
|
|
236
|
+
if (!packageSpec) {
|
|
237
|
+
io.writeStderr("Missing package spec. Usage: codex-plugin-doctor doctor npm <package> [--json] [--output <path>]");
|
|
238
|
+
return 2;
|
|
239
|
+
}
|
|
240
|
+
const npmFlags = remainingArgs.slice(1);
|
|
241
|
+
const jsonOutput = npmFlags.includes("--json");
|
|
242
|
+
const outputIndex = npmFlags.indexOf("--output");
|
|
243
|
+
const outputPath = outputIndex === -1 ? null : npmFlags[outputIndex + 1];
|
|
244
|
+
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
245
|
+
io.writeStderr("Missing path after --output.");
|
|
246
|
+
return 2;
|
|
247
|
+
}
|
|
248
|
+
const report = await buildDoctorNpmPackageReport(packageSpec, {
|
|
249
|
+
environment: {
|
|
250
|
+
env: terminalContext.env,
|
|
251
|
+
platform: terminalContext.platform
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
const renderedReport = jsonOutput
|
|
255
|
+
? renderDoctorNpmPackageReportJson(report)
|
|
256
|
+
: renderDoctorNpmPackageReport(report, { outputPath });
|
|
257
|
+
if (outputPath) {
|
|
258
|
+
await writeFile(outputPath, renderedReport, "utf8");
|
|
259
|
+
}
|
|
260
|
+
io.writeStdout(renderedReport);
|
|
261
|
+
return report.summary.exitCode;
|
|
262
|
+
}
|
|
263
|
+
if (maybePath === "diff") {
|
|
264
|
+
const beforeIndex = remainingArgs.indexOf("--before");
|
|
265
|
+
const afterIndex = remainingArgs.indexOf("--after");
|
|
266
|
+
const beforePath = beforeIndex === -1 ? null : remainingArgs[beforeIndex + 1];
|
|
267
|
+
const afterPath = afterIndex === -1 ? null : remainingArgs[afterIndex + 1];
|
|
268
|
+
if (!beforePath || beforePath.startsWith("--") || !afterPath || afterPath.startsWith("--")) {
|
|
269
|
+
io.writeStderr("Usage: codex-plugin-doctor doctor diff --before <path> --after <path> [--json] [--output <path>]");
|
|
270
|
+
return 2;
|
|
271
|
+
}
|
|
272
|
+
const jsonOutput = remainingArgs.includes("--json");
|
|
273
|
+
const outputIndex = remainingArgs.indexOf("--output");
|
|
274
|
+
const outputPath = outputIndex === -1 ? null : remainingArgs[outputIndex + 1];
|
|
275
|
+
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
276
|
+
io.writeStderr("Missing path after --output.");
|
|
277
|
+
return 2;
|
|
278
|
+
}
|
|
279
|
+
const report = await buildDoctorRiskDiffReport(beforePath, afterPath, {
|
|
280
|
+
environment: {
|
|
281
|
+
env: terminalContext.env,
|
|
282
|
+
platform: terminalContext.platform
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
const renderedReport = jsonOutput
|
|
286
|
+
? renderDoctorRiskDiffReportJson(report)
|
|
287
|
+
: renderDoctorRiskDiffReport(report, { outputPath });
|
|
288
|
+
if (outputPath) {
|
|
289
|
+
await writeFile(outputPath, renderedReport, "utf8");
|
|
290
|
+
}
|
|
291
|
+
io.writeStdout(renderedReport);
|
|
292
|
+
return report.exitCode;
|
|
293
|
+
}
|
|
229
294
|
if (maybePath === "trust") {
|
|
230
295
|
const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
231
296
|
? remainingArgs[0]
|
|
@@ -250,6 +315,38 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
250
315
|
io.writeStdout(renderedReport);
|
|
251
316
|
return report.exitCode;
|
|
252
317
|
}
|
|
318
|
+
if (maybePath === "perf") {
|
|
319
|
+
const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
320
|
+
? remainingArgs[0]
|
|
321
|
+
: ".";
|
|
322
|
+
const perfFlags = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
323
|
+
? remainingArgs.slice(1)
|
|
324
|
+
: remainingArgs;
|
|
325
|
+
const jsonOutput = perfFlags.includes("--json");
|
|
326
|
+
const outputIndex = perfFlags.indexOf("--output");
|
|
327
|
+
const outputPath = outputIndex === -1 ? null : perfFlags[outputIndex + 1];
|
|
328
|
+
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
329
|
+
io.writeStderr("Missing path after --output.");
|
|
330
|
+
return 2;
|
|
331
|
+
}
|
|
332
|
+
const report = await buildDoctorPerformanceReport(targetPath, {
|
|
333
|
+
environment: {
|
|
334
|
+
env: terminalContext.env,
|
|
335
|
+
platform: terminalContext.platform
|
|
336
|
+
},
|
|
337
|
+
runCheck: options.runCheckImpl
|
|
338
|
+
? (pathToCheck) => options.runCheckImpl(pathToCheck)
|
|
339
|
+
: undefined
|
|
340
|
+
});
|
|
341
|
+
const renderedReport = jsonOutput
|
|
342
|
+
? renderDoctorPerformanceReportJson(report)
|
|
343
|
+
: renderDoctorPerformanceReport(report, { outputPath });
|
|
344
|
+
if (outputPath) {
|
|
345
|
+
await writeFile(outputPath, renderedReport, "utf8");
|
|
346
|
+
}
|
|
347
|
+
io.writeStdout(renderedReport);
|
|
348
|
+
return report.exitCode;
|
|
349
|
+
}
|
|
253
350
|
if (maybePath === "export") {
|
|
254
351
|
const bundleIndex = remainingArgs.indexOf("--bundle");
|
|
255
352
|
if (bundleIndex === -1) {
|
|
@@ -520,7 +617,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
520
617
|
const auditFlags = maybePath ? [maybePath, ...remainingArgs] : remainingArgs;
|
|
521
618
|
const installed = auditFlags.includes("--installed");
|
|
522
619
|
if (!installed) {
|
|
523
|
-
io.writeStderr("Usage: codex-plugin-doctor audit --installed [filter] [--security] [--compat] [--json] [--output <path>]");
|
|
620
|
+
io.writeStderr("Usage: codex-plugin-doctor audit --installed [filter] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]");
|
|
524
621
|
return 2;
|
|
525
622
|
}
|
|
526
623
|
const installedIndex = auditFlags.indexOf("--installed");
|
|
@@ -535,6 +632,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
535
632
|
const policyIndex = auditFlags.indexOf("--policy");
|
|
536
633
|
const policyName = policyIndex === -1 ? null : auditFlags[policyIndex + 1];
|
|
537
634
|
const policy = parsePolicyPack(policyName);
|
|
635
|
+
const cacheEnabled = auditFlags.includes("--cache") || auditFlags.includes("--changed");
|
|
636
|
+
const changedOnly = auditFlags.includes("--changed");
|
|
637
|
+
const cacheFileIndex = auditFlags.indexOf("--cache-file");
|
|
638
|
+
const cachePath = cacheFileIndex === -1 ? null : auditFlags[cacheFileIndex + 1];
|
|
538
639
|
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
539
640
|
io.writeStderr("Missing path after --output.");
|
|
540
641
|
return 2;
|
|
@@ -543,6 +644,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
543
644
|
io.writeStderr("Missing policy after --policy.");
|
|
544
645
|
return 2;
|
|
545
646
|
}
|
|
647
|
+
if (cacheFileIndex !== -1 && (!cachePath || cachePath.startsWith("--"))) {
|
|
648
|
+
io.writeStderr("Missing path after --cache-file.");
|
|
649
|
+
return 2;
|
|
650
|
+
}
|
|
546
651
|
if (policyIndex !== -1 && !policy) {
|
|
547
652
|
io.writeStderr(`Unknown policy: ${policyName}. Supported policies: ${policyPackNames.join(", ")}.`);
|
|
548
653
|
return 2;
|
|
@@ -554,9 +659,14 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
554
659
|
includeSecurity,
|
|
555
660
|
includeCompatibility,
|
|
556
661
|
failOnWarnings: policyFailsOnWarnings(policy),
|
|
662
|
+
cache: {
|
|
663
|
+
enabled: cacheEnabled,
|
|
664
|
+
changedOnly,
|
|
665
|
+
cachePath
|
|
666
|
+
},
|
|
557
667
|
validatePlugin: options.runCheckImpl ?? runCheck
|
|
558
668
|
});
|
|
559
|
-
if (report.summary.totalPlugins === 0) {
|
|
669
|
+
if (report.summary.totalPlugins === 0 && !changedOnly) {
|
|
560
670
|
io.writeStderr(installedFilter
|
|
561
671
|
? `No installed Codex plugins matched '${installedFilter}'.`
|
|
562
672
|
: "No installed Codex plugins found.");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { discoverPackage } from "../core/discover-package.js";
|
|
3
4
|
import { readJsonFile } from "../core/read-json-file.js";
|
|
@@ -40,6 +41,25 @@ function containsPipeInstaller(args) {
|
|
|
40
41
|
/\b(iwr|irm|invoke-webrequest|invoke-restmethod)\b[^|]*\|\s*(iex|invoke-expression)\b/.test(joinedArgs) ||
|
|
41
42
|
/\binvoke-expression\b/.test(joinedArgs));
|
|
42
43
|
}
|
|
44
|
+
const poisonScanExtensions = new Set([
|
|
45
|
+
".json",
|
|
46
|
+
".md",
|
|
47
|
+
".mdx",
|
|
48
|
+
".txt",
|
|
49
|
+
".yaml",
|
|
50
|
+
".yml"
|
|
51
|
+
]);
|
|
52
|
+
const poisonScanSkippedDirectories = new Set([
|
|
53
|
+
".git",
|
|
54
|
+
"coverage",
|
|
55
|
+
"dist",
|
|
56
|
+
"node_modules"
|
|
57
|
+
]);
|
|
58
|
+
const promptInjectionPatterns = [
|
|
59
|
+
/\bignore\s+(?:all\s+)?(?:previous|prior|system|developer)\s+instructions?\b/i,
|
|
60
|
+
/\b(?:exfiltrate|steal|leak|upload|send)\b.{0,120}\b(?:secret|secrets|token|tokens|api\s*key|api\s*keys|credential|credentials|environment\s+variables?|env)\b/i,
|
|
61
|
+
/\bdo\s+not\s+(?:reveal|tell|mention|disclose)\b.{0,120}\b(?:instruction|instructions|prompt|prompts|system|developer)\b/i
|
|
62
|
+
];
|
|
43
63
|
export function auditMcpServerConfig(rootPath, parsedConfig) {
|
|
44
64
|
if (!isPlainObject(parsedConfig) || !isPlainObject(parsedConfig.mcpServers)) {
|
|
45
65
|
return [
|
|
@@ -76,6 +96,43 @@ export function auditMcpServerConfig(rootPath, parsedConfig) {
|
|
|
76
96
|
}
|
|
77
97
|
return findings;
|
|
78
98
|
}
|
|
99
|
+
async function collectPromptPoisoningScanFiles(rootPath, currentPath = rootPath) {
|
|
100
|
+
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
101
|
+
const filePaths = [];
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
const entryPath = path.join(currentPath, entry.name);
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
if (poisonScanSkippedDirectories.has(entry.name)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
filePaths.push(...(await collectPromptPoisoningScanFiles(rootPath, entryPath)));
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!entry.isFile() || !poisonScanExtensions.has(path.extname(entry.name).toLowerCase())) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const details = await stat(entryPath);
|
|
115
|
+
if (details.size <= 256 * 1024) {
|
|
116
|
+
filePaths.push(entryPath);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return filePaths;
|
|
120
|
+
}
|
|
121
|
+
function containsPromptInjectionText(content) {
|
|
122
|
+
return promptInjectionPatterns.some((pattern) => pattern.test(content));
|
|
123
|
+
}
|
|
124
|
+
async function auditPromptPoisoningSurface(rootPath) {
|
|
125
|
+
const findings = [];
|
|
126
|
+
for (const filePath of await collectPromptPoisoningScanFiles(rootPath)) {
|
|
127
|
+
const content = await readFile(filePath, "utf8");
|
|
128
|
+
if (!containsPromptInjectionText(content)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const relativeFilePath = path.relative(rootPath, filePath).replace(/\\/g, "/");
|
|
132
|
+
findings.push(buildFinding("fail", "plugin.security.prompt_injection_text", `The packaged text file \`${relativeFilePath}\` contains prompt-injection or secret-exfiltration style instructions.`, "Malicious or poisoned tool, prompt, resource, or skill text can instruct an agent to ignore higher-priority instructions or leak secrets when loaded into context.", "Remove hidden override or exfiltration instructions, then keep tool/prompt/resource descriptions scoped to the legitimate user-facing behavior."));
|
|
133
|
+
}
|
|
134
|
+
return findings;
|
|
135
|
+
}
|
|
79
136
|
async function auditMcpCommandSurface(discoveredPackage) {
|
|
80
137
|
const { manifest, rootPath } = discoveredPackage;
|
|
81
138
|
if (!manifest.mcpServers) {
|
|
@@ -154,7 +211,8 @@ export async function buildSecurityAudit(targetPath) {
|
|
|
154
211
|
const validationSecurityFindings = validationResult.findings.filter((finding) => finding.id.startsWith("plugin.security."));
|
|
155
212
|
const findings = [
|
|
156
213
|
...validationSecurityFindings,
|
|
157
|
-
...(await auditMcpCommandSurface(discoveredPackage))
|
|
214
|
+
...(await auditMcpCommandSurface(discoveredPackage)),
|
|
215
|
+
...(await auditPromptPoisoningSurface(discoveredPackage.rootPath))
|
|
158
216
|
];
|
|
159
217
|
return buildSecurityAuditFromFindings(discoveredPackage.rootPath, findings);
|
|
160
218
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Finding } from "../domain/types.js";
|
|
2
|
+
import { type SecurityAudit } from "./security-audit.js";
|
|
2
3
|
export interface TrustScoreReport {
|
|
3
4
|
schemaVersion: "1.0.0";
|
|
4
5
|
generatedAt: string;
|
|
@@ -18,6 +19,9 @@ export interface TrustScoreReport {
|
|
|
18
19
|
};
|
|
19
20
|
findings: Finding[];
|
|
20
21
|
}
|
|
21
|
-
export
|
|
22
|
+
export interface BuildTrustScoreOptions {
|
|
23
|
+
securityAudit?: SecurityAudit | null;
|
|
24
|
+
}
|
|
25
|
+
export declare function buildTrustScore(targetPath: string, options?: BuildTrustScoreOptions): Promise<TrustScoreReport>;
|
|
22
26
|
export declare function renderTrustScoreJson(report: TrustScoreReport): string;
|
|
23
27
|
export declare function renderTrustScore(report: TrustScoreReport): string;
|
|
@@ -116,7 +116,7 @@ function scoreFindings(findings) {
|
|
|
116
116
|
const warnCount = findings.filter((finding) => finding.severity === "warn").length;
|
|
117
117
|
return Math.max(0, 100 - (failCount * 35) - (warnCount * 10));
|
|
118
118
|
}
|
|
119
|
-
export async function buildTrustScore(targetPath) {
|
|
119
|
+
export async function buildTrustScore(targetPath, options = {}) {
|
|
120
120
|
const rootPath = path.resolve(targetPath);
|
|
121
121
|
const packageJson = await readPackageJson(rootPath);
|
|
122
122
|
const scriptAudit = packageJson
|
|
@@ -125,10 +125,11 @@ export async function buildTrustScore(targetPath) {
|
|
|
125
125
|
const dependencyAudit = packageJson
|
|
126
126
|
? auditDependencies(packageJson)
|
|
127
127
|
: { findings: [], dependenciesChecked: 0 };
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
const securityAudit = options.securityAudit !== undefined
|
|
129
|
+
? options.securityAudit
|
|
130
|
+
: await discoverPackage(rootPath)
|
|
131
|
+
? await buildSecurityAudit(rootPath)
|
|
132
|
+
: null;
|
|
132
133
|
const findings = dedupeFindings([
|
|
133
134
|
...scriptAudit.findings,
|
|
134
135
|
...dependencyAudit.findings,
|
package/package.json
CHANGED