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.
@@ -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
- * When false or omitted, every "##vso[" marker is blocked. When true, markers whose
18
- * command name is in the effective allowlist pass through unchanged.
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
- * Has no effect when enableVsoCommands is false.
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
- // Enabled: read the command name (up to the first space or ']') and allow the marker
156
- // only when that name is allowlisted.
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(name_1 && this.allowed.has(name_1) ? MARKER : NEUTRALIZED);
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.enabled && p.length >= MARKER.length && p.subarray(0, MARKER.length).equals(MARKER)) {
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 = new MarkerFilter(!!options.enableVsoCommands, resolveAllowed(options));
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 = new MarkerFilter(!!options.enableVsoCommands, resolveAllowed(options));
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 = new MarkerFilter(!!options.enableVsoCommands, resolveAllowed(options));
319
+ var filter = createMarkerFilter(options);
275
320
  var ended = false;
276
321
  return {
277
322
  write: function (data) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azure-pipelines-task-lib",
3
- "version": "5.281.0",
3
+ "version": "5.281.2",
4
4
  "description": "Azure Pipelines Task SDK",
5
5
  "main": "./task.js",
6
6
  "typings": "./task.d.ts",
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
- // Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another
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
- childProcess.execFileSync("cmd.exe", ["/c", "rd", "/s", "/q", im._normalizeSeparators(inputPath)]);
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
- childProcess.execFileSync("cmd.exe", ["/c", "rd", "/s", "/q", im._normalizeSeparators(realPath)]);
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
- childProcess.execFileSync("cmd.exe", ["/c", "del", "/f", "/a", im._normalizeSeparators(inputPath)]);
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.