aienvmp 0.1.9 → 0.1.11
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/BUGFIXES.md +6 -0
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/render.js +13 -2
- package/src/security.js +106 -11
package/BUGFIXES.md
CHANGED
|
@@ -28,6 +28,12 @@ Short record of bugs, fixes, and follow-up checks.
|
|
|
28
28
|
- Fix: test command used `/bin/zsh -lc` to load login shell PATH.
|
|
29
29
|
- Verification: remote test detected Node `v25.3.0`, npm `11.11.0`, and completed `aienvmp sync`.
|
|
30
30
|
|
|
31
|
+
### Security summaries lacked remediation hints
|
|
32
|
+
|
|
33
|
+
- Issue: AI agents could see vulnerable package names but not enough bounded detail to plan the next dependency update.
|
|
34
|
+
- Fix: npm and Python security summaries now include fix versions and advisory references when scanners provide them.
|
|
35
|
+
- Verification: parser tests cover npm remediation objects and pip-audit advisory ids; Windows and macOS tarball tests completed `sync --security`.
|
|
36
|
+
|
|
31
37
|
## Template
|
|
32
38
|
|
|
33
39
|
### Title
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.11
|
|
4
|
+
|
|
5
|
+
- Added bounded remediation details to security summaries, including fix versions and advisory references.
|
|
6
|
+
- Surfaced remediation hints in `AIENV.md` and the dashboard so AI agents can plan safer dependency updates.
|
|
7
|
+
- Kept security scanning read-only and opt-in.
|
|
8
|
+
|
|
9
|
+
## 0.1.10
|
|
10
|
+
|
|
11
|
+
- Added optional `pip-audit` JSON parsing for Python security summaries.
|
|
12
|
+
- Combined npm and Python vulnerable package summaries into the same AI-facing security context.
|
|
13
|
+
- Kept missing Python scanners non-blocking with explicit scanner availability metadata.
|
|
14
|
+
|
|
3
15
|
## 0.1.9
|
|
4
16
|
|
|
5
17
|
- Added top vulnerable package summaries for AI context, handoff, `AIENV.md`, and the dashboard.
|
package/package.json
CHANGED
package/src/render.js
CHANGED
|
@@ -220,7 +220,9 @@ const inventoryHtml=manifest.inventory?.enabled?('<table>'+Object.entries(invent
|
|
|
220
220
|
const sec=manifest.security||{};
|
|
221
221
|
const secSummary=sec.summary||{total:0,critical:0,high:0,moderate:0,low:0,info:0};
|
|
222
222
|
const secPackages=sec.topPackages||[];
|
|
223
|
-
const
|
|
223
|
+
const securityFix=p=>p.fixVersions?.length?\`fix \${p.fixVersions.slice(0,3).join(', ')}\`:(p.fixAvailable?'fix available':'review required');
|
|
224
|
+
const securityRefs=p=>p.advisories?.length?\` · \${p.advisories.map(a=>a.id||a.title).filter(Boolean).slice(0,2).join(', ')}\`:'';
|
|
225
|
+
const securityHtml=sec.enabled?\`<table><tr><th>Total</th><td><code>\${esc(secSummary.total||0)}</code></td></tr><tr><th>Critical</th><td><code>\${esc(secSummary.critical||0)}</code></td></tr><tr><th>High</th><td><code>\${esc(secSummary.high||0)}</code></td></tr><tr><th>Moderate</th><td><code>\${esc(secSummary.moderate||0)}</code></td></tr><tr><th>Low</th><td><code>\${esc(secSummary.low||0)}</code></td></tr></table>\${secPackages.length?'<div class="timeline">'+secPackages.slice(0,5).map(p=>\`<div class="event"><time>\${esc(p.severity)}</time><div><b>\${esc(p.name)}</b> \${esc(securityFix(p))}\${esc(securityRefs(p))}</div></div>\`).join('')+'</div>':'<div class="okline" style="margin-top:10px">No vulnerable packages reported.</div>'}\`:'<div class="okline">Security scan is off. Run <code>aienvmp sync --security</code> for read-only vulnerability summary.</div>';
|
|
224
226
|
const change=c=>c.type==='changed'?\`\${c.scope} \${c.key}: \${c.before} -> \${c.after}\`:\`\${c.scope} \${c.key}: \${c.type} \${c.after||c.before}\`;
|
|
225
227
|
const timelineLabel=t=>t.change?change(t.change):(t.summary||t.action||t.type||'recorded change');
|
|
226
228
|
const agentNames={agents:'Codex',claude:'Claude',gemini:'Gemini'};
|
|
@@ -350,12 +352,21 @@ function securityLines(security = {}) {
|
|
|
350
352
|
if (packages.length) {
|
|
351
353
|
lines.push("- Top vulnerable packages:");
|
|
352
354
|
for (const pkg of packages.slice(0, 8)) {
|
|
353
|
-
lines.push(` - ${pkg.name}: ${pkg.severity}; ${pkg
|
|
355
|
+
lines.push(` - ${pkg.name}: ${pkg.severity}; ${securityPackageNote(pkg)}`);
|
|
354
356
|
}
|
|
355
357
|
}
|
|
356
358
|
return lines;
|
|
357
359
|
}
|
|
358
360
|
|
|
361
|
+
function securityPackageNote(pkg) {
|
|
362
|
+
const fix = pkg.fixVersions?.length ? `fix ${pkg.fixVersions.slice(0, 3).join(", ")}` : pkg.fixAvailable ? "fix available" : "review required";
|
|
363
|
+
const advisories = (pkg.advisories || [])
|
|
364
|
+
.map((item) => item.id || item.title)
|
|
365
|
+
.filter(Boolean)
|
|
366
|
+
.slice(0, 2);
|
|
367
|
+
return advisories.length ? `${fix}; advisories ${advisories.join(", ")}` : fix;
|
|
368
|
+
}
|
|
369
|
+
|
|
359
370
|
function policyLines(policy) {
|
|
360
371
|
const lines = [];
|
|
361
372
|
if (policy.node) lines.push(`- Node version policy: ${policy.node}`);
|
package/src/security.js
CHANGED
|
@@ -14,15 +14,15 @@ export async function scanSecurity(dir, options = {}) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const npmAudit = await scanNpmAudit(dir);
|
|
17
|
+
const pipAudit = await scanPipAudit(dir);
|
|
18
|
+
const scanners = { npmAudit, pipAudit };
|
|
17
19
|
return {
|
|
18
20
|
mode: "security",
|
|
19
21
|
enabled: true,
|
|
20
22
|
note: "Read-only vulnerability summary. Use for review and CI policy, not automatic fixes.",
|
|
21
|
-
scanners
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
summary: summarizeScanners({ npmAudit }),
|
|
25
|
-
topPackages: topVulnerablePackages({ npmAudit })
|
|
23
|
+
scanners,
|
|
24
|
+
summary: summarizeScanners(scanners),
|
|
25
|
+
topPackages: topVulnerablePackages(scanners)
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -50,6 +50,29 @@ export async function scanNpmAudit(dir) {
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export async function scanPipAudit(dir) {
|
|
54
|
+
const hasPythonHints = await exists(path.join(dir, "pyproject.toml")) || await exists(path.join(dir, "requirements.txt"));
|
|
55
|
+
if (!hasPythonHints) return unavailable("pip-audit", "Python project hints not found");
|
|
56
|
+
|
|
57
|
+
const result = await commandResult("pip-audit", ["-f", "json"], {
|
|
58
|
+
cwd: dir,
|
|
59
|
+
timeout: 15000,
|
|
60
|
+
maxBuffer: 4 * 1024 * 1024
|
|
61
|
+
});
|
|
62
|
+
const parsed = parsePipAudit(result.stdout);
|
|
63
|
+
if (!parsed.available) {
|
|
64
|
+
return unavailable("pip-audit", result.stderr || "pip-audit did not return parseable JSON");
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
scanner: "pip-audit",
|
|
68
|
+
available: true,
|
|
69
|
+
ok: result.ok,
|
|
70
|
+
exitCode: result.code,
|
|
71
|
+
summary: parsed.summary,
|
|
72
|
+
vulnerablePackages: parsed.vulnerablePackages
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
53
76
|
export function parseNpmAudit(raw) {
|
|
54
77
|
try {
|
|
55
78
|
const parsed = JSON.parse(raw || "{}");
|
|
@@ -57,12 +80,18 @@ export function parseNpmAudit(raw) {
|
|
|
57
80
|
const vulnerabilities = parsed.vulnerabilities || {};
|
|
58
81
|
const severityCounts = Object.fromEntries(SEVERITIES.map((severity) => [severity, Number(metadata.vulnerabilities?.[severity] || 0)]));
|
|
59
82
|
const total = Number(metadata.vulnerabilities?.total || Object.values(severityCounts).reduce((sum, value) => sum + value, 0));
|
|
60
|
-
const vulnerablePackages = Object.entries(vulnerabilities).slice(0, 20).map(([name, value]) =>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
const vulnerablePackages = Object.entries(vulnerabilities).slice(0, 20).map(([name, value]) => {
|
|
84
|
+
const fixVersions = npmFixVersions(value.fixAvailable);
|
|
85
|
+
const advisories = npmAdvisories(value.via);
|
|
86
|
+
return {
|
|
87
|
+
name,
|
|
88
|
+
severity: value.severity || "unknown",
|
|
89
|
+
viaCount: Array.isArray(value.via) ? value.via.length : 0,
|
|
90
|
+
fixAvailable: hasFixAvailable(value.fixAvailable),
|
|
91
|
+
fixVersions,
|
|
92
|
+
advisories
|
|
93
|
+
};
|
|
94
|
+
});
|
|
66
95
|
return {
|
|
67
96
|
available: true,
|
|
68
97
|
summary: { total, ...severityCounts },
|
|
@@ -73,6 +102,33 @@ export function parseNpmAudit(raw) {
|
|
|
73
102
|
}
|
|
74
103
|
}
|
|
75
104
|
|
|
105
|
+
export function parsePipAudit(raw) {
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(raw || "{}");
|
|
108
|
+
const dependencies = Array.isArray(parsed.dependencies) ? parsed.dependencies : [];
|
|
109
|
+
const vulnerablePackages = dependencies
|
|
110
|
+
.filter((dependency) => Array.isArray(dependency.vulns) && dependency.vulns.length)
|
|
111
|
+
.slice(0, 20)
|
|
112
|
+
.map((dependency) => ({
|
|
113
|
+
name: dependency.name || "unknown",
|
|
114
|
+
version: dependency.version || "unknown",
|
|
115
|
+
severity: "unknown",
|
|
116
|
+
viaCount: dependency.vulns.length,
|
|
117
|
+
fixAvailable: dependency.vulns.some((vuln) => Array.isArray(vuln.fix_versions) && vuln.fix_versions.length),
|
|
118
|
+
fixVersions: unique(dependency.vulns.flatMap((vuln) => vuln.fix_versions || [])).slice(0, 5),
|
|
119
|
+
advisories: pipAdvisories(dependency.vulns)
|
|
120
|
+
}));
|
|
121
|
+
const total = vulnerablePackages.reduce((sum, pkg) => sum + pkg.viaCount, 0);
|
|
122
|
+
return {
|
|
123
|
+
available: true,
|
|
124
|
+
summary: { total, critical: 0, high: 0, moderate: 0, low: 0, info: 0 },
|
|
125
|
+
vulnerablePackages
|
|
126
|
+
};
|
|
127
|
+
} catch {
|
|
128
|
+
return { available: false };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
76
132
|
export function summarizeScanners(scanners = {}) {
|
|
77
133
|
const summary = { total: 0, critical: 0, high: 0, moderate: 0, low: 0, info: 0 };
|
|
78
134
|
for (const scanner of Object.values(scanners)) {
|
|
@@ -98,3 +154,42 @@ function unavailable(scanner, reason) {
|
|
|
98
154
|
reason
|
|
99
155
|
};
|
|
100
156
|
}
|
|
157
|
+
|
|
158
|
+
function unique(items) {
|
|
159
|
+
return [...new Set(items.filter(Boolean))];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function hasFixAvailable(value) {
|
|
163
|
+
if (typeof value === "boolean") return value;
|
|
164
|
+
return Boolean(value && typeof value === "object");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function npmFixVersions(value) {
|
|
168
|
+
if (!value || typeof value !== "object") return [];
|
|
169
|
+
return unique([value.version]).slice(0, 5);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function npmAdvisories(via = []) {
|
|
173
|
+
if (!Array.isArray(via)) return [];
|
|
174
|
+
return via
|
|
175
|
+
.filter((item) => item && typeof item === "object")
|
|
176
|
+
.map((item) => ({
|
|
177
|
+
id: String(item.source || item.id || item.cve || item.name || "").trim(),
|
|
178
|
+
title: String(item.title || "").trim(),
|
|
179
|
+
url: String(item.url || "").trim(),
|
|
180
|
+
severity: item.severity || "unknown"
|
|
181
|
+
}))
|
|
182
|
+
.filter((item) => item.id || item.title || item.url)
|
|
183
|
+
.slice(0, 5);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function pipAdvisories(vulns = []) {
|
|
187
|
+
return vulns
|
|
188
|
+
.map((vuln) => ({
|
|
189
|
+
id: String(vuln.id || vuln.aliases?.[0] || "").trim(),
|
|
190
|
+
aliases: Array.isArray(vuln.aliases) ? vuln.aliases.slice(0, 5) : [],
|
|
191
|
+
fixVersions: Array.isArray(vuln.fix_versions) ? vuln.fix_versions.slice(0, 5) : []
|
|
192
|
+
}))
|
|
193
|
+
.filter((item) => item.id || item.aliases.length || item.fixVersions.length)
|
|
194
|
+
.slice(0, 5);
|
|
195
|
+
}
|