dd-trace 6.4.0 → 6.5.0
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/README.electron.md +7 -0
- package/README.md +17 -0
- package/ci/diagnose.js +2100 -0
- package/ci/init.js +23 -2
- package/ci/runbook.md +198 -0
- package/ci/test-optimization-validation/approval-artifacts.js +143 -0
- package/ci/test-optimization-validation/approval.js +299 -0
- package/ci/test-optimization-validation/artifact-id.js +20 -0
- package/ci/test-optimization-validation/bounded-json.js +121 -0
- package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
- package/ci/test-optimization-validation/ci-discovery.js +181 -0
- package/ci/test-optimization-validation/ci-remediation.js +210 -0
- package/ci/test-optimization-validation/cli.js +903 -0
- package/ci/test-optimization-validation/command-blocker.js +81 -0
- package/ci/test-optimization-validation/command-output-policy.js +206 -0
- package/ci/test-optimization-validation/command-runner.js +707 -0
- package/ci/test-optimization-validation/command-suitability.js +471 -0
- package/ci/test-optimization-validation/executable-approval.js +48 -0
- package/ci/test-optimization-validation/executable.js +480 -0
- package/ci/test-optimization-validation/generated-file-policy.js +63 -0
- package/ci/test-optimization-validation/generated-files.js +351 -0
- package/ci/test-optimization-validation/generated-verifier.js +196 -0
- package/ci/test-optimization-validation/init-probe-preload.js +163 -0
- package/ci/test-optimization-validation/init-probe.js +246 -0
- package/ci/test-optimization-validation/late-initialization.js +63 -0
- package/ci/test-optimization-validation/local-command.js +163 -0
- package/ci/test-optimization-validation/manifest-loader.js +133 -0
- package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
- package/ci/test-optimization-validation/manifest-schema.js +862 -0
- package/ci/test-optimization-validation/offline-fixtures.js +327 -0
- package/ci/test-optimization-validation/offline-output.js +406 -0
- package/ci/test-optimization-validation/payload-normalizer.js +72 -0
- package/ci/test-optimization-validation/plan-writer.js +1120 -0
- package/ci/test-optimization-validation/preflight-runner.js +114 -0
- package/ci/test-optimization-validation/redaction.js +331 -0
- package/ci/test-optimization-validation/report-writer.js +1508 -0
- package/ci/test-optimization-validation/safe-files.js +203 -0
- package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
- package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
- package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
- package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
- package/ci/test-optimization-validation/setup-runner.js +97 -0
- package/ci/test-optimization-validation/static-diagnosis.js +159 -0
- package/ci/test-optimization-validation/test-output.js +129 -0
- package/ci/test-optimization-validation-manifest.schema.json +1 -0
- package/ci/validate-test-optimization.js +3 -0
- package/ext/exporters.js +1 -0
- package/index.d.ts +121 -8
- package/init.js +18 -0
- package/initialize.mjs +1 -1
- package/loader-hook.mjs +28 -18
- package/openfeature.d.ts +1 -0
- package/openfeature.js +4 -0
- package/package.json +10 -7
- package/packages/datadog-instrumentations/src/ai.js +8 -2
- package/packages/datadog-instrumentations/src/child_process.js +1 -1
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
- package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
- package/packages/datadog-instrumentations/src/cucumber.js +20 -7
- package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
- package/packages/datadog-instrumentations/src/fastify.js +7 -11
- package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
- package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
- package/packages/datadog-instrumentations/src/jest.js +26 -0
- package/packages/datadog-instrumentations/src/mariadb.js +1 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
- package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
- package/packages/datadog-instrumentations/src/mongodb.js +3 -3
- package/packages/datadog-instrumentations/src/mongoose.js +3 -3
- package/packages/datadog-instrumentations/src/mquery.js +2 -2
- package/packages/datadog-instrumentations/src/mysql.js +1 -1
- package/packages/datadog-instrumentations/src/next.js +24 -0
- package/packages/datadog-instrumentations/src/nyc.js +0 -2
- package/packages/datadog-instrumentations/src/oracledb.js +2 -2
- package/packages/datadog-instrumentations/src/pg.js +2 -2
- package/packages/datadog-instrumentations/src/playwright.js +159 -22
- package/packages/datadog-instrumentations/src/process.js +3 -0
- package/packages/datadog-instrumentations/src/router.js +18 -8
- package/packages/datadog-instrumentations/src/selenium.js +52 -26
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
- package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
- package/packages/datadog-instrumentations/src/ws.js +2 -1
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
- package/packages/datadog-plugin-aws-sdk/src/util.js +2 -2
- package/packages/datadog-plugin-cucumber/src/index.js +1 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -57
- package/packages/datadog-plugin-cypress/src/index.js +6 -1
- package/packages/datadog-plugin-cypress/src/support.js +92 -26
- package/packages/datadog-plugin-electron/src/ipc.js +1 -1
- package/packages/datadog-plugin-graphql/src/execute.js +87 -45
- package/packages/datadog-plugin-graphql/src/validate.js +2 -2
- package/packages/datadog-plugin-http2/src/client.js +1 -1
- package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
- package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
- package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
- package/packages/datadog-plugin-playwright/src/index.js +177 -64
- package/packages/datadog-plugin-router/src/index.js +11 -2
- package/packages/datadog-plugin-selenium/src/index.js +5 -36
- package/packages/datadog-plugin-ws/src/close.js +2 -1
- package/packages/datadog-plugin-ws/src/producer.js +2 -1
- package/packages/datadog-plugin-ws/src/receiver.js +2 -1
- package/packages/datadog-plugin-ws/src/server.js +2 -1
- package/packages/dd-trace/src/aiguard/sdk.js +1 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
- package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
- package/packages/dd-trace/src/azure_metadata.js +2 -1
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +41 -3
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -2
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +1 -1
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
- package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
- package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +168 -8
- package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +16 -0
- package/packages/dd-trace/src/config/helper.js +1 -0
- package/packages/dd-trace/src/config/index.js +10 -0
- package/packages/dd-trace/src/config/supported-configurations.json +66 -0
- package/packages/dd-trace/src/exporter.js +2 -0
- package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
- package/packages/dd-trace/src/exporters/common/request.js +59 -30
- package/packages/dd-trace/src/exporters/common/url.js +15 -1
- package/packages/dd-trace/src/feature-registry.js +10 -3
- package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
- package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
- package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
- package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
- package/packages/dd-trace/src/llmobs/noop.js +6 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
- package/packages/dd-trace/src/llmobs/sdk.js +17 -0
- package/packages/dd-trace/src/llmobs/tagger.js +68 -0
- package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
- package/packages/dd-trace/src/llmobs/util.js +32 -0
- package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
- package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
- package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
- package/packages/dd-trace/src/openfeature/index.js +0 -2
- package/packages/dd-trace/src/openfeature/noop.js +1 -28
- package/packages/dd-trace/src/openfeature/register.js +16 -27
- package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
- package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
- package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
- package/packages/dd-trace/src/opentelemetry/span.js +5 -0
- package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
- package/packages/dd-trace/src/plugins/util/git.js +3 -2
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
- package/packages/dd-trace/src/plugins/util/test.js +56 -4
- package/packages/dd-trace/src/proxy.js +84 -7
- package/packages/dd-trace/src/span_processor.js +5 -0
- package/packages/dd-trace/src/standalone/index.js +0 -14
- package/packages/dd-trace/src/telemetry/send-data.js +2 -2
- package/packages/dd-trace/src/util.js +26 -0
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/shell-quote/index.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/* eslint-disable eslint-rules/eslint-process-env */
|
|
4
|
+
|
|
5
|
+
const crypto = require('node:crypto')
|
|
6
|
+
const fs = require('node:fs')
|
|
7
|
+
const path = require('node:path')
|
|
8
|
+
|
|
9
|
+
const { bindApprovedExecutable, getApprovedExecutable } = require('./executable-approval')
|
|
10
|
+
const { getCiWiringCommand, getLocalValidationCommand } = require('./local-command')
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns an executable that is unavailable for a structured command.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} command manifest command
|
|
16
|
+
* @returns {string|undefined} unavailable executable
|
|
17
|
+
*/
|
|
18
|
+
function getUnavailableExecutable (command) {
|
|
19
|
+
for (const executableCommand of getExecutableCommands(command)) {
|
|
20
|
+
const executable = getExecutable(executableCommand)
|
|
21
|
+
if (executable && !resolveExecutable(executable, executableCommand)) return executable
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Reads the executable used to start a structured command.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} command manifest command
|
|
29
|
+
* @returns {string|undefined} command executable
|
|
30
|
+
*/
|
|
31
|
+
function getExecutable (command) {
|
|
32
|
+
if (!command?.usesShell) return command?.argv?.[0]
|
|
33
|
+
if (typeof command.shell === 'string' && command.shell.trim()) return command.shell.trim()
|
|
34
|
+
return process.platform === 'win32'
|
|
35
|
+
? process.env.ComSpec || process.env.COMSPEC || 'cmd.exe'
|
|
36
|
+
: process.env.SHELL || '/bin/sh'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolves an executable from the command working directory or PATH.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} executable executable name or path
|
|
43
|
+
* @param {object} command manifest command
|
|
44
|
+
* @returns {boolean} whether the executable can be resolved
|
|
45
|
+
*/
|
|
46
|
+
function resolveExecutable (executable, command) {
|
|
47
|
+
if (isExplicitExecutablePath(executable)) {
|
|
48
|
+
return isExecutable(path.resolve(command.cwd, executable))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const environmentPath = getEnvironmentPath(command)
|
|
52
|
+
const extensions = getExecutableExtensions()
|
|
53
|
+
|
|
54
|
+
for (const directory of environmentPath.split(path.delimiter)) {
|
|
55
|
+
if (!directory) continue
|
|
56
|
+
const resolvedDirectory = path.resolve(command.cwd, directory)
|
|
57
|
+
for (const extension of extensions) {
|
|
58
|
+
const filename = path.join(resolvedDirectory, `${executable}${extension}`)
|
|
59
|
+
if (isExecutable(filename)) return true
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolves the filesystem path used for a structured command executable.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} command manifest command
|
|
69
|
+
* @returns {string|undefined} resolved executable path
|
|
70
|
+
*/
|
|
71
|
+
function getResolvedExecutable (command) {
|
|
72
|
+
const executable = getExecutable(command)
|
|
73
|
+
if (!executable) return
|
|
74
|
+
|
|
75
|
+
if (isExplicitExecutablePath(executable)) {
|
|
76
|
+
const filename = path.resolve(command.cwd, executable)
|
|
77
|
+
return isExecutable(filename) ? filename : undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const environmentPath = getEnvironmentPath(command)
|
|
81
|
+
const extensions = getExecutableExtensions()
|
|
82
|
+
|
|
83
|
+
for (const directory of environmentPath.split(path.delimiter)) {
|
|
84
|
+
if (!directory) continue
|
|
85
|
+
const resolvedDirectory = path.resolve(command.cwd, directory)
|
|
86
|
+
for (const extension of extensions) {
|
|
87
|
+
const filename = path.join(resolvedDirectory, `${executable}${extension}`)
|
|
88
|
+
if (isExecutable(filename)) return filename
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Returns the PATH used by a command, preserving an explicitly empty value.
|
|
95
|
+
*
|
|
96
|
+
* @param {object} command manifest command
|
|
97
|
+
* @returns {string} command PATH
|
|
98
|
+
*/
|
|
99
|
+
function getEnvironmentPath (command) {
|
|
100
|
+
if (Object.hasOwn(command.env || {}, 'PATH')) return command.env.PATH || ''
|
|
101
|
+
return process.env.PATH || ''
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Binds every executable selected by an approvable manifest command to its canonical file identity.
|
|
106
|
+
*
|
|
107
|
+
* @param {object} manifest loaded manifest
|
|
108
|
+
* @returns {object[]} sorted executable identities included in approval material
|
|
109
|
+
*/
|
|
110
|
+
function bindManifestExecutables (manifest) {
|
|
111
|
+
const identities = []
|
|
112
|
+
const identitiesByPath = new Map()
|
|
113
|
+
for (const [label, command, sourceCommand] of getManifestCommands(manifest)) {
|
|
114
|
+
const identity = getCommandExecutableIdentity(command, identitiesByPath)
|
|
115
|
+
if (!identity) continue
|
|
116
|
+
bindApprovedExecutable(command, identity)
|
|
117
|
+
if (sourceCommand !== command) bindApprovedExecutable(sourceCommand, identity)
|
|
118
|
+
identities.push({ label, ...identity })
|
|
119
|
+
}
|
|
120
|
+
return identities.sort((left, right) => left.label.localeCompare(right.label))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Verifies and returns the canonical executable and approved invocation name used to spawn it.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} command command about to execute
|
|
127
|
+
* @param {{requireApproval?: boolean}} [options] verification options
|
|
128
|
+
* @returns {{argv0: string, path: string}} approved launch identity
|
|
129
|
+
*/
|
|
130
|
+
function getExecutableForSpawn (command, options = {}) {
|
|
131
|
+
const approved = getApprovedExecutable(command)
|
|
132
|
+
const current = getCommandExecutableIdentity(command)
|
|
133
|
+
if (!approved) {
|
|
134
|
+
if (options.requireApproval) {
|
|
135
|
+
throw new Error('The selected command executable was not covered by the approved execution plan.')
|
|
136
|
+
}
|
|
137
|
+
if (!current) throw new Error(`Command executable is unavailable: ${getExecutable(command)}`)
|
|
138
|
+
return getLaunchIdentity(current)
|
|
139
|
+
}
|
|
140
|
+
if (!areExecutableIdentitiesEqual(current, approved)) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
'The selected command executable changed after approval. Render and approve a fresh execution plan.'
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
return getLaunchIdentity(approved)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Resolves a command launcher and every executable delegated through env wrappers.
|
|
150
|
+
*
|
|
151
|
+
* @param {object} command manifest command
|
|
152
|
+
* @param {Map<string, object>} [identitiesByPath] identities already hashed during this approval pass
|
|
153
|
+
* @returns {{delegated?: object[], invocationPath: string, path: string, sha256: string}|undefined} identity tree
|
|
154
|
+
*/
|
|
155
|
+
function getCommandExecutableIdentity (command, identitiesByPath) {
|
|
156
|
+
const identities = []
|
|
157
|
+
for (const executableCommand of getExecutableCommands(command)) {
|
|
158
|
+
const identity = getExecutableIdentity(executableCommand, identitiesByPath)
|
|
159
|
+
if (!identity) return
|
|
160
|
+
identities.push(identity)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const [launcher, ...delegated] = identities
|
|
164
|
+
return delegated.length === 0 ? launcher : { ...launcher, delegated }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Checks whether the launcher and delegated executable identities still match approval.
|
|
169
|
+
*
|
|
170
|
+
* @param {object|undefined} current current identity tree
|
|
171
|
+
* @param {object|undefined} approved approved identity tree
|
|
172
|
+
* @returns {boolean} whether every executable matches
|
|
173
|
+
*/
|
|
174
|
+
function areExecutableIdentitiesEqual (current, approved) {
|
|
175
|
+
if (!current || !approved) return false
|
|
176
|
+
const currentIdentities = [current, ...(current.delegated || [])]
|
|
177
|
+
const approvedIdentities = [approved, ...(approved.delegated || [])]
|
|
178
|
+
if (currentIdentities.length !== approvedIdentities.length) return false
|
|
179
|
+
|
|
180
|
+
return currentIdentities.every((identity, index) => {
|
|
181
|
+
const expected = approvedIdentities[index]
|
|
182
|
+
return identity.invocationPath === expected.invocationPath && identity.path === expected.path &&
|
|
183
|
+
identity.sha256 === expected.sha256
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Expands nested env wrappers into the commands whose executables they delegate to.
|
|
189
|
+
*
|
|
190
|
+
* @param {object} command manifest command
|
|
191
|
+
* @returns {object[]} launcher followed by delegated commands
|
|
192
|
+
*/
|
|
193
|
+
function getExecutableCommands (command) {
|
|
194
|
+
const commands = [command]
|
|
195
|
+
let current = command
|
|
196
|
+
while (!current.usesShell && isEnvExecutable(current.argv?.[0])) {
|
|
197
|
+
current = getEnvDelegatedCommand(current)
|
|
198
|
+
commands.push(current)
|
|
199
|
+
}
|
|
200
|
+
return commands
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Returns the command executed by an env wrapper with its effective PATH and working directory.
|
|
205
|
+
*
|
|
206
|
+
* @param {object} command env wrapper command
|
|
207
|
+
* @returns {object} delegated command
|
|
208
|
+
*/
|
|
209
|
+
function getEnvDelegatedCommand (command) {
|
|
210
|
+
const parsed = parseArgv(command.argv)
|
|
211
|
+
if (parsed.unsupportedEnvOption) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Cannot approve env-wrapped command because option "${parsed.unsupportedEnvOption}" prevents reliable ` +
|
|
214
|
+
'executable fingerprinting.'
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
if (parsed.commandIndex >= command.argv.length) {
|
|
218
|
+
throw new Error('Cannot approve env-wrapped command because it does not identify a delegated executable.')
|
|
219
|
+
}
|
|
220
|
+
const delegatedExecutable = command.argv[parsed.commandIndex]
|
|
221
|
+
const requiresPathLookup = !isExplicitExecutablePath(delegatedExecutable)
|
|
222
|
+
if (requiresPathLookup && parsed.unsetEnvNames.some(name => name.toUpperCase() === 'PATH')) {
|
|
223
|
+
throw new Error('Cannot approve env-wrapped command because it removes PATH before selecting its executable.')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const env = parsed.ignoreEnvironment ? { ...parsed.prefixEnv } : { ...command.env, ...parsed.prefixEnv }
|
|
227
|
+
if (requiresPathLookup && parsed.ignoreEnvironment && !Object.hasOwn(env, 'PATH')) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
'Cannot approve env-wrapped command because it clears the environment without declaring an explicit PATH.'
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
...command,
|
|
235
|
+
argv: command.argv.slice(parsed.commandIndex),
|
|
236
|
+
cwd: parsed.workingDirectory ? path.resolve(command.cwd, parsed.workingDirectory) : command.cwd,
|
|
237
|
+
env,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Selects the verified path and invocation name used to launch an executable.
|
|
243
|
+
*
|
|
244
|
+
* @param {{invocationPath: string, path: string}} identity verified executable identity
|
|
245
|
+
* @returns {{argv0: string, path: string}} executable launch identity
|
|
246
|
+
*/
|
|
247
|
+
function getLaunchIdentity (identity) {
|
|
248
|
+
return {
|
|
249
|
+
argv0: identity.invocationPath,
|
|
250
|
+
// Windows package-manager shims rely on their invoked path. The canonical target is still verified above.
|
|
251
|
+
path: process.platform === 'win32' ? identity.invocationPath : identity.path,
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Resolves one command executable to a stable canonical path and content digest.
|
|
257
|
+
*
|
|
258
|
+
* @param {object} command manifest command
|
|
259
|
+
* @param {Map<string, object>} [identitiesByPath] identities already hashed during this approval pass
|
|
260
|
+
* @returns {{invocationPath: string, path: string, sha256: string}|undefined} executable identity
|
|
261
|
+
*/
|
|
262
|
+
function getExecutableIdentity (command, identitiesByPath) {
|
|
263
|
+
const resolved = getResolvedExecutable(command)
|
|
264
|
+
if (!resolved) return
|
|
265
|
+
const canonicalPath = fs.realpathSync(resolved)
|
|
266
|
+
const cached = identitiesByPath?.get(canonicalPath)
|
|
267
|
+
if (cached) return { invocationPath: resolved, ...cached }
|
|
268
|
+
const stat = fs.statSync(canonicalPath)
|
|
269
|
+
if (!stat.isFile()) return
|
|
270
|
+
const canonicalIdentity = {
|
|
271
|
+
path: canonicalPath,
|
|
272
|
+
sha256: crypto.createHash('sha256').update(fs.readFileSync(canonicalPath)).digest('hex'),
|
|
273
|
+
}
|
|
274
|
+
identitiesByPath?.set(canonicalPath, canonicalIdentity)
|
|
275
|
+
return { invocationPath: resolved, ...canonicalIdentity }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Enumerates every executable-bearing manifest command with a stable approval label.
|
|
280
|
+
*
|
|
281
|
+
* @param {object} manifest loaded manifest
|
|
282
|
+
* @returns {Array<[string, object]>} labeled commands
|
|
283
|
+
*/
|
|
284
|
+
function getManifestCommands (manifest) {
|
|
285
|
+
const commands = []
|
|
286
|
+
for (const framework of manifest.frameworks || []) {
|
|
287
|
+
const prefix = `framework:${framework.id}`
|
|
288
|
+
const basicSource = framework.existingTestCommand
|
|
289
|
+
if (basicSource) {
|
|
290
|
+
commands.push([`${prefix}:basic-reporting`, getLocalValidationCommand(framework, basicSource), basicSource])
|
|
291
|
+
}
|
|
292
|
+
if (framework.ciWiringCommand) {
|
|
293
|
+
commands.push([`${prefix}:ci-wiring`, getCiWiringCommand(framework), framework.ciWiringCommand])
|
|
294
|
+
}
|
|
295
|
+
for (const [index, command] of (framework.setup?.commands || []).entries()) {
|
|
296
|
+
commands.push([`${prefix}:setup:${index}`, command, command])
|
|
297
|
+
}
|
|
298
|
+
for (const [index, scenario] of (framework.generatedTestStrategy?.scenarios || []).entries()) {
|
|
299
|
+
if (scenario.runCommand) {
|
|
300
|
+
commands.push([
|
|
301
|
+
`${prefix}:generated:${index}`,
|
|
302
|
+
getLocalValidationCommand(framework, scenario.runCommand),
|
|
303
|
+
scenario.runCommand,
|
|
304
|
+
])
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return commands
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Detects explicit executable paths using platform path syntax.
|
|
313
|
+
*
|
|
314
|
+
* @param {string} executable executable name or path
|
|
315
|
+
* @param {string} [platform] target platform
|
|
316
|
+
* @returns {boolean} whether the value is a path rather than a PATH name
|
|
317
|
+
*/
|
|
318
|
+
function isExplicitExecutablePath (executable, platform = process.platform) {
|
|
319
|
+
return path.isAbsolute(executable) || executable.includes('/') || (platform === 'win32' && executable.includes('\\'))
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function getExecutableExtensions () {
|
|
323
|
+
if (process.platform !== 'win32') return ['']
|
|
324
|
+
return ['', ...(process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';')]
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Checks whether a filesystem entry can be executed.
|
|
329
|
+
*
|
|
330
|
+
* @param {string} filename executable candidate
|
|
331
|
+
* @returns {boolean} whether the candidate is executable
|
|
332
|
+
*/
|
|
333
|
+
function isExecutable (filename) {
|
|
334
|
+
try {
|
|
335
|
+
fs.accessSync(filename, fs.constants.X_OK)
|
|
336
|
+
return true
|
|
337
|
+
} catch {
|
|
338
|
+
return false
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Parses structured env wrappers and runtime plumbing without executing them.
|
|
344
|
+
*
|
|
345
|
+
* @param {string[]} argv command arguments
|
|
346
|
+
* @returns {object} parsed wrapper details
|
|
347
|
+
*/
|
|
348
|
+
function parseArgv (argv) {
|
|
349
|
+
const result = {
|
|
350
|
+
ignoreEnvironment: false,
|
|
351
|
+
prefixAssignments: [],
|
|
352
|
+
prefixEnv: {},
|
|
353
|
+
unsetEnvNames: [],
|
|
354
|
+
commandIndex: 0,
|
|
355
|
+
corepackIndex: -1,
|
|
356
|
+
pathAdjusted: false,
|
|
357
|
+
unsupportedEnvOption: undefined,
|
|
358
|
+
workingDirectory: undefined,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!Array.isArray(argv) || argv.length === 0) return result
|
|
362
|
+
|
|
363
|
+
let index = 0
|
|
364
|
+
if (isEnvExecutable(argv[index])) {
|
|
365
|
+
index++
|
|
366
|
+
while (index < argv.length) {
|
|
367
|
+
const option = argv[index]
|
|
368
|
+
if (option === '--') {
|
|
369
|
+
index++
|
|
370
|
+
break
|
|
371
|
+
}
|
|
372
|
+
if (option === '-' || option === '-i' || option === '--ignore-environment') {
|
|
373
|
+
result.ignoreEnvironment = true
|
|
374
|
+
index++
|
|
375
|
+
continue
|
|
376
|
+
}
|
|
377
|
+
if (option === '-u' || option === '--unset') {
|
|
378
|
+
if (typeof argv[index + 1] === 'string') result.unsetEnvNames.push(argv[index + 1])
|
|
379
|
+
index += 2
|
|
380
|
+
continue
|
|
381
|
+
}
|
|
382
|
+
const unsetMatch = /^(?:-u|--unset=)(.+)$/.exec(option)
|
|
383
|
+
if (unsetMatch) {
|
|
384
|
+
result.unsetEnvNames.push(unsetMatch[1])
|
|
385
|
+
index++
|
|
386
|
+
continue
|
|
387
|
+
}
|
|
388
|
+
if (option === '-C' || option === '--chdir') {
|
|
389
|
+
if (typeof argv[index + 1] !== 'string') {
|
|
390
|
+
result.unsupportedEnvOption = option
|
|
391
|
+
break
|
|
392
|
+
}
|
|
393
|
+
result.workingDirectory = argv[index + 1]
|
|
394
|
+
index += 2
|
|
395
|
+
continue
|
|
396
|
+
}
|
|
397
|
+
const chdirMatch = /^(?:-C(.+)|--chdir=(.+))$/.exec(option)
|
|
398
|
+
if (chdirMatch) {
|
|
399
|
+
result.workingDirectory = chdirMatch[1] || chdirMatch[2]
|
|
400
|
+
index++
|
|
401
|
+
continue
|
|
402
|
+
}
|
|
403
|
+
if (option === '-S' || option === '--split-string' || /^(?:-S.+|--split-string=.+)$/.test(option)) {
|
|
404
|
+
result.unsupportedEnvOption = option
|
|
405
|
+
break
|
|
406
|
+
}
|
|
407
|
+
if (isSupportedEnvFlag(option)) {
|
|
408
|
+
index++
|
|
409
|
+
continue
|
|
410
|
+
}
|
|
411
|
+
if (option.startsWith('-')) {
|
|
412
|
+
result.unsupportedEnvOption = option
|
|
413
|
+
break
|
|
414
|
+
}
|
|
415
|
+
if (!isEnvAssignment(option)) break
|
|
416
|
+
|
|
417
|
+
const assignment = argv[index]
|
|
418
|
+
const equalsIndex = assignment.indexOf('=')
|
|
419
|
+
const name = assignment.slice(0, equalsIndex)
|
|
420
|
+
const value = assignment.slice(equalsIndex + 1)
|
|
421
|
+
result.prefixEnv[name] = value
|
|
422
|
+
|
|
423
|
+
if (name === 'PATH') {
|
|
424
|
+
result.pathAdjusted = true
|
|
425
|
+
} else {
|
|
426
|
+
result.prefixAssignments.push(assignment)
|
|
427
|
+
}
|
|
428
|
+
index++
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
result.commandIndex = index
|
|
433
|
+
|
|
434
|
+
if (isNodeExecutable(argv[index]) && isCorepackScript(argv[index + 1]) && argv[index + 2]) {
|
|
435
|
+
result.corepackIndex = index + 1
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return result
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function isSupportedEnvFlag (option) {
|
|
442
|
+
return /^(?:-0|-v|--null|--debug|--help|--version|--list-signal-handling)$/.test(option) ||
|
|
443
|
+
/^--(?:block|default|ignore)-signal(?:=.*)?$/.test(option)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function isEnvExecutable (value) {
|
|
447
|
+
const name = getExecutableName(value)
|
|
448
|
+
return name === 'env' || name === 'env.exe'
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function isEnvAssignment (value) {
|
|
452
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(value)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function isNodeExecutable (value = '') {
|
|
456
|
+
const name = getExecutableName(value)
|
|
457
|
+
return name === 'node' || name === 'node.exe'
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function isCorepackScript (value = '') {
|
|
461
|
+
const name = getExecutableName(value)
|
|
462
|
+
return name === 'corepack' || name === 'corepack.exe' || name === 'corepack.js'
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function getExecutableName (value = '') {
|
|
466
|
+
return String(value).split(/[\\/]/).pop().toLowerCase()
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
module.exports = {
|
|
470
|
+
bindManifestExecutables,
|
|
471
|
+
getApprovedExecutable,
|
|
472
|
+
getExecutableForSpawn,
|
|
473
|
+
getManifestCommands,
|
|
474
|
+
getResolvedExecutable,
|
|
475
|
+
getUnavailableExecutable,
|
|
476
|
+
isEnvExecutable,
|
|
477
|
+
isExplicitExecutablePath,
|
|
478
|
+
isNodeExecutable,
|
|
479
|
+
parseArgv,
|
|
480
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { hasUnsafeInvisibleCharacter, sanitizeString } = require('./redaction')
|
|
4
|
+
|
|
5
|
+
const MAX_GENERATED_FILES = 8
|
|
6
|
+
const MAX_GENERATED_FILE_BYTES = 32 * 1024
|
|
7
|
+
const MAX_GENERATED_FILE_LINES = 256
|
|
8
|
+
const MAX_GENERATED_LINE_BYTES = 4096
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns why generated source is unsafe to write and execute.
|
|
12
|
+
*
|
|
13
|
+
* @param {unknown} contentLines generated source lines
|
|
14
|
+
* @returns {string|undefined} policy violation
|
|
15
|
+
*/
|
|
16
|
+
function getGeneratedFileContentError (contentLines) {
|
|
17
|
+
if (!Array.isArray(contentLines) || contentLines.some(line => typeof line !== 'string')) return
|
|
18
|
+
if (contentLines.length > MAX_GENERATED_FILE_LINES) {
|
|
19
|
+
return `must contain at most ${MAX_GENERATED_FILE_LINES} lines`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let totalBytes = 1
|
|
23
|
+
for (const line of contentLines) {
|
|
24
|
+
if (line.includes('\n') || line.includes('\r') || hasUnsafeControlCharacter(line)) {
|
|
25
|
+
return 'must use one printable source line per contentLines entry'
|
|
26
|
+
}
|
|
27
|
+
if (Buffer.byteLength(line) > MAX_GENERATED_LINE_BYTES) {
|
|
28
|
+
return `must not contain a line larger than ${MAX_GENERATED_LINE_BYTES} bytes`
|
|
29
|
+
}
|
|
30
|
+
if (sanitizeString(line) !== line) {
|
|
31
|
+
return 'must contain only synthetic source and no secret-like values'
|
|
32
|
+
}
|
|
33
|
+
totalBytes += Buffer.byteLength(line) + 1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (totalBytes > MAX_GENERATED_FILE_BYTES) {
|
|
37
|
+
return `must be at most ${MAX_GENERATED_FILE_BYTES} bytes`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const source = contentLines.join('\n')
|
|
41
|
+
if (/\bwriteFileSync\s*\(/.test(source) && /\bnew\s+URL\s*\(/.test(source)) {
|
|
42
|
+
return 'must derive generated state-file paths from fileURLToPath(import.meta.url) without new URL because ' +
|
|
43
|
+
'browser-like test environments may replace the global URL constructor'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasUnsafeControlCharacter (value) {
|
|
48
|
+
if (hasUnsafeInvisibleCharacter(value)) return true
|
|
49
|
+
|
|
50
|
+
for (const character of value) {
|
|
51
|
+
const code = character.charCodeAt(0)
|
|
52
|
+
if (code <= 0x08 || code === 0x0B || code === 0x0C || (code >= 0x0E && code <= 0x1F) ||
|
|
53
|
+
code === 0x7F) {
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
MAX_GENERATED_FILES,
|
|
62
|
+
getGeneratedFileContentError,
|
|
63
|
+
}
|