ai-saas-guard 0.25.0 → 0.26.1
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 +21 -10
- package/README.zh-CN.md +20 -11
- package/action.yml +2 -2
- package/dist/cli.js +17 -5
- package/dist/commands/checkActions.d.ts +2 -0
- package/dist/commands/checkActions.js +4 -0
- package/dist/commands/checkMcp.d.ts +2 -2
- package/dist/commands/checkMcp.js +1 -1
- package/dist/commands/checkSupabase.d.ts +2 -2
- package/dist/commands/checkSupabase.js +1 -1
- package/dist/commands/scan.js +9 -3
- package/dist/hosted/app.js +8 -1
- package/dist/hosted/contracts.js +1 -1
- package/dist/hosted/production-adapters.js +8 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/report/markdown.js +34 -0
- package/dist/report/terminal.js +40 -0
- package/dist/rules/catalog.js +182 -0
- package/dist/scanners/actions.d.ts +3 -0
- package/dist/scanners/actions.js +173 -0
- package/dist/scanners/deploy.js +146 -1
- package/dist/scanners/gitDiff.js +180 -4
- package/dist/scanners/mcp.d.ts +3 -1
- package/dist/scanners/mcp.js +122 -11
- package/dist/scanners/silentSuccess.d.ts +3 -0
- package/dist/scanners/silentSuccess.js +222 -0
- package/dist/scanners/supabase.d.ts +3 -1
- package/dist/scanners/supabase.js +171 -2
- package/dist/types.d.ts +31 -3
- package/docs/github-action.md +15 -1
- package/docs/github-app-deployment.md +1 -1
- package/docs/hosted-operations-evidence.md +43 -0
- package/docs/launch-readiness-checklist.md +23 -2
- package/docs/npm-publishing.md +4 -3
- package/docs/positioning.md +3 -2
- package/docs/project-handoff.md +5 -4
- package/docs/reddit-github-feasibility-report-2026-05-24.md +260 -0
- package/docs/reddit-market-knowledge-base-2026-05-24.md +40 -0
- package/docs/rules.md +42 -0
- package/hosted/cloudflare-worker/README.md +3 -2
- package/hosted/cloudflare-worker/src/index.js +574 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,8 @@ The risky parts are often not the obvious UI bugs. They are the small changes th
|
|
|
37
37
|
- Can a Stripe webhook grant access twice, miss a failed payment, or trust an unsigned request?
|
|
38
38
|
- Did a public environment variable expose a secret?
|
|
39
39
|
- Did an MCP tool get shell, database, or broad filesystem access?
|
|
40
|
+
- Did AI-generated error handling return fake success or demo data after a real provider failed?
|
|
41
|
+
- Will the Next/Vercel deploy have the headers, env docs, logging, and request behavior needed for launch?
|
|
40
42
|
- Did a pull request hide auth, billing, or deploy changes inside a large AI-generated diff?
|
|
41
43
|
|
|
42
44
|
`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.
|
|
@@ -74,8 +76,9 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
|
|
|
74
76
|
| JSON and SARIF output | Available |
|
|
75
77
|
| Composite GitHub Action | Available |
|
|
76
78
|
| Project config | `.ai-saas-guard.json` rule toggles, severity overrides, and fail thresholds |
|
|
77
|
-
| Versioned Action tags | `v0.
|
|
78
|
-
| npm package | `ai-saas-guard@0.
|
|
79
|
+
| Versioned Action tags | `v0.26.1`, `v0` |
|
|
80
|
+
| npm package | `ai-saas-guard@0.26.1` |
|
|
81
|
+
| Current release | `0.26.1` launch-risk expansion |
|
|
79
82
|
| npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
|
|
80
83
|
| Repository trust hardening | Strict branch protection, Dependabot, CodeQL, fast-check fuzzing, signed release provenance assets, private vulnerability reporting, secret scanning, and push protection |
|
|
81
84
|
| Runtime hardening | Per-file and total text scan caps, escaped markdown evidence, 1 MiB hosted webhook payload cap, stricter hosted deployment blockers |
|
|
@@ -83,7 +86,8 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
|
|
|
83
86
|
| Hosted app skeleton | Node/container HTTP ingress, health route, worker tick, in-memory provider adapters, and deployment plan validation |
|
|
84
87
|
| Hosted staging deployment planner | Provider binding, staging release-gate evidence, Node/container deployment composition, and GitHub App promotion gating |
|
|
85
88
|
| Hosted staging harness | File-backed webhook replay, queue/report/Check Run artifacts, worker cleanup verification, and local release-gate evidence fixtures |
|
|
86
|
-
| Cloudflare hosted ingress | Deployed at `https://ai-saas-guard-hosted.zr9959.workers.dev
|
|
89
|
+
| Cloudflare hosted ingress | Deployed at `https://ai-saas-guard-hosted.zr9959.workers.dev`; Worker health and Check Run publisher configuration are live, but end-to-end GitHub App webhook delivery is still blocked pending private App settings verification |
|
|
90
|
+
| Hosted operations evidence | Recorded in [docs/hosted-operations-evidence.md](docs/hosted-operations-evidence.md) |
|
|
87
91
|
| Hosted GitHub App staging | Private App `ai-saas-guard-hosted` (`3834787`) installed on `zr9959/ai-saas-guard` with contents read, pull requests read, metadata read, and checks write |
|
|
88
92
|
| OpenSSF Best Practices | Passing badge, project `12955`; `.bestpractices.json` remains the conservative evidence record |
|
|
89
93
|
|
|
@@ -100,8 +104,11 @@ Run focused checks:
|
|
|
100
104
|
```bash
|
|
101
105
|
npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main
|
|
102
106
|
npx ai-saas-guard@latest check-supabase --root /path/to/your-saas
|
|
107
|
+
npx ai-saas-guard@latest check-supabase --root /path/to/your-saas --doctor
|
|
103
108
|
npx ai-saas-guard@latest check-stripe --root /path/to/your-saas
|
|
104
109
|
npx ai-saas-guard@latest check-mcp --root /path/to/your-saas
|
|
110
|
+
npx ai-saas-guard@latest check-mcp --root /path/to/your-saas --policy-template
|
|
111
|
+
npx ai-saas-guard@latest check-actions --root /path/to/your-saas
|
|
105
112
|
```
|
|
106
113
|
|
|
107
114
|
Machine-readable output:
|
|
@@ -131,6 +138,7 @@ node dist/cli.js pr-risk --root /path/to/your-saas --base origin/main
|
|
|
131
138
|
node dist/cli.js check-supabase --root /path/to/your-saas
|
|
132
139
|
node dist/cli.js check-stripe --root /path/to/your-saas
|
|
133
140
|
node dist/cli.js check-mcp --root /path/to/your-saas
|
|
141
|
+
node dist/cli.js check-actions --root /path/to/your-saas
|
|
134
142
|
```
|
|
135
143
|
|
|
136
144
|
## Example Finding
|
|
@@ -154,10 +162,12 @@ Evidence:
|
|
|
154
162
|
| Secrets and env | Secret-like values, risky `NEXT_PUBLIC_*` exposure |
|
|
155
163
|
| Stripe | Missing webhook route, unsigned webhook handling, parsed-body signature risk, missing idempotency, missing failure/cancel/update/refund paths |
|
|
156
164
|
| Supabase | RLS disabled on sensitive tables, broad `USING`/`WITH CHECK`, tenant membership patterns, weak write checks, storage object policy scope |
|
|
165
|
+
| Silent success | Swallowed provider errors, hardcoded fallback success, production mock/demo data in sensitive paths, temporary trust-boundary bypasses, skipped or placeholder tests |
|
|
157
166
|
| API routes | Auth checks without obvious ownership guards, missing rate-limit hints on sensitive mutation routes |
|
|
158
|
-
| MCP | Plaintext secrets, non-localhost binds, broad filesystem/write access, shell tools, raw SQL tools |
|
|
159
|
-
|
|
|
160
|
-
|
|
|
167
|
+
| MCP | Plaintext secrets, non-localhost binds, broad filesystem/write access, shell tools, raw SQL tools, side-effect classification, local policy and receipt template |
|
|
168
|
+
| Next/Vercel deploy | Static export/runtime mismatches, Edge runtime with Node-only APIs, missing security headers, undocumented server env, public env inventory, image/request amplification hints, missing request ID logging |
|
|
169
|
+
| GitHub Actions | Broad workflow permissions, stale PR runs, docs-only full CI, missing fail-fast secret checks, shallow `pr-risk` checkout, unpinned Action refs |
|
|
170
|
+
| PR risk | Auth, billing, RLS, env, deploy, API, storage, silent-success, test-removal, missing spec context, and large mixed-diff classification |
|
|
161
171
|
|
|
162
172
|
See [docs/rules.md](docs/rules.md) for the full rule map.
|
|
163
173
|
|
|
@@ -198,9 +208,10 @@ If `--base` cannot be resolved, `pr-risk` emits `pr-risk.diff-unavailable` inste
|
|
|
198
208
|
| --- | --- |
|
|
199
209
|
| `scan` | Broad local launch preflight across secrets, Stripe, Supabase, MCP, API routes, and deploy config |
|
|
200
210
|
| `pr-risk` | Classify the current git diff or a base branch diff for review priority; supports JSON, SARIF, and PR-focused markdown |
|
|
201
|
-
| `check-supabase` | Inspect migrations and policy files for RLS and ownership risks |
|
|
211
|
+
| `check-supabase` | Inspect migrations and policy files for RLS and ownership risks; use `--doctor` for static RLS debugging steps and SQL cookbook output |
|
|
202
212
|
| `check-stripe` | Inspect webhook handlers and billing lifecycle coverage |
|
|
203
|
-
| `check-mcp` | Inventory MCP configs and classify side effects |
|
|
213
|
+
| `check-mcp` | Inventory MCP configs and classify side effects; use `--policy-template` for a local allow/deny policy and tool-call receipt format |
|
|
214
|
+
| `check-actions` | Inspect GitHub Actions hygiene that affects AI-built SaaS launch readiness |
|
|
204
215
|
|
|
205
216
|
## Launch Readiness Checklist
|
|
206
217
|
|
|
@@ -240,7 +251,7 @@ The hosted staging deployment planner is documented in [docs/hosted-staging-depl
|
|
|
240
251
|
|
|
241
252
|
The hosted staging harness is documented in [docs/hosted-staging-harness.md](docs/hosted-staging-harness.md). It exports `createFileBackedHostedStagingHarness` and `createHostedStagingHarnessEvidence` from `ai-saas-guard/hosted/staging-harness`. It runs signed webhook replay through the provider-independent hosted runtime with local file-backed queue, compact report, and Check Run adapters, then verifies worker sandbox cleanup. It is a staging rehearsal tool only; it does not call cloud providers, create a GitHub App, publish live Check Runs, or expose a public hosted service.
|
|
242
253
|
|
|
243
|
-
The first live hosted ingress is deployed on Cloudflare Workers at `https://ai-saas-guard-hosted.zr9959.workers.dev` and documented in [hosted/cloudflare-worker/README.md](hosted/cloudflare-worker/README.md). It exposes `/healthz`, `/github/app/manifest-callback`, and signed `/github/webhook` intake backed by Cloudflare KV. A private staging GitHub App, `ai-saas-guard-hosted`, is installed on `zr9959/ai-saas-guard` with selected-repository access and the first-slice permission contract. The
|
|
254
|
+
The first live hosted ingress is deployed on Cloudflare Workers at `https://ai-saas-guard-hosted.zr9959.workers.dev` and documented in [hosted/cloudflare-worker/README.md](hosted/cloudflare-worker/README.md). It exposes `/healthz`, `/github/app/manifest-callback`, and signed `/github/webhook` intake backed by Cloudflare KV. A private staging GitHub App, `ai-saas-guard-hosted`, is installed on `zr9959/ai-saas-guard` with selected-repository access and the first-slice permission contract. The Worker can verify signatures, store compact pull request identity records, exchange a scoped installation token, fetch PR file metadata from GitHub, classify PR-risk hotspots, and publish a bounded Check Run summary. Current deployed evidence is tracked in [docs/hosted-operations-evidence.md](docs/hosted-operations-evidence.md): health and Check Run publisher configuration pass, but end-to-end GitHub App webhook delivery is still blocked until the private App webhook settings are verified. It still does not run a full source checkout scan worker or store raw webhook payloads, PR title/body text, raw diffs, source, secrets, checkout paths, or installation tokens.
|
|
244
255
|
|
|
245
256
|
The hosted operational release gate is documented in [docs/hosted-operational-release-gate.md](docs/hosted-operational-release-gate.md). It defines the hosted-specific CI, replay, queue, worker cleanup, privacy, monitoring, rollback, and incident-response evidence required before any hosted environment is exposed to users. The pure gate evaluator exported from `ai-saas-guard/hosted/contracts` blocks hosted exposure unless every P0 evidence item is fresh, a container digest is recorded, and release notes avoid pentest, certification, and full-audit claims.
|
|
246
257
|
|
|
@@ -288,7 +299,7 @@ Use `suppressions` for narrower false-positive handling when one rule is noisy o
|
|
|
288
299
|
|
|
289
300
|
## GitHub Action
|
|
290
301
|
|
|
291
|
-
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.
|
|
302
|
+
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.26.1` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
|
|
292
303
|
|
|
293
304
|
```yaml
|
|
294
305
|
name: ai-saas-guard
|
package/README.zh-CN.md
CHANGED
|
@@ -36,6 +36,8 @@ AI 能很快把一个 SaaS 从想法做成可运行的产品。真正难的是
|
|
|
36
36
|
- Stripe webhook 会不会重复开通权限、漏处理付款失败,或者信任未签名请求?
|
|
37
37
|
- `NEXT_PUBLIC_*` 里是不是不小心暴露了 secret?
|
|
38
38
|
- MCP 工具是不是拿到了 shell、数据库或过宽的文件系统权限?
|
|
39
|
+
- AI 生成的错误处理会不会在真实服务失败后仍然返回“成功”或 demo 数据?
|
|
40
|
+
- Next/Vercel 上线前是不是缺 security headers、env 文档、请求日志或高请求量风险提示?
|
|
39
41
|
- AI 生成的大 PR 里,是不是把 auth、billing 或 deploy 改动藏在 UI 调整中?
|
|
40
42
|
|
|
41
43
|
`ai-saas-guard` 是面向这个时刻的本地优先、review-first 上线预检工具。它不会证明你的应用绝对安全,也不是渗透测试、认证或完整安全审计。它的目标是给 founder、独立开发者、小团队和 reviewer 一份短而有证据的清单,告诉你上线或合并 PR 前最该先看哪里。
|
|
@@ -63,7 +65,7 @@ AI 能很快把一个 SaaS 从想法做成可运行的产品。真正难的是
|
|
|
63
65
|
|
|
64
66
|
这个仓库是公开 GitHub 仓库。
|
|
65
67
|
|
|
66
|
-
CLI 已发布到 npm:`ai-saas-guard@0.
|
|
68
|
+
CLI 已发布到 npm:`ai-saas-guard@0.26.1`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.26.1`。
|
|
67
69
|
|
|
68
70
|
| 模块 | 状态 |
|
|
69
71
|
| --- | --- |
|
|
@@ -74,8 +76,8 @@ CLI 已发布到 npm:`ai-saas-guard@0.25.0`。GitHub Action 支持 `v0` 浮动
|
|
|
74
76
|
| Markdown PR summary | 已可用 |
|
|
75
77
|
| GitHub Action | 已可用 |
|
|
76
78
|
| 项目配置 | `.ai-saas-guard.json` 支持规则开关、severity 覆盖和 fail threshold |
|
|
77
|
-
| 当前版本 | `0.
|
|
78
|
-
| Action 标签 | `v0.
|
|
79
|
+
| 当前版本 | `0.26.1` launch-risk expansion |
|
|
80
|
+
| Action 标签 | `v0.26.1`、`v0` |
|
|
79
81
|
| npm 发布 | GitHub Actions Trusted Publisher/OIDC,无需长期 npm token |
|
|
80
82
|
| 仓库可信度加固 | 严格 branch protection、Dependabot、CodeQL、fast-check fuzzing、signed release provenance assets、private vulnerability reporting、secret scanning 和 push protection |
|
|
81
83
|
| 运行时加固 | 单文件和总扫描文本预算、markdown evidence 转义、1 MiB hosted webhook payload 上限、更严格的 hosted deployment 阻断 |
|
|
@@ -83,7 +85,8 @@ CLI 已发布到 npm:`ai-saas-guard@0.25.0`。GitHub Action 支持 `v0` 浮动
|
|
|
83
85
|
| Hosted app skeleton | Node/container HTTP ingress、health route、worker tick、in-memory provider adapters 和 deployment plan 校验 |
|
|
84
86
|
| Hosted staging deployment planner | provider binding、staging release-gate evidence、Node/container deployment 组合和 GitHub App promotion gating |
|
|
85
87
|
| Hosted staging harness | 本地 file-backed webhook replay、queue/report/Check Run artifact、worker cleanup 校验和 release-gate evidence fixture |
|
|
86
|
-
| Cloudflare hosted ingress | 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev
|
|
88
|
+
| Cloudflare hosted ingress | 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`;Worker health 和 Check Run publisher 配置已在线,但端到端 GitHub App webhook delivery 仍需要验证私有 App 设置 |
|
|
89
|
+
| Hosted operations evidence | 已记录在 [docs/hosted-operations-evidence.md](docs/hosted-operations-evidence.md) |
|
|
87
90
|
| Hosted GitHub App staging | 私有 App `ai-saas-guard-hosted`(`3834787`)已安装到 `zr9959/ai-saas-guard`,权限为 contents read、pull requests read、metadata read、checks write |
|
|
88
91
|
| OpenSSF Best Practices | 已获得 passing badge,项目 `12955`;`.bestpractices.json` 继续作为保守证据记录 |
|
|
89
92
|
|
|
@@ -100,8 +103,11 @@ npx ai-saas-guard@latest scan --root /path/to/your-saas
|
|
|
100
103
|
```bash
|
|
101
104
|
npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main
|
|
102
105
|
npx ai-saas-guard@latest check-supabase --root /path/to/your-saas
|
|
106
|
+
npx ai-saas-guard@latest check-supabase --root /path/to/your-saas --doctor
|
|
103
107
|
npx ai-saas-guard@latest check-stripe --root /path/to/your-saas
|
|
104
108
|
npx ai-saas-guard@latest check-mcp --root /path/to/your-saas
|
|
109
|
+
npx ai-saas-guard@latest check-mcp --root /path/to/your-saas --policy-template
|
|
110
|
+
npx ai-saas-guard@latest check-actions --root /path/to/your-saas
|
|
105
111
|
```
|
|
106
112
|
|
|
107
113
|
机器可读输出:
|
|
@@ -128,9 +134,10 @@ node dist/cli.js scan --root /path/to/your-saas
|
|
|
128
134
|
| --- | --- |
|
|
129
135
|
| `scan` | 对 secrets、Stripe、Supabase、MCP、API routes、deploy config 做整体上线预检 |
|
|
130
136
|
| `pr-risk` | 分析当前 git diff 或指定 base branch diff,判断哪些文件和风险面应该先 review |
|
|
131
|
-
| `check-supabase` | 检查 migration 和 policy 文件里的 RLS、ownership、storage policy
|
|
137
|
+
| `check-supabase` | 检查 migration 和 policy 文件里的 RLS、ownership、storage policy 风险;`--doctor` 输出静态 RLS 调试步骤和 SQL cookbook |
|
|
132
138
|
| `check-stripe` | 检查 webhook 签名、raw body、幂等、订阅生命周期和 entitlement 更新路径 |
|
|
133
|
-
| `check-mcp` | 检查 MCP 配置里的 secret、非 localhost 绑定、shell/db/filesystem
|
|
139
|
+
| `check-mcp` | 检查 MCP 配置里的 secret、非 localhost 绑定、shell/db/filesystem 等副作用;`--policy-template` 输出本地 allow/deny policy 和 tool-call receipt 格式 |
|
|
140
|
+
| `check-actions` | 检查和 AI-built SaaS 上线有关的 GitHub Actions hygiene |
|
|
134
141
|
|
|
135
142
|
## 它会检查什么
|
|
136
143
|
|
|
@@ -139,10 +146,12 @@ node dist/cli.js scan --root /path/to/your-saas
|
|
|
139
146
|
| Secrets 和 env | 类似密钥的字符串、危险的 `NEXT_PUBLIC_*` 暴露 |
|
|
140
147
|
| Stripe | webhook 缺失、未验证签名、raw body 签名风险、缺幂等、缺失败/取消/退款/更新处理 |
|
|
141
148
|
| Supabase | 敏感表没启用 RLS、policy 过宽、缺少 ownership filter、`WITH CHECK` 过弱、storage object policy 过宽 |
|
|
149
|
+
| Silent success | 捕获错误后返回假成功、敏感路径里的 hardcoded fallback、production 路径引入 mock/demo data、临时绕过 auth/webhook/ownership、跳过或占位测试 |
|
|
142
150
|
| API routes | 有 auth 但缺少明显 ownership guard,敏感 mutation route 缺少 rate-limit 提示 |
|
|
143
|
-
| MCP | 明文 secret、非 localhost 绑定、过宽文件系统权限、shell 工具、raw SQL
|
|
144
|
-
|
|
|
145
|
-
|
|
|
151
|
+
| MCP | 明文 secret、非 localhost 绑定、过宽文件系统权限、shell 工具、raw SQL 工具、side-effect 分类、本地 policy/receipt 模板 |
|
|
152
|
+
| Next/Vercel deploy | Next static export 和 API route 冲突、Edge runtime 使用 Node-only API、security headers 缺失、server env 文档缺失、public env 盘点、image/request 放大风险、request ID logging 缺失 |
|
|
153
|
+
| GitHub Actions | workflow 权限过宽、PR workflow 缺 concurrency cancel、docs-only 改动跑全量 CI、secret/tool version 缺 fail-fast、`pr-risk` checkout 太浅、Action 未 pin SHA |
|
|
154
|
+
| PR risk | auth、billing、RLS、env、deploy、API、storage、silent-success、测试删除、缺 spec/context、大型混合 diff |
|
|
146
155
|
|
|
147
156
|
完整规则请看 [docs/rules.md](docs/rules.md)。
|
|
148
157
|
|
|
@@ -266,7 +275,7 @@ jobs:
|
|
|
266
275
|
|
|
267
276
|
## Hosted GitHub App 设计
|
|
268
277
|
|
|
269
|
-
当前仓库已经包含未来 Hosted GitHub App 的设计文档、纯契约测试,以及第一个真实 Cloudflare hosted ingress。私有 staging GitHub App `ai-saas-guard-hosted` 已安装到 `zr9959/ai-saas-guard`,Cloudflare
|
|
278
|
+
当前仓库已经包含未来 Hosted GitHub App 的设计文档、纯契约测试,以及第一个真实 Cloudflare hosted ingress。私有 staging GitHub App `ai-saas-guard-hosted` 已安装到 `zr9959/ai-saas-guard`,Cloudflare 已配置所需的云端凭据绑定。Worker 代码已经能接收签名 webhook、写入 KV 队列、换取 scoped installation token、读取 GitHub PR file metadata、做 compact PR-risk classification,并发布有长度上限的 Check Run summary;但当前端到端 GitHub App webhook delivery smoke 还被私有 App webhook 设置阻断,证据记录在 [docs/hosted-operations-evidence.md](docs/hosted-operations-evidence.md)。它还不是完整 source checkout scan worker。
|
|
270
279
|
|
|
271
280
|
相关文档:
|
|
272
281
|
|
|
@@ -295,7 +304,7 @@ jobs:
|
|
|
295
304
|
- Hosted Node/container app skeleton:`ai-saas-guard/hosted/app` 导出 `createHostedHttpApp`、`createInMemoryHostedAppPlatform` 和 `planHostedNodeContainerDeployment`,提供安全 `/healthz`、签名 `/github/webhook` ingress、单 job worker tick、测试用 in-memory provider adapters,以及 secret manager、queue、compact report store、worker sandbox、GitHub Checks publisher 的部署引用校验;它本身仍然不部署或暴露公开 hosted 服务
|
|
296
305
|
- Hosted staging deployment planner:`ai-saas-guard/hosted/staging` 导出 `planHostedProviderBinding`、`planHostedStagingDeployment` 和 `planHostedGitHubAppPromotion`,把真实 provider 引用、Node/container deployment plan、hosted operational release-gate evidence 和 GitHub App deployment planning 组合起来;缺少 queue、store、worker sandbox、Check Run publisher、logs、metrics、rollback 或 incident-response 引用时,会阻止 staging exposure 和 production promotion;它本身仍然不会调用云平台、创建 GitHub App 或暴露公开 hosted 服务
|
|
297
306
|
- Hosted staging harness:`ai-saas-guard/hosted/staging-harness` 导出 `createFileBackedHostedStagingHarness` 和 `createHostedStagingHarnessEvidence`,可以在本地用 file-backed queue、compact report、Check Run request 和 worker sandbox 跑通签名 webhook replay、worker tick 和 cleanup 校验;它只是 staging 演练工具,不会调用云平台、创建 GitHub App、写真实 Check Run 或暴露公开 hosted 服务
|
|
298
|
-
- Cloudflare hosted ingress:`hosted/cloudflare-worker` 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`,提供 `/healthz`、`/github/app/manifest-callback` 和签名 `/github/webhook` intake
|
|
307
|
+
- Cloudflare hosted ingress:`hosted/cloudflare-worker` 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`,提供 `/healthz`、`/github/app/manifest-callback` 和签名 `/github/webhook` intake;Worker 已具备 compact pull request identity、file/category risk signal 和 Check Run metadata 路径;staging GitHub App ID 为 `3834787`,installation ID 为 `135085075`;真实 GitHub App webhook delivery、完整 source checkout scan worker、monitoring、rollback 和 incident-response evidence 仍需要通过 hosted operational release gate
|
|
299
308
|
- webhook event parser
|
|
300
309
|
- check-run summary renderer
|
|
301
310
|
- Check Run publication planner:要求 repository `checks: write`,只从 compact report 生成有长度上限的 Check Run payload,包含 review categories、优先 review 文件、verification steps 和本地 CLI 复现命令;MVP 不发 PR comment
|
package/action.yml
CHANGED
|
@@ -4,7 +4,7 @@ author: ai-saas-guard
|
|
|
4
4
|
|
|
5
5
|
inputs:
|
|
6
6
|
command:
|
|
7
|
-
description: "Command to run: scan, check-supabase, check-stripe, check-mcp, or pr-risk."
|
|
7
|
+
description: "Command to run: scan, check-supabase, check-stripe, check-mcp, check-actions, or pr-risk."
|
|
8
8
|
required: false
|
|
9
9
|
default: scan
|
|
10
10
|
root:
|
|
@@ -59,7 +59,7 @@ runs:
|
|
|
59
59
|
set -o pipefail
|
|
60
60
|
|
|
61
61
|
case "${INPUT_COMMAND}" in
|
|
62
|
-
scan|check-supabase|check-stripe|check-mcp|pr-risk) ;;
|
|
62
|
+
scan|check-supabase|check-stripe|check-mcp|check-actions|pr-risk) ;;
|
|
63
63
|
*)
|
|
64
64
|
echo "Invalid command input: ${INPUT_COMMAND}" >&2
|
|
65
65
|
exit 2
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { applyGuardConfig, loadGuardConfig } from "./config.js";
|
|
4
|
-
import { checkMcp, checkStripe, checkSupabase, classifyPrRisk, scanRepository } from "./index.js";
|
|
4
|
+
import { checkActions, checkMcp, checkStripe, checkSupabase, classifyPrRisk, scanRepository } from "./index.js";
|
|
5
5
|
import { formatJsonReport } from "./report/json.js";
|
|
6
6
|
import { formatMarkdownReport } from "./report/markdown.js";
|
|
7
7
|
import { formatSarifReport } from "./report/sarif.js";
|
|
@@ -19,13 +19,16 @@ async function main(argv) {
|
|
|
19
19
|
report = await scanRepository({ rootDir: args.rootDir });
|
|
20
20
|
break;
|
|
21
21
|
case "check-supabase":
|
|
22
|
-
report = await checkSupabase({ rootDir: args.rootDir });
|
|
22
|
+
report = await checkSupabase({ rootDir: args.rootDir, doctor: args.doctor });
|
|
23
23
|
break;
|
|
24
24
|
case "check-stripe":
|
|
25
25
|
report = await checkStripe({ rootDir: args.rootDir });
|
|
26
26
|
break;
|
|
27
27
|
case "check-mcp":
|
|
28
|
-
report = await checkMcp({ rootDir: args.rootDir });
|
|
28
|
+
report = await checkMcp({ rootDir: args.rootDir, policyTemplate: args.policyTemplate });
|
|
29
|
+
break;
|
|
30
|
+
case "check-actions":
|
|
31
|
+
report = await checkActions({ rootDir: args.rootDir });
|
|
29
32
|
break;
|
|
30
33
|
case "pr-risk":
|
|
31
34
|
report = await classifyPrRisk({ rootDir: args.rootDir, base: args.base });
|
|
@@ -107,6 +110,14 @@ function parseArgs(argv) {
|
|
|
107
110
|
index += 1;
|
|
108
111
|
continue;
|
|
109
112
|
}
|
|
113
|
+
if (arg === "--doctor") {
|
|
114
|
+
result.doctor = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (arg === "--policy-template") {
|
|
118
|
+
result.policyTemplate = true;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
110
121
|
if (arg === "-h" || arg === "--help") {
|
|
111
122
|
result.command = "help";
|
|
112
123
|
continue;
|
|
@@ -154,9 +165,10 @@ Repo-local launch-readiness scanner for AI-built SaaS apps.
|
|
|
154
165
|
|
|
155
166
|
Usage:
|
|
156
167
|
ai-saas-guard scan [--root <repo>] [--config <file>] [--json|--sarif] [--fail-on <severity>]
|
|
157
|
-
ai-saas-guard check-supabase [--root <repo>] [--config <file>] [--json|--sarif] [--fail-on <severity>]
|
|
168
|
+
ai-saas-guard check-supabase [--root <repo>] [--config <file>] [--doctor] [--json|--sarif] [--fail-on <severity>]
|
|
158
169
|
ai-saas-guard check-stripe [--root <repo>] [--config <file>] [--json|--sarif] [--fail-on <severity>]
|
|
159
|
-
ai-saas-guard check-mcp [--root <repo>] [--config <file>] [--json|--sarif] [--fail-on <severity>]
|
|
170
|
+
ai-saas-guard check-mcp [--root <repo>] [--config <file>] [--policy-template] [--json|--sarif] [--fail-on <severity>]
|
|
171
|
+
ai-saas-guard check-actions [--root <repo>] [--config <file>] [--json|--sarif] [--fail-on <severity>]
|
|
160
172
|
ai-saas-guard pr-risk [--root <repo>] [--config <file>] [--base <branch>] [--json|--sarif|--markdown] [--fail-on <severity>]
|
|
161
173
|
|
|
162
174
|
Defaults:
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function checkMcp(options:
|
|
1
|
+
import type { McpOptions, McpReport } from "../types.js";
|
|
2
|
+
export declare function checkMcp(options: McpOptions): Promise<McpReport>;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function checkSupabase(options:
|
|
1
|
+
import type { SupabaseOptions, SupabaseReport } from "../types.js";
|
|
2
|
+
export declare function checkSupabase(options: SupabaseOptions): Promise<SupabaseReport>;
|
package/dist/commands/scan.js
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
import { createScanContext } from "../context.js";
|
|
2
2
|
import { createReport, uniqueFindings } from "../report/findings.js";
|
|
3
|
+
import { checkActions } from "../scanners/actions.js";
|
|
3
4
|
import { scanApiRoutes } from "../scanners/apiRoutes.js";
|
|
4
5
|
import { scanDeployConfig } from "../scanners/deploy.js";
|
|
5
6
|
import { checkMcp } from "../scanners/mcp.js";
|
|
6
7
|
import { scanNextPublicEnv, scanSecrets } from "../scanners/secrets.js";
|
|
8
|
+
import { scanSilentSuccess } from "../scanners/silentSuccess.js";
|
|
7
9
|
import { checkStripe } from "../scanners/stripe.js";
|
|
8
10
|
import { checkSupabase } from "../scanners/supabase.js";
|
|
9
11
|
export async function scanRepository(options) {
|
|
10
12
|
const context = await createScanContext(options.rootDir);
|
|
11
|
-
const [secretFindings, nextPublicFindings, stripeReport, supabaseReport, mcpReport, apiFindings, deployFindings] = await Promise.all([
|
|
13
|
+
const [secretFindings, nextPublicFindings, stripeReport, supabaseReport, mcpReport, apiFindings, deployFindings, silentSuccessFindings, actionsReport] = await Promise.all([
|
|
12
14
|
scanSecrets(context),
|
|
13
15
|
scanNextPublicEnv(context),
|
|
14
16
|
checkStripe(context),
|
|
15
17
|
checkSupabase(context),
|
|
16
18
|
checkMcp(context),
|
|
17
19
|
scanApiRoutes(context),
|
|
18
|
-
scanDeployConfig(context)
|
|
20
|
+
scanDeployConfig(context),
|
|
21
|
+
scanSilentSuccess(context),
|
|
22
|
+
checkActions(context)
|
|
19
23
|
]);
|
|
20
24
|
return createReport("scan", options.rootDir, uniqueFindings([
|
|
21
25
|
...secretFindings,
|
|
@@ -24,6 +28,8 @@ export async function scanRepository(options) {
|
|
|
24
28
|
...supabaseReport.findings,
|
|
25
29
|
...mcpReport.findings,
|
|
26
30
|
...apiFindings,
|
|
27
|
-
...deployFindings
|
|
31
|
+
...deployFindings,
|
|
32
|
+
...silentSuccessFindings,
|
|
33
|
+
...actionsReport.findings
|
|
28
34
|
]), {});
|
|
29
35
|
}
|
package/dist/hosted/app.js
CHANGED
|
@@ -261,7 +261,7 @@ function isValidSecretRef(value) {
|
|
|
261
261
|
return /^secret:[A-Za-z0-9._:/@-]+$/.test(value);
|
|
262
262
|
}
|
|
263
263
|
function normalizePublicBaseUrl(publicBaseUrl) {
|
|
264
|
-
return publicBaseUrl.trim()
|
|
264
|
+
return trimTrailingSlashes(publicBaseUrl.trim());
|
|
265
265
|
}
|
|
266
266
|
function isSafePublicHttpsUrl(value) {
|
|
267
267
|
try {
|
|
@@ -279,6 +279,13 @@ function isUnsafeHostedHostname(hostname) {
|
|
|
279
279
|
isUnsafeIpv4Hostname(normalized) ||
|
|
280
280
|
isUnsafeIpv6Hostname(normalized));
|
|
281
281
|
}
|
|
282
|
+
function trimTrailingSlashes(value) {
|
|
283
|
+
let end = value.length;
|
|
284
|
+
while (end > 0 && value[end - 1] === "/") {
|
|
285
|
+
end -= 1;
|
|
286
|
+
}
|
|
287
|
+
return value.slice(0, end);
|
|
288
|
+
}
|
|
282
289
|
function normalizeHostname(hostname) {
|
|
283
290
|
const lower = hostname.toLowerCase().replace(/\.$/, "");
|
|
284
291
|
return lower.startsWith("[") && lower.endsWith("]") ? lower.slice(1, -1) : lower;
|
package/dist/hosted/contracts.js
CHANGED
|
@@ -1156,7 +1156,7 @@ function getHostedCheckRunFiles(report) {
|
|
|
1156
1156
|
return [...new Set(report.evidence.map((finding) => finding.file))].slice(0, 10);
|
|
1157
1157
|
}
|
|
1158
1158
|
function escapeMarkdownTableCell(value) {
|
|
1159
|
-
return value.
|
|
1159
|
+
return value.replaceAll("\\", "\\\\").replaceAll("|", "\\|").replaceAll("\r", " ").replaceAll("\n", " ");
|
|
1160
1160
|
}
|
|
1161
1161
|
function capitalize(value) {
|
|
1162
1162
|
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
@@ -190,7 +190,14 @@ function safeApiUrlBlockedReasons(apiBaseUrl) {
|
|
|
190
190
|
}
|
|
191
191
|
function normalizeApiBaseUrl(apiBaseUrl) {
|
|
192
192
|
const value = apiBaseUrl?.trim() || "https://api.github.com";
|
|
193
|
-
return value
|
|
193
|
+
return trimTrailingSlashes(value);
|
|
194
|
+
}
|
|
195
|
+
function trimTrailingSlashes(value) {
|
|
196
|
+
let end = value.length;
|
|
197
|
+
while (end > 0 && value[end - 1] === "/") {
|
|
198
|
+
end -= 1;
|
|
199
|
+
}
|
|
200
|
+
return value.slice(0, end);
|
|
194
201
|
}
|
|
195
202
|
function permissionsForPurpose(purpose) {
|
|
196
203
|
if (purpose === "worker_checkout") {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,11 +2,12 @@ export { scanRepository } from "./commands/scan.js";
|
|
|
2
2
|
export { checkStripe } from "./commands/checkStripe.js";
|
|
3
3
|
export { checkSupabase } from "./commands/checkSupabase.js";
|
|
4
4
|
export { checkMcp } from "./commands/checkMcp.js";
|
|
5
|
+
export { checkActions } from "./commands/checkActions.js";
|
|
5
6
|
export { classifyPrRisk } from "./commands/prRisk.js";
|
|
6
7
|
export { applyGuardConfig, defaultConfigFileName, loadGuardConfig } from "./config.js";
|
|
7
8
|
export { createScanContext } from "./context.js";
|
|
8
9
|
export { getRuleMetadata, RULE_CATALOG } from "./rules/catalog.js";
|
|
9
|
-
export type { BaseReport, CommandName, Evidence, Finding, McpReport, McpServerInventory, PrRiskFile, PrRiskReport, ScanOptions, StripeReport, SupabaseReport } from "./types.js";
|
|
10
|
+
export type { BaseReport, CommandName, Evidence, Finding, ActionsReport, McpOptions, McpPolicyTemplate, McpReport, McpServerInventory, McpSideEffect, PrRiskFile, PrRiskReport, ScanOptions, StripeReport, SupabaseOptions, SupabaseDoctorReport, SupabaseReport } from "./types.js";
|
|
10
11
|
export type { ScanContext, ScanInput } from "./context.js";
|
|
11
12
|
export type { FindingSuppression, GuardConfig, RuleConfigValue } from "./config.js";
|
|
12
13
|
export type { RuleMetadata, RuleStability } from "./rules/catalog.js";
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { scanRepository } from "./commands/scan.js";
|
|
|
2
2
|
export { checkStripe } from "./commands/checkStripe.js";
|
|
3
3
|
export { checkSupabase } from "./commands/checkSupabase.js";
|
|
4
4
|
export { checkMcp } from "./commands/checkMcp.js";
|
|
5
|
+
export { checkActions } from "./commands/checkActions.js";
|
|
5
6
|
export { classifyPrRisk } from "./commands/prRisk.js";
|
|
6
7
|
export { applyGuardConfig, defaultConfigFileName, loadGuardConfig } from "./config.js";
|
|
7
8
|
export { createScanContext } from "./context.js";
|
package/dist/report/markdown.js
CHANGED
|
@@ -45,6 +45,7 @@ function formatGenericMarkdown(report) {
|
|
|
45
45
|
lines.push("");
|
|
46
46
|
lines.push("### Findings");
|
|
47
47
|
appendFindings(lines, report.findings);
|
|
48
|
+
appendGenericExtras(lines, report);
|
|
48
49
|
return lines.join("\n");
|
|
49
50
|
}
|
|
50
51
|
function summaryLine(report) {
|
|
@@ -71,6 +72,39 @@ function appendFindings(lines, findings) {
|
|
|
71
72
|
lines.push(` - Fix direction: ${escapeMarkdownInline(finding.suggestedFix)}`);
|
|
72
73
|
}
|
|
73
74
|
}
|
|
75
|
+
function appendGenericExtras(lines, report) {
|
|
76
|
+
if (report.command === "check-supabase") {
|
|
77
|
+
const supabase = report;
|
|
78
|
+
if (supabase.doctor.sqlCookbook.length === 0)
|
|
79
|
+
return;
|
|
80
|
+
lines.push("");
|
|
81
|
+
lines.push("### Supabase RLS Doctor");
|
|
82
|
+
appendList(lines, supabase.doctor.twoAccountVerificationSteps);
|
|
83
|
+
lines.push("");
|
|
84
|
+
lines.push("```sql");
|
|
85
|
+
lines.push(...supabase.doctor.sqlCookbook);
|
|
86
|
+
lines.push("```");
|
|
87
|
+
}
|
|
88
|
+
if (report.command === "check-mcp") {
|
|
89
|
+
const mcp = report;
|
|
90
|
+
if (!mcp.policyTemplate)
|
|
91
|
+
return;
|
|
92
|
+
lines.push("");
|
|
93
|
+
lines.push("### MCP Policy Template");
|
|
94
|
+
lines.push("");
|
|
95
|
+
lines.push("```yaml");
|
|
96
|
+
lines.push(...mcp.policyTemplate.localPolicyTemplate);
|
|
97
|
+
lines.push("```");
|
|
98
|
+
lines.push("");
|
|
99
|
+
lines.push(`Receipt fields: ${mcp.policyTemplate.receiptFormat.map((field) => `\`${field}\``).join(", ")}`);
|
|
100
|
+
}
|
|
101
|
+
if (report.command === "check-actions") {
|
|
102
|
+
const actions = report;
|
|
103
|
+
lines.push("");
|
|
104
|
+
lines.push("### GitHub Actions Hygiene Checklist");
|
|
105
|
+
appendList(lines, actions.hygieneChecklist);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
74
108
|
function formatEvidence(evidence) {
|
|
75
109
|
if (!evidence)
|
|
76
110
|
return "`none`";
|
package/dist/report/terminal.js
CHANGED
|
@@ -6,6 +6,7 @@ export function formatTerminalReport(report) {
|
|
|
6
6
|
if (report.findings.length === 0) {
|
|
7
7
|
lines.push("");
|
|
8
8
|
lines.push("No heuristic launch-readiness risks found by this command.");
|
|
9
|
+
appendCommandExtras(lines, report);
|
|
9
10
|
return lines.join("\n");
|
|
10
11
|
}
|
|
11
12
|
for (const [index, item] of report.findings.entries()) {
|
|
@@ -22,8 +23,47 @@ export function formatTerminalReport(report) {
|
|
|
22
23
|
lines.push(` - ${location}${detail ? ` -> ${detail}` : ""}`);
|
|
23
24
|
}
|
|
24
25
|
}
|
|
26
|
+
appendCommandExtras(lines, report);
|
|
25
27
|
return lines.join("\n");
|
|
26
28
|
}
|
|
29
|
+
function appendCommandExtras(lines, report) {
|
|
30
|
+
if (report.command === "check-supabase") {
|
|
31
|
+
const supabase = report;
|
|
32
|
+
if (supabase.doctor.sqlCookbook.length > 0) {
|
|
33
|
+
lines.push("");
|
|
34
|
+
lines.push("Supabase RLS doctor:");
|
|
35
|
+
for (const step of supabase.doctor.twoAccountVerificationSteps.slice(0, 5)) {
|
|
36
|
+
lines.push(`- ${step}`);
|
|
37
|
+
}
|
|
38
|
+
lines.push("SQL cookbook:");
|
|
39
|
+
for (const line of supabase.doctor.sqlCookbook.slice(0, 8)) {
|
|
40
|
+
lines.push(` ${line}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (report.command === "check-mcp") {
|
|
45
|
+
const mcp = report;
|
|
46
|
+
if (mcp.policyTemplate) {
|
|
47
|
+
lines.push("");
|
|
48
|
+
lines.push("MCP policy template:");
|
|
49
|
+
for (const line of mcp.policyTemplate.localPolicyTemplate) {
|
|
50
|
+
lines.push(` ${line}`);
|
|
51
|
+
}
|
|
52
|
+
lines.push("Receipt fields:");
|
|
53
|
+
lines.push(` ${mcp.policyTemplate.receiptFormat.join(", ")}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (report.command === "check-actions") {
|
|
57
|
+
const actions = report;
|
|
58
|
+
if (actions.hygieneChecklist.length > 0) {
|
|
59
|
+
lines.push("");
|
|
60
|
+
lines.push("GitHub Actions hygiene checklist:");
|
|
61
|
+
for (const item of actions.hygieneChecklist) {
|
|
62
|
+
lines.push(`- ${item}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
27
67
|
export function formatFindingSummary(finding) {
|
|
28
68
|
const firstEvidence = finding.evidence[0];
|
|
29
69
|
const location = firstEvidence?.line ? `${firstEvidence.file}:${firstEvidence.line}` : firstEvidence?.file;
|