diff --git a/CHANGELOG.md b/CHANGELOG.md index ff6ff98..1b33a34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,52 +7,119 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +Nothing yet. + +## [0.1.0] - 2026-07-06 - AntiLink Guard OSS + +This release is a complete rebuild of the repository into **AntiLink Guard +OSS**, a self-hostable, TypeScript, open-source Discord anti-phishing and +link-moderation framework, restructured as a pnpm workspace monorepo. It +replaces the single-file bot described under +[`[0.1.0-legacy]`](#010-legacy---2024-04-14) below entirely - see +[`docs/migration-from-old-antilink.md`](./docs/migration-from-old-antilink.md) +if you were running that version. + ### Added -- Professional README with architecture overview, roadmap, and FAQ. -- `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, and `SECURITY.md`. -- GitHub issue templates (bug report, feature request) and pull request template. -- Continuous Integration workflow (lint/build) via GitHub Actions. -- Dependabot configuration for npm and GitHub Actions updates. -- `.env.example` documenting required environment variables. -- Base ESLint (flat config) for linting, with `eslint`, `@eslint/js`, and - `globals` as devDependencies and a `lint` npm script. -- `start` npm script (`node index.js`) and `engines` field (Node.js >= 18). -- `LICENSE` file (MIT), matching the license the README already declared. -- README: "This repo vs. hosted AntiLink" comparison, an "Official links" table - (website, dashboard, docs, status, invite, support, privacy, terms), and an - "Add AntiLink 2.0 to Discord" link to the hosted bot (`invite.antil.ink`, - Discord App Directory app `1280137058458927134`). + +- **`packages/core`** - the detection and policy engine. URL/invite + extraction with de-obfuscation (zero-width characters, `hxxp://`, + `example[.]com` defanging, markdown links), classification (domain + allow/blocklists, a guild-suppliable known-phishing list, URL shorteners, + punycode hostnames, Latin/Cyrillic/Greek homoglyph detection, custom + regex rules), and a policy engine resolving each message to an + `ALLOW`/`WARN`/`BLOCK`/`QUARANTINE` verdict and a + `NONE`/`LOG`/`WARN`/`DELETE`/`TIMEOUT` action via a + `log`/`warn`/`delete`/`timeout` enforcement mode ladder. No known-phishing + domains are hardcoded - that data is always guild- or operator-supplied. +- **`packages/storage`** - `MemoryStorageAdapter`, `SqliteStorageAdapter` + (the self-hosting default), `MysqlStorageAdapter`, and + `PostgresStorageAdapter`, all implementing one `StorageAdapter` interface + and validated by a shared contract test suite. Config export/import + bundles (`exportGuildConfigBundle`/`importGuildConfigBundle`/ + `parseConfigBundle`, zod-validated) back up or restore a guild's full + configuration as JSON. +- **`packages/discord-bot`** - the discord.js v14 adapter: `/antilink` + (status/enable/disable/mode), `/allowlist`, `/blocklist`, `/invites` + (allow/block-all), `/logs set-channel`, `/testlink`, and `/config` + (export/import). The message pipeline ignores bots and DMs, checks + discord.js's own permission signals (`message.deletable`, + `member.moderatable`) before ever deleting or timing out, records + metadata-only audit log entries (no message content field exists on the + type), posts mod-log embeds, and rate-limits enforcement actions per + guild via a token-bucket limiter so a spam wave can't drive the bot into + Discord's own API rate limits. `/config import` always overwrites the + imported bundle's guild ID with the guild the command was run in, so an + import can never write into a different server's data. +- **`packages/cli`** (`antilink`) - `scan`, `test-url`, `init`, + `export-config`/`import-config`, and `doctor` (offline health checks: + Node version, `.env` presence, config file validity, the better-sqlite3 + native binding, database directory write access). Runs the exact same + detection engine as the bot, entirely offline. +- **`apps/example-bot`** - a working self-hosted bot assembled from the + published packages, with a `DATABASE_DRIVER` switch (sqlite/mysql/postgres) + that's actually implemented, a command-registration script, and Docker + support (non-root user, persistent volume for SQLite). +- **`apps/dashboard-lite`** - a dependency-free (`node:http`, no framework), + local, explicitly unauthenticated read-only dashboard: bot status, guild + config, allowlist/blocklist, and recent audit log entries. +- Root `docker-compose.yml` for the example bot. +- 9 documentation pages under `docs/`: getting-started, configuration, + rules-engine, discord-setup, privacy, self-hosting, threat-model, + api-reference, migration-from-old-antilink. +- `GOVERNANCE.md` and `ROADMAP.md`. +- CI (`.github/workflows/ci.yml`) rebuilt for the pnpm monorepo: install, + build (topologically ordered), typecheck, lint, format check, and test on + Node 20 and 22, with live PostgreSQL and MySQL service containers so + every storage adapter's contract tests run for real in CI. +- `.github/workflows/codeql.yml` - static analysis on push, pull request, + and a weekly schedule. +- A `docker` ecosystem entry in `.github/dependabot.yml` for the example + bot's base image. +- 212 tests across the workspace (unit tests for the detection/policy + engine and CLI; a shared adapter contract suite run against all four + storage backends, including live tests against a real PostgreSQL + instance; discord-bot logic tested against structural fakes of + discord.js's Message/Interaction/Client, since there is no live-Discord + integration testing without a real bot token and gateway connection; + real HTTP integration tests for dashboard-lite's server). ### Changed -- Revived and re-positioned the project as the open-source edition of the - AntiLink platform. -- README now clearly separates this self-hostable open-source bot from the - **hosted AntiLink 2.0** platform, sourced from the official docs. Replaced the - earlier placeholder platform names (Security/Analytics/Billing/AI) with the - real hosted product set (dashboard, Member Defense, Verify, Automod, Honeypot, - Emergency Lockdown, custom bot, Free/Premium/AntiLink Premium plans), all - marked as a **separate hosted product** and not shipped in this repository. -- **Breaking:** upgraded from discord.js v13 to **v14** and ported `index.js` - to the v14 API (`GatewayIntentBits`, `Events`), including the now-required - `MessageContent` privileged intent. -- **Breaking:** moved the hardcoded whitelisted-channel and bypass-role IDs out - of the source into the `WHITELISTED_CHANNEL_IDS` and `IGNORED_ROLE_IDS` - environment variables (comma-separated lists). -- `WEBHOOK_URL` is now optional — without it the bot still filters links and - simply skips webhook logging; a single `WebhookClient` is reused instead of - being constructed for every deletion. - -### Fixed -- Messages from bots/webhooks and DMs are now ignored, and system messages with - no member object no longer crash the role-bypass check. + +- **Breaking, in every sense** - the single `index.js` file is gone. Its + hardcoded whitelisted-channel/ignored-role arrays and `WEBHOOK_URL` + webhook logging are replaced by per-guild database-backed configuration + and `/logs set-channel`. See + [`docs/migration-from-old-antilink.md`](./docs/migration-from-old-antilink.md). +- Tooling: npm → pnpm, plain JS → strict TypeScript, a single package.json → + a pnpm workspace of 4 packages and 2 apps, `node index.js` → a build step + required before running anything (packages resolve each other via built + `dist/` output). +- `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, the PR template, + and the issue templates were rewritten for the new monorepo and Node 20+; + stale references to an unrelated hosted product's support/docs links were + removed (`CODE_OF_CONDUCT.md`'s enforcement contact, the issue template + config). +- `package.json`'s `homepage` field now points at this repository instead + of an unrelated hosted product's website. + +### Removed + +- The old `index.js`, its npm scripts, and its env vars (`WEBHOOK_URL`, + `WHITELISTED_CHANNEL_IDS`, `IGNORED_ROLE_IDS`) - see "Changed" above for + what replaces each one. --- -## [0.1.0] - 2024-04-14 +## [0.1.0-legacy] - 2024-04-14 + +The original `Anti-Links-Discord-Bot`, before the AntiLink Guard OSS +rewrite above. Kept here for historical record and for anyone migrating +from it. - Initial anti-link Discord bot: automatic link detection and removal in configured channels, channel whitelisting, role-based bypass, and webhook notifications. -[Unreleased]: https://github.com/timeout187/Anti-Links-Discord-Bot/compare/v0.1.0...HEAD -[0.1.0]: https://github.com/timeout187/Anti-Links-Discord-Bot/releases/tag/v0.1.0 +[Unreleased]: https://github.com/timeout187/antilink-guard/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/timeout187/antilink-guard/releases/tag/v0.1.0 +[0.1.0-legacy]: https://github.com/timeout187/antilink-guard/releases/tag/v0.1.0-legacy diff --git a/README.md b/README.md index 29da133..f622693 100644 --- a/README.md +++ b/README.md @@ -1,307 +1,216 @@
-AntiLink logo — a blue shield with a crossed link +# AntiLink Guard OSS -# AntiLink - -**Open-source Discord link moderation for communities that need clean, safe chat.** +**Open-source Discord anti-phishing and link moderation framework.** [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE) -[![Node.js](https://img.shields.io/badge/node-%3E%3D18-green.svg)](https://nodejs.org) +[![Node.js](https://img.shields.io/badge/node-%3E%3D20-green.svg)](https://nodejs.org) [![discord.js](https://img.shields.io/badge/discord.js-v14-5865F2.svg)](https://discord.js.org) -[![CI](https://github.com/timeout187/Anti-Links-Discord-Bot/actions/workflows/ci.yml/badge.svg)](https://github.com/timeout187/Anti-Links-Discord-Bot/actions/workflows/ci.yml) +[![pnpm](https://img.shields.io/badge/pnpm-workspace-F69220.svg)](https://pnpm.io) +[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6.svg)](https://www.typescriptlang.org) +[![CI](https://github.com/timeout187/antilink-guard/actions/workflows/ci.yml/badge.svg)](https://github.com/timeout187/antilink-guard/actions/workflows/ci.yml) +[![CodeQL](https://github.com/timeout187/antilink-guard/actions/workflows/codeql.yml/badge.svg)](https://github.com/timeout187/antilink-guard/actions/workflows/codeql.yml) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING.md) -[![Code of Conduct](https://img.shields.io/badge/Contributor%20Covenant-2.1-purple.svg)](./CODE_OF_CONDUCT.md) - -[Website][website] · [Documentation][docs] · [Dashboard][dashboard] · [Status][status] · [Discord][discord] · [Report a Bug][issues] - -**Want the fully-featured hosted bot instead of self-hosting?** -[![Add AntiLink to Discord](https://img.shields.io/badge/Add%20AntiLink%202.0-to%20Discord-5865F2?logo=discord&logoColor=white)][invite]
--- -> **Note on scope.** This repository is the original, **self-hostable open-source** AntiLink bot. It does automatic link filtering and nothing else. The **hosted AntiLink platform** ([AntiLink 2.0][invite]) is a separate, much larger product — dashboard, paid plans, raid defense, verification, and more — and **none of those features are in this repository**. See [This repo vs. hosted AntiLink](#this-repo-vs-hosted-antilink) for the difference, and the [Roadmap](#roadmap) for what the hosted platform offers. Anything marked *Planned* or *hosted* does not ship here. - -## Table of Contents +## The problem -- [Introduction](#introduction) -- [Features](#features) -- [This repo vs. hosted AntiLink](#this-repo-vs-hosted-antilink) -- [Official links](#official-links) -- [Architecture](#architecture) -- [Installation](#installation) -- [Quick Start](#quick-start) -- [Commands](#commands) -- [Configuration](#configuration) -- [Screenshots](#screenshots) -- [Roadmap](#roadmap) -- [Contributing](#contributing) -- [Security](#security) -- [License](#license) -- [Support](#support) -- [Documentation](#documentation) -- [FAQ](#faq) +Discord communities get hit with the same handful of attacks constantly: +phishing links dressed up as free-Nitro giveaways, scam Discord invites, +punycode/homoglyph domains that look identical to legitimate ones at a +glance, and links deliberately obfuscated (`hxxps://`, `example[.]com`, +zero-width characters) to slip past naive keyword filters. Most +self-hostable anti-link bots do a plain `.includes("http")` check and call +it done. -## Introduction - -**AntiLink** is a lightweight, self-hostable Discord bot that automatically detects and removes messages containing links in the channels you choose. It was originally built to keep a busy community's general chat free of unwanted and malicious links, and it is the open-source origin of the wider **AntiLink** moderation platform. - -The open-source bot is intentionally small and easy to audit: you can read the whole thing, host it yourself, and know exactly what it does with your server's messages. - -If you'd rather not self-host, the same team runs a fully-featured **hosted** bot — **AntiLink 2.0** — with a web dashboard, invite/phishing/raid protection, verification, and more. You can [add it to your server in one click][invite]; it's a separate product from this repository (see [below](#this-repo-vs-hosted-antilink)). +**AntiLink Guard OSS** is a real detection and policy engine, not a regex +one-liner - and it's yours to run, read, and modify. ## Features -Currently implemented in this repository: - -- 🔗 **Automatic link detection** — scans messages in configured channels and removes those containing links. -- ✅ **Channel whitelisting** — designate channels where links are always allowed. -- 🛡️ **Role-based bypass** — members with configured roles (e.g. staff/mods) are exempt from filtering. -- 📣 **Webhook notifications** — report removals/actions to a channel via a Discord webhook (`WEBHOOK_URL`). -- 🪶 **Minimal footprint** — plain Node.js + discord.js, no database required to get started. - -> If you spot a discrepancy between this list and the actual code, it's a bug in the docs — please [open an issue][issues]. We keep this section limited to behavior that actually ships. - -## This repo vs. hosted AntiLink - -There are two ways to run AntiLink. **This repository is the first one.** - -| | **This repo** (open-source) | **AntiLink 2.0** ([hosted][invite]) | -| --- | --- | --- | -| How you run it | Self-host the Node.js bot yourself | Invite the hosted bot — nothing to host | -| Link filtering | ✅ Automatic `http(s)://` removal | ✅ Invites, external links, phishing, risk scoring | -| Config | Env vars in `.env` | Slash commands + [web dashboard][dashboard] | -| Whitelist | Channels & roles (via env) | Users, roles, channels, categories, domains | -| Slash commands | ❌ (none) | ✅ `/setup`, `/config`, `/whitelist`, `/guard`, … | -| Raid / member defense | ❌ | ✅ Member Defense, Honeypot, Emergency Lockdown | -| Verification, Automod | ❌ | ✅ Verify gate + Automod presets | -| Cost | Free (MIT) | Free tier + paid plans | - -This README documents **only the open-source bot**. For everything in the right-hand column, see the [hosted docs][docs] or [add AntiLink 2.0 to your server][invite]. - -## Official links - -| | | -| --- | --- | -| 🌐 Website | [antil.ink][website] | -| 🛡️ Dashboard | [dashboard.antil.ink][dashboard] | -| 📖 Documentation | [docs.antil.ink][docs] | -| 📈 Status | [status.antil.ink][status] | -| ➕ Add AntiLink 2.0 (hosted) | [invite.antil.ink][invite] | -| 💬 Support & community | [support.antil.ink][discord] | -| 🔒 Privacy Policy | [docs.antil.ink/docs/legal/privacy](https://docs.antil.ink/docs/legal/privacy) | -| 📄 Terms of Service | [docs.antil.ink/docs/legal/terms](https://docs.antil.ink/docs/legal/terms) | - -> **Only trust the domains listed above.** AntiLink will never DM you asking for your password, token, or payment details. +Everything below is implemented and tested in this repository: + +- 🔗 **Real link detection** - not just `https?://`. Catches markdown + links, bare `www.` domains, Discord invites, and links deliberately + obfuscated with `hxxp://`, `example[.]com`, or zero-width characters. +- 🕵️ **Classification, not just matching** - domain allow/blocklists, a + guild-suppliable known-phishing list, common URL shorteners, punycode + hostnames, and Latin/Cyrillic/Greek homoglyph mixing, plus your own + custom regex rules. +- ⚖️ **A real policy engine** - every message gets a score and one of four + verdicts (`ALLOW`/`WARN`/`BLOCK`/`QUARANTINE`), mapped to an action + through a `log → warn → delete → timeout` enforcement ladder you control + per server. +- 🔐 **Permission-safe by construction** - the bot checks its own Discord + permissions before every delete/timeout and fails safe (logs and skips) + rather than crashing or assuming success. +- 🗄️ **Your database, your choice** - memory (for tests), SQLite (the + self-hosting default), MySQL, or PostgreSQL, all behind one interface. +- 📊 **Metadata-only audit logging** - the audit log type has no field + capable of holding message content, by design. See + [`docs/privacy.md`](./docs/privacy.md). +- 🧰 **A real CLI** - `antilink scan`/`test-url` run the exact same engine + the bot uses, entirely offline, no Discord connection required. +- 🖥️ **A minimal local dashboard** - read-only, unauthenticated by design, + meant for `localhost` (see [`apps/dashboard-lite`](./apps/dashboard-lite)). + +No hardcoded "known phishing domain" database is bundled anywhere in this +codebase - that data is always something you (or a feed you choose) supply. +See [`docs/rules-engine.md`](./docs/rules-engine.md#why-knownphishingdomains-is-always-empty-out-of-the-box). ## Architecture -The open-source bot follows a simple, single-process design: - +```mermaid +flowchart TD + subgraph Discord + MSG[messageCreate event] + CMD[Slash command interaction] + end + + subgraph "@antilink-guard/discord-bot" + MSG --> PIPE[Moderation pipeline] + CMD --> ROUTE[Command router] + PIPE --> ENFORCE[enforce.ts
permission-gated delete/timeout] + ENFORCE --> MODLOG[Mod-log embed] + end + + subgraph "@antilink-guard/core" + EXTRACT[extractLinks
de-obfuscation] + CLASSIFY[classifyLink
scoring] + POLICY[evaluateMessage
verdict + action] + EXTRACT --> CLASSIFY --> POLICY + end + + PIPE --> EXTRACT + ROUTE --> POLICY + + subgraph "@antilink-guard/storage" + DB[(SQLite / MySQL / PostgreSQL)] + end + + POLICY <--> DB + ENFORCE --> DB + + CLI["@antilink-guard/cli
(scan / test-url / doctor)"] --> EXTRACT + DASH["apps/dashboard-lite
(local, read-only)"] --> DB ``` -┌─────────────────────────────────────────────┐ -│ Discord Gateway │ -└───────────────────────┬─────────────────────┘ - │ message events - ▼ -┌─────────────────────────────────────────────┐ -│ AntiLink Bot │ -│ │ -│ ┌────────────┐ ┌───────────────────────┐ │ -│ │ Config / │──▶│ Message Handler │ │ -│ │ .env │ │ (link detection, │ │ -│ └────────────┘ │ whitelist + bypass) │ │ -│ └───────────┬───────────┘ │ -│ │ action │ -│ ▼ │ -│ ┌───────────────────────┐ │ -│ │ Moderation actions │ │ -│ │ (delete message) │ │ -│ └───────────┬───────────┘ │ -│ │ notify │ -│ ▼ │ -│ ┌───────────────────────┐ │ -│ │ Webhook logger │ │ -│ └───────────────────────┘ │ -└─────────────────────────────────────────────┘ -``` - -Slash-command administration is *Planned* (see [Roadmap](#roadmap)); today the -bot is driven entirely by the message-event path shown above plus environment -configuration. - -**Design principles** - -- **Self-contained.** Runs as a single Node.js process; no external database required for core filtering. -- **Auditable.** Small enough to read end-to-end before you trust it in your server. -- **Configuration over code.** Secrets live in `.env`; behavior is driven by configuration (see [Configuration](#configuration)). - -> The commercial AntiLink platform uses a different, service-oriented architecture (dashboard, API, and proprietary detection) that is **not** included here. -## Installation +`packages/core` has no dependency on Discord or on any storage backend - +it's a pure detection/policy library you can use standalone (the CLI does +exactly that). See [`docs/rules-engine.md`](./docs/rules-engine.md) for the +full pipeline and [`docs/api-reference.md`](./docs/api-reference.md) for +every package's exports. -### Prerequisites - -- [Node.js](https://nodejs.org) **18 or newer** and npm -- A Discord bot application and token — see [Discord's developer docs](https://discord.com/developers/docs/quick-start/getting-started) -- The bot must be invited to your server with permission to **Read Messages/View Channels**, **Manage Messages**, and (if you use webhook logging) a webhook in your log channel - -### Steps +## Quick start ```bash -# 1. Clone the repository -git clone https://github.com/timeout187/Anti-Links-Discord-Bot.git -cd Anti-Links-Discord-Bot +git clone https://github.com/timeout187/antilink-guard.git +cd antilink-guard +pnpm install +pnpm run build -# 2. Install dependencies -npm install +cp apps/example-bot/.env.example apps/example-bot/.env +# edit apps/example-bot/.env: DISCORD_TOKEN, DISCORD_CLIENT_ID -# 3. Create your environment file -cp .env.example .env -# then edit .env and fill in your values - -# 4. Start the bot -npm start +pnpm --filter @antilink-guard/example-bot run register-commands +pnpm --filter @antilink-guard/example-bot run start ``` -## Quick Start - -1. **Create a bot** in the [Discord Developer Portal](https://discord.com/developers/applications), copy its **token**, and enable the **Message Content Intent** under *Bot → Privileged Gateway Intents*. -2. **Invite the bot** to your server with the permissions listed above. -3. **Create a webhook** in the channel where you want moderation logs, and copy its URL (optional but recommended). -4. Fill in `.env`, including your whitelisted channels and bypass roles (see [Configuration](#configuration)): - - ```dotenv - DISCORD_TOKEN=your-bot-token-here - WEBHOOK_URL=https://discord.com/api/webhooks/xxx/yyy - WHITELISTED_CHANNEL_IDS=123456789012345678,234567890123456789 - IGNORED_ROLE_IDS=345678901234567890 - ``` - -5. Run `npm start`. Post a link in a filtered channel from a non-exempt account to confirm it's removed. - -## Commands +Full walkthrough (Discord Developer Portal setup, required permissions, +Message Content Intent): [`docs/getting-started.md`](./docs/getting-started.md). -The community edition currently has **no slash commands** — it runs entirely as -**automatic message filtering**, configured through environment variables. +### Docker Compose -| Capability | Status | Description | -| ---------- | ------ | ----------- | -| Automatic link filtering | ✅ Available | Deletes any message containing an `http(s)://` link, unless the channel is whitelisted or the author has a bypass role. Runs continuously; no command needed. | -| Webhook moderation log | ✅ Available | Posts a note to your log webhook each time a message is removed. | -| `/antilink …` slash commands | 🗓️ Planned here | This open-source bot has no slash commands yet (on the [Roadmap](#roadmap)). The **hosted [AntiLink 2.0][invite]** bot already has a full slash-command suite (`/setup`, `/config`, `/whitelist`, `/guard`, …) — see the [hosted docs][docs]. | +```bash +cp apps/example-bot/.env.example apps/example-bot/.env +docker compose up --build -d +``` +Builds the whole workspace inside the container and runs the bot under a +non-root user, with a persistent volume for its SQLite database. See +[`docs/self-hosting.md`](./docs/self-hosting.md) for switching to MySQL or +PostgreSQL instead. -## Configuration +## Usage -Secrets are provided through environment variables (`.env`). Copy `.env.example` to `.env` and fill in: +``` +/antilink enable +/antilink mode block -| Variable | Required | Description | -| -------- | -------- | ----------- | -| `DISCORD_TOKEN` | ✅ | Your Discord bot token. | -| `WEBHOOK_URL` | Optional | Discord webhook URL for moderation logs. Omit to disable webhook logging. | -| `WHITELISTED_CHANNEL_IDS` | Optional | Comma-separated channel IDs where links are always allowed. | -| `IGNORED_ROLE_IDS` | Optional | Comma-separated role IDs whose members bypass link filtering. | +/allowlist add domain:cdn.example.com +/blocklist add domain:free-nitro-gift.ru -> **Never commit your `.env` file or bot token.** `.gitignore` already excludes `.env`. If a token is ever exposed, regenerate it immediately in the Developer Portal. +/invites block-all enabled:true +/invites allow invite:https://discord.gg/your-partner-server -## Screenshots +/logs set-channel channel:#mod-log -_Coming soon — captures of a link removal and the webhook moderation log will live in `docs/screenshots/`._ +/testlink url:https://xn--e1aybc.xn--p1ai +/config export +``` -## Roadmap +Every admin command requires **Manage Server**; `/testlink` is open to +everyone. Full reference: [`docs/configuration.md`](./docs/configuration.md). -Direction for **this open-source bot**. Unchecked items are not shipped here yet. +Prefer the command line? The exact same engine, no Discord required: -- [x] Migrate configuration out of code into an environment-driven setup -- [ ] Slash-command administration (`/antilink …`) -- [ ] Configurable actions beyond deletion (warn, timeout, escalate) -- [ ] Per-guild settings with optional persistence -- [ ] Structured logging + optional webhook embeds -- [ ] Test suite and typed codebase +```bash +antilink scan "check out hxxps://free-nitro[.]ru/claim" +antilink test-url https://bit.ly/abc123 +antilink doctor +``` -### The hosted AntiLink platform +## Privacy & security -Many of the above already exist — and much more — in the **hosted AntiLink 2.0** bot, which is a **separate product, not part of this repository**. It is not on this repo's roadmap; it's listed so you know what's available if you'd rather not self-host. [Add it to your server][invite] or read the [hosted docs][docs]. +- **No telemetry.** This project doesn't phone home anywhere. +- **Metadata-only audit logs** - see [`docs/privacy.md`](./docs/privacy.md) + for exactly what's stored (and what deliberately isn't). +- **Threat model**: [`docs/threat-model.md`](./docs/threat-model.md) - what + this framework does and doesn't defend against. +- **Reporting a vulnerability**: see [`SECURITY.md`](./SECURITY.md) - + please don't open a public issue for security problems. -- 🛡️ **[Web dashboard][dashboard]** — manage protection, whitelists, and logs from the browser -- 🔗 **Advanced link protection** — Discord invites, external links, phishing & risk scoring, enforcement modes (delete / quarantine / warn) -- 🧰 **Full slash-command suite** — `/setup`, `/config`, `/whitelist`, `/checklink`, `/language`, and more -- 🚨 **Member Defense** — raid detection, account-age screening, quarantine automation *(AntiLink Premium)* -- 🍯 **Honeypot trap channels** & **Emergency Lockdown** -- ✅ **Verify** — one-click member verification gate -- 🤖 **Automod** — preset spam / bad-words / caps / mention filters -- 🎭 **Custom bot** — run AntiLink under your own name & avatar *(AntiLink Premium)* -- 💳 **Plans** — Free, Premium, and AntiLink Premium (billing via Paddle) +## Repository layout -Have an idea for the **open-source bot**? [Open a feature request][issues]. +``` +apps/ + example-bot/ a working self-hosted bot built on the packages below + dashboard-lite/ a local, read-only dashboard +packages/ + core/ URL/invite extraction, classification, policy engine + storage/ memory / SQLite / MySQL / PostgreSQL adapters + discord-bot/ the discord.js v14 adapter + moderation pipeline + cli/ the `antilink` command-line tool +docs/ the guides linked throughout this README +``` ## Contributing -Contributions are welcome and appreciated. Please read **[CONTRIBUTING.md](./CONTRIBUTING.md)** and our **[Code of Conduct](./CODE_OF_CONDUCT.md)** before you start. - -The short version: - -1. Fork the repo and create a branch from `main`. -2. Make your change with clear, focused commits. -3. Run linting locally before pushing. -4. Open a pull request using the template and describe what and why. - -Good first contributions: documentation fixes, config externalization, tests, and the *Planned* roadmap items above. +Contributions are welcome - see [`CONTRIBUTING.md`](./CONTRIBUTING.md) for +the development setup (pnpm workspace, build order, testing) and +[`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md). Governance model: +[`GOVERNANCE.md`](./GOVERNANCE.md). -## Security +## Roadmap -Please **do not** report security vulnerabilities through public issues. See **[SECURITY.md](./SECURITY.md)** for how to report responsibly. +See [`ROADMAP.md`](./ROADMAP.md) for what's shipped, what's next, and what's +explicitly out of scope for this project. ## License -Distributed under the **MIT License**. See [LICENSE](./LICENSE) for details. - -## Support - -- 💬 **Community help:** [AntiLink Discord][discord] -- 🐛 **Bugs & features:** [GitHub Issues][issues] -- 📖 **Guides:** [Documentation][docs] +[MIT](./LICENSE). -## Documentation +## Background -This README is the complete guide to the **self-hosted open-source bot**. The documentation site at **[docs.antil.ink][docs]** covers the **hosted AntiLink 2.0** platform — dashboard, slash commands, plans, and the security suite — which is a separate product from this repository. - -## FAQ - -**Is AntiLink free?** -The bot in this repository is free and open-source under the MIT license. Hosted/commercial platform products are separate. - -**Do I need a database?** -No. Core link filtering runs without one. Persistence is only needed for *Planned* features like per-guild settings. - -**Why are my links not being deleted?** -Check that (1) the **Message Content Intent** is enabled, (2) the bot has **Manage Messages** in that channel, (3) the channel isn't whitelisted, and (4) your account doesn't hold a bypass role. - -**Does it work with the latest discord.js?** -The bot targets discord.js v14 and Node.js 18+. If you hit a version issue, please [open an issue][issues]. - -**What's the difference between this and "AntiLink 2.0"?** -This repo is the original, self-hostable open-source bot — automatic link filtering, configured with env vars. **AntiLink 2.0** is the separate **hosted** product with a dashboard, slash commands, raid defense, verification, and paid plans. [Add it to your server][invite] or see the [comparison](#this-repo-vs-hosted-antilink). - -**Can I use this commercially?** -Yes — MIT permits commercial use. You're responsible for your own hosting and compliance. - ---- - -
- -Built and maintained by the AntiLink community. Star ⭐ the repo if it's useful! - -
+AntiLink Guard OSS is based on lessons learned from running AntiLink, a +Discord moderation bot used across 100+ servers. This repository extracts +the reusable moderation, URL detection, and policy engine into a +self-hostable open-source toolkit for the community. - -[website]: https://antil.ink "AntiLink website" -[docs]: https://docs.antil.ink/ "AntiLink documentation" -[dashboard]: https://dashboard.antil.ink "AntiLink web dashboard (hosted)" -[status]: https://status.antil.ink "AntiLink status page" -[invite]: https://invite.antil.ink "Add the hosted AntiLink 2.0 bot to your server" -[discord]: https://support.antil.ink/ "AntiLink support & community Discord" -[issues]: https://github.com/timeout187/Anti-Links-Discord-Bot/issues +This repository contains **only** the open-source framework described +above. It is not affiliated with, and does not include, any hosted or +commercial product that happens to share part of its name. diff --git a/docs/BRANDING.md b/docs/BRANDING.md index f849343..6fa5f44 100644 --- a/docs/BRANDING.md +++ b/docs/BRANDING.md @@ -13,36 +13,36 @@ blocked links and server protection. ## Color Palette -| Role | Hex | RGB | Preview | -| ---- | --- | --- | ------- | -| **AntiLink Blue** (primary) | `#00A3F0` | `0, 163, 240` | ![#00A3F0](https://placehold.co/40x20/00A3F0/00A3F0.png) | -| **Blue Shadow** (shaded face) | `#0080C1` | `0, 128, 193` | ![#0080C1](https://placehold.co/40x20/0080C1/0080C1.png) | -| **Blue Highlight** (bevel) | `#25B0F1` | `37, 176, 241` | ![#25B0F1](https://placehold.co/40x20/25B0F1/25B0F1.png) | -| **Blue Tint** (light bevel) | `#74CBF4` | `116, 203, 244` | ![#74CBF4](https://placehold.co/40x20/74CBF4/74CBF4.png) | -| **White** (icon / outline) | `#FFFFFF` | `255, 255, 255` | ![#FFFFFF](https://placehold.co/40x20/FFFFFF/FFFFFF.png) | +| Role | Hex | RGB | Preview | +| ----------------------------- | --------- | --------------- | -------------------------------------------------------- | +| **AntiLink Blue** (primary) | `#00A3F0` | `0, 163, 240` | ![#00A3F0](https://placehold.co/40x20/00A3F0/00A3F0.png) | +| **Blue Shadow** (shaded face) | `#0080C1` | `0, 128, 193` | ![#0080C1](https://placehold.co/40x20/0080C1/0080C1.png) | +| **Blue Highlight** (bevel) | `#25B0F1` | `37, 176, 241` | ![#25B0F1](https://placehold.co/40x20/25B0F1/25B0F1.png) | +| **Blue Tint** (light bevel) | `#74CBF4` | `116, 203, 244` | ![#74CBF4](https://placehold.co/40x20/74CBF4/74CBF4.png) | +| **White** (icon / outline) | `#FFFFFF` | `255, 255, 255` | ![#FFFFFF](https://placehold.co/40x20/FFFFFF/FFFFFF.png) | ### Suggested neutrals (for docs/site UI) -| Role | Hex | -| ---- | --- | -| Ink / text | `#0B1B2B` | -| Muted text | `#5A6B7B` | +| Role | Hex | +| -------------------- | --------- | +| Ink / text | `#0B1B2B` | +| Muted text | `#5A6B7B` | | Surface / background | `#FFFFFF` | -| Subtle border | `#E3ECF3` | +| Subtle border | `#E3ECF3` | ## Design Tokens ```css :root { - --antilink-blue: #00A3F0; - --antilink-blue-shadow: #0080C1; - --antilink-blue-highlight: #25B0F1; - --antilink-blue-tint: #74CBF4; - --antilink-white: #FFFFFF; + --antilink-blue: #00a3f0; + --antilink-blue-shadow: #0080c1; + --antilink-blue-highlight: #25b0f1; + --antilink-blue-tint: #74cbf4; + --antilink-white: #ffffff; - --antilink-ink: #0B1B2B; - --antilink-muted: #5A6B7B; - --antilink-border: #E3ECF3; + --antilink-ink: #0b1b2b; + --antilink-muted: #5a6b7b; + --antilink-border: #e3ecf3; } ``` diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..5c054a5 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,128 @@ +# API Reference + +The public exports of each package, as actually declared in each +package's `src/index.ts`. If you're building your own bot on top of this +framework instead of using `apps/example-bot` directly, this is what you +import. + +## `@antilink-guard/core` + +No Discord or storage dependency - pure detection/policy logic. + +```ts +import { + evaluateMessage, + extractLinks, + classifyLink, + normalizeUrl, + stripZeroWidthChars, + undefangText, + isPunycodeHostname, + hasMixedScriptConfusables, + REASON_SEVERITY, + VERDICT_THRESHOLDS, + DEFAULT_URL_SHORTENER_DOMAINS, +} from '@antilink-guard/core'; +``` + +| Export | Signature | What it does | +| -------------------------------------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| `evaluateMessage` | `(rawText: string, context: ScanContext, policy: PolicyConfig) => ScanResult` | The full pipeline: bypasses → extraction → classification → verdict/action | +| `extractLinks` | `(rawText: string) => ExtractedLink[]` | Finds and de-obfuscates links/invites in text | +| `classifyLink` | `(link: ExtractedLink, policy: PolicyConfig, rawMessageText: string) => LinkClassification` | Scores a single already-extracted link | +| `normalizeUrl` | `(rawUrl: string) => { url: string; hostname: string } \| undefined` | Lowercases the host, strips tracking params, returns `undefined` for unparseable input | +| `isPunycodeHostname` / `hasMixedScriptConfusables` | `(hostname: string) => boolean` | The two homoglyph-related checks, individually | +| `REASON_SEVERITY` | `Record` | The severity table - see [rules-engine.md](./rules-engine.md) | +| `VERDICT_THRESHOLDS` | `{ warn: 1, block: 30, quarantine: 60 }` | The score cutoffs for each verdict | + +Key types: `Verdict`, `DetectionReason`, `EnforcementMode`, +`ModerationActionType`, `ExtractedLink`, `LinkClassification`, +`RegexRule`, `ChannelRule`, `PolicyConfig`, `ScanContext`, `ScanResult`. + +```ts +const result = evaluateMessage( + message.content, + { guildId, channelId, authorId, authorRoleIds: [], mentionCount: 0 }, + policy, +); +// result.verdict, result.action, result.score, result.reasons, result.matchedLinks +``` + +## `@antilink-guard/storage` + +```ts +import { + MemoryStorageAdapter, + SqliteStorageAdapter, + MysqlStorageAdapter, + PostgresStorageAdapter, + createDefaultGuildConfig, + exportGuildConfigBundle, + importGuildConfigBundle, + parseConfigBundle, + configBundleSchema, +} from '@antilink-guard/storage'; +``` + +All four adapter classes implement the same `StorageAdapter` interface: +`init()`, `close()`, `getGuildConfig`/`upsertGuildConfig`, +`list/add/removeAllowlistEntry`, `list/add/removeBlocklistEntry`, +`list/add/removeInviteRule`, `add/listAuditLogEntries`, +`add/listScanResults`, `add/listModerationActions`. + +```ts +const storage = new SqliteStorageAdapter({ filename: './antilink.sqlite' }); +await storage.init(); +const config = (await storage.getGuildConfig(guildId)) ?? createDefaultGuildConfig(guildId); +``` + +Config bundle helpers (used by both `/config export`/`import` and the CLI): + +```ts +const bundle = await exportGuildConfigBundle(storage, guildId); +const validated = parseConfigBundle(JSON.parse(fileContents)); // throws on invalid shape +await importGuildConfigBundle(storage, validated); // replaceExisting: true by default +``` + +Data model types: `GuildConfig`, `AllowlistEntry`, `BlocklistEntry`, +`InviteRule`, `AuditLogEntry`, `ScanResultRecord`, `ModerationActionRecord`. + +## `@antilink-guard/discord-bot` + +```ts +import { createBot, registerCommands, TokenBucketRateLimiter } from '@antilink-guard/discord-bot'; + +const client = createBot({ storage }); // wires up messageCreate + interactionCreate +await client.login(token); +``` + +| Export | Purpose | +| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| `createBot(options: CreateBotOptions) => Client` | Builds a ready-to-login discord.js `Client` with the moderation pipeline and command routing already attached | +| `registerCommands(options: RegisterCommandsOptions) => Promise` | Registers all slash commands via the Discord REST API, guild-scoped or global | +| `handleMessageCreate` / `handleInteractionCreate` | The two event handlers `createBot` wires up, exported individually if you want to compose your own `Client` | +| `enforceScanResult` | Applies a `ScanResult` to a message: permission-gated delete/timeout, audit log + moderation action recording | +| `buildPolicyConfig(inputs: PolicyInputs) => PolicyConfig` | Maps stored `GuildConfig` + lists into the shape `evaluateMessage` expects | +| `buildModLogEmbed` / `sendModLog` | Build/send the mod-log embed | +| `TokenBucketRateLimiter` | The generic rate limiter used to cap moderation actions | +| `allCommands`, `commandsByName`, `getCommandJSONBodies()` | The slash command registry, if you want to register a subset or inspect the JSON payloads | + +## `@antilink-guard/cli` + +Primarily a command-line tool (`antilink scan|test-url|init|export-config|import-config|doctor` - +see [getting-started.md](./getting-started.md#trying-the-cli-without-any-discord-setup-at-all)), +not intended to be imported as a library. `createProgram()` is exported from +`src/index.ts` if you need to embed its commander.js program elsewhere. + +## Dashboard-lite's HTTP API + +`apps/dashboard-lite` exposes a small local JSON API consumed by its own +frontend (see [privacy.md](./privacy.md) for why it has no auth): + +| Route | Returns | +| --------------------------------------------- | ----------------------------------------------------------------------------- | +| `GET /api/guild/:guildId/status` | `{ guildConfig, counts: { allowlist, blocklist, inviteRules }, lastAuditAt }` | +| `GET /api/guild/:guildId/config` | The raw `GuildConfig` | +| `GET /api/guild/:guildId/allowlist` | `AllowlistEntry[]` | +| `GET /api/guild/:guildId/blocklist` | `BlocklistEntry[]` | +| `GET /api/guild/:guildId/audit-logs?limit=50` | `AuditLogEntry[]`, newest first | diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..0fa18a1 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,97 @@ +# Configuration + +There are **two separate configuration surfaces** in this project. Knowing +which one you're touching matters: + +| | Per-guild bot configuration | Local policy file | +| ----------- | ------------------------------------------------ | ------------------------------------------- | +| Used by | `packages/discord-bot` at runtime (the live bot) | `packages/cli`'s `scan`/`test-url` commands | +| Stored in | Your database (`packages/storage`) | A local `antilink.config.json` file | +| Changed via | Slash commands | Editing the file directly | +| Scope | One Discord server (guild) | Whatever you're running the CLI against | + +## Per-guild bot configuration + +This is what actually governs moderation in your Discord server. It's the +`GuildConfig` model in `packages/storage`, plus three related lists +(allowlist, blocklist, invite rules), all keyed by guild ID. + +| Field | Slash command | Default | +| ---------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------- | +| `enabled` | `/antilink enable` / `/antilink disable` | `true` | +| `mode` | `/antilink mode ` | `delete` (see table below for what each command word maps to) | +| `logChannelId` | `/logs set-channel channel:<#channel>` | unset | +| `blockAllInvites` | `/invites block-all [enabled:]` | `false` | +| domain allowlist | `/allowlist add\|remove domain:` | empty | +| domain blocklist | `/blocklist add\|remove domain:` | empty | +| allowed invites | `/invites allow invite:` | empty | +| `bypassRoleIds` / `bypassUserIds` | not yet exposed via slash command - see [ROADMAP.md](../ROADMAP.md) | empty | +| `requireAllowlist`, `flagUnknownDomains`, `massMentionThreshold` | not yet exposed via slash command | `false` / `false` / `0` | + +`/antilink mode` accepts three user-facing words that map onto the +underlying `EnforcementMode` (see [rules-engine.md](./rules-engine.md)): + +| Command value | Stored as | Effect | +| ------------- | --------- | ------------------------------------------------------------------------------------------ | +| `block` | `delete` | Deletes messages that score BLOCK or above | +| `warn` | `warn` | Flags BLOCK/QUARANTINE-severity messages (audit log + mod-log embed) without deleting them | +| `log` | `log` | Records everything, never deletes or times out | + +A fourth mode, `timeout`, exists in the data model (it additionally times +out the author for a QUARANTINE-severity message) but isn't currently one +of the three choices `/antilink mode` offers - see +[ROADMAP.md](../ROADMAP.md). + +### Backing up / migrating a guild's configuration + +``` +/config export → a JSON file with the guild config + all three lists +/config import → re-import that file (always scoped to the guild you run it in) +``` + +The same format is produced/consumed by the CLI: + +```bash +antilink export-config --guild --db ./antilink.sqlite --out backup.json +antilink import-config backup.json --db ./antilink.sqlite +``` + +## Local policy file (`antilink.config.json`) + +Used only by the CLI's `scan` and `test-url` commands (and `apps/example-bot` +does **not** read this file - it uses per-guild config from the database). +Create one with `antilink init`, which scaffolds a starter file. It maps +directly onto `packages/core`'s `PolicyConfig`: + +```json +{ + "enabled": true, + "mode": "log", + "domainAllowlist": ["example.com"], + "domainBlocklist": ["bad-site.com"], + "knownPhishingDomains": [], + "inviteAllowlist": [], + "blockAllInvites": false, + "requireAllowlist": false, + "flagUnknownDomains": false, + "massMentionThreshold": 0, + "bypassRoleIds": [], + "bypassUserIds": [], + "channelRules": [{ "channelId": "123", "mode": "exempt" }], + "regexRules": [{ "id": "nitro-scam", "pattern": "free-nitro", "target": "url" }], + "urlShortenerDomains": [] +} +``` + +This file format is a superset of what's reachable through slash commands +today - `channelRules`, `regexRules`, and `knownPhishingDomains` can only be +set this way, not through the live bot yet. It's most useful for locally +testing "would this message be flagged?" before wiring up rules in +production. + +## `knownPhishingDomains` is never pre-populated + +Neither configuration surface ships with a bundled list of "known phishing +domains." That field is always empty until you (or a feed you choose to +subscribe to) fill it in. See [rules-engine.md](./rules-engine.md#detection-reasons) +for why this is a deliberate choice. diff --git a/docs/discord-setup.md b/docs/discord-setup.md new file mode 100644 index 0000000..7f6be2a --- /dev/null +++ b/docs/discord-setup.md @@ -0,0 +1,83 @@ +# Discord Setup + +## 1. Create the application + +1. Go to the [Discord Developer Portal](https://discord.com/developers/applications) → **New Application**. +2. Give it a name (this becomes the bot's display name unless you rename it later). + +## 2. Get your credentials + +- **Bot token**: **Bot** tab → **Reset Token**. This is `DISCORD_TOKEN`. + Treat it like a password - anyone with it fully controls your bot. +- **Application ID**: **General Information** tab. This is `DISCORD_CLIENT_ID`, + needed only for registering slash commands. + +## 3. Enable the Message Content Intent + +Still on the **Bot** tab, under **Privileged Gateway Intents**, enable +**Message Content Intent**. Without this, the bot receives messages with an +empty `content` field and cannot scan anything - the CI-tested code path +requires this intent to function at all. + +## 4. Required bot permissions + +Generate an invite URL from **OAuth2 → URL Generator**: + +- Scopes: `bot`, `applications.commands` +- Bot permissions: + +| Permission | Why | +| ---------------- | ----------------------------------------------------------------- | +| View Channels | Required to see messages at all | +| Send Messages | Required to reply to slash commands | +| Manage Messages | Required to delete flagged messages (`delete`/`warn`... see note) | +| Moderate Members | Required only if you use `timeout` mode | +| Embed Links | Required to post mod-log embeds | + +> **Note on `warn` mode:** `warn` mode does not delete messages, so it +> doesn't strictly require Manage Messages - but grant it anyway so you can +> switch modes later without re-inviting the bot. If Manage Messages is +> missing, the bot logs a warning and skips deletion rather than crashing +> (see [threat-model.md](./threat-model.md)); it never assumes permissions +> it doesn't have. + +## 5. Invite the bot + +Open the generated URL and select your test server. **Always test against a +server you control** before running this in a real community. + +## 6. Register slash commands + +```bash +pnpm --filter @antilink-guard/example-bot run register-commands +``` + +This registers: `/antilink`, `/allowlist`, `/blocklist`, `/invites`, `/logs`, +`/testlink`, `/config`. Set `DISCORD_GUILD_ID` in your `.env` to register to +one server only - guild commands update instantly, which is much faster to +iterate on than global commands (which can take up to an hour to propagate +to all servers). + +## 7. Verify + +``` +/antilink status +/antilink enable +/antilink mode block +``` + +Post a link from a non-admin test account in a non-exempt channel - it +should be deleted within a second or two. + +### Nothing happens when I post a link? + +1. Message Content Intent enabled? (step 3) +2. Bot has Manage Messages in that channel? +3. Is the domain or invite on an allowlist? +4. Testing with a server admin account and expecting it to be exempt? **It + won't be, by default.** Unlike some moderation bots, this one does not + automatically exempt Administrator/Manage Server permission holders - + the only exemptions are the explicit `bypassRoleIds`/`bypassUserIds` in + the guild's configuration (not yet settable via a slash command - see + [configuration.md](./configuration.md)). If you haven't configured any + bypasses, every account's messages are scanned, including yours. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..c3132ec --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,88 @@ +# Getting Started + +The fastest way to run AntiLink Guard OSS is [`apps/example-bot`](../apps/example-bot), +a working bot assembled entirely from the published `@antilink-guard/*` packages. + +## 1. Prerequisites + +- [Node.js](https://nodejs.org) 20 or newer +- [pnpm](https://pnpm.io) (`corepack enable` installs the pinned version) +- A Discord account and a test server you can invite a bot to + +## 2. Clone and install + +```bash +git clone https://github.com/timeout187/antilink-guard.git +cd antilink-guard +pnpm install +pnpm run build +``` + +`pnpm run build` builds every package in the correct dependency order +(`core` and `storage` first, then `discord-bot`/`cli`, then the apps) - +required once before anything can run, since packages resolve each other +through their built `dist/` output. + +## 3. Create a Discord application + +See [discord-setup.md](./discord-setup.md) for full detail. In short: create +an application in the [Discord Developer Portal](https://discord.com/developers/applications), +grab its bot token and Application ID, enable the **Message Content Intent**, +and invite it to your test server. + +## 4. Configure and run + +```bash +cp apps/example-bot/.env.example apps/example-bot/.env +# edit apps/example-bot/.env: DISCORD_TOKEN, DISCORD_CLIENT_ID + +pnpm --filter @antilink-guard/example-bot run register-commands +pnpm --filter @antilink-guard/example-bot run start +``` + +Full walkthrough, including Docker Compose: [`apps/example-bot/README.md`](../apps/example-bot/README.md). + +## 5. Turn on protection + +In your test server: + +``` +/antilink enable +/antilink mode block +``` + +Post a link from a non-admin test account in a channel - it should be +deleted. See [configuration.md](./configuration.md) for allowlists, +blocklists, and invite rules, and [rules-engine.md](./rules-engine.md) for +exactly how a message is scored. + +## 6. Optional: the local dashboard + +```bash +cp apps/dashboard-lite/.env.example apps/dashboard-lite/.env +pnpm --filter @antilink-guard/dashboard-lite run build +pnpm --filter @antilink-guard/dashboard-lite run start +``` + +Opens on `http://localhost:4000`. It's local and unauthenticated by +design - see its [README](../apps/dashboard-lite/README.md) before exposing +it anywhere other than your own machine. + +## Trying the CLI without any Discord setup at all + +```bash +pnpm --filter @antilink-guard/cli build +node packages/cli/dist/index.js scan "check out https://bit.ly/free-nitro" +node packages/cli/dist/index.js test-url https://xn--e1aybc.xn--p1ai +node packages/cli/dist/index.js doctor +``` + +The detection and policy engine (`packages/core`) has no Discord dependency, +so you can experiment with it entirely offline. + +## Where to go next + +- [`configuration.md`](./configuration.md) - the guild config model and how slash commands change it +- [`rules-engine.md`](./rules-engine.md) - the detection pipeline in depth +- [`self-hosting.md`](./self-hosting.md) - Docker, database choices, environment variables +- [`api-reference.md`](./api-reference.md) - the package APIs, if you're building on top of this framework diff --git a/docs/migration-from-old-antilink.md b/docs/migration-from-old-antilink.md new file mode 100644 index 0000000..fc717e3 --- /dev/null +++ b/docs/migration-from-old-antilink.md @@ -0,0 +1,73 @@ +# Migrating from the old Anti-Links-Discord-Bot + +This repository was originally a single `index.js` file (still visible in +this repo's git history, before the `feat!: begin AntiLink Guard OSS +migration` commit). If you were running that version, here's how its +behavior maps onto the new framework - **there is no automated migration +script**, since the old bot had no database to migrate from; this is a +manual mapping guide. + +## What the old bot did + +```js +// old index.js, roughly +const whitelistedChannels = ['1141168430620348517', ...]; // hardcoded +const ignoredRoles = ['839237666545205248', ...]; // hardcoded + +client.on('messageCreate', (message) => { + if (message.content.match(/https?:\/\/\S+/gi)) { + if (!whitelistedChannels.includes(message.channel.id) && + !hasIgnoredRole(message.member)) { + message.delete(); + sendLogToWebhook(`Deleted message from ${message.author.tag}...`); + } + } +}); +``` + +It had: a hardcoded channel whitelist, a hardcoded role bypass list, basic +`https?://` detection (no defanging/punycode/homoglyph handling), a +Discord webhook for logging, and no persistence, no slash commands, and no +per-guild configuration - it was hardcoded for one specific server. + +## What replaces each piece + +| Old bot | AntiLink Guard OSS | +| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Hardcoded `whitelistedChannels` array | `/logs`... actually: per-channel exemptions aren't yet a slash command (see [ROADMAP.md](../ROADMAP.md)) - for now, set `channelRules: [{ channelId, mode: "exempt" }]` in a local policy file if using the CLI, or exempt via the database directly | +| Hardcoded `ignoredRoles` array | `bypassRoleIds` on `GuildConfig` - not yet exposed via slash command either (see [ROADMAP.md](../ROADMAP.md)); set it directly via `/config import` with a hand-edited config bundle, or via `packages/storage`'s API | +| `WEBHOOK_URL` + manual webhook POST | `/logs set-channel channel:<#channel>` - the bot posts a proper embed to a normal channel, no separate webhook to manage | +| `message.content.match(/https?:\/\/\S+/gi)` | `packages/core`'s `extractLinks`/`evaluateMessage` - handles defanged links, punycode, homoglyphs, markdown links, and Discord invites, none of which the old regex caught | +| Always deletes | Configurable via `/antilink mode ` - the old behavior is closest to `block` | +| One server, no config UI | Per-guild configuration via slash commands, backed by a real database (SQLite/MySQL/PostgreSQL) | + +## Step by step + +1. Set up the new bot per [getting-started.md](./getting-started.md) - new + application, new token recommended (rotate rather than reuse the old + bot's token if you're replacing it in place). +2. `/antilink enable` +3. `/antilink mode block` (closest match to the old bot's always-delete behavior) +4. Re-add your old bypass roles: for each role ID that was in the old + `ignoredRoles` array, you currently need to set `bypassRoleIds` directly + (see the table above) - a slash command for this is planned but not + shipped yet. +5. Re-add your old whitelisted channels the same way, via `channelRules` + with `mode: "exempt"`. +6. `/logs set-channel channel:#your-mod-log-channel` - replaces the old + `WEBHOOK_URL`. You can delete the old webhook once this is confirmed + working. +7. Test: post a link from a non-exempt account - it should be deleted and + logged to your new mod-log channel. + +## Behavior differences to expect + +- **Broader detection.** Links the old regex missed (defanged, punycode, + homoglyph, markdown-wrapped) will now be caught. If you relied on people + being able to post `hxxps://` links to work around the old bot, that no + longer works. +- **No implicit admin bypass in either version** - this hasn't changed: + neither the old bot nor the new one auto-exempts server admins. Bypass is + always explicit (role/user lists). +- **Persistence.** Configuration now survives a bot restart/redeploy + without needing to edit and redeploy source code, once it's set. diff --git a/docs/privacy.md b/docs/privacy.md new file mode 100644 index 0000000..db2c232 --- /dev/null +++ b/docs/privacy.md @@ -0,0 +1,87 @@ +# Privacy + +AntiLink Guard OSS is self-hosted: **you run it, you control the database, +and no data goes to any third party operated by this project** - there is +no "phone home" telemetry anywhere in this codebase. + +## What is stored, and where + +Everything lives in the database you configure (SQLite by default; see +[self-hosting.md](./self-hosting.md)). The models, defined in +`packages/storage`: + +| Model | What it holds | +| ----------------------------------- | ---------------------------------------------------------------------- | +| `GuildConfig` | Your server's settings: enabled/mode/log channel/bypass lists | +| `AllowlistEntry` / `BlocklistEntry` | A domain, who added it, when, and (blocklist only) an optional reason | +| `InviteRule` | An allowed Discord invite code, who added it, when | +| `AuditLogEntry` | **Metadata about a moderation decision** - see below | +| `ScanResultRecord` | The outcome of a `/testlink` or CLI `test-url`/`scan` check | +| `ModerationActionRecord` | The action actually taken (delete/timeout/warn/log) for an audit entry | + +## What an audit log entry contains - and what it deliberately does not + +```ts +interface AuditLogEntry { + id: string; + guildId: string; + channelId: string; + userId: string; + normalizedUrl?: string; // the link that triggered the entry, normalized + hostname?: string; + verdict: Verdict; + reasons: DetectionReason[]; + score: number; + action: ModerationActionType; + createdAt: Date; +} +``` + +**There is no `content` or `message` field.** The full text of a scanned +message is never written to the database - only the specific link that +triggered a non-ALLOW verdict, plus the classification metadata needed to +show a mod-log entry. This is enforced by the type itself, not just a +convention: `AuditLogEntry` has no field capable of holding arbitrary +message text. `packages/storage`'s shared adapter contract test suite +(run against all four storage backends) includes an explicit assertion +that audit entries never carry a `content` property. + +## What's sent to Discord + +- Slash command replies (ephemeral where the code marks them so) +- Mod-log embeds, if you configure a log channel (`/logs set-channel`) - + these contain the same metadata as the audit log entry above: user + mention, channel mention, verdict, action, score, reasons, and the + triggering URL + +Deleted messages are removed via the normal Discord API `DELETE` message +call; this project doesn't retain a copy of what was deleted. + +## `apps/dashboard-lite` + +Reads directly from your database and displays exactly what's described +above - guild config, lists, and audit log entries. It has **no +authentication** and is meant to run on `localhost` only. See its +[README](../apps/dashboard-lite/README.md). + +## Third parties + +None, by this project. `packages/core`'s classification is entirely local +(offline heuristics against your own configured lists) - no message +content or URL is sent to an external API for scanning unless you build +that integration yourself (see [ROADMAP.md](../ROADMAP.md) for a possible +future plugin interface). The only network calls the bot itself makes are +to the Discord API. + +## Your responsibilities as an operator + +You are the data controller for whatever your instance stores about your +server's members. Standard good practice: + +- Only grant the bot the Discord permissions it needs (see + [discord-setup.md](./discord-setup.md)). +- Don't expose `dashboard-lite` publicly. +- If you're subject to a specific data-protection regime (GDPR, etc.), + treat `AuditLogEntry.userId` as personal data subject to your own + retention policy - this project doesn't implement automatic log + expiry yet (see [ROADMAP.md](../ROADMAP.md)). diff --git a/docs/rules-engine.md b/docs/rules-engine.md new file mode 100644 index 0000000..b5a931d --- /dev/null +++ b/docs/rules-engine.md @@ -0,0 +1,160 @@ +# Rules Engine + +How `packages/core` turns a raw Discord message into a verdict and an +action. This document describes the actual implementation - every +function and constant named here exists in the code. + +## Pipeline overview + +``` +message text + │ + ▼ +extractLinks() - find every URL/invite, de-obfuscating along the way + │ + ▼ +classifyLink() - score each link against allow/blocklists, shorteners, + │ punycode/homoglyphs, and custom regex rules + ▼ +evaluateMessage() - apply bypasses, aggregate scores, resolve a verdict + │ and an action for the configured enforcement mode + ▼ +{ verdict, action, score, reasons, matchedLinks, bypassed } +``` + +## 1. Extraction (`extractLinks`) + +Finds, in order: markdown links, `discord.gg`/`discord.com/invite` invites, +plain `https?://` URLs, and bare `www.` domains. Before matching, the text +is de-obfuscated: + +- Zero-width characters (`U+00AD`, `U+180E`, `U+200B-200D`, `U+2060`, + `U+FEFF`) are stripped, so `exa​mple.com` (zero-width space mid-word) is + read as `example.com`. +- `hxxp://` / `hxxps://` are reversed to `http://` / `https://`. +- `example[.]com`, `example(.)com`, and `http[://]example.com` style + defanging is reversed. + +Each extracted link records whether it was found in a defanged/obfuscated +form (`isDefanged`), whether it's a Discord invite (`isDiscordInvite`), and +its normalized URL (lowercased host, tracking parameters like `utm_*`, +`gclid`, `fbclid` stripped). + +## 2. Classification (`classifyLink`) + +Each link is checked, in this order, against the guild's policy: + +1. **Domain allowlist** - an exact or subdomain match short-circuits + everything else and allows the link (`ALLOWLIST_MATCH`). +2. **Known-phishing domains** - `KNOWN_PHISHING_DOMAIN` (see note below on + why this list is empty by default). +3. **Domain blocklist** - `BLOCKLIST_MATCH`. +4. **URL shorteners** - a small built-in default list (`bit.ly`, `tinyurl.com`, + `t.co`, `is.gd`, `buff.ly`, `ow.ly`, `rebrand.ly`, `cutt.ly`, `shorturl.at`, + `rb.gy`, `tiny.cc`, `lnkd.in`, `v.gd`), overridable via + `urlShortenerDomains` - `URL_SHORTENER`. +5. **Punycode hostnames** (any label starting `xn--`) - `PUNYCODE_SUSPICIOUS`. +6. **Homoglyphs** - the (punycode-decoded) hostname mixes Latin letters with + Cyrillic or Greek ones (e.g. a Cyrillic "а" standing in for a Latin "a") + - `HOMOGLYPH_SUSPICIOUS`. +7. If none of the above matched: `NOT_IN_ALLOWLIST` (only if + `requireAllowlist` is on) or `UNKNOWN_DOMAIN` (only if + `flagUnknownDomains` is on). Neither is applied by default, so an + unrecognized, otherwise-unremarkable domain is allowed by default. +8. **Custom regex rules** always run last, against either the normalized + URL or the full message text (`target: "url" | "message"`) - + `REGEX_RULE_MATCH`. + +Discord invites follow a parallel path: an invite on the invite allowlist +is `ALLOWLIST_MATCH`; otherwise it's tagged `DISCORD_INVITE`, and only +scored if `blockAllInvites` is on. + +### Detection reasons + +| Reason | Meaning | +| ------------------------ | ------------------------------------------------------------------- | +| `ALLOWLIST_MATCH` | Domain or invite explicitly allowed - overrides everything | +| `DISCORD_INVITE` | An invite link was found | +| `BLOCKLIST_MATCH` | Domain is on this guild's blocklist | +| `KNOWN_PHISHING_DOMAIN` | Domain is in `knownPhishingDomains` | +| `URL_SHORTENER` | Domain is a known link shortener | +| `PUNYCODE_SUSPICIOUS` | Hostname contains an `xn--` (punycode) label | +| `HOMOGLYPH_SUSPICIOUS` | Hostname mixes Latin with Cyrillic/Greek letters | +| `NOT_IN_ALLOWLIST` | `requireAllowlist` is on and the domain isn't listed | +| `UNKNOWN_DOMAIN` | `flagUnknownDomains` is on and the domain is unrecognized | +| `REGEX_RULE_MATCH` | A custom regex rule matched | +| `MASS_MENTION_WITH_LINK` | The message both pinged many users/roles and contained a risky link | + +### Why `knownPhishingDomains` is always empty out of the box + +This project does not bundle, and does not claim to maintain, a +threat-intelligence database of phishing domains. `knownPhishingDomains` is +entirely guild-supplied (via the local policy file - see +[configuration.md](./configuration.md)). If you want one, point it at a feed +you trust; this framework won't pretend to be that feed itself. + +## 3. Scoring and policy (`evaluateMessage`) + +Each reason has a fixed severity score: + +| Reason | Score | +| ---------------------------------------------- | -------------------------------- | +| `ALLOWLIST_MATCH` | -1000 (forces ALLOW) | +| `UNKNOWN_DOMAIN` | 5 | +| `NOT_IN_ALLOWLIST` | 10 | +| `URL_SHORTENER` | 15 | +| `DISCORD_INVITE` | 20 | +| `MASS_MENTION_WITH_LINK` | 25 | +| `PUNYCODE_SUSPICIOUS` / `HOMOGLYPH_SUSPICIOUS` | 30 each | +| `REGEX_RULE_MATCH` | 40 (or a rule-specific override) | +| `BLOCKLIST_MATCH` | 50 | +| `KNOWN_PHISHING_DOMAIN` | 100 | + +A message's score is the **maximum** across all its links (not a sum - +one bad link is enough), plus a one-time `MASS_MENTION_WITH_LINK` bonus if +the message both pings at least `massMentionThreshold` users/roles and +contains at least one already-risky link. + +### Verdict thresholds + +| Score | Verdict | +| ----- | ------------ | +| < 1 | `ALLOW` | +| ≥ 1 | `WARN` | +| ≥ 30 | `BLOCK` | +| ≥ 60 | `QUARANTINE` | + +### From verdict to action + +The guild's `mode` sets a ceiling on what the bot will actually do: + +| Verdict | `log` mode | `warn` mode | `delete` mode | `timeout` mode | +| ------------ | ---------- | ----------- | ------------- | -------------------- | +| `ALLOW` | none | none | none | none | +| `WARN` | log | log | log | log | +| `BLOCK` | log | **warn** | **delete** | delete | +| `QUARANTINE` | log | **warn** | delete | **delete + timeout** | + +A `WARN` verdict is always just logged, regardless of mode - only `mode` +determines how far the bot goes for `BLOCK`/`QUARANTINE` content. + +## 4. Bypasses (checked before any scanning happens) + +In order, `evaluateMessage` short-circuits to `ALLOW` (with `bypassed: true`) +if: + +1. The policy is disabled (`enabled: false`). +2. The author has a bypass role or is a bypass user. +3. The channel has a rule with `mode: "exempt"`. + +Only after these checks does the message actually get scanned. + +## Trying it yourself + +```bash +antilink scan "check out hxxps://free-nitro[.]ru/claim" --json +antilink test-url https://xn--e1aybc.xn--p1ai +``` + +See [api-reference.md](./api-reference.md) for calling `evaluateMessage` +directly from TypeScript. diff --git a/docs/self-hosting.md b/docs/self-hosting.md new file mode 100644 index 0000000..2325973 --- /dev/null +++ b/docs/self-hosting.md @@ -0,0 +1,103 @@ +# Self-Hosting + +## Option A: bare Node.js + +```bash +git clone https://github.com/timeout187/antilink-guard.git +cd antilink-guard +pnpm install +pnpm run build + +cp apps/example-bot/.env.example apps/example-bot/.env +# edit apps/example-bot/.env + +pnpm --filter @antilink-guard/example-bot run register-commands +pnpm --filter @antilink-guard/example-bot run start +``` + +Run it under a process manager (`systemd`, `pm2`, etc.) for production so it +restarts on crash or reboot. This project doesn't ship a systemd unit file +or process-manager config - contributions welcome. + +## Option B: Docker Compose + +```bash +cp apps/example-bot/.env.example apps/example-bot/.env +# edit apps/example-bot/.env +docker compose up --build -d +``` + +This builds the whole workspace inside the container (see +[`apps/example-bot/Dockerfile`](../apps/example-bot/Dockerfile)) and runs +the bot under a non-root user, with a named Docker volume +(`antilink-data`) persisting the SQLite file across restarts and rebuilds. + +```bash +docker compose logs -f # tail logs +docker compose down # stop (data volume is preserved) +``` + +## Choosing a storage backend + +Set `DATABASE_DRIVER` in `apps/example-bot/.env`: + +| `DATABASE_DRIVER` | Also required | Notes | +| ---------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------- | +| `sqlite` (default) | `DATABASE_SQLITE_PATH` (optional, defaults to `./data/antilink.sqlite`) | No separate server. Directory is created automatically. | +| `mysql` | `DATABASE_MYSQL_URL`, e.g. `mysql://user:pass@host:3306/antilink_guard` | Requires a running MySQL/MariaDB 8+ server | +| `postgres` (or `postgresql`) | `DATABASE_POSTGRES_URL`, e.g. `postgres://user:pass@host:5432/antilink_guard` | Requires a running PostgreSQL server | + +The selection logic lives in +[`apps/example-bot/src/create-storage.ts`](../apps/example-bot/src/create-storage.ts) +if you're building your own bot on top of `@antilink-guard/discord-bot` +instead of using the example app directly - it's a small, standalone +function you can copy or import the underlying adapters yourself (see +[api-reference.md](./api-reference.md)). + +All three adapters implement the exact same `StorageAdapter` interface and +are validated by the same test suite, so switching drivers doesn't change +bot behavior - only where the data lives. + +## Environment variables + +### `apps/example-bot` + +| Variable | Required | Description | +| ----------------------- | ------------------------ | -------------------------------------------------------------------- | +| `DISCORD_TOKEN` | Yes | Bot token | +| `DISCORD_CLIENT_ID` | For command registration | Application ID | +| `DISCORD_GUILD_ID` | No | Register commands to one server (instant) instead of globally (~1hr) | +| `DATABASE_DRIVER` | No | `sqlite` (default) / `mysql` / `postgres` | +| `DATABASE_SQLITE_PATH` | If using SQLite | Defaults to `./data/antilink.sqlite` | +| `DATABASE_MYSQL_URL` | If using MySQL | Connection string | +| `DATABASE_POSTGRES_URL` | If using PostgreSQL | Connection string | +| `LOG_LEVEL` | No | `trace`\|`debug`\|`info`\|`warn`\|`error`\|`silent`, default `info` | + +### `apps/dashboard-lite` + +| Variable | Required | Description | +| ---------------------- | -------- | -------------------------------------------------- | +| `DATABASE_SQLITE_PATH` | No | Should point at the **same** database the bot uses | +| `PORT` | No | Defaults to `4000` | + +## Updating + +```bash +git pull +pnpm install +pnpm run build +pnpm --filter @antilink-guard/example-bot run register-commands # only if commands changed - see CHANGELOG.md +docker compose up --build -d # if using Docker +``` + +## Logging + +`packages/discord-bot` uses [pino](https://getpino.io), emitting structured +JSON logs to stdout. For human-readable output while developing: + +```bash +pnpm --filter @antilink-guard/example-bot run start | npx pino-pretty +``` + +Set `LOG_LEVEL=silent` to suppress logs entirely (used automatically in +this repo's own test suite). diff --git a/docs/threat-model.md b/docs/threat-model.md new file mode 100644 index 0000000..5c84412 --- /dev/null +++ b/docs/threat-model.md @@ -0,0 +1,90 @@ +# Threat Model + +What AntiLink Guard OSS actually defends against, what it doesn't, and how +it fails when it fails. Read this before relying on it as your only line of +defense - see also [SECURITY.md](../SECURITY.md) for reporting a +vulnerability in the project itself. + +## What this project detects + +Everything here is implemented in `packages/core` - see +[rules-engine.md](./rules-engine.md) for the exact mechanics: + +- Links to domains you've explicitly blocklisted +- Links to domains on a known-phishing list **you supply** (none is bundled) +- Discord invite links, if you choose to restrict them +- Common URL shorteners (a small built-in list, extendable) +- Punycode (`xn--`) hostnames +- Homoglyph domains mixing Latin with Cyrillic/Greek letters +- Links obfuscated with `hxxp://`, `example[.]com`, or zero-width characters +- Custom patterns you define via regex rules +- A message that both pings many users/roles and contains a risky link + +## What this project does not detect + +- **Zero-day phishing domains not on any list.** There is no heuristic + content analysis of the destination page, no machine learning + classifier, and no bundled threat-intelligence feed. A brand-new + phishing domain that isn't on your blocklist and doesn't trip any other + reason above will be allowed. +- **Direct messages.** The bot only processes `messageCreate` events for + guild (server) messages - it never reads or scans DMs. +- **Non-text phishing vectors**: images, embedded QR codes, voice/video, + or links posted as plain text with no `http(s)://`/`www.` prefix and no + recognizable defanging pattern. +- **Attachments.** File uploads aren't scanned. +- **Edited messages.** The pipeline runs on `messageCreate` only - if a + message is edited _after_ being posted to add a malicious link, it is + not re-scanned (see [ROADMAP.md](../ROADMAP.md)). +- **Sophisticated, targeted social engineering** that doesn't rely on a + link at all (e.g. a scammer asking a victim to screen-share). + +## Trust boundaries + +- **The bot trusts its own database.** Anything in your `AllowlistEntry`/ + `BlocklistEntry`/`InviteRule` tables is trusted as configured. If your + database is compromised, so is your moderation policy - protect it like + any other credential store. +- **The bot trusts Discord's permission model for command access**, not + its own. Every admin slash command (`/antilink`, `/allowlist`, + `/blocklist`, `/invites`, `/logs`, `/config`) is registered with + `default_member_permissions` requiring **Manage Server** - Discord + enforces this, the bot doesn't re-check it. `/testlink` is intentionally + open to everyone (read-only, low risk). +- **The bot does not trust its own Discord permissions blindly.** Before + deleting a message or timing out a member, it checks discord.js's own + `message.deletable` / `member.moderatable` signals first (see + [rules-engine.md](./rules-engine.md) and `packages/discord-bot`'s + `moderation/enforce.ts`). If the bot lacks a permission, it logs a + warning and skips that action instead of crashing or silently pretending + to have succeeded. +- **`/config import` is guild-scoped defensively.** Even if the uploaded + JSON file claims a different guild ID (from another server, or crafted by + hand), the bot always overwrites it with the guild the command was run + in - an admin can never accidentally or maliciously import data into a + server that isn't theirs. + +## Failure modes + +| Failure | Behavior | +| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Bot lacks Manage Messages | Logs a warning, does not delete, still records an audit log entry | +| Bot lacks Moderate Members / role hierarchy issue | Logs a warning, does not time out (message may still be deleted) | +| Mod-log channel deleted or bot lacks access | Logs a warning, moderation still proceeds without the embed | +| Malformed custom regex rule | The rule is silently skipped for that message rather than crashing the scan | +| Database briefly unavailable | The message-processing error is caught and logged; that one message is not moderated, the bot keeps running | +| A raid/spam wave | The moderation-action rate limiter (see `packages/discord-bot`'s `rate-limit.ts`) caps enforcement actions per guild so the bot doesn't hammer Discord's API into its own rate limit | + +## `apps/dashboard-lite` has no authentication + +This is a deliberate, documented limitation, not an oversight - see its +[README](../apps/dashboard-lite/README.md) and +[privacy.md](./privacy.md#appsdashboard-lite). Do not expose it beyond +`localhost` without adding your own authentication layer in front of it. + +## Reporting an issue with this threat model + +If you find a gap here - a real attack this project should defend against +but doesn't, or a case where it fails unsafely rather than safely - please +report it per [SECURITY.md](../SECURITY.md) if it's exploitable, or open a +regular issue if it's a documentation gap. diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0e5a073..4e708bd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,3 @@ packages: - - "packages/*" - - "apps/*" + - 'packages/*' + - 'apps/*'