dd-trace 4.7.0 → 4.8.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.
Files changed (46) hide show
  1. package/README.md +2 -2
  2. package/package.json +3 -3
  3. package/packages/datadog-core/src/storage/async_resource.js +4 -0
  4. package/packages/datadog-instrumentations/src/couchbase.js +4 -4
  5. package/packages/datadog-instrumentations/src/jest.js +6 -4
  6. package/packages/datadog-plugin-graphql/src/execute.js +6 -4
  7. package/packages/datadog-plugin-jest/src/index.js +8 -3
  8. package/packages/datadog-plugin-openai/src/index.js +39 -16
  9. package/packages/datadog-plugin-openai/src/services.js +13 -9
  10. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +3 -1
  11. package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +45 -0
  12. package/packages/dd-trace/src/appsec/iast/analyzers/index.js +3 -3
  13. package/packages/dd-trace/src/appsec/iast/analyzers/missing-header-analyzer.js +66 -0
  14. package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +25 -8
  15. package/packages/dd-trace/src/appsec/iast/analyzers/xcontenttype-header-missing-analyzer.js +19 -0
  16. package/packages/dd-trace/src/appsec/iast/index.js +5 -2
  17. package/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +4 -2
  18. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +17 -1
  19. package/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js +1 -0
  20. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +5 -1
  21. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +3 -1
  22. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +1 -1
  23. package/packages/dd-trace/src/config.js +26 -10
  24. package/packages/dd-trace/src/external-logger/src/index.js +9 -1
  25. package/packages/dd-trace/src/external-logger/test/index.spec.js +1 -1
  26. package/packages/dd-trace/src/format.js +1 -1
  27. package/packages/dd-trace/src/lambda/handler.js +8 -1
  28. package/packages/dd-trace/src/opentelemetry/span.js +3 -1
  29. package/packages/dd-trace/src/opentracing/span_context.js +2 -1
  30. package/packages/dd-trace/src/plugins/util/ci.js +2 -1
  31. package/packages/dd-trace/src/plugins/util/web.js +1 -0
  32. package/packages/dd-trace/src/profiling/config.js +8 -5
  33. package/packages/dd-trace/src/profiling/exporters/agent.js +4 -1
  34. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  35. package/packages/dd-trace/src/profiling/profilers/wall.js +144 -4
  36. package/packages/dd-trace/src/service-naming/index.js +2 -2
  37. package/packages/dd-trace/src/service-naming/schemas/v0/graphql.js +12 -0
  38. package/packages/dd-trace/src/service-naming/schemas/v0/index.js +2 -1
  39. package/packages/dd-trace/src/service-naming/schemas/v1/graphql.js +12 -0
  40. package/packages/dd-trace/src/service-naming/schemas/v1/index.js +2 -1
  41. package/packages/dd-trace/src/span_processor.js +0 -4
  42. package/packages/dd-trace/src/span_sampler.js +1 -1
  43. package/packages/dd-trace/src/telemetry/dependencies.js +24 -12
  44. package/packages/dd-trace/src/telemetry/metrics.js +11 -1
  45. package/scripts/install_plugin_modules.js +1 -0
  46. package/scripts/version.js +0 -66
package/README.md CHANGED
@@ -143,8 +143,8 @@ $ yarn leak:plugins
143
143
 
144
144
  ### Linting
145
145
 
146
- We use [ESLint](https://eslint.org) to make sure that new code is
147
- conform to our coding standards.
146
+ We use [ESLint](https://eslint.org) to make sure that new code
147
+ conforms to our coding standards.
148
148
 
149
149
  To run the linter, use:
150
150
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "4.7.0",
3
+ "version": "4.8.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -68,9 +68,9 @@
68
68
  "dependencies": {
69
69
  "@datadog/native-appsec": "^3.2.0",
70
70
  "@datadog/native-iast-rewriter": "2.0.1",
71
- "@datadog/native-iast-taint-tracking": "^1.5.0",
71
+ "@datadog/native-iast-taint-tracking": "1.5.0",
72
72
  "@datadog/native-metrics": "^2.0.0",
73
- "@datadog/pprof": "3.0.0",
73
+ "@datadog/pprof": "3.1.0",
74
74
  "@datadog/sketches-js": "^2.1.0",
75
75
  "@opentelemetry/api": "^1.0.0",
76
76
  "@opentelemetry/core": "^1.14.0",
@@ -5,6 +5,7 @@ const { channel } = require('../../../diagnostics_channel')
5
5
 
6
6
  const beforeCh = channel('dd-trace:storage:before')
7
7
  const afterCh = channel('dd-trace:storage:after')
8
+ const enterCh = channel('dd-trace:storage:enter')
8
9
 
9
10
  let PrivateSymbol = Symbol
10
11
  function makePrivateSymbol () {
@@ -52,6 +53,7 @@ class AsyncResourceStorage {
52
53
  const resource = this._executionAsyncResource()
53
54
 
54
55
  resource[this._ddResourceStore] = store
56
+ enterCh.publish()
55
57
  }
56
58
 
57
59
  run (store, callback, ...args) {
@@ -61,11 +63,13 @@ class AsyncResourceStorage {
61
63
  const oldStore = resource[this._ddResourceStore]
62
64
 
63
65
  resource[this._ddResourceStore] = store
66
+ enterCh.publish()
64
67
 
65
68
  try {
66
69
  return callback(...args)
67
70
  } finally {
68
71
  resource[this._ddResourceStore] = oldStore
72
+ enterCh.publish()
69
73
  }
70
74
  }
71
75
 
@@ -160,7 +160,7 @@ function wrapV3Query (query) {
160
160
  }
161
161
 
162
162
  // semver >=2 <3
163
- addHook({ name: 'couchbase', file: 'lib/bucket.js', versions: ['^2.6.5'] }, Bucket => {
163
+ addHook({ name: 'couchbase', file: 'lib/bucket.js', versions: ['^2.6.12'] }, Bucket => {
164
164
  const startCh = channel('apm:couchbase:query:start')
165
165
  const finishCh = channel('apm:couchbase:query:finish')
166
166
  const errorCh = channel('apm:couchbase:query:error')
@@ -208,7 +208,7 @@ addHook({ name: 'couchbase', file: 'lib/bucket.js', versions: ['^2.6.5'] }, Buck
208
208
  return Bucket
209
209
  })
210
210
 
211
- addHook({ name: 'couchbase', file: 'lib/cluster.js', versions: ['^2.6.5'] }, Cluster => {
211
+ addHook({ name: 'couchbase', file: 'lib/cluster.js', versions: ['^2.6.12'] }, Cluster => {
212
212
  Cluster.prototype._maybeInvoke = wrapMaybeInvoke(Cluster.prototype._maybeInvoke)
213
213
  Cluster.prototype.query = wrapQuery(Cluster.prototype.query)
214
214
 
@@ -217,7 +217,7 @@ addHook({ name: 'couchbase', file: 'lib/cluster.js', versions: ['^2.6.5'] }, Clu
217
217
 
218
218
  // semver >=3 <3.2.0
219
219
 
220
- addHook({ name: 'couchbase', file: 'lib/collection.js', versions: ['>=3.0.0 <3.2.0'] }, Collection => {
220
+ addHook({ name: 'couchbase', file: 'lib/collection.js', versions: ['^3.0.7', '^3.1.3'] }, Collection => {
221
221
  wrapAllNames(['upsert', 'insert', 'replace'], name => {
222
222
  shimmer.wrap(Collection.prototype, name, wrapWithName(name))
223
223
  })
@@ -225,7 +225,7 @@ addHook({ name: 'couchbase', file: 'lib/collection.js', versions: ['>=3.0.0 <3.2
225
225
  return Collection
226
226
  })
227
227
 
228
- addHook({ name: 'couchbase', file: 'lib/cluster.js', versions: ['>=3.0.0 <3.2.0'] }, Cluster => {
228
+ addHook({ name: 'couchbase', file: 'lib/cluster.js', versions: ['^3.0.7', '^3.1.3'] }, Cluster => {
229
229
  shimmer.wrap(Cluster.prototype, 'query', wrapV3Query)
230
230
  return Cluster
231
231
  })
@@ -129,8 +129,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
129
129
  suite: this.testSuite,
130
130
  runner: 'jest-circus',
131
131
  testParameters,
132
- frameworkVersion: jestVersion,
133
- testStartLine: getTestLineStart(event.test.asyncError, this.testSuite)
132
+ frameworkVersion: jestVersion
134
133
  })
135
134
  originalTestFns.set(event.test, event.test.fn)
136
135
  event.test.fn = asyncResource.bind(event.test.fn)
@@ -145,7 +144,10 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
145
144
  const formattedError = formatJestError(event.test.errors[0])
146
145
  testErrCh.publish(formattedError)
147
146
  }
148
- testRunFinishCh.publish(status)
147
+ testRunFinishCh.publish({
148
+ status,
149
+ testStartLine: getTestLineStart(event.test.asyncError, this.testSuite)
150
+ })
149
151
  // restore in case it is retried
150
152
  event.test.fn = originalTestFns.get(event.test)
151
153
  })
@@ -471,7 +473,7 @@ function jasmineAsyncInstallWraper (jasmineAsyncInstallExport, jestVersion) {
471
473
  const formattedError = formatJestError(spec.result.failedExpectations[0].error)
472
474
  testErrCh.publish(formattedError)
473
475
  }
474
- testRunFinishCh.publish(specStatusToTestStatus[spec.result.status])
476
+ testRunFinishCh.publish({ status: specStatusToTestStatus[spec.result.status] })
475
477
  onComplete.apply(this, arguments)
476
478
  })
477
479
  arguments[0] = callback
@@ -7,6 +7,8 @@ let tools
7
7
  class GraphQLExecutePlugin extends TracingPlugin {
8
8
  static get id () { return 'graphql' }
9
9
  static get operation () { return 'execute' }
10
+ static get type () { return 'graphql' }
11
+ static get kind () { return 'server' }
10
12
 
11
13
  start ({ operation, args, docSource }) {
12
14
  const type = operation && operation.operation
@@ -14,11 +16,11 @@ class GraphQLExecutePlugin extends TracingPlugin {
14
16
  const document = args.document
15
17
  const source = this.config.source && document && docSource
16
18
 
17
- const span = this.startSpan('graphql.execute', {
18
- service: this.config.service,
19
+ const span = this.startSpan(this.operationName(), {
20
+ service: this.config.service || this.serviceName(),
19
21
  resource: getSignature(document, name, type, this.config.signature),
20
- kind: 'server',
21
- type: 'graphql',
22
+ kind: this.constructor.kind,
23
+ type: this.constructor.type,
22
24
  meta: {
23
25
  'graphql.operation.type': type,
24
26
  'graphql.operation.name': name,
@@ -166,9 +166,12 @@ class JestPlugin extends CiPlugin {
166
166
  this.enter(span, store)
167
167
  })
168
168
 
169
- this.addSub('ci:jest:test:finish', (status) => {
169
+ this.addSub('ci:jest:test:finish', ({ status, testStartLine }) => {
170
170
  const span = storage.getStore().span
171
171
  span.setTag(TEST_STATUS, status)
172
+ if (testStartLine) {
173
+ span.setTag(TEST_SOURCE_START, testStartLine)
174
+ }
172
175
  span.finish()
173
176
  finishAllTraceSpans(span)
174
177
  })
@@ -197,8 +200,10 @@ class JestPlugin extends CiPlugin {
197
200
  const extraTags = {
198
201
  [JEST_TEST_RUNNER]: runner,
199
202
  [TEST_PARAMETERS]: testParameters,
200
- [TEST_FRAMEWORK_VERSION]: frameworkVersion,
201
- [TEST_SOURCE_START]: testStartLine
203
+ [TEST_FRAMEWORK_VERSION]: frameworkVersion
204
+ }
205
+ if (testStartLine) {
206
+ extraTags[TEST_SOURCE_START] = testStartLine
202
207
  }
203
208
 
204
209
  return super.startTestSpan(name, suite, this.testSuiteSpan, extraTags)
@@ -8,6 +8,10 @@ const services = require('./services')
8
8
  const Sampler = require('../../dd-trace/src/sampler')
9
9
  const { MEASURED } = require('../../../ext/tags')
10
10
 
11
+ // String#replaceAll unavailable on Node.js@v14 (dd-trace@<=v3)
12
+ const RE_NEWLINE = /\n/g
13
+ const RE_TAB = /\t/g
14
+
11
15
  // TODO: In the future we should refactor config.js to make it requirable
12
16
  let MAX_TEXT_LEN = 128
13
17
 
@@ -26,7 +30,9 @@ class OpenApiPlugin extends TracingPlugin {
26
30
  this.sampler = new Sampler(0.1) // default 10% log sampling
27
31
 
28
32
  // hoist the max length env var to avoid making all of these functions a class method
29
- MAX_TEXT_LEN = this._tracerConfig.openaiSpanCharLimit
33
+ if (this._tracerConfig) {
34
+ MAX_TEXT_LEN = this._tracerConfig.openaiSpanCharLimit
35
+ }
30
36
  }
31
37
 
32
38
  configure (config) {
@@ -83,11 +89,11 @@ class OpenApiPlugin extends TracingPlugin {
83
89
  store.prompt = prompt
84
90
  if (typeof prompt === 'string' || (Array.isArray(prompt) && typeof prompt[0] === 'number')) {
85
91
  // This is a single prompt, either String or [Number]
86
- tags[`openai.request.prompt`] = normalizeStringOrTokenArray(prompt)
92
+ tags[`openai.request.prompt`] = normalizeStringOrTokenArray(prompt, true)
87
93
  } else if (Array.isArray(prompt)) {
88
94
  // This is multiple prompts, either [String] or [[Number]]
89
95
  for (let i = 0; i < prompt.length; i++) {
90
- tags[`openai.request.prompt.${i}`] = normalizeStringOrTokenArray(prompt[i])
96
+ tags[`openai.request.prompt.${i}`] = normalizeStringOrTokenArray(prompt[i], true)
91
97
  }
92
98
  }
93
99
  }
@@ -363,6 +369,8 @@ function retrieveModelResponseExtraction (tags, body) {
363
369
  tags['openai.response.parent'] = body.parent
364
370
  tags['openai.response.root'] = body.root
365
371
 
372
+ if (!body.permission) return
373
+
366
374
  tags['openai.response.permission.id'] = body.permission[0].id
367
375
  tags['openai.response.permission.created'] = body.permission[0].created
368
376
  tags['openai.response.permission.allow_create_engine'] = body.permission[0].allow_create_engine
@@ -382,10 +390,14 @@ function commonLookupFineTuneRequestExtraction (tags, body) {
382
390
  }
383
391
 
384
392
  function listModelsResponseExtraction (tags, body) {
393
+ if (!body.data) return
394
+
385
395
  tags['openai.response.count'] = body.data.length
386
396
  }
387
397
 
388
398
  function commonImageResponseExtraction (tags, body) {
399
+ if (!body.data) return
400
+
389
401
  tags['openai.response.images_count'] = body.data.length
390
402
 
391
403
  for (let i = 0; i < body.data.length; i++) {
@@ -400,7 +412,7 @@ function createAudioResponseExtraction (tags, body) {
400
412
  tags['openai.response.text'] = body.text
401
413
  tags['openai.response.language'] = body.language
402
414
  tags['openai.response.duration'] = body.duration
403
- tags['openai.response.segments_count'] = body.segments.length
415
+ tags['openai.response.segments_count'] = defensiveArrayLength(body.segments)
404
416
  }
405
417
 
406
418
  function createFineTuneRequestExtraction (tags, body) {
@@ -417,21 +429,24 @@ function createFineTuneRequestExtraction (tags, body) {
417
429
  }
418
430
 
419
431
  function commonFineTuneResponseExtraction (tags, body) {
420
- tags['openai.response.events_count'] = body.events.length
432
+ tags['openai.response.events_count'] = defensiveArrayLength(body.events)
421
433
  tags['openai.response.fine_tuned_model'] = body.fine_tuned_model
422
- tags['openai.response.hyperparams.n_epochs'] = body.hyperparams.n_epochs
423
- tags['openai.response.hyperparams.batch_size'] = body.hyperparams.batch_size
424
- tags['openai.response.hyperparams.prompt_loss_weight'] = body.hyperparams.prompt_loss_weight
425
- tags['openai.response.hyperparams.learning_rate_multiplier'] = body.hyperparams.learning_rate_multiplier
426
- tags['openai.response.training_files_count'] = body.training_files.length
427
- tags['openai.response.result_files_count'] = body.result_files.length
428
- tags['openai.response.validation_files_count'] = body.validation_files.length
434
+ if (body.hyperparams) {
435
+ tags['openai.response.hyperparams.n_epochs'] = body.hyperparams.n_epochs
436
+ tags['openai.response.hyperparams.batch_size'] = body.hyperparams.batch_size
437
+ tags['openai.response.hyperparams.prompt_loss_weight'] = body.hyperparams.prompt_loss_weight
438
+ tags['openai.response.hyperparams.learning_rate_multiplier'] = body.hyperparams.learning_rate_multiplier
439
+ }
440
+ tags['openai.response.training_files_count'] = defensiveArrayLength(body.training_files)
441
+ tags['openai.response.result_files_count'] = defensiveArrayLength(body.result_files)
442
+ tags['openai.response.validation_files_count'] = defensiveArrayLength(body.validation_files)
429
443
  tags['openai.response.updated_at'] = body.updated_at
430
444
  tags['openai.response.status'] = body.status
431
445
  }
432
446
 
433
447
  // the OpenAI package appears to stream the content download then provide it all as a singular string
434
448
  function downloadFileResponseExtraction (tags, body) {
449
+ if (!body.file) return
435
450
  tags['openai.response.total_bytes'] = body.file.length
436
451
  }
437
452
 
@@ -472,6 +487,8 @@ function createRetrieveFileResponseExtraction (tags, body) {
472
487
  function createEmbeddingResponseExtraction (tags, body) {
473
488
  usageExtraction(tags, body)
474
489
 
490
+ if (!body.data) return
491
+
475
492
  tags['openai.response.embeddings_count'] = body.data.length
476
493
  for (let i = 0; i < body.data.length; i++) {
477
494
  tags[`openai.response.embedding.${i}.embedding_length`] = body.data[i].embedding.length
@@ -479,6 +496,7 @@ function createEmbeddingResponseExtraction (tags, body) {
479
496
  }
480
497
 
481
498
  function commonListCountResponseExtraction (tags, body) {
499
+ if (!body.data) return
482
500
  tags['openai.response.count'] = body.data.length
483
501
  }
484
502
 
@@ -486,6 +504,9 @@ function commonListCountResponseExtraction (tags, body) {
486
504
  function createModerationResponseExtraction (tags, body) {
487
505
  tags['openai.response.id'] = body.id
488
506
  // tags[`openai.response.model`] = body.model // redundant, already extracted globally
507
+
508
+ if (!body.results) return
509
+
489
510
  tags['openai.response.flagged'] = body.results[0].flagged
490
511
 
491
512
  for (const [category, match] of Object.entries(body.results[0].categories)) {
@@ -501,6 +522,8 @@ function createModerationResponseExtraction (tags, body) {
501
522
  function commonCreateResponseExtraction (tags, body, store) {
502
523
  usageExtraction(tags, body)
503
524
 
525
+ if (!body.choices) return
526
+
504
527
  tags['openai.response.choices_count'] = body.choices.length
505
528
 
506
529
  store.choices = body.choices
@@ -530,7 +553,7 @@ function usageExtraction (tags, body) {
530
553
  }
531
554
 
532
555
  function truncateApiKey (apiKey) {
533
- return `sk-...${apiKey.substr(apiKey.length - 4)}`
556
+ return apiKey && `sk-...${apiKey.substr(apiKey.length - 4)}`
534
557
  }
535
558
 
536
559
  /**
@@ -540,8 +563,8 @@ function truncateText (text) {
540
563
  if (!text) return
541
564
 
542
565
  text = text
543
- .replaceAll('\n', '\\n')
544
- .replaceAll('\t', '\\t')
566
+ .replace(RE_NEWLINE, '\\n')
567
+ .replace(RE_TAB, '\\t')
545
568
 
546
569
  if (text.length > MAX_TEXT_LEN) {
547
570
  return text.substring(0, MAX_TEXT_LEN) + '...'
@@ -671,7 +694,7 @@ function normalizeRequestPayload (methodName, args) {
671
694
  * "foo" -> "foo"
672
695
  * [1,2,3] -> "[1, 2, 3]"
673
696
  */
674
- function normalizeStringOrTokenArray (input, truncate = true) {
697
+ function normalizeStringOrTokenArray (input, truncate) {
675
698
  const normalized = Array.isArray(input)
676
699
  ? `[${input.join(', ')}]` // "[1, 2, 999]"
677
700
  : input // "foo"
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { DogStatsDClient, NoopDogStatsDClient } = require('../../dd-trace/src/dogstatsd')
4
- const ExternalLogger = require('../../dd-trace/src/external-logger/src')
4
+ const { ExternalLogger, NoopExternalLogger } = require('../../dd-trace/src/external-logger/src')
5
5
 
6
6
  const FLUSH_INTERVAL = 10 * 1000
7
7
 
@@ -10,7 +10,7 @@ let logger = null
10
10
  let interval = null
11
11
 
12
12
  module.exports.init = function (tracerConfig) {
13
- if (tracerConfig.dogstatsd) {
13
+ if (tracerConfig && tracerConfig.dogstatsd) {
14
14
  metrics = new DogStatsDClient({
15
15
  host: tracerConfig.dogstatsd.hostname,
16
16
  port: tracerConfig.dogstatsd.port,
@@ -24,13 +24,17 @@ module.exports.init = function (tracerConfig) {
24
24
  metrics = new NoopDogStatsDClient()
25
25
  }
26
26
 
27
- logger = new ExternalLogger({
28
- ddsource: 'openai',
29
- hostname: tracerConfig.hostname,
30
- service: tracerConfig.service,
31
- apiKey: tracerConfig.apiKey,
32
- interval: FLUSH_INTERVAL
33
- })
27
+ if (tracerConfig && tracerConfig.apiKey) {
28
+ logger = new ExternalLogger({
29
+ ddsource: 'openai',
30
+ hostname: tracerConfig.hostname,
31
+ service: tracerConfig.service,
32
+ apiKey: tracerConfig.apiKey,
33
+ interval: FLUSH_INTERVAL
34
+ })
35
+ } else {
36
+ logger = new NoopExternalLogger()
37
+ }
34
38
 
35
39
  interval = setInterval(() => {
36
40
  metrics.flush()
@@ -2,6 +2,7 @@
2
2
 
3
3
  module.exports = {
4
4
  'COMMAND_INJECTION_ANALYZER': require('./command-injection-analyzer'),
5
+ 'HSTS_HEADER_MISSING_ANALYZER': require('./hsts-header-missing-analyzer'),
5
6
  'INSECURE_COOKIE_ANALYZER': require('./insecure-cookie-analyzer'),
6
7
  'LDAP_ANALYZER': require('./ldap-injection-analyzer'),
7
8
  'NO_HTTPONLY_COOKIE_ANALYZER': require('./no-httponly-cookie-analyzer'),
@@ -11,5 +12,6 @@ module.exports = {
11
12
  'SSRF': require('./ssrf-analyzer'),
12
13
  'UNVALIDATED_REDIRECT_ANALYZER': require('./unvalidated-redirect-analyzer'),
13
14
  'WEAK_CIPHER_ANALYZER': require('./weak-cipher-analyzer'),
14
- 'WEAK_HASH_ANALYZER': require('./weak-hash-analyzer')
15
+ 'WEAK_HASH_ANALYZER': require('./weak-hash-analyzer'),
16
+ 'XCONTENTTYPE_HEADER_MISSING_ANALYZER': require('./xcontenttype-header-missing-analyzer')
15
17
  }
@@ -0,0 +1,45 @@
1
+ 'use strict'
2
+
3
+ const { HSTS_HEADER_MISSING } = require('../vulnerabilities')
4
+ const { MissingHeaderAnalyzer } = require('./missing-header-analyzer')
5
+
6
+ const HSTS_HEADER_NAME = 'Strict-Transport-Security'
7
+ const HEADER_VALID_PREFIX = 'max-age'
8
+ class HstsHeaderMissingAnalyzer extends MissingHeaderAnalyzer {
9
+ constructor () {
10
+ super(HSTS_HEADER_MISSING, HSTS_HEADER_NAME)
11
+ }
12
+ _isVulnerableFromRequestAndResponse (req, res) {
13
+ const headerToCheck = res.getHeader(HSTS_HEADER_NAME)
14
+ return !this._isHeaderValid(headerToCheck) && this._isHttpsProtocol(req)
15
+ }
16
+
17
+ _isHeaderValid (headerValue) {
18
+ if (!headerValue) {
19
+ return false
20
+ }
21
+ headerValue = headerValue.trim()
22
+
23
+ if (!headerValue.startsWith(HEADER_VALID_PREFIX)) {
24
+ return false
25
+ }
26
+
27
+ const semicolonIndex = headerValue.indexOf(';')
28
+ let timestampString
29
+ if (semicolonIndex > -1) {
30
+ timestampString = headerValue.substring(HEADER_VALID_PREFIX.length + 1, semicolonIndex)
31
+ } else {
32
+ timestampString = headerValue.substring(HEADER_VALID_PREFIX.length + 1)
33
+ }
34
+
35
+ const timestamp = parseInt(timestampString)
36
+ // eslint-disable-next-line eqeqeq
37
+ return timestamp == timestampString && timestamp > 0
38
+ }
39
+
40
+ _isHttpsProtocol (req) {
41
+ return req.protocol === 'https' || req.headers['x-forwarded-proto'] === 'https'
42
+ }
43
+ }
44
+
45
+ module.exports = new HstsHeaderMissingAnalyzer()
@@ -3,10 +3,10 @@
3
3
  const analyzers = require('./analyzers')
4
4
  const setCookiesHeaderInterceptor = require('./set-cookies-header-interceptor')
5
5
 
6
- function enableAllAnalyzers () {
7
- setCookiesHeaderInterceptor.configure(true)
6
+ function enableAllAnalyzers (tracerConfig) {
7
+ setCookiesHeaderInterceptor.configure({ enabled: true, tracerConfig })
8
8
  for (const analyzer in analyzers) {
9
- analyzers[analyzer].configure(true)
9
+ analyzers[analyzer].configure({ enabled: true, tracerConfig })
10
10
  }
11
11
  }
12
12
 
@@ -0,0 +1,66 @@
1
+ 'use strict'
2
+
3
+ const Analyzer = require('./vulnerability-analyzer')
4
+
5
+ const SC_MOVED_PERMANENTLY = 301
6
+ const SC_MOVED_TEMPORARILY = 302
7
+ const SC_NOT_MODIFIED = 304
8
+ const SC_TEMPORARY_REDIRECT = 307
9
+ const SC_NOT_FOUND = 404
10
+ const SC_GONE = 410
11
+ const SC_INTERNAL_SERVER_ERROR = 500
12
+
13
+ const IGNORED_RESPONSE_STATUS_LIST = [SC_MOVED_PERMANENTLY, SC_MOVED_TEMPORARILY, SC_NOT_MODIFIED,
14
+ SC_TEMPORARY_REDIRECT, SC_NOT_FOUND, SC_GONE, SC_INTERNAL_SERVER_ERROR]
15
+ const HTML_CONTENT_TYPES = ['text/html', 'application/xhtml+xml']
16
+
17
+ class MissingHeaderAnalyzer extends Analyzer {
18
+ constructor (type, headerName) {
19
+ super(type)
20
+
21
+ this.headerName = headerName
22
+ }
23
+
24
+ onConfigure () {
25
+ this.addSub({
26
+ channelName: 'datadog:iast:response-end',
27
+ moduleName: 'http'
28
+ }, (data) => this.analyze(data))
29
+ }
30
+
31
+ _getLocation () {
32
+ return undefined
33
+ }
34
+
35
+ _checkOCE (context) {
36
+ return true
37
+ }
38
+
39
+ _createHashSource (type, evidence, location) {
40
+ return `${type}:${this.config.tracerConfig.service}`
41
+ }
42
+
43
+ _getEvidence ({ res }) {
44
+ return { value: res.getHeader(this.headerName) }
45
+ }
46
+
47
+ _isVulnerable ({ req, res }, context) {
48
+ if (!IGNORED_RESPONSE_STATUS_LIST.includes(res.statusCode) && this._isResponseHtml(res)) {
49
+ return this._isVulnerableFromRequestAndResponse(req, res)
50
+ }
51
+ return false
52
+ }
53
+
54
+ _isVulnerableFromRequestAndResponse (req, res) {
55
+ return false
56
+ }
57
+
58
+ _isResponseHtml (res) {
59
+ const contentType = res.getHeader('content-type')
60
+ return contentType && HTML_CONTENT_TYPES.some(htmlContentType => {
61
+ return htmlContentType === contentType || contentType.startsWith(htmlContentType + ';')
62
+ })
63
+ }
64
+ }
65
+
66
+ module.exports = { MissingHeaderAnalyzer }
@@ -4,7 +4,11 @@ const InjectionAnalyzer = require('./injection-analyzer')
4
4
  const { UNVALIDATED_REDIRECT } = require('../vulnerabilities')
5
5
  const { getNodeModulesPaths } = require('../path-line')
6
6
  const { getRanges } = require('../taint-tracking/operations')
7
- const { HTTP_REQUEST_HEADER_VALUE } = require('../taint-tracking/source-types')
7
+ const {
8
+ HTTP_REQUEST_HEADER_VALUE,
9
+ HTTP_REQUEST_PATH,
10
+ HTTP_REQUEST_PATH_PARAM
11
+ } = require('../taint-tracking/source-types')
8
12
 
9
13
  const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js')
10
14
 
@@ -17,9 +21,6 @@ class UnvalidatedRedirectAnalyzer extends InjectionAnalyzer {
17
21
  this.addSub('datadog:http:server:response:set-header:finish', ({ name, value }) => this.analyze(name, value))
18
22
  }
19
23
 
20
- // TODO: In case the location header value is tainted, this analyzer should check the ranges of the tainted.
21
- // And do not report a vulnerability if source of the ranges (range.iinfo.type) are exclusively url or path params
22
- // to avoid false positives.
23
24
  analyze (name, value) {
24
25
  if (!this.isLocationHeader(name) || typeof value !== 'string') return
25
26
 
@@ -34,12 +35,28 @@ class UnvalidatedRedirectAnalyzer extends InjectionAnalyzer {
34
35
  if (!value) return false
35
36
 
36
37
  const ranges = getRanges(iastContext, value)
37
- return ranges && ranges.length > 0 && !this._isRefererHeader(ranges)
38
+ return ranges && ranges.length > 0 && !this._areSafeRanges(ranges)
38
39
  }
39
40
 
40
- _isRefererHeader (ranges) {
41
- return ranges && ranges.every(range => range.iinfo.type === HTTP_REQUEST_HEADER_VALUE &&
42
- range.iinfo.parameterName && range.iinfo.parameterName.toLowerCase() === 'referer')
41
+ // Do not report vulnerability if ranges sources are exclusively url,
42
+ // path params or referer header to avoid false positives.
43
+ _areSafeRanges (ranges) {
44
+ return ranges && ranges.every(
45
+ range => this._isPathParam(range) || this._isUrl(range) || this._isRefererHeader(range)
46
+ )
47
+ }
48
+
49
+ _isRefererHeader (range) {
50
+ return range.iinfo.type === HTTP_REQUEST_HEADER_VALUE &&
51
+ range.iinfo.parameterName && range.iinfo.parameterName.toLowerCase() === 'referer'
52
+ }
53
+
54
+ _isPathParam (range) {
55
+ return range.iinfo.type === HTTP_REQUEST_PATH_PARAM
56
+ }
57
+
58
+ _isUrl (range) {
59
+ return range.iinfo.type === HTTP_REQUEST_PATH
43
60
  }
44
61
 
45
62
  _getExcludedPaths () {
@@ -0,0 +1,19 @@
1
+ 'use strict'
2
+
3
+ const { XCONTENTTYPE_HEADER_MISSING } = require('../vulnerabilities')
4
+ const { MissingHeaderAnalyzer } = require('./missing-header-analyzer')
5
+
6
+ const XCONTENTTYPEOPTIONS_HEADER_NAME = 'X-Content-Type-Options'
7
+
8
+ class XcontenttypeHeaderMissingAnalyzer extends MissingHeaderAnalyzer {
9
+ constructor () {
10
+ super(XCONTENTTYPE_HEADER_MISSING, XCONTENTTYPEOPTIONS_HEADER_NAME)
11
+ }
12
+
13
+ _isVulnerableFromRequestAndResponse (req, res) {
14
+ const headerToCheck = res.getHeader(XCONTENTTYPEOPTIONS_HEADER_NAME)
15
+ return !headerToCheck || headerToCheck.trim().toLowerCase() !== 'nosniff'
16
+ }
17
+ }
18
+
19
+ module.exports = new XcontenttypeHeaderMissingAnalyzer()
@@ -19,10 +19,11 @@ const iastTelemetry = require('./telemetry')
19
19
  // order of the callbacks can be enforce
20
20
  const requestStart = dc.channel('dd-trace:incomingHttpRequestStart')
21
21
  const requestClose = dc.channel('dd-trace:incomingHttpRequestEnd')
22
+ const iastResponseEnd = dc.channel('datadog:iast:response-end')
22
23
 
23
24
  function enable (config, _tracer) {
24
25
  iastTelemetry.configure(config, config.iast && config.iast.telemetryVerbosity)
25
- enableAllAnalyzers()
26
+ enableAllAnalyzers(config)
26
27
  enableTaintTracking(config.iast, iastTelemetry.verbosity)
27
28
  requestStart.subscribe(onIncomingHttpRequestStart)
28
29
  requestClose.subscribe(onIncomingHttpRequestEnd)
@@ -54,7 +55,7 @@ function onIncomingHttpRequestStart (data) {
54
55
  createTransaction(rootSpan.context().toSpanId(), iastContext)
55
56
  overheadController.initializeRequestContext(iastContext)
56
57
  iastTelemetry.onRequestStart(iastContext)
57
- taintTrackingPlugin.taintHeaders(data.req.headers, iastContext)
58
+ taintTrackingPlugin.taintRequest(data.req, iastContext)
58
59
  }
59
60
  if (rootSpan.addTags) {
60
61
  rootSpan.addTags({
@@ -72,6 +73,8 @@ function onIncomingHttpRequestEnd (data) {
72
73
  const topContext = web.getContext(data.req)
73
74
  const iastContext = iastContextFunctions.getIastContext(store, topContext)
74
75
  if (iastContext && iastContext.rootSpan) {
76
+ iastResponseEnd.publish(data)
77
+
75
78
  const vulnerabilities = iastContext.vulnerabilities
76
79
  const rootSpan = iastContext.rootSpan
77
80
  vulnerabilityReporter.sendVulnerabilities(vulnerabilities, rootSpan)
@@ -1,11 +1,13 @@
1
1
  'use strict'
2
2
 
3
3
  const { enableRewriter, disableRewriter } = require('./rewriter')
4
- const { createTransaction,
4
+ const {
5
+ createTransaction,
5
6
  removeTransaction,
6
7
  setMaxTransactions,
7
8
  enableTaintOperations,
8
- disableTaintOperations } = require('./operations')
9
+ disableTaintOperations
10
+ } = require('./operations')
9
11
 
10
12
  const taintTrackingPlugin = require('./plugin')
11
13