Skip to content

Commit b67e83f

Browse files
committed
More fix command work
1 parent 3a44545 commit b67e83f

File tree

9 files changed

+404
-250
lines changed

9 files changed

+404
-250
lines changed

src/commands/fix/npm-fix.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { getManifestData } from '@socketsecurity/registry'
2+
import { runScript } from '@socketsecurity/registry/lib/npm'
3+
import {
4+
fetchPackagePackument,
5+
readPackageJson
6+
} from '@socketsecurity/registry/lib/packages'
7+
8+
import constants from '../../constants'
9+
import {
10+
Arborist,
11+
SAFE_ARBORIST_REIFY_OPTIONS_OVERRIDES,
12+
SafeArborist
13+
} from '../../shadow/npm/arborist/lib/arborist'
14+
import {
15+
findPackageNodes,
16+
getAlertsMapFromArborist,
17+
updateNode
18+
} from '../../utils/lockfile/package-lock-json'
19+
import { getCveInfoByAlertsMap } from '../../utils/socket-package-alert'
20+
21+
import type { SafeNode } from '../../shadow/npm/arborist/lib/node'
22+
import type { EnvDetails } from '../../utils/package-environment-detector'
23+
import type { Spinner } from '@socketsecurity/registry/lib/spinner'
24+
25+
const { NPM } = constants
26+
27+
function isTopLevel(tree: SafeNode, node: SafeNode): boolean {
28+
return tree.children.get(node.name) === node
29+
}
30+
31+
type NpmFixOptions = {
32+
spinner?: Spinner | undefined
33+
}
34+
35+
export async function npmFix(
36+
_pkgEnvDetails: EnvDetails,
37+
cwd: string,
38+
options?: NpmFixOptions | undefined
39+
) {
40+
const { spinner } = { __proto__: null, ...options } as NpmFixOptions
41+
42+
spinner?.start()
43+
44+
const arb = new SafeArborist({
45+
path: cwd,
46+
...SAFE_ARBORIST_REIFY_OPTIONS_OVERRIDES
47+
})
48+
49+
await arb.reify()
50+
51+
const alertsMap = await getAlertsMapFromArborist(arb, {
52+
consolidate: true,
53+
include: {
54+
existing: true,
55+
unfixable: false,
56+
upgrade: false
57+
}
58+
})
59+
60+
const infoByPkg = getCveInfoByAlertsMap(alertsMap)
61+
if (!infoByPkg) {
62+
spinner?.stop()
63+
return
64+
}
65+
66+
await arb.buildIdealTree()
67+
68+
const editablePkgJson = await readPackageJson(cwd, { editable: true })
69+
70+
for (const { 0: name, 1: infos } of infoByPkg) {
71+
const revertToIdealTree = arb.idealTree!
72+
arb.idealTree = null
73+
// eslint-disable-next-line no-await-in-loop
74+
await arb.buildIdealTree()
75+
76+
const tree = arb.idealTree!
77+
78+
const hasUpgrade = !!getManifestData(NPM, name)
79+
if (hasUpgrade) {
80+
spinner?.info(`Skipping ${name}. Socket Optimize package exists.`)
81+
continue
82+
}
83+
84+
const nodes = findPackageNodes(tree, name)
85+
86+
const packument =
87+
nodes.length && infos.length
88+
? // eslint-disable-next-line no-await-in-loop
89+
await fetchPackagePackument(name)
90+
: null
91+
if (!packument) {
92+
continue
93+
}
94+
95+
for (let i = 0, { length: nodesLength } = nodes; i < nodesLength; i += 1) {
96+
const node = nodes[i]!
97+
for (
98+
let j = 0, { length: infosLength } = infos;
99+
j < infosLength;
100+
j += 1
101+
) {
102+
const { firstPatchedVersionIdentifier, vulnerableVersionRange } =
103+
infos[j]!
104+
const { version: oldVersion } = node
105+
if (
106+
updateNode(
107+
node,
108+
packument,
109+
vulnerableVersionRange,
110+
firstPatchedVersionIdentifier
111+
)
112+
) {
113+
try {
114+
// eslint-disable-next-line no-await-in-loop
115+
await runScript('test', [], { spinner, stdio: 'ignore' })
116+
117+
spinner?.info(`Patched ${name} ${oldVersion} -> ${node.version}`)
118+
119+
if (isTopLevel(tree, node)) {
120+
for (const depField of [
121+
'dependencies',
122+
'optionalDependencies',
123+
'peerDependencies'
124+
]) {
125+
const { content: pkgJson } = editablePkgJson
126+
const oldVersion = (pkgJson[depField] as any)?.[name]
127+
if (oldVersion) {
128+
const decorator = /^[~^]/.exec(oldVersion)?.[0] ?? ''
129+
;(pkgJson as any)[depField][name] =
130+
`${decorator}${node.version}`
131+
}
132+
}
133+
}
134+
// eslint-disable-next-line no-await-in-loop
135+
await editablePkgJson.save()
136+
} catch {
137+
spinner?.error(`Reverting ${name} to ${oldVersion}`)
138+
arb.idealTree = revertToIdealTree
139+
}
140+
} else {
141+
spinner?.error(`Could not patch ${name} ${oldVersion}`)
142+
}
143+
}
144+
}
145+
}
146+
147+
const arb2 = new Arborist({ path: cwd })
148+
arb2.idealTree = arb.idealTree
149+
await arb2.reify()
150+
151+
spinner?.stop()
152+
}

src/commands/fix/pnpm-fix.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { readWantedLockfile } from '@pnpm/lockfile-file'
2+
3+
import { getManifestData } from '@socketsecurity/registry'
4+
import {
5+
fetchPackagePackument,
6+
readPackageJson
7+
} from '@socketsecurity/registry/lib/packages'
8+
9+
import constants from '../../constants'
10+
import {
11+
SAFE_ARBORIST_REIFY_OPTIONS_OVERRIDES,
12+
SafeArborist
13+
} from '../../shadow/npm/arborist/lib/arborist'
14+
import {
15+
findBestPatchVersion,
16+
findPackageNodes
17+
} from '../../utils/lockfile/package-lock-json'
18+
import { getAlertsMapFromPnpmLockfile } from '../../utils/lockfile/pnpm-lock-yaml'
19+
import { getCveInfoByAlertsMap } from '../../utils/socket-package-alert'
20+
import { runAgentInstall } from '../optimize/run-agent'
21+
22+
import type { EnvDetails } from '../../utils/package-environment-detector'
23+
import type { Spinner } from '@socketsecurity/registry/lib/spinner'
24+
25+
const { NPM, OVERRIDES, PNPM } = constants
26+
27+
type PnpmFixOptions = {
28+
spinner?: Spinner | undefined
29+
}
30+
31+
export async function pnpmFix(
32+
pkgEnvDetails: EnvDetails,
33+
cwd: string,
34+
options?: PnpmFixOptions | undefined
35+
) {
36+
const { spinner } = { __proto__: null, ...options } as PnpmFixOptions
37+
38+
spinner?.start()
39+
40+
const lockfile = await readWantedLockfile(cwd, { ignoreIncompatible: false })
41+
if (!lockfile) {
42+
spinner?.stop()
43+
return
44+
}
45+
46+
const alertsMap = await getAlertsMapFromPnpmLockfile(lockfile, {
47+
consolidate: true,
48+
include: {
49+
existing: true,
50+
unfixable: false,
51+
upgrade: false
52+
}
53+
})
54+
55+
const infoByPkg = getCveInfoByAlertsMap(alertsMap)
56+
if (!infoByPkg) {
57+
spinner?.stop()
58+
return
59+
}
60+
61+
const arb = new SafeArborist({
62+
path: cwd,
63+
...SAFE_ARBORIST_REIFY_OPTIONS_OVERRIDES
64+
})
65+
66+
await arb.loadActual()
67+
68+
const editablePkgJson = await readPackageJson(cwd, { editable: true })
69+
const { content: pkgJson } = editablePkgJson
70+
71+
for (const { 0: name, 1: infos } of infoByPkg) {
72+
const tree = arb.actualTree!
73+
74+
const hasUpgrade = !!getManifestData(NPM, name)
75+
if (hasUpgrade) {
76+
spinner?.info(`Skipping ${name}. Socket Optimize package exists.`)
77+
continue
78+
}
79+
80+
const nodes = findPackageNodes(tree, name)
81+
82+
const packument =
83+
nodes.length && infos.length
84+
? // eslint-disable-next-line no-await-in-loop
85+
await fetchPackagePackument(name)
86+
: null
87+
if (!packument) {
88+
continue
89+
}
90+
91+
for (let i = 0, { length: nodesLength } = nodes; i < nodesLength; i += 1) {
92+
const node = nodes[i]!
93+
for (
94+
let j = 0, { length: infosLength } = infos;
95+
j < infosLength;
96+
j += 1
97+
) {
98+
const { firstPatchedVersionIdentifier, vulnerableVersionRange } =
99+
infos[j]!
100+
const { version: oldVersion } = node
101+
const availableVersions = Object.keys(packument.versions)
102+
// Find the highest non-vulnerable version within the same major range
103+
const targetVersion = findBestPatchVersion(
104+
node,
105+
availableVersions,
106+
vulnerableVersionRange,
107+
firstPatchedVersionIdentifier
108+
)
109+
const targetPackument = targetVersion
110+
? packument.versions[targetVersion]
111+
: undefined
112+
if (targetPackument) {
113+
const oldPnpm = (pkgJson as any)[PNPM]
114+
const oldOverrides = oldPnpm?.[OVERRIDES] as
115+
| { [key: string]: string }
116+
| undefined
117+
try {
118+
editablePkgJson.update({
119+
[PNPM]: {
120+
...oldPnpm,
121+
[OVERRIDES]: {
122+
[`${node.name}@${vulnerableVersionRange}`]: `^${targetVersion}`,
123+
...oldOverrides
124+
}
125+
}
126+
})
127+
128+
spinner?.info(`Patched ${name} ${oldVersion} -> ${node.version}`)
129+
130+
// eslint-disable-next-line no-await-in-loop
131+
await editablePkgJson.save()
132+
// eslint-disable-next-line no-await-in-loop
133+
await runAgentInstall(pkgEnvDetails, { spinner })
134+
} catch {
135+
spinner?.error(`Reverting ${name} to ${oldVersion}`)
136+
}
137+
} else {
138+
spinner?.error(`Could not patch ${name} ${oldVersion}`)
139+
}
140+
}
141+
}
142+
}
143+
144+
spinner?.stop()
145+
}

src/commands/fix/run-fix.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { npmFix } from './run-npm-fix'
1+
import { logger } from '@socketsecurity/registry/lib/logger'
2+
3+
import { npmFix } from './npm-fix'
4+
import { pnpmFix } from './pnpm-fix'
25
import constants from '../../constants'
3-
import { detectPackageEnvironment } from '../../utils/package-environment-detector'
6+
import { detectAndValidatePackageEnvironment } from '../optimize/detect-and-validate-package-environment'
47

5-
const { NPM } = constants
8+
const { NPM, PNPM } = constants
69

710
export async function runFix() {
811
// Lazily access constants.spinner.
@@ -11,9 +14,24 @@ export async function runFix() {
1114
spinner.start()
1215

1316
const cwd = process.cwd()
14-
const details = await detectPackageEnvironment({ cwd })
15-
if (details.agent === NPM) {
16-
await npmFix(cwd)
17+
18+
const pkgEnvDetails = await detectAndValidatePackageEnvironment(cwd, {
19+
logger
20+
})
21+
if (!pkgEnvDetails) {
22+
spinner.stop()
23+
return
24+
}
25+
26+
switch (pkgEnvDetails.agent) {
27+
case NPM: {
28+
await npmFix(pkgEnvDetails, cwd)
29+
break
30+
}
31+
case PNPM: {
32+
await pnpmFix(pkgEnvDetails, cwd)
33+
break
34+
}
1735
}
18-
spinner.stop()
36+
spinner.successAndStop('Socket.dev fix successful')
1937
}

0 commit comments

Comments
 (0)