fg-devkit 1.4.0 → 1.5.0
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/README.md +30 -0
- package/configs/dependency-policy.vue-workspace.json +6 -0
- package/package.json +4 -1
- package/scripts/check-dependency-policy.mjs +26 -7
- package/scripts/fg-devkit.mjs +16 -35
- package/scripts/fleet-vulns.mjs +164 -0
- package/scripts/policy-vulns.mjs +3 -7
- package/scripts/refresh-dependency-policy-from-repos.mjs +10 -0
- package/scripts/repo-vulns.mjs +26 -0
- package/scripts/sync-dependency-policy.mjs +27 -2
package/README.md
CHANGED
|
@@ -78,6 +78,36 @@ Check outdated policy versions:
|
|
|
78
78
|
pnpm exec fgd policy-outdated
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
Check vulnerabilities for all policy packages together (including transitive dependencies):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pnpm exec fgd policy-vulns
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Check vulnerabilities for all dependencies in the current repo:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pnpm exec fgd repo-vulns
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Check vulnerabilities across all Vue repositories under the projects root:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pnpm exec fgd fleet-vulns
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
For summary-only mode (no full per-repo pnpm audit output):
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pnpm exec fgd fleet-vulns --summary-only
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
If npm audit is rate-limited (429), slow the scan down:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pnpm exec fgd fleet-vulns --delay-ms=8000
|
|
109
|
+
```
|
|
110
|
+
|
|
81
111
|
Upgrade policy versions interactively:
|
|
82
112
|
|
|
83
113
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fg-devkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Shared configs, scripts, and DX tooling for Vue / TypeScript frontend repos",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
"./scripts/sync-consumer-lefthook.mjs": "./scripts/sync-consumer-lefthook.mjs",
|
|
30
30
|
"./scripts/prune-consumer-deps.mjs": "./scripts/prune-consumer-deps.mjs",
|
|
31
31
|
"./scripts/policy-outdated.mjs": "./scripts/policy-outdated.mjs",
|
|
32
|
+
"./scripts/policy-vulns.mjs": "./scripts/policy-vulns.mjs",
|
|
33
|
+
"./scripts/repo-vulns.mjs": "./scripts/repo-vulns.mjs",
|
|
34
|
+
"./scripts/fleet-vulns.mjs": "./scripts/fleet-vulns.mjs",
|
|
32
35
|
"./scripts/policy-upgrade.mjs": "./scripts/policy-upgrade.mjs",
|
|
33
36
|
"./scripts/check-dependency-policy.mjs": "./scripts/check-dependency-policy.mjs",
|
|
34
37
|
"./scripts/sync-dependency-policy.mjs": "./scripts/sync-dependency-policy.mjs",
|
|
@@ -21,6 +21,7 @@ import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mj
|
|
|
21
21
|
|
|
22
22
|
const cwd = process.cwd()
|
|
23
23
|
const pkgPath = path.resolve(cwd, 'package.json')
|
|
24
|
+
const lockfilePath = path.resolve(cwd, 'pnpm-lock.yaml')
|
|
24
25
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|
25
26
|
const policyPath = resolveDependencyPolicyPath(scriptDir, cwd, process.argv)
|
|
26
27
|
|
|
@@ -42,6 +43,30 @@ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
|
42
43
|
const policy = JSON.parse(fs.readFileSync(policyPath, 'utf8'))
|
|
43
44
|
const mismatches = []
|
|
44
45
|
|
|
46
|
+
function getOverridePackageName(overrideKey) {
|
|
47
|
+
const match = overrideKey.match(/^(@[^/]+\/[^@]+|[^@]+)@/)
|
|
48
|
+
return match ? match[1] : overrideKey
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getRelevantPolicyOverrides(policyOverrides) {
|
|
52
|
+
const entries = Object.entries(policyOverrides ?? {})
|
|
53
|
+
if (entries.length === 0) {
|
|
54
|
+
return {}
|
|
55
|
+
}
|
|
56
|
+
if (!fs.existsSync(lockfilePath)) {
|
|
57
|
+
return Object.fromEntries(entries)
|
|
58
|
+
}
|
|
59
|
+
const lockfile = fs.readFileSync(lockfilePath, 'utf8')
|
|
60
|
+
const relevant = {}
|
|
61
|
+
for (const [name, version] of entries) {
|
|
62
|
+
const pkgName = getOverridePackageName(name)
|
|
63
|
+
if (lockfile.includes(`${pkgName}@`) || lockfile.includes(`/${pkgName}/`)) {
|
|
64
|
+
relevant[name] = version
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return relevant
|
|
68
|
+
}
|
|
69
|
+
|
|
45
70
|
/** True if this package+section is governed by the policy (so global pin checks skip it). */
|
|
46
71
|
function isPolicyManagedInSection(section, name) {
|
|
47
72
|
if (policy.managedAnywhere?.[name] !== undefined) {
|
|
@@ -56,7 +81,6 @@ for (const [section, dependencies] of Object.entries(policy.managedDependencies
|
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
mismatches.push(...collectPinViolations('[policy] managedAnywhere.', policy.managedAnywhere ?? {}))
|
|
59
|
-
mismatches.push(...collectPinViolations('[policy] overrides.', policy.overrides ?? {}))
|
|
60
84
|
|
|
61
85
|
// Intersection: only packages that exist in both policy and consumer are compared.
|
|
62
86
|
for (const [section, dependencies] of Object.entries(policy.managedDependencies ?? {})) {
|
|
@@ -98,12 +122,10 @@ for (const [name, expected] of Object.entries(policy.managedAnywhere ?? {})) {
|
|
|
98
122
|
|
|
99
123
|
// Optional: policy may define pnpm.overrides entries the consumer must mirror.
|
|
100
124
|
const overrides = pkg.pnpm?.overrides ?? {}
|
|
101
|
-
for (const [name, expected] of Object.entries(policy.overrides ?? {})) {
|
|
125
|
+
for (const [name, expected] of Object.entries(getRelevantPolicyOverrides(policy.overrides ?? {}))) {
|
|
102
126
|
const actual = overrides[name]
|
|
103
127
|
if (actual !== expected) {
|
|
104
128
|
mismatches.push(`pnpm.overrides.${name}: expected ${expected}, got ${actual ?? '(missing)'}`)
|
|
105
|
-
} else if (actual !== undefined && !isPinnedDependencyVersion(actual)) {
|
|
106
|
-
mismatches.push(`pnpm.overrides.${name}: must be pinned, got ${JSON.stringify(actual)}`)
|
|
107
129
|
}
|
|
108
130
|
}
|
|
109
131
|
|
|
@@ -121,9 +143,6 @@ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies'
|
|
|
121
143
|
}
|
|
122
144
|
}
|
|
123
145
|
|
|
124
|
-
// Consumer pnpm.overrides must be pinned (ranges reported even if not in policy).
|
|
125
|
-
mismatches.push(...collectPinViolations('[pins] pnpm.overrides.', pkg.pnpm?.overrides ?? {}))
|
|
126
|
-
|
|
127
146
|
if (mismatches.length > 0) {
|
|
128
147
|
console.error('[check-deps] Dependency policy mismatches found:')
|
|
129
148
|
for (const mismatch of mismatches) {
|
package/scripts/fg-devkit.mjs
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* pnpm exec fg-devkit sync-deps [policy-file]
|
|
8
8
|
* pnpm exec fg-devkit policy-outdated [policy-file]
|
|
9
9
|
* pnpm exec fg-devkit policy-vulns [policy-file]
|
|
10
|
+
* pnpm exec fg-devkit repo-vulns
|
|
11
|
+
* pnpm exec fg-devkit fleet-vulns [projects-root] [--summary-only] [--delay-ms=<ms>]
|
|
10
12
|
* pnpm exec fg-devkit policy-upgrade [policy-file] [ncu-args...]
|
|
11
13
|
* pnpm exec fg-devkit sync-project-configs (VS Code + tooling sync; overwrites config files)
|
|
12
14
|
* pnpm exec fg-devkit sync-tooling-configs (tooling sync only; overwrites config files)
|
|
@@ -98,24 +100,6 @@ function runPnpmInstall() {
|
|
|
98
100
|
return r.status ?? 1
|
|
99
101
|
}
|
|
100
102
|
|
|
101
|
-
/** Run pnpm audit in current repo and return normalized status info. */
|
|
102
|
-
function runPnpmAuditCurrentRepo() {
|
|
103
|
-
const r = spawnSync('pnpm', ['audit'], {
|
|
104
|
-
stdio: 'inherit',
|
|
105
|
-
cwd: process.cwd(),
|
|
106
|
-
env: process.env,
|
|
107
|
-
})
|
|
108
|
-
if (r.error) {
|
|
109
|
-
console.error('[fg-devkit]', r.error.message)
|
|
110
|
-
return { ok: false, hasVulns: false, exitCode: 1 }
|
|
111
|
-
}
|
|
112
|
-
const code = r.status ?? 1
|
|
113
|
-
if (code === 1) {
|
|
114
|
-
return { ok: true, hasVulns: true, exitCode: 0 }
|
|
115
|
-
}
|
|
116
|
-
return { ok: code === 0, hasVulns: false, exitCode: code }
|
|
117
|
-
}
|
|
118
|
-
|
|
119
103
|
/** Run Lefthook from fg-devkit bundled dependency set. */
|
|
120
104
|
function runBundledLefthook(args = []) {
|
|
121
105
|
let lefthookBin
|
|
@@ -275,22 +259,9 @@ function runMaintainerPolicyUpgradeAndCommit() {
|
|
|
275
259
|
return commitStaged(commitMessage)
|
|
276
260
|
}
|
|
277
261
|
|
|
278
|
-
/** Maintainer flow: run
|
|
262
|
+
/** Maintainer flow: run full repo vulnerability audit in fg-devkit root. */
|
|
279
263
|
function runMaintainerAuditAll() {
|
|
280
|
-
|
|
281
|
-
const policyCode = runNode('policy-vulns.mjs')
|
|
282
|
-
if (policyCode !== 0) return policyCode
|
|
283
|
-
|
|
284
|
-
console.log('[fg-devkit] Running fg-devkit repository audit...')
|
|
285
|
-
const repoAudit = runPnpmAuditCurrentRepo()
|
|
286
|
-
if (!repoAudit.ok) return repoAudit.exitCode
|
|
287
|
-
|
|
288
|
-
if (repoAudit.hasVulns) {
|
|
289
|
-
console.log('[fg-devkit] Completed: vulnerabilities were reported in fg-devkit repository audit.')
|
|
290
|
-
} else {
|
|
291
|
-
console.log('[fg-devkit] Completed: no vulnerabilities reported in fg-devkit repository audit.')
|
|
292
|
-
}
|
|
293
|
-
return 0
|
|
264
|
+
return runNode('repo-vulns.mjs')
|
|
294
265
|
}
|
|
295
266
|
|
|
296
267
|
/** Prompt release type in CLI before invoking release.sh to avoid a "stuck" terminal feeling. */
|
|
@@ -435,6 +406,8 @@ const DIRECT_COMMANDS = {
|
|
|
435
406
|
'sync-deps': 'sync-dependency-policy.mjs',
|
|
436
407
|
'policy-outdated': 'policy-outdated.mjs',
|
|
437
408
|
'policy-vulns': 'policy-vulns.mjs',
|
|
409
|
+
'repo-vulns': 'repo-vulns.mjs',
|
|
410
|
+
'fleet-vulns': 'fleet-vulns.mjs',
|
|
438
411
|
'policy-upgrade': 'policy-upgrade.mjs',
|
|
439
412
|
'sync-project-configs': 'sync-project-configs.mjs',
|
|
440
413
|
'sync-tooling-configs': 'sync-tooling-configs.mjs',
|
|
@@ -452,7 +425,10 @@ Commands:
|
|
|
452
425
|
check-deps [policy-file] Validate package.json + pins against policy
|
|
453
426
|
sync-deps [policy-file] Align declared versions to policy (existing keys only)
|
|
454
427
|
policy-outdated [policy-file] Show outdated policy packages via pnpm outdated
|
|
455
|
-
policy-vulns [policy-file] Check policy packages
|
|
428
|
+
policy-vulns [policy-file] Check all policy packages together (including transitives) via pnpm audit
|
|
429
|
+
repo-vulns Check all repo dependencies for vulnerabilities via pnpm audit
|
|
430
|
+
fleet-vulns [projects-root] [--summary-only] [--delay-ms=<ms>]
|
|
431
|
+
Run pnpm audit across Vue repos (full output + combined summary)
|
|
456
432
|
policy-upgrade [policy-file] [ncu-args...]
|
|
457
433
|
Upgrade policy packages via npm-check-updates (default interactive)
|
|
458
434
|
sync-project-configs VS Code + tooling sync (overwrites config files)
|
|
@@ -556,9 +532,14 @@ const MAINTAINER_MENU = [
|
|
|
556
532
|
},
|
|
557
533
|
{
|
|
558
534
|
key: 'm6',
|
|
559
|
-
label: 'Maintainer: Check
|
|
535
|
+
label: 'Maintainer: Check fg-devkit repo vulnerabilities',
|
|
560
536
|
run: () => runMaintainerAuditAll(),
|
|
561
537
|
},
|
|
538
|
+
{
|
|
539
|
+
key: 'm7',
|
|
540
|
+
label: 'Maintainer: Check vulnerabilities across all Vue repos',
|
|
541
|
+
run: () => runNode('fleet-vulns.mjs'),
|
|
542
|
+
},
|
|
562
543
|
]
|
|
563
544
|
|
|
564
545
|
function isFgDevkitMaintainerContext() {
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Runs vulnerability audits across Vue repositories and prints both per-repo
|
|
4
|
+
* pnpm audit output and a combined summary.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* pnpm exec fg-devkit fleet-vulns [projects-root] [--summary-only] [--delay-ms=<ms>]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
import { spawnSync } from 'node:child_process'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
|
|
15
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|
16
|
+
const cliArgs = process.argv.slice(2)
|
|
17
|
+
const summaryOnly = cliArgs.includes('--summary-only')
|
|
18
|
+
const projectsRootArg = cliArgs.find((a) => !a.startsWith('-'))
|
|
19
|
+
const projectsRoot = projectsRootArg ? path.resolve(process.cwd(), projectsRootArg) : path.resolve(scriptDir, '../..')
|
|
20
|
+
const delayArg = cliArgs.find((a) => a.startsWith('--delay-ms='))
|
|
21
|
+
const delayMs = Number(delayArg?.split('=')[1] ?? 2500)
|
|
22
|
+
|
|
23
|
+
function sleep(ms) {
|
|
24
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function walk(dir, acc = []) {
|
|
28
|
+
for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
29
|
+
if (item.name === 'node_modules' || item.name === '.git') continue
|
|
30
|
+
const p = path.join(dir, item.name)
|
|
31
|
+
if (item.isDirectory()) walk(p, acc)
|
|
32
|
+
else if (item.name === 'package.json') acc.push(p)
|
|
33
|
+
}
|
|
34
|
+
return acc
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readJson(p) {
|
|
38
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isVueProject(pkg) {
|
|
42
|
+
return Boolean(pkg.dependencies?.vue || pkg.devDependencies?.vue)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function countTotal(severityMap = {}) {
|
|
46
|
+
return Object.values(severityMap).reduce((sum, n) => sum + Number(n || 0), 0)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const packageJsonFiles = walk(projectsRoot)
|
|
50
|
+
const repos = []
|
|
51
|
+
for (const packageJsonPath of packageJsonFiles) {
|
|
52
|
+
try {
|
|
53
|
+
const pkg = readJson(packageJsonPath)
|
|
54
|
+
const repoDir = path.dirname(packageJsonPath)
|
|
55
|
+
if (!isVueProject(pkg)) continue
|
|
56
|
+
if (pkg.name === 'fg-devkit') continue
|
|
57
|
+
repos.push(repoDir)
|
|
58
|
+
} catch {
|
|
59
|
+
// Ignore invalid package.json files.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const uniqueRepos = [...new Set(repos)].sort((a, b) => a.localeCompare(b))
|
|
64
|
+
if (uniqueRepos.length === 0) {
|
|
65
|
+
console.log(`[fleet-vulns] No Vue repositories found under ${projectsRoot}`)
|
|
66
|
+
process.exit(0)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(`[fleet-vulns] projectsRoot=${projectsRoot}`)
|
|
70
|
+
console.log(`[fleet-vulns] repositories=${uniqueRepos.length}`)
|
|
71
|
+
|
|
72
|
+
const results = []
|
|
73
|
+
let hasFailures = false
|
|
74
|
+
let hasVulnerabilities = false
|
|
75
|
+
|
|
76
|
+
for (const repoDir of uniqueRepos) {
|
|
77
|
+
const repoName = path.basename(repoDir)
|
|
78
|
+
const lockfilePath = path.join(repoDir, 'pnpm-lock.yaml')
|
|
79
|
+
if (!fs.existsSync(lockfilePath)) {
|
|
80
|
+
console.log(`[fleet-vulns] SKIP ${repoName}: missing pnpm-lock.yaml`)
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(`\n[fleet-vulns] Auditing ${repoName}`)
|
|
85
|
+
if (!summaryOnly) {
|
|
86
|
+
const prettyAudit = spawnSync('pnpm', ['audit'], {
|
|
87
|
+
cwd: repoDir,
|
|
88
|
+
env: process.env,
|
|
89
|
+
stdio: 'inherit',
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
if (prettyAudit.error) {
|
|
93
|
+
hasFailures = true
|
|
94
|
+
console.error(`[fleet-vulns] FAIL ${repoName}: ${prettyAudit.error.message}`)
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
const prettyCode = prettyAudit.status ?? 1
|
|
98
|
+
if (prettyCode !== 0 && prettyCode !== 1) {
|
|
99
|
+
hasFailures = true
|
|
100
|
+
console.error(`[fleet-vulns] FAIL ${repoName}: pnpm audit exited with code ${prettyCode}`)
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const audit = spawnSync('pnpm', ['audit', '--json'], {
|
|
106
|
+
cwd: repoDir,
|
|
107
|
+
env: process.env,
|
|
108
|
+
encoding: 'utf8',
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
if (audit.error) {
|
|
112
|
+
hasFailures = true
|
|
113
|
+
console.error(`[fleet-vulns] FAIL ${repoName}: ${audit.error.message}`)
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let report = null
|
|
118
|
+
try {
|
|
119
|
+
report = JSON.parse(audit.stdout || '{}')
|
|
120
|
+
} catch {
|
|
121
|
+
report = null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!report || typeof report !== 'object') {
|
|
125
|
+
hasFailures = true
|
|
126
|
+
console.error(`[fleet-vulns] FAIL ${repoName}: could not parse pnpm audit --json output`)
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const vulnerabilities = report.metadata?.vulnerabilities ?? {}
|
|
131
|
+
const total = countTotal(vulnerabilities)
|
|
132
|
+
if (total > 0) hasVulnerabilities = true
|
|
133
|
+
|
|
134
|
+
results.push({
|
|
135
|
+
repoName,
|
|
136
|
+
vulnerabilities,
|
|
137
|
+
total,
|
|
138
|
+
dependencies: report.metadata?.totalDependencies ?? 0,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
if (delayMs > 0) {
|
|
142
|
+
await sleep(delayMs)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log('\n[fleet-vulns] Summary')
|
|
147
|
+
for (const r of results) {
|
|
148
|
+
const v = r.vulnerabilities
|
|
149
|
+
console.log(
|
|
150
|
+
`- ${r.repoName}: total=${r.total} (critical=${v.critical ?? 0}, high=${v.high ?? 0}, moderate=${v.moderate ?? 0}, low=${v.low ?? 0}, info=${v.info ?? 0}) deps=${r.dependencies}`,
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (results.length === 0) {
|
|
155
|
+
console.log('[fleet-vulns] No repositories were audited.')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (hasFailures) {
|
|
159
|
+
process.exit(1)
|
|
160
|
+
}
|
|
161
|
+
if (hasVulnerabilities) {
|
|
162
|
+
process.exit(1)
|
|
163
|
+
}
|
|
164
|
+
process.exit(0)
|
package/scripts/policy-vulns.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Checks vulnerabilities for dependency policy entries by projecting
|
|
4
|
-
* into
|
|
3
|
+
* Checks vulnerabilities for all dependency policy entries together by projecting
|
|
4
|
+
* policy data into one temporary package.json and running `pnpm audit` there.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
7
|
* pnpm exec fg-devkit policy-vulns [policy-file]
|
|
@@ -22,6 +22,7 @@ function readJson(p) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const policy = readJson(policyPath)
|
|
25
|
+
console.log(`[policy-vulns] policy=${policyPath}`)
|
|
25
26
|
const deps = {
|
|
26
27
|
...(policy.managedDependencies?.dependencies ?? {}),
|
|
27
28
|
...(policy.managedAnywhere ?? {}),
|
|
@@ -48,7 +49,6 @@ fs.writeFileSync(
|
|
|
48
49
|
'utf8',
|
|
49
50
|
)
|
|
50
51
|
|
|
51
|
-
console.log(`[policy-vulns] policy=${policyPath}`)
|
|
52
52
|
const install = spawnSync('pnpm', ['install', '--ignore-scripts'], {
|
|
53
53
|
cwd: tmpDir,
|
|
54
54
|
stdio: 'inherit',
|
|
@@ -71,8 +71,4 @@ if (audit.error) {
|
|
|
71
71
|
console.error('[policy-vulns]', audit.error.message)
|
|
72
72
|
process.exit(1)
|
|
73
73
|
}
|
|
74
|
-
// pnpm audit returns exit code 1 when vulnerabilities are found.
|
|
75
|
-
if ((audit.status ?? 1) === 1) {
|
|
76
|
-
process.exit(0)
|
|
77
|
-
}
|
|
78
74
|
process.exit(audit.status ?? 1)
|
|
@@ -39,6 +39,15 @@ const policyPath = process.argv[3]
|
|
|
39
39
|
? path.resolve(process.cwd(), process.argv[3])
|
|
40
40
|
: path.join(fgDevkitRoot, 'configs/dependency-policy.vue-workspace.json')
|
|
41
41
|
|
|
42
|
+
let existingPolicy = {}
|
|
43
|
+
try {
|
|
44
|
+
if (fs.existsSync(policyPath)) {
|
|
45
|
+
existingPolicy = JSON.parse(fs.readFileSync(policyPath, 'utf8'))
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
existingPolicy = {}
|
|
49
|
+
}
|
|
50
|
+
|
|
42
51
|
/** Relative paths under projectsRoot whose package.json files are ignored (maintainer choice). */
|
|
43
52
|
/** Relative path prefixes under projectsRoot to skip when scanning (add repo names as needed). */
|
|
44
53
|
const SKIP_DIR_PREFIXES = []
|
|
@@ -180,6 +189,7 @@ const policy = {
|
|
|
180
189
|
devDependencies: sortRecord(outDev),
|
|
181
190
|
},
|
|
182
191
|
managedAnywhere: sortRecord(outAny),
|
|
192
|
+
overrides: sortRecord(existingPolicy.overrides ?? {}),
|
|
183
193
|
}
|
|
184
194
|
|
|
185
195
|
const policyErrors = [
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Checks vulnerabilities for the full dependency graph in the current repository
|
|
4
|
+
* by running `pnpm audit` in the working directory.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* pnpm exec fg-devkit repo-vulns
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from 'node:child_process'
|
|
11
|
+
|
|
12
|
+
console.log(`[repo-vulns] cwd=${process.cwd()}`)
|
|
13
|
+
const audit = spawnSync('pnpm', ['audit'], {
|
|
14
|
+
cwd: process.cwd(),
|
|
15
|
+
stdio: 'inherit',
|
|
16
|
+
env: process.env,
|
|
17
|
+
})
|
|
18
|
+
if (audit.error) {
|
|
19
|
+
console.error('[repo-vulns]', audit.error.message)
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
// pnpm audit returns exit code 1 when vulnerabilities are found.
|
|
23
|
+
if ((audit.status ?? 1) === 1) {
|
|
24
|
+
process.exit(0)
|
|
25
|
+
}
|
|
26
|
+
process.exit(audit.status ?? 1)
|
|
@@ -20,6 +20,7 @@ import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mj
|
|
|
20
20
|
|
|
21
21
|
const cwd = process.cwd()
|
|
22
22
|
const pkgPath = path.resolve(cwd, 'package.json')
|
|
23
|
+
const lockfilePath = path.resolve(cwd, 'pnpm-lock.yaml')
|
|
23
24
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|
24
25
|
const policyPath = resolveDependencyPolicyPath(scriptDir, cwd, process.argv)
|
|
25
26
|
|
|
@@ -43,7 +44,6 @@ const policyErrors = [
|
|
|
43
44
|
collectPinViolations(`managedDependencies.${section}.`, deps),
|
|
44
45
|
),
|
|
45
46
|
...collectPinViolations('managedAnywhere.', policy.managedAnywhere ?? {}),
|
|
46
|
-
...collectPinViolations('overrides.', policy.overrides ?? {}),
|
|
47
47
|
]
|
|
48
48
|
|
|
49
49
|
if (policyErrors.length > 0) {
|
|
@@ -57,6 +57,31 @@ if (policyErrors.length > 0) {
|
|
|
57
57
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
58
58
|
const changes = []
|
|
59
59
|
|
|
60
|
+
function getOverridePackageName(overrideKey) {
|
|
61
|
+
const match = overrideKey.match(/^(@[^/]+\/[^@]+|[^@]+)@/)
|
|
62
|
+
return match ? match[1] : overrideKey
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getRelevantPolicyOverrides(policyOverrides) {
|
|
66
|
+
const entries = Object.entries(policyOverrides ?? {})
|
|
67
|
+
if (entries.length === 0) {
|
|
68
|
+
return {}
|
|
69
|
+
}
|
|
70
|
+
if (!fs.existsSync(lockfilePath)) {
|
|
71
|
+
return Object.fromEntries(entries)
|
|
72
|
+
}
|
|
73
|
+
const lockfile = fs.readFileSync(lockfilePath, 'utf8')
|
|
74
|
+
const relevant = {}
|
|
75
|
+
for (const [name, version] of entries) {
|
|
76
|
+
const pkgName = getOverridePackageName(name)
|
|
77
|
+
// Conservative: only apply overrides for packages present in lockfile.
|
|
78
|
+
if (lockfile.includes(`${pkgName}@`) || lockfile.includes(`/${pkgName}/`)) {
|
|
79
|
+
relevant[name] = version
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return relevant
|
|
83
|
+
}
|
|
84
|
+
|
|
60
85
|
// Bump versions for managed dependencies / devDependencies where the key already exists.
|
|
61
86
|
for (const [section, dependencies] of Object.entries(policy.managedDependencies ?? {})) {
|
|
62
87
|
pkg[section] ||= {}
|
|
@@ -104,7 +129,7 @@ pkg.pnpm ||= {}
|
|
|
104
129
|
pkg.pnpm.overrides ||= {}
|
|
105
130
|
|
|
106
131
|
// Apply policy-defined override versions when present in the policy.
|
|
107
|
-
for (const [name, version] of Object.entries(policy.overrides ?? {})) {
|
|
132
|
+
for (const [name, version] of Object.entries(getRelevantPolicyOverrides(policy.overrides ?? {}))) {
|
|
108
133
|
if (pkg.pnpm.overrides[name] !== version) {
|
|
109
134
|
changes.push(`pnpm.overrides.${name}: ${pkg.pnpm.overrides[name] ?? '(missing)'} -> ${version}`)
|
|
110
135
|
pkg.pnpm.overrides[name] = version
|