-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathpsCommandService.js
407 lines (337 loc) · 13.1 KB
/
psCommandService.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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
module.exports = PSCommandService;
var Promise = require('promise');
var Mustache = require('mustache');
/**
* Reserved variables in Powershell to allow as arguments
* @see https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables?view=powershell-7.2
*/
const reservedVariableNames = ['$null', '$false', '$true'];
/**
* PSCommandService
*
* @param statefulProcessCommandProxy all commands will be executed over this
*
* @param commandRegistry registry/hash of Powershell commands
* @see o365CommandRegistry.js for examples
*
* @param logFunction optional function that should have the signature
* (severity,origin,message), where log messages will
* be sent to. If null, logs will just go to console
*
*/
function PSCommandService(statefulProcessCommandProxy,commandRegistry,logFunction) {
this._statefulProcessCommandProxy = statefulProcessCommandProxy;
this._commandRegistry = commandRegistry;
this._logFunction = logFunction;
}
// log function for no origin
PSCommandService.prototype._log = function(severity,msg) {
this._log2(severity,this.__proto__.constructor.name,msg);
}
// Log function w/ origin
PSCommandService.prototype._log2 = function(severity,origin,msg) {
if (this._logFunction) {
this._logFunction(severity,origin,msg);
} else {
console.log(severity.toUpperCase() + " " + origin + " " + msg);
}
}
/**
* Returns an array of all available command objects
*
* { commandName:name, command:commandString, arguments:{}, return: {} }
*
*/
PSCommandService.prototype.getAvailableCommands = function() {
var commands = [];
for (var cmd in this._commandRegistry) {
commands.push({
'commandName' : cmd,
'command' : this._commandRegistry[cmd].command,
'arguments' : this._commandRegistry[cmd].arguments,
'return' : this._commandRegistry[cmd].return
});
}
return commands;
}
/**
* getStatus()
*
* Return the status of all managed processes, an array
* of structured ProcessProxy status objects
*/
PSCommandService.prototype.getStatus = function() {
var status = this._statefulProcessCommandProxy.getStatus();
return status;
}
// get a CommandConfig by commandName, throws error otherwise
PSCommandService.prototype._getCommandConfig = function(commandName) {
var commandConfig = this._commandRegistry[commandName];
if (!commandConfig) {
var msg = ("No command registered by name: " + commandName);
this._log('error',msg)
throw new Error(msg);
}
return commandConfig;
}
/**
* generateCommand()
*
* Generates an actual powershell command as registered in the
* command registry, applying the values from the argument map
* returns a literal command string that can be executed
*
*
* @param commandName
* @param argument2ValueMap
* @return command generated, otherwise Error if command not found
*/
PSCommandService.prototype.generateCommand = function(commandName, argument2ValueMap) {
var commandConfig = this._getCommandConfig(commandName);
var generated = this._generateCommand(commandConfig, argument2ValueMap);
return generated;
}
/**
* execute()
*
* Executes a named powershell command as registered in the
* command registry, applying the values from the argument map
* returns a promise that when fulfilled returns the cmdResult
* object from the command which contains properties
* {commandName: name, command:generatedCommand, stdout:xxxx, stderr:xxxxx}
*
* On reject an Error object
*
* @param array of commands
*/
PSCommandService.prototype.execute = function(commandName, argument2ValueMap) {
var command = this.generateCommand(commandName, argument2ValueMap);
var self = this;
return new Promise(function(fulfill,reject) {
self._execute(command)
.then(function(cmdResult) {
// tack on commandName
cmdResult['commandName'] = commandName;
fulfill(cmdResult);
}).catch(function(error){
reject(error);
});
});
}
/**
* executeAll()
*
* Expects an array of commandNames -> argMaps to execute in order
* [
* {commandName: name1, argMap: {param:value, param:value, ...}},
* {commandName: name2, argMap: {param:value, param:value, ...}},
* ]
*
* Executes the named powershell commands as registered in the
* command registry, applying the values from the argument maps
* returns a promise that when fulfilled returns an cmdResults array
* where each entry contains
* [
* {commandName: name1, command:cmd1, stdout:xxxx, stderr:xxxxx},
* {commandName: name2, command:cmd2, stdout:xxxx, stderr:xxxxx}
* ]
*
* On reject an Error object
*
* @param array of {commandName -> arglist}
*/
PSCommandService.prototype.executeAll = function(cmdName2ArgValuesList) {
var commandsToExec = [];
for (var i=0; i<cmdName2ArgValuesList.length; i++) {
var cmdRequest = cmdName2ArgValuesList[i];
var command = this.generateCommand(cmdRequest.commandName, cmdRequest.argMap);
commandsToExec.push(command);
}
var self = this;
// execute and get back ordered results
return new Promise(function(fulfill,reject) {
self._executeCommands(commandsToExec)
.then(function(cmdResults) {
// iterate over them (the order will match the order of the cmdName2ArgValuesList)
// modify each cmdResult adding the commandName attribute
for (var i=0; i<cmdResults.length; i++) {
var cmdResult = cmdResults[i];
var cmdRequest = cmdName2ArgValuesList[i];
cmdResult['commandName'] = cmdRequest.commandName;
}
fulfill(cmdResults);
}).catch(function(error) {
self._log('error','Unexepected error in executeAll(): ' + error + ' ' + error.stack);
reject(error);
});
});
}
/**
* _execute()
*
* Executes one powershell command generated by _generateCommand(),
* returns a promise when fulfilled returns the cmdResult object from the command
* which contains 3 properties (command, stdout, stderr)
*
* On reject an Error Object
*
* @param array of commands
*/
PSCommandService.prototype._execute = function(command) {
var self = this;
return new Promise(function(fulfill,reject) {
self._executeCommands([command])
.then(function(cmdResults) {
fulfill(cmdResults[0]); // only one will return
}).catch(function(error) {
self._log('error','Unexepected error in _execute(): ' + error + ' ' + error.stack);
reject(error);
});
});
}
/**
* _executeCommands()
*
* Executes one or more powershell commands generated by _generateCommand(),
* returns a promise when fulfilled returns an hash of results in the form:
* { <command> : {command: <command>, stdout: value, stderr: value }}
*
* On reject an Error object
*
* @param array of commands
*/
PSCommandService.prototype._executeCommands = function(commands) {
var self = this;
var logBuffer = "";
for (var i=0; i<commands.length; i++) {
logBuffer += commands[i] + "\n";
}
self._log('info','Executing:\n'+logBuffer+'\n');
return new Promise(function(fulfill,reject) {
self._statefulProcessCommandProxy.executeCommands(commands)
.then(function(cmdResults) {
fulfill(cmdResults);
}).catch(function(error) {
self._log('error','Unexepected error from _statefulProcessCommandProxy.executeCommands(): ' + error + ' ' + error.stack);
reject(error);
});
});
}
/**
* _generateCommand()
*
* @param commandConfig a command config object that the argumentMap will be applied to
* @param argument2ValueMap map of argument names -> values (valid for the passed commandConfig)
*
* @return a formatted powershell command string suitable for execution
*
* @throws Error if any exception occurs
*
* !!!! TODO: review security protection for "injection" (i.e command termination, newlines etc)
*/
PSCommandService.prototype._generateCommand = function(commandConfig, argument2ValueMap) {
try {
var argumentsConfig = commandConfig.arguments;
var argumentsString = "";
for (var argumentName in argumentsConfig) {
if(argumentsConfig.hasOwnProperty(argumentName)) {
var argument = argumentsConfig[argumentName];
// is argument valued
if ((argument.hasOwnProperty('valued') ? argument.valued : true)) {
var isQuoted = (argument.hasOwnProperty('quoted') ? argument.quoted : true);
var passedArgValues = argument2ValueMap[argumentName];
if (!(passedArgValues instanceof Array)) {
if (typeof passedArgValues === 'undefined') {
if (argument.hasOwnProperty('default')) {
passedArgValues = [argument.default];
} else {
passedArgValues = [];
}
} else {
passedArgValues = [passedArgValues];
}
}
var argumentValues = "";
for (var i=0; i<passedArgValues.length; i++) {
var passedArgValue = passedArgValues[i];
var valueToSet;
if (passedArgValue && passedArgValue != 'undefined') {
valueToSet = passedArgValue;
} else if (argument.hasOwnProperty('default')) {
valueToSet = argument.default;
}
// append the value
if (valueToSet && valueToSet.trim().length > 0) {
// sanitize
valueToSet = this._sanitize(valueToSet,isQuoted);
// append w/ quotes (SINGLE QUOTES, not double to avoid expansion)
argumentValues += (this._finalizeParameterValue(valueToSet,isQuoted) + ",");
}
}
// were values appended?
if (argumentValues.length > 0) {
// append to arg string
argumentsString += (("-"+argumentName+" ") + argumentValues);
if (argumentsString.lastIndexOf(',') == (argumentsString.length -1)) {
argumentsString = argumentsString.substring(0,argumentsString.length-1);
}
argumentsString += " ";
}
// argument is NOT valued, just append the name
} else {
argumentsString += ("-"+argumentName+" ");
}
}
}
return Mustache.render(commandConfig.command,{'arguments':argumentsString});
} catch(exception) {
var msg = ("Unexpected error in _generateCommand(): " + exception + ' ' + exception.stack);
this._log('error',msg)
throw new Error(msg);
}
}
PSCommandService.prototype._finalizeParameterValue = function(valueToSet, applyQuotes) {
valueToSet = ((applyQuotes?"'":'')+valueToSet+(applyQuotes?"'":''));
return valueToSet;
}
PSCommandService.prototype._sanitize = function (toSanitize, isQuoted) {
toSanitize = toSanitize
.replace(/[\n\r]/g, "") // kill true newlines/feeds
.replace(/\\n/g, "\\$&") // kill string based newline attempts
.replace(/[`#]/g, "`$&"); // escape stuff that could screw up variables
const sanitizeRegex = /[;\$\|\(\)\{\}\[\]\\]/g;
const multiValuedRegex = /@\{([^}]*)\}/g;
if (isQuoted) { // if quoted, escape all quotes
toSanitize = toSanitize.replace(/'/g, "'$&");
} else if (multiValuedRegex.test(toSanitize)) {
// process is this is multi-valued parameter
const extractParams = (str, key) => {
// values must be wrapped in double quotes, so we can split them by comma
const match = str.match(new RegExp(`${key}="([^;]+)(?:";|"})`, "i"));
return match
? match[1]
.split(",")
.map((param) =>
param.trim().replace(sanitizeRegex, "`$&").replace(/^"|"$/g, "")
)
: [];
};
const addItemsSanitized = extractParams(toSanitize, "Add");
const removeItemsSanitized = extractParams(toSanitize, "Remove");
if (addItemsSanitized.length > 0 || removeItemsSanitized.length > 0) {
let result = "@{";
if (addItemsSanitized.length > 0) {
result += `Add="${addItemsSanitized.join('","')}"`;
}
if (removeItemsSanitized.length > 0) {
if (addItemsSanitized.length > 0) result += "; ";
result += `Remove="${removeItemsSanitized.join('","')}"`;
}
result += "}";
toSanitize = result;
}
} else if (!reservedVariableNames.includes(toSanitize)) { // skip if this is reserved variable name
toSanitize = toSanitize.replace(sanitizeRegex, "`$&");
}
return toSanitize;
};