Files
broccolini-bot/services/debugLog.js
indifferentketchup 3e9ad658d0 audit week 3 [SEC-004 + SEC-005]: scope members.fetch + redact PII in debug logs
[SEC-004] services/staffThread.js — addRoleMembersToThread previously called
the unscoped guild.members.fetch() on every ticket creation, chunking every
member of the guild. With STAFF_THREAD_AUTO_ADD_ROLE on and a 50-member
staff role, the 300ms-per-add loop also blocked ticket creation for ~15s.

  - Read role.members directly (computed from guild.members.cache, kept in
    sync via the GuildMembers gateway intent set on the client). Skip the
    explicit unscoped fetch in the hot path.
  - Cache-cold fallback: one scoped guild.members.fetch({ withPresences:
    false }) — irrelevant presence sync stays off the wire.
  - createStaffThread no longer awaits the add-loop. Wraps the call in
    setImmediate(...) so ticket creation returns immediately while the
    rate-limited add-loop runs in the background.

[SEC-005] services/debugLog.js — stacks/messages posted to the debug
channel could leak customer email addresses (interpolated through ticket
errors) and Discord member/channel IDs. Add a redactPII helper applied to
both logError's message + stack and logWarn's body:

  - Email regex /[\w.+-]+@[\w.-]+\.\w+/g → [EMAIL_REDACTED]
  - Discord snowflake /\b\d{18,20}\b/g → [ID_REDACTED]

interaction.user.tag in the User: line is intentionally not redacted —
it's needed for triage and is not PII (Discord usernames are public).
2026-05-08 20:42:48 +00:00

100 lines
3.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Structured logging service posts embeds to dedicated Discord channels.
* Call setClient(client) from the main bot on ready so logs can be posted.
*/
const { EmbedBuilder } = require('discord.js');
const { CONFIG } = require('../config');
let client = null;
function setClient(c) {
client = c;
}
// --- PII redaction ---
// Email addresses (loose regex — covers most RFC 5321 local parts that show up
// in support traffic) and Discord snowflakes (1820 digit numeric IDs) get
// redacted before stack/message text reaches the debug channel. Both can land
// in error stacks via senderEmail interpolation, channel IDs in error
// messages, etc. — redacting at the boundary keeps the debug channel useful
// for triage without leaking customer addresses or staff member IDs.
const EMAIL_REDACT_RE = /[\w.+-]+@[\w.-]+\.\w+/g;
const SNOWFLAKE_REDACT_RE = /\b\d{18,20}\b/g;
function redactPII(s) {
if (s == null) return '';
return String(s)
.replace(EMAIL_REDACT_RE, '[EMAIL_REDACTED]')
.replace(SNOWFLAKE_REDACT_RE, '[ID_REDACTED]');
}
// --- Helpers ---
async function sendToChannel(channelId, embed, overrideClient) {
const c = overrideClient || client;
if (!c || !channelId) return;
try {
const channel = await c.channels.fetch(channelId);
if (channel) await channel.send({ embeds: [embed] });
} catch (_) {
// ignore send failures
}
}
// --- logError (backwards-compatible) ---
async function logError(context, error, interaction = null, overrideClient = null) {
const c = overrideClient || client;
if (!c || !CONFIG.DEBUGGING_CHANNEL_ID) return;
try {
const channel = await c.channels.fetch(CONFIG.DEBUGGING_CHANNEL_ID);
const userLine = interaction?.user?.tag
? `User: ${interaction.user.tag}\n`
: '';
const commandLine = (interaction?.commandName || interaction?.customId)
? `Command/Button: ${interaction.commandName || interaction.customId}\n`
: '';
const message = redactPII(error.message || String(error));
const stack = redactPII(error.stack || error.message || String(error)).slice(0, 1500);
await channel.send({
content: `\`[${context}]\` ${message}\n${userLine}${commandLine}\n\`\`\`${stack}\`\`\``
});
} catch (_) {
// ignore send failures
}
}
// --- logWarn ---
async function logWarn(context, message, overrideClient = null) {
const embed = new EmbedBuilder()
.setTitle(`Warning: ${context}`)
.setDescription(redactPII(String(message)).slice(0, 4000))
.setColor(0xFFFF00)
.setTimestamp();
await sendToChannel(CONFIG.DEBUGGING_CHANNEL_ID, embed, overrideClient);
}
// --- logTicketEvent ---
async function logTicketEvent(action, fields, interaction = null) {
const embed = new EmbedBuilder()
.setTitle(action)
.setColor(CONFIG.EMBED_COLOR_INFO || 0x1e2124)
.addFields(fields.map(f => ({ name: f.name, value: String(f.value).slice(0, 1024), inline: f.inline ?? true })))
.setTimestamp();
if (interaction?.user?.tag) {
embed.setFooter({ text: interaction.user.tag });
}
await sendToChannel(CONFIG.LOGGING_CHANNEL_ID, embed, interaction?.client);
}
module.exports = {
setClient,
logError,
logWarn,
logTicketEvent
};