ai-saas-guard 0.29.2 → 0.30.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 +38 -32
- package/dist/cli.js +24 -9
- package/dist/commands/demo.d.ts +2 -0
- package/dist/commands/demo.js +21 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/report/launchGate.d.ts +1 -0
- package/dist/report/launchGate.js +22 -0
- package/dist/report/markdown.js +36 -1
- package/dist/report/summary.d.ts +2 -0
- package/dist/report/summary.js +67 -0
- package/dist/report/terminal.js +43 -1
- package/dist/scanners/deploy.js +1 -1
- package/dist/scanners/silentSuccess.js +3 -3
- package/dist/scanners/stripe.js +2 -2
- package/dist/scanners/supabase.js +6 -6
- package/dist/types.d.ts +9 -1
- package/docs/README.zh-CN.md +38 -32
- package/docs/demo-quickstart.md +16 -0
- package/docs/npm-publishing.md +3 -3
- package/docs/sample-launch-report.md +33 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,10 +46,16 @@ These are the failures that hurt after real users arrive:
|
|
|
46
46
|
|
|
47
47
|
## 60-Second Local Check
|
|
48
48
|
|
|
49
|
+
See the public demo output without cloning a repo:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx ai-saas-guard@latest demo --summary
|
|
53
|
+
```
|
|
54
|
+
|
|
49
55
|
Run it against your app without installing anything globally:
|
|
50
56
|
|
|
51
57
|
```bash
|
|
52
|
-
npx ai-saas-guard@latest scan --root /path/to/your-saas
|
|
58
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas --summary
|
|
53
59
|
```
|
|
54
60
|
|
|
55
61
|
For an AI-heavy pull request:
|
|
@@ -58,42 +64,40 @@ For an AI-heavy pull request:
|
|
|
58
64
|
npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --markdown
|
|
59
65
|
```
|
|
60
66
|
|
|
61
|
-
|
|
67
|
+
The summary starts with the launch gate, top risks, manual proof steps, and next actions. Rerun without `--summary` for every finding with rule IDs, severity, file evidence, why it matters, manual verification, and fix direction. The scanner is deterministic, read-only, and does not call an LLM.
|
|
62
68
|
|
|
63
69
|
## Try The Demo Fixtures
|
|
64
70
|
|
|
65
71
|
Want to see the report before scanning your own repo?
|
|
66
72
|
|
|
67
73
|
```bash
|
|
68
|
-
|
|
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
|
|
74
|
+
npx ai-saas-guard@latest demo --summary
|
|
72
75
|
```
|
|
73
76
|
|
|
74
|
-
The
|
|
77
|
+
The demo command uses packaged public fixtures: `examples/demo-risky-saas` currently returns 19 intentional findings across Stripe, Supabase, silent-success paths, Next/Vercel deploy hints, and GitHub Actions; `examples/demo-safe-saas` returns 0 findings for the same broad surfaces with safer static patterns. Rerun `demo` without `--summary` for the full human-readable report, or see [docs/demo-quickstart.md](docs/demo-quickstart.md) if you want to inspect the fixture files locally.
|
|
75
78
|
|
|
76
79
|
## See The Output
|
|
77
80
|
|
|
78
81
|
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).
|
|
79
82
|
|
|
80
83
|
```text
|
|
81
|
-
|
|
82
|
-
19 findings: 2 critical, 6 high, 7 medium, 3 low, 1 info
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
84
|
+
ai-saas-guard scan summary
|
|
85
|
+
Findings: 19 findings: 2 critical, 6 high, 7 medium, 3 low, 1 info
|
|
86
|
+
Launch gate: blocked: critical launch-readiness findings need review before inviting users
|
|
87
|
+
|
|
88
|
+
Top risks:
|
|
89
|
+
- CRITICAL stripe.webhook.missing-signature at app/api/stripe/webhook/route.ts:1 - Stripe webhook does not verify the Stripe signature
|
|
90
|
+
- CRITICAL supabase.rls.broad-policy at supabase/migrations/001_accounts.sql:10 - Broad Supabase RLS policy on public.accounts
|
|
91
|
+
- HIGH silent-success.swallowed-error at app/api/billing/checkout/route.ts:4 - Catch block may turn upstream failure into success
|
|
92
|
+
|
|
93
|
+
Manual proof to run next:
|
|
94
|
+
- Send a request without a valid Stripe signature and confirm the handler rejects it before changing entitlement state.
|
|
95
|
+
- Run the generated two-account IDOR test and confirm User B cannot read, update, or delete User A resources.
|
|
96
|
+
- Force the upstream billing call to fail and confirm the route returns an error, not fake success.
|
|
97
|
+
|
|
98
|
+
Next steps
|
|
99
|
+
- Fix critical and high trust-boundary findings first.
|
|
100
|
+
- Run the manual proof steps in staging and confirm each risky path fails closed.
|
|
97
101
|
```
|
|
98
102
|
|
|
99
103
|
## What You Get
|
|
@@ -106,7 +110,7 @@ One command returns a launch-readiness report with:
|
|
|
106
110
|
- why the finding matters for an AI-built SaaS launch
|
|
107
111
|
- manual verification steps you can actually run
|
|
108
112
|
- practical fix direction, not generic advice
|
|
109
|
-
- terminal, JSON, SARIF, and PR markdown output for local review or CI
|
|
113
|
+
- short `--summary`, terminal, JSON, SARIF, and PR markdown output for local review or CI
|
|
110
114
|
|
|
111
115
|
## Problems It Helps You Catch
|
|
112
116
|
|
|
@@ -125,7 +129,8 @@ One command returns a launch-readiness report with:
|
|
|
125
129
|
Run the published CLI without installing it globally:
|
|
126
130
|
|
|
127
131
|
```bash
|
|
128
|
-
npx ai-saas-guard@latest
|
|
132
|
+
npx ai-saas-guard@latest demo --summary
|
|
133
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas --summary
|
|
129
134
|
```
|
|
130
135
|
|
|
131
136
|
Run focused checks:
|
|
@@ -140,9 +145,10 @@ npx ai-saas-guard@latest check-mcp --root /path/to/your-saas --policy-template
|
|
|
140
145
|
npx ai-saas-guard@latest check-actions --root /path/to/your-saas
|
|
141
146
|
```
|
|
142
147
|
|
|
143
|
-
|
|
148
|
+
Short or machine-readable output:
|
|
144
149
|
|
|
145
150
|
```bash
|
|
151
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas --summary
|
|
146
152
|
npx ai-saas-guard@latest scan --root /path/to/your-saas --json
|
|
147
153
|
npx ai-saas-guard@latest scan --root /path/to/your-saas --sarif > ai-saas-guard.sarif
|
|
148
154
|
npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --markdown > ai-saas-guard-pr.md
|
|
@@ -179,13 +185,13 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
|
|
|
179
185
|
| Area | Status |
|
|
180
186
|
| --- | --- |
|
|
181
187
|
| Public GitHub repository | Available |
|
|
182
|
-
| npm CLI | `ai-saas-guard@0.
|
|
183
|
-
| GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.
|
|
184
|
-
| Outputs |
|
|
188
|
+
| npm CLI | `ai-saas-guard@0.30.1` |
|
|
189
|
+
| GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.30.1` |
|
|
190
|
+
| Outputs | Short summary, terminal, JSON, SARIF, and PR-focused markdown |
|
|
185
191
|
| Project config | `.ai-saas-guard.json` rule toggles, severity overrides, suppressions, and fail thresholds |
|
|
186
192
|
| Privacy model | Local-first, read-only scan commands, no LLM calls, no code upload |
|
|
187
|
-
| Versioned Action tags | `v0.
|
|
188
|
-
| Current release | `0.
|
|
193
|
+
| Versioned Action tags | `v0.30.1`, `v0` |
|
|
194
|
+
| Current release | `0.30.1` adds `--summary` for first-run launch triage and sharper fix directions for Stripe, Supabase, silent-success, and Next/Vercel findings |
|
|
189
195
|
| npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
|
|
190
196
|
| Repository trust hardening | Strict branch protection, Dependabot, CodeQL, fast-check fuzzing, signed release provenance assets, private vulnerability reporting, secret scanning, and push protection |
|
|
191
197
|
| 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 |
|
|
@@ -352,7 +358,7 @@ Use `suppressions` for narrower false-positive handling when one rule is noisy o
|
|
|
352
358
|
|
|
353
359
|
## GitHub Action
|
|
354
360
|
|
|
355
|
-
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.
|
|
361
|
+
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.30.1` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
|
|
356
362
|
|
|
357
363
|
```yaml
|
|
358
364
|
name: ai-saas-guard
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { applyGuardConfig, loadGuardConfig } from "./config.js";
|
|
4
|
-
import { checkActions, checkMcp, checkStripe, checkSupabase, classifyPrRisk, scanRepository } from "./index.js";
|
|
4
|
+
import { checkActions, checkMcp, checkStripe, checkSupabase, classifyPrRisk, runShowcase, 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";
|
|
8
|
+
import { formatSummaryReport } from "./report/summary.js";
|
|
8
9
|
import { formatTerminalReport } from "./report/terminal.js";
|
|
9
10
|
async function main(argv) {
|
|
10
11
|
const args = parseArgs(argv);
|
|
@@ -12,6 +13,11 @@ async function main(argv) {
|
|
|
12
13
|
process.stdout.write(helpText());
|
|
13
14
|
return 0;
|
|
14
15
|
}
|
|
16
|
+
if (args.command === "demo") {
|
|
17
|
+
const report = await runShowcase();
|
|
18
|
+
process.stdout.write(formatReport(report, args.format));
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
15
21
|
const config = await loadGuardConfig(args.rootDir, args.configPath);
|
|
16
22
|
let report;
|
|
17
23
|
switch (args.command) {
|
|
@@ -69,10 +75,14 @@ function parseArgs(argv) {
|
|
|
69
75
|
result.format = "markdown";
|
|
70
76
|
continue;
|
|
71
77
|
}
|
|
78
|
+
if (arg === "--summary") {
|
|
79
|
+
result.format = "summary";
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
72
82
|
if (arg === "--format") {
|
|
73
83
|
const value = argv[index + 1];
|
|
74
|
-
if (value !== "terminal" && value !== "json" && value !== "sarif" && value !== "markdown") {
|
|
75
|
-
throw new Error("--format requires terminal, json, sarif, or
|
|
84
|
+
if (value !== "terminal" && value !== "json" && value !== "sarif" && value !== "markdown" && value !== "summary") {
|
|
85
|
+
throw new Error("--format requires terminal, json, sarif, markdown, or summary");
|
|
76
86
|
}
|
|
77
87
|
result.format = value;
|
|
78
88
|
index += 1;
|
|
@@ -138,6 +148,8 @@ function formatReport(report, format) {
|
|
|
138
148
|
return formatSarifReport(report);
|
|
139
149
|
if (format === "markdown")
|
|
140
150
|
return formatMarkdownReport(report);
|
|
151
|
+
if (format === "summary")
|
|
152
|
+
return `${formatSummaryReport(report)}\n`;
|
|
141
153
|
return `${formatTerminalReport(report)}\n`;
|
|
142
154
|
}
|
|
143
155
|
function shouldFail(report, failOn) {
|
|
@@ -164,20 +176,23 @@ function helpText() {
|
|
|
164
176
|
Repo-local launch-readiness scanner for AI-built SaaS apps.
|
|
165
177
|
|
|
166
178
|
Usage:
|
|
167
|
-
ai-saas-guard scan [--root <repo>] [--config <file>] [--json|--sarif] [--fail-on <severity>]
|
|
168
|
-
ai-saas-guard
|
|
169
|
-
ai-saas-guard check-
|
|
170
|
-
ai-saas-guard check-
|
|
171
|
-
ai-saas-guard check-
|
|
172
|
-
ai-saas-guard
|
|
179
|
+
ai-saas-guard scan [--root <repo>] [--config <file>] [--json|--sarif|--summary] [--fail-on <severity>]
|
|
180
|
+
ai-saas-guard demo [--json|--markdown|--summary]
|
|
181
|
+
ai-saas-guard check-supabase [--root <repo>] [--config <file>] [--doctor] [--json|--sarif|--summary] [--fail-on <severity>]
|
|
182
|
+
ai-saas-guard check-stripe [--root <repo>] [--config <file>] [--json|--sarif|--summary] [--fail-on <severity>]
|
|
183
|
+
ai-saas-guard check-mcp [--root <repo>] [--config <file>] [--policy-template] [--json|--sarif|--summary] [--fail-on <severity>]
|
|
184
|
+
ai-saas-guard check-actions [--root <repo>] [--config <file>] [--json|--sarif|--summary] [--fail-on <severity>]
|
|
185
|
+
ai-saas-guard pr-risk [--root <repo>] [--config <file>] [--base <branch>] [--json|--sarif|--markdown|--summary] [--fail-on <severity>]
|
|
173
186
|
|
|
174
187
|
Defaults:
|
|
175
188
|
- read-only
|
|
176
189
|
- no network calls
|
|
177
190
|
- no account or login required
|
|
191
|
+
- demo uses packaged public fixtures and ignores project config/fail thresholds
|
|
178
192
|
- terminal output by default, JSON with --json
|
|
179
193
|
- SARIF output for GitHub code scanning with --sarif
|
|
180
194
|
- PR-focused markdown summary with --markdown
|
|
195
|
+
- first-run launch summary with --summary
|
|
181
196
|
- project config auto-loaded from .ai-saas-guard.json when present
|
|
182
197
|
`;
|
|
183
198
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { dirname, resolve } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { scanRepository } from "./scan.js";
|
|
4
|
+
import { createReport } from "../report/findings.js";
|
|
5
|
+
import { nextSteps } from "../report/launchGate.js";
|
|
6
|
+
export async function runShowcase() {
|
|
7
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
8
|
+
const risky = await scanRepository({
|
|
9
|
+
rootDir: resolve(packageRoot, "examples", "demo-risky-saas")
|
|
10
|
+
});
|
|
11
|
+
const safe = await scanRepository({
|
|
12
|
+
rootDir: resolve(packageRoot, "examples", "demo-safe-saas")
|
|
13
|
+
});
|
|
14
|
+
return createReport("demo", packageRoot, risky.findings, {
|
|
15
|
+
demos: {
|
|
16
|
+
risky,
|
|
17
|
+
safe
|
|
18
|
+
},
|
|
19
|
+
nextSteps: nextSteps(risky.findings)
|
|
20
|
+
});
|
|
21
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { scanRepository } from "./commands/scan.js";
|
|
2
|
+
export { runShowcase } from "./commands/demo.js";
|
|
2
3
|
export { checkStripe } from "./commands/checkStripe.js";
|
|
3
4
|
export { checkSupabase } from "./commands/checkSupabase.js";
|
|
4
5
|
export { checkMcp } from "./commands/checkMcp.js";
|
|
@@ -7,7 +8,8 @@ export { classifyPrRisk } from "./commands/prRisk.js";
|
|
|
7
8
|
export { applyGuardConfig, defaultConfigFileName, loadGuardConfig } from "./config.js";
|
|
8
9
|
export { createScanContext } from "./context.js";
|
|
9
10
|
export { getRuleMetadata, RULE_CATALOG } from "./rules/catalog.js";
|
|
10
|
-
export
|
|
11
|
+
export { formatSummaryReport } from "./report/summary.js";
|
|
12
|
+
export type { BaseReport, CommandName, Evidence, Finding, ActionsReport, McpOptions, McpPolicyTemplate, McpReport, McpServerInventory, McpSideEffect, PrRiskFile, PrRiskReport, ScanOptions, ShowcaseReport, StripeReport, SupabaseOptions, SupabaseDoctorReport, SupabaseReport } from "./types.js";
|
|
11
13
|
export type { ScanContext, ScanInput } from "./context.js";
|
|
12
14
|
export type { FindingSuppression, GuardConfig, RuleConfigValue } from "./config.js";
|
|
13
15
|
export type { RuleMetadata, RuleStability } from "./rules/catalog.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { scanRepository } from "./commands/scan.js";
|
|
2
|
+
export { runShowcase } from "./commands/demo.js";
|
|
2
3
|
export { checkStripe } from "./commands/checkStripe.js";
|
|
3
4
|
export { checkSupabase } from "./commands/checkSupabase.js";
|
|
4
5
|
export { checkMcp } from "./commands/checkMcp.js";
|
|
@@ -7,3 +8,4 @@ export { classifyPrRisk } from "./commands/prRisk.js";
|
|
|
7
8
|
export { applyGuardConfig, defaultConfigFileName, loadGuardConfig } from "./config.js";
|
|
8
9
|
export { createScanContext } from "./context.js";
|
|
9
10
|
export { getRuleMetadata, RULE_CATALOG } from "./rules/catalog.js";
|
|
11
|
+
export { formatSummaryReport } from "./report/summary.js";
|
|
@@ -2,3 +2,4 @@ import type { BaseReport, Finding } from "../types.js";
|
|
|
2
2
|
export declare function launchGateVerdict(report: BaseReport): string;
|
|
3
3
|
export declare function reviewFirst(findings: Finding[], limit?: number): string[];
|
|
4
4
|
export declare function manualProofSteps(findings: Finding[], limit?: number): string[];
|
|
5
|
+
export declare function nextSteps(findings: Finding[]): string[];
|
|
@@ -34,3 +34,25 @@ export function manualProofSteps(findings, limit = 3) {
|
|
|
34
34
|
}
|
|
35
35
|
return steps;
|
|
36
36
|
}
|
|
37
|
+
export function nextSteps(findings) {
|
|
38
|
+
if (findings.length === 0) {
|
|
39
|
+
return [
|
|
40
|
+
"Keep this report with the launch checklist, then run a two-account auth/data-access check and a deploy-preview smoke before inviting real users."
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
const steps = [];
|
|
44
|
+
const hasCriticalOrHigh = findings.some((finding) => finding.severity === "critical" || finding.severity === "high");
|
|
45
|
+
const hasMedium = findings.some((finding) => finding.severity === "medium");
|
|
46
|
+
const hasLowNoise = findings.some((finding) => finding.severity === "low" || finding.severity === "info");
|
|
47
|
+
if (hasCriticalOrHigh) {
|
|
48
|
+
steps.push("Fix critical and high trust-boundary findings first: auth/session, billing/webhook, tenant data, and silent-success paths.");
|
|
49
|
+
}
|
|
50
|
+
else if (hasMedium) {
|
|
51
|
+
steps.push("Verify medium-risk launch findings with the owning developer before production traffic.");
|
|
52
|
+
}
|
|
53
|
+
steps.push("Run the manual proof steps above in staging and confirm each risky path fails closed.");
|
|
54
|
+
if (hasLowNoise) {
|
|
55
|
+
steps.push("Treat low and info deploy/CI hygiene hints as cleanup after critical, high, and medium launch paths are understood.");
|
|
56
|
+
}
|
|
57
|
+
return steps;
|
|
58
|
+
}
|
package/dist/report/markdown.js
CHANGED
|
@@ -1,9 +1,36 @@
|
|
|
1
|
-
import { launchGateVerdict, manualProofSteps, reviewFirst } from "./launchGate.js";
|
|
1
|
+
import { launchGateVerdict, manualProofSteps, nextSteps, reviewFirst } from "./launchGate.js";
|
|
2
2
|
export function formatMarkdownReport(report) {
|
|
3
|
+
if (report.command === "demo")
|
|
4
|
+
return `${formatDemoMarkdown(report)}\n`;
|
|
3
5
|
if (report.command === "pr-risk")
|
|
4
6
|
return `${formatPrRiskMarkdown(report)}\n`;
|
|
5
7
|
return `${formatGenericMarkdown(report)}\n`;
|
|
6
8
|
}
|
|
9
|
+
function formatDemoMarkdown(report) {
|
|
10
|
+
const lines = [];
|
|
11
|
+
lines.push("## ai-saas-guard demo");
|
|
12
|
+
lines.push("");
|
|
13
|
+
lines.push("Synthetic public demo for the local-first launch gate. This is not a pentest, full audit, or certification.");
|
|
14
|
+
lines.push("");
|
|
15
|
+
lines.push(`- Risky demo: ${escapeMarkdownInline(summaryText(report.demos.risky))}`);
|
|
16
|
+
lines.push(`- Safe demo: ${escapeMarkdownInline(summaryText(report.demos.safe))}`);
|
|
17
|
+
lines.push("");
|
|
18
|
+
lines.push("### Review First");
|
|
19
|
+
appendList(lines, reviewFirst(report.demos.risky.findings).map(escapeMarkdownInline));
|
|
20
|
+
lines.push("");
|
|
21
|
+
lines.push("### Manual Proof To Run Next");
|
|
22
|
+
appendList(lines, manualProofSteps(report.demos.risky.findings).map(escapeMarkdownInline));
|
|
23
|
+
lines.push("");
|
|
24
|
+
lines.push("### Next Steps");
|
|
25
|
+
appendList(lines, report.nextSteps.map(escapeMarkdownInline));
|
|
26
|
+
lines.push("");
|
|
27
|
+
lines.push("Run against your app:");
|
|
28
|
+
lines.push("");
|
|
29
|
+
lines.push("```bash");
|
|
30
|
+
lines.push("npx ai-saas-guard@latest scan --root /path/to/your-saas");
|
|
31
|
+
lines.push("```");
|
|
32
|
+
return lines.join("\n");
|
|
33
|
+
}
|
|
7
34
|
function formatPrRiskMarkdown(report) {
|
|
8
35
|
const lines = [];
|
|
9
36
|
lines.push("## ai-saas-guard PR risk summary");
|
|
@@ -69,6 +96,9 @@ function appendLaunchQueue(lines, findings) {
|
|
|
69
96
|
lines.push("");
|
|
70
97
|
lines.push("### Manual Proof To Run Next");
|
|
71
98
|
appendList(lines, manualProofSteps(findings).map(escapeMarkdownInline));
|
|
99
|
+
lines.push("");
|
|
100
|
+
lines.push("### Next Steps");
|
|
101
|
+
appendList(lines, nextSteps(findings).map(escapeMarkdownInline));
|
|
72
102
|
}
|
|
73
103
|
function appendFindings(lines, findings) {
|
|
74
104
|
if (findings.length === 0) {
|
|
@@ -135,3 +165,8 @@ function escapeMarkdownTableCell(value) {
|
|
|
135
165
|
function escapeMarkdownInline(value) {
|
|
136
166
|
return value.replace(/\r?\n/g, " ").replaceAll("|", "\\|").trim();
|
|
137
167
|
}
|
|
168
|
+
function summaryText(report) {
|
|
169
|
+
if (report.summary.total === 0)
|
|
170
|
+
return "0 findings";
|
|
171
|
+
return `${report.summary.total} findings: ${report.summary.critical} critical, ${report.summary.high} high, ${report.summary.medium} medium, ${report.summary.low} low, ${report.summary.info} info`;
|
|
172
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { launchGateVerdict, manualProofSteps, nextSteps, reviewFirst } from "./launchGate.js";
|
|
2
|
+
export function formatSummaryReport(report) {
|
|
3
|
+
if (report.command === "demo")
|
|
4
|
+
return formatShowcaseSummary(report);
|
|
5
|
+
const lines = [];
|
|
6
|
+
lines.push(`ai-saas-guard ${report.command} summary`);
|
|
7
|
+
lines.push(`Root: ${report.rootDir}`);
|
|
8
|
+
lines.push(`Findings: ${summaryText(report)}`);
|
|
9
|
+
lines.push(`Launch gate: ${launchGateVerdict(report)}`);
|
|
10
|
+
if (report.findings.length === 0) {
|
|
11
|
+
lines.push("");
|
|
12
|
+
lines.push("No heuristic launch-readiness risks found by this command.");
|
|
13
|
+
lines.push("");
|
|
14
|
+
lines.push("Next steps:");
|
|
15
|
+
appendList(lines, nextSteps(report.findings));
|
|
16
|
+
lines.push("");
|
|
17
|
+
lines.push("Full report:");
|
|
18
|
+
lines.push(" Rerun without --summary, or use --json, --sarif, or --markdown where supported.");
|
|
19
|
+
return lines.join("\n");
|
|
20
|
+
}
|
|
21
|
+
lines.push("");
|
|
22
|
+
lines.push("Top risks:");
|
|
23
|
+
appendList(lines, reviewFirst(report.findings, 3));
|
|
24
|
+
lines.push("");
|
|
25
|
+
lines.push("Manual proof to run next:");
|
|
26
|
+
appendList(lines, manualProofSteps(report.findings, 3));
|
|
27
|
+
lines.push("");
|
|
28
|
+
lines.push("Next steps:");
|
|
29
|
+
appendList(lines, nextSteps(report.findings));
|
|
30
|
+
lines.push("");
|
|
31
|
+
lines.push("Full report:");
|
|
32
|
+
lines.push(" Rerun without --summary, or use --json, --sarif, or --markdown where supported.");
|
|
33
|
+
return lines.join("\n");
|
|
34
|
+
}
|
|
35
|
+
function formatShowcaseSummary(report) {
|
|
36
|
+
const lines = [];
|
|
37
|
+
lines.push("ai-saas-guard demo summary");
|
|
38
|
+
lines.push("Synthetic public demo for the local-first launch gate.");
|
|
39
|
+
lines.push("This is not a pentest, full audit, or certification.");
|
|
40
|
+
lines.push("");
|
|
41
|
+
lines.push(`Risky demo: ${summaryText(report.demos.risky)}`);
|
|
42
|
+
lines.push(`Safe demo: ${summaryText(report.demos.safe)}`);
|
|
43
|
+
lines.push(`Launch gate: ${launchGateVerdict(report.demos.risky)}`);
|
|
44
|
+
lines.push("");
|
|
45
|
+
lines.push("Top risks:");
|
|
46
|
+
appendList(lines, reviewFirst(report.demos.risky.findings, 3));
|
|
47
|
+
lines.push("");
|
|
48
|
+
lines.push("Manual proof to run next:");
|
|
49
|
+
appendList(lines, manualProofSteps(report.demos.risky.findings, 3));
|
|
50
|
+
lines.push("");
|
|
51
|
+
lines.push("Next steps:");
|
|
52
|
+
appendList(lines, report.nextSteps);
|
|
53
|
+
lines.push("");
|
|
54
|
+
lines.push("Full report:");
|
|
55
|
+
lines.push(" Rerun `ai-saas-guard demo` without --summary or use --json/--markdown.");
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
function appendList(lines, items) {
|
|
59
|
+
for (const item of items) {
|
|
60
|
+
lines.push(`- ${item}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function summaryText(report) {
|
|
64
|
+
if (report.summary.total === 0)
|
|
65
|
+
return "0 findings";
|
|
66
|
+
return `${report.summary.total} findings: ${report.summary.critical} critical, ${report.summary.high} high, ${report.summary.medium} medium, ${report.summary.low} low, ${report.summary.info} info`;
|
|
67
|
+
}
|
package/dist/report/terminal.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { launchGateVerdict, manualProofSteps, reviewFirst } from "./launchGate.js";
|
|
1
|
+
import { launchGateVerdict, manualProofSteps, nextSteps, reviewFirst } from "./launchGate.js";
|
|
2
2
|
export function formatTerminalReport(report) {
|
|
3
|
+
if (report.command === "demo")
|
|
4
|
+
return formatDemoTerminalReport(report);
|
|
3
5
|
const lines = [];
|
|
4
6
|
lines.push(`ai-saas-guard ${report.command}`);
|
|
5
7
|
lines.push(`Root: ${report.rootDir}`);
|
|
@@ -21,6 +23,11 @@ export function formatTerminalReport(report) {
|
|
|
21
23
|
for (const step of manualProofSteps(report.findings)) {
|
|
22
24
|
lines.push(`- ${step}`);
|
|
23
25
|
}
|
|
26
|
+
lines.push("");
|
|
27
|
+
lines.push("Next steps:");
|
|
28
|
+
for (const step of nextSteps(report.findings)) {
|
|
29
|
+
lines.push(`- ${step}`);
|
|
30
|
+
}
|
|
24
31
|
for (const [index, item] of report.findings.entries()) {
|
|
25
32
|
lines.push("");
|
|
26
33
|
lines.push(`${index + 1}. [${item.severity.toUpperCase()}] ${item.title}`);
|
|
@@ -38,6 +45,41 @@ export function formatTerminalReport(report) {
|
|
|
38
45
|
appendCommandExtras(lines, report);
|
|
39
46
|
return lines.join("\n");
|
|
40
47
|
}
|
|
48
|
+
function formatDemoTerminalReport(report) {
|
|
49
|
+
const lines = [];
|
|
50
|
+
lines.push("ai-saas-guard demo");
|
|
51
|
+
lines.push("Synthetic public demo for the local-first launch gate.");
|
|
52
|
+
lines.push("This is not a pentest, full audit, or certification.");
|
|
53
|
+
lines.push("");
|
|
54
|
+
lines.push(`Risky demo: ${summaryText(report.demos.risky)}`);
|
|
55
|
+
lines.push(`Safe demo: ${summaryText(report.demos.safe)}`);
|
|
56
|
+
lines.push("");
|
|
57
|
+
lines.push("Review first:");
|
|
58
|
+
for (const item of reviewFirst(report.demos.risky.findings)) {
|
|
59
|
+
lines.push(`- ${item}`);
|
|
60
|
+
}
|
|
61
|
+
lines.push("");
|
|
62
|
+
lines.push("Manual proof to run next:");
|
|
63
|
+
for (const step of manualProofSteps(report.demos.risky.findings)) {
|
|
64
|
+
lines.push(`- ${step}`);
|
|
65
|
+
}
|
|
66
|
+
lines.push("");
|
|
67
|
+
lines.push("Next steps:");
|
|
68
|
+
for (const step of report.nextSteps) {
|
|
69
|
+
lines.push(`- ${step}`);
|
|
70
|
+
}
|
|
71
|
+
lines.push("");
|
|
72
|
+
lines.push("Run against your app:");
|
|
73
|
+
lines.push(" npx ai-saas-guard@latest scan --root /path/to/your-saas");
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push("Read more: https://github.com/zr9959/ai-saas-guard/blob/main/docs/demo-quickstart.md");
|
|
76
|
+
return lines.join("\n");
|
|
77
|
+
}
|
|
78
|
+
function summaryText(report) {
|
|
79
|
+
if (report.summary.total === 0)
|
|
80
|
+
return "0 findings";
|
|
81
|
+
return `${report.summary.total} findings: ${report.summary.critical} critical, ${report.summary.high} high, ${report.summary.medium} medium, ${report.summary.low} low, ${report.summary.info} info`;
|
|
82
|
+
}
|
|
41
83
|
function appendCommandExtras(lines, report) {
|
|
42
84
|
if (report.command === "check-supabase") {
|
|
43
85
|
const supabase = report;
|
package/dist/scanners/deploy.js
CHANGED
|
@@ -120,7 +120,7 @@ export async function scanDeployConfig(input) {
|
|
|
120
120
|
evidence: [{ file: evidenceFile.path, line, snippet: lineAt(evidenceFile.content, line) }],
|
|
121
121
|
why: "Auth, payment, and API routes should launch with explicit browser security headers rather than relying on platform defaults.",
|
|
122
122
|
suggestedVerification: "Run a production build or deploy preview and inspect response headers for auth, billing, and API pages.",
|
|
123
|
-
suggestedFix: "Add `headers()` in `next.config` or middleware for
|
|
123
|
+
suggestedFix: "Add `headers()` in `next.config` or middleware for `Content-Security-Policy`, `X-Frame-Options`, `X-Content-Type-Options`, and `Referrer-Policy` on auth, billing, and API surfaces."
|
|
124
124
|
}));
|
|
125
125
|
}
|
|
126
126
|
if (envExample) {
|
|
@@ -40,7 +40,7 @@ function scanSwallowedErrors(filePath, content) {
|
|
|
40
40
|
evidence: [{ file: filePath, line: block.line, snippet: lineAt(content, block.line) }],
|
|
41
41
|
why: "AI-generated SaaS code often hides integration failures by returning empty, null, or success-shaped data after an exception.",
|
|
42
42
|
suggestedVerification: "Force the upstream API, auth provider, billing provider, or database call to fail and confirm the route returns an error or disclosed degraded mode, not a fake success.",
|
|
43
|
-
suggestedFix: "Log the failure, return
|
|
43
|
+
suggestedFix: "Log the failure with a request id, return a 4xx/5xx error or explicit degraded-mode response, and do not grant entitlement, change ownership, or mutate tenant data after the failed dependency."
|
|
44
44
|
}));
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -53,7 +53,7 @@ function scanSwallowedErrors(filePath, content) {
|
|
|
53
53
|
evidence: [{ file: filePath, line, snippet: lineAt(content, line) }],
|
|
54
54
|
why: "A `.catch()` fallback that returns default success data can make failed integrations look healthy before launch.",
|
|
55
55
|
suggestedVerification: "Make the promise reject in a local test and confirm callers receive an error or disclosed degraded mode.",
|
|
56
|
-
suggestedFix: "Propagate the error or return an explicit failure response with logging and
|
|
56
|
+
suggestedFix: "Propagate the error or return an explicit failure response with request-id logging, and keep entitlement, ownership, and tenant mutations behind the successful dependency path."
|
|
57
57
|
}));
|
|
58
58
|
}
|
|
59
59
|
return findings;
|
|
@@ -102,7 +102,7 @@ function scanHardcodedFallbacks(filePath, content) {
|
|
|
102
102
|
evidence: [{ file: filePath, line, snippet: lineAt(content, line) }],
|
|
103
103
|
why: "Hardcoded fallback responses in auth, Stripe, Supabase, OpenAI, payment, or entitlement paths can grant access or hide broken integrations.",
|
|
104
104
|
suggestedVerification: "Disable the real upstream provider and confirm the route does not return hardcoded active subscriptions, successful auth, or generated sample data.",
|
|
105
|
-
suggestedFix: "Replace hardcoded success fallbacks with explicit error/degraded responses and a
|
|
105
|
+
suggestedFix: "Replace hardcoded success fallbacks with explicit error/degraded responses, request-id logging, and a staging check that proves real provider failure cannot grant access."
|
|
106
106
|
}));
|
|
107
107
|
}
|
|
108
108
|
return findings;
|
package/dist/scanners/stripe.js
CHANGED
|
@@ -76,7 +76,7 @@ export async function checkStripe(input) {
|
|
|
76
76
|
],
|
|
77
77
|
why: "Without `stripe.webhooks.constructEvent` and the `stripe-signature` header, attackers can forge billing events that grant or revoke access.",
|
|
78
78
|
suggestedVerification: "Send a request without a valid Stripe signature and confirm the handler rejects it before changing entitlement state.",
|
|
79
|
-
suggestedFix: "
|
|
79
|
+
suggestedFix: "In Next.js route handlers, read the payload with `await req.text()`, read the `stripe-signature` header, call `stripe.webhooks.constructEvent(body, signature, STRIPE_WEBHOOK_SECRET)`, and return 400 before any entitlement mutation when verification fails."
|
|
80
80
|
}));
|
|
81
81
|
}
|
|
82
82
|
if (hasConstructEvent && usesJsonBody && !usesRawBody) {
|
|
@@ -87,7 +87,7 @@ export async function checkStripe(input) {
|
|
|
87
87
|
evidence: [{ file: file.path, line: firstLineMatching(file.content, /req\.json\s*\(/), snippet: firstSnippetMatching(file.content, /req\.json\s*\(/) }],
|
|
88
88
|
why: "Stripe signature checks require the exact raw payload bytes; parsed JSON can make verification fail or be bypassed in rewrites.",
|
|
89
89
|
suggestedVerification: "Replay a signed test webhook through the deployed route and confirm signature verification succeeds only with the raw body.",
|
|
90
|
-
suggestedFix: "Use `await req.text()` in Next.js route handlers or equivalent raw-body middleware
|
|
90
|
+
suggestedFix: "Use `await req.text()` in Next.js route handlers or equivalent raw-body middleware, pass that exact body into `constructEvent`, and reject bad signatures before any billing state change."
|
|
91
91
|
}));
|
|
92
92
|
}
|
|
93
93
|
if (/NEXT_PUBLIC_STRIPE_WEBHOOK_SECRET|NEXT_PUBLIC_STRIPE_SECRET_KEY/.test(file.content)) {
|
|
@@ -56,7 +56,7 @@ export async function checkSupabase(input, options = {}) {
|
|
|
56
56
|
evidence: [{ file: file.path, line, snippet: lineAt(file.content, line) }],
|
|
57
57
|
why: "A broad RLS predicate can make user data readable or writable across accounts even when login exists.",
|
|
58
58
|
suggestedVerification: "Run the generated two-account IDOR test and confirm User B cannot read, update, or delete User A resources.",
|
|
59
|
-
suggestedFix: "Replace broad predicates with ownership checks such as `auth.uid() = user_id`
|
|
59
|
+
suggestedFix: "Replace broad predicates with ownership checks such as `auth.uid() = user_id`; for writes, mirror the same scope in `WITH CHECK`, and rerun the two-account cross-tenant verification."
|
|
60
60
|
}));
|
|
61
61
|
}
|
|
62
62
|
const combinedPredicate = predicates.join(" ");
|
|
@@ -75,7 +75,7 @@ export async function checkSupabase(input, options = {}) {
|
|
|
75
75
|
evidence: [{ file: file.path, line, snippet: lineAt(file.content, line) }],
|
|
76
76
|
why: "Founders often confuse authentication with authorization; table policies need resource-level ownership checks.",
|
|
77
77
|
suggestedVerification: "Create the same resource as User A and attempt to read, update, and delete it with User B's session.",
|
|
78
|
-
suggestedFix: "Reference `auth.uid()` and a stable
|
|
78
|
+
suggestedFix: "Reference `auth.uid()` and a stable owner column, or join through a tenant/workspace membership table, in every sensitive table policy."
|
|
79
79
|
}));
|
|
80
80
|
}
|
|
81
81
|
if (sensitiveTablePattern.test(tableName) && hasWeakWithCheck(policy)) {
|
|
@@ -93,7 +93,7 @@ export async function checkSupabase(input, options = {}) {
|
|
|
93
93
|
evidence: [{ file: file.path, line, snippet: lineAt(file.content, line) }],
|
|
94
94
|
why: "A write policy can read the right tenant rows but still allow inserted or updated rows to move into another owner or tenant unless `WITH CHECK` is scoped.",
|
|
95
95
|
suggestedVerification: "As User A, try inserting or updating a row with User B's owner, organization, workspace, or tenant ID and confirm the database rejects it.",
|
|
96
|
-
suggestedFix: "
|
|
96
|
+
suggestedFix: "Use `auth.uid()` in a `WITH CHECK` predicate tied to the same owner, tenant, or membership relationship used by the read policy."
|
|
97
97
|
}));
|
|
98
98
|
}
|
|
99
99
|
}
|
|
@@ -174,7 +174,7 @@ function buildDoctorFindings(files, tables, rlsEnabledTables, policies) {
|
|
|
174
174
|
evidence: [{ file: policy.file, line: policy.line, snippet: lineAt(files.find((file) => file.path === policy.file)?.content ?? "", policy.line) }],
|
|
175
175
|
why: "A common RLS launch failure is reads working while inserts, updates, or deletes silently fail because write policies are missing.",
|
|
176
176
|
suggestedVerification: "As User A, try INSERT, UPDATE, and DELETE on an owned row; then repeat as User B and confirm only the intended operations pass.",
|
|
177
|
-
suggestedFix: "Add scoped INSERT/UPDATE/DELETE policies with `WITH CHECK` predicates where the product supports writes."
|
|
177
|
+
suggestedFix: "Add scoped INSERT/UPDATE/DELETE policies with `WITH CHECK` predicates tied to `auth.uid()` or tenant membership where the product supports writes."
|
|
178
178
|
}));
|
|
179
179
|
}
|
|
180
180
|
if (isTenantLikeTable(table) && tablePolicies.length > 0 && !tablePolicies.some((policy) => hasTenantPredicate(policy, table))) {
|
|
@@ -186,7 +186,7 @@ function buildDoctorFindings(files, tables, rlsEnabledTables, policies) {
|
|
|
186
186
|
evidence: [{ file: policy.file, line: policy.line, snippet: lineAt(files.find((file) => file.path === policy.file)?.content ?? "", policy.line) }],
|
|
187
187
|
why: "Multi-tenant tables need tenant, workspace, organization, project, client, owner, or membership predicates, not just generic login checks.",
|
|
188
188
|
suggestedVerification: "Create rows in two tenants and confirm User A cannot SELECT, INSERT, UPDATE, or DELETE User B's tenant rows.",
|
|
189
|
-
suggestedFix: "Tie every policy to tenant/workspace/organization membership or owner columns and mirror the same scope in `WITH CHECK
|
|
189
|
+
suggestedFix: "Tie every policy to tenant/workspace/organization membership or owner columns, and mirror the same tenant scope in `WITH CHECK` for INSERT and UPDATE."
|
|
190
190
|
}));
|
|
191
191
|
}
|
|
192
192
|
}
|
|
@@ -201,7 +201,7 @@ function buildDoctorFindings(files, tables, rlsEnabledTables, policies) {
|
|
|
201
201
|
evidence: [{ file: policy.file, line: policy.line, snippet: lineAt(fileContent, policy.line) }],
|
|
202
202
|
why: "Public write policies can allow anonymous or unintended clients to insert or mutate data when predicates are incomplete or misunderstood.",
|
|
203
203
|
suggestedVerification: "Try the INSERT/UPDATE/DELETE path with an anonymous client and with User B's session; expected result is denial unless explicitly intended.",
|
|
204
|
-
suggestedFix: "Grant write policies to authenticated roles only and require owner or tenant `WITH CHECK` predicates."
|
|
204
|
+
suggestedFix: "Grant write policies to authenticated roles only and require owner or tenant `WITH CHECK` predicates tied to `auth.uid()` or membership."
|
|
205
205
|
}));
|
|
206
206
|
}
|
|
207
207
|
const mismatch = findAuthUidColumnMismatch(predicate, tables.find((table) => table.name === policy.tableName));
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type Severity = "critical" | "high" | "medium" | "low" | "info";
|
|
2
|
-
export type CommandName = "scan" | "check-supabase" | "check-stripe" | "check-mcp" | "check-actions" | "pr-risk";
|
|
2
|
+
export type CommandName = "scan" | "demo" | "check-supabase" | "check-stripe" | "check-mcp" | "check-actions" | "pr-risk";
|
|
3
3
|
export interface Evidence {
|
|
4
4
|
file: string;
|
|
5
5
|
line?: number;
|
|
@@ -40,6 +40,14 @@ export interface BaseReport {
|
|
|
40
40
|
findings: Finding[];
|
|
41
41
|
summary: Summary;
|
|
42
42
|
}
|
|
43
|
+
export interface ShowcaseReport extends BaseReport {
|
|
44
|
+
command: "demo";
|
|
45
|
+
demos: {
|
|
46
|
+
risky: BaseReport;
|
|
47
|
+
safe: BaseReport;
|
|
48
|
+
};
|
|
49
|
+
nextSteps: string[];
|
|
50
|
+
}
|
|
43
51
|
export interface StripeReport extends BaseReport {
|
|
44
52
|
command: "check-stripe";
|
|
45
53
|
webhookFiles: string[];
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -45,10 +45,16 @@ AI 能很快把一个 SaaS 做到“看起来能用”:能登录、能打开 c
|
|
|
45
45
|
|
|
46
46
|
## 60 秒本地检查
|
|
47
47
|
|
|
48
|
+
不用 clone 仓库,先看公开 demo 输出:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx ai-saas-guard@latest demo --summary
|
|
52
|
+
```
|
|
53
|
+
|
|
48
54
|
无需全局安装,直接扫你的应用:
|
|
49
55
|
|
|
50
56
|
```bash
|
|
51
|
-
npx ai-saas-guard@latest scan --root /path/to/your-saas
|
|
57
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas --summary
|
|
52
58
|
```
|
|
53
59
|
|
|
54
60
|
如果是 AI 生成的大 PR:
|
|
@@ -57,42 +63,40 @@ npx ai-saas-guard@latest scan --root /path/to/your-saas
|
|
|
57
63
|
npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --markdown
|
|
58
64
|
```
|
|
59
65
|
|
|
60
|
-
|
|
66
|
+
`--summary` 会先给上线判断、前三个风险、人工验证和下一步。去掉 `--summary` 后会看到每个 finding 的 rule ID、severity、文件证据、为什么重要、如何人工验证,以及具体修复方向。扫描是 deterministic、只读的,不调用 LLM。
|
|
61
67
|
|
|
62
68
|
## 先试公开 demo
|
|
63
69
|
|
|
64
70
|
如果你还不想先扫自己的私有仓库,可以先跑公开 fixture:
|
|
65
71
|
|
|
66
72
|
```bash
|
|
67
|
-
|
|
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
|
|
73
|
+
npx ai-saas-guard@latest demo --summary
|
|
71
74
|
```
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
demo 命令使用包内公开 fixture:`examples/demo-risky-saas` 当前会故意触发 19 个 finding,覆盖 Stripe、Supabase、silent-success、Next/Vercel deploy 提示和 GitHub Actions;`examples/demo-safe-saas` 在同类风险面上使用更安全的静态写法,当前返回 0 个 finding。去掉 `--summary` 可看完整报告;想本地查看 fixture 文件时再看 [demo-quickstart.md](demo-quickstart.md)。
|
|
74
77
|
|
|
75
78
|
## 输出长什么样
|
|
76
79
|
|
|
77
80
|
报告是给上线前或合并 AI 大 PR 前快速阅读的。更完整的可复制样例见 [docs/sample-launch-report.md](sample-launch-report.md)。
|
|
78
81
|
|
|
79
82
|
```text
|
|
80
|
-
|
|
81
|
-
19 findings: 2 critical, 6 high, 7 medium, 3 low, 1 info
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
83
|
+
ai-saas-guard scan summary
|
|
84
|
+
Findings: 19 findings: 2 critical, 6 high, 7 medium, 3 low, 1 info
|
|
85
|
+
Launch gate: blocked: critical launch-readiness findings need review before inviting users
|
|
86
|
+
|
|
87
|
+
Top risks:
|
|
88
|
+
- CRITICAL stripe.webhook.missing-signature at app/api/stripe/webhook/route.ts:1 - Stripe webhook does not verify the Stripe signature
|
|
89
|
+
- CRITICAL supabase.rls.broad-policy at supabase/migrations/001_accounts.sql:10 - Broad Supabase RLS policy on public.accounts
|
|
90
|
+
- HIGH silent-success.swallowed-error at app/api/billing/checkout/route.ts:4 - Catch block may turn upstream failure into success
|
|
91
|
+
|
|
92
|
+
Manual proof to run next:
|
|
93
|
+
- Send a request without a valid Stripe signature and confirm the handler rejects it before changing entitlement state.
|
|
94
|
+
- Run the generated two-account IDOR test and confirm User B cannot read, update, or delete User A resources.
|
|
95
|
+
- Force the upstream billing call to fail and confirm the route returns an error, not fake success.
|
|
96
|
+
|
|
97
|
+
Next steps
|
|
98
|
+
- 先修 critical/high 的信任边界 finding。
|
|
99
|
+
- 在 staging 跑 manual proof,确认每个风险路径都会 fail closed。
|
|
96
100
|
```
|
|
97
101
|
|
|
98
102
|
## 你会得到什么
|
|
@@ -105,7 +109,7 @@ Verify: inspect production response headers for auth, billing, and API pages.
|
|
|
105
109
|
- 说明它为什么会影响 AI 构建的 SaaS 上线
|
|
106
110
|
- 给出可以人工复现的验证步骤
|
|
107
111
|
- 给出实际修复方向,不只是一句泛泛建议
|
|
108
|
-
-
|
|
112
|
+
- 支持短 `--summary`、terminal、JSON、SARIF 和 PR markdown,方便本地或 CI 使用
|
|
109
113
|
|
|
110
114
|
## 它能帮你抓住哪些问题
|
|
111
115
|
|
|
@@ -124,7 +128,8 @@ Verify: inspect production response headers for auth, billing, and API pages.
|
|
|
124
128
|
无需全局安装,直接运行:
|
|
125
129
|
|
|
126
130
|
```bash
|
|
127
|
-
npx ai-saas-guard@latest
|
|
131
|
+
npx ai-saas-guard@latest demo --summary
|
|
132
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas --summary
|
|
128
133
|
```
|
|
129
134
|
|
|
130
135
|
运行专项检查:
|
|
@@ -139,9 +144,10 @@ npx ai-saas-guard@latest check-mcp --root /path/to/your-saas --policy-template
|
|
|
139
144
|
npx ai-saas-guard@latest check-actions --root /path/to/your-saas
|
|
140
145
|
```
|
|
141
146
|
|
|
142
|
-
|
|
147
|
+
短输出和机器可读输出:
|
|
143
148
|
|
|
144
149
|
```bash
|
|
150
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas --summary
|
|
145
151
|
npx ai-saas-guard@latest scan --root /path/to/your-saas --json
|
|
146
152
|
npx ai-saas-guard@latest scan --root /path/to/your-saas --sarif > ai-saas-guard.sarif
|
|
147
153
|
npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --markdown > ai-saas-guard-pr.md
|
|
@@ -161,18 +167,18 @@ node dist/cli.js scan --root /path/to/your-saas
|
|
|
161
167
|
|
|
162
168
|
这个仓库是公开 GitHub 仓库。
|
|
163
169
|
|
|
164
|
-
CLI 已发布到 npm:`ai-saas-guard@0.
|
|
170
|
+
CLI 已发布到 npm:`ai-saas-guard@0.30.1`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.30.1`。
|
|
165
171
|
|
|
166
172
|
| 模块 | 状态 |
|
|
167
173
|
| --- | --- |
|
|
168
174
|
| 公开 GitHub 仓库 | 已可用 |
|
|
169
|
-
| npm CLI | `ai-saas-guard@0.
|
|
170
|
-
| GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.
|
|
171
|
-
| 输出格式 | Terminal、JSON、SARIF 和 PR markdown |
|
|
175
|
+
| npm CLI | `ai-saas-guard@0.30.1` |
|
|
176
|
+
| GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.30.1` |
|
|
177
|
+
| 输出格式 | 短 summary、Terminal、JSON、SARIF 和 PR markdown |
|
|
172
178
|
| 项目配置 | `.ai-saas-guard.json` 支持规则开关、severity 覆盖、suppressions 和 fail threshold |
|
|
173
179
|
| 隐私模型 | 本地优先、只读扫描、不调用 LLM、不上传代码 |
|
|
174
|
-
| 当前版本 | `0.
|
|
175
|
-
| Action 标签 | `v0.
|
|
180
|
+
| 当前版本 | `0.30.1` 增加首次使用的 `--summary` 输出,并强化 Stripe、Supabase、silent-success、Next/Vercel finding 的具体修复方向 |
|
|
181
|
+
| Action 标签 | `v0.30.1`、`v0` |
|
|
176
182
|
| npm 发布 | GitHub Actions Trusted Publisher/OIDC,无需长期 npm token |
|
|
177
183
|
| 仓库可信度加固 | 严格 branch protection、Dependabot、CodeQL、fast-check fuzzing、signed release provenance assets、private vulnerability reporting、secret scanning 和 push protection |
|
|
178
184
|
| Cloudflare hosted ingress | 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`;签名 GitHub App webhook delivery 和 compact Check Run staging smoke 已通过 |
|
package/docs/demo-quickstart.md
CHANGED
|
@@ -2,10 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
Use these public fixtures when you want to understand `ai-saas-guard` before pointing it at a private repository.
|
|
4
4
|
|
|
5
|
+
## Fastest Path
|
|
6
|
+
|
|
7
|
+
Run the packaged demo without cloning this repository:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx ai-saas-guard@latest demo --summary
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This prints the risky and safe demo summaries, the top risky files to review, manual verification steps, and launch-focused next steps. It uses only public fixture code shipped in the npm package. Rerun `npx ai-saas-guard@latest demo` without `--summary` for the full human-readable report.
|
|
14
|
+
|
|
5
15
|
## Risky Demo
|
|
6
16
|
|
|
17
|
+
Clone the repository only if you want to inspect or edit the fixture files:
|
|
18
|
+
|
|
7
19
|
```bash
|
|
20
|
+
git clone https://github.com/zr9959/ai-saas-guard.git
|
|
21
|
+
cd ai-saas-guard
|
|
8
22
|
npx ai-saas-guard@latest scan --root examples/demo-risky-saas
|
|
23
|
+
npx ai-saas-guard@latest scan --root examples/demo-risky-saas --summary
|
|
9
24
|
```
|
|
10
25
|
|
|
11
26
|
The risky demo intentionally includes unsigned Stripe webhook handling, a silent-success billing fallback, broad Supabase RLS, and overpowered GitHub Actions permissions.
|
|
@@ -35,6 +50,7 @@ node dist/cli.js scan --root examples/demo-risky-saas
|
|
|
35
50
|
|
|
36
51
|
```bash
|
|
37
52
|
npx ai-saas-guard@latest scan --root examples/demo-safe-saas
|
|
53
|
+
npx ai-saas-guard@latest scan --root examples/demo-safe-saas --summary
|
|
38
54
|
```
|
|
39
55
|
|
|
40
56
|
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.
|
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.
|
|
8
|
+
- Current published version: `0.30.1`
|
|
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.
|
|
12
|
+
- GitHub Release: `v0.30.1`
|
|
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.
|
|
21
|
+
1. Create and review a release tag such as `v0.30.1`.
|
|
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.
|
|
@@ -3,7 +3,31 @@
|
|
|
3
3
|
This is a synthetic, public-safe example of the kind of review queue `ai-saas-guard` produces. Paths, snippets, and checks are intentionally small so a founder or reviewer can understand the output before running the tool.
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
|
-
|
|
6
|
+
ai-saas-guard scan summary
|
|
7
|
+
Findings: 6 findings: 0 critical, 3 high, 3 medium, 0 low, 0 info
|
|
8
|
+
Launch gate: review required: high-risk launch paths need manual verification before launch
|
|
9
|
+
|
|
10
|
+
Top risks:
|
|
11
|
+
- HIGH stripe.webhook.missing-signature at app/api/stripe/webhook/route.ts:12 - Stripe webhook does not verify the Stripe signature
|
|
12
|
+
- HIGH auth.clerk.unsafe-metadata at app/api/auth/profile/route.ts:8 - Clerk unsafe metadata is used as authorization input
|
|
13
|
+
- HIGH data.prisma.tenant-scope-missing at app/api/projects/[projectId]/route.ts:9 - Prisma query lacks an obvious tenant or owner predicate
|
|
14
|
+
|
|
15
|
+
Manual proof to run next:
|
|
16
|
+
- Replay a webhook with an invalid signature and confirm the route rejects it.
|
|
17
|
+
- Try changing the same metadata as a normal signed-in user and confirm it cannot grant admin, paid plan, tenant, workspace, or entitlement access.
|
|
18
|
+
- Create this resource as Tenant/User A, then attempt the same update with Tenant/User B.
|
|
19
|
+
|
|
20
|
+
Next steps
|
|
21
|
+
- Fix critical and high trust-boundary findings first: auth/session, billing/webhook, tenant data, and silent-success paths.
|
|
22
|
+
- Run the manual proof steps above in staging and confirm each risky path fails closed.
|
|
23
|
+
|
|
24
|
+
Full report:
|
|
25
|
+
Rerun without --summary, or use --json, --sarif, or --markdown where supported.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The full terminal report expands each finding:
|
|
29
|
+
|
|
30
|
+
```text
|
|
7
31
|
6 findings: 3 high, 3 medium
|
|
8
32
|
|
|
9
33
|
HIGH stripe.webhook.missing-signature
|
|
@@ -11,7 +35,7 @@ Rule: stripe.webhook.missing-signature
|
|
|
11
35
|
File: app/api/stripe/webhook/route.ts:12
|
|
12
36
|
Why: Billing access can be granted from a webhook path that does not verify Stripe signatures.
|
|
13
37
|
Verify: Replay a webhook with an invalid signature and confirm the route rejects it.
|
|
14
|
-
Fix direction:
|
|
38
|
+
Fix direction: In Next.js route handlers, read the payload with `await req.text()`, read the `stripe-signature` header, call `stripe.webhooks.constructEvent(body, signature, STRIPE_WEBHOOK_SECRET)`, and return 400 before any entitlement mutation when verification fails.
|
|
15
39
|
|
|
16
40
|
HIGH auth.clerk.unsafe-metadata
|
|
17
41
|
Rule: auth.clerk.unsafe-metadata
|
|
@@ -32,7 +56,7 @@ Rule: supabase.rls.tenant-predicate-missing
|
|
|
32
56
|
File: supabase/migrations/20260524_projects.sql:22
|
|
33
57
|
Why: Multi-tenant tables need tenant, workspace, organization, owner, or membership predicates.
|
|
34
58
|
Verify: Sign in as user A and user B; confirm neither can SELECT, INSERT, UPDATE, or DELETE the other's rows.
|
|
35
|
-
Fix direction:
|
|
59
|
+
Fix direction: Tie every policy to tenant/workspace/organization membership or owner columns, and mirror the same tenant scope in `WITH CHECK` for INSERT and UPDATE.
|
|
36
60
|
|
|
37
61
|
MEDIUM deploy.vercel.cron-missing-guard
|
|
38
62
|
Rule: deploy.vercel.cron-missing-guard
|
|
@@ -46,7 +70,12 @@ Rule: silent-success.swallowed-error
|
|
|
46
70
|
File: app/api/billing/checkout/route.ts:31
|
|
47
71
|
Why: Swallowed provider, auth, billing, or data errors can make a launch path look successful when it failed.
|
|
48
72
|
Verify: Force the upstream provider call to fail and confirm the route returns an error or disclosed degraded mode.
|
|
49
|
-
Fix direction: Log the failure, return
|
|
73
|
+
Fix direction: Log the failure with a request id, return a 4xx/5xx error or explicit degraded-mode response, and do not grant entitlement, change ownership, or mutate tenant data after the failed dependency.
|
|
74
|
+
|
|
75
|
+
Next steps
|
|
76
|
+
- Fix critical and high trust-boundary findings first: auth/session, billing/webhook, tenant data, and silent-success paths.
|
|
77
|
+
- Run the manual proof steps above in staging and confirm each risky path fails closed.
|
|
78
|
+
- Treat low and info deploy/CI hygiene hints as cleanup after critical, high, and medium launch paths are understood.
|
|
50
79
|
```
|
|
51
80
|
|
|
52
81
|
## How To Read It
|
package/package.json
CHANGED