Skip to content

Commit

Permalink
Add guardrails for SSI
Browse files Browse the repository at this point in the history
  • Loading branch information
bengl committed Jun 18, 2024
1 parent 1cd017b commit eb145ba
Show file tree
Hide file tree
Showing 9 changed files with 526 additions and 79 deletions.
32 changes: 31 additions & 1 deletion init.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@

const path = require('path')
const Module = require('module')
const telemetry = require('./packages/dd-trace/src/telemetry/init-telemetry')
const Config = require('./packages/dd-trace/src/config')
const log = require('./packages/dd-trace/src/log')

// eslint-disable-next-line no-new
new Config() // we need this to initialize the logger

let initBailout = false
let clobberBailout = false
const forced = ['1', 'true', 'True'].includes(process.env.DD_INJECT_FORCE)

if (process.env.DD_INJECTION_ENABLED) {
// If we're running via single-step install, and we're not in the app's
Expand All @@ -19,13 +27,35 @@ if (process.env.DD_INJECTION_ENABLED) {
if (resolvedInApp) {
const ourselves = path.join(__dirname, 'index.js')
if (ourselves !== resolvedInApp) {
clobberBailout = true
}
}

// If we're running via single-step install, and the runtime doesn't match
// the engines field in package.json, then we should not initialize the tracer.
if (!clobberBailout) {
const { engines } = require('./package.json')
const version = process.versions.node
const semver = require('semver')
if (!semver.satisfies(version, engines.node)) {
initBailout = true
telemetry([
{ name: 'abort', tags: ['reason:incompatible_runtime'] },
{ name: 'abort.runtime', tags: [] }
])
log.info('Aborting application instrumentation due to incompatible_runtime.')
log.info(`Found incompatible runtime nodejs ${version}, Supported runtimes: nodejs ${engines.node}.`)
if (forced) {
log.info('DD_INJECT_FORCE enabled, allowing unsupported runtimes and continuing.')
}
}
}
}

if (!initBailout) {
if (!clobberBailout && (!initBailout || forced)) {
const tracer = require('.')
tracer.init()
module.exports = tracer
telemetry('complete', [`injection_forced:${forced && initBailout ? 'true' : 'false'}`])
log.info('Application instrumentation bootstrapping complete')
}
170 changes: 168 additions & 2 deletions integration-tests/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const path = require('path')
const rimraf = promisify(require('rimraf'))
const id = require('../packages/dd-trace/src/id')
const upload = require('multer')()
const assert = require('assert')

const hookFile = 'dd-trace/loader-hook.mjs'

Expand Down Expand Up @@ -162,6 +163,94 @@ class FakeAgent extends EventEmitter {
}
}

async function runAndCheckOutput (filename, cwd, expectedOut) {
const proc = fork(filename, { cwd, stdio: 'pipe' })
const pid = proc.pid
let out = await new Promise((resolve, reject) => {
proc.on('error', reject)
let out = Buffer.alloc(0)
proc.stdout.on('data', data => {
out = Buffer.concat([out, data])
})
proc.on('exit', () => resolve(out.toString('utf8')))
setTimeout(() => {
if (proc.exitCode === null) proc.kill()
}, 1000) // TODO this introduces flakiness. find a better way to end the process.
})
if (typeof expectedOut === 'function') {
expectedOut(out)
} else {
if (process.env.DD_TRACE_DEBUG) {
// Debug adds this, which we don't care about in these tests
out = out.replace('Flushing 0 metrics via HTTP\n', '')
}
assert.strictEqual(out, expectedOut)
}
return pid
}

// This is set by the useSandbox function
let sandbox

// This _must_ be used with the useSandbox function
async function runAndCheckWithTelemetry (filename, expectedOut, ...expectedTelemetryPoints) {
const cwd = sandbox.folder
const cleanup = telemetryForwarder(expectedTelemetryPoints)
const pid = await runAndCheckOutput(filename, cwd, expectedOut)
const msgs = await cleanup()
if (expectedTelemetryPoints.length === 0) {
// assert no telemetry sent
try {
assert.deepStrictEqual(msgs.length, 0)
} catch (e) {
// This console.log is useful for debugging telemetry. Plz don't remove.
// eslint-disable-next-line no-console
console.error('Expected no telemetry, but got:\n', msgs.map(msg => JSON.stringify(msg[1].points)).join('\n'))
throw e
}
return
}
let points = []
for (const [telemetryType, data] of msgs) {
assert.strictEqual(telemetryType, 'library_entrypoint')
assert.deepStrictEqual(data.metadata, meta(pid))
points = points.concat(data.points)
}
let expectedPoints = getPoints(...expectedTelemetryPoints)
// We now have to sort both the expected and actual telemetry points.
// This is because data can come in in any order.
// We'll just contatenate all the data together for each point and sort them.
points = points.map(p => p.name + '\t' + p.tags.join(',')).sort().join('\n')
expectedPoints = expectedPoints.map(p => p.name + '\t' + p.tags.join(',')).sort().join('\n')
assert.strictEqual(points, expectedPoints)

function getPoints (...args) {
const expectedPoints = []
let currentPoint = {}
for (const arg of args) {
if (!currentPoint.name) {
currentPoint.name = 'library_entrypoint.' + arg
} else {
currentPoint.tags = arg.split(',')
expectedPoints.push(currentPoint)
currentPoint = {}
}
}
return expectedPoints
}

function meta (pid) {
return {
language_name: 'nodejs',
language_version: process.env.FAKE_VERSION || process.versions.node,
runtime_name: 'nodejs',
runtime_version: process.env.FAKE_VERSION || process.versions.node,
tracer_version: require('../package.json').version,
pid: Number(pid)
}
}
}

function spawnProc (filename, options = {}, stdioHandler) {
const proc = fork(filename, { ...options, stdio: 'pipe' })
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -206,7 +295,7 @@ async function createSandbox (dependencies = [], isGitRepo = false,
const { NODE_OPTIONS, ...restOfEnv } = process.env

await fs.mkdir(folder)
await exec(`yarn pack --filename ${out}`) // TODO: cache this
await exec(`yarn pack --filename ${out}`, { env: restOfEnv }) // TODO: cache this
await exec(`yarn add ${allDependencies.join(' ')}`, { cwd: folder, env: restOfEnv })

for (const path of integrationTestsPaths) {
Expand Down Expand Up @@ -245,6 +334,54 @@ async function createSandbox (dependencies = [], isGitRepo = false,
}
}

function telemetryForwarder (expectedTelemetryPoints) {
process.env.DD_TELEMETRY_FORWARDER_PATH =
path.join(__dirname, 'telemetry-forwarder.sh')
process.env.FORWARDER_OUT = path.join(__dirname, `forwarder-${Date.now()}.out`)

let retries = 0

const tryAgain = async function () {
retries += 1
await new Promise(resolve => setTimeout(resolve, 100))
return cleanup()
}

const cleanup = async function () {
let msgs
try {
msgs = (await fs.readFile(process.env.FORWARDER_OUT, 'utf8')).trim().split('\n')
} catch (e) {
if (expectedTelemetryPoints.length && e.code === 'ENOENT' && retries < 10) {
return tryAgain()
}
return []
}
for (let i = 0; i < msgs.length; i++) {
const [telemetryType, data] = msgs[i].split('\t')
if (!data && retries < 10) {
return tryAgain()
}
let parsed
try {
parsed = JSON.parse(data)
} catch (e) {
if (!data && retries < 10) {
return tryAgain()
}
throw new SyntaxError(`error parsing data: ${e.message}\n${data}`)
}
msgs[i] = [telemetryType, parsed]
}
await fs.unlink(process.env.FORWARDER_OUT)
delete process.env.FORWARDER_OUT
delete process.env.DD_TELEMETRY_FORWARDER_PATH
return msgs
}

return cleanup
}

async function curl (url, useHttp2 = false) {
if (typeof url === 'object') {
if (url.then) {
Expand Down Expand Up @@ -313,14 +450,43 @@ async function spawnPluginIntegrationTestProc (cwd, serverFile, agentPort, stdio
}, stdioHandler)
}

function useEnv (env) {
before(() => {
Object.assign(process.env, env)
})
after(() => {
for (const key of Object.keys(env)) {
delete process.env[key]
}
})
}

function useSandbox (...args) {
before(async () => {
sandbox = await createSandbox(...args)
})
after(() => {
const oldSandbox = sandbox
sandbox = undefined
return oldSandbox.remove()
})
}
function sandboxCwd () {
return sandbox.folder
}

module.exports = {
FakeAgent,
spawnProc,
runAndCheckWithTelemetry,
createSandbox,
curl,
curlAndAssertMessage,
getCiVisAgentlessConfig,
getCiVisEvpProxyConfig,
checkSpansForServiceName,
spawnPluginIntegrationTestProc
spawnPluginIntegrationTestProc,
useEnv,
useSandbox,
sandboxCwd
}
Loading

0 comments on commit eb145ba

Please sign in to comment.