aienvmp 0.1.26 → 0.1.28
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 -3
- package/package.json +1 -1
- package/src/commands/context.js +10 -0
- package/src/commands/plan.js +2 -0
- package/src/dependencies.js +143 -0
- package/src/manifest.js +10 -1
- package/src/render.js +30 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.28
|
|
4
|
+
|
|
5
|
+
- Linked vulnerable package summaries to the dependency snapshot when the package is directly declared.
|
|
6
|
+
- Added direct dependency metadata to remediation steps and dashboard security rows.
|
|
7
|
+
- Kept the linkage read-only and file-based so security context stays lightweight and non-disruptive.
|
|
8
|
+
|
|
9
|
+
## 0.1.27
|
|
10
|
+
|
|
11
|
+
- Added a read-only dependency snapshot to the manifest, context, AIENV.md, and dashboard.
|
|
12
|
+
- Captured npm and Python project dependencies from `package.json`, `requirements.txt`, and `pyproject.toml` without installing or resolving packages.
|
|
13
|
+
- Linked the env map and light SBOM story so AI agents can see runtime, dependency, and vulnerability context together.
|
|
14
|
+
|
|
3
15
|
## 0.1.26
|
|
4
16
|
|
|
5
17
|
- Shared one AI decision contract across `context --json`, `plan`, and `handoff`.
|
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
`aienvmp` is an AI-first environment map for shared coding machines.
|
|
10
10
|
|
|
11
|
-
It helps Codex, Claude, Gemini, and humans avoid silent Node, Python, package manager, Docker, and security drift.
|
|
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, give AI a shared decision contract,
|
|
13
|
+
Core loop: scan once, link runtime/dependency/security context, give AI a shared decision contract, and hand off safe next steps.
|
|
14
14
|
|
|
15
15
|
## Quick Start
|
|
16
16
|
|
|
@@ -48,7 +48,7 @@ AIENV.md
|
|
|
48
48
|
.aienvmp/timeline.jsonl
|
|
49
49
|
.aienvmp/plan.json
|
|
50
50
|
.aienvmp/plan.md
|
|
51
|
-
.aienvmp/dashboard.html # includes plan, remediation, and environment cards
|
|
51
|
+
.aienvmp/dashboard.html # includes dependencies, plan, remediation, and environment cards
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
Trust states are machine-readable: `observed`, `planned`, `changed`, `review`, `verified`, `stale`.
|
package/package.json
CHANGED
package/src/commands/context.js
CHANGED
|
@@ -32,6 +32,7 @@ export async function contextWorkspace(args) {
|
|
|
32
32
|
packageManagers: manifest.packageManagers,
|
|
33
33
|
containers: manifest.containers,
|
|
34
34
|
inventory: inventorySummary(manifest.inventory),
|
|
35
|
+
dependencySnapshot: dependencySummary(manifest.dependencySnapshot),
|
|
35
36
|
security: securitySummary(manifest.security),
|
|
36
37
|
projectHints: manifest.projectHints,
|
|
37
38
|
warnings,
|
|
@@ -45,6 +46,15 @@ export async function contextWorkspace(args) {
|
|
|
45
46
|
console.log(renderContext(manifest, timeline, warnings, intents, policy, actions));
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
function dependencySummary(snapshot = {}) {
|
|
50
|
+
return {
|
|
51
|
+
mode: snapshot.mode || "snapshot",
|
|
52
|
+
enabled: snapshot.enabled === true,
|
|
53
|
+
summary: snapshot.summary || { ecosystems: [], manifests: 0, packages: 0 },
|
|
54
|
+
packages: (snapshot.packages || []).slice(0, 12)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
function securitySummary(security = {}) {
|
|
49
59
|
return {
|
|
50
60
|
mode: security.mode || "basic",
|
package/src/commands/plan.js
CHANGED
|
@@ -175,6 +175,8 @@ function remediationSteps(security = {}) {
|
|
|
175
175
|
package: pkg.name,
|
|
176
176
|
scanner: pkg.scanner || "unknown",
|
|
177
177
|
severity: pkg.severity || "unknown",
|
|
178
|
+
directDependency: pkg.directDependency === true,
|
|
179
|
+
dependency: pkg.dependency || null,
|
|
178
180
|
fixAvailable: pkg.fixAvailable === true,
|
|
179
181
|
fixVersions,
|
|
180
182
|
advisories,
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { exists, readJson } from "./fsutil.js";
|
|
4
|
+
|
|
5
|
+
const NODE_GROUPS = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
6
|
+
|
|
7
|
+
export async function scanDependencySnapshot(dir) {
|
|
8
|
+
const node = await scanNodeDependencies(dir);
|
|
9
|
+
const python = await scanPythonDependencies(dir);
|
|
10
|
+
const packages = [...node.packages, ...python.packages];
|
|
11
|
+
const manifests = [...node.manifests, ...python.manifests];
|
|
12
|
+
return {
|
|
13
|
+
mode: "snapshot",
|
|
14
|
+
enabled: true,
|
|
15
|
+
note: "Read-only dependency snapshot from project files. It does not install, update, or resolve packages.",
|
|
16
|
+
manifests,
|
|
17
|
+
summary: {
|
|
18
|
+
ecosystems: [...new Set(packages.map((pkg) => pkg.ecosystem))],
|
|
19
|
+
manifests: manifests.length,
|
|
20
|
+
packages: packages.length
|
|
21
|
+
},
|
|
22
|
+
packages: packages.slice(0, 80)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function linkVulnerableDependencies(security = {}, snapshot = {}) {
|
|
27
|
+
const dependencyIndex = new Map((snapshot.packages || []).map((pkg) => [dependencyKey(pkg.ecosystem, pkg.name), pkg]));
|
|
28
|
+
return {
|
|
29
|
+
...security,
|
|
30
|
+
topPackages: (security.topPackages || []).map((pkg) => {
|
|
31
|
+
const dependency = dependencyIndex.get(dependencyKey(scannerEcosystem(pkg.scanner), pkg.name));
|
|
32
|
+
return {
|
|
33
|
+
...pkg,
|
|
34
|
+
directDependency: Boolean(dependency),
|
|
35
|
+
dependency: dependency ? {
|
|
36
|
+
ecosystem: dependency.ecosystem,
|
|
37
|
+
manifest: dependency.manifest,
|
|
38
|
+
group: dependency.group,
|
|
39
|
+
version: dependency.version
|
|
40
|
+
} : null
|
|
41
|
+
};
|
|
42
|
+
})
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function scanNodeDependencies(dir) {
|
|
47
|
+
const file = path.join(dir, "package.json");
|
|
48
|
+
if (!(await exists(file))) return { manifests: [], packages: [] };
|
|
49
|
+
const json = await readJson(file, {});
|
|
50
|
+
const packages = [];
|
|
51
|
+
for (const group of NODE_GROUPS) {
|
|
52
|
+
for (const [name, version] of Object.entries(json[group] || {})) {
|
|
53
|
+
packages.push({
|
|
54
|
+
ecosystem: "npm",
|
|
55
|
+
manager: "npm",
|
|
56
|
+
manifest: "package.json",
|
|
57
|
+
group,
|
|
58
|
+
name,
|
|
59
|
+
version: String(version)
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { manifests: ["package.json"], packages };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function scanPythonDependencies(dir) {
|
|
67
|
+
const requirements = await scanRequirementsTxt(dir);
|
|
68
|
+
const pyproject = await scanPyproject(dir);
|
|
69
|
+
return {
|
|
70
|
+
manifests: [...requirements.manifests, ...pyproject.manifests],
|
|
71
|
+
packages: [...requirements.packages, ...pyproject.packages]
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function scanRequirementsTxt(dir) {
|
|
76
|
+
const file = path.join(dir, "requirements.txt");
|
|
77
|
+
if (!(await exists(file))) return { manifests: [], packages: [] };
|
|
78
|
+
const raw = await fs.readFile(file, "utf8");
|
|
79
|
+
const packages = raw
|
|
80
|
+
.split(/\r?\n/)
|
|
81
|
+
.map((line) => line.trim())
|
|
82
|
+
.filter((line) => line && !line.startsWith("#") && !line.startsWith("-"))
|
|
83
|
+
.slice(0, 80)
|
|
84
|
+
.map((line) => {
|
|
85
|
+
const parsed = parseRequirementLine(line);
|
|
86
|
+
return {
|
|
87
|
+
ecosystem: "python",
|
|
88
|
+
manager: "pip",
|
|
89
|
+
manifest: "requirements.txt",
|
|
90
|
+
group: "requirements",
|
|
91
|
+
name: parsed.name,
|
|
92
|
+
version: parsed.version
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
return { manifests: ["requirements.txt"], packages };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function scanPyproject(dir) {
|
|
99
|
+
const file = path.join(dir, "pyproject.toml");
|
|
100
|
+
if (!(await exists(file))) return { manifests: [], packages: [] };
|
|
101
|
+
const raw = await fs.readFile(file, "utf8");
|
|
102
|
+
const packages = parsePyprojectDependencies(raw).slice(0, 80).map((line) => {
|
|
103
|
+
const parsed = parseRequirementLine(line);
|
|
104
|
+
return {
|
|
105
|
+
ecosystem: "python",
|
|
106
|
+
manager: "pyproject",
|
|
107
|
+
manifest: "pyproject.toml",
|
|
108
|
+
group: "project.dependencies",
|
|
109
|
+
name: parsed.name,
|
|
110
|
+
version: parsed.version
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
return { manifests: ["pyproject.toml"], packages };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function parseRequirementLine(line) {
|
|
117
|
+
const cleaned = String(line).split("#")[0].trim();
|
|
118
|
+
const match = cleaned.match(/^([A-Za-z0-9_.-]+)\s*(.*)$/);
|
|
119
|
+
return {
|
|
120
|
+
name: match?.[1] || cleaned,
|
|
121
|
+
version: (match?.[2] || "unspecified").trim() || "unspecified"
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function scannerEcosystem(scanner = "") {
|
|
126
|
+
if (String(scanner).includes("pip")) return "python";
|
|
127
|
+
return "npm";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function dependencyKey(ecosystem = "", name = "") {
|
|
131
|
+
return `${String(ecosystem).toLowerCase()}:${String(name).toLowerCase()}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function parsePyprojectDependencies(raw) {
|
|
135
|
+
const lines = [];
|
|
136
|
+
const match = String(raw).match(/dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
137
|
+
if (!match) return lines;
|
|
138
|
+
for (const item of match[1].split(/\r?\n/)) {
|
|
139
|
+
const cleaned = item.trim().replace(/,$/, "").replace(/^["']|["']$/g, "");
|
|
140
|
+
if (cleaned && !cleaned.startsWith("#")) lines.push(cleaned);
|
|
141
|
+
}
|
|
142
|
+
return lines;
|
|
143
|
+
}
|
package/src/manifest.js
CHANGED
|
@@ -6,9 +6,12 @@ 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
10
|
|
|
10
11
|
export async function buildManifest(dir, options = {}) {
|
|
11
12
|
const now = new Date().toISOString();
|
|
13
|
+
const dependencySnapshot = await scanDependencySnapshot(dir);
|
|
14
|
+
const security = linkVulnerableDependencies(await scanSecurity(dir, { security: options.security }), dependencySnapshot);
|
|
12
15
|
const manifest = {
|
|
13
16
|
schemaName: "aienvmp.runtime-sbom",
|
|
14
17
|
schemaVersion: 1,
|
|
@@ -27,8 +30,9 @@ export async function buildManifest(dir, options = {}) {
|
|
|
27
30
|
packageManagers: await scanPackageManagers(),
|
|
28
31
|
containers: await scanContainers(),
|
|
29
32
|
projectHints: await scanProjectHints(dir),
|
|
33
|
+
dependencySnapshot,
|
|
30
34
|
inventory: await scanGlobalInventory({ deep: options.deep }),
|
|
31
|
-
security
|
|
35
|
+
security,
|
|
32
36
|
agentFiles: await scanAgentFiles(dir),
|
|
33
37
|
agentProtocol: {
|
|
34
38
|
sourceOfTruth: "AIENV.md",
|
|
@@ -84,6 +88,11 @@ export async function buildManifest(dir, options = {}) {
|
|
|
84
88
|
mode: "basic by default; vulnerability summaries only when requested",
|
|
85
89
|
npmAudit: "npm audit --json"
|
|
86
90
|
},
|
|
91
|
+
dependencySnapshot: {
|
|
92
|
+
mode: "read-only project file snapshot",
|
|
93
|
+
node: "package.json dependencies",
|
|
94
|
+
python: "requirements.txt and pyproject.toml dependencies"
|
|
95
|
+
},
|
|
87
96
|
projectHints: [
|
|
88
97
|
".nvmrc",
|
|
89
98
|
".python-version",
|
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("## Dependency Snapshot", "");
|
|
35
|
+
lines.push(...dependencyLines(manifest.dependencySnapshot), "");
|
|
34
36
|
lines.push("## Security Summary", "");
|
|
35
37
|
lines.push(...securityLines(manifest.security), "");
|
|
36
38
|
lines.push("## Project Requirements And Hints", "");
|
|
@@ -99,6 +101,7 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
|
|
|
99
101
|
`Python: ${manifest.runtimes.python || manifest.runtimes.python3 || "not detected"}`,
|
|
100
102
|
`Docker: ${manifest.containers.docker ? "available" : "not detected"}`,
|
|
101
103
|
`Inventory: ${manifest.inventory?.mode || "basic"}${manifest.inventory?.enabled ? " enabled" : " disabled"}`,
|
|
104
|
+
`Dependencies: ${manifest.dependencySnapshot?.summary?.packages || 0} packages across ${(manifest.dependencySnapshot?.summary?.ecosystems || []).join(", ") || "no ecosystems"}`,
|
|
102
105
|
`Security: ${manifest.security?.mode || "basic"}${manifest.security?.enabled ? ` enabled (${manifest.security.summary?.total || 0} vulnerabilities)` : " disabled"}`,
|
|
103
106
|
`Policy Node: ${policy.node || "not set"}`,
|
|
104
107
|
`Policy Python: ${policy.python || "not set"}`,
|
|
@@ -208,8 +211,9 @@ function environmentLines(item) {
|
|
|
208
211
|
function remediationLines(item) {
|
|
209
212
|
const fix = item.fixVersions?.length ? `fix ${item.fixVersions.join(", ")}` : item.fixAvailable ? "fix available" : "review required";
|
|
210
213
|
const advisories = (item.advisories || []).map((advisory) => advisory.id || advisory.title).filter(Boolean).slice(0, 2);
|
|
214
|
+
const dependency = item.directDependency && item.dependency ? `; declared in ${item.dependency.manifest} ${item.dependency.version}` : "; not found in dependency snapshot";
|
|
211
215
|
return [
|
|
212
|
-
`- ${item.package}: ${item.severity}; ${fix}${advisories.length ? `; advisories ${advisories.join(", ")}` : ""}`,
|
|
216
|
+
`- ${item.package}: ${item.severity}; ${fix}${dependency}${advisories.length ? `; advisories ${advisories.join(", ")}` : ""}`,
|
|
213
217
|
...item.steps.slice(0, 4).map((step) => ` - ${step}`)
|
|
214
218
|
];
|
|
215
219
|
}
|
|
@@ -273,12 +277,17 @@ const rows=o=>entries(o).map(([k,v])=>\`<tr><th>\${esc(k)}</th><td><code>\${esc(
|
|
|
273
277
|
const inventoryGroups=manifest.inventory?.tools||{};
|
|
274
278
|
const inventoryCount=Object.values(inventoryGroups).reduce((sum,items)=>sum+(Array.isArray(items)?items.length:0),0);
|
|
275
279
|
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>';
|
|
280
|
+
const deps=manifest.dependencySnapshot||{};
|
|
281
|
+
const depSummary=deps.summary||{ecosystems:[],manifests:0,packages:0};
|
|
282
|
+
const depPackages=deps.packages||[];
|
|
283
|
+
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>';
|
|
276
284
|
const sec=manifest.security||{};
|
|
277
285
|
const secSummary=sec.summary||{total:0,critical:0,high:0,moderate:0,low:0,info:0};
|
|
278
286
|
const secPackages=sec.topPackages||[];
|
|
279
287
|
const securityFix=p=>p.fixVersions?.length?\`fix \${p.fixVersions.slice(0,3).join(', ')}\`:(p.fixAvailable?'fix available':'review required');
|
|
280
288
|
const securityRefs=p=>p.advisories?.length?\` - \${p.advisories.map(a=>a.id||a.title).filter(Boolean).slice(0,2).join(', ')}\`:'';
|
|
281
|
-
const
|
|
289
|
+
const securityDep=p=>p.directDependency&&p.dependency?\`<div class="path">\${esc(p.dependency.manifest)} / \${esc(p.dependency.group)} / \${esc(p.dependency.version)}</div>\`:'<div class="path">not found in dependency snapshot</div>';
|
|
290
|
+
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))}\${securityDep(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>';
|
|
282
291
|
const change=c=>c.type==='changed'?\`\${c.scope} \${c.key}: \${c.before} -> \${c.after}\`:\`\${c.scope} \${c.key}: \${c.type} \${c.after||c.before}\`;
|
|
283
292
|
const timelineLabel=t=>t.change?change(t.change):(t.summary||t.action||t.type||'recorded change');
|
|
284
293
|
const agentNames={agents:'Codex',claude:'Claude',gemini:'Gemini'};
|
|
@@ -336,6 +345,7 @@ document.getElementById('app').innerHTML=\`
|
|
|
336
345
|
\${card('Containers',manifest.containers?.docker?'<span class="pill">available</span>':'<span class="pill off">not detected</span>',\`<table>\${rows(manifest.containers)}</table>\`)}
|
|
337
346
|
\${card('Project Hints',\`<span class="pill">\${entries(manifest.projectHints).length} hints</span>\`,\`<table>\${rows(manifest.projectHints)}</table>\`)}
|
|
338
347
|
\${card('Global Inventory',manifest.inventory?.enabled?'<span class="pill">deep</span>':'<span class="pill off">basic</span>',inventoryHtml)}
|
|
348
|
+
\${card('Dependency Snapshot','<span class="pill">'+(depSummary.packages||0)+' packages</span>',depHtml)}
|
|
339
349
|
\${card('Security Summary',sec.enabled?'<span class="pill warn">security</span>':'<span class="pill off">basic</span>',securityHtml)}
|
|
340
350
|
</div>
|
|
341
351
|
<aside>
|
|
@@ -416,6 +426,22 @@ function inventoryLines(inventory = {}) {
|
|
|
416
426
|
return lines;
|
|
417
427
|
}
|
|
418
428
|
|
|
429
|
+
function dependencyLines(snapshot = {}) {
|
|
430
|
+
const summary = snapshot.summary || {};
|
|
431
|
+
const packages = snapshot.packages || [];
|
|
432
|
+
const ecosystems = summary.ecosystems?.length ? summary.ecosystems.join(", ") : "none";
|
|
433
|
+
const lines = [
|
|
434
|
+
`- Mode: ${snapshot.mode || "snapshot"}`,
|
|
435
|
+
`- Manifests: ${(snapshot.manifests || []).join(", ") || "none"}`,
|
|
436
|
+
`- Ecosystems: ${ecosystems}`,
|
|
437
|
+
`- Packages: ${summary.packages || 0}`
|
|
438
|
+
];
|
|
439
|
+
for (const pkg of packages.slice(0, 10)) {
|
|
440
|
+
lines.push(`- ${pkg.ecosystem}/${pkg.name}: ${pkg.version} (${pkg.manifest})`);
|
|
441
|
+
}
|
|
442
|
+
return lines;
|
|
443
|
+
}
|
|
444
|
+
|
|
419
445
|
function securityLines(security = {}) {
|
|
420
446
|
if (!security.enabled) return ["- Mode: basic", "- Security scan is disabled. Run `aienvmp sync --security` when vulnerability context is needed."];
|
|
421
447
|
const summary = security.summary || {};
|
|
@@ -439,11 +465,12 @@ function securityLines(security = {}) {
|
|
|
439
465
|
|
|
440
466
|
function securityPackageNote(pkg) {
|
|
441
467
|
const fix = pkg.fixVersions?.length ? `fix ${pkg.fixVersions.slice(0, 3).join(", ")}` : pkg.fixAvailable ? "fix available" : "review required";
|
|
468
|
+
const dependency = pkg.directDependency && pkg.dependency ? `; declared in ${pkg.dependency.manifest} ${pkg.dependency.version}` : "; not found in dependency snapshot";
|
|
442
469
|
const advisories = (pkg.advisories || [])
|
|
443
470
|
.map((item) => item.id || item.title)
|
|
444
471
|
.filter(Boolean)
|
|
445
472
|
.slice(0, 2);
|
|
446
|
-
return advisories.length ? `${fix}; advisories ${advisories.join(", ")}` : fix
|
|
473
|
+
return advisories.length ? `${fix}${dependency}; advisories ${advisories.join(", ")}` : `${fix}${dependency}`;
|
|
447
474
|
}
|
|
448
475
|
|
|
449
476
|
function policyLines(policy) {
|