codex-plugin-doctor 0.11.0 → 0.13.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 +28 -3
- package/dist/audit/ecosystem-audit.d.ts +36 -0
- package/dist/audit/ecosystem-audit.js +135 -0
- package/dist/core/doctor-export-bundle.d.ts +22 -0
- package/dist/core/doctor-export-bundle.js +79 -0
- package/dist/core/doctor-recommendations.d.ts +42 -0
- package/dist/core/doctor-recommendations.js +161 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/mcp/generic-mcp-doctor.d.ts +16 -0
- package/dist/mcp/generic-mcp-doctor.js +166 -0
- package/dist/policy/policy-packs.d.ts +9 -0
- package/dist/policy/policy-packs.js +33 -0
- package/dist/run-cli.js +200 -5
- package/dist/security/security-audit.d.ts +2 -0
- package/dist/security/security-audit.js +40 -33
- package/dist/security/trust-score.d.ts +23 -0
- package/dist/security/trust-score.js +196 -0
- package/package.json +1 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { discoverPackage } from "../core/discover-package.js";
|
|
4
|
+
import { parseJsonText } from "../core/read-json-file.js";
|
|
5
|
+
import { buildSecurityAudit } from "./security-audit.js";
|
|
6
|
+
const lifecycleScripts = new Set([
|
|
7
|
+
"preinstall",
|
|
8
|
+
"install",
|
|
9
|
+
"postinstall",
|
|
10
|
+
"prepublish",
|
|
11
|
+
"prepare"
|
|
12
|
+
]);
|
|
13
|
+
function buildFinding(severity, id, message, impact, suggestedFix) {
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
severity,
|
|
17
|
+
message,
|
|
18
|
+
impact,
|
|
19
|
+
suggestedFix
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function fileExists(targetPath) {
|
|
23
|
+
try {
|
|
24
|
+
const details = await stat(targetPath);
|
|
25
|
+
return details.isFile();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function isPlainObject(value) {
|
|
32
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
function containsRemotePipeInstall(script) {
|
|
35
|
+
const normalized = script.toLowerCase();
|
|
36
|
+
return (/\b(curl|wget)\b[^|]*\|\s*(sh|bash)\b/.test(normalized) ||
|
|
37
|
+
/\b(iwr|irm|invoke-webrequest|invoke-restmethod)\b[^|]*\|\s*(iex|invoke-expression)\b/.test(normalized) ||
|
|
38
|
+
/\binvoke-expression\b/.test(normalized));
|
|
39
|
+
}
|
|
40
|
+
async function readPackageJson(rootPath) {
|
|
41
|
+
const packageJsonPath = path.join(rootPath, "package.json");
|
|
42
|
+
if (!(await fileExists(packageJsonPath))) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const parsed = parseJsonText(await readFile(packageJsonPath, "utf8"));
|
|
47
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function auditScripts(packageJson) {
|
|
54
|
+
const scripts = isPlainObject(packageJson.scripts) ? packageJson.scripts : {};
|
|
55
|
+
const findings = [];
|
|
56
|
+
let scriptsChecked = 0;
|
|
57
|
+
for (const [scriptName, scriptValue] of Object.entries(scripts)) {
|
|
58
|
+
if (!lifecycleScripts.has(scriptName) || typeof scriptValue !== "string") {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
scriptsChecked += 1;
|
|
62
|
+
if (containsRemotePipeInstall(scriptValue)) {
|
|
63
|
+
findings.push(buildFinding("fail", "trust.package.remote_pipe_install", `The package lifecycle script \`${scriptName}\` pipes remote content into a shell.`, "Remote download-and-execute scripts can run unreviewed code during install or publish workflows.", "Replace remote pipe execution with pinned package dependencies or a checked-in reviewed setup script."));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
findings.push(buildFinding("warn", "trust.package.lifecycle_script", `The package defines lifecycle script \`${scriptName}\`.`, "Lifecycle scripts execute automatically during package manager workflows and increase supply-chain review scope.", "Keep lifecycle scripts minimal, documented, and covered by release review."));
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
findings,
|
|
70
|
+
scriptsChecked
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function dependencySections(packageJson) {
|
|
74
|
+
return [
|
|
75
|
+
packageJson.dependencies,
|
|
76
|
+
packageJson.devDependencies,
|
|
77
|
+
packageJson.optionalDependencies,
|
|
78
|
+
packageJson.peerDependencies
|
|
79
|
+
].filter(isPlainObject);
|
|
80
|
+
}
|
|
81
|
+
function auditDependencies(packageJson) {
|
|
82
|
+
const findings = [];
|
|
83
|
+
let dependenciesChecked = 0;
|
|
84
|
+
for (const dependencies of dependencySections(packageJson)) {
|
|
85
|
+
for (const [dependencyName, versionSpec] of Object.entries(dependencies)) {
|
|
86
|
+
if (typeof versionSpec !== "string") {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
dependenciesChecked += 1;
|
|
90
|
+
if (versionSpec === "*" || versionSpec.toLowerCase() === "latest") {
|
|
91
|
+
findings.push(buildFinding("warn", "trust.package.unpinned_dependency", `The dependency \`${dependencyName}\` uses broad version spec \`${versionSpec}\`.`, "Broad dependency ranges make package resolution less reproducible across installs and releases.", "Pin the dependency to a specific compatible range or exact version."));
|
|
92
|
+
}
|
|
93
|
+
if (/^(git\+|github:|http:\/\/|https:\/\/)/i.test(versionSpec)) {
|
|
94
|
+
findings.push(buildFinding("warn", "trust.package.remote_dependency", `The dependency \`${dependencyName}\` resolves from remote spec \`${versionSpec}\`.`, "Remote dependency specs can change outside the npm registry's normal version and integrity workflow.", "Prefer registry-published dependencies with pinned semver ranges."));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
findings,
|
|
100
|
+
dependenciesChecked
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function dedupeFindings(findings) {
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
return findings.filter((finding) => {
|
|
106
|
+
const key = `${finding.id}\n${finding.message}`;
|
|
107
|
+
if (seen.has(key)) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
seen.add(key);
|
|
111
|
+
return true;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function scoreFindings(findings) {
|
|
115
|
+
const failCount = findings.filter((finding) => finding.severity === "fail").length;
|
|
116
|
+
const warnCount = findings.filter((finding) => finding.severity === "warn").length;
|
|
117
|
+
return Math.max(0, 100 - (failCount * 35) - (warnCount * 10));
|
|
118
|
+
}
|
|
119
|
+
export async function buildTrustScore(targetPath) {
|
|
120
|
+
const rootPath = path.resolve(targetPath);
|
|
121
|
+
const packageJson = await readPackageJson(rootPath);
|
|
122
|
+
const scriptAudit = packageJson
|
|
123
|
+
? auditScripts(packageJson)
|
|
124
|
+
: { findings: [], scriptsChecked: 0 };
|
|
125
|
+
const dependencyAudit = packageJson
|
|
126
|
+
? auditDependencies(packageJson)
|
|
127
|
+
: { findings: [], dependenciesChecked: 0 };
|
|
128
|
+
const discoveredPackage = await discoverPackage(rootPath);
|
|
129
|
+
const securityAudit = discoveredPackage
|
|
130
|
+
? await buildSecurityAudit(rootPath)
|
|
131
|
+
: null;
|
|
132
|
+
const findings = dedupeFindings([
|
|
133
|
+
...scriptAudit.findings,
|
|
134
|
+
...dependencyAudit.findings,
|
|
135
|
+
...(securityAudit?.findings ?? [])
|
|
136
|
+
]);
|
|
137
|
+
const fail = findings.filter((finding) => finding.severity === "fail").length;
|
|
138
|
+
const warn = findings.filter((finding) => finding.severity === "warn").length;
|
|
139
|
+
const score = scoreFindings(findings);
|
|
140
|
+
const status = fail > 0
|
|
141
|
+
? "fail"
|
|
142
|
+
: warn > 0
|
|
143
|
+
? "warn"
|
|
144
|
+
: "pass";
|
|
145
|
+
return {
|
|
146
|
+
schemaVersion: "1.0.0",
|
|
147
|
+
generatedAt: new Date().toISOString(),
|
|
148
|
+
targetPath: rootPath,
|
|
149
|
+
status,
|
|
150
|
+
exitCode: status === "fail" ? 1 : 0,
|
|
151
|
+
score,
|
|
152
|
+
findingCounts: {
|
|
153
|
+
fail,
|
|
154
|
+
warn,
|
|
155
|
+
total: findings.length
|
|
156
|
+
},
|
|
157
|
+
packageJson: {
|
|
158
|
+
present: packageJson !== null,
|
|
159
|
+
scriptsChecked: scriptAudit.scriptsChecked,
|
|
160
|
+
dependenciesChecked: dependencyAudit.dependenciesChecked
|
|
161
|
+
},
|
|
162
|
+
findings
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export function renderTrustScoreJson(report) {
|
|
166
|
+
return JSON.stringify(report, null, 2);
|
|
167
|
+
}
|
|
168
|
+
export function renderTrustScore(report) {
|
|
169
|
+
const lines = [
|
|
170
|
+
"Doctor Trust Score",
|
|
171
|
+
"==================",
|
|
172
|
+
`Target: ${report.targetPath}`,
|
|
173
|
+
`Status: ${report.status.toUpperCase()}`,
|
|
174
|
+
`Score: ${report.score}/100`,
|
|
175
|
+
`Summary: ${report.findingCounts.fail} fail, ${report.findingCounts.warn} warn, ${report.findingCounts.total} total`
|
|
176
|
+
];
|
|
177
|
+
if (report.findings.length === 0) {
|
|
178
|
+
lines.push("", "No trust findings.");
|
|
179
|
+
return lines.join("\n");
|
|
180
|
+
}
|
|
181
|
+
const appendSection = (title, findings, marker) => {
|
|
182
|
+
if (findings.length === 0) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
lines.push("", title, "--------");
|
|
186
|
+
for (const finding of findings) {
|
|
187
|
+
lines.push(`${marker} ${finding.id}`);
|
|
188
|
+
lines.push(` Message: ${finding.message}`);
|
|
189
|
+
lines.push(` Impact: ${finding.impact}`);
|
|
190
|
+
lines.push(` Suggested fix: ${finding.suggestedFix}`);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
appendSection("Failures", report.findings.filter((finding) => finding.severity === "fail"), "x");
|
|
194
|
+
appendSection("Warnings", report.findings.filter((finding) => finding.severity === "warn"), "!");
|
|
195
|
+
return lines.join("\n");
|
|
196
|
+
}
|
package/package.json
CHANGED