aienvmp 0.1.36 → 0.1.37
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 +8 -0
- package/README.md +6 -1
- package/package.json +1 -1
- package/src/commands/context.js +26 -0
- package/src/dependencies.js +159 -0
- package/src/manifest.js +3 -1
- package/src/render.js +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.37
|
|
4
|
+
|
|
5
|
+
- Added `lightSbom` to the manifest as an AI-ready package and vulnerability summary.
|
|
6
|
+
- Linked dependency manifests, ecosystem/group counts, vulnerable direct dependencies, and top risk packages into one compact SBOM view.
|
|
7
|
+
- Surfaced dependency change hints in AI-facing outputs and the dashboard so agents and humans can identify relevant manifests before edits.
|
|
8
|
+
- Added read-only lockfile awareness to dependency snapshots and light SBOM hints.
|
|
9
|
+
- Added package manager policy hints from lockfiles to reduce accidental npm/pnpm/yarn drift.
|
|
10
|
+
|
|
3
11
|
## 0.1.36
|
|
4
12
|
|
|
5
13
|
- Added an explicit enforcement profile to the shared AI preflight contract.
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
It helps Codex, Claude, Gemini, and humans avoid silent Node, Python, package manager, dependency, Docker, and security drift.
|
|
12
12
|
|
|
13
|
-
Core loop: scan once, link runtime/dependency/security context, give AI a shared decision contract with
|
|
13
|
+
Core loop: scan once, link runtime/dependency/security context, give AI a shared decision contract with a light SBOM summary, and hand off safe next steps.
|
|
14
14
|
|
|
15
15
|
## Quick Start
|
|
16
16
|
|
|
@@ -108,6 +108,11 @@ aienvmp doctor --strict security # fail only scoped warnings
|
|
|
108
108
|
- dashboard and preflight explain advisory default vs optional strict mode
|
|
109
109
|
- non-blocking unless strict mode is explicitly requested
|
|
110
110
|
- security checks are opt-in and read-only
|
|
111
|
+
- light SBOM is generated from project files and optional scanner summaries
|
|
112
|
+
- dependency change hints point AI agents to the relevant manifest before edits
|
|
113
|
+
- lockfiles are detected read-only and shown before dependency edits
|
|
114
|
+
- package manager policy is inferred from lockfiles to avoid accidental npm/pnpm/yarn drift
|
|
115
|
+
- dashboard mirrors AI-facing SBOM hints so humans can review the same dependency context
|
|
111
116
|
|
|
112
117
|
## Development
|
|
113
118
|
|
package/package.json
CHANGED
package/src/commands/context.js
CHANGED
|
@@ -38,6 +38,7 @@ export async function contextWorkspace(args) {
|
|
|
38
38
|
containers: manifest.containers,
|
|
39
39
|
inventory: inventorySummary(manifest.inventory),
|
|
40
40
|
dependencySnapshot: dependencySummary(manifest.dependencySnapshot),
|
|
41
|
+
lightSbom: lightSbomSummary(manifest.lightSbom),
|
|
41
42
|
security: securitySummary(manifest.security),
|
|
42
43
|
projectHints: manifest.projectHints,
|
|
43
44
|
warnings,
|
|
@@ -51,6 +52,31 @@ export async function contextWorkspace(args) {
|
|
|
51
52
|
console.log(renderContext(manifest, timeline, warnings, intents, policy, actions));
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
function lightSbomSummary(lightSbom = {}) {
|
|
56
|
+
return {
|
|
57
|
+
mode: lightSbom.mode || "light-sbom",
|
|
58
|
+
summary: lightSbom.summary || {
|
|
59
|
+
ecosystems: {},
|
|
60
|
+
managers: {},
|
|
61
|
+
groups: {},
|
|
62
|
+
manifests: [],
|
|
63
|
+
lockfiles: [],
|
|
64
|
+
packages: 0,
|
|
65
|
+
vulnerabilities: 0,
|
|
66
|
+
directVulnerablePackages: 0,
|
|
67
|
+
transitiveOrUnmatchedVulnerablePackages: 0
|
|
68
|
+
},
|
|
69
|
+
topRisk: (lightSbom.topRisk || []).slice(0, 8),
|
|
70
|
+
packageManagerPolicy: lightSbom.packageManagerPolicy || {
|
|
71
|
+
status: "no-lockfile",
|
|
72
|
+
ecosystems: {},
|
|
73
|
+
guidance: "No lockfile policy detected."
|
|
74
|
+
},
|
|
75
|
+
dependencyChangeHints: (lightSbom.dependencyChangeHints || []).slice(0, 8),
|
|
76
|
+
aiUse: lightSbom.aiUse || {}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
54
80
|
function dependencySummary(snapshot = {}) {
|
|
55
81
|
return {
|
|
56
82
|
mode: snapshot.mode || "snapshot",
|
package/src/dependencies.js
CHANGED
|
@@ -3,20 +3,33 @@ import path from "node:path";
|
|
|
3
3
|
import { exists, readJson } from "./fsutil.js";
|
|
4
4
|
|
|
5
5
|
const NODE_GROUPS = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
6
|
+
const LOCKFILE_CANDIDATES = [
|
|
7
|
+
{ file: "package-lock.json", ecosystem: "npm", manager: "npm" },
|
|
8
|
+
{ file: "npm-shrinkwrap.json", ecosystem: "npm", manager: "npm" },
|
|
9
|
+
{ file: "pnpm-lock.yaml", ecosystem: "npm", manager: "pnpm" },
|
|
10
|
+
{ file: "yarn.lock", ecosystem: "npm", manager: "yarn" },
|
|
11
|
+
{ file: "bun.lockb", ecosystem: "npm", manager: "bun" },
|
|
12
|
+
{ file: "uv.lock", ecosystem: "python", manager: "uv" },
|
|
13
|
+
{ file: "poetry.lock", ecosystem: "python", manager: "poetry" },
|
|
14
|
+
{ file: "Pipfile.lock", ecosystem: "python", manager: "pipenv" }
|
|
15
|
+
];
|
|
6
16
|
|
|
7
17
|
export async function scanDependencySnapshot(dir) {
|
|
8
18
|
const node = await scanNodeDependencies(dir);
|
|
9
19
|
const python = await scanPythonDependencies(dir);
|
|
10
20
|
const packages = [...node.packages, ...python.packages];
|
|
11
21
|
const manifests = [...node.manifests, ...python.manifests];
|
|
22
|
+
const lockfiles = await scanDependencyLockfiles(dir);
|
|
12
23
|
return {
|
|
13
24
|
mode: "snapshot",
|
|
14
25
|
enabled: true,
|
|
15
26
|
note: "Read-only dependency snapshot from project files. It does not install, update, or resolve packages.",
|
|
16
27
|
manifests,
|
|
28
|
+
lockfiles,
|
|
17
29
|
summary: {
|
|
18
30
|
ecosystems: [...new Set(packages.map((pkg) => pkg.ecosystem))],
|
|
19
31
|
manifests: manifests.length,
|
|
32
|
+
lockfiles: lockfiles.length,
|
|
20
33
|
packages: packages.length
|
|
21
34
|
},
|
|
22
35
|
packages: packages.slice(0, 80)
|
|
@@ -45,6 +58,144 @@ export function linkVulnerableDependencies(security = {}, snapshot = {}) {
|
|
|
45
58
|
};
|
|
46
59
|
}
|
|
47
60
|
|
|
61
|
+
export function buildLightSbom(snapshot = {}, security = {}) {
|
|
62
|
+
const packages = snapshot.packages || [];
|
|
63
|
+
const vulnerable = security.topPackages || [];
|
|
64
|
+
const directVulnerable = vulnerable.filter((pkg) => pkg.directDependency === true);
|
|
65
|
+
const transitiveOrUnmatched = vulnerable.filter((pkg) => pkg.directDependency !== true);
|
|
66
|
+
const topRisk = vulnerable.slice(0, 8).map((pkg) => ({
|
|
67
|
+
name: pkg.name,
|
|
68
|
+
ecosystem: scannerEcosystem(pkg.scanner),
|
|
69
|
+
severity: pkg.severity || "unknown",
|
|
70
|
+
directDependency: pkg.directDependency === true,
|
|
71
|
+
manifest: pkg.dependency?.manifest || "",
|
|
72
|
+
version: pkg.dependency?.version || pkg.version || "",
|
|
73
|
+
priority: pkg.remediationPriority?.level || "low",
|
|
74
|
+
score: pkg.remediationPriority?.score || 0,
|
|
75
|
+
fixAvailable: pkg.fixAvailable === true || Boolean(pkg.fixVersions?.length),
|
|
76
|
+
fixVersions: (pkg.fixVersions || []).slice(0, 3)
|
|
77
|
+
}));
|
|
78
|
+
return {
|
|
79
|
+
schemaVersion: 1,
|
|
80
|
+
mode: "light-sbom",
|
|
81
|
+
note: "AI-ready package and vulnerability summary from read-only project files and optional scanners.",
|
|
82
|
+
summary: {
|
|
83
|
+
ecosystems: countBy(packages, "ecosystem"),
|
|
84
|
+
managers: countBy(packages, "manager"),
|
|
85
|
+
groups: countBy(packages, "group"),
|
|
86
|
+
manifests: snapshot.manifests || [],
|
|
87
|
+
lockfiles: snapshot.lockfiles || [],
|
|
88
|
+
packages: packages.length,
|
|
89
|
+
vulnerabilities: Number(security.summary?.total || 0),
|
|
90
|
+
directVulnerablePackages: directVulnerable.length,
|
|
91
|
+
transitiveOrUnmatchedVulnerablePackages: transitiveOrUnmatched.length
|
|
92
|
+
},
|
|
93
|
+
topRisk,
|
|
94
|
+
packageManagerPolicy: packageManagerPolicy(snapshot.lockfiles || []),
|
|
95
|
+
dependencyChangeHints: dependencyChangeHints(packages, topRisk, snapshot.lockfiles || []),
|
|
96
|
+
aiUse: {
|
|
97
|
+
beforeDependencyChanges: "Read lightSbom.summary and lightSbom.topRisk before changing dependencies.",
|
|
98
|
+
securityMode: security.enabled ? "scanner-summary" : "scanner-off",
|
|
99
|
+
dependencySource: "project manifests only; no install or resolver is run"
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function packageManagerPolicy(lockfiles = []) {
|
|
105
|
+
const byEcosystem = {};
|
|
106
|
+
for (const lockfile of lockfiles) {
|
|
107
|
+
const ecosystem = lockfile.ecosystem || "unknown";
|
|
108
|
+
const managers = byEcosystem[ecosystem]?.managers || new Set();
|
|
109
|
+
managers.add(lockfile.manager || "unknown");
|
|
110
|
+
byEcosystem[ecosystem] = {
|
|
111
|
+
ecosystem,
|
|
112
|
+
managers,
|
|
113
|
+
lockfiles: [...(byEcosystem[ecosystem]?.lockfiles || []), lockfile.file]
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const ecosystems = Object.fromEntries(Object.entries(byEcosystem).map(([name, value]) => {
|
|
117
|
+
const managers = [...value.managers].sort();
|
|
118
|
+
return [name, {
|
|
119
|
+
managers,
|
|
120
|
+
lockfiles: value.lockfiles.sort(),
|
|
121
|
+
status: managers.length > 1 ? "mixed-lockfiles" : "single-manager",
|
|
122
|
+
recommendedManager: managers[0] || "not-detected",
|
|
123
|
+
guidance: managers.length > 1
|
|
124
|
+
? "Review with the user before dependency changes; multiple package manager lockfiles are present."
|
|
125
|
+
: "Use the detected package manager for dependency changes unless the user says otherwise."
|
|
126
|
+
}];
|
|
127
|
+
}));
|
|
128
|
+
return {
|
|
129
|
+
status: Object.keys(ecosystems).length === 0
|
|
130
|
+
? "no-lockfile"
|
|
131
|
+
: Object.values(ecosystems).some((item) => item.status === "mixed-lockfiles") ? "review-required" : "clear",
|
|
132
|
+
ecosystems,
|
|
133
|
+
guidance: Object.keys(ecosystems).length === 0
|
|
134
|
+
? "No lockfile detected; avoid creating one with an unexpected package manager without user approval."
|
|
135
|
+
: "Preserve existing lockfile and package manager choices during dependency changes."
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function dependencyChangeHints(packages = [], topRisk = [], lockfiles = []) {
|
|
140
|
+
const byManifest = new Map();
|
|
141
|
+
for (const pkg of packages) {
|
|
142
|
+
const key = pkg.manifest || "unknown";
|
|
143
|
+
const entry = byManifest.get(key) || {
|
|
144
|
+
manifest: key,
|
|
145
|
+
ecosystem: pkg.ecosystem || "unknown",
|
|
146
|
+
manager: pkg.manager || "unknown",
|
|
147
|
+
groups: new Set(),
|
|
148
|
+
packages: 0,
|
|
149
|
+
riskPackages: []
|
|
150
|
+
};
|
|
151
|
+
entry.groups.add(pkg.group || "unknown");
|
|
152
|
+
entry.packages += 1;
|
|
153
|
+
byManifest.set(key, entry);
|
|
154
|
+
}
|
|
155
|
+
for (const risk of topRisk) {
|
|
156
|
+
if (!risk.manifest || !byManifest.has(risk.manifest)) continue;
|
|
157
|
+
byManifest.get(risk.manifest).riskPackages.push({
|
|
158
|
+
name: risk.name,
|
|
159
|
+
severity: risk.severity,
|
|
160
|
+
priority: risk.priority,
|
|
161
|
+
fixAvailable: risk.fixAvailable
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return [...byManifest.values()].map((entry) => ({
|
|
165
|
+
manifest: entry.manifest,
|
|
166
|
+
ecosystem: entry.ecosystem,
|
|
167
|
+
manager: entry.manager,
|
|
168
|
+
groups: [...entry.groups].sort(),
|
|
169
|
+
packages: entry.packages,
|
|
170
|
+
riskPackages: entry.riskPackages.slice(0, 5),
|
|
171
|
+
lockfiles: lockfilesForEntry(entry, lockfiles),
|
|
172
|
+
beforeChange: [
|
|
173
|
+
`Read ${entry.manifest} and the active package manager policy before editing dependencies.`,
|
|
174
|
+
lockfilesForEntry(entry, lockfiles).length
|
|
175
|
+
? `Preserve related lockfiles: ${lockfilesForEntry(entry, lockfiles).map((item) => item.file).join(", ")}.`
|
|
176
|
+
: "No related lockfile was detected; do not create one with a different package manager without approval.",
|
|
177
|
+
"Record an intent before dependency or lockfile changes when another AI may be working."
|
|
178
|
+
],
|
|
179
|
+
afterChange: [
|
|
180
|
+
"Run project tests or the narrowest relevant validation.",
|
|
181
|
+
"Run aienvmp sync.",
|
|
182
|
+
"Record the dependency change with aienvmp record."
|
|
183
|
+
]
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function scanDependencyLockfiles(dir) {
|
|
188
|
+
const found = [];
|
|
189
|
+
for (const item of LOCKFILE_CANDIDATES) {
|
|
190
|
+
if (await exists(path.join(dir, item.file))) found.push(item);
|
|
191
|
+
}
|
|
192
|
+
return found;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function lockfilesForEntry(entry, lockfiles = []) {
|
|
196
|
+
return lockfiles.filter((lockfile) => lockfile.ecosystem === entry.ecosystem);
|
|
197
|
+
}
|
|
198
|
+
|
|
48
199
|
export function remediationPriority(pkg = {}, context = {}) {
|
|
49
200
|
const severityScore = { critical: 90, high: 70, moderate: 45, low: 20, info: 5, unknown: 30 };
|
|
50
201
|
const severity = String(pkg.severity || "unknown").toLowerCase();
|
|
@@ -66,6 +217,14 @@ export function remediationPriority(pkg = {}, context = {}) {
|
|
|
66
217
|
return { level, score, reasons };
|
|
67
218
|
}
|
|
68
219
|
|
|
220
|
+
function countBy(items = [], key) {
|
|
221
|
+
return Object.fromEntries(Object.entries(items.reduce((acc, item) => {
|
|
222
|
+
const value = item[key] || "unknown";
|
|
223
|
+
acc[value] = (acc[value] || 0) + 1;
|
|
224
|
+
return acc;
|
|
225
|
+
}, {})).sort(([a], [b]) => a.localeCompare(b)));
|
|
226
|
+
}
|
|
227
|
+
|
|
69
228
|
async function scanNodeDependencies(dir) {
|
|
70
229
|
const file = path.join(dir, "package.json");
|
|
71
230
|
if (!(await exists(file))) return { manifests: [], packages: [] };
|
package/src/manifest.js
CHANGED
|
@@ -6,12 +6,13 @@ import { exists } from "./fsutil.js";
|
|
|
6
6
|
import { observedTrust } from "./trust.js";
|
|
7
7
|
import { scanGlobalInventory } from "./inventory.js";
|
|
8
8
|
import { scanSecurity } from "./security.js";
|
|
9
|
-
import { linkVulnerableDependencies, scanDependencySnapshot } from "./dependencies.js";
|
|
9
|
+
import { buildLightSbom, linkVulnerableDependencies, scanDependencySnapshot } from "./dependencies.js";
|
|
10
10
|
|
|
11
11
|
export async function buildManifest(dir, options = {}) {
|
|
12
12
|
const now = new Date().toISOString();
|
|
13
13
|
const dependencySnapshot = await scanDependencySnapshot(dir);
|
|
14
14
|
const security = linkVulnerableDependencies(await scanSecurity(dir, { security: options.security }), dependencySnapshot);
|
|
15
|
+
const lightSbom = buildLightSbom(dependencySnapshot, security);
|
|
15
16
|
const manifest = {
|
|
16
17
|
schemaName: "aienvmp.runtime-sbom",
|
|
17
18
|
schemaVersion: 1,
|
|
@@ -31,6 +32,7 @@ export async function buildManifest(dir, options = {}) {
|
|
|
31
32
|
containers: await scanContainers(),
|
|
32
33
|
projectHints: await scanProjectHints(dir),
|
|
33
34
|
dependencySnapshot,
|
|
35
|
+
lightSbom,
|
|
34
36
|
inventory: await scanGlobalInventory({ deep: options.deep }),
|
|
35
37
|
security,
|
|
36
38
|
agentFiles: await scanAgentFiles(dir),
|
package/src/render.js
CHANGED
|
@@ -33,6 +33,8 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
|
|
|
33
33
|
lines.push(...inventoryLines(manifest.inventory), "");
|
|
34
34
|
lines.push("## Dependency Snapshot", "");
|
|
35
35
|
lines.push(...dependencyLines(manifest.dependencySnapshot), "");
|
|
36
|
+
lines.push("## Light SBOM", "");
|
|
37
|
+
lines.push(...lightSbomLines(manifest.lightSbom), "");
|
|
36
38
|
lines.push("## Security Summary", "");
|
|
37
39
|
lines.push(...securityLines(manifest.security), "");
|
|
38
40
|
lines.push("## Project Requirements And Hints", "");
|
|
@@ -284,6 +286,14 @@ const deps=manifest.dependencySnapshot||{};
|
|
|
284
286
|
const depSummary=deps.summary||{ecosystems:[],manifests:0,packages:0};
|
|
285
287
|
const depPackages=deps.packages||[];
|
|
286
288
|
const depHtml=depPackages.length?'<table><tr><th>Packages</th><td><code>'+esc(depSummary.packages||0)+'</code></td></tr><tr><th>Ecosystems</th><td><code>'+esc((depSummary.ecosystems||[]).join(', ')||'none')+'</code></td></tr><tr><th>Manifests</th><td><code>'+esc((deps.manifests||[]).join(', ')||'none')+'</code></td></tr></table><div class="timeline">'+depPackages.slice(0,8).map(p=>\`<div class="event"><time>\${esc(p.ecosystem)}</time><div><b>\${esc(p.name)}</b> <code>\${esc(p.version)}</code><div class="path">\${esc(p.manifest)} / \${esc(p.group)}</div></div></div>\`).join('')+'</div>':'<div class="okline">No project dependency manifests detected.</div>';
|
|
289
|
+
const lightSbom=manifest.lightSbom||{};
|
|
290
|
+
const lightSbomSummary=lightSbom.summary||{};
|
|
291
|
+
const pmPolicy=lightSbom.packageManagerPolicy||{};
|
|
292
|
+
const topRisk=lightSbom.topRisk||[];
|
|
293
|
+
const dependencyHints=lightSbom.dependencyChangeHints||[];
|
|
294
|
+
const dependencyHintsHtml=dependencyHints.length?'<div class="timeline">'+dependencyHints.slice(0,5).map(h=>\`<div class="event"><time>\${esc(h.ecosystem||'deps')}</time><div><b>\${esc(h.manifest)}</b> <code>\${esc(h.manager||'unknown')}</code> \${esc(h.packages||0)} packages\${h.riskPackages?.length?\`<div class="path">risk: \${esc(h.riskPackages.map(p=>p.name).join(', '))}</div>\`:''}<div class="path">\${esc((h.groups||[]).join(', ')||'no groups')}\${h.lockfiles?.length?\` / lockfiles: \${esc(h.lockfiles.map(l=>l.file).join(', '))}\`:''}</div></div></div>\`).join('')+'</div>':'<div class="okline">No dependency change hints available.</div>';
|
|
295
|
+
const pmPolicyHtml='<table><tr><th>Status</th><td><code>'+esc(pmPolicy.status||'no-lockfile')+'</code></td></tr><tr><th>Guidance</th><td>'+esc(pmPolicy.guidance||'No lockfile policy detected.')+'</td></tr></table>';
|
|
296
|
+
const lightSbomHtml=\`<table><tr><th>Packages</th><td><code>\${esc(lightSbomSummary.packages||0)}</code></td></tr><tr><th>Vulnerabilities</th><td><code>\${esc(lightSbomSummary.vulnerabilities||0)}</code></td></tr><tr><th>Direct vulnerable</th><td><code>\${esc(lightSbomSummary.directVulnerablePackages||0)}</code></td></tr><tr><th>Manifests</th><td><code>\${esc((lightSbomSummary.manifests||[]).join(', ')||'none')}</code></td></tr><tr><th>Lockfiles</th><td><code>\${esc((lightSbomSummary.lockfiles||[]).map(l=>l.file).join(', ')||'none')}</code></td></tr></table><h3 style="margin-top:12px">Package manager policy</h3>\${pmPolicyHtml}\${topRisk.length?'<div class="timeline">'+topRisk.slice(0,5).map(p=>\`<div class="event"><time>\${esc(p.priority)}</time><div><b>\${esc(p.name)}</b> \${esc(p.severity)} \${p.directDependency?'<code>direct</code>':'<code>transitive</code>'}<div class="path">\${esc(p.manifest||p.ecosystem)} \${esc(p.version||'')}</div></div></div>\`).join('')+'</div>':'<div class="okline">No high-risk package summary in the current light SBOM.</div>'}<h3 style="margin-top:12px">Dependency change hints</h3>\${dependencyHintsHtml}\`;
|
|
287
297
|
const sec=manifest.security||{};
|
|
288
298
|
const secSummary=sec.summary||{total:0,critical:0,high:0,moderate:0,low:0,info:0};
|
|
289
299
|
const secPackages=sec.topPackages||[];
|
|
@@ -353,6 +363,7 @@ document.getElementById('app').innerHTML=\`
|
|
|
353
363
|
\${card('Project Hints',\`<span class="pill">\${entries(manifest.projectHints).length} hints</span>\`,\`<table>\${rows(manifest.projectHints)}</table>\`)}
|
|
354
364
|
\${card('Global Inventory',manifest.inventory?.enabled?'<span class="pill">deep</span>':'<span class="pill off">basic</span>',inventoryHtml)}
|
|
355
365
|
\${card('Dependency Snapshot','<span class="pill">'+(depSummary.packages||0)+' packages</span>',depHtml)}
|
|
366
|
+
\${card('Light SBOM','<span class="pill">'+(lightSbomSummary.packages||0)+' packages</span>',lightSbomHtml)}
|
|
356
367
|
\${card('Security Summary',sec.enabled?'<span class="pill warn">security</span>':'<span class="pill off">basic</span>',securityHtml)}
|
|
357
368
|
</div>
|
|
358
369
|
<aside>
|
|
@@ -451,6 +462,39 @@ function dependencyLines(snapshot = {}) {
|
|
|
451
462
|
return lines;
|
|
452
463
|
}
|
|
453
464
|
|
|
465
|
+
function lightSbomLines(lightSbom = {}) {
|
|
466
|
+
const summary = lightSbom.summary || {};
|
|
467
|
+
const lines = [
|
|
468
|
+
`- Mode: ${lightSbom.mode || "light-sbom"}`,
|
|
469
|
+
`- Packages: ${summary.packages || 0}`,
|
|
470
|
+
`- Vulnerabilities: ${summary.vulnerabilities || 0}`,
|
|
471
|
+
`- Direct vulnerable packages: ${summary.directVulnerablePackages || 0}`,
|
|
472
|
+
`- Transitive or unmatched vulnerable packages: ${summary.transitiveOrUnmatchedVulnerablePackages || 0}`,
|
|
473
|
+
`- Lockfiles: ${(summary.lockfiles || []).map((item) => item.file).join(", ") || "none"}`
|
|
474
|
+
];
|
|
475
|
+
if (lightSbom.packageManagerPolicy) {
|
|
476
|
+
lines.push(`- Package manager policy: ${lightSbom.packageManagerPolicy.status}`);
|
|
477
|
+
lines.push(`- Package manager guidance: ${lightSbom.packageManagerPolicy.guidance}`);
|
|
478
|
+
}
|
|
479
|
+
const risks = lightSbom.topRisk || [];
|
|
480
|
+
if (risks.length) {
|
|
481
|
+
lines.push("- Top risk:");
|
|
482
|
+
for (const item of risks.slice(0, 8)) {
|
|
483
|
+
lines.push(` - ${item.name}: ${item.severity}; ${item.priority}/${item.score}; ${item.directDependency ? "direct" : "transitive-or-unmatched"}${item.version ? `; ${item.version}` : ""}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const hints = lightSbom.dependencyChangeHints || [];
|
|
487
|
+
if (hints.length) {
|
|
488
|
+
lines.push("- Dependency change hints:");
|
|
489
|
+
for (const hint of hints.slice(0, 6)) {
|
|
490
|
+
const risk = hint.riskPackages?.length ? `; risk: ${hint.riskPackages.map((pkg) => pkg.name).join(", ")}` : "";
|
|
491
|
+
const lockfiles = hint.lockfiles?.length ? `; lockfiles: ${hint.lockfiles.map((item) => item.file).join(", ")}` : "";
|
|
492
|
+
lines.push(` - ${hint.manifest}: ${hint.ecosystem}/${hint.manager}; ${hint.packages} packages${risk}${lockfiles}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return lines;
|
|
496
|
+
}
|
|
497
|
+
|
|
454
498
|
function securityLines(security = {}) {
|
|
455
499
|
if (!security.enabled) return ["- Mode: basic", "- Security scan is disabled. Run `aienvmp sync --security` when vulnerability context is needed."];
|
|
456
500
|
const summary = security.summary || {};
|