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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ jobs:

- name: Format check
run: npm run fmt:check

- name: Build Docker image
run: docker build -t alphafi-betterstack:ci .
# Docker image build/publish is owned by the alphafi metarepo
# (build-alphafi-betterstack.yml -> shared-docker-build.yml), which builds
# this service's Dockerfile with the metarepo as the build context. The
# Dockerfile's COPY paths are metarepo-root-relative (alphafi-betterstack/...),
# so a standalone `docker build .` here cannot resolve them. CI keeps
# typecheck/lint/format; image build is verified in the metarepo pipeline.
12 changes: 7 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ production so future changes are fast and safe.
Authorized users are **not** in code. `ALLOWED_USER_IDS` (comma-separated numeric
Telegram user IDs) is stored in AWS Secrets Manager and injected into the ECS task.

| | Staging | Production |
|---|---|---|
| Secret | `alphafi-betterstack-allowed-user-ids-staging` | `alphafi-betterstack-allowed-user-ids-production` |
| ECS service | `alphafi-betterstack-staging` | `alphafi-betterstack-production` |
| Bot | @AlphafiAsirStagingBot | @AlphafiAsirBot |
| | Staging | Production |
| ----------- | ---------------------------------------------- | ------------------------------------------------- |
| Secret | `alphafi-betterstack-allowed-user-ids-staging` | `alphafi-betterstack-allowed-user-ids-production` |
| ECS service | `alphafi-betterstack-staging` | `alphafi-betterstack-production` |
| Bot | @AlphafiAsirStagingBot | @AlphafiAsirBot |

**Account / access (both environments):**

- AWS account: **v3** — `705393004398`
- AWS profile: `v3-mgmt-admin` (SSO session `alphafi`)
- Region: `us-east-1`
Expand Down Expand Up @@ -73,6 +74,7 @@ To **remove** an ID, do the same read-then-write but build the new string withou
that ID (e.g. `tr ',' '\n' | grep -vx "$ID" | paste -sd,`), then redeploy.

## Caveats learned the hard way

- The README's "Adding authorized users" snippet is outdated: it overwrites instead
of appending and uses the cluster name `alphafi-production` (the real cluster is
`AlphafiCluster-production`). Prefer the recipe above.
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ LABEL org.opencontainers.image.source="https://github.com/AlphaFiTech/alphafi-be

WORKDIR /app

COPY package*.json ./
COPY alphafi-betterstack/package*.json ./
RUN npm ci --production

COPY bot.ts ./
COPY alphafi-betterstack/bot.ts ./

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD kill -0 1 || exit 1
Expand Down
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ AlphaFi Security and Incident Response (ASIR) Bot — bridges Telegram `/alert`

## Environment variables

| Variable | Required | Description |
|---|---|---|
| `TELEGRAM_BOT_TOKEN` | Yes | Telegram bot token from @BotFather |
| `BETTER_STACK_API_TOKEN` | Yes | Better Stack API token |
| `ESCALATION_POLICY_ID` | Yes | Better Stack escalation policy ID (numeric string) |
| `ALLOWED_USER_IDS` | Yes | Comma-separated numeric Telegram user IDs authorized to trigger alerts |
| `REQUESTER_EMAIL` | No | Email shown in Better Stack incidents (default: `admin@alphafi.xyz`) |
| `LOG_LEVEL` | No | Pino log level: `fatal`, `error`, `warn`, `info`, `debug` (default: `info`) |
| Variable | Required | Description |
| ------------------------ | -------- | --------------------------------------------------------------------------- |
| `TELEGRAM_BOT_TOKEN` | Yes | Telegram bot token from @BotFather |
| `BETTER_STACK_API_TOKEN` | Yes | Better Stack API token |
| `ESCALATION_POLICY_ID` | Yes | Better Stack escalation policy ID (numeric string) |
| `ALLOWED_USER_IDS` | Yes | Comma-separated numeric Telegram user IDs authorized to trigger alerts |
| `REQUESTER_EMAIL` | No | Email shown in Better Stack incidents (default: `admin@alphafi.xyz`) |
| `LOG_LEVEL` | No | Pino log level: `fatal`, `error`, `warn`, `info`, `debug` (default: `info`) |

## Running locally

Expand Down Expand Up @@ -46,6 +46,7 @@ make ci # typecheck + lint + fmt check
```

Install pre-commit hooks (requires [pre-commit](https://pre-commit.com)):

```bash
pre-commit install
```
Expand Down
29 changes: 23 additions & 6 deletions bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ function escapeMarkdown(text: string): string {
}

function sanitizeForApi(text: string): string {
return text.replace(/[\x00-\x1F\x7F]/g, '').replace(/<[^>]*>/g, '').trim();
return text
.replace(/[\x00-\x1F\x7F]/g, '')
.replace(/<[^>]*>/g, '')
.trim();
}

// Startup Guard: Validate critical environment variables
Expand All @@ -36,7 +39,9 @@ if (!/^\d+$/.test(POLICY_ID)) {

// Use numeric User IDs instead of usernames (more secure/immutable)
const ALLOWED_USERS: string[] = process.env.ALLOWED_USER_IDS
? process.env.ALLOWED_USER_IDS.split(',').map((id) => id.trim()).filter(Boolean)
? process.env.ALLOWED_USER_IDS.split(',')
.map((id) => id.trim())
.filter(Boolean)
: [];

if (ALLOWED_USERS.length === 0) {
Expand Down Expand Up @@ -134,7 +139,10 @@ bot.command('alert', async (ctx: Context) => {
{ parse_mode: 'Markdown' },
);

log.info({ incidentId, userId, userLabel: rawUserLabel }, 'ASIR policy triggered successfully');
log.info(
{ incidentId, userId, userLabel: rawUserLabel },
'ASIR policy triggered successfully',
);
} else {
await ctx.telegram.editMessageText(
ctx.chat!.id,
Expand All @@ -145,10 +153,17 @@ bot.command('alert', async (ctx: Context) => {
);
}
} catch (error) {
const axiosError = error as { response?: { status?: number; data?: unknown }; message?: string };
const axiosError = error as {
response?: { status?: number; data?: unknown };
message?: string;
};
const statusCode = axiosError.response?.status ? `(Status: ${axiosError.response.status})` : '';
log.error(
{ userId, status: axiosError.response?.status, detail: axiosError.response?.data ?? axiosError.message },
{
userId,
status: axiosError.response?.status,
detail: axiosError.response?.data ?? axiosError.message,
},
'ASIR policy trigger failed',
);

Expand All @@ -161,7 +176,9 @@ bot.command('alert', async (ctx: Context) => {
{ parse_mode: 'Markdown' },
);
} else {
await ctx.reply(`❌ ASIR_bot Error: Failed to trigger Escalation Policy. ${statusCode}\nCheck API logs.`);
await ctx.reply(
`❌ ASIR_bot Error: Failed to trigger Escalation Policy. ${statusCode}\nCheck API logs.`,
);
}
}
});
Expand Down
16 changes: 8 additions & 8 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import tseslint from "@typescript-eslint/eslint-plugin";
import tsparser from "@typescript-eslint/parser";
import prettier from "eslint-config-prettier";
import tseslint from '@typescript-eslint/eslint-plugin';
import tsparser from '@typescript-eslint/parser';
import prettier from 'eslint-config-prettier';

export default [
{
files: ["**/*.ts"],
files: ['**/*.ts'],
languageOptions: {
parser: tsparser,
parserOptions: {
project: "./tsconfig.json",
project: './tsconfig.json',
},
},
plugins: {
"@typescript-eslint": tseslint,
'@typescript-eslint': tseslint,
},
rules: {
...tseslint.configs.recommended.rules,
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"@typescript-eslint/explicit-function-return-type": "off",
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/explicit-function-return-type': 'off',
},
},
prettier,
Expand Down
Loading