dd-trace 4.11.1 → 4.13.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 (34) hide show
  1. package/README.md +4 -9
  2. package/index.d.ts +43 -0
  3. package/package.json +3 -2
  4. package/packages/datadog-esbuild/index.js +29 -9
  5. package/packages/datadog-instrumentations/src/cucumber.js +30 -11
  6. package/packages/datadog-instrumentations/src/jest.js +22 -11
  7. package/packages/datadog-instrumentations/src/mocha.js +30 -8
  8. package/packages/datadog-instrumentations/src/next.js +102 -16
  9. package/packages/datadog-instrumentations/src/openai.js +1 -1
  10. package/packages/datadog-instrumentations/src/pg.js +46 -0
  11. package/packages/datadog-plugin-cucumber/src/index.js +14 -2
  12. package/packages/datadog-plugin-cypress/src/plugin.js +17 -8
  13. package/packages/datadog-plugin-graphql/src/index.js +3 -3
  14. package/packages/datadog-plugin-jest/src/index.js +10 -2
  15. package/packages/datadog-plugin-jest/src/util.js +10 -4
  16. package/packages/datadog-plugin-mocha/src/index.js +14 -2
  17. package/packages/datadog-plugin-next/src/index.js +11 -3
  18. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +19 -4
  19. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations.js +1 -1
  20. package/packages/dd-trace/src/appsec/iast/telemetry/index.js +14 -5
  21. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +120 -10
  22. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +0 -1
  23. package/packages/dd-trace/src/dogstatsd.js +65 -5
  24. package/packages/dd-trace/src/exporters/agent/writer.js +9 -9
  25. package/packages/dd-trace/src/exporters/common/request.js +13 -4
  26. package/packages/dd-trace/src/opentracing/span.js +13 -13
  27. package/packages/dd-trace/src/opentracing/tracer.js +3 -3
  28. package/packages/dd-trace/src/plugins/ci_plugin.js +22 -1
  29. package/packages/dd-trace/src/plugins/util/test.js +18 -1
  30. package/packages/dd-trace/src/profiling/config.js +3 -1
  31. package/packages/dd-trace/src/profiling/profilers/wall.js +23 -7
  32. package/packages/dd-trace/src/proxy.js +23 -2
  33. package/packages/dd-trace/src/ritm.js +10 -2
  34. /package/packages/dd-trace/src/{metrics.js → runtime_metrics.js} +0 -0
@@ -11,8 +11,12 @@ const startCh = channel('apm:pg:query:start')
11
11
  const finishCh = channel('apm:pg:query:finish')
12
12
  const errorCh = channel('apm:pg:query:error')
13
13
 
14
+ const startPoolQueryCh = channel('datadog:pg:pool:query:start')
15
+ const finishPoolQueryCh = channel('datadog:pg:pool:query:finish')
16
+
14
17
  addHook({ name: 'pg', versions: ['>=8.0.3'] }, pg => {
15
18
  shimmer.wrap(pg.Client.prototype, 'query', query => wrapQuery(query))
19
+ shimmer.wrap(pg.Pool.prototype, 'query', query => wrapPoolQuery(query))
16
20
  return pg
17
21
  })
18
22
 
@@ -97,3 +101,45 @@ function wrapQuery (query) {
97
101
  })
98
102
  }
99
103
  }
104
+
105
+ function wrapPoolQuery (query) {
106
+ return function () {
107
+ if (!startPoolQueryCh.hasSubscribers) {
108
+ return query.apply(this, arguments)
109
+ }
110
+
111
+ const asyncResource = new AsyncResource('bound-anonymous-fn')
112
+
113
+ const pgQuery = arguments[0] && typeof arguments[0] === 'object' ? arguments[0] : { text: arguments[0] }
114
+
115
+ return asyncResource.runInAsyncScope(() => {
116
+ startPoolQueryCh.publish({
117
+ query: pgQuery
118
+ })
119
+
120
+ const finish = asyncResource.bind(function () {
121
+ finishPoolQueryCh.publish()
122
+ })
123
+
124
+ const cb = arguments[arguments.length - 1]
125
+ if (typeof cb === 'function') {
126
+ arguments[arguments.length - 1] = shimmer.wrap(cb, function () {
127
+ finish()
128
+ return cb.apply(this, arguments)
129
+ })
130
+ }
131
+
132
+ const retval = query.apply(this, arguments)
133
+
134
+ if (retval && retval.then) {
135
+ retval.then(() => {
136
+ finish()
137
+ }).catch(() => {
138
+ finish()
139
+ })
140
+ }
141
+
142
+ return retval
143
+ })
144
+ }
145
+ }
@@ -25,12 +25,24 @@ class CucumberPlugin extends CiPlugin {
25
25
 
26
26
  this.sourceRoot = process.cwd()
27
27
 
28
- this.addSub('ci:cucumber:session:finish', ({ status, isSuitesSkipped, testCodeCoverageLinesTotal }) => {
28
+ this.addSub('ci:cucumber:session:finish', ({
29
+ status,
30
+ isSuitesSkipped,
31
+ numSkippedSuites,
32
+ testCodeCoverageLinesTotal
33
+ }) => {
29
34
  const { isSuitesSkippingEnabled, isCodeCoverageEnabled } = this.itrConfig || {}
30
35
  addIntelligentTestRunnerSpanTags(
31
36
  this.testSessionSpan,
32
37
  this.testModuleSpan,
33
- { isSuitesSkipped, isSuitesSkippingEnabled, isCodeCoverageEnabled, testCodeCoverageLinesTotal }
38
+ {
39
+ isSuitesSkipped,
40
+ isSuitesSkippingEnabled,
41
+ isCodeCoverageEnabled,
42
+ testCodeCoverageLinesTotal,
43
+ skippingCount: numSkippedSuites,
44
+ skippingType: 'suite'
45
+ }
34
46
  )
35
47
 
36
48
  this.testSessionSpan.setTag(TEST_STATUS, status)
@@ -20,7 +20,8 @@ const {
20
20
  finishAllTraceSpans,
21
21
  getCoveredFilenamesFromCoverage,
22
22
  getTestSuitePath,
23
- addIntelligentTestRunnerSpanTags
23
+ addIntelligentTestRunnerSpanTags,
24
+ TEST_SKIPPED_BY_ITR
24
25
  } = require('../../dd-trace/src/plugins/util/test')
25
26
  const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants')
26
27
  const log = require('../../dd-trace/src/log')
@@ -120,6 +121,7 @@ function getSkippableTests (isSuitesSkippingEnabled, tracer, testConfiguration)
120
121
 
121
122
  module.exports = (on, config) => {
122
123
  let isTestsSkipped = false
124
+ const skippedTests = []
123
125
  const tracer = require('../../dd-trace')
124
126
  const testEnvironmentMetadata = getTestEnvironmentMetadata(TEST_FRAMEWORK_NAME)
125
127
 
@@ -248,19 +250,23 @@ module.exports = (on, config) => {
248
250
  const cypressTests = tests || []
249
251
  const finishedTests = finishedTestsByFile[spec.relative] || []
250
252
 
251
- // Get tests that didn't go through `dd:afterEach` and haven't been skipped by ITR
253
+ // Get tests that didn't go through `dd:afterEach`
252
254
  // and create a skipped test span for each of them
253
255
  cypressTests.filter(({ title }) => {
254
256
  const cypressTestName = title.join(' ')
255
- const isSkippedByItr = testsToSkip.find(test =>
256
- cypressTestName === test.name && spec.relative === test.suite
257
- )
258
257
  const isTestFinished = finishedTests.find(({ testName }) => cypressTestName === testName)
259
258
 
260
- return !isSkippedByItr && !isTestFinished
259
+ return !isTestFinished
261
260
  }).forEach(({ title }) => {
262
- const skippedTestSpan = getTestSpan(title.join(' '), spec.relative)
261
+ const cypressTestName = title.join(' ')
262
+ const isSkippedByItr = testsToSkip.find(test =>
263
+ cypressTestName === test.name && spec.relative === test.suite
264
+ )
265
+ const skippedTestSpan = getTestSpan(cypressTestName, spec.relative)
263
266
  skippedTestSpan.setTag(TEST_STATUS, 'skip')
267
+ if (isSkippedByItr) {
268
+ skippedTestSpan.setTag(TEST_SKIPPED_BY_ITR, 'true')
269
+ }
264
270
  skippedTestSpan.finish()
265
271
  })
266
272
 
@@ -309,7 +315,9 @@ module.exports = (on, config) => {
309
315
  {
310
316
  isSuitesSkipped: isTestsSkipped,
311
317
  isSuitesSkippingEnabled,
312
- isCodeCoverageEnabled
318
+ isCodeCoverageEnabled,
319
+ skippingType: 'test',
320
+ skippingCount: skippedTests.length
313
321
  }
314
322
  )
315
323
 
@@ -353,6 +361,7 @@ module.exports = (on, config) => {
353
361
  if (testsToSkip.find(test => {
354
362
  return testName === test.name && testSuite === test.suite
355
363
  })) {
364
+ skippedTests.push(test)
356
365
  isTestsSkipped = true
357
366
  return { shouldSkip: true }
358
367
  }
@@ -56,9 +56,9 @@ function getVariablesFilter (config) {
56
56
 
57
57
  function getHooks (config) {
58
58
  const noop = () => { }
59
- const execute = (config.hooks && config.hooks.execute) || noop
60
- const parse = (config.hooks && config.hooks.parse) || noop
61
- const validate = (config.hooks && config.hooks.validate) || noop
59
+ const execute = config.hooks?.execute || noop
60
+ const parse = config.hooks?.parse || noop
61
+ const validate = config.hooks?.validate || noop
62
62
 
63
63
  return { execute, parse, validate }
64
64
  }
@@ -49,7 +49,8 @@ class JestPlugin extends CiPlugin {
49
49
  isSuitesSkipped,
50
50
  isSuitesSkippingEnabled,
51
51
  isCodeCoverageEnabled,
52
- testCodeCoverageLinesTotal
52
+ testCodeCoverageLinesTotal,
53
+ numSkippedSuites
53
54
  }) => {
54
55
  this.testSessionSpan.setTag(TEST_STATUS, status)
55
56
  this.testModuleSpan.setTag(TEST_STATUS, status)
@@ -57,7 +58,14 @@ class JestPlugin extends CiPlugin {
57
58
  addIntelligentTestRunnerSpanTags(
58
59
  this.testSessionSpan,
59
60
  this.testModuleSpan,
60
- { isSuitesSkipped, isSuitesSkippingEnabled, isCodeCoverageEnabled, testCodeCoverageLinesTotal }
61
+ {
62
+ isSuitesSkipped,
63
+ isSuitesSkippingEnabled,
64
+ isCodeCoverageEnabled,
65
+ testCodeCoverageLinesTotal,
66
+ skippingType: 'suite',
67
+ skippingCount: numSkippedSuites
68
+ }
61
69
  )
62
70
 
63
71
  this.testModuleSpan.finish()
@@ -48,10 +48,16 @@ function getJestTestName (test) {
48
48
  }
49
49
 
50
50
  function getJestSuitesToRun (skippableSuites, originalTests, rootDir) {
51
- return originalTests.filter(({ path: testPath }) => {
52
- const relativePath = getTestSuitePath(testPath, rootDir)
53
- return !skippableSuites.includes(relativePath)
54
- })
51
+ return originalTests.reduce((acc, test) => {
52
+ const relativePath = getTestSuitePath(test.path, rootDir)
53
+ const shouldBeSkipped = skippableSuites.includes(relativePath)
54
+ if (shouldBeSkipped) {
55
+ acc.skippedSuites.push(relativePath)
56
+ } else {
57
+ acc.suitesToRun.push(test)
58
+ }
59
+ return acc
60
+ }, { skippedSuites: [], suitesToRun: [] })
55
61
  }
56
62
 
57
63
  module.exports = { getFormattedJestTestParameters, getJestTestName, getJestSuitesToRun }
@@ -135,7 +135,12 @@ class MochaPlugin extends CiPlugin {
135
135
  this._testNameToParams[name] = params
136
136
  })
137
137
 
138
- this.addSub('ci:mocha:session:finish', ({ status, isSuitesSkipped, testCodeCoverageLinesTotal }) => {
138
+ this.addSub('ci:mocha:session:finish', ({
139
+ status,
140
+ isSuitesSkipped,
141
+ testCodeCoverageLinesTotal,
142
+ numSkippedSuites
143
+ }) => {
139
144
  if (this.testSessionSpan) {
140
145
  const { isSuitesSkippingEnabled, isCodeCoverageEnabled } = this.itrConfig || {}
141
146
  this.testSessionSpan.setTag(TEST_STATUS, status)
@@ -144,7 +149,14 @@ class MochaPlugin extends CiPlugin {
144
149
  addIntelligentTestRunnerSpanTags(
145
150
  this.testSessionSpan,
146
151
  this.testModuleSpan,
147
- { isSuitesSkipped, isSuitesSkippingEnabled, isCodeCoverageEnabled, testCodeCoverageLinesTotal }
152
+ {
153
+ isSuitesSkipped,
154
+ isSuitesSkippingEnabled,
155
+ isCodeCoverageEnabled,
156
+ testCodeCoverageLinesTotal,
157
+ skippingCount: numSkippedSuites,
158
+ skippingType: 'suite'
159
+ }
148
160
  )
149
161
 
150
162
  this.testModuleSpan.finish()
@@ -4,6 +4,7 @@ const ServerPlugin = require('../../dd-trace/src/plugins/server')
4
4
  const { storage } = require('../../datadog-core')
5
5
  const analyticsSampler = require('../../dd-trace/src/analytics_sampler')
6
6
  const { COMPONENT } = require('../../dd-trace/src/constants')
7
+ const web = require('../../dd-trace/src/plugins/util/web')
7
8
 
8
9
  class NextPlugin extends ServerPlugin {
9
10
  static get id () {
@@ -16,7 +17,7 @@ class NextPlugin extends ServerPlugin {
16
17
  this.addSub('apm:next:page:load', message => this.pageLoad(message))
17
18
  }
18
19
 
19
- start ({ req, res }) {
20
+ bindStart ({ req, res }) {
20
21
  const store = storage.getStore()
21
22
  const childOf = store ? store.span : store
22
23
  const span = this.tracer.startSpan(this.operationName(), {
@@ -33,9 +34,13 @@ class NextPlugin extends ServerPlugin {
33
34
 
34
35
  analyticsSampler.sample(span, this.config.measured, true)
35
36
 
36
- this.enter(span, store)
37
-
38
37
  this._requests.set(span, req)
38
+
39
+ return { ...store, span }
40
+ }
41
+
42
+ error ({ span, error }) {
43
+ this.addError(error, span)
39
44
  }
40
45
 
41
46
  finish ({ req, res }) {
@@ -45,6 +50,7 @@ class NextPlugin extends ServerPlugin {
45
50
 
46
51
  const span = store.span
47
52
  const error = span.context()._tags['error']
53
+ const page = span.context()._tags['next.page']
48
54
 
49
55
  if (!this.config.validateStatus(res.statusCode) && !error) {
50
56
  span.setTag('error', true)
@@ -54,6 +60,8 @@ class NextPlugin extends ServerPlugin {
54
60
  'http.status_code': res.statusCode
55
61
  })
56
62
 
63
+ if (page) web.setRoute(req, page)
64
+
57
65
  this.config.hooks.request(span, req, res)
58
66
 
59
67
  span.finish()
@@ -8,7 +8,7 @@ const { getIastContext } = require('../iast-context')
8
8
  const { addVulnerability } = require('../vulnerability-reporter')
9
9
  const { getNodeModulesPaths } = require('../path-line')
10
10
 
11
- const EXCLUDED_PATHS = getNodeModulesPaths('mysql2', 'sequelize')
11
+ const EXCLUDED_PATHS = getNodeModulesPaths('mysql2', 'sequelize', 'pg-pool')
12
12
 
13
13
  class SqlInjectionAnalyzer extends InjectionAnalyzer {
14
14
  constructor () {
@@ -23,7 +23,7 @@ class SqlInjectionAnalyzer extends InjectionAnalyzer {
23
23
  this.addSub('datadog:sequelize:query:start', ({ sql, dialect }) => {
24
24
  const parentStore = storage.getStore()
25
25
  if (parentStore) {
26
- this.analyze(sql, dialect.toUpperCase())
26
+ this.analyze(sql, dialect.toUpperCase(), parentStore)
27
27
 
28
28
  storage.enterWith({ ...parentStore, sqlAnalyzed: true, sequelizeParentStore: parentStore })
29
29
  }
@@ -35,6 +35,22 @@ class SqlInjectionAnalyzer extends InjectionAnalyzer {
35
35
  storage.enterWith(store.sequelizeParentStore)
36
36
  }
37
37
  })
38
+
39
+ this.addSub('datadog:pg:pool:query:start', ({ query }) => {
40
+ const parentStore = storage.getStore()
41
+ if (parentStore) {
42
+ this.analyze(query.text, 'POSTGRES', parentStore)
43
+
44
+ storage.enterWith({ ...parentStore, sqlAnalyzed: true, pgPoolParentStore: parentStore })
45
+ }
46
+ })
47
+
48
+ this.addSub('datadog:pg:pool:query:finish', () => {
49
+ const store = storage.getStore()
50
+ if (store && store.pgPoolParentStore) {
51
+ storage.enterWith(store.pgPoolParentStore)
52
+ }
53
+ })
38
54
  }
39
55
 
40
56
  _getEvidence (value, iastContext, dialect) {
@@ -42,8 +58,7 @@ class SqlInjectionAnalyzer extends InjectionAnalyzer {
42
58
  return { value, ranges, dialect }
43
59
  }
44
60
 
45
- analyze (value, dialect) {
46
- const store = storage.getStore()
61
+ analyze (value, dialect, store = storage.getStore()) {
47
62
  if (!(store && store.sqlAnalyzed)) {
48
63
  const iastContext = getIastContext(store)
49
64
  if (this._isInvalidContext(store, iastContext)) return
@@ -66,7 +66,7 @@ function taintObject (iastContext, object, type, keyTainting, keyType) {
66
66
  const taintedProperty = TaintedUtils.newTaintedString(transactionId, key, property, keyType)
67
67
  parent[taintedProperty] = tainted
68
68
  } else {
69
- parent[property] = tainted
69
+ parent[key] = tainted
70
70
  }
71
71
  }
72
72
  } else if (typeof value === 'object' && !visited.has(value)) {
@@ -5,11 +5,20 @@ const telemetryLogs = require('./log')
5
5
  const { Verbosity, getVerbosity } = require('./verbosity')
6
6
  const { initRequestNamespace, finalizeRequestNamespace, globalNamespace } = require('./namespaces')
7
7
 
8
+ function isIastMetricsEnabled (metrics) {
9
+ // TODO: let DD_TELEMETRY_METRICS_ENABLED as undefined in config.js to avoid read here the env property
10
+ return process.env.DD_TELEMETRY_METRICS_ENABLED !== undefined ? metrics : true
11
+ }
12
+
8
13
  class Telemetry {
9
14
  configure (config, verbosity) {
10
- // in order to telemetry be enabled, tracer telemetry and metrics collection have to be enabled
11
- this.enabled = config && config.telemetry && config.telemetry.enabled && config.telemetry.metrics
12
- this.verbosity = this.enabled ? getVerbosity(verbosity) : Verbosity.OFF
15
+ const telemetryAndMetricsEnabled = config &&
16
+ config.telemetry &&
17
+ config.telemetry.enabled &&
18
+ isIastMetricsEnabled(config.telemetry.metrics)
19
+
20
+ this.verbosity = telemetryAndMetricsEnabled ? getVerbosity(verbosity) : Verbosity.OFF
21
+ this.enabled = this.verbosity !== Verbosity.OFF
13
22
 
14
23
  if (this.enabled) {
15
24
  telemetryMetrics.manager.set('iast', globalNamespace)
@@ -30,13 +39,13 @@ class Telemetry {
30
39
  }
31
40
 
32
41
  onRequestStart (context) {
33
- if (this.isEnabled() && this.verbosity !== Verbosity.OFF) {
42
+ if (this.isEnabled()) {
34
43
  initRequestNamespace(context)
35
44
  }
36
45
  }
37
46
 
38
47
  onRequestEnd (context, rootSpan) {
39
- if (this.isEnabled() && this.verbosity !== Verbosity.OFF) {
48
+ if (this.isEnabled()) {
40
49
  finalizeRequestNamespace(context, rootSpan)
41
50
  }
42
51
  }
@@ -14,6 +14,8 @@ const DEFAULT_IAST_REDACTION_NAME_PATTERN = '(?:p(?:ass)?w(?:or)?d|pass(?:_?phra
14
14
  // eslint-disable-next-line max-len
15
15
  const DEFAULT_IAST_REDACTION_VALUE_PATTERN = '(?:bearer\\s+[a-z0-9\\._\\-]+|glpat-[\\w\\-]{20}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=\\-]+\\.ey[I-L][\\w=\\-]+(?:\\.[\\w.+/=\\-]+)?|(?:[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY[\\-]{5}|ssh-rsa\\s*[a-z0-9/\\.+]{100,}))'
16
16
 
17
+ const REDACTED_SOURCE_BUFFER = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
18
+
17
19
  class SensitiveHandler {
18
20
  constructor () {
19
21
  this._namePattern = new RegExp(DEFAULT_IAST_REDACTION_NAME_PATTERN, 'gmi')
@@ -54,6 +56,7 @@ class SensitiveHandler {
54
56
  toRedactedJson (evidence, sensitive, sourcesIndexes, sources) {
55
57
  const valueParts = []
56
58
  const redactedSources = []
59
+ const redactedSourcesContext = []
57
60
 
58
61
  const { value, ranges } = evidence
59
62
 
@@ -71,21 +74,41 @@ class SensitiveHandler {
71
74
  sourceIndex = sourcesIndexes[nextTaintedIndex]
72
75
 
73
76
  while (nextSensitive != null && contains(nextTainted, nextSensitive)) {
74
- sourceIndex != null && redactedSources.push(sourceIndex)
77
+ const redactionStart = nextSensitive.start - nextTainted.start
78
+ const redactionEnd = nextSensitive.end - nextTainted.start
79
+ this.redactSource(sources, redactedSources, redactedSourcesContext, sourceIndex, redactionStart, redactionEnd)
75
80
  nextSensitive = sensitive.shift()
76
81
  }
77
82
 
78
83
  if (nextSensitive != null && intersects(nextSensitive, nextTainted)) {
79
- sourceIndex != null && redactedSources.push(sourceIndex)
84
+ const redactionStart = nextSensitive.start - nextTainted.start
85
+ const redactionEnd = nextSensitive.end - nextTainted.start
86
+ this.redactSource(sources, redactedSources, redactedSourcesContext, sourceIndex, redactionStart, redactionEnd)
80
87
 
81
88
  const entries = remove(nextSensitive, nextTainted)
82
89
  nextSensitive = entries.length > 0 ? entries[0] : null
83
90
  }
84
91
 
85
- this.isSensibleSource(sources[sourceIndex]) && redactedSources.push(sourceIndex)
92
+ if (this.isSensibleSource(sources[sourceIndex])) {
93
+ if (!sources[sourceIndex].redacted) {
94
+ redactedSources.push(sourceIndex)
95
+ sources[sourceIndex].pattern = ''.padEnd(sources[sourceIndex].value.length, REDACTED_SOURCE_BUFFER)
96
+ sources[sourceIndex].redacted = true
97
+ }
98
+ }
86
99
 
87
100
  if (redactedSources.indexOf(sourceIndex) > -1) {
88
- this.writeRedactedValuePart(valueParts, sourceIndex)
101
+ const partValue = value.substring(i, i + (nextTainted.end - nextTainted.start))
102
+ this.writeRedactedValuePart(
103
+ valueParts,
104
+ partValue.length,
105
+ sourceIndex,
106
+ partValue,
107
+ sources[sourceIndex],
108
+ redactedSourcesContext[sourceIndex],
109
+ this.isSensibleSource(sources[sourceIndex])
110
+ )
111
+ redactedSourcesContext[sourceIndex] = []
89
112
  } else {
90
113
  const substringEnd = Math.min(nextTainted.end, value.length)
91
114
  this.writeValuePart(valueParts, value.substring(nextTainted.start, substringEnd), sourceIndex)
@@ -100,7 +123,10 @@ class SensitiveHandler {
100
123
  this.writeValuePart(valueParts, value.substring(start, i), sourceIndex)
101
124
  if (nextTainted != null && intersects(nextSensitive, nextTainted)) {
102
125
  sourceIndex = sourcesIndexes[nextTaintedIndex]
103
- sourceIndex != null && redactedSources.push(sourceIndex)
126
+
127
+ const redactionStart = nextSensitive.start - nextTainted.start
128
+ const redactionEnd = nextSensitive.end - nextTainted.start
129
+ this.redactSource(sources, redactedSources, redactedSourcesContext, sourceIndex, redactionStart, redactionEnd)
104
130
 
105
131
  for (const entry of remove(nextSensitive, nextTainted)) {
106
132
  if (entry.start === i) {
@@ -111,9 +137,10 @@ class SensitiveHandler {
111
137
  }
112
138
  }
113
139
 
114
- this.writeRedactedValuePart(valueParts)
140
+ const _length = nextSensitive.end - nextSensitive.start
141
+ this.writeRedactedValuePart(valueParts, _length)
115
142
 
116
- start = i + (nextSensitive.end - nextSensitive.start)
143
+ start = i + _length
117
144
  i = start - 1
118
145
  nextSensitive = sensitive.shift()
119
146
  }
@@ -126,6 +153,24 @@ class SensitiveHandler {
126
153
  return { redactedValueParts: valueParts, redactedSources }
127
154
  }
128
155
 
156
+ redactSource (sources, redactedSources, redactedSourcesContext, sourceIndex, start, end) {
157
+ if (sourceIndex != null) {
158
+ if (!sources[sourceIndex].redacted) {
159
+ redactedSources.push(sourceIndex)
160
+ sources[sourceIndex].pattern = ''.padEnd(sources[sourceIndex].value.length, REDACTED_SOURCE_BUFFER)
161
+ sources[sourceIndex].redacted = true
162
+ }
163
+
164
+ if (!redactedSourcesContext[sourceIndex]) {
165
+ redactedSourcesContext[sourceIndex] = []
166
+ }
167
+ redactedSourcesContext[sourceIndex].push({
168
+ start,
169
+ end
170
+ })
171
+ }
172
+ }
173
+
129
174
  writeValuePart (valueParts, value, source) {
130
175
  if (value.length > 0) {
131
176
  if (source != null) {
@@ -136,9 +181,74 @@ class SensitiveHandler {
136
181
  }
137
182
  }
138
183
 
139
- writeRedactedValuePart (valueParts, source) {
140
- if (source != null) {
141
- valueParts.push({ redacted: true, source })
184
+ writeRedactedValuePart (
185
+ valueParts,
186
+ length,
187
+ sourceIndex,
188
+ partValue,
189
+ source,
190
+ sourceRedactionContext,
191
+ isSensibleSource
192
+ ) {
193
+ if (sourceIndex != null) {
194
+ const placeholder = source.value.includes(partValue)
195
+ ? source.pattern
196
+ : '*'.repeat(length)
197
+
198
+ if (isSensibleSource) {
199
+ valueParts.push({ redacted: true, source: sourceIndex, pattern: placeholder })
200
+ } else {
201
+ let _value = partValue
202
+ const dedupedSourceRedactionContexts = []
203
+
204
+ sourceRedactionContext.forEach(_sourceRedactionContext => {
205
+ const isPresentInDeduped = dedupedSourceRedactionContexts.some(_dedupedSourceRedactionContext =>
206
+ _dedupedSourceRedactionContext.start === _sourceRedactionContext.start &&
207
+ _dedupedSourceRedactionContext.end === _sourceRedactionContext.end
208
+ )
209
+
210
+ if (!isPresentInDeduped) {
211
+ dedupedSourceRedactionContexts.push(_sourceRedactionContext)
212
+ }
213
+ })
214
+
215
+ let offset = 0
216
+ dedupedSourceRedactionContexts.forEach((_sourceRedactionContext) => {
217
+ if (_sourceRedactionContext.start > 0) {
218
+ valueParts.push({
219
+ source: sourceIndex,
220
+ value: _value.substring(0, _sourceRedactionContext.start - offset)
221
+ })
222
+
223
+ _value = _value.substring(_sourceRedactionContext.start - offset)
224
+ offset = _sourceRedactionContext.start
225
+ }
226
+
227
+ const sensitive =
228
+ _value.substring(_sourceRedactionContext.start - offset, _sourceRedactionContext.end - offset)
229
+ const indexOfPartValueInPattern = source.value.indexOf(sensitive)
230
+
231
+ const pattern = indexOfPartValueInPattern > -1
232
+ ? placeholder.substring(indexOfPartValueInPattern, indexOfPartValueInPattern + sensitive.length)
233
+ : placeholder.substring(_sourceRedactionContext.start, _sourceRedactionContext.end)
234
+
235
+ valueParts.push({
236
+ redacted: true,
237
+ source: sourceIndex,
238
+ pattern
239
+ })
240
+
241
+ _value = _value.substring(pattern.length)
242
+ offset += pattern.length
243
+ })
244
+
245
+ if (_value.length) {
246
+ valueParts.push({
247
+ source: sourceIndex,
248
+ value: _value
249
+ })
250
+ }
251
+ }
142
252
  } else {
143
253
  valueParts.push({ redacted: true })
144
254
  }
@@ -28,7 +28,6 @@ class VulnerabilityFormatter {
28
28
  const { redactedValueParts, redactedSources } = scrubbingResult
29
29
  redactedSources.forEach(i => {
30
30
  delete sources[i].value
31
- sources[i].redacted = true
32
31
  })
33
32
  return { valueParts: redactedValueParts }
34
33
  }
@@ -35,14 +35,14 @@ class DogStatsDClient {
35
35
  this._udp6 = this._socket('udp6')
36
36
  }
37
37
 
38
- gauge (stat, value, tags) {
39
- this._add(stat, value, TYPE_GAUGE, tags)
40
- }
41
-
42
38
  increment (stat, value, tags) {
43
39
  this._add(stat, value, TYPE_COUNTER, tags)
44
40
  }
45
41
 
42
+ gauge (stat, value, tags) {
43
+ this._add(stat, value, TYPE_GAUGE, tags)
44
+ }
45
+
46
46
  distribution (stat, value, tags) {
47
47
  this._add(stat, value, TYPE_DISTRIBUTION, tags)
48
48
  }
@@ -153,7 +153,67 @@ class NoopDogStatsDClient {
153
153
  flush () { }
154
154
  }
155
155
 
156
+ // This is a simplified user-facing proxy to the underlying DogStatsDClient instance
157
+ class CustomMetrics {
158
+ constructor (options) {
159
+ this.dogstatsd = new DogStatsDClient(options)
160
+ }
161
+
162
+ increment (stat, value = 1, tags) {
163
+ return this.dogstatsd.increment(
164
+ stat,
165
+ value,
166
+ CustomMetrics.tagTranslator(tags)
167
+ )
168
+ }
169
+
170
+ decrement (stat, value = 1, tags) {
171
+ return this.dogstatsd.increment(
172
+ stat,
173
+ value * -1,
174
+ CustomMetrics.tagTranslator(tags)
175
+ )
176
+ }
177
+
178
+ gauge (stat, value, tags) {
179
+ return this.dogstatsd.gauge(
180
+ stat,
181
+ value,
182
+ CustomMetrics.tagTranslator(tags)
183
+ )
184
+ }
185
+
186
+ distribution (stat, value, tags) {
187
+ return this.dogstatsd.distribution(
188
+ stat,
189
+ value,
190
+ CustomMetrics.tagTranslator(tags)
191
+ )
192
+ }
193
+
194
+ flush () {
195
+ return this.dogstatsd.flush()
196
+ }
197
+
198
+ /**
199
+ * Exposing { tagName: 'tagValue' } to the end user
200
+ * These are translated into [ 'tagName:tagValue' ] for internal use
201
+ */
202
+ static tagTranslator (objTags) {
203
+ const arrTags = []
204
+
205
+ if (!objTags) return arrTags
206
+
207
+ for (const [key, value] of Object.entries(objTags)) {
208
+ arrTags.push(`${key}:${value}`)
209
+ }
210
+
211
+ return arrTags
212
+ }
213
+ }
214
+
156
215
  module.exports = {
157
216
  DogStatsDClient,
158
- NoopDogStatsDClient
217
+ NoopDogStatsDClient,
218
+ CustomMetrics
159
219
  }