Skip to content

Commit

Permalink
[FEATURE] Ajouter un bouton permettant de désactiver une règle de blo…
Browse files Browse the repository at this point in the history
…cage auprès du CDN

 #500
  • Loading branch information
pix-service-auto-merge authored Jan 28, 2025
2 parents cef6bc7 + 0be0bd0 commit 243a8f0
Show file tree
Hide file tree
Showing 16 changed files with 495 additions and 58 deletions.
2 changes: 1 addition & 1 deletion common/services/slack/surfaces/messages/post-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ async function postMessage({ message, attachments, channel = '#tech-releases', i
if (!slackResponse.data.ok) {
logger.error({
event: 'slack-post-message',
message: `Slack error occured while sending message : ${slackResponse.data.error}`,
message: `Slack error occurred while sending message : ${slackResponse.data.error}`,
stack: `Payload for error was ${JSON.stringify(payload)}`,
});
}
Expand Down
32 changes: 32 additions & 0 deletions common/services/slack/surfaces/messages/update-message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { config } from '../../../../../config.js';
import { httpAgent } from '../../../../http-agent.js';
import { logger } from '../../../logger.js';

async function updateMessage({ message, ts, attachments, channel = '#tech-releases', injectedHttpAgent = httpAgent }) {
const url = 'https://slack.com/api/chat.update';

const headers = {
'content-type': 'application/json',
authorization: `Bearer ${config.slack.botToken}`,
};
const payload = {
channel,
ts,
as_user: true,
text: message,
attachments: attachments,
};

const slackResponse = await injectedHttpAgent.post({ url, payload, headers });
if (slackResponse.isSuccessful) {
if (!slackResponse.data.ok) {
logger.error({
event: 'slack-update-message',
message: `Slack error occurred while sending message : ${slackResponse.data.error}`,
stack: `Payload for error was ${JSON.stringify(payload)}`,
});
}
}
}

export default { updateMessage };
5 changes: 3 additions & 2 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const configuration = (function () {
requestSigningSecret: process.env.SLACK_SIGNING_SECRET || 'slack-super-signing-secret',
botToken: process.env.SLACK_BOT_TOKEN,
webhookUrlForReporting: process.env.SLACK_WEBHOOK_URL_FOR_REPORTING,
blockedAccessesChannel: process.env.SLACK_BLOCKED_ACCESSES_CHANNEL,
blockedAccessesChannelId: process.env.SLACK_BLOCKED_ACCESSES_CHANNEL_ID,
},

github: {
Expand Down Expand Up @@ -188,7 +188,8 @@ const configuration = (function () {
config.baleen.appNamespaces = _getJSON('{"Pix_Test":"Pix_Namespace","Pix_Test_2":"Pix Namespace 2"}');
config.baleen.protectedFrontApps = ['Pix_Test'];

config.slack.blockedAccessesChannel = 'blocked-accesses-channel';
config.slack.blockedAccessesChannelId = 'C08700JG7QU';
config.slack.botToken = 'fakeToken';

config.datadog.token = 'token';

Expand Down
6 changes: 3 additions & 3 deletions run/controllers/applications.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Boom from '@hapi/boom';

import { config } from '../../config.js';
import * as cdnServices from '../services/cdn.js';
import cdnService from '../services/cdn.js';

const applications = {
async invalidateCdnCache(request) {
Expand All @@ -10,9 +10,9 @@ const applications = {
}

try {
return await cdnServices.invalidateCdnCache(request.params.name);
return await cdnService.invalidateCdnCache(request.params.name);
} catch (error) {
if (error instanceof cdnServices.NamespaceNotFoundError) {
if (error instanceof cdnService.NamespaceNotFoundError) {
return Boom.badRequest();
}
return error;
Expand Down
31 changes: 7 additions & 24 deletions run/controllers/security.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,10 @@
import Boom from '@hapi/boom';

import { config } from '../../config.js';
import * as cdnServices from '../services/cdn.js';
import cdnService from '../services/cdn.js';
import { logger } from '../../common/services/logger.js';
import slackPostMessageService from '../../common/services/slack/surfaces/messages/post-message.js';
import { Attachment, Context, Message, Section } from 'slack-block-builder';

const _buildSlackMessage = function ({ ip, ja3 }) {
return {
channel: `#${config.slack.blockedAccessesChannel}`,
message: 'Règle de blocage mise en place sur Baleen.',
attachments: Message()
.attachments(
Attachment({ color: '#106c1f' })
.blocks(
Section().fields(`IP`, `${ip}`),
Section().fields(`JA3`, `${ja3}`),
Context().elements(`At ${new Date().toLocaleString()}`),
)
.fallback('Règle de blocage mise en place sur Baleen.'),
)
.buildToObject().attachments,
};
};
import { AutomaticRule } from '../models/AutomaticRule.js';

const securities = {
async blockAccessOnBaleen(request) {
Expand Down Expand Up @@ -62,11 +44,12 @@ const securities = {
}

try {
const result = await cdnServices.blockAccess({ ip, ja3, monitorId });
await slackPostMessageService.postMessage(_buildSlackMessage({ ip, ja3 }));
return result;
const addedRules = await cdnService.blockAccess({ ip, ja3, monitorId });
const automaticRule = new AutomaticRule({ ip, ja3 });
await slackPostMessageService.postMessage(automaticRule.getInitialMessage({ addedRules }));
return `Règles de blocage mises en place.`;
} catch (error) {
if (error instanceof cdnServices.NamespaceNotFoundError) {
if (error instanceof cdnService.NamespaceNotFoundError) {
return Boom.badRequest();
}
return error;
Expand Down
8 changes: 7 additions & 1 deletion run/controllers/slack.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { getAppStatusFromScalingo } from '../services/slack/app-status-from-scal
import * as commands from '../services/slack/commands.js';
import shortcuts from '../services/slack/shortcuts.js';
import viewSubmissions from '../services/slack/view-submissions.js';
import blockActions from '../services/slack/block-actions.js';
import { AutomaticRule } from '../models/AutomaticRule.js';

function _getDeployStartedMessage(release, appName) {
return `Commande de déploiement de la release "${release}" pour ${appName} en production bien reçue.`;
Expand Down Expand Up @@ -154,8 +156,12 @@ const slack = {
return viewSubmissions.submitCreateAppOnScalingoConfirmation(payload);
}
return null;
case 'view_closed':
case 'block_actions':
if (payload?.actions[0]?.action_id === AutomaticRule.DISABLE) {
return blockActions.disableAutomaticRule(payload);
}
return null;
case 'view_closed':
default:
logger.info({ event: 'slack', message: 'This kind of interaction is not yet supported by Pix Bot.' });
return null;
Expand Down
72 changes: 72 additions & 0 deletions run/models/AutomaticRule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { config } from '../../config.js';
import { Actions, Attachment, Button, Context, Divider, Message, Section } from 'slack-block-builder';
import dayjs from 'dayjs';

export class AutomaticRule {
static DISABLE = 'disable-automatic-rule';

constructor({ ip, ja3, date = dayjs() }) {
this.ip = ip;
this.ja3 = ja3;
this.date = date;
}

static parseMessage(message) {
const messageObject = message;

const ip = messageObject.attachments[0]?.blocks[0]?.fields[1]?.text;
if (!ip) {
throw new Error('IP field not found.');
}

const ja3 = messageObject.attachments[0]?.blocks[1]?.fields[1]?.text;
if (!ja3) {
throw new Error('JA3 field not found.');
}

const date = messageObject.attachments[0]?.blocks[2]?.elements[0]?.text?.slice(3);
if (!date) {
throw new Error('Date field not found.');
}

return new AutomaticRule({ ip, ja3, date: dayjs(date) });
}

getInitialMessage({ addedRules }) {
return this.#buildMessage({ isActive: true, addedRules });
}

getDeactivatedMessage() {
return this.#buildMessage({ isActive: false });
}

#buildMessage({ isActive, addedRules }) {
return {
channel: `${config.slack.blockedAccessesChannelId}`,
message: 'Règle de blocage mise en place sur Baleen.',
attachments: Message()
.attachments(
Attachment({ color: '#106c1f' })
.blocks(
Section().fields(`IP`, `${this.ip}`),
Section().fields(`JA3`, `${this.ja3}`),
Context().elements(`At ${this.date.format('DD/MM/YYYY HH:mm:ss')}`),
Divider(),
this.#buildMessageFooter({ isActive, addedRules }),
)
.fallback('Règle de blocage mise en place sur Baleen.'),
)
.buildToObject().attachments,
};
}

#buildMessageFooter({ isActive, addedRules }) {
if (isActive) {
return Actions().elements(
Button().text('Désactiver').actionId(AutomaticRule.DISABLE).value(JSON.stringify(addedRules)).danger(),
);
} else {
return Section().fields(`Règle désactivée le`, `${dayjs().format('DD/MM/YYYY HH:mm:ss')}`);
}
}
}
37 changes: 34 additions & 3 deletions run/services/cdn.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,10 @@ async function blockAccess({ ip, ja3, monitorId }) {

const namespaceKeys = await _getNamespaceKey(config.baleen.protectedFrontApps);

const addedRules = [];
for (const namespaceKey of namespaceKeys) {
try {
await axios.post(
const response = await axios.post(
`${CDN_URL}/configs/custom-static-rules`,
{
category: 'block',
Expand All @@ -117,14 +118,44 @@ async function blockAccess({ ip, ja3, monitorId }) {
},
},
);

addedRules.push({ namespaceKey, ruleId: response.data.id });
} 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.`;
return addedRules;
}

async function disableRule({ namespaceKey, ruleId }) {
if (!namespaceKey || namespaceKey === '') {
throw new Error('namespaceKey cannot be empty.');
}

if (!ruleId || ruleId === '') {
throw new Error('ruleId cannot be empty.');
}

try {
await axios.patch(
`${CDN_URL}/configs/custom-static-rules/${ruleId}`,
{ enabled: false },
{
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);
}
}

export { blockAccess, invalidateCdnCache, NamespaceNotFoundError };
export default { blockAccess, disableRule, invalidateCdnCache, NamespaceNotFoundError };
21 changes: 21 additions & 0 deletions run/services/slack/block-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import cdnService from '../cdn.js';
import { AutomaticRule } from '../../models/AutomaticRule.js';
import slackService from '../../../common/services/slack/surfaces/messages/update-message.js';

const blockActions = {
async disableAutomaticRule(payload) {
const rules = JSON.parse(payload.actions[0].value);
const messageTimestamp = payload.message.ts;

for (const rule of rules) {
await cdnService.disableRule(rule);
}

const automaticRule = AutomaticRule.parseMessage(payload.message);
await slackService.updateMessage({ ts: messageTimestamp, ...automaticRule.getDeactivatedMessage() });

return 'Automatic rule disabled.';
},
};

export default blockActions;
6 changes: 3 additions & 3 deletions sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,13 @@ SLACK_BOT_TOKEN=__CHANGE_ME__
# default: none
SLACK_WEBHOOK_URL_FOR_REPORTING=__CHANGE_ME__

# Slack "Pix Bot" channel to send baleen blocked accesses
# Slack "Pix Bot" channel id to send baleen blocked accesses
#
# Channel name without the # before
# Channel ID not the channel name
# presence: required
# type: string
# default: none
SLACK_BLOCKED_ACCESSES_CHANNEL=__CHANGE_ME__
SLACK_BLOCKED_ACCESSES_CHANNEL_ID=__CHANGE_ME__

# ======================
# CDN MANAGEMENT
Expand Down
28 changes: 24 additions & 4 deletions test/acceptance/run/security_test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { config } from '../../../config.js';
import server from '../../../server.js';
import { expect, nock, sinon } from '../../test-helper.js';
import dayjs from 'dayjs';

describe('Acceptance | Run | Security', function () {
let now;
Expand All @@ -23,6 +24,7 @@ describe('Acceptance | Run | Security', function () {
const monitorId = '1234';
const ip = '127.0.0.1';
const ja3 = '9709730930';
const addedRuleId = 'aa1c6158-9512-4e56-a93e-cc8c4de9bc23';

nock('https://console.baleen.cloud/api', {
reqheaders: {
Expand Down Expand Up @@ -58,7 +60,7 @@ describe('Acceptance | Run | Security', function () {
],
],
})
.reply(200);
.reply(200, { id: addedRuleId });

nock('https://slack.com', {
reqheaders: {
Expand All @@ -67,7 +69,7 @@ describe('Acceptance | Run | Security', function () {
},
})
.post('/api/chat.postMessage', {
channel: `#${config.slack.blockedAccessesChannel}`,
channel: `${config.slack.blockedAccessesChannelId}`,
text: 'Règle de blocage mise en place sur Baleen.',
attachments: [
{
Expand Down Expand Up @@ -102,12 +104,30 @@ describe('Acceptance | Run | Security', function () {
{
elements: [
{
text: `At ${now.toLocaleString()}`,
text: `At ${dayjs(now).format('DD/MM/YYYY HH:mm:ss')}`,
type: 'mrkdwn',
},
],
type: 'context',
},
{
type: 'divider',
},
{
elements: [
{
text: {
type: 'plain_text',
text: 'Désactiver',
},
action_id: 'disable-automatic-rule',
style: 'danger',
type: 'button',
value: '[{"namespaceKey":"namespace-key1","ruleId":"aa1c6158-9512-4e56-a93e-cc8c4de9bc23"}]',
},
],
type: 'actions',
},
],
fallback: 'Règle de blocage mise en place sur Baleen.',
},
Expand All @@ -131,7 +151,7 @@ describe('Acceptance | Run | Security', function () {
});

expect(res.statusCode).to.equal(200);
expect(res.result).to.eql('Règle de blocage mise en place.');
expect(res.result).to.equal(`Règles de blocage mises en place.`);
expect(nock.isDone()).to.be.true;
});
});
Expand Down
Loading

0 comments on commit 243a8f0

Please sign in to comment.