dd-trace 4.11.1 → 4.16.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/LICENSE-3rdparty.csv +1 -0
- package/README.md +4 -9
- package/ext/tags.d.ts +1 -0
- package/ext/tags.js +1 -0
- package/index.d.ts +44 -0
- package/package.json +9 -6
- package/packages/datadog-esbuild/index.js +57 -32
- package/packages/datadog-instrumentations/src/body-parser.js +2 -2
- package/packages/datadog-instrumentations/src/cookie-parser.js +37 -0
- package/packages/datadog-instrumentations/src/cucumber.js +30 -11
- package/packages/datadog-instrumentations/src/express.js +1 -1
- package/packages/datadog-instrumentations/src/graphql.js +10 -4
- package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
- package/packages/datadog-instrumentations/src/http/server.js +1 -1
- package/packages/datadog-instrumentations/src/jest.js +22 -11
- package/packages/datadog-instrumentations/src/kafkajs.js +3 -4
- package/packages/datadog-instrumentations/src/mocha.js +33 -8
- package/packages/datadog-instrumentations/src/mysql.js +39 -1
- package/packages/datadog-instrumentations/src/next.js +47 -19
- package/packages/datadog-instrumentations/src/openai.js +1 -1
- package/packages/datadog-instrumentations/src/pg.js +60 -15
- package/packages/datadog-instrumentations/src/playwright.js +15 -3
- package/packages/datadog-plugin-cucumber/src/index.js +14 -2
- package/packages/datadog-plugin-cypress/src/plugin.js +49 -13
- package/packages/datadog-plugin-graphql/src/index.js +3 -3
- package/packages/datadog-plugin-graphql/src/resolve.js +27 -2
- package/packages/datadog-plugin-jest/src/index.js +10 -2
- package/packages/datadog-plugin-jest/src/util.js +10 -4
- package/packages/datadog-plugin-mocha/src/index.js +14 -2
- package/packages/datadog-plugin-mongodb-core/src/index.js +6 -2
- package/packages/datadog-plugin-mysql/src/index.js +2 -2
- package/packages/datadog-plugin-next/src/index.js +22 -5
- package/packages/datadog-plugin-pg/src/index.js +2 -2
- package/packages/dd-trace/src/appsec/addresses.js +1 -0
- package/packages/dd-trace/src/appsec/channels.js +2 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/ldap-injection-analyzer.js +7 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +29 -18
- package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +19 -1
- package/packages/dd-trace/src/appsec/iast/path-line.js +1 -0
- package/packages/dd-trace/src/appsec/iast/taint-tracking/operations.js +1 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +48 -5
- package/packages/dd-trace/src/appsec/iast/telemetry/index.js +14 -5
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +131 -10
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +0 -1
- package/packages/dd-trace/src/appsec/index.js +42 -7
- package/packages/dd-trace/src/appsec/recommended.json +655 -31
- package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -1
- package/packages/dd-trace/src/appsec/remote_config/index.js +2 -0
- package/packages/dd-trace/src/appsec/reporter.js +26 -0
- package/packages/dd-trace/src/appsec/telemetry.js +132 -0
- package/packages/dd-trace/src/appsec/waf/index.js +1 -1
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +13 -5
- package/packages/dd-trace/src/appsec/waf/waf_manager.js +12 -14
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-itr-configuration.js +1 -14
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -13
- package/packages/dd-trace/src/datastreams/processor.js +6 -2
- package/packages/dd-trace/src/dogstatsd.js +108 -8
- package/packages/dd-trace/src/exporters/agent/writer.js +9 -9
- package/packages/dd-trace/src/exporters/common/request.js +13 -4
- package/packages/dd-trace/src/format.js +6 -1
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +2 -2
- package/packages/dd-trace/src/opentracing/span.js +13 -13
- package/packages/dd-trace/src/opentracing/tracer.js +3 -5
- package/packages/dd-trace/src/plugin_manager.js +1 -2
- package/packages/dd-trace/src/plugins/ci_plugin.js +22 -1
- package/packages/dd-trace/src/plugins/database.js +14 -4
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/plugins/outbound.js +4 -3
- package/packages/dd-trace/src/plugins/tracing.js +1 -1
- package/packages/dd-trace/src/plugins/util/test.js +20 -3
- package/packages/dd-trace/src/profiling/config.js +3 -1
- package/packages/dd-trace/src/profiling/profilers/wall.js +31 -7
- package/packages/dd-trace/src/proxy.js +13 -2
- package/packages/dd-trace/src/ritm.js +10 -2
- package/packages/dd-trace/src/{metrics.js → runtime_metrics.js} +1 -32
- package/packages/dd-trace/src/telemetry/dependencies.js +15 -0
- package/packages/dd-trace/src/telemetry/index.js +21 -2
- package/packages/dd-trace/src/util.js +1 -1
|
@@ -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,52 @@ class SensitiveHandler {
|
|
|
71
74
|
sourceIndex = sourcesIndexes[nextTaintedIndex]
|
|
72
75
|
|
|
73
76
|
while (nextSensitive != null && contains(nextTainted, nextSensitive)) {
|
|
74
|
-
|
|
77
|
+
const redactionStart = nextSensitive.start - nextTainted.start
|
|
78
|
+
const redactionEnd = nextSensitive.end - nextTainted.start
|
|
79
|
+
if (redactionStart === redactionEnd) {
|
|
80
|
+
this.writeRedactedValuePart(valueParts, 0)
|
|
81
|
+
} else {
|
|
82
|
+
this.redactSource(
|
|
83
|
+
sources,
|
|
84
|
+
redactedSources,
|
|
85
|
+
redactedSourcesContext,
|
|
86
|
+
sourceIndex,
|
|
87
|
+
redactionStart,
|
|
88
|
+
redactionEnd
|
|
89
|
+
)
|
|
90
|
+
}
|
|
75
91
|
nextSensitive = sensitive.shift()
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
if (nextSensitive != null && intersects(nextSensitive, nextTainted)) {
|
|
79
|
-
|
|
95
|
+
const redactionStart = nextSensitive.start - nextTainted.start
|
|
96
|
+
const redactionEnd = nextSensitive.end - nextTainted.start
|
|
97
|
+
this.redactSource(sources, redactedSources, redactedSourcesContext, sourceIndex, redactionStart, redactionEnd)
|
|
80
98
|
|
|
81
99
|
const entries = remove(nextSensitive, nextTainted)
|
|
82
100
|
nextSensitive = entries.length > 0 ? entries[0] : null
|
|
83
101
|
}
|
|
84
102
|
|
|
85
|
-
this.isSensibleSource(sources[sourceIndex])
|
|
103
|
+
if (this.isSensibleSource(sources[sourceIndex])) {
|
|
104
|
+
if (!sources[sourceIndex].redacted) {
|
|
105
|
+
redactedSources.push(sourceIndex)
|
|
106
|
+
sources[sourceIndex].pattern = ''.padEnd(sources[sourceIndex].value.length, REDACTED_SOURCE_BUFFER)
|
|
107
|
+
sources[sourceIndex].redacted = true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
86
110
|
|
|
87
111
|
if (redactedSources.indexOf(sourceIndex) > -1) {
|
|
88
|
-
|
|
112
|
+
const partValue = value.substring(i, i + (nextTainted.end - nextTainted.start))
|
|
113
|
+
this.writeRedactedValuePart(
|
|
114
|
+
valueParts,
|
|
115
|
+
partValue.length,
|
|
116
|
+
sourceIndex,
|
|
117
|
+
partValue,
|
|
118
|
+
sources[sourceIndex],
|
|
119
|
+
redactedSourcesContext[sourceIndex],
|
|
120
|
+
this.isSensibleSource(sources[sourceIndex])
|
|
121
|
+
)
|
|
122
|
+
redactedSourcesContext[sourceIndex] = []
|
|
89
123
|
} else {
|
|
90
124
|
const substringEnd = Math.min(nextTainted.end, value.length)
|
|
91
125
|
this.writeValuePart(valueParts, value.substring(nextTainted.start, substringEnd), sourceIndex)
|
|
@@ -100,7 +134,10 @@ class SensitiveHandler {
|
|
|
100
134
|
this.writeValuePart(valueParts, value.substring(start, i), sourceIndex)
|
|
101
135
|
if (nextTainted != null && intersects(nextSensitive, nextTainted)) {
|
|
102
136
|
sourceIndex = sourcesIndexes[nextTaintedIndex]
|
|
103
|
-
|
|
137
|
+
|
|
138
|
+
const redactionStart = nextSensitive.start - nextTainted.start
|
|
139
|
+
const redactionEnd = nextSensitive.end - nextTainted.start
|
|
140
|
+
this.redactSource(sources, redactedSources, redactedSourcesContext, sourceIndex, redactionStart, redactionEnd)
|
|
104
141
|
|
|
105
142
|
for (const entry of remove(nextSensitive, nextTainted)) {
|
|
106
143
|
if (entry.start === i) {
|
|
@@ -111,9 +148,10 @@ class SensitiveHandler {
|
|
|
111
148
|
}
|
|
112
149
|
}
|
|
113
150
|
|
|
114
|
-
|
|
151
|
+
const _length = nextSensitive.end - nextSensitive.start
|
|
152
|
+
this.writeRedactedValuePart(valueParts, _length)
|
|
115
153
|
|
|
116
|
-
start = i +
|
|
154
|
+
start = i + _length
|
|
117
155
|
i = start - 1
|
|
118
156
|
nextSensitive = sensitive.shift()
|
|
119
157
|
}
|
|
@@ -126,6 +164,24 @@ class SensitiveHandler {
|
|
|
126
164
|
return { redactedValueParts: valueParts, redactedSources }
|
|
127
165
|
}
|
|
128
166
|
|
|
167
|
+
redactSource (sources, redactedSources, redactedSourcesContext, sourceIndex, start, end) {
|
|
168
|
+
if (sourceIndex != null) {
|
|
169
|
+
if (!sources[sourceIndex].redacted) {
|
|
170
|
+
redactedSources.push(sourceIndex)
|
|
171
|
+
sources[sourceIndex].pattern = ''.padEnd(sources[sourceIndex].value.length, REDACTED_SOURCE_BUFFER)
|
|
172
|
+
sources[sourceIndex].redacted = true
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!redactedSourcesContext[sourceIndex]) {
|
|
176
|
+
redactedSourcesContext[sourceIndex] = []
|
|
177
|
+
}
|
|
178
|
+
redactedSourcesContext[sourceIndex].push({
|
|
179
|
+
start,
|
|
180
|
+
end
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
129
185
|
writeValuePart (valueParts, value, source) {
|
|
130
186
|
if (value.length > 0) {
|
|
131
187
|
if (source != null) {
|
|
@@ -136,9 +192,74 @@ class SensitiveHandler {
|
|
|
136
192
|
}
|
|
137
193
|
}
|
|
138
194
|
|
|
139
|
-
writeRedactedValuePart (
|
|
140
|
-
|
|
141
|
-
|
|
195
|
+
writeRedactedValuePart (
|
|
196
|
+
valueParts,
|
|
197
|
+
length,
|
|
198
|
+
sourceIndex,
|
|
199
|
+
partValue,
|
|
200
|
+
source,
|
|
201
|
+
sourceRedactionContext,
|
|
202
|
+
isSensibleSource
|
|
203
|
+
) {
|
|
204
|
+
if (sourceIndex != null) {
|
|
205
|
+
const placeholder = source.value.includes(partValue)
|
|
206
|
+
? source.pattern
|
|
207
|
+
: '*'.repeat(length)
|
|
208
|
+
|
|
209
|
+
if (isSensibleSource) {
|
|
210
|
+
valueParts.push({ redacted: true, source: sourceIndex, pattern: placeholder })
|
|
211
|
+
} else {
|
|
212
|
+
let _value = partValue
|
|
213
|
+
const dedupedSourceRedactionContexts = []
|
|
214
|
+
|
|
215
|
+
sourceRedactionContext.forEach(_sourceRedactionContext => {
|
|
216
|
+
const isPresentInDeduped = dedupedSourceRedactionContexts.some(_dedupedSourceRedactionContext =>
|
|
217
|
+
_dedupedSourceRedactionContext.start === _sourceRedactionContext.start &&
|
|
218
|
+
_dedupedSourceRedactionContext.end === _sourceRedactionContext.end
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if (!isPresentInDeduped) {
|
|
222
|
+
dedupedSourceRedactionContexts.push(_sourceRedactionContext)
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
let offset = 0
|
|
227
|
+
dedupedSourceRedactionContexts.forEach((_sourceRedactionContext) => {
|
|
228
|
+
if (_sourceRedactionContext.start > 0) {
|
|
229
|
+
valueParts.push({
|
|
230
|
+
source: sourceIndex,
|
|
231
|
+
value: _value.substring(0, _sourceRedactionContext.start - offset)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
_value = _value.substring(_sourceRedactionContext.start - offset)
|
|
235
|
+
offset = _sourceRedactionContext.start
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const sensitive =
|
|
239
|
+
_value.substring(_sourceRedactionContext.start - offset, _sourceRedactionContext.end - offset)
|
|
240
|
+
const indexOfPartValueInPattern = source.value.indexOf(sensitive)
|
|
241
|
+
|
|
242
|
+
const pattern = indexOfPartValueInPattern > -1
|
|
243
|
+
? placeholder.substring(indexOfPartValueInPattern, indexOfPartValueInPattern + sensitive.length)
|
|
244
|
+
: placeholder.substring(_sourceRedactionContext.start, _sourceRedactionContext.end)
|
|
245
|
+
|
|
246
|
+
valueParts.push({
|
|
247
|
+
redacted: true,
|
|
248
|
+
source: sourceIndex,
|
|
249
|
+
pattern
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
_value = _value.substring(pattern.length)
|
|
253
|
+
offset += pattern.length
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
if (_value.length) {
|
|
257
|
+
valueParts.push({
|
|
258
|
+
source: sourceIndex,
|
|
259
|
+
value: _value
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
}
|
|
142
263
|
} else {
|
|
143
264
|
valueParts.push({ redacted: true })
|
|
144
265
|
}
|
|
@@ -4,15 +4,18 @@ const log = require('../log')
|
|
|
4
4
|
const RuleManager = require('./rule_manager')
|
|
5
5
|
const remoteConfig = require('./remote_config')
|
|
6
6
|
const {
|
|
7
|
+
bodyParser,
|
|
8
|
+
cookieParser,
|
|
9
|
+
graphqlFinishExecute,
|
|
7
10
|
incomingHttpRequestStart,
|
|
8
11
|
incomingHttpRequestEnd,
|
|
9
|
-
bodyParser,
|
|
10
12
|
passportVerify,
|
|
11
13
|
queryParser
|
|
12
14
|
} = require('./channels')
|
|
13
15
|
const waf = require('./waf')
|
|
14
16
|
const addresses = require('./addresses')
|
|
15
17
|
const Reporter = require('./reporter')
|
|
18
|
+
const appsecTelemetry = require('./telemetry')
|
|
16
19
|
const web = require('../plugins/util/web')
|
|
17
20
|
const { extractIp } = require('../plugins/util/ip_extractor')
|
|
18
21
|
const { HTTP_CLIENT_IP } = require('../../../../ext/tags')
|
|
@@ -35,10 +38,14 @@ function enable (_config) {
|
|
|
35
38
|
|
|
36
39
|
Reporter.setRateLimit(_config.appsec.rateLimit)
|
|
37
40
|
|
|
41
|
+
appsecTelemetry.enable(_config.telemetry)
|
|
42
|
+
|
|
38
43
|
incomingHttpRequestStart.subscribe(incomingHttpStartTranslator)
|
|
39
44
|
incomingHttpRequestEnd.subscribe(incomingHttpEndTranslator)
|
|
40
45
|
bodyParser.subscribe(onRequestBodyParsed)
|
|
41
46
|
queryParser.subscribe(onRequestQueryParsed)
|
|
47
|
+
cookieParser.subscribe(onRequestCookieParser)
|
|
48
|
+
graphqlFinishExecute.subscribe(onGraphqlFinishExecute)
|
|
42
49
|
|
|
43
50
|
if (_config.appsec.eventTracking.enabled) {
|
|
44
51
|
passportVerify.subscribe(onPassportVerify)
|
|
@@ -105,12 +112,9 @@ function incomingHttpEndTranslator ({ req, res }) {
|
|
|
105
112
|
payload[addresses.HTTP_INCOMING_PARAMS] = req.params
|
|
106
113
|
}
|
|
107
114
|
|
|
115
|
+
// we need to keep this to support other cookie parsers
|
|
108
116
|
if (req.cookies && typeof req.cookies === 'object') {
|
|
109
|
-
payload[addresses.HTTP_INCOMING_COOKIES] =
|
|
110
|
-
|
|
111
|
-
for (const k of Object.keys(req.cookies)) {
|
|
112
|
-
payload[addresses.HTTP_INCOMING_COOKIES][k] = [req.cookies[k]]
|
|
113
|
-
}
|
|
117
|
+
payload[addresses.HTTP_INCOMING_COOKIES] = req.cookies
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
waf.run(payload, req)
|
|
@@ -146,6 +150,19 @@ function onRequestQueryParsed ({ req, res, abortController }) {
|
|
|
146
150
|
handleResults(results, req, res, rootSpan, abortController)
|
|
147
151
|
}
|
|
148
152
|
|
|
153
|
+
function onRequestCookieParser ({ req, res, abortController, cookies }) {
|
|
154
|
+
const rootSpan = web.root(req)
|
|
155
|
+
if (!rootSpan) return
|
|
156
|
+
|
|
157
|
+
if (!cookies || typeof cookies !== 'object') return
|
|
158
|
+
|
|
159
|
+
const results = waf.run({
|
|
160
|
+
[addresses.HTTP_INCOMING_COOKIES]: cookies
|
|
161
|
+
}, req)
|
|
162
|
+
|
|
163
|
+
handleResults(results, req, res, rootSpan, abortController)
|
|
164
|
+
}
|
|
165
|
+
|
|
149
166
|
function onPassportVerify ({ credentials, user }) {
|
|
150
167
|
const store = storage.getStore()
|
|
151
168
|
const rootSpan = store && store.req && web.root(store.req)
|
|
@@ -158,6 +175,20 @@ function onPassportVerify ({ credentials, user }) {
|
|
|
158
175
|
passportTrackEvent(credentials, user, rootSpan, config.appsec.eventTracking.mode)
|
|
159
176
|
}
|
|
160
177
|
|
|
178
|
+
function onGraphqlFinishExecute ({ context }) {
|
|
179
|
+
const store = storage.getStore()
|
|
180
|
+
const req = store?.req
|
|
181
|
+
|
|
182
|
+
if (!req) return
|
|
183
|
+
|
|
184
|
+
const resolvers = context?.resolvers
|
|
185
|
+
|
|
186
|
+
if (!resolvers || typeof resolvers !== 'object') return
|
|
187
|
+
|
|
188
|
+
// Don't collect blocking result because it only works in monitor mode.
|
|
189
|
+
waf.run({ [addresses.HTTP_INCOMING_GRAPHQL_RESOLVERS]: resolvers }, req)
|
|
190
|
+
}
|
|
191
|
+
|
|
161
192
|
function handleResults (actions, req, res, rootSpan, abortController) {
|
|
162
193
|
if (!actions || !req || !res || !rootSpan || !abortController) return
|
|
163
194
|
|
|
@@ -172,13 +203,17 @@ function disable () {
|
|
|
172
203
|
|
|
173
204
|
RuleManager.clearAllRules()
|
|
174
205
|
|
|
206
|
+
appsecTelemetry.disable()
|
|
207
|
+
|
|
175
208
|
remoteConfig.disableWafUpdate()
|
|
176
209
|
|
|
177
210
|
// Channel#unsubscribe() is undefined for non active channels
|
|
211
|
+
if (bodyParser.hasSubscribers) bodyParser.unsubscribe(onRequestBodyParsed)
|
|
212
|
+
if (graphqlFinishExecute.hasSubscribers) graphqlFinishExecute.unsubscribe(onGraphqlFinishExecute)
|
|
178
213
|
if (incomingHttpRequestStart.hasSubscribers) incomingHttpRequestStart.unsubscribe(incomingHttpStartTranslator)
|
|
179
214
|
if (incomingHttpRequestEnd.hasSubscribers) incomingHttpRequestEnd.unsubscribe(incomingHttpEndTranslator)
|
|
180
|
-
if (bodyParser.hasSubscribers) bodyParser.unsubscribe(onRequestBodyParsed)
|
|
181
215
|
if (queryParser.hasSubscribers) queryParser.unsubscribe(onRequestQueryParsed)
|
|
216
|
+
if (cookieParser.hasSubscribers) cookieParser.unsubscribe(onRequestCookieParser)
|
|
182
217
|
if (passportVerify.hasSubscribers) passportVerify.unsubscribe(onPassportVerify)
|
|
183
218
|
}
|
|
184
219
|
|