aienvmp 0.1.10 → 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 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,11 @@
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
+
3
9
  ## 0.1.10
4
10
 
5
11
  - Added optional `pip-audit` JSON parsing for Python security summaries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "AI-first environment maps and lightweight runtime SBOMs for shared development machines.",
5
5
  "type": "module",
6
6
  "bin": {
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 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> \${p.fixAvailable?'fix available':'review required'}</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>';
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.fixAvailable ? "fix available" : "review required"}`);
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
@@ -80,12 +80,18 @@ export function parseNpmAudit(raw) {
80
80
  const vulnerabilities = parsed.vulnerabilities || {};
81
81
  const severityCounts = Object.fromEntries(SEVERITIES.map((severity) => [severity, Number(metadata.vulnerabilities?.[severity] || 0)]));
82
82
  const total = Number(metadata.vulnerabilities?.total || Object.values(severityCounts).reduce((sum, value) => sum + value, 0));
83
- const vulnerablePackages = Object.entries(vulnerabilities).slice(0, 20).map(([name, value]) => ({
84
- name,
85
- severity: value.severity || "unknown",
86
- viaCount: Array.isArray(value.via) ? value.via.length : 0,
87
- fixAvailable: Boolean(value.fixAvailable)
88
- }));
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
+ });
89
95
  return {
90
96
  available: true,
91
97
  summary: { total, ...severityCounts },
@@ -109,7 +115,8 @@ export function parsePipAudit(raw) {
109
115
  severity: "unknown",
110
116
  viaCount: dependency.vulns.length,
111
117
  fixAvailable: dependency.vulns.some((vuln) => Array.isArray(vuln.fix_versions) && vuln.fix_versions.length),
112
- fixVersions: unique(dependency.vulns.flatMap((vuln) => vuln.fix_versions || [])).slice(0, 5)
118
+ fixVersions: unique(dependency.vulns.flatMap((vuln) => vuln.fix_versions || [])).slice(0, 5),
119
+ advisories: pipAdvisories(dependency.vulns)
113
120
  }));
114
121
  const total = vulnerablePackages.reduce((sum, pkg) => sum + pkg.viaCount, 0);
115
122
  return {
@@ -151,3 +158,38 @@ function unavailable(scanner, reason) {
151
158
  function unique(items) {
152
159
  return [...new Set(items.filter(Boolean))];
153
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
+ }