ai-saas-guard 0.29.0 → 0.29.2
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 +66 -31
- package/action.yml +17 -6
- package/docs/README.zh-CN.md +65 -30
- package/docs/demo-quickstart.md +60 -0
- package/docs/github-action.md +3 -1
- package/docs/npm-publishing.md +3 -3
- package/examples/demo-risky-saas/.github/workflows/ci.yml +13 -0
- package/examples/demo-risky-saas/README.md +25 -0
- package/examples/demo-risky-saas/app/api/billing/checkout/route.ts +14 -0
- package/examples/demo-risky-saas/app/api/stripe/webhook/route.ts +14 -0
- package/examples/demo-risky-saas/package.json +5 -0
- package/examples/demo-risky-saas/supabase/migrations/001_accounts.sql +14 -0
- package/examples/demo-safe-saas/.env.example +3 -0
- package/examples/demo-safe-saas/.github/workflows/ci.yml +32 -0
- package/examples/demo-safe-saas/README.md +17 -0
- package/examples/demo-safe-saas/app/api/stripe/webhook/route.ts +89 -0
- package/examples/demo-safe-saas/app/api/tenant/[tenantId]/route.ts +18 -0
- package/examples/demo-safe-saas/app/dashboard/[tenantId]/page.tsx +11 -0
- package/examples/demo-safe-saas/next.config.js +22 -0
- package/examples/demo-safe-saas/package.json +5 -0
- package/examples/demo-safe-saas/supabase/migrations/001_projects.sql +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
|
|
8
|
+
A local-first launch gate for AI-built Next.js, Supabase, Stripe, Vercel, GitHub Actions, and MCP SaaS apps. It focuses on auth, billing, data access, secrets, MCP, and deploy paths, then turns risky files into a short review queue before launch or merge. It runs locally, reads your repo only, and does not upload code.
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -27,9 +27,11 @@
|
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
30
|
-
##
|
|
30
|
+
## Before You Invite Users
|
|
31
31
|
|
|
32
|
-
AI can make a SaaS look finished
|
|
32
|
+
AI can make a SaaS look finished: login works, checkout opens, the dashboard loads, and tests are green. The launch risk is usually hidden in trust-boundary code that decides who gets access, who pays, what data they can see, and whether failures are visible.
|
|
33
|
+
|
|
34
|
+
These are the failures that hurt after real users arrive:
|
|
33
35
|
|
|
34
36
|
- one customer can see or change another customer's data
|
|
35
37
|
- Stripe grants access from an unsigned, duplicated, missing, or failed webhook path
|
|
@@ -42,23 +44,56 @@ AI can make a SaaS look finished while the real launch blockers sit in trust-bou
|
|
|
42
44
|
|
|
43
45
|
`ai-saas-guard` gives you a short local review queue for those risks. It does not prove the app is secure, certify a release, or replace human review. It tells founders, solo builders, small teams, and reviewers what deserves attention first.
|
|
44
46
|
|
|
47
|
+
## 60-Second Local Check
|
|
48
|
+
|
|
49
|
+
Run it against your app without installing anything globally:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For an AI-heavy pull request:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --markdown
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
You get rule IDs, severity, file evidence, why it matters, how to verify it manually, and a concrete fix direction. The scanner is deterministic, read-only, and does not call an LLM.
|
|
62
|
+
|
|
63
|
+
## Try The Demo Fixtures
|
|
64
|
+
|
|
65
|
+
Want to see the report before scanning your own repo?
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/zr9959/ai-saas-guard.git
|
|
69
|
+
cd ai-saas-guard
|
|
70
|
+
npx ai-saas-guard@latest scan --root examples/demo-risky-saas
|
|
71
|
+
npx ai-saas-guard@latest scan --root examples/demo-safe-saas
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The risky demo currently returns 19 intentional findings across Stripe, Supabase, silent-success paths, Next/Vercel deploy hints, and GitHub Actions. The safe demo returns 0 findings for the same broad surfaces with safer static patterns. See [docs/demo-quickstart.md](docs/demo-quickstart.md).
|
|
75
|
+
|
|
45
76
|
## See The Output
|
|
46
77
|
|
|
47
78
|
The report is designed to be read before launch or before merging an AI-heavy PR. A longer copy-paste example is in [docs/sample-launch-report.md](docs/sample-launch-report.md).
|
|
48
79
|
|
|
49
80
|
```text
|
|
50
81
|
Launch Gate: review before launch
|
|
51
|
-
|
|
82
|
+
19 findings: 2 critical, 6 high, 7 medium, 3 low, 1 info
|
|
52
83
|
|
|
53
|
-
|
|
84
|
+
CRITICAL stripe.webhook.missing-signature
|
|
54
85
|
File: app/api/stripe/webhook/route.ts
|
|
55
86
|
Why: billing access can be granted from a webhook path that does not verify Stripe signatures.
|
|
56
87
|
Verify: replay a webhook with an invalid signature and confirm the route rejects it.
|
|
57
88
|
Fix: read the raw body, call stripe.webhooks.constructEvent, and make event handling idempotent.
|
|
58
89
|
|
|
59
|
-
|
|
60
|
-
File:
|
|
61
|
-
Verify:
|
|
90
|
+
HIGH silent-success.swallowed-error
|
|
91
|
+
File: app/api/billing/checkout/route.ts
|
|
92
|
+
Verify: force the upstream billing call to fail and confirm the route returns an error, not fake success.
|
|
93
|
+
|
|
94
|
+
MEDIUM deploy.next.missing-security-headers
|
|
95
|
+
File: app/api/billing/checkout/route.ts
|
|
96
|
+
Verify: inspect production response headers for auth, billing, and API pages.
|
|
62
97
|
```
|
|
63
98
|
|
|
64
99
|
## What You Get
|
|
@@ -85,28 +120,6 @@ One command returns a launch-readiness report with:
|
|
|
85
120
|
| Are tools and CI overpowered? | MCP side-effect classes, local policy/receipt templates, GitHub Actions permissions, concurrency, checkout depth, action pinning |
|
|
86
121
|
| Can reviewers trust the PR? | `pr-risk` ranking for auth, billing, RLS, deploy, API, storage, tests, silent-success paths, missing spec context, and large AI diffs |
|
|
87
122
|
|
|
88
|
-
## Current Status
|
|
89
|
-
|
|
90
|
-
This repository is public on GitHub.
|
|
91
|
-
|
|
92
|
-
The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is available through versioned release tags. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag for controlled upgrades, or a reviewed commit SHA for stricter supply-chain pinning.
|
|
93
|
-
|
|
94
|
-
| Area | Status |
|
|
95
|
-
| --- | --- |
|
|
96
|
-
| Public GitHub repository | Available |
|
|
97
|
-
| npm CLI | `ai-saas-guard@0.29.0` |
|
|
98
|
-
| GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.29.0` |
|
|
99
|
-
| Outputs | Terminal, JSON, SARIF, and PR-focused markdown |
|
|
100
|
-
| Project config | `.ai-saas-guard.json` rule toggles, severity overrides, suppressions, and fail thresholds |
|
|
101
|
-
| Privacy model | Local-first, read-only scan commands, no LLM calls, no code upload |
|
|
102
|
-
| Versioned Action tags | `v0.29.0`, `v0` |
|
|
103
|
-
| Current release | `0.29.0` hosted Node checkout platform composition, Clerk unsafe metadata rule, Prisma tenant-scope rule, Vercel cron guard rule, sample launch report, and Marketplace wrapper decision |
|
|
104
|
-
| npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
|
|
105
|
-
| Repository trust hardening | Strict branch protection, Dependabot, CodeQL, fast-check fuzzing, signed release provenance assets, private vulnerability reporting, secret scanning, and push protection |
|
|
106
|
-
| Cloudflare hosted ingress | Deployed at `https://ai-saas-guard-hosted.zr9959.workers.dev`; signed GitHub App webhook delivery and compact Check Run smoke now pass in staging |
|
|
107
|
-
| Hosted GitHub App staging | Private App `ai-saas-guard-hosted` (`3834787`) installed on `zr9959/ai-saas-guard`; hosted operations evidence is in [docs/hosted-operations-evidence.md](docs/hosted-operations-evidence.md) |
|
|
108
|
-
| OpenSSF Best Practices | Passing badge, project `12955`; `.bestpractices.json` remains the conservative evidence record |
|
|
109
|
-
|
|
110
123
|
## Quick Start
|
|
111
124
|
|
|
112
125
|
Run the published CLI without installing it globally:
|
|
@@ -157,6 +170,28 @@ node dist/cli.js check-mcp --root /path/to/your-saas
|
|
|
157
170
|
node dist/cli.js check-actions --root /path/to/your-saas
|
|
158
171
|
```
|
|
159
172
|
|
|
173
|
+
## Current Status
|
|
174
|
+
|
|
175
|
+
This repository is public on GitHub.
|
|
176
|
+
|
|
177
|
+
The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is available through versioned release tags. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag for controlled upgrades, or a reviewed commit SHA for stricter supply-chain pinning.
|
|
178
|
+
|
|
179
|
+
| Area | Status |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| Public GitHub repository | Available |
|
|
182
|
+
| npm CLI | `ai-saas-guard@0.29.2` |
|
|
183
|
+
| GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.29.2` |
|
|
184
|
+
| Outputs | Terminal, JSON, SARIF, and PR-focused markdown |
|
|
185
|
+
| Project config | `.ai-saas-guard.json` rule toggles, severity overrides, suppressions, and fail thresholds |
|
|
186
|
+
| Privacy model | Local-first, read-only scan commands, no LLM calls, no code upload |
|
|
187
|
+
| Versioned Action tags | `v0.29.2`, `v0` |
|
|
188
|
+
| Current release | `0.29.2` publishes public risky/safe demo fixtures, a demo quickstart, quickstart feedback template, and refreshed first-run README guidance |
|
|
189
|
+
| npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
|
|
190
|
+
| Repository trust hardening | Strict branch protection, Dependabot, CodeQL, fast-check fuzzing, signed release provenance assets, private vulnerability reporting, secret scanning, and push protection |
|
|
191
|
+
| Cloudflare hosted ingress | Deployed at `https://ai-saas-guard-hosted.zr9959.workers.dev`; signed GitHub App webhook delivery and compact Check Run smoke now pass in staging |
|
|
192
|
+
| Hosted GitHub App staging | Private App `ai-saas-guard-hosted` (`3834787`) installed on `zr9959/ai-saas-guard`; hosted operations evidence is in [docs/hosted-operations-evidence.md](docs/hosted-operations-evidence.md) |
|
|
193
|
+
| OpenSSF Best Practices | Passing badge, project `12955`; `.bestpractices.json` remains the conservative evidence record |
|
|
194
|
+
|
|
160
195
|
## Example Finding
|
|
161
196
|
|
|
162
197
|
Terminal output is designed to be useful to a reviewer, not just a scanner dashboard.
|
|
@@ -317,7 +352,7 @@ Use `suppressions` for narrower false-positive handling when one rule is noisy o
|
|
|
317
352
|
|
|
318
353
|
## GitHub Action
|
|
319
354
|
|
|
320
|
-
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.29.
|
|
355
|
+
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.29.2` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
|
|
321
356
|
|
|
322
357
|
```yaml
|
|
323
358
|
name: ai-saas-guard
|
package/action.yml
CHANGED
|
@@ -8,11 +8,11 @@ inputs:
|
|
|
8
8
|
required: false
|
|
9
9
|
default: scan
|
|
10
10
|
root:
|
|
11
|
-
description: Repository path to scan.
|
|
11
|
+
description: "Repository path to scan. Usually github.workspace."
|
|
12
12
|
required: false
|
|
13
13
|
default: ${{ github.workspace }}
|
|
14
14
|
format:
|
|
15
|
-
description: "Output format: terminal, json, sarif, or markdown."
|
|
15
|
+
description: "Output format: terminal, json, sarif, or markdown. Use markdown for PR summaries and sarif for code scanning."
|
|
16
16
|
required: false
|
|
17
17
|
default: terminal
|
|
18
18
|
fail-on:
|
|
@@ -20,15 +20,15 @@ inputs:
|
|
|
20
20
|
required: false
|
|
21
21
|
default: none
|
|
22
22
|
base:
|
|
23
|
-
description: Base ref for pr-risk.
|
|
23
|
+
description: "Base ref for pr-risk. Use origin/main with actions/checkout fetch-depth: 0."
|
|
24
24
|
required: false
|
|
25
25
|
default: ""
|
|
26
26
|
config:
|
|
27
|
-
description: Optional ai-saas-guard JSON config path.
|
|
27
|
+
description: "Optional ai-saas-guard JSON config path, relative to the workflow workspace or root."
|
|
28
28
|
required: false
|
|
29
29
|
default: ""
|
|
30
30
|
output:
|
|
31
|
-
description: Optional path to write output while also keeping the command exit code.
|
|
31
|
+
description: "Optional path to write output while also keeping the command exit code."
|
|
32
32
|
required: false
|
|
33
33
|
default: ""
|
|
34
34
|
|
|
@@ -56,7 +56,7 @@ runs:
|
|
|
56
56
|
INPUT_CONFIG: ${{ inputs.config }}
|
|
57
57
|
INPUT_OUTPUT: ${{ inputs.output }}
|
|
58
58
|
run: |
|
|
59
|
-
set -
|
|
59
|
+
set -euo pipefail
|
|
60
60
|
|
|
61
61
|
case "${INPUT_COMMAND}" in
|
|
62
62
|
scan|check-supabase|check-stripe|check-mcp|check-actions|pr-risk) ;;
|
|
@@ -82,6 +82,16 @@ runs:
|
|
|
82
82
|
;;
|
|
83
83
|
esac
|
|
84
84
|
|
|
85
|
+
if [ ! -d "${INPUT_ROOT}" ]; then
|
|
86
|
+
echo "Root path does not exist or is not a directory: ${INPUT_ROOT}" >&2
|
|
87
|
+
exit 2
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
if [ -n "${INPUT_CONFIG}" ] && [ ! -f "${INPUT_CONFIG}" ] && [ ! -f "${INPUT_ROOT%/}/${INPUT_CONFIG}" ]; then
|
|
91
|
+
echo "Config file not found: ${INPUT_CONFIG}" >&2
|
|
92
|
+
exit 2
|
|
93
|
+
fi
|
|
94
|
+
|
|
85
95
|
args=("${INPUT_COMMAND}" "--root" "${INPUT_ROOT}")
|
|
86
96
|
|
|
87
97
|
if [ "${INPUT_FORMAT}" = "json" ]; then
|
|
@@ -105,6 +115,7 @@ runs:
|
|
|
105
115
|
fi
|
|
106
116
|
|
|
107
117
|
if [ -n "${INPUT_OUTPUT}" ]; then
|
|
118
|
+
mkdir -p -- "$(dirname -- "${INPUT_OUTPUT}")"
|
|
108
119
|
node "${GITHUB_ACTION_PATH}/dist/cli.js" "${args[@]}" | tee -- "${INPUT_OUTPUT}"
|
|
109
120
|
else
|
|
110
121
|
node "${GITHUB_ACTION_PATH}/dist/cli.js" "${args[@]}"
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
|
|
8
|
+
面向 AI 构建的 Next.js、Supabase、Stripe、Vercel、GitHub Actions 和 MCP SaaS 的本地优先上线 gate。它聚焦 auth、billing、data access、secrets、MCP 和 deploy,把仓库里最容易出事的风险路径变成一份短 review 队列,让你在上线前或合并 PR 前知道该先看哪里。它本地运行、只读仓库、不上传代码。
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -26,9 +26,11 @@
|
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## 邀请真实用户前先看这里
|
|
30
30
|
|
|
31
|
-
AI 能很快把一个 SaaS
|
|
31
|
+
AI 能很快把一个 SaaS 做到“看起来能用”:能登录、能打开 checkout、dashboard 能加载、测试也是绿的。真正危险的是信任边界代码,它决定谁有权限、谁付了钱、谁能看哪些数据,以及服务失败时会不会被悄悄伪装成成功。
|
|
32
|
+
|
|
33
|
+
这些问题通常会在真实用户来了以后才变痛:
|
|
32
34
|
|
|
33
35
|
- 一个用户能看到或修改另一个客户的数据
|
|
34
36
|
- Stripe webhook 因为未签名、重复、漏处理失败事件而错误开通权限
|
|
@@ -41,23 +43,56 @@ AI 能很快把一个 SaaS 做到“看起来能用”。真正危险的是上
|
|
|
41
43
|
|
|
42
44
|
`ai-saas-guard` 是面向这个时刻的本地优先、review-first 上线预检工具。它不会证明你的应用绝对安全,也不是渗透测试、认证或完整安全审计。它的目标是给 founder、独立开发者、小团队和 reviewer 一份短而有证据的清单,告诉你上线或合并 PR 前最该先看哪里。
|
|
43
45
|
|
|
46
|
+
## 60 秒本地检查
|
|
47
|
+
|
|
48
|
+
无需全局安装,直接扫你的应用:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
如果是 AI 生成的大 PR:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --markdown
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
你会得到 rule ID、severity、文件证据、为什么重要、如何人工验证,以及具体修复方向。扫描是 deterministic、只读的,不调用 LLM。
|
|
61
|
+
|
|
62
|
+
## 先试公开 demo
|
|
63
|
+
|
|
64
|
+
如果你还不想先扫自己的私有仓库,可以先跑公开 fixture:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git clone https://github.com/zr9959/ai-saas-guard.git
|
|
68
|
+
cd ai-saas-guard
|
|
69
|
+
npx ai-saas-guard@latest scan --root examples/demo-risky-saas
|
|
70
|
+
npx ai-saas-guard@latest scan --root examples/demo-safe-saas
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
risky demo 当前会故意触发 19 个 finding,覆盖 Stripe、Supabase、silent-success、Next/Vercel deploy 提示和 GitHub Actions。safe demo 在同类风险面上使用更安全的静态写法,当前返回 0 个 finding。说明见 [demo-quickstart.md](demo-quickstart.md)。
|
|
74
|
+
|
|
44
75
|
## 输出长什么样
|
|
45
76
|
|
|
46
77
|
报告是给上线前或合并 AI 大 PR 前快速阅读的。更完整的可复制样例见 [docs/sample-launch-report.md](sample-launch-report.md)。
|
|
47
78
|
|
|
48
79
|
```text
|
|
49
80
|
Launch Gate: review before launch
|
|
50
|
-
|
|
81
|
+
19 findings: 2 critical, 6 high, 7 medium, 3 low, 1 info
|
|
51
82
|
|
|
52
|
-
|
|
83
|
+
CRITICAL stripe.webhook.missing-signature
|
|
53
84
|
File: app/api/stripe/webhook/route.ts
|
|
54
85
|
Why: billing access can be granted from a webhook path that does not verify Stripe signatures.
|
|
55
86
|
Verify: replay a webhook with an invalid signature and confirm the route rejects it.
|
|
56
87
|
Fix: read the raw body, call stripe.webhooks.constructEvent, and make event handling idempotent.
|
|
57
88
|
|
|
58
|
-
|
|
59
|
-
File:
|
|
60
|
-
Verify:
|
|
89
|
+
HIGH silent-success.swallowed-error
|
|
90
|
+
File: app/api/billing/checkout/route.ts
|
|
91
|
+
Verify: force the upstream billing call to fail and confirm the route returns an error, not fake success.
|
|
92
|
+
|
|
93
|
+
MEDIUM deploy.next.missing-security-headers
|
|
94
|
+
File: app/api/billing/checkout/route.ts
|
|
95
|
+
Verify: inspect production response headers for auth, billing, and API pages.
|
|
61
96
|
```
|
|
62
97
|
|
|
63
98
|
## 你会得到什么
|
|
@@ -84,28 +119,6 @@ Verify: sign in as user A and user B; confirm neither can SELECT or UPDATE the o
|
|
|
84
119
|
| 工具和 CI 权限是不是过大? | MCP side-effect 分类、本地 policy/receipt 模板、GitHub Actions 权限、concurrency、checkout depth、Action pinning |
|
|
85
120
|
| reviewer 能不能看懂 AI PR? | `pr-risk` 对 auth、billing、RLS、deploy、API、storage、测试、silent-success、缺 spec context 和大型 diff 排序 |
|
|
86
121
|
|
|
87
|
-
## 当前状态
|
|
88
|
-
|
|
89
|
-
这个仓库是公开 GitHub 仓库。
|
|
90
|
-
|
|
91
|
-
CLI 已发布到 npm:`ai-saas-guard@0.29.0`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.29.0`。
|
|
92
|
-
|
|
93
|
-
| 模块 | 状态 |
|
|
94
|
-
| --- | --- |
|
|
95
|
-
| 公开 GitHub 仓库 | 已可用 |
|
|
96
|
-
| npm CLI | `ai-saas-guard@0.29.0` |
|
|
97
|
-
| GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.29.0` |
|
|
98
|
-
| 输出格式 | Terminal、JSON、SARIF 和 PR markdown |
|
|
99
|
-
| 项目配置 | `.ai-saas-guard.json` 支持规则开关、severity 覆盖、suppressions 和 fail threshold |
|
|
100
|
-
| 隐私模型 | 本地优先、只读扫描、不调用 LLM、不上传代码 |
|
|
101
|
-
| 当前版本 | `0.29.0` hosted Node checkout platform 组合入口、Clerk unsafe metadata 规则、Prisma tenant-scope 规则、Vercel cron guard 规则、sample launch report 和 Marketplace wrapper 决策 |
|
|
102
|
-
| Action 标签 | `v0.29.0`、`v0` |
|
|
103
|
-
| npm 发布 | GitHub Actions Trusted Publisher/OIDC,无需长期 npm token |
|
|
104
|
-
| 仓库可信度加固 | 严格 branch protection、Dependabot、CodeQL、fast-check fuzzing、signed release provenance assets、private vulnerability reporting、secret scanning 和 push protection |
|
|
105
|
-
| Cloudflare hosted ingress | 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`;签名 GitHub App webhook delivery 和 compact Check Run staging smoke 已通过 |
|
|
106
|
-
| Hosted GitHub App staging | 私有 App `ai-saas-guard-hosted`(`3834787`)已安装到 `zr9959/ai-saas-guard`;hosted operations evidence 见 [docs/hosted-operations-evidence.md](hosted-operations-evidence.md) |
|
|
107
|
-
| OpenSSF Best Practices | 已获得 passing badge,项目 `12955`;`.bestpractices.json` 继续作为保守证据记录 |
|
|
108
|
-
|
|
109
122
|
## 快速开始
|
|
110
123
|
|
|
111
124
|
无需全局安装,直接运行:
|
|
@@ -144,6 +157,28 @@ npm run build
|
|
|
144
157
|
node dist/cli.js scan --root /path/to/your-saas
|
|
145
158
|
```
|
|
146
159
|
|
|
160
|
+
## 当前状态
|
|
161
|
+
|
|
162
|
+
这个仓库是公开 GitHub 仓库。
|
|
163
|
+
|
|
164
|
+
CLI 已发布到 npm:`ai-saas-guard@0.29.2`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.29.2`。
|
|
165
|
+
|
|
166
|
+
| 模块 | 状态 |
|
|
167
|
+
| --- | --- |
|
|
168
|
+
| 公开 GitHub 仓库 | 已可用 |
|
|
169
|
+
| npm CLI | `ai-saas-guard@0.29.2` |
|
|
170
|
+
| GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.29.2` |
|
|
171
|
+
| 输出格式 | Terminal、JSON、SARIF 和 PR markdown |
|
|
172
|
+
| 项目配置 | `.ai-saas-guard.json` 支持规则开关、severity 覆盖、suppressions 和 fail threshold |
|
|
173
|
+
| 隐私模型 | 本地优先、只读扫描、不调用 LLM、不上传代码 |
|
|
174
|
+
| 当前版本 | `0.29.2` 发布公开 risky/safe demo fixtures、demo quickstart、quickstart 反馈模板,并刷新首次试用 README 指引 |
|
|
175
|
+
| Action 标签 | `v0.29.2`、`v0` |
|
|
176
|
+
| npm 发布 | GitHub Actions Trusted Publisher/OIDC,无需长期 npm token |
|
|
177
|
+
| 仓库可信度加固 | 严格 branch protection、Dependabot、CodeQL、fast-check fuzzing、signed release provenance assets、private vulnerability reporting、secret scanning 和 push protection |
|
|
178
|
+
| Cloudflare hosted ingress | 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`;签名 GitHub App webhook delivery 和 compact Check Run staging smoke 已通过 |
|
|
179
|
+
| Hosted GitHub App staging | 私有 App `ai-saas-guard-hosted`(`3834787`)已安装到 `zr9959/ai-saas-guard`;hosted operations evidence 见 [docs/hosted-operations-evidence.md](hosted-operations-evidence.md) |
|
|
180
|
+
| OpenSSF Best Practices | 已获得 passing badge,项目 `12955`;`.bestpractices.json` 继续作为保守证据记录 |
|
|
181
|
+
|
|
147
182
|
## 主要命令
|
|
148
183
|
|
|
149
184
|
| 命令 | 用途 |
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Demo Quickstart
|
|
2
|
+
|
|
3
|
+
Use these public fixtures when you want to understand `ai-saas-guard` before pointing it at a private repository.
|
|
4
|
+
|
|
5
|
+
## Risky Demo
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx ai-saas-guard@latest scan --root examples/demo-risky-saas
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The risky demo intentionally includes unsigned Stripe webhook handling, a silent-success billing fallback, broad Supabase RLS, and overpowered GitHub Actions permissions.
|
|
12
|
+
|
|
13
|
+
Expected summary:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
19 findings: 2 critical, 6 high, 7 medium, 3 low, 1 info
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The first findings should point at trust-boundary files such as:
|
|
20
|
+
|
|
21
|
+
- `app/api/stripe/webhook/route.ts` for missing Stripe signature verification and idempotency
|
|
22
|
+
- `supabase/migrations/001_accounts.sql` for broad RLS and missing tenant predicates
|
|
23
|
+
- `app/api/billing/checkout/route.ts` for a silent-success billing fallback
|
|
24
|
+
- `.github/workflows/ci.yml` for launch-related workflow hygiene hints
|
|
25
|
+
|
|
26
|
+
For local development from this repository checkout:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm ci
|
|
30
|
+
npm run build
|
|
31
|
+
node dist/cli.js scan --root examples/demo-risky-saas
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Safe Demo
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx ai-saas-guard@latest scan --root examples/demo-safe-saas
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The safe demo keeps the same broad surfaces but uses safer static patterns: Stripe signature verification and idempotency hints, scoped RLS, security headers, documented env variables, request IDs, and bounded GitHub Actions permissions.
|
|
41
|
+
|
|
42
|
+
Expected summary:
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
0 findings
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For local development from this repository checkout:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
node dist/cli.js scan --root examples/demo-safe-saas
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## What To Look For
|
|
55
|
+
|
|
56
|
+
- Every finding has a rule ID, severity, file evidence, why it matters, a manual verification step, and a fix direction.
|
|
57
|
+
- The risky demo is intentionally noisy enough to show the report shape.
|
|
58
|
+
- The safe demo is intentionally small; it is not a complete SaaS template and does not certify a real app.
|
|
59
|
+
|
|
60
|
+
Do not paste real API keys, customer data, private source code, webhook secrets, or production URLs into public issues when sharing output.
|
package/docs/github-action.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
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.29.0` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
|
|
6
|
+
|
|
7
|
+
The Action runs the same local scanner inside the GitHub-hosted runner. It reads the checked-out repository, does not call an LLM, and does not upload source code. For `pr-risk`, always use `actions/checkout` with `fetch-depth: 0` so the base branch comparison is available.
|
|
6
8
|
|
|
7
9
|
## PR Summary
|
|
8
10
|
|
package/docs/npm-publishing.md
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
## Current State
|
|
6
6
|
|
|
7
7
|
- Package name: `ai-saas-guard`
|
|
8
|
-
- Current published version: `0.29.
|
|
8
|
+
- Current published version: `0.29.2`
|
|
9
9
|
- Next source candidate: none
|
|
10
10
|
- npm registry state: published at <https://www.npmjs.com/package/ai-saas-guard>
|
|
11
11
|
- First npm-published version: `0.1.1`
|
|
12
|
-
- GitHub Release: `v0.29.
|
|
12
|
+
- GitHub Release: `v0.29.2`
|
|
13
13
|
- Publish workflow: `.github/workflows/npm-publish.yml`
|
|
14
14
|
- Trusted Publisher: GitHub Actions, `zr9959/ai-saas-guard`, workflow `npm-publish.yml`, allowed action `npm publish`
|
|
15
15
|
- Long-lived npm publish token: not required
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
Use GitHub Actions with npm Trusted Publisher/OIDC:
|
|
20
20
|
|
|
21
|
-
1. Create and review a release tag such as `v0.29.
|
|
21
|
+
1. Create and review a release tag such as `v0.29.2`.
|
|
22
22
|
2. Publish from the GitHub Release or run the `Publish npm` workflow manually with `ref` set to that tag.
|
|
23
23
|
3. Keep `permissions.id-token: write` in the workflow so npm can exchange the GitHub Actions OIDC identity for a short-lived publish credential.
|
|
24
24
|
4. Run `npm publish --access public` from the workflow. Trusted publishing automatically generates provenance for this public package from this public repository.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Risky Demo SaaS
|
|
2
|
+
|
|
3
|
+
This is a tiny public fixture that intentionally contains common AI-built SaaS launch risks.
|
|
4
|
+
|
|
5
|
+
Run from the repository root:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
node dist/cli.js scan --root examples/demo-risky-saas
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or with the published package:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx ai-saas-guard@latest scan --root examples/demo-risky-saas
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Expected themes:
|
|
18
|
+
|
|
19
|
+
- unsigned Stripe webhook handling
|
|
20
|
+
- silent-success billing fallback
|
|
21
|
+
- broad Supabase RLS policy
|
|
22
|
+
- over-broad GitHub Actions permissions
|
|
23
|
+
- stale PR workflow risk
|
|
24
|
+
|
|
25
|
+
This fixture uses inert placeholder code only. It does not contain real secrets, customer data, or production URLs.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export async function POST() {
|
|
2
|
+
try {
|
|
3
|
+
return Response.json({ checkoutUrl: await createCheckout() });
|
|
4
|
+
} catch {
|
|
5
|
+
return Response.json({
|
|
6
|
+
success: true,
|
|
7
|
+
checkoutUrl: "/billing/demo-success"
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function createCheckout() {
|
|
13
|
+
throw new Error("provider unavailable");
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export async function POST(req: Request) {
|
|
2
|
+
const event = await req.json();
|
|
3
|
+
|
|
4
|
+
if (event.type === "checkout.session.completed") {
|
|
5
|
+
await grantAccess(event.data.object.customer);
|
|
6
|
+
return Response.json({ success: true });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return Response.json({ ok: true });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function grantAccess(customerId: string) {
|
|
13
|
+
console.log("granting access", customerId);
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
create table public.accounts (
|
|
2
|
+
id uuid primary key,
|
|
3
|
+
tenant_id uuid not null,
|
|
4
|
+
user_id uuid not null,
|
|
5
|
+
plan text not null
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
alter table public.accounts enable row level security;
|
|
9
|
+
|
|
10
|
+
create policy "public read accounts"
|
|
11
|
+
on public.accounts
|
|
12
|
+
for select
|
|
13
|
+
to public
|
|
14
|
+
using (true);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Safe demo CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
paths-ignore:
|
|
6
|
+
- "docs/**"
|
|
7
|
+
- "*.md"
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
env:
|
|
20
|
+
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
|
23
|
+
with:
|
|
24
|
+
fetch-depth: 0
|
|
25
|
+
persist-credentials: false
|
|
26
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
|
27
|
+
with:
|
|
28
|
+
node-version: 20
|
|
29
|
+
- run: test -n "${STRIPE_SECRET_KEY:-}"
|
|
30
|
+
- run: npm ci
|
|
31
|
+
- run: npx ai-saas-guard pr-risk --base origin/main
|
|
32
|
+
- run: npm test
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Safe Demo SaaS
|
|
2
|
+
|
|
3
|
+
This is a tiny public fixture with safer launch-readiness patterns for the same broad surfaces as the risky demo.
|
|
4
|
+
|
|
5
|
+
Run from the repository root:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
node dist/cli.js scan --root examples/demo-safe-saas
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Expected result:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
No heuristic launch-readiness risks found by this command.
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The fixture is intentionally small. It is not a complete SaaS template and does not prove a real app is secure.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import Stripe from "stripe";
|
|
2
|
+
|
|
3
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
4
|
+
|
|
5
|
+
export async function POST(req: Request) {
|
|
6
|
+
const requestId = req.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
7
|
+
await rateLimit(req);
|
|
8
|
+
const body = await req.text();
|
|
9
|
+
const signature = req.headers.get("stripe-signature");
|
|
10
|
+
const event = stripe.webhooks.constructEvent(
|
|
11
|
+
body,
|
|
12
|
+
signature!,
|
|
13
|
+
process.env.STRIPE_WEBHOOK_KEY!
|
|
14
|
+
);
|
|
15
|
+
const tenant_id = await resolveTenantForStripeCustomer(event.data.object.customer);
|
|
16
|
+
|
|
17
|
+
console.info("stripe webhook received", { requestId, eventId: event.id, tenant_id });
|
|
18
|
+
|
|
19
|
+
if (await hasProcessedEvent(event.id)) {
|
|
20
|
+
return new Response("ok");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
switch (event.type) {
|
|
24
|
+
case "checkout.session.completed":
|
|
25
|
+
await syncEntitlement(event.data.object.customer);
|
|
26
|
+
break;
|
|
27
|
+
case "invoice.payment_failed":
|
|
28
|
+
await markPastDue(event.data.object.customer);
|
|
29
|
+
break;
|
|
30
|
+
case "invoice.payment_action_required":
|
|
31
|
+
await requestPaymentAction(event.data.object.customer);
|
|
32
|
+
break;
|
|
33
|
+
case "customer.subscription.updated":
|
|
34
|
+
await syncSubscription(event.data.object);
|
|
35
|
+
break;
|
|
36
|
+
case "customer.subscription.deleted":
|
|
37
|
+
await revokeEntitlement(event.data.object.customer);
|
|
38
|
+
break;
|
|
39
|
+
case "charge.refunded":
|
|
40
|
+
await reviewRefund(event.data.object.customer);
|
|
41
|
+
break;
|
|
42
|
+
default:
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await recordProcessedEvent(event.id);
|
|
47
|
+
return new Response("ok");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function hasProcessedEvent(eventId: string) {
|
|
51
|
+
return eventId.length === 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function rateLimit(req: Request) {
|
|
55
|
+
console.log(req.headers.get("x-forwarded-for") ?? "local");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function resolveTenantForStripeCustomer(customerId: unknown) {
|
|
59
|
+
console.log(customerId);
|
|
60
|
+
return String(customerId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function recordProcessedEvent(eventId: string) {
|
|
64
|
+
console.log(eventId);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function syncEntitlement(customerId: string) {
|
|
68
|
+
console.log(customerId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function markPastDue(customerId: string) {
|
|
72
|
+
console.log(customerId);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function requestPaymentAction(customerId: string) {
|
|
76
|
+
console.log(customerId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function syncSubscription(subscription: unknown) {
|
|
80
|
+
console.log(subscription);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function revokeEntitlement(customerId: string) {
|
|
84
|
+
console.log(customerId);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function reviewRefund(customerId: string) {
|
|
88
|
+
console.log(customerId);
|
|
89
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export async function POST(request: Request) {
|
|
2
|
+
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
3
|
+
await rateLimit(request);
|
|
4
|
+
const payload = await request.json();
|
|
5
|
+
|
|
6
|
+
console.info("tenant update", { requestId, tenantId: payload.tenantId });
|
|
7
|
+
await updateTenantBilling(payload.tenantId, process.env.STRIPE_SECRET_KEY);
|
|
8
|
+
|
|
9
|
+
return Response.json({ ok: true, requestId });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function updateTenantBilling(tenantId: string, stripeKey?: string) {
|
|
13
|
+
console.log(tenantId, Boolean(stripeKey));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function rateLimit(request: Request) {
|
|
17
|
+
console.log(request.headers.get("x-forwarded-for") ?? "local");
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import Image from "next/image";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
|
|
4
|
+
export default function TenantDashboard() {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
<Image src="https://cdn.example.com/logo.png" width={240} height={120} alt="" />
|
|
8
|
+
<Link prefetch={false} href="/dashboard/settings">Open settings</Link>
|
|
9
|
+
</>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
async headers() {
|
|
3
|
+
return [
|
|
4
|
+
{
|
|
5
|
+
source: "/(.*)",
|
|
6
|
+
headers: [
|
|
7
|
+
{ key: "X-Frame-Options", value: "DENY" },
|
|
8
|
+
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
9
|
+
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
];
|
|
13
|
+
},
|
|
14
|
+
images: {
|
|
15
|
+
remotePatterns: [
|
|
16
|
+
{
|
|
17
|
+
protocol: "https",
|
|
18
|
+
hostname: "cdn.example.com"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
create table public.projects (
|
|
2
|
+
id uuid primary key,
|
|
3
|
+
user_id uuid not null,
|
|
4
|
+
name text
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
alter table public.projects enable row level security;
|
|
8
|
+
|
|
9
|
+
create policy "users read own projects"
|
|
10
|
+
on public.projects
|
|
11
|
+
for select
|
|
12
|
+
using (auth.uid() = user_id);
|
|
13
|
+
|
|
14
|
+
create policy "users write own projects"
|
|
15
|
+
on public.projects
|
|
16
|
+
for all
|
|
17
|
+
using (auth.uid() = user_id)
|
|
18
|
+
with check (auth.uid() = user_id);
|
package/package.json
CHANGED