-
Notifications
You must be signed in to change notification settings - Fork 534
/
apply-patch.js
192 lines (171 loc) · 6.27 KB
/
apply-patch.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
module.exports = applyPatch;
const debug = require('debug')('snyk');
const diff = require('diff');
const exec = require('child_process').exec;
const path = require('path');
const fs = require('fs');
const uuid = require('uuid/v4');
const semver = require('semver');
const errorAnalytics = require('../analytics').single;
function applyPatch(patchFileName, vuln, live, patchUrl) {
let cwd = vuln.source;
return new Promise(((resolve, reject) => {
if (!cwd) {
cwd = process.cwd();
}
const relative = path.relative(process.cwd(), cwd);
debug('DRY RUN: relative: %s', relative);
try {
let pkg = {};
const packageJsonPath = path.resolve(relative, 'package.json');
try {
const packageJson = fs.readFileSync(packageJsonPath);
pkg = JSON.parse(packageJson);
debug('package at patch target location: %s@%s', pkg.name, pkg.version);
} catch (err) {
debug('Failed loading package.json at %s. Skipping patch!', packageJsonPath, err);
return resolve();
}
const versionOfPackageToPatch = pkg.version;
const patchableVersionsRange = vuln.patches.version;
if (semver.satisfies(versionOfPackageToPatch, patchableVersionsRange)) {
debug('Patch version range %s matches package version %s',
patchableVersionsRange, versionOfPackageToPatch);
} else {
debug('Patch version range %s does not match package version %s. Skipping patch!',
patchableVersionsRange, versionOfPackageToPatch);
return resolve();
}
const patchContent = fs.readFileSync(path.resolve(relative, patchFileName), 'utf8');
jsDiff(patchContent, relative, live).then(() => {
debug('patch succeed');
resolve();
});
} catch (error) {
debug('patch command failed', relative, error);
patchError(error, relative, vuln, patchUrl).catch(reject);
};
}));
}
function jsDiff(patchContent, relative, live) {
const patchedFiles = {};
return new Promise(((resolve, reject) => {
diff.applyPatches(patchContent, {
loadFile: function (index, callback) {
try {
const fileName = trimUpToFirstSlash(index.oldFileName);
if (patchedFiles[fileName]) {
return callback(null, patchedFiles[fileName]);
}
const filePath = path.resolve(relative, fileName);
const content = fs.readFileSync(filePath, 'utf8');
// create an `.orig` copy of the file prior to patching it
// used in case we need to revert a patch
const origFilePath = filePath + '.orig';
fs.writeFileSync(origFilePath, content);
callback(null, content);
} catch (err) {
// collect patch metadata for error analysis
err.patchIssue = JSON.stringify(index);
callback(err);
}
},
patched: function (index, content, callback) {
try {
if (content === false) {
// `false` means the patch does not match the original content.
const error = new Error('Found a mismatching patch');
error.patchIssue = JSON.stringify(index);
throw error;
}
const newFileName = trimUpToFirstSlash(index.newFileName);
const oldFileName = trimUpToFirstSlash(index.oldFileName);
if (newFileName !== oldFileName) {
patchedFiles[oldFileName] = null;
}
patchedFiles[newFileName] = content;
callback();
} catch (err) {
callback(err);
}
},
compareLine: function (_, line, operation, patchContent) {
if (operation === ' ') {
// Ignore when no patch operators as GNU patch does
return true;
}
return line === patchContent;
},
complete: function (error) {
if (error) {
return reject(error);
}
if (!live) {
return resolve();
}
try {
// write patched files back to disk, unlink files completely removed by patching
for (const fileName in patchedFiles) {
if (typeof patchedFiles[fileName] === 'string') {
fs.writeFileSync(path.resolve(relative, fileName), patchedFiles[fileName]);
} else {
fs.unlinkSync(path.resolve(relative, fileName));
}
}
resolve();
} catch (err) {
reject(err);
}
},
});
}));
}
// diff data compares the same file with a dummy path (a/path/to/real.file vs b/path/to/real.file)
// skipping the dummy folder name by trimming up to the first slash
function trimUpToFirstSlash(fileName) {
return fileName && fileName.replace(/^[^\/]+\//, '');
}
function patchError(error, dir, vuln, patchUrl) {
if (error && error.code === 'ENOENT') {
error.message = 'Failed to patch: the target could not be found (' + error.message + ').';
return Promise.reject(error);
}
return new Promise(((resolve, reject) => {
const id = vuln.id;
exec('npm -v', {
env: process.env,
}, (npmVError, versions) => { // stderr is ignored
const npmVersion = versions && versions.split('\n').shift();
const referenceId = uuid();
// this is a general "patch failed", since we already check if the
// patch was applied via a flag, this means something else went
// wrong, so we'll ask the user for help to diagnose.
const filename = path.relative(process.cwd(), dir);
// post metadata to help diagnose
errorAnalytics({
command: 'patch-fail',
metadata: {
from: vuln.from.slice(1),
vulnId: id,
packageName: vuln.name,
packageVersion: vuln.version,
package: vuln.name + '@' + vuln.version,
patchError: Object.assign({}, {
message: error.message,
stack: error.stack,
name: error.name,
}, error),
'npm-version': npmVersion,
referenceId: referenceId,
patchUrl: patchUrl,
filename: filename,
},
});
const msg = id + ' on ' + vuln.name + '@' + vuln.version + ' at "' + filename + '"\n' +
error + ', ' + 'reference ID: ' + referenceId + '\n';
error = new Error(msg);
error.code = 'FAIL_PATCH';
reject(error);
});
}));
}