azure-pipelines-task-lib 5.281.1 → 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.1",
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
@@ -2466,7 +2466,9 @@ exports.ToolRunner = trm.ToolRunner;
2466
2466
  *
2467
2467
  * By default every marker is blocked. Set enableVsoCommands to true (optionally with an
2468
2468
  * explicit allowedVsoCommands list) to permit a small set of command names on a
2469
- * 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.
2470
2472
  *
2471
2473
  * @param options External output options. See ExternalOutputOptions.
2472
2474
  * @returns A writable/readable stream to pipe untrusted output into.