用户登录

技术文章

外部链接将在新标签页打开。

AWS Continuum 自动修复 PR 的五道关卡

一句话回答

Continuum 只把漏洞修成 PR,合不合得由你把关。本文给出五道关卡的判定条件、CI 准入脚本与 webhook 验签实现,把自动合并限制在高证据等级、低爆炸半径的改动上。

核心要点 TL;DR
  • Continuum 每条修复都需人工批准,自动化应只加在验证环节
  • 以沙箱复现成功作为进入自动通道的硬条件,可砍掉多数噪音
  • PR 准入门禁看四项:文件数、净增行、高风险路径、检查项状态

Continuum 只负责把漏洞修成 PR,不负责判断这个 PR 能不能合。我的做法是在它前后加五道关卡:可达性、可利用性、爆炸半径、可回滚、审批签名。少一道,自动修复就会变成自动事故。

用一句不太严谨的话说,它像一个会写补丁的安全工程师:你给它代码仓库和云账户的读权限,它把可疑代码在沙箱里跑一遍,能打穿的写成 PoC,再提一个 PR 等你点合并。难的不是让它写补丁,是决定哪一类补丁可以不用你看。

AWS 在 10 月底把 Continuum for code vulnerabilities 转为 GA,官方描述里有两个词值得抄下来:证据等级(Evidence Level)和上下文图(Context Graph)。前者决定一条 finding 值不值得修,后者决定它排在第几位。这两样东西,正是关卡判定的输入。

Continuum 的漏洞修复流水线包含哪几道关卡?#

先把 GA 版本的实际能力摆清楚,再说需要自己补什么。AWS News Blog 的 GA 公告列出的事实是:Continuum 从代码仓库和 AWS 环境两处构建上下文图,给每条 finding 打证据等级,在隔离沙箱中复现可利用项并生成 PoC 代码,按攻击路径与业务关键系统(客户数据库、支付链路、认证服务)的距离排序,最后以补丁或拉取请求的形式交付修复。

控制边界同样写得清楚——GA 阶段每一条修复都必须人工审批后才能落地,官方把"决定 Continuum 能自主执行哪些动作"放在未来版本。这句话的意思是:自动化的空间在验证环节,不在合并环节。

对比表:扫描器与 Continuum 的交付物差异

能力传统 SCA/SAST 扫描器Continuum(GA)
输出形态按 CVSS 排序的 finding 清单带证据等级的 finding + PoC + PR
是否判断可达一般不判断用 AWS Config 资源与网络配置推断
是否验证可利用不验证隔离沙箱内复现
排序依据通用严重度分数到业务关键系统的攻击路径距离
修复交付给建议,人写代码生成补丁或直接开 PR
合并控制无强制人工审批

我把它拆成五道关卡,前两道 Continuum 原生能给你信号,后三道必须自己搭。

关卡清单:五道关卡的分工与失效后果

关卡Continuum 原生提供你需要补的部分跳过后的典型后果
1. 可达性基于 Config 与网络配置的可达推断核对生产安全组、白名单的真实开放面给只有 dev 可达的漏洞排高优先级,挤占修复窗口
2. 可利用性沙箱复现 + PoC 代码把"复现成功"设为进入自动通道的硬条件被"可能可利用"淹没,审批人开始闭眼点通过
3. 爆炸半径按业务关键系统排名自算调用方数量、接口契约变更、数据迁移影响补丁本身正确,但打断了下游服务
4. 可回滚无禁止不可逆 schema 变更进自动通道出问题回不去,只能向前修
5. 审批签名强制人工批准审批人身份、回调签名、幂等与防重放伪造审批或重复触发,PR 被非预期合并

第三道关卡最容易被低估。Netflix 工程团队写过一篇专门讲变更爆炸半径的工具文章,核心观点是 API 兼容性检查只能覆盖语法层,真正的风险在调用方行为变化上(Netflix Tech Blog)。修复补丁也是变更,同样适用这套判断。

为什么不能直接让 AI 合并修复 PR?#

因为 2026 年的 PR 洪峰已经不是人力能靠加班消化的量级。GitHub Octoverse 2026 记录提交与推送双双创下新高,增量很大一部分来自 agent 参与的开发流程。安全修复 PR 会和功能 PR 抢同一批 reviewer 的时间。

另一个更重要的事实:能编译、能过测试,和能在生产跑,是两回事。Martin Fowler 站点上那篇讨论开发者为何对 agentic development 保持警惕的文章说得直白——built 不等于 working in production(martinfowler.com)。修复补丁恰恰是最需要"在生产语境里验证"的一类改动。

Stack Overflow 有一篇标题很扎人的文章:JSON schema 不是 AI 治理层(Stack Overflow Blog)。治理讨论永远发生在真正能拦住 agent 的那行代码之上一层。落到本文的场景:审批策略写进文档没用,得写进 CI 的退出码。

时间压力确实存在。AWS Threat Intelligence 在 MadPot 蜜罐的遥测里,10 月 17 日就看到了 PHP2Shell 的利用尝试,比 CVE-2026-109618 与 CVE-2026-109619 公开披露早了四天;披露当天 02:00 UTC 已有 PoC 在流传,比 14:00 UTC 的解禁时间早约十二小时(AWS Weekly Roundup,2026-10-26)。Krebs 同期的报道提到,近七成网站需要更新核心软件才能修掉这对漏洞(Krebs on Security)。

结论很短:把速度加在验证上,不要加在合并上。沙箱复现越快,人工审批的压力越小;合并越自动,出事时的责任越模糊。

Meta 工程团队在 JiTTesting 的生产实践里用的也是同一套思路——LLM 生成测试、人来把关,human-on-the-loop 而不是 human-out-of-the-loop(Engineering at Meta)。这个模式放在漏洞修复上同样成立。

如何把审批关卡写进 CI?#

我的准入规则很朴素:小、干净、可回滚、有复现证据。满足四条才允许走快速通道,否则一律留在人工队列。规则先落成模板,再落成代码。

规范模板:修复 PR 准入规则

  1. 变更文件数不超过 8,净增行数不超过 300——超过说明补丁在重构,不是修漏洞。
  2. 路径命中 migrations/、db/schema/、.github/workflows/,或扩展名为 .sql、.tf,直接转人工。
  3. 必须存在名为 continuum/verify 的检查项且结论为 success,代表沙箱复现已通过。
  4. 其余检查项不得出现 failure 或 cancelled。
  5. 任一条件不满足,脚本退出码为 2,流水线阻塞。

可执行脚本:Python 3.11 环境下的 PR 准入门禁,依赖标准库,无需额外安装

#!/usr/bin/env python3
# continuum_gate.py — 修复 PR 准入判定
# 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
import json, os, sys, time
import urllib.request, urllib.error

API  = "https://api.github.com"
REPO = os.environ["GH_REPO"]        # 形如 owner/repo,运行时注入,不硬编码
PR   = int(os.environ["PR_NUMBER"])
TOK  = os.environ["GH_TOKEN"]

MAX_FILES, MAX_LINES = 8, 300
RISKY_PREFIX = ("migrations/", "db/schema/", ".github/workflows/")
RISKY_EXT    = (".sql", ".tf", "Dockerfile")
REQUIRED_CHECK = "continuum/verify"

def gh(path, timeout=8, retries=3):
    last = None
    for attempt in range(retries):
        req = urllib.request.Request(
            API + path,
            headers={"Authorization": "Bearer " + TOK,
                     "Accept": "application/vnd.github+json",
                     "X-GitHub-Api-Version": "2022-11-28",
                     "User-Agent": "continuum-gate/1.0"})
        try:
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                return json.loads(resp.read().decode("utf-8"))
        except urllib.error.HTTPError as e:
            last = e
            if e.code in (403, 429) and attempt < retries - 1:
                time.sleep(2 ** attempt)   # 退避 1s、2s
                continue
            raise
        except (urllib.error.URLError, TimeoutError) as e:
            last = e
            if attempt < retries - 1:
                time.sleep(2 ** attempt)
                continue
            raise
    raise RuntimeError("GitHub API 连续失败: %s" % last)

def evaluate():
    pr    = gh("/repos/%s/pulls/%d" % (REPO, PR))
    files = gh("/repos/%s/pulls/%d/files?per_page=100" % (REPO, PR))
    runs  = gh("/repos/%s/commits/%s/check-runs" % (REPO, pr["head"]["sha"]))
    reasons = []

    if len(files) >= 100:
        reasons.append("文件数触顶 100,需拆分 PR")
    elif len(files) > MAX_FILES:
        reasons.append("变更文件 %d > %d" % (len(files), MAX_FILES))

    added = sum(f.get("additions", 0) for f in files)
    if added > MAX_LINES:
        reasons.append("净增 %d 行 > %d" % (added, MAX_LINES))

    for f in files:
        name = f["filename"]
        if name.startswith(RISKY_PREFIX) or name.endswith(RISKY_EXT):
            reasons.append("触及高风险路径 %s" % name)

    table = {c["name"]: c["conclusion"] for c in runs.get("check_runs", [])}
    if table.get(REQUIRED_CHECK) != "success":
        reasons.append("缺少 %s 通过结果" % REQUIRED_CHECK)
    if any(v not in ("success", "neutral", "skipped") for v in table.values()):
        reasons.append("存在未通过的检查项")

    return ("hold" if reasons else "approve"), reasons

if __name__ == "__main__":
    decision, reasons = evaluate()
    print(json.dumps({"pr": PR, "decision": decision, "reasons": reasons},
                     ensure_ascii=False))
    sys.exit(0 if decision == "approve" else 2)

参数含义:GH_REPO 是 owner/repo 形式,从流水线变量注入;PR_NUMBER 由事件载荷带入;timeout=8 秒是单次请求上限,重试三次最坏耗时约 24 秒加退避,可接受。退出码 0 表示放行,2 表示阻塞,CI 侧按退出码决定是否继续。

命令与预期输出:仓库名替换为你自己的,PR 号取实际值

$ export GH_REPO=acme/payments-api PR_NUMBER=4821
$ export GH_TOKEN=$(cat /run/secrets/gh_token)
$ python3 continuum_gate.py
{"pr": 4821, "decision": "hold", "reasons": ["变更文件 11 > 8", "触及高风险路径 migrations/20261027_add_index.sql", "缺少 continuum/verify 通过结果"]}
$ echo $?
2

输出里三条原因同时命中,说明这个补丁既超尺寸又动了 schema,还没跑完沙箱复现。这类 PR 走人工,不冤。

补丁落地后跑一条外部探针,确认出口网络、DNS、TLS 与 JSON 解析链路没被改坏——我常用 随机动漫壁纸接口 这类稳定返回 JSON 的公开端点,因为它不依赖内部服务,失败即指向自身链路。API.PLUS(沐小悠API)的这类接口在冒烟阶段够用。

审批回调怎么防伪造和重放?#

Continuum 的审批动作最终会以 webhook 或 API 调用的形式回到你的系统。这一步如果没有验签,前面所有关卡都白设。先解决来源可信:用 IP 归属查询核对回调发起方是否在预期网段或 ASN 内。

可执行命令:查询回调来源 IP 的归属信息,连接超时 3 秒、整体超时 5 秒、失败重试 2 次

curl -sS --connect-timeout 3 --max-time 5 \
     --retry 2 --retry-delay 1 \
     "https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8"

apikey 换成你自己的密钥,ip 传待查地址。返回片段(字段示意,实际以 GET 示例接口 文档为准):

{
  "code": 200,
  "msg": "success",
  "data": {
    "ip": "8.8.8.8",
    "country": "美国",
    "province": "",
    "city": "",
    "isp": "Google LLC"
  }
}

拿到 isp 与 country 就能做白名单比对。这里有个边界必须提前定:外部查询失败时放行还是拒绝。我的选择是拒绝,转人工队列。审批链路宁可慢五分钟,也不能因为第三方接口抖一下就放进一个伪造请求。

可执行脚本:PHP 8.2 + Redis 扩展,回调验签与幂等处理

<?php
// continuum_webhook.php — 审批回调验签与来源校验
// 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
declare(strict_types=1);

const SECRET     = 'FROM_SECRETS_MANAGER';   // 禁止写进代码库
const MAX_SKEW   = 300;                      // 允许时钟偏移 5 分钟
const IP_API     = 'https://www.api.plus/api/get';
const IP_API_KEY = 'YOUR_KEY';
const ALLOW_ISP  = ['Amazon.com', 'Amazon Technologies Inc.'];

function trusted_ip(string $ip): bool {
    if (filter_var($ip, FILTER_VALIDATE_IP) === false) {
        return false;                        // 边界保护:非法 IP 直接拒
    }
    $url = IP_API . '?apikey=' . urlencode(IP_API_KEY) . '&ip=' . urlencode($ip);
    $ch  = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 2,
        CURLOPT_TIMEOUT        => 3,
        CURLOPT_FAILONERROR    => true,
    ]);
    $body = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($body === false || $code !== 200) {
        return false;                        // 降级:查询失败不走快速通道
    }
    $json = json_decode($body, true);
    if (!is_array($json) || ($json['code'] ?? 0) !== 200) {
        return false;
    }
    return in_array($json['data']['isp'] ?? '', ALLOW_ISP, true);
}

$raw   = file_get_contents('php://input') ?: '';
$ts    = $_SERVER['HTTP_X_CONTINUUM_TS']    ?? '';
$nonce = $_SERVER['HTTP_X_CONTINUUM_NONCE'] ?? '';
$sig   = $_SERVER['HTTP_X_CONTINUUM_SIG']   ?? '';

if ($raw === '' || !ctype_digit($ts) || strlen($nonce) < 16 || strlen($sig) !== 64) {
    http_response_code(400); exit('bad request');
}
if (abs(time() - (int)$ts) > MAX_SKEW) {
    http_response_code(401); exit('stale timestamp');
}
$expect = hash_hmac('sha256', $ts . '.' . $nonce . '.' . $raw, SECRET);
if (!hash_equals($expect, $sig)) {
    http_response_code(401); exit('bad signature');
}
if (!trusted_ip($_SERVER['REMOTE_ADDR'] ?? '')) {
    http_response_code(403); exit('untrusted source');
}

try {
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379, 1.0);
    $redis->setOption(Redis::OPT_READ_TIMEOUT, 1.0);
    if (!$redis->set('continuum:nonce:' . $nonce, '1', ['nx', 'ex' => 600])) {
        http_response_code(409); exit('replayed');   // 10 分钟内重复即拒
    }
} catch (RedisException $e) {
    error_log('redis down: ' . $e->getMessage());
    http_response_code(503); exit('retry later');     // 拿不到幂等保证就不接
}

$payload = json_decode($raw, true);
if (!is_array($payload) || empty($payload['pr']) || empty($payload['decision'])) {
    http_response_code(422); exit('bad payload');
}
echo json_encode(['ok' => true, 'pr' => (int)$payload['pr']]);

三段校验的顺序有讲究:先做格式与时间窗(毫秒级,无外部依赖),再做签名(微秒级),最后做 IP 归属(秒级、依赖外部)。把慢的放最后,能在洪峰下少打很多外部请求。Redis 不可用时返回 503 而不是放行,是这条链路唯一的正确选择。

监控与告警清单#

关卡设完不等于生效,得能看见它在拦什么、漏什么。下面是我在一套日增约 400 条 finding 的环境里用的指标集,阈值按经验给,需要结合你们自己的基线调整。

监控清单:关键指标、建议阈值与告警动作

指标建议阈值告警动作
待审批修复 PR 排队时长 P95> 4 小时通知当班安全负责人,加派 reviewer
沙箱复现成功率(复现成功 / 进入复现队列)< 30%检查 collector 覆盖与安全组数据新鲜度
证据等级为复现成功的占比< 10%排查上下文图是否缺网络配置
验签失败次数> 5 次/分钟限流该来源并对 /doc/get 查询结果做溯源
IP 归属查询失败率> 5%确认降级路径生效,回调全部转人工
修复 PR 合并后 7 天内回滚率> 2%收紧准入规则,下调自动通道额度
高风险路径转人工比例> 40%说明补丁粒度太粗,回头调 Continuum 的修复范围

回滚率是唯一能证伪整套设计的指标。它上涨,说明前面的关卡在放水,别去看别的数字自我安慰。

风险清单#

风险清单:风险、影响与规避方式

风险影响规避方式
collector 被授予写权限安全服务本身成为攻击面IAM 只读,权限边界用 SCP 固定
上下文图过期可达性误判,优先级排序失真资源变更事件驱动刷新,最长不超过 24 小时
PoC 代码进入公开 PR 描述给未修复的实例提供攻击样本PoC 只存内部工单,PR 描述只引用编号
审批人闭眼通过人工关卡形同虚设单 PR 审阅时长低于 60 秒即记录并抽查
补丁破坏接口契约下游服务 5xx 激增契约测试纳入必需检查项,参考 API 文章中心 里的兼容性检查方法
bot 提交的 PR 绕过 CODEOWNERS无人复核直接进主干保护分支要求人类审批,bot 审批不计入

第二条最容易被忽略。配置漂移是常态,上下文图一旦落后于真实网络,Continuum 的排序就会把你引向错误的方向,而你很难察觉——因为它看起来一直很专业。

总结:这套关卡适合什么规模#

Continuum 把漏洞管理从"给清单"推到"给补丁",价值在于把验证成本前置。但补丁的正确性和合入时机是两件事,后者只能由你的组织来定义。五道关卡里真正省时间的是第二道:有了沙箱复现证据,reviewer 的判断从"这漏洞严重吗"变成"这补丁写得对吗",认知负担下降一个量级。

适用范围:

  • 应用数量在 50 个以上、每日 finding 超过 200 条,人工分诊已经排不过来的团队。
  • 主要负载跑在 AWS 上,且安全组、路由、Config 数据能被 collector 完整采集。
  • 已有保护分支与 CODEOWNERS 流程,能保证 bot 提交的 PR 不被自动放行。
  • 愿意接受"先慢后快":前期两到三周用于校准证据等级与准入阈值。

不适用范围:

  • 混合云或多云为主,AWS 侧资源占比不足一半——上下文图残缺,排序结论不可信。
  • 单体应用、每日 finding 不足 50 条,人工处理成本低于搭建门禁的成本。
  • 团队没有 CI 基础设施,或检查项状态不可通过 API 读取,脚本无法落地。
  • 强合规场景要求所有生产变更有独立变更单,自动 PR 与现有流程冲突。

最后一句提醒:官方尚未公布未来自主修复的置信度阈值范围,任何声称"可以完全无人值守"的说法目前都缺乏依据,待验证。

本站实测数据(近 24 小时)#

以下数据来自 API.PLUS 线上调用日志(统计时间 2026-10-30 06:07,口径:仅统计已上线接口,不含扫描与无效路径),可作为容量规划与稳定性判断的现实参考。

常见问题(FAQ)

AWS Continuum 会自动合并修复 PR 吗?
不会。GA 版本每条修复都必须人工批准后才能合并,官方只表示未来可让用户按置信度阈值放行部分类别,具体阈值数值尚未公布,待验证。
没有 AWS 环境能单独用 Continuum 吗?
上下文图依赖账户内 collector 采集的 AWS Config 资源与网络配置,缺这部分数据,可达性判断会退化成静态扫描,按业务影响排序的价值基本消失。
沙箱复现失败率高说明什么?
多半是 collector 覆盖不全或安全组、路由数据过期,导致 Continuum 判断不出网络可达路径。先查上下文图新鲜度,再调流水线。

参考来源

  1. AWS Continuum for code vulnerabilities is now generally available
  2. AWS Weekly Roundup: Amazon Polly Generative engine, TorchNeuron for Trainium, and more (October 26, 2026)

以上为本文写作时的参考资料,引用观点以原文为准。

作者:沐小悠

本文约 32 分钟读完 · 关键词:AWS Continuum、自动修复PR、可利用性验证、CI门禁、代码安全

更多文章 GET 示例接口

「」标签下的其他文章