From 68d8db48fbfe5eda046127c2d437c543dc119a91 Mon Sep 17 00:00:00 2001 From: Mathieu Gilet Date: Thu, 5 Dec 2024 16:09:17 +0100 Subject: [PATCH] feat: add webhook to block users in Baleen from Datadog --- config.js | 7 ++- package-lock.json | 49 ++++++++++++++++ package.json | 1 + run/controllers/security.js | 27 +++++++++ run/routes/security.js | 21 +++++++ run/services/cdn.js | 71 ++++++++++++++++++----- server.js | 2 + test/integration/run/services/cdn_test.js | 2 +- 8 files changed, 163 insertions(+), 17 deletions(-) create mode 100644 run/controllers/security.js create mode 100644 run/routes/security.js diff --git a/config.js b/config.js index 937cf20d..afd8960f 100644 --- a/config.js +++ b/config.js @@ -1,4 +1,4 @@ -import process from 'node:process'; +import 'dotenv/config'; function _getNumber(numberAsString, defaultIntNumber) { const number = parseInt(numberAsString, 10); @@ -32,6 +32,7 @@ const configuration = (function () { appNamespaces: _getJSON(process.env.BALEEN_APP_NAMESPACES), CDNInvalidationRetryCount: _getNumber(process.env.BALEEN_CDN_INVALIDATION_RETRY_COUNT, 3), CDNInvalidationRetryDelay: _getNumber(process.env.BALEEN_CDN_INVALIDATION_RETRY_DELAY, 2000), + protectedFrontApps: _getJSON(process.env.BALEEN_PROTECTED_FRONT_APPS), }, scalingo: { @@ -109,6 +110,10 @@ const configuration = (function () { schedule: process.env.PIX_SITE_DEPLOY_SCHEDULE, }, + datadog: { + token: process.env.DATADOG_TOKEN, + }, + tasks: { autoScaleEnabled: isFeatureEnabled(process.env.FT_AUTOSCALE_WEB), scheduleAutoScaleUp: process.env.SCHEDULE_AUTOSCALE_UP || '* 0 8 * * *', diff --git a/package-lock.json b/package-lock.json index c734ce51..bb1294ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "cron": "^2.1.0", "dayjs": "^1.11.7", "dotenv": "^16.0.3", + "joi": "^17.6.0", "knex": "^3.1.0", "lodash": "^4.17.21", "node-fetch": "^3.0.0", @@ -1304,6 +1305,29 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/address/node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -3860,6 +3884,31 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/joi/node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "node_modules/joi/node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index 3352606d..26ac0fd7 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "cron": "^2.1.0", "dayjs": "^1.11.7", "dotenv": "^16.0.3", + "joi": "^17.6.0", "knex": "^3.1.0", "lodash": "^4.17.21", "node-fetch": "^3.0.0", diff --git a/run/controllers/security.js b/run/controllers/security.js new file mode 100644 index 00000000..be367071 --- /dev/null +++ b/run/controllers/security.js @@ -0,0 +1,27 @@ +import Boom from '@hapi/boom'; + +import { config } from '../../config.js'; +import * as cdnServices from '../services/cdn.js'; + +const securities = { + async blockAccessOnBaleen(request) { + console.log(request.headers.authorization); + console.log(config.datadog.token); + + if (request.headers.authorization !== config.datadog.token) { + throw Boom.unauthorized('Token is missing or is incorrect'); + } + + const { ip, ja3, eventId } = request.payload; + try { + return await cdnServices.blockAccess({ ip, ja3, eventId }); + } catch (error) { + if (error instanceof cdnServices.NamespaceNotFoundError) { + return Boom.badRequest(); + } + return error; + } + }, +}; + +export default securities; diff --git a/run/routes/security.js b/run/routes/security.js new file mode 100644 index 00000000..f90aaf61 --- /dev/null +++ b/run/routes/security.js @@ -0,0 +1,21 @@ +import securityController from '../controllers/security.js'; +import Joi from 'joi'; + +const securities = [ + { + method: 'POST', + path: '/security/block-access-on-baleen-from-datadog', + handler: securityController.blockAccessOnBaleen, + config: { + validate: { + payload: Joi.object({ + eventId: Joi.string().required(), + ip: Joi.string(), + ja3: Joi.string() + }).or('ip', 'ja3'), + }, + }, + }, +]; + +export default securities; diff --git a/run/services/cdn.js b/run/services/cdn.js index 941b1b30..ca577958 100644 --- a/run/services/cdn.js +++ b/run/services/cdn.js @@ -9,12 +9,12 @@ const CDN_URL = 'https://console.baleen.cloud/api'; class NamespaceNotFoundError extends Error { constructor(application) { - const message = `Namespace for the application: ${application} are not found`; + const message = `A namespace could not been found.`; super(message); } } -async function _getNamespaceKey(application) { +async function _getNamespaceKey(applications) { const urlForAccountDetails = `${CDN_URL}/account`; const accountDetails = await axios.get(urlForAccountDetails, { headers: { @@ -23,24 +23,23 @@ async function _getNamespaceKey(application) { }, }); - const namespaces = config.baleen.appNamespaces; - const namespace = _.find(namespaces, (v, k) => { - return k === application; - }); - - const namespaceKey = _.findKey(accountDetails.data.namespaces, (v) => { - return v === namespace; - }); + const namespaces = applications.map((app) => config.baleen.appNamespaces[app]); + const namespaceKeys = namespaces.map((namespace) => { + return _.findKey(accountDetails.data.namespaces, (v) => { + return v === namespace; + }); + }) + .filter((n) => Boolean(n)); - if (!namespaceKey) { - throw new NamespaceNotFoundError(application); + if (namespaceKeys.length !== applications.length) { + throw new NamespaceNotFoundError(); } - return namespaceKey; + return namespaceKeys; } async function invalidateCdnCache(application) { - const namespaceKey = await _getNamespaceKey(application); + const namespaceKey = await _getNamespaceKey([application]); const urlForInvalidate = `${CDN_URL}/cache/invalidations`; axiosRetry(axios, { @@ -81,4 +80,46 @@ async function invalidateCdnCache(application) { return `Cache CDN invalidé pour l‘application ${application}.`; } -export { invalidateCdnCache, NamespaceNotFoundError }; +async function blockAccess({ ip, ja3, eventId }) { + const namespaceKeys = await _getNamespaceKey(config.baleen.protectedFrontApps); + + for (const namespaceKey of namespaceKeys) { + try { + const name = `Blocage ${ip ? `ip: ${ip}` : ''} ${ja3 ? `ja3: ${ja3}` : ''}`; + const conditions = []; + if (ip) { + conditions.push({ type: 'ip', operator: 'match', value: ip }); + } + if (ja3) { + conditions.push({ type: 'ja3', operator: 'equals', value: ja3 }); + } + + await axios.post( + `${CDN_URL}/configs/custom-static-rules`, + { + category: 'block', + name, + description: `Blocage automatique depuis le monitor Datadog ${eventId}`, + enabled: true, + labels: ['automatic-rule'], + conditions: [conditions], + }, + { + headers: { + 'X-Api-Key': config.baleen.pat, + 'Content-type': 'application/json', + Cookie: `baleen-namespace=${namespaceKey}`, + }, + }, + ); + } catch (error) { + const cdnResponseMessage = JSON.stringify(error.response.data); + const message = `Request failed with status code ${error.response.status} and message ${cdnResponseMessage}`; + throw new Error(message); + } + } + + return `Règle de blocage mise en place.`; +} + +export { blockAccess, invalidateCdnCache, NamespaceNotFoundError }; diff --git a/server.js b/server.js index c978042c..da656e66 100644 --- a/server.js +++ b/server.js @@ -20,6 +20,7 @@ import runRoutesApplication from './run/routes/applications.js'; import deploySitesRoutes from './run/routes/deploy-sites.js'; import runRoutesManifest from './run/routes/manifest.js'; import runGitHubRoutes from './run/routes/github.js'; +import runSecurityRoutes from './run/routes/security.js'; const manifests = [runManifest, buildManifest]; const setupErrorHandling = function (server) { @@ -40,6 +41,7 @@ server.route(runRoutesApplication); server.route(scalingoRoutes); server.route(deploySitesRoutes); server.route(runGitHubRoutes); +server.route(runSecurityRoutes); registerSlashCommands(runDeployConfiguration.deployConfiguration, runManifest); diff --git a/test/integration/run/services/cdn_test.js b/test/integration/run/services/cdn_test.js index 1cdbab0f..1493691c 100644 --- a/test/integration/run/services/cdn_test.js +++ b/test/integration/run/services/cdn_test.js @@ -99,7 +99,7 @@ describe('Integration | CDN', function () { // then expect(result).to.be.instanceOf(cdn.NamespaceNotFoundError); - expect(result.message).to.be.equal('Namespace for the application: Not_existing_application are not found'); + expect(result.message).to.be.equal('A namespace could not been found.'); }); });