import { RuleTester } from 'eslint'; import tsParser from '@typescript-eslint/parser'; import test from 'node:test'; import rule from '../../src/eslint/rules/webhook-handler-requires-signature-verification.js'; const ruleTester = new RuleTester({ languageOptions: { parser: tsParser, ecmaVersion: 2022, sourceType: 'module', }, }); const PARSE_FILENAME = '/repo/integrations/foo/src/handlers/parseWebhooks.ts'; const PARSE_FILENAME_ALT = '/repo/integrations/foo/src/routes/webhookParse.ts'; const NON_WEBHOOK_FILENAME = '/repo/integrations/foo/src/handlers/task.ts'; test('webhook-handler-requires-signature-verification', () => { ruleTester.run('webhook-handler-requires-signature-verification', rule, { valid: [ { name: 'parseWebhooks.ts importing node:crypto verifier helpers', filename: PARSE_FILENAME, code: ` import { createHmac, timingSafeEqual } from 'node:crypto'; import { ParseWebhooksHandler } from '@unito/integration-sdk'; export const parseWebhooks: ParseWebhooksHandler = async (ctx) => { return []; }; `, }, { name: 'webhookParse.ts importing legacy `crypto` module', filename: PARSE_FILENAME_ALT, code: ` import * as crypto from 'crypto'; import { ParseWebhooksHandler } from '@unito/integration-sdk'; export const parseWebhooks: ParseWebhooksHandler = async (ctx) => { return []; }; `, }, { name: 'parseWebhooks.ts with @webhook-no-signature opt-out', filename: PARSE_FILENAME, code: ` import { ParseWebhooksHandler } from '@unito/integration-sdk'; /** @webhook-no-signature: provider does not sign webhooks (legacy ServiceNow business rule) */ export const parseWebhooks: ParseWebhooksHandler = async (ctx) => { return []; }; `, }, { name: 'AcknowledgeWebhooksHandler export is out of scope', filename: '/repo/integrations/foo/src/handlers/webhookAcknowledge.ts', code: ` import { AcknowledgeWebhooksHandler } from '@unito/integration-sdk'; export const acknowledgeWebhooks: AcknowledgeWebhooksHandler = async (ctx) => { return { handshake: 'ok' }; }; `, }, { name: 'non-webhook file is out of scope', filename: NON_WEBHOOK_FILENAME, code: ` import { GetItemHandler } from '@unito/integration-sdk'; export const getTask: GetItemHandler = async (ctx) => ({}); `, }, { name: 'arbitrary file that imports ParseWebhooksHandler with verifier (in-scope by import)', filename: '/repo/integrations/foo/src/helpers/webhookHelper.ts', code: ` import { createHmac } from 'node:crypto'; import { ParseWebhooksHandler } from '@unito/integration-sdk'; export const parseWebhooks: ParseWebhooksHandler = async (ctx) => []; `, }, { name: 'parseWebhooks.ts importing SDK verifier helper by name', filename: PARSE_FILENAME, code: ` import { ParseWebhooksHandler, verifyWebhookSignature } from '@unito/integration-sdk'; export const parseWebhooks: ParseWebhooksHandler = async (ctx) => { verifyWebhookSignature(ctx.body.headers, ctx.body.payload); return []; }; `, }, { name: 'webhookParse.ts only an Acknowledge export plus crypto-less helper imports', filename: '/repo/integrations/foo/src/handlers/webhookSubscriptions.ts', code: ` import { AcknowledgeWebhooksHandler } from '@unito/integration-sdk'; export const acknowledgeWebhooks: AcknowledgeWebhooksHandler = async (ctx) => ({}); `, }, ], invalid: [ { name: 'parseWebhooks.ts exporting ParseWebhooksHandler with no crypto import', filename: PARSE_FILENAME, code: ` import { ParseWebhooksHandler } from '@unito/integration-sdk'; export const parseWebhooks: ParseWebhooksHandler = async (ctx) => { const payload = JSON.parse(ctx.body.payload); return []; }; `, errors: [{ messageId: 'missingVerification', data: { name: 'parseWebhooks' } }], }, { name: 'webhookParse.ts (alternate name) with no verifier import', filename: PARSE_FILENAME_ALT, code: ` import { ParseWebhooksHandler, ParseWebhooksContext } from '@unito/integration-sdk'; export const parseWebhooks: ParseWebhooksHandler = async (ctx: ParseWebhooksContext) => { return []; }; `, errors: [{ messageId: 'missingVerification', data: { name: 'parseWebhooks' } }], }, { name: 'parseWebhooks.ts with raw JSON.parse and no signature check', filename: PARSE_FILENAME, code: ` import { ParseWebhooksHandler } from '@unito/integration-sdk'; export const parseWebhooks: ParseWebhooksHandler = async (ctx) => { const data = JSON.parse(ctx.body.payload); return data.events.map((e) => ({ itemPath: '/x', date: '', impactedRelations: [] })); }; `, errors: [{ messageId: 'missingVerification', data: { name: 'parseWebhooks' } }], }, { name: 'arbitrary filename in-scope by ParseWebhooksHandler import only', filename: '/repo/integrations/foo/src/helpers/somethingElse.ts', code: ` import { ParseWebhooksHandler } from '@unito/integration-api'; export const parseWebhooks: ParseWebhooksHandler = async () => []; `, errors: [{ messageId: 'missingVerification', data: { name: 'parseWebhooks' } }], }, { name: 'parseWebhooks.ts opt-out without reason is rejected', filename: PARSE_FILENAME, code: ` import { ParseWebhooksHandler } from '@unito/integration-sdk'; /** @webhook-no-signature: */ export const parseWebhooks: ParseWebhooksHandler = async (ctx) => []; `, errors: [{ messageId: 'missingVerification', data: { name: 'parseWebhooks' } }], }, { name: 'parseWebhooks.ts inferred from export name even without type annotation', filename: PARSE_FILENAME, code: ` export const parseWebhooks = async (ctx) => { return []; }; `, errors: [{ messageId: 'missingVerification', data: { name: 'parseWebhooks' } }], }, { name: 'parseWebhooks.ts with non-crypto import still flagged', filename: PARSE_FILENAME, code: ` import { JSON5 } from 'some-other-pkg'; import { ParseWebhooksHandler } from '@unito/integration-sdk'; export const parseWebhooks: ParseWebhooksHandler = async (ctx) => { return JSON5.parse(ctx.body.payload); }; `, errors: [{ messageId: 'missingVerification', data: { name: 'parseWebhooks' } }], }, { name: 'webhookParse.ts with separate const decl and `export { parseWebhooks }` form', filename: PARSE_FILENAME_ALT, code: ` import { ParseWebhooksHandler, ParseWebhooksContext } from '@unito/integration-sdk'; const parseWebhooks: ParseWebhooksHandler = async (ctx: ParseWebhooksContext) => { return []; }; export { parseWebhooks }; `, errors: [{ messageId: 'missingVerification', data: { name: 'parseWebhooks' } }], }, { name: 'Express-route webhookParse.ts: only exports a `router` but file is in scope by filename', filename: '/repo/integrations/foo/src/routes/webhookParse.ts', code: ` import { Router } from 'express'; export const router = Router(); router.post('/', async (req, res) => { res.json({}); }); `, errors: [{ messageId: 'missingVerification', data: { name: '' } }], }, ], }); });