Skip to content

Commit 179e273

Browse files
authored
[appsec] Stripe business logic events (#7138)
1 parent 1bd5724 commit 179e273

File tree

10 files changed

+1254
-1
lines changed

10 files changed

+1254
-1
lines changed

.github/workflows/appsec.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,3 +514,21 @@ jobs:
514514
with:
515515
api_key: ${{ secrets.DD_API_KEY }}
516516
service: dd-trace-js-tests
517+
518+
stripe:
519+
runs-on: ubuntu-latest
520+
env:
521+
PLUGINS: stripe
522+
steps:
523+
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
524+
- uses: ./.github/actions/node/oldest-maintenance-lts
525+
- uses: ./.github/actions/install
526+
- run: yarn test:appsec:plugins:ci
527+
- uses: ./.github/actions/node/latest
528+
- run: yarn test:appsec:plugins:ci
529+
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
530+
- uses: DataDog/junit-upload-github-action@762867566348d59ac9bcf479ebb4ec040db8940a # v2.0.0
531+
if: always() && github.actor != 'dependabot[bot]'
532+
with:
533+
api_key: ${{ secrets.DD_API_KEY }}
534+
service: dd-trace-js-tests

packages/datadog-instrumentations/src/helpers/hooks.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ module.exports = {
136136
'selenium-webdriver': () => require('../selenium'),
137137
sequelize: () => require('../sequelize'),
138138
sharedb: () => require('../sharedb'),
139+
stripe: () => require('../stripe'),
139140
tedious: () => require('../tedious'),
140141
tinypool: { esmFirst: true, fn: () => require('../vitest') },
141142
undici: () => require('../undici'),
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
'use strict'
2+
3+
const shimmer = require('../../datadog-shimmer')
4+
const { channel, addHook } = require('./helpers/instrument')
5+
6+
const checkoutSessionCreateFinishCh = channel('datadog:stripe:checkoutSession:create:finish')
7+
const paymentIntentCreateFinishCh = channel('datadog:stripe:paymentIntent:create:finish')
8+
const constructEventFinishCh = channel('datadog:stripe:constructEvent:finish')
9+
10+
function wrapSessionCreate (create) {
11+
return function wrappedSessionCreate () {
12+
const promise = create.apply(this, arguments)
13+
14+
if (!checkoutSessionCreateFinishCh.hasSubscribers) return promise
15+
16+
return promise.then((result) => {
17+
checkoutSessionCreateFinishCh.publish(result)
18+
return result
19+
})
20+
}
21+
}
22+
23+
function wrapPaymentIntentCreate (create) {
24+
return function wrappedPaymentIntentCreate () {
25+
const promise = create.apply(this, arguments)
26+
27+
if (!paymentIntentCreateFinishCh.hasSubscribers) return promise
28+
29+
return promise.then((result) => {
30+
paymentIntentCreateFinishCh.publish(result)
31+
return result
32+
})
33+
}
34+
}
35+
36+
function wrapConstructEvent (constructEvent) {
37+
return function wrappedConstructEvent () {
38+
const result = constructEvent.apply(this, arguments)
39+
40+
// no need to check for hasSubscribers,
41+
// if it's false, the publish function will be noop
42+
constructEventFinishCh.publish(result)
43+
44+
return result
45+
}
46+
}
47+
48+
function wrapConstructEventAsync (constructEventAsync) {
49+
return function wrappedConstructEventAsync () {
50+
const promise = constructEventAsync.apply(this, arguments)
51+
52+
if (!constructEventFinishCh.hasSubscribers) return promise
53+
54+
return promise.then((result) => {
55+
constructEventFinishCh.publish(result)
56+
return result
57+
})
58+
}
59+
}
60+
61+
function wrapStripe (Stripe) {
62+
return function wrappedStripe () {
63+
let stripe = Stripe.apply(this, arguments)
64+
65+
// to support both with and without "new" operator syntax
66+
if (this instanceof Stripe) {
67+
stripe = this
68+
}
69+
70+
if (typeof stripe.checkout?.sessions?.create === 'function') {
71+
shimmer.wrap(stripe.checkout.sessions, 'create', wrapSessionCreate)
72+
}
73+
if (typeof stripe.paymentIntents?.create === 'function') {
74+
shimmer.wrap(stripe.paymentIntents, 'create', wrapPaymentIntentCreate)
75+
}
76+
if (typeof stripe.webhooks?.constructEvent === 'function') {
77+
shimmer.wrap(stripe.webhooks, 'constructEvent', wrapConstructEvent)
78+
}
79+
if (typeof stripe.webhooks?.constructEventAsync === 'function') {
80+
shimmer.wrap(stripe.webhooks, 'constructEventAsync', wrapConstructEventAsync)
81+
}
82+
83+
return stripe
84+
}
85+
}
86+
87+
addHook({
88+
name: 'stripe',
89+
versions: ['9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '>=20.0.0'],
90+
}, Stripe => {
91+
return shimmer.wrapFunction(Stripe, wrapStripe)
92+
})

packages/dd-trace/src/appsec/addresses.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,9 @@ module.exports = {
4343

4444
LOGIN_SUCCESS: 'server.business_logic.users.login.success',
4545
LOGIN_FAILURE: 'server.business_logic.users.login.failure',
46+
47+
PAYMENT_CREATION: 'server.business_logic.payment.creation',
48+
PAYMENT_SUCCESS: 'server.business_logic.payment.success',
49+
PAYMENT_FAILURE: 'server.business_logic.payment.failure',
50+
PAYMENT_CANCELLATION: 'server.business_logic.payment.cancellation',
4651
}

packages/dd-trace/src/appsec/channels.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,13 @@ module.exports = {
3838
responseBody: dc.channel('datadog:express:response:json:start'),
3939
responseSetHeader: dc.channel('datadog:http:server:response:set-header:start'),
4040
responseWriteHead: dc.channel('apm:http:server:response:writeHead:start'),
41-
routerParam: dc.channel('datadog:router:param:start'),
4241
routerMiddlewareError: dc.channel('apm:router:middleware:error'),
42+
routerParam: dc.channel('datadog:router:param:start'),
4343
setCookieChannel: dc.channel('datadog:iast:set-cookie'),
4444
setUncaughtExceptionCaptureCallbackStart: dc.channel('datadog:process:setUncaughtExceptionCaptureCallback:start'),
4545
startGraphqlResolve: dc.channel('datadog:graphql:resolver:start'),
46+
stripeCheckoutSessionCreate: dc.channel('datadog:stripe:checkoutSession:create:finish'),
47+
stripeConstructEvent: dc.channel('datadog:stripe:constructEvent:finish'),
48+
stripePaymentIntentCreate: dc.channel('datadog:stripe:paymentIntent:create:finish'),
4649
wafRunFinished: dc.channel('datadog:waf:run:finish'),
4750
}

packages/dd-trace/src/appsec/index.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ const {
3030
routerParam,
3131
fastifyResponseChannel,
3232
fastifyPathParams,
33+
stripeCheckoutSessionCreate,
34+
stripePaymentIntentCreate,
35+
stripeConstructEvent,
3336
} = require('./channels')
3437
const waf = require('./waf')
3538
const addresses = require('./addresses')
@@ -92,6 +95,9 @@ function enable (_config) {
9295
fastifyResponseChannel.subscribe(onResponseBody)
9396
responseWriteHead.subscribe(onResponseWriteHead)
9497
responseSetHeader.subscribe(onResponseSetHeader)
98+
stripeCheckoutSessionCreate.subscribe(onStripeCheckoutSessionCreate)
99+
stripePaymentIntentCreate.subscribe(onStripePaymentIntentCreate)
100+
stripeConstructEvent.subscribe(onStripeConstructEvent)
95101

96102
isEnabled = true
97103
config = _config
@@ -382,6 +388,100 @@ function onResponseSetHeader ({ res, abortController }) {
382388
}
383389
}
384390

391+
function onStripeCheckoutSessionCreate (payload) {
392+
if (payload?.mode !== 'payment') return
393+
394+
waf.run({
395+
persistent: {
396+
[addresses.PAYMENT_CREATION]: {
397+
integration: 'stripe',
398+
id: payload.id,
399+
amount_total: payload.amount_total,
400+
client_reference_id: payload.client_reference_id,
401+
currency: payload.currency,
402+
'discounts.coupon': payload.discounts?.[0]?.coupon,
403+
'discounts.promotion_code': payload.discounts?.[0]?.promotion_code,
404+
livemode: payload.livemode,
405+
'total_details.amount_discount': payload.total_details?.amount_discount,
406+
'total_details.amount_shipping': payload.total_details?.amount_shipping,
407+
},
408+
},
409+
})
410+
}
411+
412+
function onStripePaymentIntentCreate (payload) {
413+
if (payload === null || typeof payload !== 'object') return
414+
415+
waf.run({
416+
persistent: {
417+
[addresses.PAYMENT_CREATION]: {
418+
integration: 'stripe',
419+
id: payload.id,
420+
amount: payload.amount,
421+
currency: payload.currency,
422+
livemode: payload.livemode,
423+
payment_method: payload.payment_method,
424+
},
425+
},
426+
})
427+
}
428+
429+
function onStripeConstructEvent (payload) {
430+
const object = payload?.data?.object
431+
if (object === null || typeof object !== 'object') return
432+
433+
let persistent
434+
435+
switch (payload.type) {
436+
case 'payment_intent.succeeded':
437+
persistent = {
438+
[addresses.PAYMENT_SUCCESS]: {
439+
integration: 'stripe',
440+
id: object.id,
441+
amount: object.amount,
442+
currency: object.currency,
443+
livemode: object.livemode,
444+
payment_method: object.payment_method,
445+
},
446+
}
447+
break
448+
449+
case 'payment_intent.payment_failed':
450+
persistent = {
451+
[addresses.PAYMENT_FAILURE]: {
452+
integration: 'stripe',
453+
id: object.id,
454+
amount: object.amount,
455+
currency: object.currency,
456+
'last_payment_error.code': object.last_payment_error?.code,
457+
'last_payment_error.decline_code': object.last_payment_error?.decline_code,
458+
'last_payment_error.payment_method.id': object.last_payment_error?.payment_method?.id,
459+
'last_payment_error.payment_method.type': object.last_payment_error?.payment_method?.type,
460+
livemode: object.livemode,
461+
},
462+
}
463+
break
464+
465+
case 'payment_intent.canceled':
466+
persistent = {
467+
[addresses.PAYMENT_CANCELLATION]: {
468+
integration: 'stripe',
469+
id: object.id,
470+
amount: object.amount,
471+
cancellation_reason: object.cancellation_reason,
472+
currency: object.currency,
473+
livemode: object.livemode,
474+
},
475+
}
476+
break
477+
478+
default:
479+
return
480+
}
481+
482+
waf.run({ persistent })
483+
}
484+
385485
function handleResults (actions, req, res, rootSpan, abortController) {
386486
if (!actions || !req || !res || !rootSpan || !abortController) return
387487

@@ -427,6 +527,9 @@ function disable () {
427527
if (fastifyResponseChannel.hasSubscribers) fastifyResponseChannel.unsubscribe(onResponseBody)
428528
if (responseWriteHead.hasSubscribers) responseWriteHead.unsubscribe(onResponseWriteHead)
429529
if (responseSetHeader.hasSubscribers) responseSetHeader.unsubscribe(onResponseSetHeader)
530+
if (stripeCheckoutSessionCreate.hasSubscribers) stripeCheckoutSessionCreate.unsubscribe(onStripeCheckoutSessionCreate)
531+
if (stripePaymentIntentCreate.hasSubscribers) stripePaymentIntentCreate.unsubscribe(onStripePaymentIntentCreate)
532+
if (stripeConstructEvent.hasSubscribers) stripeConstructEvent.unsubscribe(onStripeConstructEvent)
430533
}
431534

432535
// this is faster than Object.keys().length === 0

0 commit comments

Comments
 (0)