aienvmp 0.1.7 → 0.1.9
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/CHANGELOG.md +12 -0
- package/README.md +3 -1
- package/ROADMAP.md +4 -0
- package/package.json +1 -1
- package/src/cli.js +2 -2
- package/src/commands/context.js +10 -0
- package/src/commands/handoff.js +10 -0
- package/src/commands/scan.js +1 -1
- package/src/doctor.js +13 -0
- package/src/manifest.js +11 -1
- package/src/render.js +30 -0
- package/src/security.js +100 -0
- package/src/shell.js +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.9
|
|
4
|
+
|
|
5
|
+
- Added top vulnerable package summaries for AI context, handoff, `AIENV.md`, and the dashboard.
|
|
6
|
+
- Ranked vulnerable packages by severity so agents can prioritize review without reading full audit output.
|
|
7
|
+
- Kept security package details bounded to preserve lightweight output.
|
|
8
|
+
|
|
9
|
+
## 0.1.8
|
|
10
|
+
|
|
11
|
+
- Added optional `sync --security` / `scan --security` vulnerability summary collection.
|
|
12
|
+
- Added read-only npm audit parsing for light SBOM security awareness.
|
|
13
|
+
- Exposed security summaries to AI-facing context, handoff, `AIENV.md`, and the dashboard.
|
|
14
|
+
|
|
3
15
|
## 0.1.7
|
|
4
16
|
|
|
5
17
|
- Added stale open intent warnings for long-running environment change plans.
|
package/README.md
CHANGED
|
@@ -21,10 +21,11 @@ npx aienvmp handoff
|
|
|
21
21
|
npx aienvmp handoff --record --actor agent:codex
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Optional
|
|
24
|
+
Optional read-only checks:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
npx aienvmp sync --deep
|
|
28
|
+
npx aienvmp sync --security
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
## Output
|
|
@@ -68,6 +69,7 @@ aienvmp doctor --ci # strict CI check
|
|
|
68
69
|
- lightweight
|
|
69
70
|
- one advisory engine, optional enforcement with `doctor --ci`
|
|
70
71
|
- non-blocking unless strict mode is explicitly requested
|
|
72
|
+
- security checks are opt-in and read-only
|
|
71
73
|
|
|
72
74
|
## Development
|
|
73
75
|
|
package/ROADMAP.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
- Strengthen trust states: observed, planned, changed, review, verified, stale
|
|
8
8
|
- Detect multi-agent environment intent conflicts
|
|
9
9
|
- Keep one advisory decision engine with optional strict enforcement
|
|
10
|
+
- Keep vulnerability checks opt-in and read-only
|
|
10
11
|
- Stabilize `.aienvmp/manifest.json` and JSON command schemas
|
|
11
12
|
- Keep `sync`, `context`, and `handoff` as the simple core flow
|
|
12
13
|
- Improve the dashboard for 10-second human review
|
|
@@ -20,6 +21,9 @@
|
|
|
20
21
|
- Global tool inventory:
|
|
21
22
|
- richer summaries for `npm -g`, `pipx list`, `uv tool list`, and Homebrew
|
|
22
23
|
- optional `--deep` scanners for more toolchains
|
|
24
|
+
- Security summaries:
|
|
25
|
+
- Python vulnerability summary via optional scanner detection
|
|
26
|
+
- OS/container vulnerability summaries through optional external tools
|
|
23
27
|
- Conflict detection:
|
|
24
28
|
- package manager policy vs lockfile mismatch
|
|
25
29
|
- monorepo/project boundary aware intent targets
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -74,7 +74,7 @@ function printUsage() {
|
|
|
74
74
|
console.log(`aienvmp - AI-first env map + lightweight runtime SBOM
|
|
75
75
|
|
|
76
76
|
Usage:
|
|
77
|
-
aienvmp sync [--dir .] [--json] [--quiet] [--deep]
|
|
77
|
+
aienvmp sync [--dir .] [--json] [--quiet] [--deep] [--security]
|
|
78
78
|
aienvmp context [--dir .] [--json]
|
|
79
79
|
aienvmp handoff [--dir .] [--json] [--record --actor agent:id]
|
|
80
80
|
|
|
@@ -87,7 +87,7 @@ Common:
|
|
|
87
87
|
|
|
88
88
|
Advanced:
|
|
89
89
|
aienvmp init [--dir .]
|
|
90
|
-
aienvmp scan [--dir .] [--deep]
|
|
90
|
+
aienvmp scan [--dir .] [--deep] [--security]
|
|
91
91
|
aienvmp intent [--dir .] --actor agent:codex --action "install pnpm"
|
|
92
92
|
aienvmp resolve [--dir .] --actor human:you --id <intent-id> [--status resolved|cancelled]
|
|
93
93
|
aienvmp record [--dir .] --actor agent:codex --summary "updated .nvmrc" [--target node] [--before 20] [--after 24]
|
package/src/commands/context.js
CHANGED
|
@@ -25,6 +25,7 @@ export async function contextWorkspace(args) {
|
|
|
25
25
|
packageManagers: manifest.packageManagers,
|
|
26
26
|
containers: manifest.containers,
|
|
27
27
|
inventory: inventorySummary(manifest.inventory),
|
|
28
|
+
security: securitySummary(manifest.security),
|
|
28
29
|
projectHints: manifest.projectHints,
|
|
29
30
|
warnings,
|
|
30
31
|
policy,
|
|
@@ -37,6 +38,15 @@ export async function contextWorkspace(args) {
|
|
|
37
38
|
console.log(renderContext(manifest, timeline, warnings, intents, policy));
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
function securitySummary(security = {}) {
|
|
42
|
+
return {
|
|
43
|
+
mode: security.mode || "basic",
|
|
44
|
+
enabled: security.enabled === true,
|
|
45
|
+
summary: security.summary || { total: 0, critical: 0, high: 0, moderate: 0, low: 0, info: 0 },
|
|
46
|
+
topPackages: security.topPackages || []
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
function inventorySummary(inventory = {}) {
|
|
41
51
|
const tools = inventory.tools || {};
|
|
42
52
|
return {
|
package/src/commands/handoff.js
CHANGED
|
@@ -53,6 +53,7 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
|
|
|
53
53
|
docker: manifest.containers?.docker ? "available" : "not detected"
|
|
54
54
|
},
|
|
55
55
|
inventory: inventorySummary(manifest.inventory),
|
|
56
|
+
security: securitySummary(manifest.security),
|
|
56
57
|
policy: {
|
|
57
58
|
node: policy.node || "not set",
|
|
58
59
|
python: policy.python || "not set",
|
|
@@ -74,6 +75,15 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
|
|
|
74
75
|
};
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
function securitySummary(security = {}) {
|
|
79
|
+
return {
|
|
80
|
+
mode: security.mode || "basic",
|
|
81
|
+
enabled: security.enabled === true,
|
|
82
|
+
summary: security.summary || { total: 0, critical: 0, high: 0, moderate: 0, low: 0, info: 0 },
|
|
83
|
+
topPackages: security.topPackages || []
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
77
87
|
function inventorySummary(inventory = {}) {
|
|
78
88
|
const tools = inventory.tools || {};
|
|
79
89
|
return {
|
package/src/commands/scan.js
CHANGED
|
@@ -11,7 +11,7 @@ export async function scanWorkspace(args) {
|
|
|
11
11
|
if (previous && await exists(currentPath)) {
|
|
12
12
|
await fs.copyFile(currentPath, previousManifestPath(dir));
|
|
13
13
|
}
|
|
14
|
-
const manifest = await buildManifest(dir, { deep: args.deep });
|
|
14
|
+
const manifest = await buildManifest(dir, { deep: args.deep, security: args.security });
|
|
15
15
|
await writeJson(currentPath, manifest);
|
|
16
16
|
const changes = diffManifests(previous, manifest);
|
|
17
17
|
for (const change of changes) {
|
package/src/doctor.js
CHANGED
|
@@ -42,12 +42,25 @@ export function diagnose(manifest, context = {}) {
|
|
|
42
42
|
message: "Environment snapshot is older than 24 hours. Run `aienvmp sync` before changing the environment."
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
|
+
warnings.push(...securityWarnings(manifest.security));
|
|
45
46
|
warnings.push(...coordinationWarnings(context.intents || []));
|
|
46
47
|
warnings.push(...staleIntentWarnings(context.intents || []));
|
|
47
48
|
warnings.push(...handoffWarnings(context.timeline || []));
|
|
48
49
|
return warnings;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
export function securityWarnings(security = {}) {
|
|
53
|
+
if (!security.enabled || !security.summary) return [];
|
|
54
|
+
const summary = security.summary;
|
|
55
|
+
if (Number(summary.critical || 0) > 0 || Number(summary.high || 0) > 0) {
|
|
56
|
+
return [{
|
|
57
|
+
code: "security-vulnerabilities",
|
|
58
|
+
message: `Security scan found ${summary.critical || 0} critical and ${summary.high || 0} high vulnerabilities. Review before changing or deploying the environment.`
|
|
59
|
+
}];
|
|
60
|
+
}
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
export function coordinationWarnings(intents = []) {
|
|
52
65
|
const warnings = [];
|
|
53
66
|
const byTarget = new Map();
|
package/src/manifest.js
CHANGED
|
@@ -5,6 +5,7 @@ import { commandOutput, commandVersion } from "./shell.js";
|
|
|
5
5
|
import { exists } from "./fsutil.js";
|
|
6
6
|
import { observedTrust } from "./trust.js";
|
|
7
7
|
import { scanGlobalInventory } from "./inventory.js";
|
|
8
|
+
import { scanSecurity } from "./security.js";
|
|
8
9
|
|
|
9
10
|
export async function buildManifest(dir, options = {}) {
|
|
10
11
|
const now = new Date().toISOString();
|
|
@@ -14,7 +15,7 @@ export async function buildManifest(dir, options = {}) {
|
|
|
14
15
|
generatedAt: now,
|
|
15
16
|
generatedBy: {
|
|
16
17
|
name: "aienvmp",
|
|
17
|
-
command: options
|
|
18
|
+
command: generatedCommand(options)
|
|
18
19
|
},
|
|
19
20
|
trust: observedTrust(new Date(now)),
|
|
20
21
|
workspace: {
|
|
@@ -27,6 +28,7 @@ export async function buildManifest(dir, options = {}) {
|
|
|
27
28
|
containers: await scanContainers(),
|
|
28
29
|
projectHints: await scanProjectHints(dir),
|
|
29
30
|
inventory: await scanGlobalInventory({ deep: options.deep }),
|
|
31
|
+
security: await scanSecurity(dir, { security: options.security }),
|
|
30
32
|
agentFiles: await scanAgentFiles(dir),
|
|
31
33
|
agentProtocol: {
|
|
32
34
|
sourceOfTruth: "AIENV.md",
|
|
@@ -78,6 +80,10 @@ export async function buildManifest(dir, options = {}) {
|
|
|
78
80
|
uvTools: "uv tool list",
|
|
79
81
|
brew: "brew list --versions"
|
|
80
82
|
},
|
|
83
|
+
security: {
|
|
84
|
+
mode: "basic by default; vulnerability summaries only when requested",
|
|
85
|
+
npmAudit: "npm audit --json"
|
|
86
|
+
},
|
|
81
87
|
projectHints: [
|
|
82
88
|
".nvmrc",
|
|
83
89
|
".python-version",
|
|
@@ -96,6 +102,10 @@ export async function buildManifest(dir, options = {}) {
|
|
|
96
102
|
return manifest;
|
|
97
103
|
}
|
|
98
104
|
|
|
105
|
+
function generatedCommand(options = {}) {
|
|
106
|
+
return ["aienvmp", "sync", options.deep ? "--deep" : "", options.security ? "--security" : ""].filter(Boolean).join(" ");
|
|
107
|
+
}
|
|
108
|
+
|
|
99
109
|
async function scanOS() {
|
|
100
110
|
return {
|
|
101
111
|
platform: os.platform(),
|
package/src/render.js
CHANGED
|
@@ -31,6 +31,8 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
|
|
|
31
31
|
pushMap(lines, "Containers", manifest.containers);
|
|
32
32
|
lines.push("## Global Tool Inventory", "");
|
|
33
33
|
lines.push(...inventoryLines(manifest.inventory), "");
|
|
34
|
+
lines.push("## Security Summary", "");
|
|
35
|
+
lines.push(...securityLines(manifest.security), "");
|
|
34
36
|
lines.push("## Project Requirements And Hints", "");
|
|
35
37
|
pushMap(lines, "Detected", manifest.projectHints);
|
|
36
38
|
lines.push("## Drift And Warnings", "");
|
|
@@ -96,6 +98,7 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
|
|
|
96
98
|
`Python: ${manifest.runtimes.python || manifest.runtimes.python3 || "not detected"}`,
|
|
97
99
|
`Docker: ${manifest.containers.docker ? "available" : "not detected"}`,
|
|
98
100
|
`Inventory: ${manifest.inventory?.mode || "basic"}${manifest.inventory?.enabled ? " enabled" : " disabled"}`,
|
|
101
|
+
`Security: ${manifest.security?.mode || "basic"}${manifest.security?.enabled ? ` enabled (${manifest.security.summary?.total || 0} vulnerabilities)` : " disabled"}`,
|
|
99
102
|
`Policy Node: ${policy.node || "not set"}`,
|
|
100
103
|
`Policy Python: ${policy.python || "not set"}`,
|
|
101
104
|
`Policy Package Manager: ${policy.packageManager || "not set"}`,
|
|
@@ -135,6 +138,7 @@ export function renderHandoff(handoff) {
|
|
|
135
138
|
`- Python: ${handoff.safeRuntime.python}`,
|
|
136
139
|
`- Docker: ${handoff.safeRuntime.docker}`,
|
|
137
140
|
`- Inventory: ${handoff.inventory?.mode || "basic"}${handoff.inventory?.enabled ? " enabled" : " disabled"}`,
|
|
141
|
+
`- Security: ${handoff.security?.mode || "basic"}${handoff.security?.enabled ? ` enabled (${handoff.security.summary?.total || 0} vulnerabilities)` : " disabled"}`,
|
|
138
142
|
"",
|
|
139
143
|
"Open intents:",
|
|
140
144
|
...(handoff.openIntents.length ? handoff.openIntents.map((i) => `- ${i.actor}: ${i.action}${i.target ? ` (${i.target})` : ""}`) : ["- none"]),
|
|
@@ -213,6 +217,10 @@ const rows=o=>entries(o).map(([k,v])=>\`<tr><th>\${esc(k)}</th><td><code>\${esc(
|
|
|
213
217
|
const inventoryGroups=manifest.inventory?.tools||{};
|
|
214
218
|
const inventoryCount=Object.values(inventoryGroups).reduce((sum,items)=>sum+(Array.isArray(items)?items.length:0),0);
|
|
215
219
|
const inventoryHtml=manifest.inventory?.enabled?('<table>'+Object.entries(inventoryGroups).map(([k,v])=>\`<tr><th>\${esc(k)}</th><td><code>\${Array.isArray(v)?v.length:0} tools</code></td></tr>\`).join('')+'</table>'):'<div class="okline">Deep global inventory is off. Run <code>aienvmp sync --deep</code> when an AI needs global tool awareness.</div>';
|
|
220
|
+
const sec=manifest.security||{};
|
|
221
|
+
const secSummary=sec.summary||{total:0,critical:0,high:0,moderate:0,low:0,info:0};
|
|
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>';
|
|
216
224
|
const change=c=>c.type==='changed'?\`\${c.scope} \${c.key}: \${c.before} -> \${c.after}\`:\`\${c.scope} \${c.key}: \${c.type} \${c.after||c.before}\`;
|
|
217
225
|
const timelineLabel=t=>t.change?change(t.change):(t.summary||t.action||t.type||'recorded change');
|
|
218
226
|
const agentNames={agents:'Codex',claude:'Claude',gemini:'Gemini'};
|
|
@@ -257,6 +265,7 @@ document.getElementById('app').innerHTML=\`
|
|
|
257
265
|
\${card('Containers',manifest.containers?.docker?'<span class="pill">available</span>':'<span class="pill off">not detected</span>',\`<table>\${rows(manifest.containers)}</table>\`)}
|
|
258
266
|
\${card('Project Hints',\`<span class="pill">\${entries(manifest.projectHints).length} hints</span>\`,\`<table>\${rows(manifest.projectHints)}</table>\`)}
|
|
259
267
|
\${card('Global Inventory',manifest.inventory?.enabled?'<span class="pill">deep</span>':'<span class="pill off">basic</span>',inventoryHtml)}
|
|
268
|
+
\${card('Security Summary',sec.enabled?'<span class="pill warn">security</span>':'<span class="pill off">basic</span>',securityHtml)}
|
|
260
269
|
</div>
|
|
261
270
|
<aside>
|
|
262
271
|
\${card('Environment Health',warnings.length?'<span class="pill warn">attention</span>':'<span class="pill">clear</span>',warnHtml)}
|
|
@@ -326,6 +335,27 @@ function inventoryLines(inventory = {}) {
|
|
|
326
335
|
return lines;
|
|
327
336
|
}
|
|
328
337
|
|
|
338
|
+
function securityLines(security = {}) {
|
|
339
|
+
if (!security.enabled) return ["- Mode: basic", "- Security scan is disabled. Run `aienvmp sync --security` when vulnerability context is needed."];
|
|
340
|
+
const summary = security.summary || {};
|
|
341
|
+
const lines = [
|
|
342
|
+
"- Mode: security",
|
|
343
|
+
`- Total vulnerabilities: ${summary.total || 0}`,
|
|
344
|
+
`- Critical: ${summary.critical || 0}`,
|
|
345
|
+
`- High: ${summary.high || 0}`,
|
|
346
|
+
`- Moderate: ${summary.moderate || 0}`,
|
|
347
|
+
`- Low: ${summary.low || 0}`
|
|
348
|
+
];
|
|
349
|
+
const packages = security.topPackages || [];
|
|
350
|
+
if (packages.length) {
|
|
351
|
+
lines.push("- Top vulnerable packages:");
|
|
352
|
+
for (const pkg of packages.slice(0, 8)) {
|
|
353
|
+
lines.push(` - ${pkg.name}: ${pkg.severity}; ${pkg.fixAvailable ? "fix available" : "review required"}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return lines;
|
|
357
|
+
}
|
|
358
|
+
|
|
329
359
|
function policyLines(policy) {
|
|
330
360
|
const lines = [];
|
|
331
361
|
if (policy.node) lines.push(`- Node version policy: ${policy.node}`);
|
package/src/security.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { exists } from "./fsutil.js";
|
|
2
|
+
import { commandResult } from "./shell.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const SEVERITIES = ["critical", "high", "moderate", "low", "info"];
|
|
6
|
+
|
|
7
|
+
export async function scanSecurity(dir, options = {}) {
|
|
8
|
+
if (!options.security) {
|
|
9
|
+
return {
|
|
10
|
+
mode: "basic",
|
|
11
|
+
enabled: false,
|
|
12
|
+
note: "Run `aienvmp sync --security` to collect read-only vulnerability summaries."
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const npmAudit = await scanNpmAudit(dir);
|
|
17
|
+
return {
|
|
18
|
+
mode: "security",
|
|
19
|
+
enabled: true,
|
|
20
|
+
note: "Read-only vulnerability summary. Use for review and CI policy, not automatic fixes.",
|
|
21
|
+
scanners: {
|
|
22
|
+
npmAudit
|
|
23
|
+
},
|
|
24
|
+
summary: summarizeScanners({ npmAudit }),
|
|
25
|
+
topPackages: topVulnerablePackages({ npmAudit })
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function scanNpmAudit(dir) {
|
|
30
|
+
const hasPackageJson = await exists(path.join(dir, "package.json"));
|
|
31
|
+
if (!hasPackageJson) return unavailable("npm-audit", "package.json not found");
|
|
32
|
+
|
|
33
|
+
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
34
|
+
const result = await commandResult(command, ["audit", "--json"], {
|
|
35
|
+
cwd: dir,
|
|
36
|
+
timeout: 15000,
|
|
37
|
+
maxBuffer: 4 * 1024 * 1024
|
|
38
|
+
});
|
|
39
|
+
const parsed = parseNpmAudit(result.stdout);
|
|
40
|
+
if (!parsed.available) {
|
|
41
|
+
return unavailable("npm-audit", result.stderr || "npm audit did not return parseable JSON");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
scanner: "npm-audit",
|
|
45
|
+
available: true,
|
|
46
|
+
ok: result.ok,
|
|
47
|
+
exitCode: result.code,
|
|
48
|
+
summary: parsed.summary,
|
|
49
|
+
vulnerablePackages: parsed.vulnerablePackages
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function parseNpmAudit(raw) {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(raw || "{}");
|
|
56
|
+
const metadata = parsed.metadata || {};
|
|
57
|
+
const vulnerabilities = parsed.vulnerabilities || {};
|
|
58
|
+
const severityCounts = Object.fromEntries(SEVERITIES.map((severity) => [severity, Number(metadata.vulnerabilities?.[severity] || 0)]));
|
|
59
|
+
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
|
+
name,
|
|
62
|
+
severity: value.severity || "unknown",
|
|
63
|
+
viaCount: Array.isArray(value.via) ? value.via.length : 0,
|
|
64
|
+
fixAvailable: Boolean(value.fixAvailable)
|
|
65
|
+
}));
|
|
66
|
+
return {
|
|
67
|
+
available: true,
|
|
68
|
+
summary: { total, ...severityCounts },
|
|
69
|
+
vulnerablePackages
|
|
70
|
+
};
|
|
71
|
+
} catch {
|
|
72
|
+
return { available: false };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function summarizeScanners(scanners = {}) {
|
|
77
|
+
const summary = { total: 0, critical: 0, high: 0, moderate: 0, low: 0, info: 0 };
|
|
78
|
+
for (const scanner of Object.values(scanners)) {
|
|
79
|
+
if (!scanner?.available) continue;
|
|
80
|
+
for (const key of Object.keys(summary)) summary[key] += Number(scanner.summary?.[key] || 0);
|
|
81
|
+
}
|
|
82
|
+
return summary;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function topVulnerablePackages(scanners = {}, limit = 8) {
|
|
86
|
+
const rank = { critical: 0, high: 1, moderate: 2, low: 3, info: 4, unknown: 5 };
|
|
87
|
+
return Object.values(scanners)
|
|
88
|
+
.filter((scanner) => scanner?.available)
|
|
89
|
+
.flatMap((scanner) => (scanner.vulnerablePackages || []).map((pkg) => ({ ...pkg, scanner: scanner.scanner })))
|
|
90
|
+
.sort((a, b) => (rank[a.severity] ?? rank.unknown) - (rank[b.severity] ?? rank.unknown) || a.name.localeCompare(b.name))
|
|
91
|
+
.slice(0, limit);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function unavailable(scanner, reason) {
|
|
95
|
+
return {
|
|
96
|
+
scanner,
|
|
97
|
+
available: false,
|
|
98
|
+
reason
|
|
99
|
+
};
|
|
100
|
+
}
|
package/src/shell.js
CHANGED
|
@@ -20,6 +20,7 @@ export async function commandOutput(command, args = [], options = {}) {
|
|
|
20
20
|
const { stdout } = await execFileAsync(command, args, {
|
|
21
21
|
timeout: options.timeout || 2500,
|
|
22
22
|
maxBuffer: options.maxBuffer || 1024 * 1024,
|
|
23
|
+
cwd: options.cwd,
|
|
23
24
|
windowsHide: true
|
|
24
25
|
});
|
|
25
26
|
return stdout.trim();
|
|
@@ -28,6 +29,25 @@ export async function commandOutput(command, args = [], options = {}) {
|
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
export async function commandResult(command, args = [], options = {}) {
|
|
33
|
+
try {
|
|
34
|
+
const { stdout, stderr } = await execFileAsync(command, args, {
|
|
35
|
+
timeout: options.timeout || 5000,
|
|
36
|
+
maxBuffer: options.maxBuffer || 2 * 1024 * 1024,
|
|
37
|
+
cwd: options.cwd,
|
|
38
|
+
windowsHide: true
|
|
39
|
+
});
|
|
40
|
+
return { ok: true, code: 0, stdout: stdout.trim(), stderr: stderr.trim() };
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
code: typeof error.code === "number" ? error.code : 1,
|
|
45
|
+
stdout: String(error.stdout || "").trim(),
|
|
46
|
+
stderr: String(error.stderr || error.message || "").trim()
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
export function firstVersion(text) {
|
|
32
52
|
const match = String(text).match(/(?:v)?(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)/);
|
|
33
53
|
return match ? match[1] : null;
|