Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ can verify this by looking at the source code, which you should do before execut

* Everything is split in different [modules](#modules) - you can enable, configure and disable it how you want
* Highly configurable - The goal with this bot is that you can change *everything*
<<<<<<< HEAD
* Built-in phishing and scam URL detection with customizable patterns & ML heuristics (moderation module) including optional log channel for detections

* A small suite of unit tests for the moderation/phishing logic – run with `npm test`
=======
>>>>>>> bdf48c957889f18888d1525806101cb792e35246
Comment on lines +96 to +101
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge conflict markers are present in README.md, which will render incorrectly on GitHub. Resolve the conflict and keep the intended documentation changes.

Suggested change
<<<<<<< HEAD
* Built-in phishing and scam URL detection with customizable patterns & ML heuristics (moderation module) including optional log channel for detections
* A small suite of unit tests for the moderation/phishing logic – run with `npm test`
=======
>>>>>>> bdf48c957889f18888d1525806101cb792e35246
* Built-in phishing and scam URL detection with customizable patterns & ML heuristics (moderation module) including optional log channel for detections
* A small suite of unit tests for the moderation/phishing logic – run with `npm test`

Copilot uses AI. Check for mistakes.
* Add your own modules
* Easy configuration - Every config field has a description in an example file

Expand Down
8 changes: 8 additions & 0 deletions anti-phish.code-workspace
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}
Comment on lines +1 to +8
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Editor workspace files are environment-specific and typically shouldn't be committed to the repository. Consider removing this file and adding *.code-workspace to .gitignore instead.

Suggested change
{
"folders": [
{
"path": "."
}
],
"settings": {}
}

Copilot uses AI. Check for mistakes.
54 changes: 54 additions & 0 deletions modules/moderation/__tests__/phishingService.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const heuristics = require('../phishingHeuristics');
const { checkPhishing, setCustomPatterns } = require('../phishingService');

describe('phishing heuristics', () => {
test('isIpAddress detects IPv4 and IPv6', () => {
expect(heuristics.isIpAddress('127.0.0.1')).toBe(true);
expect(heuristics.isIpAddress('::1')).toBe(true);
expect(heuristics.isIpAddress('example.com')).toBe(false);
});

test('domainSimilarity returns info for typos', () => {
const res = heuristics.domainSimilarity('paypal.com', ['paypal.com']);
expect(res).toBeNull();
const res2 = heuristics.domainSimilarity('paypa1.com', ['paypal.com']);
expect(res2).not.toBeNull();
expect(res2.match).toBe('paypal.com');
});

test('extractFirstUrlFromMessage picks a URL', () => {
expect(heuristics.extractFirstUrlFromMessage('no link here')).toBeNull();
expect(
heuristics.extractFirstUrlFromMessage('visit https://example.com now')
).toBe('https://example.com');
});
});

describe('checkPhishing integration', () => {
test('flags obvious phishing link', async () => {
const { isPhishing, reasons } = await checkPhishing({ url: 'http://paypal.com.example.tk/login' });
expect(isPhishing).toBe(true);
expect(reasons.some(r => r.includes('Suspicious TLD'))).toBe(true);
});

test('returns false for benign https site', async () => {
const { isPhishing } = await checkPhishing({ url: 'https://example.com' });
expect(isPhishing).toBe(false);
});
Comment on lines +28 to +37
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests call checkPhishing without mocking DNS/HTTP, so results will vary by network availability (and can be flaky/offline-failing). Mock dns.resolve and axios.head (and any external API calls) so unit tests are deterministic.

Copilot uses AI. Check for mistakes.

test('custom patterns can match', async () => {
setCustomPatterns(['evil']);
const { isPhishing, reasons } = await checkPhishing({ url: 'https://good.com/evil' });
expect(isPhishing).toBe(true);
expect(reasons.some(r => r.includes('Custom pattern'))).toBe(true);
});

test('override configuration works', async () => {
// artificially lower threshold so non-secure example flags
const { isPhishing } = await checkPhishing(
{ url: 'https://example.com' },
{ config: { thresholds: { phishingScore: 0 } } }
);
expect(isPhishing).toBe(true);
});
});
40 changes: 40 additions & 0 deletions modules/moderation/configs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,26 @@
]
},
{
<<<<<<< HEAD
"name": "phishing-log-channel-id",
"humanName": {
"de": "Phishing-Protokoll-Kanal",
"en": "Phishing Log Channel"
},
"default": {
"en": "",
"de": ""
},
"description": {
"en": "Optional channel where auto phishing detections are logged.",
"de": "Optionaler Kanal, in dem automatische Phishing-Erkennungen protokolliert werden."
},
"type": "channelID",
"allowNull": true
},
{
=======
>>>>>>> bdf48c957889f18888d1525806101cb792e35246
"name": "scam_link_level",
Comment on lines +248 to 268
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge conflict markers are present in this JSON file, making it invalid JSON and likely breaking config generation/loading. Resolve the conflict and remove the marker lines.

Copilot uses AI. Check for mistakes.
"humanName": {
"de": "Level der Scam-Link-Erkennung",
Expand Down Expand Up @@ -316,6 +336,26 @@
"content": "string"
},
{
<<<<<<< HEAD
"name": "phishing-custom-patterns",
"humanName": {
"de": "Eigene Phishing-Muster",
"en": "Custom phishing patterns"
},
"default": {
"en": [],
"de": []
},
"description": {
"en": "Add your own regexes/keywords/strings to flag as phishing (one entry per line, RegExp syntax supported)",
"de": "Füge eigene Regexe/Stichwörter/String hinzu, die als Phishing markiert werden sollen (jeweils eine pro Zeile, RegExp-Syntax möglich)"
},
"type": "array",
"content": "string"
},
{
=======
>>>>>>> bdf48c957889f18888d1525806101cb792e35246
"name": "action_on_posting_blacklisted_word",
Comment on lines +339 to 359
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional merge conflict markers are present later in the same JSON file, leaving it invalid. Resolve this conflict block as well so the moderation config schema remains valid JSON.

Copilot uses AI. Check for mistakes.
"humanName": {
"de": "Aktion bei gesperrtem Wort",
Expand Down
22 changes: 22 additions & 0 deletions modules/moderation/configs/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,28 @@
"allowEmbed": true
},
{
<<<<<<< HEAD
"name": "phishing-log-entry",
"humanName": {},
"default": {
"en": "**Phishing detected** by %user% in %channel%:\n%content%",
"de": "**Phishing erkannt** von %user% in %channel%:\n%content%"
},
"description": {
"en": "Log message sent when auto phishing detection triggers",
"de": "Protokollnachricht, die gesendet wird, wenn die automatische Phishing-Erkennung auslöst"
},
"type": "string",
"allowEmbed": true,
"params": [
{ "name": "user", "description": { "en": "Tag of the user who posted the link" } },
{ "name": "channel", "description": { "en": "Channel where the link was posted" } },
{ "name": "content", "description": { "en": "Original message content" } }
]
},
{
=======
>>>>>>> bdf48c957889f18888d1525806101cb792e35246
"name": "submitted-report-message",
Comment on lines +73 to 95
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge conflict markers are present in this JSON file, making it invalid JSON and likely breaking config generation/loading. Resolve the conflict and remove the marker lines.

Copilot uses AI. Check for mistakes.
"humanName": {},
"default": {
Expand Down
15 changes: 15 additions & 0 deletions modules/moderation/events/botReady.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ const {Op} = require('sequelize');
const {localize} = require('../../../src/functions/localize');
const {embedType} = require('../../../src/functions/helpers');
const {scheduleJob} = require('node-schedule');
<<<<<<< HEAD
// import phishing service so that we can feed it custom patterns from configuration
const { setCustomPatterns } = require('../phishingService');
=======
>>>>>>> bdf48c957889f18888d1525806101cb792e35246
Comment on lines +6 to +10
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unresolved Git merge conflict markers are present in this file, which will cause a syntax error when the moderation module loads. Resolve the conflict and remove the marker lines.

Copilot uses AI. Check for mistakes.
const memberCache = {};
const durationParser = require('parse-duration');

Expand Down Expand Up @@ -33,6 +38,16 @@ exports.run = async (client) => {
});
}

<<<<<<< HEAD
// configure phishing service with custom patterns from settings
const customPatterns = client.configurations['moderation']['config']['phishing-custom-patterns'];
if (Array.isArray(customPatterns) && customPatterns.length > 0) {
setCustomPatterns(customPatterns);
client.logger.info('[moderation] loaded ' + customPatterns.length + ' custom phishing pattern(s)');
}

=======
>>>>>>> bdf48c957889f18888d1525806101cb792e35246
Comment on lines +41 to +50
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unresolved Git merge conflict markers are present around the custom phishing pattern configuration; this currently makes the file invalid JS. Resolve the conflict and ensure the phishing pattern initialization is either kept or intentionally removed.

Copilot uses AI. Check for mistakes.
const verificationConfig = client.configurations['moderation']['verification'];
if (!verificationConfig.enabled || !verificationConfig['restart-verification-channel']) return;
const channel = await client.channels.fetch(verificationConfig['restart-verification-channel']).catch(() => {
Expand Down
28 changes: 28 additions & 0 deletions modules/moderation/events/messageCreate.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ const {moderationAction} = require('../moderationActions');
const {embedType} = require('../../../src/functions/helpers');
const {localize} = require('../../../src/functions/localize');
const stopPhishing = require('stop-discord-phishing');
<<<<<<< HEAD
// built-in phishing service (uses ML/heuristics plus configurable patterns)
const { checkPhishing } = require('../phishingService');
=======
>>>>>>> bdf48c957889f18888d1525806101cb792e35246
Comment on lines +5 to +9
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unresolved Git merge conflict markers are committed in this JS file; this will cause a syntax error at runtime. Resolve the conflict and remove the conflict marker lines.

Suggested change
<<<<<<< HEAD
// built-in phishing service (uses ML/heuristics plus configurable patterns)
const { checkPhishing } = require('../phishingService');
=======
>>>>>>> bdf48c957889f18888d1525806101cb792e35246
// built-in phishing service (uses ML/heuristics plus configurable patterns)
const { checkPhishing } = require('../phishingService');

Copilot uses AI. Check for mistakes.

const messageCache = {};

Expand Down Expand Up @@ -88,6 +93,29 @@ async function performBadWordAndInviteProtection(msg) {
const moduleConfig = msg.client.configurations['moderation']['config'];
if (msg.member.roles.cache.find(r => moduleConfig['moderator-roles_level2'].includes(r.id) || moduleConfig['moderator-roles_level3'].includes(r.id) || moduleConfig['moderator-roles_level4'].includes(r.id))) return;
if (moduleConfig['action_on_scam_link'] !== 'none') {
<<<<<<< HEAD
// first run our custom analyzer, which also respects user-defined patterns
const analysis = await checkPhishing({message: msg.content});
if (analysis.isPhishing) {
// send a log message if channel configured
Comment on lines +96 to +100
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unresolved Git merge conflict markers in the scam-link handling block will cause a syntax error and prevent moderation from running. Resolve the conflict and ensure the intended phishing checks execute in the correct order.

Copilot uses AI. Check for mistakes.
const logChanId = moduleConfig['phishing-log-channel-id'] || moduleConfig['logchannel-id'];
if (logChanId) {
const logChan = await msg.client.channels.fetch(logChanId).catch(() => null);
if (logChan && logChan.isText()) {
logChan.send(embedType(msg.client.configurations['moderation']['strings']['phishing-log-entry'] || '**Phishing detected**', {
'%user%': msg.author.tag,
'%channel%': msg.channel.toString(),
'%content%': msg.content
}));
}
}
await msg.delete();
await moderationAction(msg.client, moduleConfig['action_on_scam_link'], msg.client, msg.member, localize('moderation', 'scam-url-sent', {c: msg.channel.toString()}), {roles: msg.member.roles.cache.keys()});
return;
}
// fallback to old library if desired
=======
>>>>>>> bdf48c957889f18888d1525806101cb792e35246
if (await stopPhishing.checkMessage(msg.content, moduleConfig['action_on_scam_link'] === 'suspicious')) {
await msg.delete();
await moderationAction(msg.client, moduleConfig['action_on_scam_link'], msg.client, msg.member, localize('moderation', 'scam-url-sent', {c: msg.channel.toString()}), {roles: msg.member.roles.cache.keys()});
Expand Down
43 changes: 43 additions & 0 deletions modules/moderation/phishingConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Central configuration for phishing detection heuristics

module.exports = {
// list of well-known domains to compare against for typosquatting
legitDomains: [
'paypal.com', 'google.com', 'bankofamerica.com', 'apple.com', 'amazon.com',
'microsoft.com', 'facebook.com', 'twitter.com', 'instagram.com', 'netflix.com',
'chase.com', 'wellsfargo.com', 'citibank.com', 'usbank.com', 'capitalone.com'
],

phishingKeywords: [
'login', 'secure', 'account', 'verify', 'update', 'banking', 'password',
'signin', 'auth', 'recovery', 'billing', 'payment', 'support', 'helpdesk'
],

suspiciousTLDs: ['tk', 'ml', 'ga', 'cf', 'gq', 'xyz', 'top', 'club', 'site', 'online'],

urlShorteners: ['bit.ly', 'tinyurl.com', 'goo.gl', 't.co', 'ow.ly', 'is.gd', 'buff.ly'],

// scoring thresholds / weights - can be tuned externally
thresholds: {
phishingScore: 50 // score above which we consider a link phishing
},

weights: {
ipAddress: 30,
shortener: 20,
typosquatting: 40,
idnHomograph: 35,
idnSimilarity: 20,
suspiciousTld: 15,
keyword: 10,
longUrl: 10,
nonHttps: 25,
dnsFailure: 20,
atSymbol: 30,
encodedChars: 15,
redirect: 20,
googleSafeBrowsing: 50,
virusTotal: 30,
phishTank: 60
}
};
101 changes: 101 additions & 0 deletions modules/moderation/phishingHeuristics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
const stringSimilarity = require('string-similarity');
const tld = require('tldjs');
const punycode = require('punycode');
const { URL } = require('url');

Comment on lines +4 to +5
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import is unused in this module. Removing it avoids confusion and keeps the heuristics module focused on what it actually uses.

Suggested change
const { URL } = require('url');

Copilot uses AI. Check for mistakes.
// expose basic tld utilities so callers don't have to import tldjs directly
function getDomain(hostname) {
return tld.getDomain ? tld.getDomain(hostname) : hostname;
}
function getSubdomain(hostname) {
return tld.getSubdomain ? tld.getSubdomain(hostname) : '';
}
function getTld(hostname) {
// tldjs doesn't expose a simple getter in v2; derive manually
const parts = hostname.split('.');
return parts.length ? parts[parts.length - 1] : '';
}

// individual heuristic helpers

function isIpAddress(hostname) {
return (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname) || /^[\da-f:]+$/i.test(hostname));
}

function domainSimilarity(domain, legitDomains, threshold = 0.7) {
for (const legit of legitDomains) {
const sim = stringSimilarity.compareTwoStrings(domain, legit);
if (sim > threshold && domain !== legit) {
return { match: legit, score: sim };
}
}
return null;
}

function isIdnHomograph(hostname, legitDomains) {
const puny = punycode.toASCII(hostname);
if (puny !== hostname) {
for (const legit of legitDomains) {
const sim = stringSimilarity.compareTwoStrings(puny.replace(/^xn--/, ''), legit);
if (sim > 0.8) {
return { match: legit, score: sim };
}
}
return { homograph: true };
}
return null;
}

function hasSuspiciousTldOrDeepSubdomain(hostname, suspiciousTLDs) {
const subdomain = getSubdomain(hostname);
const parsedTld = getTld(hostname);
if (subdomain.split('.').length > 2 || suspiciousTLDs.includes(parsedTld)) {
return true;
}
return false;
}

function containsPhishingKeyword(pathOrQuery, keywords) {
const lower = pathOrQuery.toLowerCase();
return keywords.filter(kw => lower.includes(kw));
}

function isUrlLong(url, length = 100) {
return url.length > length;
}

function isNonHttps(protocol) {
return protocol !== 'https:';
}

function containsAtSymbol(url) {
return url.includes('@');
}

function hasEncodedChars(url) {
return /%[0-9A-Fa-f]{2}/.test(url) || /\\x[0-9A-Fa-f]{2}/.test(url);
}

function extractFirstUrlFromMessage(message) {
const urlRegex = /(https?:\/\/[^\s]+)/g;
const matches = message.match(urlRegex);
return matches ? matches[0] : null;
}

module.exports = {
isIpAddress,
domainSimilarity,
isIdnHomograph,
hasSuspiciousTldOrDeepSubdomain,
containsPhishingKeyword,
isUrlLong,
isNonHttps,
containsAtSymbol,
hasEncodedChars,
extractFirstUrlFromMessage,

// tld utilities
getDomain,
getSubdomain,
getTld
};
Loading