aienvmp 0.1.35 → 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 +14 -0
- package/README.md +7 -1
- package/package.json +1 -1
- package/src/commands/context.js +26 -0
- package/src/commands/dash.js +2 -0
- package/src/dependencies.js +159 -0
- package/src/manifest.js +3 -1
- package/src/preflight.js +14 -0
- package/src/render.js +49 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.1.36
|
|
12
|
+
|
|
13
|
+
- Added an explicit enforcement profile to the shared AI preflight contract.
|
|
14
|
+
- Surfaced advisory-by-default vs optional strict mode in the dashboard.
|
|
15
|
+
- Clarified that strict checks are intended for CI or explicit human-requested gates, not default local blocking.
|
|
16
|
+
|
|
3
17
|
## 0.1.35
|
|
4
18
|
|
|
5
19
|
- Added a shared AI preflight contract across status, context, plan, and handoff outputs.
|
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
|
|
|
@@ -105,8 +105,14 @@ aienvmp doctor --strict security # fail only scoped warnings
|
|
|
105
105
|
- one advisory engine, optional enforcement with `doctor --ci`
|
|
106
106
|
- scoped enforcement with `doctor --strict security|policy|coordination|all`
|
|
107
107
|
- context and plan expose suggested strict scopes for CI
|
|
108
|
+
- dashboard and preflight explain advisory default vs optional strict mode
|
|
108
109
|
- non-blocking unless strict mode is explicitly requested
|
|
109
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
|
|
110
116
|
|
|
111
117
|
## Development
|
|
112
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/commands/dash.js
CHANGED
|
@@ -9,6 +9,7 @@ import { renderDashboard } from "../render.js";
|
|
|
9
9
|
import { loadPolicy, policyWarnings } from "../policy.js";
|
|
10
10
|
import { recommendedActions } from "../actions.js";
|
|
11
11
|
import { strictResult } from "../enforcement.js";
|
|
12
|
+
import { buildPreflight } from "../preflight.js";
|
|
12
13
|
|
|
13
14
|
export async function dashWorkspace(args) {
|
|
14
15
|
const dir = workspaceDir(args);
|
|
@@ -23,6 +24,7 @@ export async function dashWorkspace(args) {
|
|
|
23
24
|
const planEnvironment = await detectedPlanEnvironment(dir);
|
|
24
25
|
const html = renderDashboard({
|
|
25
26
|
...manifest,
|
|
27
|
+
preflight: buildPreflight(manifest, warnings, intents),
|
|
26
28
|
recommendedActions: recommendedActions(manifest, { warnings, intents }),
|
|
27
29
|
planArtifacts,
|
|
28
30
|
planRemediation,
|
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/preflight.js
CHANGED
|
@@ -16,6 +16,20 @@ export function buildPreflight(manifest = {}, warnings = [], intents = []) {
|
|
|
16
16
|
: "Review warnings or open intents before environment changes.",
|
|
17
17
|
decision,
|
|
18
18
|
enforcement,
|
|
19
|
+
enforcementProfile: {
|
|
20
|
+
defaultMode: "advisory",
|
|
21
|
+
localOperation: "non-blocking",
|
|
22
|
+
strictMode: "optional",
|
|
23
|
+
strictUse: "CI or explicit human-requested checks only",
|
|
24
|
+
reason: "Avoid disrupting shared servers or developer machines while still making drift visible.",
|
|
25
|
+
recommendedStrictCommand: enforcement.recommendedCommand,
|
|
26
|
+
strictCommands: [
|
|
27
|
+
"aienvmp doctor --strict security",
|
|
28
|
+
"aienvmp doctor --strict policy",
|
|
29
|
+
"aienvmp doctor --strict coordination",
|
|
30
|
+
"aienvmp doctor --strict all"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
19
33
|
counts: {
|
|
20
34
|
warnings: warnings.length,
|
|
21
35
|
openIntents: intents.length,
|
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||[];
|
|
@@ -313,6 +323,9 @@ const envStepsHtml=envSteps.length?'<div class="timeline">'+envSteps.map(s=>\`<d
|
|
|
313
323
|
const ciReadiness=manifest.ciReadiness||[];
|
|
314
324
|
const ciHasFailure=ciReadiness.some(s=>s.status==='fail');
|
|
315
325
|
const ciReadinessHtml=ciReadiness.length?'<table>'+ciReadiness.map(s=>\`<tr><th>\${esc(s.scope)}</th><td><code>\${esc(s.status)}</code>\${s.matchedWarningCodes?.length?\` \${esc(s.matchedWarningCodes.join(', '))}\`:''}</td></tr>\`).join('')+'</table>':'<div class="okline">Run <code>aienvmp doctor --strict security|policy|coordination|all</code> to choose CI enforcement scope.</div>';
|
|
326
|
+
const enforcementProfile=manifest.preflight?.enforcementProfile||{};
|
|
327
|
+
const strictCommands=enforcementProfile.strictCommands||[];
|
|
328
|
+
const enforcementHtml=\`<table><tr><th>Default</th><td><code>\${esc(enforcementProfile.defaultMode||'advisory')}</code></td></tr><tr><th>Local</th><td>\${esc(enforcementProfile.localOperation||'non-blocking')}</td></tr><tr><th>Strict</th><td>\${esc(enforcementProfile.strictUse||'CI or explicit checks only')}</td></tr><tr><th>Recommended</th><td><code>\${esc(enforcementProfile.recommendedStrictCommand||'aienvmp doctor --strict all')}</code></td></tr></table><div class="timeline">\${strictCommands.slice(0,4).map(cmd=>\`<div class="event"><time>CI</time><div><code>\${esc(cmd)}</code></div></div>\`).join('')}</div><div class="path">\${esc(enforcementProfile.reason||'Warnings stay advisory unless strict mode is requested.')}</div>\`;
|
|
316
329
|
const card=(title,badge,body)=>\`<section class="card"><div class="card-head"><h2>\${title}</h2>\${badge||''}</div>\${body}</section>\`;
|
|
317
330
|
const reviewRequired=warnings.length>0||intents.length>0;
|
|
318
331
|
const recentChanges=timeline.slice(-8).length;
|
|
@@ -350,6 +363,7 @@ document.getElementById('app').innerHTML=\`
|
|
|
350
363
|
\${card('Project Hints',\`<span class="pill">\${entries(manifest.projectHints).length} hints</span>\`,\`<table>\${rows(manifest.projectHints)}</table>\`)}
|
|
351
364
|
\${card('Global Inventory',manifest.inventory?.enabled?'<span class="pill">deep</span>':'<span class="pill off">basic</span>',inventoryHtml)}
|
|
352
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)}
|
|
353
367
|
\${card('Security Summary',sec.enabled?'<span class="pill warn">security</span>':'<span class="pill off">basic</span>',securityHtml)}
|
|
354
368
|
</div>
|
|
355
369
|
<aside>
|
|
@@ -361,6 +375,8 @@ document.getElementById('app').innerHTML=\`
|
|
|
361
375
|
<div style="height:14px"></div>
|
|
362
376
|
\${card('Environment Steps',envSteps.length?'<span class="pill warn">'+envSteps.length+' items</span>':'<span class="pill off">none</span>',envStepsHtml)}
|
|
363
377
|
<div style="height:14px"></div>
|
|
378
|
+
\${card('Enforcement Mode','<span class="pill">advisory</span>',enforcementHtml)}
|
|
379
|
+
<div style="height:14px"></div>
|
|
364
380
|
\${card('CI Readiness',ciHasFailure?'<span class="pill warn">review</span>':'<span class="pill">ready</span>',ciReadinessHtml)}
|
|
365
381
|
<div style="height:14px"></div>
|
|
366
382
|
\${card('Environment Health',warnings.length?'<span class="pill warn">attention</span>':'<span class="pill">clear</span>',warnHtml)}
|
|
@@ -446,6 +462,39 @@ function dependencyLines(snapshot = {}) {
|
|
|
446
462
|
return lines;
|
|
447
463
|
}
|
|
448
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
|
+
|
|
449
498
|
function securityLines(security = {}) {
|
|
450
499
|
if (!security.enabled) return ["- Mode: basic", "- Security scan is disabled. Run `aienvmp sync --security` when vulnerability context is needed."];
|
|
451
500
|
const summary = security.summary || {};
|