3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-03-18 15:00:00 +00:00
zeppelin/backend/src/data/Phisherman.ts

249 lines
7.5 KiB
TypeScript
Raw Normal View History

Update djs & knub (#395) * update pkgs Signed-off-by: GitHub <noreply@github.com> * new knub typings Signed-off-by: GitHub <noreply@github.com> * more pkg updates Signed-off-by: GitHub <noreply@github.com> * more fixes Signed-off-by: GitHub <noreply@github.com> * channel typings Signed-off-by: GitHub <noreply@github.com> * more message utils typings fixes Signed-off-by: GitHub <noreply@github.com> * migrate permissions Signed-off-by: GitHub <noreply@github.com> * fix: InternalPoster webhookables Signed-off-by: GitHub <noreply@github.com> * djs typings: Attachment & Util Signed-off-by: GitHub <noreply@github.com> * more typings Signed-off-by: GitHub <noreply@github.com> * fix: rename permissionNames Signed-off-by: GitHub <noreply@github.com> * more fixes Signed-off-by: GitHub <noreply@github.com> * half the number of errors * knub commands => messageCommands Signed-off-by: GitHub <noreply@github.com> * configPreprocessor => configParser Signed-off-by: GitHub <noreply@github.com> * fix channel.messages Signed-off-by: GitHub <noreply@github.com> * revert automod any typing Signed-off-by: GitHub <noreply@github.com> * more configParser typings Signed-off-by: GitHub <noreply@github.com> * revert Signed-off-by: GitHub <noreply@github.com> * remove knub type params Signed-off-by: GitHub <noreply@github.com> * fix more MessageEmbed / MessageOptions Signed-off-by: GitHub <noreply@github.com> * dumb commit for @almeidx to see why this is stupid Signed-off-by: GitHub <noreply@github.com> * temp disable custom_events Signed-off-by: GitHub <noreply@github.com> * more minor typings fixes - 23 err left Signed-off-by: GitHub <noreply@github.com> * update djs dep * +debug build method (revert this) Signed-off-by: GitHub <noreply@github.com> * Revert "+debug build method (revert this)" This reverts commit a80af1e729b742d1aad1097df538d224fbd32ce7. * Redo +debug build (Revert this) Signed-off-by: GitHub <noreply@github.com> * uniform before/after Load shorthands Signed-off-by: GitHub <noreply@github.com> * remove unused imports & add prettier plugin Signed-off-by: GitHub <noreply@github.com> * env fixes for web platform hosting Signed-off-by: GitHub <noreply@github.com> * feat: knub v32-next; related fixes * fix: allow legacy keys in change_perms action * fix: request Message Content intent * fix: use Knub's config validation logic in API * fix(dashboard): fix error when there are no message and/or slash commands in a plugin * fix(automod): start_thread action thread options * fix(CustomEvents): message command types * chore: remove unneeded type annotation * feat: add forum channel icon; use thread icon for news threads * chore: make tslint happy * chore: fix formatting --------- Signed-off-by: GitHub <noreply@github.com> Co-authored-by: almeidx <almeidx@pm.me> Co-authored-by: Dragory <2606411+Dragory@users.noreply.github.com>
2023-04-01 12:58:17 +01:00
import crypto from "crypto";
import moment from "moment-timezone";
import { getRepository, Repository } from "typeorm";
import { env } from "../env";
2021-10-31 17:17:31 +02:00
import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils";
Update djs & knub (#395) * update pkgs Signed-off-by: GitHub <noreply@github.com> * new knub typings Signed-off-by: GitHub <noreply@github.com> * more pkg updates Signed-off-by: GitHub <noreply@github.com> * more fixes Signed-off-by: GitHub <noreply@github.com> * channel typings Signed-off-by: GitHub <noreply@github.com> * more message utils typings fixes Signed-off-by: GitHub <noreply@github.com> * migrate permissions Signed-off-by: GitHub <noreply@github.com> * fix: InternalPoster webhookables Signed-off-by: GitHub <noreply@github.com> * djs typings: Attachment & Util Signed-off-by: GitHub <noreply@github.com> * more typings Signed-off-by: GitHub <noreply@github.com> * fix: rename permissionNames Signed-off-by: GitHub <noreply@github.com> * more fixes Signed-off-by: GitHub <noreply@github.com> * half the number of errors * knub commands => messageCommands Signed-off-by: GitHub <noreply@github.com> * configPreprocessor => configParser Signed-off-by: GitHub <noreply@github.com> * fix channel.messages Signed-off-by: GitHub <noreply@github.com> * revert automod any typing Signed-off-by: GitHub <noreply@github.com> * more configParser typings Signed-off-by: GitHub <noreply@github.com> * revert Signed-off-by: GitHub <noreply@github.com> * remove knub type params Signed-off-by: GitHub <noreply@github.com> * fix more MessageEmbed / MessageOptions Signed-off-by: GitHub <noreply@github.com> * dumb commit for @almeidx to see why this is stupid Signed-off-by: GitHub <noreply@github.com> * temp disable custom_events Signed-off-by: GitHub <noreply@github.com> * more minor typings fixes - 23 err left Signed-off-by: GitHub <noreply@github.com> * update djs dep * +debug build method (revert this) Signed-off-by: GitHub <noreply@github.com> * Revert "+debug build method (revert this)" This reverts commit a80af1e729b742d1aad1097df538d224fbd32ce7. * Redo +debug build (Revert this) Signed-off-by: GitHub <noreply@github.com> * uniform before/after Load shorthands Signed-off-by: GitHub <noreply@github.com> * remove unused imports & add prettier plugin Signed-off-by: GitHub <noreply@github.com> * env fixes for web platform hosting Signed-off-by: GitHub <noreply@github.com> * feat: knub v32-next; related fixes * fix: allow legacy keys in change_perms action * fix: request Message Content intent * fix: use Knub's config validation logic in API * fix(dashboard): fix error when there are no message and/or slash commands in a plugin * fix(automod): start_thread action thread options * fix(CustomEvents): message command types * chore: remove unneeded type annotation * feat: add forum channel icon; use thread icon for news threads * chore: make tslint happy * chore: fix formatting --------- Signed-off-by: GitHub <noreply@github.com> Co-authored-by: almeidx <almeidx@pm.me> Co-authored-by: Dragory <2606411+Dragory@users.noreply.github.com>
2023-04-01 12:58:17 +01:00
import { PhishermanCacheEntry } from "./entities/PhishermanCacheEntry";
2021-10-31 17:17:31 +02:00
import { PhishermanKeyCacheEntry } from "./entities/PhishermanKeyCacheEntry";
Update djs & knub (#395) * update pkgs Signed-off-by: GitHub <noreply@github.com> * new knub typings Signed-off-by: GitHub <noreply@github.com> * more pkg updates Signed-off-by: GitHub <noreply@github.com> * more fixes Signed-off-by: GitHub <noreply@github.com> * channel typings Signed-off-by: GitHub <noreply@github.com> * more message utils typings fixes Signed-off-by: GitHub <noreply@github.com> * migrate permissions Signed-off-by: GitHub <noreply@github.com> * fix: InternalPoster webhookables Signed-off-by: GitHub <noreply@github.com> * djs typings: Attachment & Util Signed-off-by: GitHub <noreply@github.com> * more typings Signed-off-by: GitHub <noreply@github.com> * fix: rename permissionNames Signed-off-by: GitHub <noreply@github.com> * more fixes Signed-off-by: GitHub <noreply@github.com> * half the number of errors * knub commands => messageCommands Signed-off-by: GitHub <noreply@github.com> * configPreprocessor => configParser Signed-off-by: GitHub <noreply@github.com> * fix channel.messages Signed-off-by: GitHub <noreply@github.com> * revert automod any typing Signed-off-by: GitHub <noreply@github.com> * more configParser typings Signed-off-by: GitHub <noreply@github.com> * revert Signed-off-by: GitHub <noreply@github.com> * remove knub type params Signed-off-by: GitHub <noreply@github.com> * fix more MessageEmbed / MessageOptions Signed-off-by: GitHub <noreply@github.com> * dumb commit for @almeidx to see why this is stupid Signed-off-by: GitHub <noreply@github.com> * temp disable custom_events Signed-off-by: GitHub <noreply@github.com> * more minor typings fixes - 23 err left Signed-off-by: GitHub <noreply@github.com> * update djs dep * +debug build method (revert this) Signed-off-by: GitHub <noreply@github.com> * Revert "+debug build method (revert this)" This reverts commit a80af1e729b742d1aad1097df538d224fbd32ce7. * Redo +debug build (Revert this) Signed-off-by: GitHub <noreply@github.com> * uniform before/after Load shorthands Signed-off-by: GitHub <noreply@github.com> * remove unused imports & add prettier plugin Signed-off-by: GitHub <noreply@github.com> * env fixes for web platform hosting Signed-off-by: GitHub <noreply@github.com> * feat: knub v32-next; related fixes * fix: allow legacy keys in change_perms action * fix: request Message Content intent * fix: use Knub's config validation logic in API * fix(dashboard): fix error when there are no message and/or slash commands in a plugin * fix(automod): start_thread action thread options * fix(CustomEvents): message command types * chore: remove unneeded type annotation * feat: add forum channel icon; use thread icon for news threads * chore: make tslint happy * chore: fix formatting --------- Signed-off-by: GitHub <noreply@github.com> Co-authored-by: almeidx <almeidx@pm.me> Co-authored-by: Dragory <2606411+Dragory@users.noreply.github.com>
2023-04-01 12:58:17 +01:00
import { PhishermanDomainInfo, PhishermanUnknownDomain } from "./types/phisherman";
2021-10-31 17:17:31 +02:00
const API_URL = "https://api.phisherman.gg";
const MASTER_API_KEY = env.PHISHERMAN_API_KEY;
2021-10-31 17:17:31 +02:00
let caughtDomainTrackingMap: Map<string, Map<string, number[]>> = new Map();
const pendingApiRequests: Map<string, Promise<unknown>> = new Map();
const pendingDomainInfoChecks: Map<string, Promise<PhishermanDomainInfo | null>> = new Map();
type MemoryCacheEntry = {
info: PhishermanDomainInfo | null;
expires: number;
};
const memoryCache: Map<string, MemoryCacheEntry> = new Map();
setInterval(() => {
const now = Date.now();
for (const [key, entry] of memoryCache.entries()) {
if (entry.expires <= now) {
memoryCache.delete(key);
}
}
}, 2 * MINUTES);
const UNKNOWN_DOMAIN_CACHE_LIFETIME = 2 * MINUTES;
const DETECTED_DOMAIN_CACHE_LIFETIME = 15 * MINUTES;
const SAFE_DOMAIN_CACHE_LIFETIME = 7 * DAYS;
const KEY_VALIDITY_LIFETIME = 24 * HOURS;
let cacheRepository: Repository<PhishermanCacheEntry> | null = null;
function getCacheRepository(): Repository<PhishermanCacheEntry> {
if (cacheRepository == null) {
cacheRepository = getRepository(PhishermanCacheEntry);
}
return cacheRepository;
}
let keyCacheRepository: Repository<PhishermanKeyCacheEntry> | null = null;
function getKeyCacheRepository(): Repository<PhishermanKeyCacheEntry> {
if (keyCacheRepository == null) {
keyCacheRepository = getRepository(PhishermanKeyCacheEntry);
}
return keyCacheRepository;
}
class PhishermanApiError extends Error {
method: string;
url: string;
2021-10-31 17:17:31 +02:00
status: number;
constructor(method: string, url: string, status: number, message: string) {
2021-10-31 17:17:31 +02:00
super(message);
this.method = method;
this.url = url;
2021-10-31 17:17:31 +02:00
this.status = status;
}
toString() {
return `Error ${this.status} in ${this.method} ${this.url}: ${this.message}`;
}
2021-10-31 17:17:31 +02:00
}
export function hasPhishermanMasterAPIKey() {
return MASTER_API_KEY != null && MASTER_API_KEY !== "";
}
export function phishermanDomainIsSafe(info: PhishermanDomainInfo): boolean {
return info.classification === "safe";
}
const leadingSlashRegex = /^\/+/g;
function trimLeadingSlash(str: string): string {
return str.replace(leadingSlashRegex, "");
}
/**
* Make an arbitrary API call to the Phisherman API
*/
async function apiCall<T>(
method: "GET" | "POST",
resource: string,
payload?: Record<string, unknown> | null,
): Promise<T> {
if (!hasPhishermanMasterAPIKey()) {
throw new Error("Phisherman master API key missing");
}
const url = `${API_URL}/${trimLeadingSlash(resource)}`;
const key = `${method} ${url}`;
if (pendingApiRequests.has(key)) {
return pendingApiRequests.get(key)! as Promise<T>;
}
let requestPromise = (async () => {
2021-10-31 17:17:31 +02:00
const response = await fetch(url, {
method,
headers: new Headers({
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${MASTER_API_KEY}`,
}),
body: payload ? JSON.stringify(payload) : undefined,
});
const data = await response.json().catch(() => null);
if (!response.ok || (data as any)?.success === false) {
throw new PhishermanApiError(method, url, response.status, (data as any)?.message ?? "");
2021-10-31 17:17:31 +02:00
}
return data;
})();
requestPromise = requestPromise.finally(() => {
2021-10-31 17:17:31 +02:00
pendingApiRequests.delete(key);
});
pendingApiRequests.set(key, requestPromise);
return requestPromise as Promise<T>;
}
type DomainInfoApiCallResult = PhishermanUnknownDomain | PhishermanDomainInfo;
async function fetchDomainInfo(domain: string): Promise<PhishermanDomainInfo | null> {
// tslint:disable-next-line:no-console
console.log(`[PHISHERMAN] Requesting domain information: ${domain}`);
const result = await apiCall<Record<string, DomainInfoApiCallResult>>("GET", `/v2/domains/info/${domain}`);
const firstKey = Object.keys(result)[0];
const domainInfo = firstKey ? result[firstKey] : null;
if (!domainInfo) {
// tslint:disable-next-line:no-console
console.warn(`Unexpected Phisherman API response for ${domain}:`, result);
return null;
}
2021-10-31 17:17:31 +02:00
if (domainInfo.classification === "unknown") {
return null;
}
return domainInfo;
}
export async function getPhishermanDomainInfo(domain: string): Promise<PhishermanDomainInfo | null> {
if (pendingDomainInfoChecks.has(domain)) {
return pendingDomainInfoChecks.get(domain)!;
}
let promise = (async () => {
2021-10-31 17:17:31 +02:00
if (memoryCache.has(domain)) {
return memoryCache.get(domain)!.info;
}
const dbCache = getCacheRepository();
const existingCachedEntry = await dbCache.findOne({ domain });
if (existingCachedEntry) {
return existingCachedEntry.data;
}
const freshData = await fetchDomainInfo(domain);
const expiryTime =
freshData === null
? UNKNOWN_DOMAIN_CACHE_LIFETIME
: phishermanDomainIsSafe(freshData)
? SAFE_DOMAIN_CACHE_LIFETIME
: DETECTED_DOMAIN_CACHE_LIFETIME;
memoryCache.set(domain, {
info: freshData,
expires: Date.now() + expiryTime,
});
if (freshData) {
// Database cache only stores safe/detected domains, not unknown ones
await dbCache.insert({
domain,
data: freshData,
expires_at: moment().add(expiryTime, "ms").format(DBDateFormat),
});
}
return freshData;
})();
promise = promise.finally(() => {
2021-10-31 17:17:31 +02:00
pendingDomainInfoChecks.delete(domain);
});
pendingDomainInfoChecks.set(domain, promise);
return promise;
}
export async function phishermanApiKeyIsValid(apiKey: string): Promise<boolean> {
if (apiKey === MASTER_API_KEY) {
return true;
}
2021-10-31 17:17:31 +02:00
const keyCache = getKeyCacheRepository();
const hash = crypto.createHash("sha256").update(apiKey).digest("hex");
const entry = await keyCache.findOne({ hash });
if (entry) {
return entry.is_valid;
}
const { valid: isValid } = await apiCall<{ valid: boolean }>("POST", "/zeppelin/check-key", { apiKey });
await keyCache.insert({
hash,
is_valid: isValid,
expires_at: moment().add(KEY_VALIDITY_LIFETIME, "ms").format(DBDateFormat),
});
return isValid;
}
export function trackPhishermanCaughtDomain(apiKey: string, domain: string) {
if (!caughtDomainTrackingMap.has(apiKey)) {
caughtDomainTrackingMap.set(apiKey, new Map());
}
const apiKeyMap = caughtDomainTrackingMap.get(apiKey)!;
if (!apiKeyMap.has(domain)) {
apiKeyMap.set(domain, []);
}
const timestamps = apiKeyMap.get(domain)!;
timestamps.push(Date.now());
}
export async function reportTrackedDomainsToPhisherman() {
const result = {};
for (const [apiKey, domains] of caughtDomainTrackingMap.entries()) {
result[apiKey] = {};
for (const [domain, timestamps] of domains.entries()) {
result[apiKey][domain] = timestamps;
}
}
if (Object.keys(result).length > 0) {
await apiCall("POST", "/v2/phish/caught/bulk", result);
caughtDomainTrackingMap = new Map();
}
}
export async function deleteStalePhishermanCacheEntries() {
await getCacheRepository().createQueryBuilder().where("expires_at <= NOW()").delete().execute();
}
export async function deleteStalePhishermanKeyCacheEntries() {
await getKeyCacheRepository().createQueryBuilder().where("expires_at <= NOW()").delete().execute();
}