文档

监控与变化提醒

AppSafe 会将已验证网站的每次扫描与上一次扫描进行比较,并告诉你发生了哪些变化。配合定时扫描或部署后重新扫描,这就成了持续监控,并可通过邮件、Slack、Discord 或签名 webhook 发送提醒。

变化提醒包含在 Pro、Agency 和 Team 套餐中。Pro 和 Agency 的定时扫描每天或每周运行一次,Team 可每小时运行一次。

AppSafe 会比较什么

定时扫描和部署后扫描会发送提醒,每个问题只提醒一次:保持不变的问题在下一次扫描时不会再次提醒。如果问题消失后又重新出现,你会再次收到提醒。

设置提醒

在你的仪表盘中打开一个已验证的网站,并在“监控与提醒”下添加渠道。每个渠道都有最低严重程度设置和“发送测试提醒”按钮。

Webhook 负载

通用 webhook 会收到这段 JSON(Content-Type: application/json)。测试提醒使用事件 monitoring.test,且 scan 为 null。

{
  "event": "monitoring.changes",
  "deliveryId": "5f2c9a0e41b7d3a8c6e1f0b2",
  "createdAt": "2026-10-26T12:00:00.000Z",
  "site": {
    "domain": "myapp.com"
  },
  "scan": {
    "id": "3f1c2b9e-1111-4222-8333-944455556666",
    "grade": "C",
    "score": 67,
    "reportUrl": "https://appsafe.dev/report/…"
  },
  "summary": "2 changes on myapp.com",
  "changes": [
    {
      "type": "header_removed",
      "severity": "high",
      "subject": "content-security-policy",
      "message": "content-security-policy header removed"
    },
    {
      "type": "finding_new",
      "severity": "critical",
      "subject": "baas-supabase-table-readable",
      "title": "Supabase tables readable without logging in",
      "message": "New issue: Supabase tables readable without logging in"
    }
  ]
}

变化类型:finding_new、finding_resolved、header_added、header_removed、header_weakened、header_changed、tls_changed、tls_expiring(附带 days)、script_domain_new、sri_missing、subdomain_new、subdomain_dangling、dns_changed。严重程度:low、medium、high、critical。提醒只包含退化性变化,因此 finding_resolved 和 header_added 会显示在仪表盘和报告中,但本身不会触发提醒。

验证签名

每个 webhook 请求都带有 X-AppSafe-Signature 请求头。使用你的渠道密钥,对时间戳、一个点号和原始请求体计算 HMAC-SHA256,并以恒定时间与 v1 进行比较。拒绝时间戳超过 5 分钟的请求。

X-AppSafe-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
X-AppSafe-Event: monitoring.changes | monitoring.test
import { createHmac, timingSafeEqual } from "node:crypto";

// header: the X-AppSafe-Signature value, e.g. "t=1761480000,v1=5d41…"
// body:   the raw request body (string), before JSON.parse
export function verifyAppSafeSignature(secret, header, body) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const timestamp = Number(parts.t);
  if (!parts.v1 || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${timestamp}.${body}`).digest();
  const given = Buffer.from(parts.v1, "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}

AppSafe 每条提醒只发送一次,不会重试。请在 8 秒内返回 2xx 状态码。仪表盘会显示该渠道最近一次的发送错误。

提醒中绝不会包含的内容

提醒负载包含变化类型、严重程度、问题标题、响应头名称、脚本和子域名的主机名,以及报告链接。绝不会包含问题详情、密钥(即使已脱敏也不会)或个人数据。