ai-saas-guard 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -13
- package/README.zh-CN.md +294 -0
- package/dist/hosted/contracts.d.ts +90 -0
- package/dist/hosted/contracts.js +189 -1
- package/docs/github-action.md +1 -1
- package/docs/hosted-preimplementation-contracts.md +50 -0
- package/docs/npm-publishing.md +3 -3
- package/docs/project-handoff.md +6 -4
- package/docs/release-quality-knowledge-base.md +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
<h1 align="center">ai-saas-guard</h1>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<strong>
|
|
4
|
+
<strong>You used AI to build your SaaS. Now you need to know what is risky before launch.</strong>
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
|
|
8
|
+
ai-saas-guard points reviewers to the auth, billing, data access, secrets, MCP, and deploy changes that deserve human attention first. It runs locally, reads your repo only, and does not upload code.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
It is not a pentest. It is a practical review checklist for launch-risk hotspots.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
English | <a href="README.zh-CN.md">中文 README</a>
|
|
9
17
|
</p>
|
|
10
18
|
|
|
11
19
|
<p align="center">
|
|
@@ -18,13 +26,29 @@
|
|
|
18
26
|
|
|
19
27
|
---
|
|
20
28
|
|
|
21
|
-
##
|
|
29
|
+
## The Problem It Solves
|
|
22
30
|
|
|
23
|
-
|
|
31
|
+
AI can turn an idea into a working SaaS quickly. The harder question is whether the app is ready for real users.
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
The risky parts are often not the obvious UI bugs. They are the small changes that decide who can see data, who gets paid access, where secrets are exposed, and what an AI tool is allowed to do:
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
- Can one customer read another customer's data?
|
|
36
|
+
- Can a Stripe webhook grant access twice, miss a failed payment, or trust an unsigned request?
|
|
37
|
+
- Did a public environment variable expose a secret?
|
|
38
|
+
- Did an MCP tool get shell, database, or broad filesystem access?
|
|
39
|
+
- Did a pull request hide auth, billing, or deploy changes inside a large AI-generated diff?
|
|
40
|
+
|
|
41
|
+
`ai-saas-guard` is a local-first, review-first preflight for that moment. It does not try to prove your app is secure. It is not a pentest, certification, or full audit. It gives founders, solo builders, small teams, and reviewers a short, evidence-backed list of what to check before launch or merge.
|
|
42
|
+
|
|
43
|
+
## What You Get
|
|
44
|
+
|
|
45
|
+
Run it against a repository or pull request and get findings with:
|
|
46
|
+
|
|
47
|
+
- the rule that matched
|
|
48
|
+
- severity and file evidence
|
|
49
|
+
- why the issue matters in a SaaS launch
|
|
50
|
+
- how to verify it manually
|
|
51
|
+
- a practical fix direction
|
|
28
52
|
|
|
29
53
|
It is built for common AI-SaaS stacks:
|
|
30
54
|
|
|
@@ -35,8 +59,6 @@ It is built for common AI-SaaS stacks:
|
|
|
35
59
|
- MCP server configuration
|
|
36
60
|
- AI-generated pull requests with large mixed diffs
|
|
37
61
|
|
|
38
|
-
It is intentionally evidence-first. Findings include a rule ID, severity, file evidence, why it matters, how to verify it, and a fix direction.
|
|
39
|
-
|
|
40
62
|
## Current Status
|
|
41
63
|
|
|
42
64
|
This repository is public on GitHub.
|
|
@@ -51,8 +73,8 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
|
|
|
51
73
|
| JSON and SARIF output | Available |
|
|
52
74
|
| Composite GitHub Action | Available |
|
|
53
75
|
| Project config | `.ai-saas-guard.json` rule toggles, severity overrides, and fail thresholds |
|
|
54
|
-
| Versioned Action tags | `v0.
|
|
55
|
-
| npm package | `ai-saas-guard@0.
|
|
76
|
+
| Versioned Action tags | `v0.12.0`, `v0` |
|
|
77
|
+
| npm package | `ai-saas-guard@0.12.0` |
|
|
56
78
|
| npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
|
|
57
79
|
|
|
58
80
|
## Quick Start
|
|
@@ -192,13 +214,13 @@ Hosted uninstall and data deletion behavior is documented in [docs/hosted-uninst
|
|
|
192
214
|
|
|
193
215
|
Hosted pricing and packaging boundaries are documented in [docs/hosted-pricing-packaging.md](docs/hosted-pricing-packaging.md). Core local scanning stays useful without an account; hosted plans may add workflow convenience, saved reports, team policy, and optional human review, but they do not gate local CLI scanning.
|
|
194
216
|
|
|
195
|
-
Hosted pre-implementation pure contracts are documented in [docs/hosted-preimplementation-contracts.md](docs/hosted-preimplementation-contracts.md). They cover queue-safe webhook event parsing, bounded check-run summary rendering, idempotent queue cleanup planning, worker checkout cleanup planning, and other service-free helpers exported from `ai-saas-guard/hosted/contracts`.
|
|
217
|
+
Hosted pre-implementation pure contracts are documented in [docs/hosted-preimplementation-contracts.md](docs/hosted-preimplementation-contracts.md). They now include a pull request webhook intake planner that verifies signatures before parsing or queueing, plus a durable scan queue planner that reuses queued, running, and completed jobs for the same trusted scan key. They also cover queue-safe webhook event parsing, bounded check-run summary rendering, idempotent queue cleanup planning, worker checkout cleanup planning, and other service-free helpers exported from `ai-saas-guard/hosted/contracts`.
|
|
196
218
|
|
|
197
219
|
A public hosted compact report schema fixture is available at [examples/hosted-compact-report.json](examples/hosted-compact-report.json). It is synthetic and public-safe: compact evidence only, no raw source, raw diffs, secrets, webhook payload bodies, customer payloads, private URLs, or worker checkout paths.
|
|
198
220
|
|
|
199
221
|
The proposed hosted app permission boundary is intentionally narrow: repository contents read, pull requests read, checks write, and metadata read for the first version. Optional PR comments require repository policy opt-in, and broad permissions such as administration, deployments, Actions write, and repository secrets are out of scope.
|
|
200
222
|
|
|
201
|
-
The repository also includes pure pre-implementation hosted contract helpers and tests for webhook verification, installation token scoping, queue idempotency, compact reports, and retention limits. These helpers do not implement or deploy a hosted service.
|
|
223
|
+
The repository also includes pure pre-implementation hosted contract helpers and tests for webhook intake order, webhook verification, installation token scoping, durable queue idempotency, compact reports, and retention limits. These helpers do not implement or deploy a hosted service.
|
|
202
224
|
|
|
203
225
|
Users should prefer the local CLI for private repositories, offline review, or no-account workflows where hosted code processing is not acceptable.
|
|
204
226
|
|
|
@@ -232,7 +254,7 @@ Use `suppressions` for narrower false-positive handling when one rule is noisy o
|
|
|
232
254
|
|
|
233
255
|
## GitHub Action
|
|
234
256
|
|
|
235
|
-
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.
|
|
257
|
+
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.12.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
|
|
236
258
|
|
|
237
259
|
```yaml
|
|
238
260
|
name: ai-saas-guard
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
<h1 align="center">ai-saas-guard</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>你用 AI 把 SaaS 做出来了。现在要知道上线前哪里最容易出事。</strong>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
ai-saas-guard 会优先指出 auth、billing、data access、secrets、MCP 和 deploy 里最值得人工 review 的改动。它本地运行、只读仓库、不上传代码。
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
它不是渗透测试,而是一份面向上线风险点的实用 review 清单。
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="README.md">English README</a> | 中文
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 它解决什么问题
|
|
22
|
+
|
|
23
|
+
AI 能很快把一个 SaaS 从想法做成可运行的产品。真正难的是:它能不能放心给真实用户用。
|
|
24
|
+
|
|
25
|
+
上线前最危险的通常不是界面小 bug,而是那些会影响用户数据、付费权限、密钥暴露和 AI 工具权限的小改动:
|
|
26
|
+
|
|
27
|
+
- 一个用户会不会看到另一个客户的数据?
|
|
28
|
+
- Stripe webhook 会不会重复开通权限、漏处理付款失败,或者信任未签名请求?
|
|
29
|
+
- `NEXT_PUBLIC_*` 里是不是不小心暴露了 secret?
|
|
30
|
+
- MCP 工具是不是拿到了 shell、数据库或过宽的文件系统权限?
|
|
31
|
+
- AI 生成的大 PR 里,是不是把 auth、billing 或 deploy 改动藏在 UI 调整中?
|
|
32
|
+
|
|
33
|
+
`ai-saas-guard` 是面向这个时刻的本地优先、review-first 上线预检工具。它不会证明你的应用绝对安全,也不是渗透测试、认证或完整安全审计。它的目标是给 founder、独立开发者、小团队和 reviewer 一份短而有证据的清单,告诉你上线或合并 PR 前最该先看哪里。
|
|
34
|
+
|
|
35
|
+
## 你会得到什么
|
|
36
|
+
|
|
37
|
+
对仓库或 PR 运行后,它会给出:
|
|
38
|
+
|
|
39
|
+
- 命中的 rule
|
|
40
|
+
- severity 和文件证据
|
|
41
|
+
- 为什么这个问题会影响 SaaS 上线
|
|
42
|
+
- 如何人工验证
|
|
43
|
+
- 实际修复方向
|
|
44
|
+
|
|
45
|
+
它适合常见 AI 构建的 SaaS 技术栈:
|
|
46
|
+
|
|
47
|
+
- Next.js 和 Vercel
|
|
48
|
+
- Supabase RLS、storage policy、SQL migration
|
|
49
|
+
- Stripe checkout、subscription、webhook
|
|
50
|
+
- Prisma 或 SQL migration
|
|
51
|
+
- MCP server 配置
|
|
52
|
+
- AI 生成的大型混合 PR
|
|
53
|
+
|
|
54
|
+
## 当前状态
|
|
55
|
+
|
|
56
|
+
这个仓库是公开 GitHub 仓库。
|
|
57
|
+
|
|
58
|
+
CLI 已发布到 npm:`ai-saas-guard@0.12.0`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.12.0`。
|
|
59
|
+
|
|
60
|
+
| 模块 | 状态 |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| 公开 GitHub 仓库 | 已可用 |
|
|
63
|
+
| npm CLI | 已发布为 `ai-saas-guard` |
|
|
64
|
+
| 本地源码运行 | 已可用 |
|
|
65
|
+
| JSON 和 SARIF 输出 | 已可用 |
|
|
66
|
+
| Markdown PR summary | 已可用 |
|
|
67
|
+
| GitHub Action | 已可用 |
|
|
68
|
+
| 项目配置 | `.ai-saas-guard.json` 支持规则开关、severity 覆盖和 fail threshold |
|
|
69
|
+
| 当前版本 | `0.12.0` |
|
|
70
|
+
| Action 标签 | `v0.12.0`、`v0` |
|
|
71
|
+
| npm 发布 | GitHub Actions Trusted Publisher/OIDC,无需长期 npm token |
|
|
72
|
+
|
|
73
|
+
## 快速开始
|
|
74
|
+
|
|
75
|
+
无需全局安装,直接运行:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
运行专项检查:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main
|
|
85
|
+
npx ai-saas-guard@latest check-supabase --root /path/to/your-saas
|
|
86
|
+
npx ai-saas-guard@latest check-stripe --root /path/to/your-saas
|
|
87
|
+
npx ai-saas-guard@latest check-mcp --root /path/to/your-saas
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
机器可读输出:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas --json
|
|
94
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas --sarif > ai-saas-guard.sarif
|
|
95
|
+
npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --markdown > ai-saas-guard-pr.md
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
本地开发:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
git clone https://github.com/zr9959/ai-saas-guard.git
|
|
102
|
+
cd ai-saas-guard
|
|
103
|
+
npm ci
|
|
104
|
+
npm run build
|
|
105
|
+
node dist/cli.js scan --root /path/to/your-saas
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 主要命令
|
|
109
|
+
|
|
110
|
+
| 命令 | 用途 |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `scan` | 对 secrets、Stripe、Supabase、MCP、API routes、deploy config 做整体上线预检 |
|
|
113
|
+
| `pr-risk` | 分析当前 git diff 或指定 base branch diff,判断哪些文件和风险面应该先 review |
|
|
114
|
+
| `check-supabase` | 检查 migration 和 policy 文件里的 RLS、ownership、storage policy 风险 |
|
|
115
|
+
| `check-stripe` | 检查 webhook 签名、raw body、幂等、订阅生命周期和 entitlement 更新路径 |
|
|
116
|
+
| `check-mcp` | 检查 MCP 配置里的 secret、非 localhost 绑定、shell/db/filesystem 等副作用 |
|
|
117
|
+
|
|
118
|
+
## 它会检查什么
|
|
119
|
+
|
|
120
|
+
| 风险面 | 例子 |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| Secrets 和 env | 类似密钥的字符串、危险的 `NEXT_PUBLIC_*` 暴露 |
|
|
123
|
+
| Stripe | webhook 缺失、未验证签名、raw body 签名风险、缺幂等、缺失败/取消/退款/更新处理 |
|
|
124
|
+
| Supabase | 敏感表没启用 RLS、policy 过宽、缺少 ownership filter、`WITH CHECK` 过弱、storage object policy 过宽 |
|
|
125
|
+
| API routes | 有 auth 但缺少明显 ownership guard,敏感 mutation route 缺少 rate-limit 提示 |
|
|
126
|
+
| MCP | 明文 secret、非 localhost 绑定、过宽文件系统权限、shell 工具、raw SQL 工具 |
|
|
127
|
+
| Deploy config | Next static export 和 API route 冲突、Edge runtime 使用 Node-only API、关键 env 文档缺失 |
|
|
128
|
+
| PR risk | auth、billing、RLS、env、deploy、API、storage、测试删除、大型混合 diff |
|
|
129
|
+
|
|
130
|
+
完整规则请看 [docs/rules.md](docs/rules.md)。
|
|
131
|
+
|
|
132
|
+
## PR 风险分流
|
|
133
|
+
|
|
134
|
+
`scan` 可以扫整个仓库,但这个项目更锋利的入口是 PR review。
|
|
135
|
+
|
|
136
|
+
AI 生成的 PR 经常把很多东西混在一起:
|
|
137
|
+
|
|
138
|
+
- UI 调整
|
|
139
|
+
- auth/session 改动
|
|
140
|
+
- database migration
|
|
141
|
+
- Stripe checkout 或 webhook 改动
|
|
142
|
+
- Supabase policy
|
|
143
|
+
- Vercel 配置
|
|
144
|
+
- 测试被删除或削弱
|
|
145
|
+
|
|
146
|
+
`pr-risk` 会输出:
|
|
147
|
+
|
|
148
|
+
- 最应该先 review 的文件
|
|
149
|
+
- PR 触碰到的敏感类别
|
|
150
|
+
- review-first checklist
|
|
151
|
+
- 建议拆分 PR 的方向
|
|
152
|
+
- 必要测试或人工验证步骤
|
|
153
|
+
- 当 base ref 或 shallow checkout 导致无法比较时,给出明确诊断
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
node dist/cli.js pr-risk --root /path/to/your-saas --base origin/main --json
|
|
157
|
+
node dist/cli.js pr-risk --root /path/to/your-saas --base origin/main --markdown
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## 目标用户
|
|
161
|
+
|
|
162
|
+
这个工具主要面向:
|
|
163
|
+
|
|
164
|
+
- 用 AI 编程工具快速做 SaaS MVP 的 founder 或独立开发者
|
|
165
|
+
- 没有专职安全工程师的小团队
|
|
166
|
+
- 需要 review 大型 AI-generated PR 的 reviewer
|
|
167
|
+
- 给客户交付 SaaS 的开发者或 agency
|
|
168
|
+
- 希望在 CI 里加一道轻量上线预检的小团队
|
|
169
|
+
|
|
170
|
+
它的目标不是吓人,也不是制造大量噪音,而是帮你把 review 时间用在最值得看的地方。
|
|
171
|
+
|
|
172
|
+
## GitHub Action
|
|
173
|
+
|
|
174
|
+
可以在 GitHub Actions 里直接使用:
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
name: ai-saas-guard
|
|
178
|
+
|
|
179
|
+
on:
|
|
180
|
+
pull_request:
|
|
181
|
+
|
|
182
|
+
permissions:
|
|
183
|
+
contents: read
|
|
184
|
+
|
|
185
|
+
jobs:
|
|
186
|
+
preflight:
|
|
187
|
+
runs-on: ubuntu-latest
|
|
188
|
+
steps:
|
|
189
|
+
- uses: actions/checkout@v6.0.2
|
|
190
|
+
with:
|
|
191
|
+
fetch-depth: 0
|
|
192
|
+
- uses: zr9959/ai-saas-guard@v0
|
|
193
|
+
with:
|
|
194
|
+
command: pr-risk
|
|
195
|
+
root: ${{ github.workspace }}
|
|
196
|
+
base: origin/main
|
|
197
|
+
fail-on: high
|
|
198
|
+
config: .ai-saas-guard.json
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
更多 GitHub Action 示例请看 [docs/github-action.md](docs/github-action.md)。
|
|
202
|
+
|
|
203
|
+
## 项目配置
|
|
204
|
+
|
|
205
|
+
在仓库根目录添加 `.ai-saas-guard.json` 可以调整规则:
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"failOn": "high",
|
|
210
|
+
"rules": {
|
|
211
|
+
"stripe.webhook.missing-signature": "off",
|
|
212
|
+
"stripe.webhook.missing-idempotency": "critical",
|
|
213
|
+
"deploy.env.example-missing": "info"
|
|
214
|
+
},
|
|
215
|
+
"suppressions": [
|
|
216
|
+
{
|
|
217
|
+
"ruleId": "stripe.webhook.missing-idempotency",
|
|
218
|
+
"paths": ["app/api/stripe/webhook/route.ts"],
|
|
219
|
+
"reason": "Temporary launch exception with duplicate-event coverage in integration tests."
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`rules` 可以关闭规则或覆盖 severity。`suppressions` 适合处理某个具体路径上的 false positive。`failOn` 用于设置 CI 失败阈值。
|
|
226
|
+
|
|
227
|
+
## 隐私模型
|
|
228
|
+
|
|
229
|
+
`ai-saas-guard` 设计上适合在私有本地仓库中运行。
|
|
230
|
+
|
|
231
|
+
- 本地运行
|
|
232
|
+
- 读取仓库文件和 git diff
|
|
233
|
+
- scan 命令无网络调用
|
|
234
|
+
- 不上传代码
|
|
235
|
+
- 不需要账号或登录
|
|
236
|
+
- 不修改被扫描仓库
|
|
237
|
+
- 对类似 secret 的 evidence 做 redaction
|
|
238
|
+
|
|
239
|
+
## Hosted GitHub App 设计
|
|
240
|
+
|
|
241
|
+
当前仓库已经包含未来 Hosted GitHub App 的设计文档和纯契约测试,但还没有部署真实 hosted 服务。
|
|
242
|
+
|
|
243
|
+
相关文档:
|
|
244
|
+
|
|
245
|
+
- [docs/github-app-design.md](docs/github-app-design.md)
|
|
246
|
+
- [docs/hosted-first-service-slice.md](docs/hosted-first-service-slice.md)
|
|
247
|
+
- [docs/hosted-deployment-model.md](docs/hosted-deployment-model.md)
|
|
248
|
+
- [docs/hosted-operational-release-gate.md](docs/hosted-operational-release-gate.md)
|
|
249
|
+
- [docs/hosted-uninstall-data-deletion.md](docs/hosted-uninstall-data-deletion.md)
|
|
250
|
+
- [docs/hosted-pricing-packaging.md](docs/hosted-pricing-packaging.md)
|
|
251
|
+
- [docs/hosted-preimplementation-contracts.md](docs/hosted-preimplementation-contracts.md)
|
|
252
|
+
|
|
253
|
+
已经实现的 hosted 预实现纯契约包括:
|
|
254
|
+
|
|
255
|
+
- pull request webhook intake planner:先验签,再解析 payload、生成可信 identity、校验 selected-repository scope,并默认只走 check-run-only 输出
|
|
256
|
+
- durable scan queue planner:同一个 trusted scan key 的 queued/running/completed job 会复用,不重复排 worker,也不会把源码、diff、secret 或 PR 正文放进队列 payload
|
|
257
|
+
- webhook event parser
|
|
258
|
+
- check-run summary renderer
|
|
259
|
+
- queue cleanup planner
|
|
260
|
+
- worker checkout cleanup planner
|
|
261
|
+
- hosted compact report fixture:[examples/hosted-compact-report.json](examples/hosted-compact-report.json)
|
|
262
|
+
|
|
263
|
+
这些 helper 不会启动服务、不会调用 GitHub API、不会请求 installation token、不会写 check run,也不会上传源码。
|
|
264
|
+
|
|
265
|
+
## 它不是什么
|
|
266
|
+
|
|
267
|
+
这个项目刻意避免过度安全承诺。
|
|
268
|
+
|
|
269
|
+
- 不是渗透测试
|
|
270
|
+
- 不是完整 SAST 平台
|
|
271
|
+
- 不能证明你的应用绝对安全
|
|
272
|
+
- 不能替代两账号权限测试
|
|
273
|
+
- 不执行 Stripe、Supabase、Vercel 或浏览器流程
|
|
274
|
+
- 不检查没有体现在本地文件里的生产设置
|
|
275
|
+
- 不替代 Semgrep、Gitleaks、TruffleHog、Bearer、CodeQL 或人工 review
|
|
276
|
+
|
|
277
|
+
正确使用方式是:把它当成上线前和 PR review 前的 preflight,帮助你决定应该先把人工注意力放在哪里。
|
|
278
|
+
|
|
279
|
+
## 开发
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
npm ci
|
|
283
|
+
npm test
|
|
284
|
+
npm run build
|
|
285
|
+
node dist/cli.js scan --root .
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
发布 CLI、GitHub Action、npm package 或任何公开仓库更新前,必须按照 [docs/release-quality-knowledge-base.md](docs/release-quality-knowledge-base.md) 的 release gate 执行。
|
|
289
|
+
|
|
290
|
+
以后更新英文 `README.md` 时,也要同步检查并更新本中文 `README.zh-CN.md`。
|
|
291
|
+
|
|
292
|
+
## 安全报告
|
|
293
|
+
|
|
294
|
+
报告漏洞前请阅读 [SECURITY.md](SECURITY.md)。不要在公开 issue 中发布真实 API key、客户数据、私有源码或生产 URL。
|
|
@@ -46,6 +46,43 @@ export interface HostedPullRequestEventDecision {
|
|
|
46
46
|
action?: string;
|
|
47
47
|
identity?: HostedScanIdentity;
|
|
48
48
|
}
|
|
49
|
+
export type HostedPullRequestWebhookIntakeStage = "signature" | "payload" | "event" | "installation_scope" | "queue";
|
|
50
|
+
export type HostedPullRequestWebhookIntakeRejectReason = WebhookRejectReason | PullRequestEventRejectReason | InstallationScopeRejectReason | "invalid_json" | "missing_delivery_id";
|
|
51
|
+
export interface HostedPullRequestWebhookIntakeInput {
|
|
52
|
+
payload: string | Buffer;
|
|
53
|
+
signatureHeader?: string;
|
|
54
|
+
signingKey: string | Buffer;
|
|
55
|
+
deliveryId?: string;
|
|
56
|
+
seenDeliveryIds?: Set<string>;
|
|
57
|
+
scannerVersion: string;
|
|
58
|
+
selectedRepositoryIds: number[];
|
|
59
|
+
removedRepositoryIds?: number[];
|
|
60
|
+
queue: Map<string, HostedScanJobState>;
|
|
61
|
+
allowDraft?: boolean;
|
|
62
|
+
supportedActions?: string[];
|
|
63
|
+
manualRerun?: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface HostedPullRequestWebhookIntakeDecision {
|
|
66
|
+
accepted: boolean;
|
|
67
|
+
stage: HostedPullRequestWebhookIntakeStage;
|
|
68
|
+
reason?: HostedPullRequestWebhookIntakeRejectReason;
|
|
69
|
+
action?: string;
|
|
70
|
+
deliveryId?: string;
|
|
71
|
+
identity?: HostedScanIdentity;
|
|
72
|
+
job?: HostedScanJobDecision;
|
|
73
|
+
shouldQueueScanJob: boolean;
|
|
74
|
+
shouldFetchRepository: boolean;
|
|
75
|
+
shouldCreateCheckRun: boolean;
|
|
76
|
+
shouldCreatePrComment: false;
|
|
77
|
+
privacy: {
|
|
78
|
+
includesRawWebhookPayload: false;
|
|
79
|
+
includesUntrustedPrText: false;
|
|
80
|
+
includesRawSource: false;
|
|
81
|
+
includesRawDiffs: false;
|
|
82
|
+
includesSecrets: false;
|
|
83
|
+
includesCustomerPayloads: false;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
49
86
|
export type InstallationScopeRejectReason = "installation_mismatch" | "repository_not_installed" | "repository_removed_from_installation";
|
|
50
87
|
export interface InstallationScopeInput {
|
|
51
88
|
identity: HostedScanIdentity;
|
|
@@ -77,6 +114,57 @@ export interface HostedScanJobDecision {
|
|
|
77
114
|
shouldCreatePrComment: boolean;
|
|
78
115
|
}
|
|
79
116
|
export type HostedQueueJobStatus = "queued" | "running" | "completed" | "failed" | "cancelled";
|
|
117
|
+
export interface HostedScanQueueRecord {
|
|
118
|
+
key: string;
|
|
119
|
+
identity: HostedScanIdentity;
|
|
120
|
+
status: HostedQueueJobStatus;
|
|
121
|
+
attempt: number;
|
|
122
|
+
deliveryIds: string[];
|
|
123
|
+
createdAt: string;
|
|
124
|
+
updatedAt: string;
|
|
125
|
+
reportId?: string;
|
|
126
|
+
}
|
|
127
|
+
export interface HostedScanQueuePayload {
|
|
128
|
+
key: string;
|
|
129
|
+
identity: HostedScanIdentity;
|
|
130
|
+
deliveryId: string;
|
|
131
|
+
attempt: number;
|
|
132
|
+
requestedAt: string;
|
|
133
|
+
source: "github_pull_request";
|
|
134
|
+
}
|
|
135
|
+
export interface HostedScanQueueUpsertInput {
|
|
136
|
+
identity: HostedScanIdentity;
|
|
137
|
+
deliveryId: string;
|
|
138
|
+
requestedAt: string;
|
|
139
|
+
queue: Map<string, HostedScanQueueRecord>;
|
|
140
|
+
manualRerun?: boolean;
|
|
141
|
+
rawSource?: string;
|
|
142
|
+
rawDiff?: string;
|
|
143
|
+
secretValues?: string[];
|
|
144
|
+
untrustedPrText?: string;
|
|
145
|
+
customerPayload?: unknown;
|
|
146
|
+
}
|
|
147
|
+
export interface HostedScanQueueUpsertDecision {
|
|
148
|
+
key: string;
|
|
149
|
+
idempotent: true;
|
|
150
|
+
created: boolean;
|
|
151
|
+
reusedExistingJob: boolean;
|
|
152
|
+
existingStatus?: HostedQueueJobStatus;
|
|
153
|
+
attempt: number;
|
|
154
|
+
queueRecord: HostedScanQueueRecord;
|
|
155
|
+
queuePayload: HostedScanQueuePayload;
|
|
156
|
+
shouldEnqueueWorker: boolean;
|
|
157
|
+
shouldReuseCompletedReport: boolean;
|
|
158
|
+
shouldCreateCheckRun: boolean;
|
|
159
|
+
shouldCreatePrComment: false;
|
|
160
|
+
privacy: {
|
|
161
|
+
includesRawSource: false;
|
|
162
|
+
includesRawDiffs: false;
|
|
163
|
+
includesSecrets: false;
|
|
164
|
+
includesUntrustedPrText: false;
|
|
165
|
+
includesCustomerPayloads: false;
|
|
166
|
+
};
|
|
167
|
+
}
|
|
80
168
|
export type HostedQueueCleanupTrigger = "repository_removed" | "installation_deleted" | "repeated_cleanup";
|
|
81
169
|
export interface HostedQueueCleanupJobState {
|
|
82
170
|
key: string;
|
|
@@ -260,11 +348,13 @@ export declare const HOSTED_PRIVACY_DEFAULTS: {
|
|
|
260
348
|
readonly deleteWorkerCheckout: "after_scan_completion";
|
|
261
349
|
};
|
|
262
350
|
export declare function verifyGitHubWebhook(input: GitHubWebhookInput): GitHubWebhookDecision;
|
|
351
|
+
export declare function planHostedPullRequestWebhookIntake(input: HostedPullRequestWebhookIntakeInput): HostedPullRequestWebhookIntakeDecision;
|
|
263
352
|
export declare function buildHostedScanIdentity(input: HostedScanIdentityInput): HostedScanIdentity;
|
|
264
353
|
export declare function parseHostedPullRequestEvent(input: HostedPullRequestEventInput): HostedPullRequestEventDecision;
|
|
265
354
|
export declare function authorizeInstallationTokenScope(input: InstallationScopeInput): InstallationScopeDecision;
|
|
266
355
|
export declare function getHostedScanIdempotencyKey(identity: HostedScanIdentity): string;
|
|
267
356
|
export declare function upsertHostedScanJob(queue: Map<string, HostedScanJobState>, input: HostedScanJobInput): HostedScanJobDecision;
|
|
357
|
+
export declare function planHostedScanQueueUpsert(input: HostedScanQueueUpsertInput): HostedScanQueueUpsertDecision;
|
|
268
358
|
export declare function getHostedQueueCleanupIdempotencyKey(input: {
|
|
269
359
|
trigger: HostedQueueCleanupTrigger;
|
|
270
360
|
installationId: number;
|
package/dist/hosted/contracts.js
CHANGED
|
@@ -42,6 +42,65 @@ export function verifyGitHubWebhook(input) {
|
|
|
42
42
|
deliveryId
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
+
export function planHostedPullRequestWebhookIntake(input) {
|
|
46
|
+
const signatureDecision = verifyGitHubWebhook({
|
|
47
|
+
payload: input.payload,
|
|
48
|
+
signatureHeader: input.signatureHeader,
|
|
49
|
+
signingKey: input.signingKey,
|
|
50
|
+
deliveryId: input.deliveryId,
|
|
51
|
+
seenDeliveryIds: input.seenDeliveryIds
|
|
52
|
+
});
|
|
53
|
+
if (!signatureDecision.accepted) {
|
|
54
|
+
return rejectPullRequestWebhookIntake("signature", signatureDecision.reason ?? "invalid_signature", input.deliveryId);
|
|
55
|
+
}
|
|
56
|
+
if (!input.deliveryId) {
|
|
57
|
+
return rejectPullRequestWebhookIntake("event", "missing_delivery_id");
|
|
58
|
+
}
|
|
59
|
+
const payload = parseJsonPayload(input.payload);
|
|
60
|
+
if (!payload) {
|
|
61
|
+
return rejectPullRequestWebhookIntake("payload", "invalid_json", input.deliveryId);
|
|
62
|
+
}
|
|
63
|
+
const eventDecision = parseHostedPullRequestEvent({
|
|
64
|
+
payload,
|
|
65
|
+
scannerVersion: input.scannerVersion,
|
|
66
|
+
allowDraft: input.allowDraft,
|
|
67
|
+
supportedActions: input.supportedActions
|
|
68
|
+
});
|
|
69
|
+
if (!eventDecision.accepted || !eventDecision.identity) {
|
|
70
|
+
return rejectPullRequestWebhookIntake("event", eventDecision.reason ?? "missing_required_field", input.deliveryId, eventDecision.action);
|
|
71
|
+
}
|
|
72
|
+
const scopeDecision = authorizeInstallationTokenScope({
|
|
73
|
+
identity: eventDecision.identity,
|
|
74
|
+
installationId: eventDecision.identity.installationId,
|
|
75
|
+
selectedRepositoryIds: input.selectedRepositoryIds,
|
|
76
|
+
removedRepositoryIds: input.removedRepositoryIds
|
|
77
|
+
});
|
|
78
|
+
if (!scopeDecision.authorized) {
|
|
79
|
+
return rejectPullRequestWebhookIntake("installation_scope", scopeDecision.reason ?? "repository_not_installed", input.deliveryId, eventDecision.action, eventDecision.identity);
|
|
80
|
+
}
|
|
81
|
+
const queueDecision = upsertHostedScanJob(input.queue, {
|
|
82
|
+
identity: eventDecision.identity,
|
|
83
|
+
deliveryId: input.deliveryId,
|
|
84
|
+
manualRerun: input.manualRerun
|
|
85
|
+
});
|
|
86
|
+
const checkRunOnlyQueueDecision = {
|
|
87
|
+
...queueDecision,
|
|
88
|
+
shouldCreatePrComment: false
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
accepted: true,
|
|
92
|
+
stage: "queue",
|
|
93
|
+
action: eventDecision.action,
|
|
94
|
+
deliveryId: input.deliveryId,
|
|
95
|
+
identity: eventDecision.identity,
|
|
96
|
+
job: checkRunOnlyQueueDecision,
|
|
97
|
+
shouldQueueScanJob: queueDecision.created || input.manualRerun === true,
|
|
98
|
+
shouldFetchRepository: queueDecision.shouldCreateCheckRun,
|
|
99
|
+
shouldCreateCheckRun: queueDecision.shouldCreateCheckRun,
|
|
100
|
+
shouldCreatePrComment: false,
|
|
101
|
+
privacy: hostedWebhookIntakePrivacy()
|
|
102
|
+
};
|
|
103
|
+
}
|
|
45
104
|
export function buildHostedScanIdentity(input) {
|
|
46
105
|
return {
|
|
47
106
|
installationId: input.installationId,
|
|
@@ -145,7 +204,7 @@ export function upsertHostedScanJob(queue, input) {
|
|
|
145
204
|
reusedExistingReport: false,
|
|
146
205
|
attempt: 1,
|
|
147
206
|
shouldCreateCheckRun: true,
|
|
148
|
-
shouldCreatePrComment:
|
|
207
|
+
shouldCreatePrComment: false
|
|
149
208
|
};
|
|
150
209
|
}
|
|
151
210
|
if (!existing.deliveryIds.includes(input.deliveryId)) {
|
|
@@ -163,6 +222,76 @@ export function upsertHostedScanJob(queue, input) {
|
|
|
163
222
|
shouldCreatePrComment: false
|
|
164
223
|
};
|
|
165
224
|
}
|
|
225
|
+
export function planHostedScanQueueUpsert(input) {
|
|
226
|
+
const key = getHostedScanIdempotencyKey(input.identity);
|
|
227
|
+
const existing = input.queue.get(key);
|
|
228
|
+
if (!existing) {
|
|
229
|
+
const record = {
|
|
230
|
+
key,
|
|
231
|
+
identity: input.identity,
|
|
232
|
+
status: "queued",
|
|
233
|
+
attempt: 1,
|
|
234
|
+
deliveryIds: [input.deliveryId],
|
|
235
|
+
createdAt: input.requestedAt,
|
|
236
|
+
updatedAt: input.requestedAt
|
|
237
|
+
};
|
|
238
|
+
input.queue.set(key, record);
|
|
239
|
+
return {
|
|
240
|
+
key,
|
|
241
|
+
idempotent: true,
|
|
242
|
+
created: true,
|
|
243
|
+
reusedExistingJob: false,
|
|
244
|
+
attempt: record.attempt,
|
|
245
|
+
queueRecord: { ...record, deliveryIds: [...record.deliveryIds] },
|
|
246
|
+
queuePayload: createHostedScanQueuePayload(record, input.deliveryId, input.requestedAt),
|
|
247
|
+
shouldEnqueueWorker: true,
|
|
248
|
+
shouldReuseCompletedReport: false,
|
|
249
|
+
shouldCreateCheckRun: true,
|
|
250
|
+
shouldCreatePrComment: false,
|
|
251
|
+
privacy: hostedScanQueuePrivacy()
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (!existing.deliveryIds.includes(input.deliveryId)) {
|
|
255
|
+
existing.deliveryIds.push(input.deliveryId);
|
|
256
|
+
}
|
|
257
|
+
const existingStatus = existing.status;
|
|
258
|
+
if (input.manualRerun) {
|
|
259
|
+
existing.attempt += 1;
|
|
260
|
+
existing.status = "queued";
|
|
261
|
+
existing.updatedAt = input.requestedAt;
|
|
262
|
+
return {
|
|
263
|
+
key,
|
|
264
|
+
idempotent: true,
|
|
265
|
+
created: false,
|
|
266
|
+
reusedExistingJob: false,
|
|
267
|
+
existingStatus,
|
|
268
|
+
attempt: existing.attempt,
|
|
269
|
+
queueRecord: cloneHostedScanQueueRecord(existing),
|
|
270
|
+
queuePayload: createHostedScanQueuePayload(existing, input.deliveryId, input.requestedAt),
|
|
271
|
+
shouldEnqueueWorker: true,
|
|
272
|
+
shouldReuseCompletedReport: false,
|
|
273
|
+
shouldCreateCheckRun: true,
|
|
274
|
+
shouldCreatePrComment: false,
|
|
275
|
+
privacy: hostedScanQueuePrivacy()
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
existing.updatedAt = input.requestedAt;
|
|
279
|
+
return {
|
|
280
|
+
key,
|
|
281
|
+
idempotent: true,
|
|
282
|
+
created: false,
|
|
283
|
+
reusedExistingJob: true,
|
|
284
|
+
existingStatus,
|
|
285
|
+
attempt: existing.attempt,
|
|
286
|
+
queueRecord: cloneHostedScanQueueRecord(existing),
|
|
287
|
+
queuePayload: createHostedScanQueuePayload(existing, input.deliveryId, input.requestedAt),
|
|
288
|
+
shouldEnqueueWorker: false,
|
|
289
|
+
shouldReuseCompletedReport: existingStatus === "completed",
|
|
290
|
+
shouldCreateCheckRun: false,
|
|
291
|
+
shouldCreatePrComment: false,
|
|
292
|
+
privacy: hostedScanQueuePrivacy()
|
|
293
|
+
};
|
|
294
|
+
}
|
|
166
295
|
export function getHostedQueueCleanupIdempotencyKey(input) {
|
|
167
296
|
return ["queue-cleanup", input.trigger, input.installationId, input.repositoryId ?? "all"].join(":");
|
|
168
297
|
}
|
|
@@ -357,6 +486,65 @@ function rejectInstallationScope(reason) {
|
|
|
357
486
|
reason
|
|
358
487
|
};
|
|
359
488
|
}
|
|
489
|
+
function rejectPullRequestWebhookIntake(stage, reason, deliveryId, action, identity) {
|
|
490
|
+
return {
|
|
491
|
+
accepted: false,
|
|
492
|
+
stage,
|
|
493
|
+
reason,
|
|
494
|
+
...(deliveryId === undefined ? {} : { deliveryId }),
|
|
495
|
+
...(action === undefined ? {} : { action }),
|
|
496
|
+
...(identity === undefined ? {} : { identity }),
|
|
497
|
+
shouldQueueScanJob: false,
|
|
498
|
+
shouldFetchRepository: false,
|
|
499
|
+
shouldCreateCheckRun: false,
|
|
500
|
+
shouldCreatePrComment: false,
|
|
501
|
+
privacy: hostedWebhookIntakePrivacy()
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function hostedWebhookIntakePrivacy() {
|
|
505
|
+
return {
|
|
506
|
+
includesRawWebhookPayload: false,
|
|
507
|
+
includesUntrustedPrText: false,
|
|
508
|
+
includesRawSource: false,
|
|
509
|
+
includesRawDiffs: false,
|
|
510
|
+
includesSecrets: false,
|
|
511
|
+
includesCustomerPayloads: false
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function createHostedScanQueuePayload(record, deliveryId, requestedAt) {
|
|
515
|
+
return {
|
|
516
|
+
key: record.key,
|
|
517
|
+
identity: record.identity,
|
|
518
|
+
deliveryId,
|
|
519
|
+
attempt: record.attempt,
|
|
520
|
+
requestedAt,
|
|
521
|
+
source: "github_pull_request"
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function cloneHostedScanQueueRecord(record) {
|
|
525
|
+
return {
|
|
526
|
+
...record,
|
|
527
|
+
identity: { ...record.identity },
|
|
528
|
+
deliveryIds: [...record.deliveryIds]
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function hostedScanQueuePrivacy() {
|
|
532
|
+
return {
|
|
533
|
+
includesRawSource: false,
|
|
534
|
+
includesRawDiffs: false,
|
|
535
|
+
includesSecrets: false,
|
|
536
|
+
includesUntrustedPrText: false,
|
|
537
|
+
includesCustomerPayloads: false
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function parseJsonPayload(payload) {
|
|
541
|
+
try {
|
|
542
|
+
return JSON.parse(Buffer.isBuffer(payload) ? payload.toString("utf8") : payload);
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
360
548
|
function queueCleanupMatches(job, input, scope) {
|
|
361
549
|
if (job.identity.installationId !== input.installationId) {
|
|
362
550
|
return false;
|
package/docs/github-action.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`ai-saas-guard` ships as a composite GitHub Action for pull request and code scanning workflows.
|
|
4
4
|
|
|
5
|
-
Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.
|
|
5
|
+
Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.12.0` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
|
|
6
6
|
|
|
7
7
|
## PR Summary
|
|
8
8
|
|
|
@@ -4,6 +4,49 @@ This document collects pure hosted contracts that can be tested before any hoste
|
|
|
4
4
|
|
|
5
5
|
The helpers live in `src/hosted/contracts.ts` and are exported from `ai-saas-guard/hosted/contracts`.
|
|
6
6
|
|
|
7
|
+
## Pull Request Webhook Intake Planner
|
|
8
|
+
|
|
9
|
+
The pull request webhook intake planner is the first pure implementation slice for the future hosted service. It composes the earlier contracts into one safe order without starting a server or calling GitHub APIs.
|
|
10
|
+
|
|
11
|
+
Default behavior:
|
|
12
|
+
|
|
13
|
+
- verify `X-Hub-Signature-256` before parsing JSON, queueing work, authorizing token scope, or planning repository fetches
|
|
14
|
+
- reject invalid, missing, malformed, or replayed signatures before payload parsing
|
|
15
|
+
- parse only signed pull request payloads
|
|
16
|
+
- derive scan identity from trusted GitHub event fields through the webhook event parser
|
|
17
|
+
- authorize selected-repository installation scope before any fetch is planned
|
|
18
|
+
- upsert one idempotent scan job by installation, repository, pull request, head SHA, and scanner version
|
|
19
|
+
- default to check-run-only output; PR comments remain disabled for the first hosted slice
|
|
20
|
+
|
|
21
|
+
Privacy boundaries:
|
|
22
|
+
|
|
23
|
+
- return only trusted identity, queue metadata, stage, reason, and booleans needed by an ingress or worker
|
|
24
|
+
- do not return raw webhook payloads, untrusted PR text, raw source, raw diffs, secrets, or customer payloads
|
|
25
|
+
- keep local CLI usage independent from the hosted service
|
|
26
|
+
|
|
27
|
+
The exported helper is `planHostedPullRequestWebhookIntake`. It is intentionally service-free: callers still need a real webhook server, queue provider, installation token lookup, worker checkout, scanner execution, compact report storage, and GitHub Checks API writer before any hosted environment exists.
|
|
28
|
+
|
|
29
|
+
## Durable Scan Queue Planner
|
|
30
|
+
|
|
31
|
+
The durable scan queue planner defines how the future hosted service should create or reuse scan jobs once a signed pull request webhook has produced trusted scan identity. It is a pure planner only: it does not connect to a queue provider, run a worker, fetch source, write reports, or call GitHub APIs.
|
|
32
|
+
|
|
33
|
+
Default behavior:
|
|
34
|
+
|
|
35
|
+
- compute the same logical scan key from installation ID, repository ID, pull request number, head SHA, and scanner version
|
|
36
|
+
- create one queued job when no matching job exists
|
|
37
|
+
- reuse existing queued, running, or completed jobs for duplicate deliveries
|
|
38
|
+
- reuse completed compact reports instead of enqueueing duplicate worker work
|
|
39
|
+
- allow manual reruns to increment `attempt` while keeping the same logical scan key
|
|
40
|
+
- keep the first hosted slice check-run-only; PR comments remain disabled
|
|
41
|
+
|
|
42
|
+
Queue payload boundaries:
|
|
43
|
+
|
|
44
|
+
- include only scan identity, job key, delivery ID, attempt, requested time, and source
|
|
45
|
+
- do not include raw source, raw diffs, secret values, untrusted PR text, webhook payload bodies, customer payloads, private URLs, or worker checkout paths
|
|
46
|
+
- return safe queue metadata that can be stored by a durable queue without leaking source code
|
|
47
|
+
|
|
48
|
+
The exported helper is `planHostedScanQueueUpsert`. It is intended to be the queue-provider-independent contract for the first real hosted queue implementation.
|
|
49
|
+
|
|
7
50
|
## Webhook Event Parser
|
|
8
51
|
|
|
9
52
|
The webhook event parser runs after webhook signature verification. It converts a reduced GitHub `pull_request` webhook payload into a queue-safe scan request identity.
|
|
@@ -139,6 +182,13 @@ These contracts do not:
|
|
|
139
182
|
|
|
140
183
|
Automated tests must cover:
|
|
141
184
|
|
|
185
|
+
- signed pull request webhook intake verifies signatures before JSON parsing or queueing
|
|
186
|
+
- accepted pull request webhook intake queues one check-run-only scan request from trusted fields
|
|
187
|
+
- rejected installation scope stops before repository fetch planning
|
|
188
|
+
- durable scan queue planning creates one queued job for a new trusted scan key
|
|
189
|
+
- duplicate deliveries reuse queued, running, and completed jobs without enqueueing duplicate worker work
|
|
190
|
+
- completed duplicate jobs reuse compact reports
|
|
191
|
+
- manual reruns increment attempt without changing the logical scan key
|
|
142
192
|
- accepted pull request events build the expected trusted scan identity
|
|
143
193
|
- unsupported actions are rejected
|
|
144
194
|
- draft pull requests are rejected by default
|
package/docs/npm-publishing.md
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
## Current State
|
|
6
6
|
|
|
7
7
|
- Package name: `ai-saas-guard`
|
|
8
|
-
- Current version: `0.
|
|
8
|
+
- Current version: `0.12.0`
|
|
9
9
|
- npm registry state: published at <https://www.npmjs.com/package/ai-saas-guard>
|
|
10
10
|
- First npm-published version: `0.1.1`
|
|
11
|
-
- GitHub Release: `v0.
|
|
11
|
+
- GitHub Release: `v0.12.0`
|
|
12
12
|
- Publish workflow: `.github/workflows/npm-publish.yml`
|
|
13
13
|
- Trusted Publisher: GitHub Actions, `zr9959/ai-saas-guard`, workflow `npm-publish.yml`, allowed action `npm publish`
|
|
14
14
|
- Long-lived npm publish token: not required
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
Use GitHub Actions with npm Trusted Publisher/OIDC:
|
|
19
19
|
|
|
20
|
-
1. Create and review a release tag such as `v0.
|
|
20
|
+
1. Create and review a release tag such as `v0.12.0`.
|
|
21
21
|
2. Publish from the GitHub Release or run the `Publish npm` workflow manually with `ref` set to that tag.
|
|
22
22
|
3. Keep `permissions.id-token: write` in the workflow so npm can exchange the GitHub Actions OIDC identity for a short-lived publish credential.
|
|
23
23
|
4. Run `npm publish --access public` from the workflow. Trusted publishing automatically generates provenance for this public package from this public repository.
|
package/docs/project-handoff.md
CHANGED
|
@@ -57,9 +57,9 @@ Implemented surfaces:
|
|
|
57
57
|
- hosted operational release gate document requiring hosted CI, webhook replay, dependency and container scanning, privacy and retention verification, worker cleanup, monitoring and alerting, manual rollback, and incident response evidence before exposure
|
|
58
58
|
- hosted uninstall and data deletion document defining repository removal, full app uninstall, compact report deletion, queue cancellation, audit record retention, repeated cleanup idempotency, and user-facing deletion wording
|
|
59
59
|
- hosted pricing and packaging document defining open-source CLI boundaries, free/public repo hosted behavior, private repo hosted behavior, PR comments, saved reports, team policy, optional Launch Review, and no pentest/certification/full-audit claims
|
|
60
|
-
- hosted pre-implementation contracts document, hosted compact report fixture, and pure helpers for queue-safe pull request event parsing from trusted GitHub event fields, bounded check-run summary rendering, idempotent queue cleanup planning, and worker checkout cleanup planning
|
|
60
|
+
- hosted pre-implementation contracts document, hosted compact report fixture, and pure helpers for pull request webhook intake planning, durable scan queue upsert planning, queue-safe pull request event parsing from trusted GitHub event fields, bounded check-run summary rendering, idempotent queue cleanup planning, and worker checkout cleanup planning
|
|
61
61
|
- implementation-ready hosted GitHub App permission contract for required permissions, optional PR comment permissions, selected repository installation, and out-of-scope broad permissions
|
|
62
|
-
- pure hosted GitHub App contract helpers and tests for webhook verification, installation token scoping, scan queue idempotency, compact reports, and retention limits
|
|
62
|
+
- pure hosted GitHub App contract helpers and tests for webhook intake order, webhook verification, installation token scoping, durable scan queue idempotency, compact reports, and retention limits
|
|
63
63
|
- GitHub issue templates for bug reports, false positives, false negatives, rule requests, and public-safe security reports
|
|
64
64
|
- CODEOWNERS for source, tests, docs, workflows, Action, and package metadata
|
|
65
65
|
- JSON output
|
|
@@ -113,7 +113,8 @@ GitHub Project:
|
|
|
113
113
|
|
|
114
114
|
Current issue set:
|
|
115
115
|
|
|
116
|
-
-
|
|
116
|
+
- Closed hosted MVP issue: #24 webhook intake.
|
|
117
|
+
- Open hosted MVP roadmap issues: #25 idempotent queue contract, #26 read-only worker checkout, #27 Check summaries, #28 retention/uninstall cleanup, and #29 hosted operational release gate.
|
|
117
118
|
|
|
118
119
|
CI:
|
|
119
120
|
|
|
@@ -125,7 +126,7 @@ CI:
|
|
|
125
126
|
Publishing:
|
|
126
127
|
|
|
127
128
|
- npm package: `ai-saas-guard`
|
|
128
|
-
- Current release line: `v0.
|
|
129
|
+
- Current release line: `v0.12.0`
|
|
129
130
|
- Publish workflow: `.github/workflows/npm-publish.yml`
|
|
130
131
|
- Trusted Publisher: GitHub Actions for `zr9959/ai-saas-guard`, workflow `npm-publish.yml`
|
|
131
132
|
- Long-lived npm publish tokens should not be required.
|
|
@@ -137,6 +138,7 @@ Allowed in this public repository:
|
|
|
137
138
|
- CLI source code
|
|
138
139
|
- tests and intentionally vulnerable fixtures
|
|
139
140
|
- public docs
|
|
141
|
+
- English README and Chinese README; when `README.md` changes, review and update `README.zh-CN.md` in the same change
|
|
140
142
|
- GitHub Action wrapper
|
|
141
143
|
- examples that contain only inert fake data
|
|
142
144
|
- release-quality process docs
|
|
@@ -235,6 +235,7 @@ P0:
|
|
|
235
235
|
- `types` points to generated declaration files if package exports TypeScript API.
|
|
236
236
|
- `exports` is accurate.
|
|
237
237
|
- README install examples match the published package name.
|
|
238
|
+
- If `README.md` changes, `README.zh-CN.md` must be reviewed and updated or explicitly confirmed still current.
|
|
238
239
|
- Version follows semver.
|
|
239
240
|
- Release notes state breaking changes, new checks, false-positive changes, and migration notes.
|
|
240
241
|
- Publish with npm trusted publishing/OIDC when possible.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-saas-guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Repo-local launch-readiness scanner for AI-built SaaS apps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/zr9959/ai-saas-guard#readme",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"docs",
|
|
53
53
|
"dist",
|
|
54
54
|
"examples",
|
|
55
|
-
"README.md"
|
|
55
|
+
"README.md",
|
|
56
|
+
"README.zh-CN.md"
|
|
56
57
|
],
|
|
57
58
|
"license": "MIT",
|
|
58
59
|
"devDependencies": {
|