azure-pipelines-task-lib 5.281.0 → 5.281.2
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/externaloutput.d.ts +20 -4
- package/externaloutput.js +56 -11
- package/package.json +1 -1
- package/task.d.ts +3 -1
- package/task.js +51 -6
package/externaloutput.d.ts
CHANGED
|
@@ -5,23 +5,36 @@ import stream = require('stream');
|
|
|
5
5
|
import im = require('./internal');
|
|
6
6
|
/** Commands allowed by default once VSO commands are enabled for a path. */
|
|
7
7
|
export declare const defaultAllowedVsoCommands: readonly string[];
|
|
8
|
+
/** Controls whether VSO commands are blocked, reported through telemetry, both, or neither. */
|
|
9
|
+
export type VsoCommandMode = 'disabled' | 'telemetryOnly' | 'blockOnly' | 'blockAndTelemetry';
|
|
8
10
|
/**
|
|
9
11
|
* Where the external output originates. Used for documentation and telemetry only; it does
|
|
10
12
|
* NOT select an automatic allowlist. A task must explicitly enable VSO commands when needed.
|
|
11
13
|
*/
|
|
12
14
|
export type ExternalOutputSource = 'remote' | 'childProcess' | 'repository';
|
|
15
|
+
export interface VsoCommandTelemetryEvent {
|
|
16
|
+
source: ExternalOutputSource;
|
|
17
|
+
commandName: string;
|
|
18
|
+
blocked: boolean;
|
|
19
|
+
}
|
|
20
|
+
export type VsoCommandTelemetrySink = (event: VsoCommandTelemetryEvent) => void;
|
|
13
21
|
export interface ExternalOutputOptions {
|
|
14
22
|
/** Origin of the output. Documentation/telemetry only. */
|
|
15
23
|
source: ExternalOutputSource;
|
|
16
24
|
/**
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
* Controls VSO command blocking and telemetry.
|
|
26
|
+
* @defaultValue 'blockOnly'
|
|
27
|
+
*/
|
|
28
|
+
vsoCommandMode?: VsoCommandMode;
|
|
29
|
+
/**
|
|
30
|
+
* When blocking is active, false or omitted blocks every "##vso[" marker. When true,
|
|
31
|
+
* markers whose command name is in the effective allowlist pass through unchanged.
|
|
19
32
|
*/
|
|
20
33
|
enableVsoCommands?: boolean;
|
|
21
34
|
/**
|
|
22
35
|
* The allowlist of "area.event" command names to permit when enableVsoCommands is true.
|
|
23
36
|
* Replaces (does not extend) the default list. An explicit empty array allows nothing.
|
|
24
|
-
|
|
37
|
+
* Has no effect when enableVsoCommands is false or blocking is inactive.
|
|
25
38
|
*/
|
|
26
39
|
allowedVsoCommands?: readonly string[];
|
|
27
40
|
/** Destination for filtered output. Defaults to process.stdout. */
|
|
@@ -35,8 +48,11 @@ export interface ExternalOutputOptions {
|
|
|
35
48
|
export declare class MarkerFilter {
|
|
36
49
|
private readonly enabled;
|
|
37
50
|
private readonly allowed;
|
|
51
|
+
private readonly block;
|
|
52
|
+
private readonly source?;
|
|
53
|
+
private readonly telemetry?;
|
|
38
54
|
private pending;
|
|
39
|
-
constructor(enabled: boolean, allowed: Set<string
|
|
55
|
+
constructor(enabled: boolean, allowed: Set<string>, block?: boolean, source?: ExternalOutputSource | undefined, telemetry?: VsoCommandTelemetrySink | undefined);
|
|
40
56
|
push(chunk: Buffer): Buffer;
|
|
41
57
|
flush(): Buffer;
|
|
42
58
|
}
|
package/externaloutput.js
CHANGED
|
@@ -63,6 +63,38 @@ var LF = 0x0A;
|
|
|
63
63
|
var MAX_HEADER = 256;
|
|
64
64
|
/** Commands allowed by default once VSO commands are enabled for a path. */
|
|
65
65
|
exports.defaultAllowedVsoCommands = Object.freeze(['task.debug', 'task.setprogress']);
|
|
66
|
+
function resolveVsoCommandMode(mode) {
|
|
67
|
+
switch (mode || 'blockOnly') {
|
|
68
|
+
case 'disabled':
|
|
69
|
+
return { block: false, telemetry: false };
|
|
70
|
+
case 'telemetryOnly':
|
|
71
|
+
return { block: false, telemetry: true };
|
|
72
|
+
case 'blockAndTelemetry':
|
|
73
|
+
return { block: true, telemetry: true };
|
|
74
|
+
case 'blockOnly':
|
|
75
|
+
return { block: true, telemetry: false };
|
|
76
|
+
default:
|
|
77
|
+
// JavaScript callers can bypass the TypeScript union. Preserve the secure default.
|
|
78
|
+
return { block: true, telemetry: false };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function publishVsoCommandTelemetry(event) {
|
|
82
|
+
try {
|
|
83
|
+
var telemetryEvent = __assign(__assign({}, event), { SYSTEM_TASKINSTANCEID: im._getVariable('SYSTEM_TASKINSTANCEID') || '', SYSTEM_TASKINSTANCENAME: im._getVariable('SYSTEM_TASKINSTANCENAME') || '', SYSTEM_JOBID: im._getVariable('SYSTEM_JOBID') || '', SYSTEM_PLANID: im._getVariable('SYSTEM_PLANID') || '' });
|
|
84
|
+
im._command('telemetry.publish', { area: 'TaskLib', feature: 'ExternalOutputVsoCommand' }, JSON.stringify(telemetryEvent));
|
|
85
|
+
}
|
|
86
|
+
catch (_err) {
|
|
87
|
+
console.error(_err);
|
|
88
|
+
// Telemetry is best effort and must not interrupt external-output handling.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function createMarkerFilter(options) {
|
|
92
|
+
var mode = resolveVsoCommandMode(options.vsoCommandMode);
|
|
93
|
+
var telemetry = mode.telemetry
|
|
94
|
+
? function (event) { return publishVsoCommandTelemetry(event); }
|
|
95
|
+
: undefined;
|
|
96
|
+
return new MarkerFilter(!!options.enableVsoCommands, resolveAllowed(options), mode.block, options.source, telemetry);
|
|
97
|
+
}
|
|
66
98
|
function resolveAllowed(options) {
|
|
67
99
|
if (!options.enableVsoCommands) {
|
|
68
100
|
return new Set();
|
|
@@ -120,9 +152,13 @@ function scanHeader(buf, start) {
|
|
|
120
152
|
* boundary, so memory stays bounded regardless of input size.
|
|
121
153
|
*/
|
|
122
154
|
var MarkerFilter = /** @class */ (function () {
|
|
123
|
-
function MarkerFilter(enabled, allowed) {
|
|
155
|
+
function MarkerFilter(enabled, allowed, block, source, telemetry) {
|
|
156
|
+
if (block === void 0) { block = true; }
|
|
124
157
|
this.enabled = enabled;
|
|
125
158
|
this.allowed = allowed;
|
|
159
|
+
this.block = block;
|
|
160
|
+
this.source = source;
|
|
161
|
+
this.telemetry = telemetry;
|
|
126
162
|
this.pending = EMPTY;
|
|
127
163
|
}
|
|
128
164
|
MarkerFilter.prototype.push = function (chunk) {
|
|
@@ -147,18 +183,23 @@ var MarkerFilter = /** @class */ (function () {
|
|
|
147
183
|
out.push(buf.subarray(pos, idx));
|
|
148
184
|
}
|
|
149
185
|
var afterMarker = idx + MARKER.length;
|
|
150
|
-
if (!this.enabled) {
|
|
186
|
+
if (this.block && !this.enabled && !this.telemetry) {
|
|
151
187
|
out.push(NEUTRALIZED);
|
|
152
188
|
pos = afterMarker;
|
|
153
189
|
continue;
|
|
154
190
|
}
|
|
155
|
-
|
|
156
|
-
|
|
191
|
+
if (!this.block && !this.telemetry) {
|
|
192
|
+
out.push(MARKER);
|
|
193
|
+
pos = afterMarker;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
// Read the command name (up to the first space or ']') for telemetry and, when
|
|
197
|
+
// blocking, to determine whether the marker is allowlisted.
|
|
157
198
|
var _a = scanHeader(buf, afterMarker), term = _a.term, newline = _a.newline;
|
|
158
199
|
if (term === -1) {
|
|
159
200
|
if (buf.length - afterMarker >= MAX_HEADER) {
|
|
160
201
|
// No terminator within the bound: fail closed.
|
|
161
|
-
out.push(NEUTRALIZED);
|
|
202
|
+
out.push(this.block ? NEUTRALIZED : MARKER);
|
|
162
203
|
pos = afterMarker;
|
|
163
204
|
continue;
|
|
164
205
|
}
|
|
@@ -169,14 +210,18 @@ var MarkerFilter = /** @class */ (function () {
|
|
|
169
210
|
// A CR/LF before the terminator means the agent (which parses per line) would never
|
|
170
211
|
// treat this as a command, so we fail closed and neutralize.
|
|
171
212
|
if (newline) {
|
|
172
|
-
out.push(NEUTRALIZED);
|
|
213
|
+
out.push(this.block ? NEUTRALIZED : MARKER);
|
|
173
214
|
pos = afterMarker;
|
|
174
215
|
continue;
|
|
175
216
|
}
|
|
176
217
|
var name_1 = canonicalCommandName(buf.toString('utf8', afterMarker, term));
|
|
218
|
+
var blocked = this.block && !(this.enabled && name_1 && this.allowed.has(name_1));
|
|
219
|
+
if (this.telemetry && name_1 && this.source) {
|
|
220
|
+
this.telemetry({ source: this.source, commandName: name_1, blocked: blocked });
|
|
221
|
+
}
|
|
177
222
|
// Allowlisted markers pass unchanged; the header/data after them flow through as
|
|
178
223
|
// ordinary bytes and any later markers are evaluated independently.
|
|
179
|
-
out.push(
|
|
224
|
+
out.push(blocked ? NEUTRALIZED : MARKER);
|
|
180
225
|
pos = afterMarker;
|
|
181
226
|
}
|
|
182
227
|
if (out.length === 0) {
|
|
@@ -196,7 +241,7 @@ var MarkerFilter = /** @class */ (function () {
|
|
|
196
241
|
return EMPTY;
|
|
197
242
|
}
|
|
198
243
|
// An incomplete command candidate that begins with the full marker is neutralized.
|
|
199
|
-
if (this.
|
|
244
|
+
if (this.block && p.length >= MARKER.length && p.subarray(0, MARKER.length).equals(MARKER)) {
|
|
200
245
|
return Buffer.concat([NEUTRALIZED, p.subarray(MARKER.length)]);
|
|
201
246
|
}
|
|
202
247
|
// A partial marker (fewer than the full bytes) cannot be executed by the agent, so it
|
|
@@ -215,7 +260,7 @@ var ExternalOutputStream = /** @class */ (function (_super) {
|
|
|
215
260
|
__extends(ExternalOutputStream, _super);
|
|
216
261
|
function ExternalOutputStream(options) {
|
|
217
262
|
var _this = _super.call(this) || this;
|
|
218
|
-
_this.markerFilter =
|
|
263
|
+
_this.markerFilter = createMarkerFilter(options);
|
|
219
264
|
return _this;
|
|
220
265
|
}
|
|
221
266
|
ExternalOutputStream.prototype._transform = function (chunk, _encoding, callback) {
|
|
@@ -262,7 +307,7 @@ function createExternalOutputStream(options) {
|
|
|
262
307
|
exports.createExternalOutputStream = createExternalOutputStream;
|
|
263
308
|
/** Filters one complete value for internal task-lib consumers. */
|
|
264
309
|
function filterExternalOutput(data, options) {
|
|
265
|
-
var filter =
|
|
310
|
+
var filter = createMarkerFilter(options);
|
|
266
311
|
var buf = Buffer.isBuffer(data) ? data : Buffer.from(String(data), 'utf8');
|
|
267
312
|
var filtered = filter.push(buf);
|
|
268
313
|
var pending = filter.flush();
|
|
@@ -271,7 +316,7 @@ function filterExternalOutput(data, options) {
|
|
|
271
316
|
exports.filterExternalOutput = filterExternalOutput;
|
|
272
317
|
/** Creates a stateful writer for internal consumers that receive output in chunks. */
|
|
273
318
|
function createFilteredWriter(options, destination) {
|
|
274
|
-
var filter =
|
|
319
|
+
var filter = createMarkerFilter(options);
|
|
275
320
|
var ended = false;
|
|
276
321
|
return {
|
|
277
322
|
write: function (data) {
|
package/package.json
CHANGED
package/task.d.ts
CHANGED
|
@@ -848,7 +848,9 @@ export type ExternalOutputIssueOptions = eom.ExternalOutputIssueOptions;
|
|
|
848
848
|
*
|
|
849
849
|
* By default every marker is blocked. Set enableVsoCommands to true (optionally with an
|
|
850
850
|
* explicit allowedVsoCommands list) to permit a small set of command names on a
|
|
851
|
-
* compatibility-sensitive path.
|
|
851
|
+
* compatibility-sensitive path. Set vsoCommandMode to control blocking and Kusto telemetry
|
|
852
|
+
* independently. Telemetry includes only the output source, canonical command name, and
|
|
853
|
+
* whether the command was blocked; command properties and data are never included.
|
|
852
854
|
*
|
|
853
855
|
* @param options External output options. See ExternalOutputOptions.
|
|
854
856
|
* @returns A writable/readable stream to pipe untrusted output into.
|
package/task.js
CHANGED
|
@@ -1533,6 +1533,50 @@ function _legacyFindFiles_getMatchingItems(includePatterns, excludePatterns, inc
|
|
|
1533
1533
|
}
|
|
1534
1534
|
return Object.keys(allFiles).sort();
|
|
1535
1535
|
}
|
|
1536
|
+
function _quoteWindowsCmdPath(value) {
|
|
1537
|
+
// Percent expansion remains active inside quotes, so quote each literal percent separately.
|
|
1538
|
+
return "\"".concat(value.replace(/%/g, '"^%"'), "\"");
|
|
1539
|
+
}
|
|
1540
|
+
function _publishRmRFSpecialPathTelemetry(inputPath) {
|
|
1541
|
+
try {
|
|
1542
|
+
var hasPercent = inputPath.includes('%');
|
|
1543
|
+
var hasCmdMetachar = /[&|<>^]/.test(inputPath);
|
|
1544
|
+
var hasDelayedExpansion = inputPath.includes('!');
|
|
1545
|
+
var hasParentheses = /[()]/.test(inputPath);
|
|
1546
|
+
if (!hasPercent && !hasCmdMetachar && !hasDelayedExpansion && !hasParentheses) {
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
var message = JSON.stringify({
|
|
1550
|
+
event: 'SpecialPathDetected',
|
|
1551
|
+
hasPercent: hasPercent,
|
|
1552
|
+
hasCmdMetachar: hasCmdMetachar,
|
|
1553
|
+
hasDelayedExpansion: hasDelayedExpansion,
|
|
1554
|
+
hasParentheses: hasParentheses
|
|
1555
|
+
});
|
|
1556
|
+
console.log("##vso[telemetry.publish area=TaskHub;feature=TaskLibRmRF]".concat(message));
|
|
1557
|
+
}
|
|
1558
|
+
catch (err) {
|
|
1559
|
+
(0, exports.debug)("Failed to publish rmRF telemetry: ".concat(err.message));
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
function _rmWindows(targetPath, isDirectory) {
|
|
1563
|
+
var normalizedPath = im._normalizeSeparators(targetPath);
|
|
1564
|
+
if (!getPipelineFeature('EnableRmRFCommandPathQuoting')) {
|
|
1565
|
+
var legacyArgs = isDirectory
|
|
1566
|
+
? ['/c', 'rd', '/s', '/q', normalizedPath]
|
|
1567
|
+
: ['/c', 'del', '/f', '/a', normalizedPath];
|
|
1568
|
+
childProcess.execFileSync('cmd.exe', legacyArgs);
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
var quotedPath = _quoteWindowsCmdPath(normalizedPath);
|
|
1572
|
+
var args = isDirectory
|
|
1573
|
+
? ['/d', '/v:off', '/c', 'rd', '/s', '/q', quotedPath]
|
|
1574
|
+
: ['/d', '/v:off', '/c', 'del', '/f', '/a', quotedPath];
|
|
1575
|
+
var options = {
|
|
1576
|
+
windowsVerbatimArguments: true
|
|
1577
|
+
};
|
|
1578
|
+
childProcess.execFileSync('cmd.exe', args, options);
|
|
1579
|
+
}
|
|
1536
1580
|
/**
|
|
1537
1581
|
* Remove a path recursively with force
|
|
1538
1582
|
*
|
|
@@ -1543,13 +1587,12 @@ function _legacyFindFiles_getMatchingItems(includePatterns, excludePatterns, inc
|
|
|
1543
1587
|
function rmRF(inputPath) {
|
|
1544
1588
|
(0, exports.debug)('rm -rf ' + inputPath);
|
|
1545
1589
|
if (getPlatform() == Platform.Windows) {
|
|
1546
|
-
|
|
1547
|
-
// program (e.g. antivirus), it won't be deleted. To address this, we shell out the work to rd/del.
|
|
1590
|
+
_publishRmRFSpecialPathTelemetry(inputPath);
|
|
1548
1591
|
try {
|
|
1549
1592
|
var lstats = fs.lstatSync(inputPath);
|
|
1550
1593
|
if (lstats.isDirectory() && !lstats.isSymbolicLink()) {
|
|
1551
1594
|
(0, exports.debug)('removing directory ' + inputPath);
|
|
1552
|
-
|
|
1595
|
+
_rmWindows(inputPath, true);
|
|
1553
1596
|
}
|
|
1554
1597
|
else if (lstats.isSymbolicLink()) {
|
|
1555
1598
|
(0, exports.debug)('removing symbolic link ' + inputPath);
|
|
@@ -1557,7 +1600,7 @@ function rmRF(inputPath) {
|
|
|
1557
1600
|
if (fs.existsSync(realPath)) {
|
|
1558
1601
|
var stats_4 = fs.statSync(realPath);
|
|
1559
1602
|
if (stats_4.isDirectory()) {
|
|
1560
|
-
|
|
1603
|
+
_rmWindows(realPath, true);
|
|
1561
1604
|
fs.unlinkSync(inputPath);
|
|
1562
1605
|
}
|
|
1563
1606
|
else {
|
|
@@ -1571,7 +1614,7 @@ function rmRF(inputPath) {
|
|
|
1571
1614
|
}
|
|
1572
1615
|
else {
|
|
1573
1616
|
(0, exports.debug)('removing file ' + inputPath);
|
|
1574
|
-
|
|
1617
|
+
_rmWindows(inputPath, false);
|
|
1575
1618
|
}
|
|
1576
1619
|
}
|
|
1577
1620
|
catch (err) {
|
|
@@ -2423,7 +2466,9 @@ exports.ToolRunner = trm.ToolRunner;
|
|
|
2423
2466
|
*
|
|
2424
2467
|
* By default every marker is blocked. Set enableVsoCommands to true (optionally with an
|
|
2425
2468
|
* explicit allowedVsoCommands list) to permit a small set of command names on a
|
|
2426
|
-
* compatibility-sensitive path.
|
|
2469
|
+
* compatibility-sensitive path. Set vsoCommandMode to control blocking and Kusto telemetry
|
|
2470
|
+
* independently. Telemetry includes only the output source, canonical command name, and
|
|
2471
|
+
* whether the command was blocked; command properties and data are never included.
|
|
2427
2472
|
*
|
|
2428
2473
|
* @param options External output options. See ExternalOutputOptions.
|
|
2429
2474
|
* @returns A writable/readable stream to pipe untrusted output into.
|