Skip to content

Commit

Permalink
feat: add webhook to block users in Baleen from Datadog
Browse files Browse the repository at this point in the history
  • Loading branch information
MathieuGilet committed Dec 5, 2024
1 parent d46831c commit 68d8db4
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 17 deletions.
7 changes: 6 additions & 1 deletion config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import process from 'node:process';
import 'dotenv/config';

function _getNumber(numberAsString, defaultIntNumber) {
const number = parseInt(numberAsString, 10);
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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 * * *',
Expand Down
49 changes: 49 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 27 additions & 0 deletions run/controllers/security.js
Original file line number Diff line number Diff line change
@@ -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;
21 changes: 21 additions & 0 deletions run/routes/security.js
Original file line number Diff line number Diff line change
@@ -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;
71 changes: 56 additions & 15 deletions run/services/cdn.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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, {
Expand Down Expand Up @@ -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 };
2 changes: 2 additions & 0 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -40,6 +41,7 @@ server.route(runRoutesApplication);
server.route(scalingoRoutes);
server.route(deploySitesRoutes);
server.route(runGitHubRoutes);
server.route(runSecurityRoutes);

registerSlashCommands(runDeployConfiguration.deployConfiguration, runManifest);

Expand Down
2 changes: 1 addition & 1 deletion test/integration/run/services/cdn_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
});
});

Expand Down

0 comments on commit 68d8db4

Please sign in to comment.