dd-trace 4.16.0 → 4.18.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 (71) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/index.d.ts +9 -1
  3. package/package.json +5 -4
  4. package/packages/datadog-instrumentations/src/body-parser.js +2 -1
  5. package/packages/datadog-instrumentations/src/cucumber.js +29 -4
  6. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +45 -0
  7. package/packages/datadog-instrumentations/src/express.js +2 -1
  8. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  9. package/packages/datadog-instrumentations/src/jest.js +58 -20
  10. package/packages/datadog-instrumentations/src/knex.js +69 -1
  11. package/packages/datadog-instrumentations/src/mocha.js +34 -4
  12. package/packages/datadog-instrumentations/src/mongodb.js +63 -0
  13. package/packages/datadog-instrumentations/src/mongoose.js +140 -1
  14. package/packages/datadog-instrumentations/src/next.js +98 -23
  15. package/packages/datadog-instrumentations/src/playwright.js +22 -8
  16. package/packages/datadog-plugin-cucumber/src/index.js +17 -5
  17. package/packages/datadog-plugin-cypress/src/plugin.js +38 -8
  18. package/packages/datadog-plugin-http/src/client.js +2 -0
  19. package/packages/datadog-plugin-jest/src/index.js +29 -6
  20. package/packages/datadog-plugin-jest/src/util.js +45 -2
  21. package/packages/datadog-plugin-memcached/src/index.js +10 -5
  22. package/packages/datadog-plugin-mocha/src/index.js +25 -6
  23. package/packages/datadog-plugin-next/src/index.js +4 -3
  24. package/packages/datadog-plugin-playwright/src/index.js +4 -1
  25. package/packages/dd-trace/src/appsec/channels.js +3 -1
  26. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +2 -0
  27. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-analyzer.js +60 -0
  28. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +269 -0
  29. package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +5 -2
  30. package/packages/dd-trace/src/appsec/iast/analyzers/missing-header-analyzer.js +22 -4
  31. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +173 -0
  32. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +21 -1
  33. package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +3 -3
  34. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +1 -2
  35. package/packages/dd-trace/src/appsec/iast/analyzers/xcontenttype-header-missing-analyzer.js +2 -2
  36. package/packages/dd-trace/src/appsec/iast/iast-log.js +9 -4
  37. package/packages/dd-trace/src/appsec/iast/iast-plugin.js +4 -0
  38. package/packages/dd-trace/src/appsec/iast/path-line.js +6 -1
  39. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations.js +25 -12
  40. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +4 -4
  41. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +13 -2
  42. package/packages/dd-trace/src/appsec/iast/taint-tracking/secure-marks-generator.js +13 -0
  43. package/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js +2 -1
  44. package/packages/dd-trace/src/appsec/iast/telemetry/index.js +1 -14
  45. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/json-sensitive-analyzer.js +16 -0
  46. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +22 -4
  47. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +9 -0
  48. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +15 -2
  49. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +169 -0
  50. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +2 -0
  51. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +5 -1
  52. package/packages/dd-trace/src/appsec/index.js +31 -13
  53. package/packages/dd-trace/src/appsec/remote_config/manager.js +11 -3
  54. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +14 -1
  55. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-itr-configuration.js +4 -2
  56. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -0
  57. package/packages/dd-trace/src/config.js +37 -13
  58. package/packages/dd-trace/src/format.js +3 -0
  59. package/packages/dd-trace/src/git_properties.js +16 -15
  60. package/packages/dd-trace/src/plugin_manager.js +3 -1
  61. package/packages/dd-trace/src/plugins/util/ci.js +17 -0
  62. package/packages/dd-trace/src/plugins/util/git.js +26 -4
  63. package/packages/dd-trace/src/plugins/util/test.js +45 -2
  64. package/packages/dd-trace/src/profiling/config.js +20 -3
  65. package/packages/dd-trace/src/profiling/profilers/wall.js +51 -40
  66. package/packages/dd-trace/src/service-naming/extra-services.js +24 -0
  67. package/packages/dd-trace/src/telemetry/index.js +4 -0
  68. package/packages/dd-trace/src/telemetry/logs/index.js +65 -0
  69. package/packages/dd-trace/src/{appsec/iast/telemetry/log → telemetry/logs}/log-collector.js +9 -22
  70. package/packages/dd-trace/src/telemetry/metrics.js +0 -5
  71. package/packages/dd-trace/src/appsec/iast/telemetry/log/index.js +0 -87
@@ -1,4 +1,8 @@
1
+ const { readFileSync } = require('fs')
2
+ const { parse, extract } = require('jest-docblock')
3
+
1
4
  const { getTestSuitePath } = require('../../dd-trace/src/plugins/util/test')
5
+ const log = require('../../dd-trace/src/log')
2
6
 
3
7
  /**
4
8
  * There are two ways to call `test.each` in `jest`:
@@ -47,17 +51,56 @@ function getJestTestName (test) {
47
51
  return titles.join(' ')
48
52
  }
49
53
 
54
+ function isMarkedAsUnskippable (test) {
55
+ let docblocks
56
+
57
+ try {
58
+ const testSource = readFileSync(test.path, 'utf8')
59
+ docblocks = parse(extract(testSource))
60
+ } catch (e) {
61
+ // If we have issues parsing the file, we'll assume no unskippable was passed
62
+ return false
63
+ }
64
+
65
+ // docblocks were correctly parsed but it does not include a @datadog block
66
+ if (!docblocks?.datadog) {
67
+ return false
68
+ }
69
+
70
+ try {
71
+ return JSON.parse(docblocks.datadog).unskippable
72
+ } catch (e) {
73
+ // If the @datadog block comment is present but malformed, we'll run the suite
74
+ log.warn('@datadog block comment is malformed.')
75
+ return true
76
+ }
77
+ }
78
+
50
79
  function getJestSuitesToRun (skippableSuites, originalTests, rootDir) {
51
80
  return originalTests.reduce((acc, test) => {
52
81
  const relativePath = getTestSuitePath(test.path, rootDir)
53
82
  const shouldBeSkipped = skippableSuites.includes(relativePath)
83
+
84
+ if (isMarkedAsUnskippable(test)) {
85
+ acc.suitesToRun.push(test)
86
+ if (test?.context?.config?.testEnvironmentOptions) {
87
+ test.context.config.testEnvironmentOptions['_ddUnskippable'] = true
88
+ acc.hasUnskippableSuites = true
89
+ if (shouldBeSkipped) {
90
+ test.context.config.testEnvironmentOptions['_ddForcedToRun'] = true
91
+ acc.hasForcedToRunSuites = true
92
+ }
93
+ }
94
+ return acc
95
+ }
96
+
54
97
  if (shouldBeSkipped) {
55
98
  acc.skippedSuites.push(relativePath)
56
99
  } else {
57
100
  acc.suitesToRun.push(test)
58
101
  }
59
102
  return acc
60
- }, { skippedSuites: [], suitesToRun: [] })
103
+ }, { skippedSuites: [], suitesToRun: [], hasUnskippableSuites: false, hasForcedToRunSuites: false })
61
104
  }
62
105
 
63
- module.exports = { getFormattedJestTestParameters, getJestTestName, getJestSuitesToRun }
106
+ module.exports = { getFormattedJestTestParameters, getJestTestName, getJestSuitesToRun, isMarkedAsUnskippable }
@@ -9,15 +9,20 @@ class MemcachedPlugin extends CachePlugin {
9
9
  start ({ client, server, query }) {
10
10
  const address = getAddress(client, server, query)
11
11
 
12
+ const meta = {
13
+ 'out.host': address[0],
14
+ [CLIENT_PORT_KEY]: address[1]
15
+ }
16
+
17
+ if (this.config.memcachedCommandEnabled) {
18
+ meta['memcached.command'] = query.command
19
+ }
20
+
12
21
  this.startSpan({
13
22
  service: this.serviceName({ pluginConfig: this.config, system: this.system }),
14
23
  resource: query.type,
15
24
  type: 'memcached',
16
- meta: {
17
- 'memcached.command': query.command,
18
- 'out.host': address[0],
19
- [CLIENT_PORT_KEY]: address[1]
20
- }
25
+ meta
21
26
  })
22
27
  }
23
28
  }
@@ -11,7 +11,9 @@ const {
11
11
  getTestParametersString,
12
12
  getTestSuiteCommonTags,
13
13
  addIntelligentTestRunnerSpanTags,
14
- TEST_SOURCE_START
14
+ TEST_SOURCE_START,
15
+ TEST_ITR_UNSKIPPABLE,
16
+ TEST_ITR_FORCED_RUN
15
17
  } = require('../../dd-trace/src/plugins/util/test')
16
18
  const { COMPONENT } = require('../../dd-trace/src/constants')
17
19
 
@@ -47,14 +49,21 @@ class MochaPlugin extends CiPlugin {
47
49
  this.tracer._exporter.exportCoverage(formattedCoverage)
48
50
  })
49
51
 
50
- this.addSub('ci:mocha:test-suite:start', (suite) => {
52
+ this.addSub('ci:mocha:test-suite:start', ({ testSuite, isUnskippable, isForcedToRun }) => {
51
53
  const store = storage.getStore()
52
54
  const testSuiteMetadata = getTestSuiteCommonTags(
53
55
  this.command,
54
56
  this.frameworkVersion,
55
- getTestSuitePath(suite.file, this.sourceRoot),
57
+ getTestSuitePath(testSuite, this.sourceRoot),
56
58
  'mocha'
57
59
  )
60
+ if (isUnskippable) {
61
+ testSuiteMetadata[TEST_ITR_UNSKIPPABLE] = 'true'
62
+ }
63
+ if (isForcedToRun) {
64
+ testSuiteMetadata[TEST_ITR_FORCED_RUN] = 'true'
65
+ }
66
+
58
67
  const testSuiteSpan = this.tracer.startSpan('mocha.test_suite', {
59
68
  childOf: this.testModuleSpan,
60
69
  tags: {
@@ -64,7 +73,7 @@ class MochaPlugin extends CiPlugin {
64
73
  }
65
74
  })
66
75
  this.enter(testSuiteSpan, store)
67
- this._testSuites.set(suite.file, testSuiteSpan)
76
+ this._testSuites.set(testSuite, testSuiteSpan)
68
77
  })
69
78
 
70
79
  this.addSub('ci:mocha:test-suite:finish', (status) => {
@@ -139,13 +148,21 @@ class MochaPlugin extends CiPlugin {
139
148
  status,
140
149
  isSuitesSkipped,
141
150
  testCodeCoverageLinesTotal,
142
- numSkippedSuites
151
+ numSkippedSuites,
152
+ hasForcedToRunSuites,
153
+ hasUnskippableSuites,
154
+ error
143
155
  }) => {
144
156
  if (this.testSessionSpan) {
145
157
  const { isSuitesSkippingEnabled, isCodeCoverageEnabled } = this.itrConfig || {}
146
158
  this.testSessionSpan.setTag(TEST_STATUS, status)
147
159
  this.testModuleSpan.setTag(TEST_STATUS, status)
148
160
 
161
+ if (error) {
162
+ this.testSessionSpan.setTag('error', error)
163
+ this.testModuleSpan.setTag('error', error)
164
+ }
165
+
149
166
  addIntelligentTestRunnerSpanTags(
150
167
  this.testSessionSpan,
151
168
  this.testModuleSpan,
@@ -155,7 +172,9 @@ class MochaPlugin extends CiPlugin {
155
172
  isCodeCoverageEnabled,
156
173
  testCodeCoverageLinesTotal,
157
174
  skippingCount: numSkippedSuites,
158
- skippingType: 'suite'
175
+ skippingType: 'suite',
176
+ hasForcedToRunSuites,
177
+ hasUnskippableSuites
159
178
  }
160
179
  )
161
180
 
@@ -43,7 +43,7 @@ class NextPlugin extends ServerPlugin {
43
43
  this.addError(error, span)
44
44
  }
45
45
 
46
- finish ({ req, res }) {
46
+ finish ({ req, res, nextRequest = {} }) {
47
47
  const store = storage.getStore()
48
48
 
49
49
  if (!store) return
@@ -52,7 +52,8 @@ class NextPlugin extends ServerPlugin {
52
52
  const error = span.context()._tags['error']
53
53
 
54
54
  if (!this.config.validateStatus(res.statusCode) && !error) {
55
- span.setTag('error', true)
55
+ span.setTag('error', req.error || nextRequest.error || true)
56
+ web.addError(req, req.error || nextRequest.error || true)
56
57
  }
57
58
 
58
59
  span.addTags({
@@ -64,7 +65,7 @@ class NextPlugin extends ServerPlugin {
64
65
  span.finish()
65
66
  }
66
67
 
67
- pageLoad ({ page, isAppPath }) {
68
+ pageLoad ({ page, isAppPath = false }) {
68
69
  const store = storage.getStore()
69
70
 
70
71
  if (!store) return
@@ -72,7 +72,7 @@ class PlaywrightPlugin extends CiPlugin {
72
72
 
73
73
  this.enter(span, store)
74
74
  })
75
- this.addSub('ci:playwright:test:finish', ({ testStatus, steps, error }) => {
75
+ this.addSub('ci:playwright:test:finish', ({ testStatus, steps, error, extraTags }) => {
76
76
  const store = storage.getStore()
77
77
  const span = store && store.span
78
78
  if (!span) return
@@ -82,6 +82,9 @@ class PlaywrightPlugin extends CiPlugin {
82
82
  if (error) {
83
83
  span.setTag('error', error)
84
84
  }
85
+ if (extraTags) {
86
+ span.addTags(extraTags)
87
+ }
85
88
 
86
89
  steps.forEach(step => {
87
90
  const stepStartTime = step.startTime.getTime()
@@ -11,5 +11,7 @@ module.exports = {
11
11
  incomingHttpRequestEnd: dc.channel('dd-trace:incomingHttpRequestEnd'),
12
12
  passportVerify: dc.channel('datadog:passport:verify:finish'),
13
13
  queryParser: dc.channel('datadog:query:read:finish'),
14
- setCookieChannel: dc.channel('datadog:iast:set-cookie')
14
+ setCookieChannel: dc.channel('datadog:iast:set-cookie'),
15
+ nextBodyParsed: dc.channel('apm:next:body-parsed'),
16
+ nextQueryParsed: dc.channel('apm:next:query-parsed')
15
17
  }
@@ -2,11 +2,13 @@
2
2
 
3
3
  module.exports = {
4
4
  'COMMAND_INJECTION_ANALYZER': require('./command-injection-analyzer'),
5
+ 'HARCODED_SECRET_ANALYZER': require('./hardcoded-secret-analyzer'),
5
6
  'HSTS_HEADER_MISSING_ANALYZER': require('./hsts-header-missing-analyzer'),
6
7
  'INSECURE_COOKIE_ANALYZER': require('./insecure-cookie-analyzer'),
7
8
  'LDAP_ANALYZER': require('./ldap-injection-analyzer'),
8
9
  'NO_HTTPONLY_COOKIE_ANALYZER': require('./no-httponly-cookie-analyzer'),
9
10
  'NO_SAMESITE_COOKIE_ANALYZER': require('./no-samesite-cookie-analyzer'),
11
+ 'NOSQL_MONGODB_INJECTION': require('./nosql-injection-mongodb-analyzer'),
10
12
  'PATH_TRAVERSAL_ANALYZER': require('./path-traversal-analyzer'),
11
13
  'SQL_INJECTION_ANALYZER': require('./sql-injection-analyzer'),
12
14
  'SSRF': require('./ssrf-analyzer'),
@@ -0,0 +1,60 @@
1
+ 'use strict'
2
+
3
+ const Analyzer = require('./vulnerability-analyzer')
4
+ const { HARDCODED_SECRET } = require('../vulnerabilities')
5
+ const { getRelativePath } = require('../path-line')
6
+
7
+ const secretRules = require('./hardcoded-secrets-rules')
8
+
9
+ class HardcodedSecretAnalyzer extends Analyzer {
10
+ constructor () {
11
+ super(HARDCODED_SECRET)
12
+ }
13
+
14
+ onConfigure () {
15
+ this.addSub('datadog:secrets:result', (secrets) => { this.analyze(secrets) })
16
+ }
17
+
18
+ analyze (secrets) {
19
+ if (!secrets?.file || !secrets.literals) return
20
+
21
+ const matches = secrets.literals
22
+ .filter(literal => literal.value && literal.locations?.length)
23
+ .map(literal => {
24
+ const match = secretRules.find(rule => literal.value.match(rule.regex))
25
+
26
+ return match ? { locations: literal.locations, ruleId: match.id } : undefined
27
+ })
28
+ .filter(match => !!match)
29
+
30
+ if (matches.length) {
31
+ const file = getRelativePath(secrets.file)
32
+
33
+ matches.forEach(match => {
34
+ match.locations
35
+ .filter(location => location.line)
36
+ .forEach(location => this._report({
37
+ file,
38
+ line: location.line,
39
+ column: location.column,
40
+ data: match.ruleId
41
+ }))
42
+ })
43
+ }
44
+ }
45
+
46
+ _getEvidence (value) {
47
+ return { value: `${value.data}` }
48
+ }
49
+
50
+ _getLocation (value) {
51
+ return {
52
+ path: value.file,
53
+ line: value.line,
54
+ column: value.column,
55
+ isInternal: false
56
+ }
57
+ }
58
+ }
59
+
60
+ module.exports = new HardcodedSecretAnalyzer()
@@ -0,0 +1,269 @@
1
+ /* eslint-disable max-len */
2
+ 'use strict'
3
+
4
+ module.exports = [
5
+ {
6
+ 'id': 'adobe-client-secret',
7
+ 'regex': /\b((p8e-)[a-z0-9]{32})(?:['"\s\x60;]|$)/i
8
+ },
9
+ {
10
+ 'id': 'age-secret-key',
11
+ 'regex': /AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{58}/
12
+ },
13
+ {
14
+ 'id': 'alibaba-access-key-id',
15
+ 'regex': /\b((LTAI)[a-z0-9]{20})(?:['"\s\x60;]|$)/i
16
+ },
17
+ {
18
+ 'id': 'authress-service-client-access-key',
19
+ 'regex': /\b((?:sc|ext|scauth|authress)_[a-z0-9]{5,30}\.[a-z0-9]{4,6}\.acc[_-][a-z0-9-]{10,32}\.[a-z0-9+/_=-]{30,120})(?:['"\s\x60;]|$)/i
20
+ },
21
+ {
22
+ 'id': 'aws-access-token',
23
+ 'regex': /\b((A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16})(?:['"\s\x60;]|$)/
24
+ },
25
+ {
26
+ 'id': 'clojars-api-token',
27
+ 'regex': /(CLOJARS_)[a-z0-9]{60}/i
28
+ },
29
+ {
30
+ 'id': 'databricks-api-token',
31
+ 'regex': /\b(dapi[a-h0-9]{32})(?:['"\s\x60;]|$)/i
32
+ },
33
+ {
34
+ 'id': 'digitalocean-access-token',
35
+ 'regex': /\b(doo_v1_[a-f0-9]{64})(?:['"\s\x60;]|$)/i
36
+ },
37
+ {
38
+ 'id': 'digitalocean-pat',
39
+ 'regex': /\b(dop_v1_[a-f0-9]{64})(?:['"\s\x60;]|$)/i
40
+ },
41
+ {
42
+ 'id': 'digitalocean-refresh-token',
43
+ 'regex': /\b(dor_v1_[a-f0-9]{64})(?:['"\s\x60;]|$)/i
44
+ },
45
+ {
46
+ 'id': 'doppler-api-token',
47
+ 'regex': /(dp\.pt\.)[a-z0-9]{43}/i
48
+ },
49
+ {
50
+ 'id': 'duffel-api-token',
51
+ 'regex': /duffel_(test|live)_[a-z0-9_\-=]{43}/i
52
+ },
53
+ {
54
+ 'id': 'dynatrace-api-token',
55
+ 'regex': /dt0c01\.[a-z0-9]{24}\.[a-z0-9]{64}/i
56
+ },
57
+ {
58
+ 'id': 'easypost-api-token',
59
+ 'regex': /\bEZAK[a-z0-9]{54}/i
60
+ },
61
+ {
62
+ 'id': 'flutterwave-public-key',
63
+ 'regex': /FLWPUBK_TEST-[a-h0-9]{32}-X/i
64
+ },
65
+ {
66
+ 'id': 'frameio-api-token',
67
+ 'regex': /fio-u-[a-z0-9\-_=]{64}/i
68
+ },
69
+ {
70
+ 'id': 'gcp-api-key',
71
+ 'regex': /\b(AIza[0-9a-z\-_]{35})(?:['"\s\x60;]|$)/i
72
+ },
73
+ {
74
+ 'id': 'github-app-token',
75
+ 'regex': /(ghu|ghs)_[0-9a-zA-Z]{36}/
76
+ },
77
+ {
78
+ 'id': 'github-fine-grained-pat',
79
+ 'regex': /github_pat_[0-9a-zA-Z_]{82}/
80
+ },
81
+ {
82
+ 'id': 'github-oauth',
83
+ 'regex': /gho_[0-9a-zA-Z]{36}/
84
+ },
85
+ {
86
+ 'id': 'github-pat',
87
+ 'regex': /ghp_[0-9a-zA-Z]{36}/
88
+ },
89
+ {
90
+ 'id': 'gitlab-pat',
91
+ 'regex': /glpat-[0-9a-zA-Z\-_]{20}/
92
+ },
93
+ {
94
+ 'id': 'gitlab-ptt',
95
+ 'regex': /glptt-[0-9a-f]{40}/
96
+ },
97
+ {
98
+ 'id': 'gitlab-rrt',
99
+ 'regex': /GR1348941[0-9a-zA-Z\-_]{20}/
100
+ },
101
+ {
102
+ 'id': 'grafana-api-key',
103
+ 'regex': /\b(eyJrIjoi[a-z0-9]{70,400}={0,2})(?:['"\s\x60;]|$)/i
104
+ },
105
+ {
106
+ 'id': 'grafana-cloud-api-token',
107
+ 'regex': /\b(glc_[a-z0-9+/]{32,400}={0,2})(?:['"\s\x60;]|$)/i
108
+ },
109
+ {
110
+ 'id': 'grafana-service-account-token',
111
+ 'regex': /\b(glsa_[a-z0-9]{32}_[a-f0-9]{8})(?:['"\s\x60;]|$)/i
112
+ },
113
+ {
114
+ 'id': 'hashicorp-tf-api-token',
115
+ 'regex': /[a-z0-9]{14}\.atlasv1\.[a-z0-9\-_=]{60,70}/i
116
+ },
117
+ {
118
+ 'id': 'jwt',
119
+ 'regex': /\b(ey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9/_-]{17,}\.(?:[a-zA-Z0-9/_-]{10,}={0,2})?)(?:['"\s\x60;]|$)/
120
+ },
121
+ {
122
+ 'id': 'linear-api-key',
123
+ 'regex': /lin_api_[a-z0-9]{40}/i
124
+ },
125
+ {
126
+ 'id': 'npm-access-token',
127
+ 'regex': /\b(npm_[a-z0-9]{36})(?:['"\s\x60;]|$)/i
128
+ },
129
+ {
130
+ 'id': 'openai-api-key',
131
+ 'regex': /\b(sk-[a-z0-9]{20}T3BlbkFJ[a-z0-9]{20})(?:['"\s\x60;]|$)/i
132
+ },
133
+ {
134
+ 'id': 'planetscale-api-token',
135
+ 'regex': /\b(pscale_tkn_[a-z0-9=\-_.]{32,64})(?:['"\s\x60;]|$)/i
136
+ },
137
+ {
138
+ 'id': 'planetscale-oauth-token',
139
+ 'regex': /\b(pscale_oauth_[a-z0-9=\-_.]{32,64})(?:['"\s\x60;]|$)/i
140
+ },
141
+ {
142
+ 'id': 'planetscale-password',
143
+ 'regex': /\b(pscale_pw_[a-z0-9=\-_.]{32,64})(?:['"\s\x60;]|$)/i
144
+ },
145
+ {
146
+ 'id': 'postman-api-token',
147
+ 'regex': /\b(PMAK-[a-f0-9]{24}-[a-f0-9]{34})(?:['"\s\x60;]|$)/i
148
+ },
149
+ {
150
+ 'id': 'prefect-api-token',
151
+ 'regex': /\b(pnu_[a-z0-9]{36})(?:['"\s\x60;]|$)/i
152
+ },
153
+ {
154
+ 'id': 'private-key',
155
+ 'regex': /-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY( BLOCK)?-----[\s\S]*KEY( BLOCK)?----/i
156
+ },
157
+ {
158
+ 'id': 'pulumi-api-token',
159
+ 'regex': /\b(pul-[a-f0-9]{40})(?:['"\s\x60;]|$)/i
160
+ },
161
+ {
162
+ 'id': 'pypi-upload-token',
163
+ 'regex': /pypi-AgEIcHlwaS5vcmc[A-Za-z0-9\-_]{50,1000}/
164
+ },
165
+ {
166
+ 'id': 'readme-api-token',
167
+ 'regex': /\b(rdme_[a-z0-9]{70})(?:['"\s\x60;]|$)/i
168
+ },
169
+ {
170
+ 'id': 'rubygems-api-token',
171
+ 'regex': /\b(rubygems_[a-f0-9]{48})(?:['"\s\x60;]|$)/i
172
+ },
173
+ {
174
+ 'id': 'scalingo-api-token',
175
+ 'regex': /tk-us-[a-zA-Z0-9-_]{48}/
176
+ },
177
+ {
178
+ 'id': 'sendgrid-api-token',
179
+ 'regex': /\b(SG\.[a-z0-9=_\-.]{66})(?:['"\s\x60;]|$)/i
180
+ },
181
+ {
182
+ 'id': 'sendinblue-api-token',
183
+ 'regex': /\b(xkeysib-[a-f0-9]{64}-[a-z0-9]{16})(?:['"\s\x60;]|$)/i
184
+ },
185
+ {
186
+ 'id': 'shippo-api-token',
187
+ 'regex': /\b(shippo_(live|test)_[a-f0-9]{40})(?:['"\s\x60;]|$)/i
188
+ },
189
+ {
190
+ 'id': 'shopify-access-token',
191
+ 'regex': /shpat_[a-fA-F0-9]{32}/
192
+ },
193
+ {
194
+ 'id': 'shopify-custom-access-token',
195
+ 'regex': /shpca_[a-fA-F0-9]{32}/
196
+ },
197
+ {
198
+ 'id': 'shopify-private-app-access-token',
199
+ 'regex': /shppa_[a-fA-F0-9]{32}/
200
+ },
201
+ {
202
+ 'id': 'shopify-shared-secret',
203
+ 'regex': /shpss_[a-fA-F0-9]{32}/
204
+ },
205
+ {
206
+ 'id': 'slack-app-token',
207
+ 'regex': /(xapp-\d-[A-Z0-9]+-\d+-[a-z0-9]+)/i
208
+ },
209
+ {
210
+ 'id': 'slack-bot-token',
211
+ 'regex': /(xoxb-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*)/
212
+ },
213
+ {
214
+ 'id': 'slack-config-access-token',
215
+ 'regex': /(xoxe.xox[bp]-\d-[A-Z0-9]{163,166})/i
216
+ },
217
+ {
218
+ 'id': 'slack-config-refresh-token',
219
+ 'regex': /(xoxe-\d-[A-Z0-9]{146})/i
220
+ },
221
+ {
222
+ 'id': 'slack-legacy-bot-token',
223
+ 'regex': /(xoxb-[0-9]{8,14}-[a-zA-Z0-9]{18,26})/
224
+ },
225
+ {
226
+ 'id': 'slack-legacy-token',
227
+ 'regex': /(xox[os]-\d+-\d+-\d+-[a-fA-F\d]+)/
228
+ },
229
+ {
230
+ 'id': 'slack-legacy-workspace-token',
231
+ 'regex': /(xox[ar]-(?:\d-)?[0-9a-zA-Z]{8,48})/
232
+ },
233
+ {
234
+ 'id': 'slack-user-token',
235
+ 'regex': /(xox[pe](?:-[0-9]{10,13}){3}-[a-zA-Z0-9-]{28,34})/
236
+ },
237
+ {
238
+ 'id': 'slack-webhook-url',
239
+ 'regex': /(https?:\/\/)?hooks.slack.com\/(services|workflows)\/[A-Za-z0-9+/]{43,46}/
240
+ },
241
+ {
242
+ 'id': 'square-access-token',
243
+ 'regex': /\b(sq0atp-[0-9a-z\-_]{22})(?:['"\s\x60;]|$)/i
244
+ },
245
+ {
246
+ 'id': 'square-secret',
247
+ 'regex': /\b(sq0csp-[0-9a-z\-_]{43})(?:['"\s\x60;]|$)/i
248
+ },
249
+ {
250
+ 'id': 'stripe-access-token',
251
+ 'regex': /(sk|pk)_(test|live)_[0-9a-z]{10,32}/i
252
+ },
253
+ {
254
+ 'id': 'telegram-bot-api-token',
255
+ 'regex': /(?:^|[^0-9])([0-9]{5,16}:A[a-z0-9_-]{34})(?:$|[^a-z0-9_-])/i
256
+ },
257
+ {
258
+ 'id': 'twilio-api-key',
259
+ 'regex': /SK[0-9a-fA-F]{32}/
260
+ },
261
+ {
262
+ 'id': 'vault-batch-token',
263
+ 'regex': /\b(hvb\.[a-z0-9_-]{138,212})(?:['"\s\x60;]|$)/i
264
+ },
265
+ {
266
+ 'id': 'vault-service-token',
267
+ 'regex': /\b(hvs\.[a-z0-9_-]{90,100})(?:['"\s\x60;]|$)/i
268
+ }
269
+ ]
@@ -10,8 +10,11 @@ class HstsHeaderMissingAnalyzer extends MissingHeaderAnalyzer {
10
10
  super(HSTS_HEADER_MISSING, HSTS_HEADER_NAME)
11
11
  }
12
12
  _isVulnerableFromRequestAndResponse (req, res) {
13
- const headerToCheck = res.getHeader(HSTS_HEADER_NAME)
14
- return !this._isHeaderValid(headerToCheck) && this._isHttpsProtocol(req)
13
+ const headerValues = this._getHeaderValues(res, HSTS_HEADER_NAME)
14
+ return this._isHttpsProtocol(req) && (
15
+ headerValues.length === 0 ||
16
+ headerValues.some(headerValue => !this._isHeaderValid(headerValue))
17
+ )
15
18
  }
16
19
 
17
20
  _isHeaderValid (headerValue) {
@@ -28,6 +28,15 @@ class MissingHeaderAnalyzer extends Analyzer {
28
28
  }, (data) => this.analyze(data))
29
29
  }
30
30
 
31
+ _getHeaderValues (res, headerName) {
32
+ const headerValue = res.getHeader(headerName)
33
+ if (Array.isArray(headerValue)) {
34
+ return headerValue
35
+ } else {
36
+ return headerValue ? [headerValue.toString()] : []
37
+ }
38
+ }
39
+
31
40
  _getLocation () {
32
41
  return undefined
33
42
  }
@@ -41,7 +50,14 @@ class MissingHeaderAnalyzer extends Analyzer {
41
50
  }
42
51
 
43
52
  _getEvidence ({ res }) {
44
- return { value: res.getHeader(this.headerName) }
53
+ const headerValues = this._getHeaderValues(res, this.headerName)
54
+ let value
55
+ if (headerValues.length === 1) {
56
+ value = headerValues[0]
57
+ } else if (headerValues.length > 0) {
58
+ value = JSON.stringify(headerValues)
59
+ }
60
+ return { value }
45
61
  }
46
62
 
47
63
  _isVulnerable ({ req, res }, context) {
@@ -56,9 +72,11 @@ class MissingHeaderAnalyzer extends Analyzer {
56
72
  }
57
73
 
58
74
  _isResponseHtml (res) {
59
- const contentType = res.getHeader('content-type')
60
- return contentType && HTML_CONTENT_TYPES.some(htmlContentType => {
61
- return htmlContentType === contentType || contentType.startsWith(htmlContentType + ';')
75
+ const contentTypes = this._getHeaderValues(res, 'content-type')
76
+ return contentTypes.some(contentType => {
77
+ return contentType && HTML_CONTENT_TYPES.some(htmlContentType => {
78
+ return htmlContentType === contentType || contentType.startsWith(htmlContentType + ';')
79
+ })
62
80
  })
63
81
  }
64
82
  }