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.
Files changed (43) hide show
  1. package/README.md +21 -10
  2. package/README.zh-CN.md +20 -11
  3. package/action.yml +2 -2
  4. package/dist/cli.js +17 -5
  5. package/dist/commands/checkActions.d.ts +2 -0
  6. package/dist/commands/checkActions.js +4 -0
  7. package/dist/commands/checkMcp.d.ts +2 -2
  8. package/dist/commands/checkMcp.js +1 -1
  9. package/dist/commands/checkSupabase.d.ts +2 -2
  10. package/dist/commands/checkSupabase.js +1 -1
  11. package/dist/commands/scan.js +9 -3
  12. package/dist/hosted/app.js +8 -1
  13. package/dist/hosted/contracts.js +1 -1
  14. package/dist/hosted/production-adapters.js +8 -1
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.js +1 -0
  17. package/dist/report/markdown.js +34 -0
  18. package/dist/report/terminal.js +40 -0
  19. package/dist/rules/catalog.js +182 -0
  20. package/dist/scanners/actions.d.ts +3 -0
  21. package/dist/scanners/actions.js +173 -0
  22. package/dist/scanners/deploy.js +146 -1
  23. package/dist/scanners/gitDiff.js +180 -4
  24. package/dist/scanners/mcp.d.ts +3 -1
  25. package/dist/scanners/mcp.js +122 -11
  26. package/dist/scanners/silentSuccess.d.ts +3 -0
  27. package/dist/scanners/silentSuccess.js +222 -0
  28. package/dist/scanners/supabase.d.ts +3 -1
  29. package/dist/scanners/supabase.js +171 -2
  30. package/dist/types.d.ts +31 -3
  31. package/docs/github-action.md +15 -1
  32. package/docs/github-app-deployment.md +1 -1
  33. package/docs/hosted-operations-evidence.md +43 -0
  34. package/docs/launch-readiness-checklist.md +23 -2
  35. package/docs/npm-publishing.md +4 -3
  36. package/docs/positioning.md +3 -2
  37. package/docs/project-handoff.md +5 -4
  38. package/docs/reddit-github-feasibility-report-2026-05-24.md +260 -0
  39. package/docs/reddit-market-knowledge-base-2026-05-24.md +40 -0
  40. package/docs/rules.md +42 -0
  41. package/hosted/cloudflare-worker/README.md +3 -2
  42. package/hosted/cloudflare-worker/src/index.js +574 -7
  43. 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.25.0`, `v0` |
78
- | npm package | `ai-saas-guard@0.25.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` for signed GitHub webhook intake and KV queueing; scan workers and Check Runs are still gated |
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
- | Deploy config | Next static export/runtime mismatches, Edge runtime with Node-only APIs, missing important env documentation |
160
- | PR risk | Auth, billing, RLS, env, deploy, API, storage, test-removal, and large mixed-diff classification |
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 deployed ingress has the required Cloudflare credential bindings configured. It deliberately queues compact pull request identity records only; scan worker execution, GitHub installation-token exchange, PR diff fetching, and Check Run publishing remain blocked until their deployed evidence passes the hosted operational release gate.
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.25.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
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.25.0`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.25.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.25.0` |
78
- | Action 标签 | `v0.25.0`、`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`,用于签名 GitHub webhook intake KV 排队;scan worker Check Run 仍然受 release gate 阻断 |
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
- | Deploy config | Next static export 和 API route 冲突、Edge runtime 使用 Node-only API、关键 env 文档缺失 |
145
- | PR risk | authbillingRLS、env、deploy、API、storage、测试删除、大型混合 diff |
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 canceldocs-only 改动跑全量 CIsecret/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 已配置所需的云端凭据绑定。这个 ingress 已经能接收签名 webhook 并写入 KV 队列,但还不是完整自动扫描服务。
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,并只把 compact pull request identity 写入 Cloudflare KV;staging GitHub App ID 为 `3834787`,installation ID 为 `135085075`;GitHub installation-token exchange、PR diff fetching、scan worker 和 Check Run publishing 仍然需要通过 hosted operational release gate
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、rollbackincident-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:
@@ -0,0 +1,2 @@
1
+ import type { ActionsReport, ScanOptions } from "../types.js";
2
+ export declare function checkActions(options: ScanOptions): Promise<ActionsReport>;
@@ -0,0 +1,4 @@
1
+ import { checkActions as runActionsScanner } from "../scanners/actions.js";
2
+ export function checkActions(options) {
3
+ return runActionsScanner(options.rootDir);
4
+ }
@@ -1,2 +1,2 @@
1
- import type { McpReport, ScanOptions } from "../types.js";
2
- export declare function checkMcp(options: ScanOptions): Promise<McpReport>;
1
+ import type { McpOptions, McpReport } from "../types.js";
2
+ export declare function checkMcp(options: McpOptions): Promise<McpReport>;
@@ -1,4 +1,4 @@
1
1
  import { checkMcp as runMcpScanner } from "../scanners/mcp.js";
2
2
  export function checkMcp(options) {
3
- return runMcpScanner(options.rootDir);
3
+ return runMcpScanner(options.rootDir, { policyTemplate: options.policyTemplate });
4
4
  }
@@ -1,2 +1,2 @@
1
- import type { ScanOptions, SupabaseReport } from "../types.js";
2
- export declare function checkSupabase(options: ScanOptions): Promise<SupabaseReport>;
1
+ import type { SupabaseOptions, SupabaseReport } from "../types.js";
2
+ export declare function checkSupabase(options: SupabaseOptions): Promise<SupabaseReport>;
@@ -1,4 +1,4 @@
1
1
  import { checkSupabase as runSupabaseScanner } from "../scanners/supabase.js";
2
2
  export function checkSupabase(options) {
3
- return runSupabaseScanner(options.rootDir);
3
+ return runSupabaseScanner(options.rootDir, { doctor: options.doctor });
4
4
  }
@@ -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
  }
@@ -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().replace(/\/+$/, "");
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;
@@ -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.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
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.replace(/\/+$/, "");
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";
@@ -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`";
@@ -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;