ai-saas-guard 0.30.0 → 0.30.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 CHANGED
@@ -49,13 +49,13 @@ These are the failures that hurt after real users arrive:
49
49
  See the public demo output without cloning a repo:
50
50
 
51
51
  ```bash
52
- npx ai-saas-guard@latest demo
52
+ npx ai-saas-guard@latest demo --summary
53
53
  ```
54
54
 
55
55
  Run it against your app without installing anything globally:
56
56
 
57
57
  ```bash
58
- npx ai-saas-guard@latest scan --root /path/to/your-saas
58
+ npx ai-saas-guard@latest scan --root /path/to/your-saas --summary
59
59
  ```
60
60
 
61
61
  For an AI-heavy pull request:
@@ -64,39 +64,36 @@ For an AI-heavy pull request:
64
64
  npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --markdown
65
65
  ```
66
66
 
67
- 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.
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.
68
68
 
69
69
  ## Try The Demo Fixtures
70
70
 
71
71
  Want to see the report before scanning your own repo?
72
72
 
73
73
  ```bash
74
- npx ai-saas-guard@latest demo
74
+ npx ai-saas-guard@latest demo --summary
75
75
  ```
76
76
 
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. See [docs/demo-quickstart.md](docs/demo-quickstart.md) if you want to inspect the fixture files locally.
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.
78
78
 
79
79
  ## See The Output
80
80
 
81
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).
82
82
 
83
83
  ```text
84
- Launch Gate: review before launch
85
- 19 findings: 2 critical, 6 high, 7 medium, 3 low, 1 info
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
86
87
 
87
- CRITICAL stripe.webhook.missing-signature
88
- File: app/api/stripe/webhook/route.ts
89
- Why: billing access can be granted from a webhook path that does not verify Stripe signatures.
90
- Verify: replay a webhook with an invalid signature and confirm the route rejects it.
91
- Fix: read the raw body, call stripe.webhooks.constructEvent, and make event handling idempotent.
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
92
 
93
- HIGH silent-success.swallowed-error
94
- File: app/api/billing/checkout/route.ts
95
- Verify: force the upstream billing call to fail and confirm the route returns an error, not fake success.
96
-
97
- MEDIUM deploy.next.missing-security-headers
98
- File: app/api/billing/checkout/route.ts
99
- Verify: inspect production response headers for auth, billing, and API pages.
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.
100
97
 
101
98
  Next steps
102
99
  - Fix critical and high trust-boundary findings first.
@@ -113,7 +110,7 @@ One command returns a launch-readiness report with:
113
110
  - why the finding matters for an AI-built SaaS launch
114
111
  - manual verification steps you can actually run
115
112
  - practical fix direction, not generic advice
116
- - 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
117
114
 
118
115
  ## Problems It Helps You Catch
119
116
 
@@ -127,13 +124,15 @@ One command returns a launch-readiness report with:
127
124
  | Are tools and CI overpowered? | MCP side-effect classes, local policy/receipt templates, GitHub Actions permissions, concurrency, checkout depth, action pinning |
128
125
  | 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 |
129
126
 
127
+ For a concise comparison with Semgrep, zizmor, OpenSSF Scorecard, Snyk, and GitHub code scanning, see [docs/launch-gate-positioning.md](docs/launch-gate-positioning.md).
128
+
130
129
  ## Quick Start
131
130
 
132
131
  Run the published CLI without installing it globally:
133
132
 
134
133
  ```bash
135
- npx ai-saas-guard@latest demo
136
- npx ai-saas-guard@latest scan --root /path/to/your-saas
134
+ npx ai-saas-guard@latest demo --summary
135
+ npx ai-saas-guard@latest scan --root /path/to/your-saas --summary
137
136
  ```
138
137
 
139
138
  Run focused checks:
@@ -148,9 +147,10 @@ npx ai-saas-guard@latest check-mcp --root /path/to/your-saas --policy-template
148
147
  npx ai-saas-guard@latest check-actions --root /path/to/your-saas
149
148
  ```
150
149
 
151
- Machine-readable output:
150
+ Short or machine-readable output:
152
151
 
153
152
  ```bash
153
+ npx ai-saas-guard@latest scan --root /path/to/your-saas --summary
154
154
  npx ai-saas-guard@latest scan --root /path/to/your-saas --json
155
155
  npx ai-saas-guard@latest scan --root /path/to/your-saas --sarif > ai-saas-guard.sarif
156
156
  npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --markdown > ai-saas-guard-pr.md
@@ -187,13 +187,13 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
187
187
  | Area | Status |
188
188
  | --- | --- |
189
189
  | Public GitHub repository | Available |
190
- | npm CLI | `ai-saas-guard@0.30.0` |
191
- | GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.30.0` |
192
- | Outputs | Terminal, JSON, SARIF, and PR-focused markdown |
190
+ | npm CLI | `ai-saas-guard@0.30.2` |
191
+ | GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.30.2` |
192
+ | Outputs | Short summary, terminal, JSON, SARIF, and PR-focused markdown |
193
193
  | Project config | `.ai-saas-guard.json` rule toggles, severity overrides, suppressions, and fail thresholds |
194
194
  | Privacy model | Local-first, read-only scan commands, no LLM calls, no code upload |
195
- | Versioned Action tags | `v0.30.0`, `v0` |
196
- | Current release | `0.30.0` adds `ai-saas-guard demo`, Next steps in human-readable reports, a more targeted quickstart feedback template, and refreshed first-run docs |
195
+ | Versioned Action tags | `v0.30.2`, `v0` |
196
+ | Current release | `0.30.2` adds post-release quality tuning: lower-noise Vercel/Actions checks, focused launch-gate positioning docs, and hosted worker evidence boundaries |
197
197
  | npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
198
198
  | Repository trust hardening | Strict branch protection, Dependabot, CodeQL, fast-check fuzzing, signed release provenance assets, private vulnerability reporting, secret scanning, and push protection |
199
199
  | 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 |
@@ -360,7 +360,7 @@ Use `suppressions` for narrower false-positive handling when one rule is noisy o
360
360
 
361
361
  ## GitHub Action
362
362
 
363
- The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.30.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
363
+ The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.30.2` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
364
364
 
365
365
  ```yaml
366
366
  name: ai-saas-guard
package/dist/cli.js CHANGED
@@ -5,6 +5,7 @@ import { checkActions, checkMcp, checkStripe, checkSupabase, classifyPrRisk, run
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);
@@ -74,10 +75,14 @@ function parseArgs(argv) {
74
75
  result.format = "markdown";
75
76
  continue;
76
77
  }
78
+ if (arg === "--summary") {
79
+ result.format = "summary";
80
+ continue;
81
+ }
77
82
  if (arg === "--format") {
78
83
  const value = argv[index + 1];
79
- if (value !== "terminal" && value !== "json" && value !== "sarif" && value !== "markdown") {
80
- throw new Error("--format requires terminal, json, sarif, or markdown");
84
+ if (value !== "terminal" && value !== "json" && value !== "sarif" && value !== "markdown" && value !== "summary") {
85
+ throw new Error("--format requires terminal, json, sarif, markdown, or summary");
81
86
  }
82
87
  result.format = value;
83
88
  index += 1;
@@ -143,6 +148,8 @@ function formatReport(report, format) {
143
148
  return formatSarifReport(report);
144
149
  if (format === "markdown")
145
150
  return formatMarkdownReport(report);
151
+ if (format === "summary")
152
+ return `${formatSummaryReport(report)}\n`;
146
153
  return `${formatTerminalReport(report)}\n`;
147
154
  }
148
155
  function shouldFail(report, failOn) {
@@ -169,13 +176,13 @@ function helpText() {
169
176
  Repo-local launch-readiness scanner for AI-built SaaS apps.
170
177
 
171
178
  Usage:
172
- ai-saas-guard scan [--root <repo>] [--config <file>] [--json|--sarif] [--fail-on <severity>]
173
- ai-saas-guard demo [--json|--markdown]
174
- ai-saas-guard check-supabase [--root <repo>] [--config <file>] [--doctor] [--json|--sarif] [--fail-on <severity>]
175
- ai-saas-guard check-stripe [--root <repo>] [--config <file>] [--json|--sarif] [--fail-on <severity>]
176
- ai-saas-guard check-mcp [--root <repo>] [--config <file>] [--policy-template] [--json|--sarif] [--fail-on <severity>]
177
- ai-saas-guard check-actions [--root <repo>] [--config <file>] [--json|--sarif] [--fail-on <severity>]
178
- ai-saas-guard pr-risk [--root <repo>] [--config <file>] [--base <branch>] [--json|--sarif|--markdown] [--fail-on <severity>]
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>]
179
186
 
180
187
  Defaults:
181
188
  - read-only
@@ -185,6 +192,7 @@ Defaults:
185
192
  - terminal output by default, JSON with --json
186
193
  - SARIF output for GitHub code scanning with --sarif
187
194
  - PR-focused markdown summary with --markdown
195
+ - first-run launch summary with --summary
188
196
  - project config auto-loaded from .ai-saas-guard.json when present
189
197
  `;
190
198
  }
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export { classifyPrRisk } from "./commands/prRisk.js";
8
8
  export { applyGuardConfig, defaultConfigFileName, loadGuardConfig } from "./config.js";
9
9
  export { createScanContext } from "./context.js";
10
10
  export { getRuleMetadata, RULE_CATALOG } from "./rules/catalog.js";
11
+ export { formatSummaryReport } from "./report/summary.js";
11
12
  export type { BaseReport, CommandName, Evidence, Finding, ActionsReport, McpOptions, McpPolicyTemplate, McpReport, McpServerInventory, McpSideEffect, PrRiskFile, PrRiskReport, ScanOptions, ShowcaseReport, StripeReport, SupabaseOptions, SupabaseDoctorReport, SupabaseReport } from "./types.js";
12
13
  export type { ScanContext, ScanInput } from "./context.js";
13
14
  export type { FindingSuppression, GuardConfig, RuleConfigValue } from "./config.js";
package/dist/index.js CHANGED
@@ -8,3 +8,4 @@ export { classifyPrRisk } from "./commands/prRisk.js";
8
8
  export { applyGuardConfig, defaultConfigFileName, loadGuardConfig } from "./config.js";
9
9
  export { createScanContext } from "./context.js";
10
10
  export { getRuleMetadata, RULE_CATALOG } from "./rules/catalog.js";
11
+ export { formatSummaryReport } from "./report/summary.js";
@@ -0,0 +1,2 @@
1
+ import type { BaseReport } from "../types.js";
2
+ export declare function formatSummaryReport(report: BaseReport): string;
@@ -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
+ }
@@ -101,7 +101,17 @@ function runsOnPullRequestOrPush(content) {
101
101
  return /^\s*(pull_request(?:_target)?|push):/im.test(content);
102
102
  }
103
103
  function hasCancelInProgressConcurrency(content) {
104
- return /^\s*concurrency:/im.test(content) && /^\s*cancel-in-progress:\s*true\b/im.test(content);
104
+ if (!/^\s*concurrency:/im.test(content))
105
+ return false;
106
+ const match = /^\s*cancel-in-progress:\s*(.+?)\s*$/im.exec(content);
107
+ if (!match)
108
+ return false;
109
+ const value = match[1]?.trim() ?? "";
110
+ if (/^true\b/i.test(value))
111
+ return true;
112
+ if (/^\$\{\{[\s\S]*\}\}$/.test(value) && !/\bfalse\b/i.test(value))
113
+ return true;
114
+ return false;
105
115
  }
106
116
  function hasPathFilters(content) {
107
117
  return /^\s*(paths|paths-ignore):\s*$/im.test(content) || /^\s*(paths|paths-ignore):\s*\[/im.test(content);
@@ -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 headers such as `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, and an appropriate CSP."
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) {
@@ -180,11 +180,18 @@ function isImportantServerEnv(name) {
180
180
  return /(STRIPE|SUPABASE|DATABASE|NEXTAUTH|AUTH|WEBHOOK|SECRET|TOKEN|OPENAI|ANTHROPIC|PAYMENT)/.test(name);
181
181
  }
182
182
  function hasSecurityHeaders(files) {
183
- const combined = files
183
+ const nextOrMiddleware = files
184
184
  .filter((file) => /(^|\/)(next\.config\.[cm]?[jt]s|middleware\.[cm]?[jt]s)$/i.test(file.path))
185
185
  .map((file) => file.content)
186
186
  .join("\n");
187
- return /\bheaders\s*\(/i.test(combined) && /(X-Frame-Options|Content-Security-Policy|X-Content-Type-Options|Referrer-Policy)/i.test(combined);
187
+ if (/\bheaders\s*\(/i.test(nextOrMiddleware) && hasBrowserSecurityHeaderNames(nextOrMiddleware))
188
+ return true;
189
+ return files.some((file) => {
190
+ return /(^|\/)vercel\.json$/i.test(file.path) && /"headers"\s*:/i.test(file.content) && hasBrowserSecurityHeaderNames(file.content);
191
+ });
192
+ }
193
+ function hasBrowserSecurityHeaderNames(content) {
194
+ return /(X-Frame-Options|Content-Security-Policy|X-Content-Type-Options|Referrer-Policy)/i.test(content);
188
195
  }
189
196
  function usesUnboundedNextImage(path, content, nextConfigContent) {
190
197
  if (/next\.config\.(ts|js|mjs|cjs)$/.test(path) && /remotePatterns[\s\S]{0,240}hostname\s*:\s*["'](?:\*\*|\*)["']/i.test(content))
@@ -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 an explicit error status or degraded response, and avoid granting access or mutating state after the failed dependency."
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 no entitlement or ownership side effect."
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 launch checklist item for real provider verification."
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;
@@ -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: "Read the raw request body, call `stripe.webhooks.constructEvent(body, signature, STRIPE_WEBHOOK_SECRET)`, and reject invalid signatures."
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 before calling `constructEvent`."
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` or a tenant membership join."
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 ownership or membership column in every sensitive table policy."
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: "Add a `WITH CHECK` predicate tied to `auth.uid()` and the same owner, tenant, or membership relationship used by the read policy."
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));
@@ -48,13 +48,13 @@ AI 能很快把一个 SaaS 做到“看起来能用”:能登录、能打开 c
48
48
  不用 clone 仓库,先看公开 demo 输出:
49
49
 
50
50
  ```bash
51
- npx ai-saas-guard@latest demo
51
+ npx ai-saas-guard@latest demo --summary
52
52
  ```
53
53
 
54
54
  无需全局安装,直接扫你的应用:
55
55
 
56
56
  ```bash
57
- npx ai-saas-guard@latest scan --root /path/to/your-saas
57
+ npx ai-saas-guard@latest scan --root /path/to/your-saas --summary
58
58
  ```
59
59
 
60
60
  如果是 AI 生成的大 PR:
@@ -63,39 +63,36 @@ npx ai-saas-guard@latest scan --root /path/to/your-saas
63
63
  npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --markdown
64
64
  ```
65
65
 
66
- 你会得到 rule ID、severity、文件证据、为什么重要、如何人工验证,以及具体修复方向。扫描是 deterministic、只读的,不调用 LLM。
66
+ `--summary` 会先给上线判断、前三个风险、人工验证和下一步。去掉 `--summary` 后会看到每个 finding 的 rule ID、severity、文件证据、为什么重要、如何人工验证,以及具体修复方向。扫描是 deterministic、只读的,不调用 LLM。
67
67
 
68
68
  ## 先试公开 demo
69
69
 
70
70
  如果你还不想先扫自己的私有仓库,可以先跑公开 fixture:
71
71
 
72
72
  ```bash
73
- npx ai-saas-guard@latest demo
73
+ npx ai-saas-guard@latest demo --summary
74
74
  ```
75
75
 
76
- demo 命令使用包内公开 fixture:`examples/demo-risky-saas` 当前会故意触发 19 个 finding,覆盖 Stripe、Supabase、silent-success、Next/Vercel deploy 提示和 GitHub Actions;`examples/demo-safe-saas` 在同类风险面上使用更安全的静态写法,当前返回 0 个 finding。想本地查看 fixture 文件时再看 [demo-quickstart.md](demo-quickstart.md)。
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)。
77
77
 
78
78
  ## 输出长什么样
79
79
 
80
80
  报告是给上线前或合并 AI 大 PR 前快速阅读的。更完整的可复制样例见 [docs/sample-launch-report.md](sample-launch-report.md)。
81
81
 
82
82
  ```text
83
- Launch Gate: review before launch
84
- 19 findings: 2 critical, 6 high, 7 medium, 3 low, 1 info
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
85
86
 
86
- CRITICAL stripe.webhook.missing-signature
87
- File: app/api/stripe/webhook/route.ts
88
- Why: billing access can be granted from a webhook path that does not verify Stripe signatures.
89
- Verify: replay a webhook with an invalid signature and confirm the route rejects it.
90
- Fix: read the raw body, call stripe.webhooks.constructEvent, and make event handling idempotent.
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
91
 
92
- HIGH silent-success.swallowed-error
93
- File: app/api/billing/checkout/route.ts
94
- Verify: force the upstream billing call to fail and confirm the route returns an error, not fake success.
95
-
96
- MEDIUM deploy.next.missing-security-headers
97
- File: app/api/billing/checkout/route.ts
98
- Verify: inspect production response headers for auth, billing, and API pages.
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.
99
96
 
100
97
  Next steps
101
98
  - 先修 critical/high 的信任边界 finding。
@@ -112,7 +109,7 @@ Next steps
112
109
  - 说明它为什么会影响 AI 构建的 SaaS 上线
113
110
  - 给出可以人工复现的验证步骤
114
111
  - 给出实际修复方向,不只是一句泛泛建议
115
- - 支持 terminal、JSON、SARIF 和 PR markdown,方便本地或 CI 使用
112
+ - 支持短 `--summary`、terminal、JSON、SARIF 和 PR markdown,方便本地或 CI 使用
116
113
 
117
114
  ## 它能帮你抓住哪些问题
118
115
 
@@ -126,13 +123,15 @@ Next steps
126
123
  | 工具和 CI 权限是不是过大? | MCP side-effect 分类、本地 policy/receipt 模板、GitHub Actions 权限、concurrency、checkout depth、Action pinning |
127
124
  | reviewer 能不能看懂 AI PR? | `pr-risk` 对 auth、billing、RLS、deploy、API、storage、测试、silent-success、缺 spec context 和大型 diff 排序 |
128
125
 
126
+ 如果想看它和 Semgrep、zizmor、OpenSSF Scorecard、Snyk、GitHub code scanning 的边界区别,见 [launch-gate-positioning.md](launch-gate-positioning.md)。
127
+
129
128
  ## 快速开始
130
129
 
131
130
  无需全局安装,直接运行:
132
131
 
133
132
  ```bash
134
- npx ai-saas-guard@latest demo
135
- npx ai-saas-guard@latest scan --root /path/to/your-saas
133
+ npx ai-saas-guard@latest demo --summary
134
+ npx ai-saas-guard@latest scan --root /path/to/your-saas --summary
136
135
  ```
137
136
 
138
137
  运行专项检查:
@@ -147,9 +146,10 @@ npx ai-saas-guard@latest check-mcp --root /path/to/your-saas --policy-template
147
146
  npx ai-saas-guard@latest check-actions --root /path/to/your-saas
148
147
  ```
149
148
 
150
- 机器可读输出:
149
+ 短输出和机器可读输出:
151
150
 
152
151
  ```bash
152
+ npx ai-saas-guard@latest scan --root /path/to/your-saas --summary
153
153
  npx ai-saas-guard@latest scan --root /path/to/your-saas --json
154
154
  npx ai-saas-guard@latest scan --root /path/to/your-saas --sarif > ai-saas-guard.sarif
155
155
  npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --markdown > ai-saas-guard-pr.md
@@ -169,18 +169,18 @@ node dist/cli.js scan --root /path/to/your-saas
169
169
 
170
170
  这个仓库是公开 GitHub 仓库。
171
171
 
172
- CLI 已发布到 npm:`ai-saas-guard@0.30.0`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.30.0`。
172
+ CLI 已发布到 npm:`ai-saas-guard@0.30.2`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.30.2`。
173
173
 
174
174
  | 模块 | 状态 |
175
175
  | --- | --- |
176
176
  | 公开 GitHub 仓库 | 已可用 |
177
- | npm CLI | `ai-saas-guard@0.30.0` |
178
- | GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.30.0` |
179
- | 输出格式 | Terminal、JSON、SARIF 和 PR markdown |
177
+ | npm CLI | `ai-saas-guard@0.30.2` |
178
+ | GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.30.2` |
179
+ | 输出格式 | 短 summary、Terminal、JSON、SARIF 和 PR markdown |
180
180
  | 项目配置 | `.ai-saas-guard.json` 支持规则开关、severity 覆盖、suppressions 和 fail threshold |
181
181
  | 隐私模型 | 本地优先、只读扫描、不调用 LLM、不上传代码 |
182
- | 当前版本 | `0.30.0` 增加 `ai-saas-guard demo`、human-readable 报告里的 Next steps、更有针对性的 quickstart 反馈模板,并刷新首次试用文档 |
183
- | Action 标签 | `v0.30.0`、`v0` |
182
+ | 当前版本 | `0.30.2` 做发布后质量优化:降低 Vercel/Actions 误报、增加 launch-gate 定位文档,并补 hosted worker 证据边界 |
183
+ | Action 标签 | `v0.30.2`、`v0` |
184
184
  | npm 发布 | GitHub Actions Trusted Publisher/OIDC,无需长期 npm token |
185
185
  | 仓库可信度加固 | 严格 branch protection、Dependabot、CodeQL、fast-check fuzzing、signed release provenance assets、private vulnerability reporting、secret scanning 和 push protection |
186
186
  | Cloudflare hosted ingress | 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`;签名 GitHub App webhook delivery 和 compact Check Run staging smoke 已通过 |
@@ -7,10 +7,10 @@ Use these public fixtures when you want to understand `ai-saas-guard` before poi
7
7
  Run the packaged demo without cloning this repository:
8
8
 
9
9
  ```bash
10
- npx ai-saas-guard@latest demo
10
+ npx ai-saas-guard@latest demo --summary
11
11
  ```
12
12
 
13
- This prints the risky and safe demo summaries, the first risky files to review, manual verification steps, and launch-focused next steps. It uses only public fixture code shipped in the npm package.
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
14
 
15
15
  ## Risky Demo
16
16
 
@@ -20,6 +20,7 @@ Clone the repository only if you want to inspect or edit the fixture files:
20
20
  git clone https://github.com/zr9959/ai-saas-guard.git
21
21
  cd ai-saas-guard
22
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
23
24
  ```
24
25
 
25
26
  The risky demo intentionally includes unsigned Stripe webhook handling, a silent-success billing fallback, broad Supabase RLS, and overpowered GitHub Actions permissions.
@@ -49,6 +50,7 @@ node dist/cli.js scan --root examples/demo-risky-saas
49
50
 
50
51
  ```bash
51
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
52
54
  ```
53
55
 
54
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.
@@ -137,6 +137,27 @@ Required evidence:
137
137
 
138
138
  The worker must not persist repository files across jobs.
139
139
 
140
+ ### Read-Only Checkout Worker Evidence
141
+
142
+ For the first source checkout worker, the release blocks unless the deployed artifact records fresh evidence for both success and failure cleanup.
143
+
144
+ Required proof:
145
+
146
+ - checkout identity comes only from signed GitHub event fields and selected-repository installation scope.
147
+ - repository token permissions are limited to `contents: read` for checkout.
148
+ - runtime credentials reach git through temporary askpass material only.
149
+ - the CLI phase runs after credential material is removed from the environment.
150
+ - the worker command is fixed to the deterministic read-only `pr-risk --json` shape.
151
+ - success deletes the worker checkout, askpass material, generated JSON/SARIF scratch files, and local package tarballs.
152
+ - failure cleanup covers clone failure, timeout, CLI failure, malformed JSON output, Check Run write failure, cancellation, and process interruption.
153
+ - cleanup failures create an operator-review event without returning raw source, raw diffs, installation tokens, checkout paths, private URLs, or low-level filesystem errors to users.
154
+
155
+ ### Log Boundary Evidence
156
+
157
+ Before exposure, sample ingress, queue, worker, report, and Check Run logs for the release candidate. The sample may contain scan key, installation ID, repository ID, PR number, head SHA, scanner version, duration, summary counts, error class, and cleanup status.
158
+
159
+ The sample must show no raw source, no raw diffs, no secrets, no installation tokens, no customer payloads, no private URLs, no checkout paths, and no untrusted PR prose.
160
+
140
161
  ## Dependency And Container Scanning
141
162
 
142
163
  Hosted releases must include dependency and container scanning for the deployed artifact.
@@ -29,6 +29,22 @@ The hosted release gate still requires fresh deployed evidence for:
29
29
  - dependency and container artifact scanning for the deployed worker image
30
30
  - retention and uninstall cleanup against the deployed provider stores
31
31
 
32
+ ## Read-Only Checkout Worker Evidence Checklist
33
+
34
+ Before any hosted source checkout worker is exposed beyond staging, attach fresh evidence for each row below. The current Cloudflare ingress evidence above does not satisfy these rows because it publishes compact PR-risk signals without running a full source checkout scan worker.
35
+
36
+ | Evidence area | Required proof | Status before hosted exposure |
37
+ | --- | --- | --- |
38
+ | Trusted checkout identity | Worker input is derived from signed GitHub event identity, selected-repository installation scope, and repository `contents: read`; PR title, body, branch names, README, and code cannot choose the repository, token scope, checkout path, or command | Required |
39
+ | Runtime credential boundary | Installation credentials are passed to git only through temporary askpass material, are removed before the CLI scan phase, and are never returned in worker output, compact reports, Check Runs, or logs | Required |
40
+ | Fixed scanner command | Worker runs the fixed read-only command shape `ai-saas-guard pr-risk --root <worker-checkout> --base <trusted-base-sha> --json` without shell parsing or PR-authored arguments | Required |
41
+ | Success cleanup | A successful worker run deletes the checkout directory, askpass material, generated JSON/SARIF scratch files, and any local package tarballs | Required |
42
+ | Failure cleanup | A failed clone, timeout, CLI non-zero exit, malformed JSON output, Check Run write failure, cancellation, or process interruption still attempts checkout deletion and records only a safe cleanup status | Required |
43
+ | Log boundary | Logs may include scan key, installation ID, repository ID, PR number, head SHA, scanner version, duration, summary counts, error class, and cleanup status; logs must include no raw source, no raw diffs, no secrets, no installation tokens, no customer payloads, no private URLs, and no checkout paths | Required |
44
+ | Retention boundary | Compact report retention and uninstall cleanup delete repository-scoped records and worker checkout references without exposing low-level cleanup errors | Required |
45
+
46
+ Use the checklist above together with [hosted-operational-release-gate.md](hosted-operational-release-gate.md). The release remains blocked until deployed worker evidence covers success, failure cleanup, log boundary sampling, monitoring, rollback, and incident response.
47
+
32
48
  ## Smoke Procedure
33
49
 
34
50
  Use this sequence after each hosted Worker deployment:
@@ -0,0 +1,33 @@
1
+ # Launch Gate Positioning
2
+
3
+ `ai-saas-guard` is a local-first launch gate for AI-built SaaS apps. It is focused on the code paths that decide whether a product is ready to invite users: auth, billing, tenant data, provider failure handling, deploy config, MCP tool power, GitHub Actions hygiene, and AI-heavy PR review.
4
+
5
+ The narrow bet is simple: a founder or reviewer should know which launch-risk files to inspect first, what manual proof to run, and what fix direction to try before traffic reaches real users.
6
+
7
+ ## Where It Fits
8
+
9
+ | Tool category | Typical strength | How ai-saas-guard fits beside it |
10
+ | --- | --- | --- |
11
+ | Semgrep | Broad customizable static rules across many languages and frameworks | Adds SaaS launch-specific heuristics and review wording for Stripe, Supabase, silent-success, Next/Vercel, MCP, Actions, and AI PR trust boundaries |
12
+ | zizmor | Deep GitHub Actions security analysis | Adds a smaller launch-readiness hygiene check for workflow permissions, PR concurrency, docs-only CI cost, shallow checkout risk, and first-run guidance |
13
+ | OpenSSF Scorecard | Repository supply-chain posture signals | Adds app-level launch triage inside the repo, while Scorecard remains useful for public project maintenance controls |
14
+ | Snyk and dependency scanners | Dependency, container, license, and known-vulnerability workflows | Adds local source review for SaaS trust-boundary mistakes that may not be dependency vulnerabilities |
15
+ | GitHub code scanning | SARIF ingestion and code-security workflow inside GitHub | Emits SARIF, but keeps the primary workflow local-first and focused on launch review queues |
16
+
17
+ ## What It Optimizes For
18
+
19
+ - local-first: source stays on the machine or in the caller's CI job
20
+ - deterministic: no LLM calls and no code upload
21
+ - founder-readable output: severity, rule ID, file evidence, why, manual proof, and fix direction
22
+ - AI-built SaaS launch risk: not every static-analysis finding, only the paths likely to break auth, billing, data access, deploy, or review trust
23
+ - PR triage: ranking trust-boundary files before cosmetic or unrelated AI-generated changes
24
+
25
+ ## What It Does Not Try To Be
26
+
27
+ - not a general vulnerability management platform
28
+ - not a dependency advisory database
29
+ - not a cloud posture dashboard
30
+ - not a runtime WAF or MCP proxy
31
+ - not a guarantee that an app is safe to launch
32
+
33
+ Use it as a short, evidence-first launch review queue alongside dependency scanning, repository hardening, code scanning, manual two-account authorization tests, Stripe webhook replay, deploy-preview checks, and human review.
@@ -5,11 +5,11 @@
5
5
  ## Current State
6
6
 
7
7
  - Package name: `ai-saas-guard`
8
- - Current published version: `0.30.0`
8
+ - Current published version: `0.30.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.30.0`
12
+ - GitHub Release: `v0.30.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.30.0`.
21
+ 1. Create and review a release tag such as `v0.30.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.
@@ -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
- Launch Gate: review before launch
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: Read the raw body, call stripe.webhooks.constructEvent, and keep entitlement changes idempotent.
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: Add tenant or membership predicates and rerun a two-account staging check.
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,7 @@ 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 an explicit error status, and avoid granting access after the failed dependency.
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.
50
74
 
51
75
  Next steps
52
76
  - Fix critical and high trust-boundary findings first: auth/session, billing/webhook, tenant data, and silent-success paths.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-saas-guard",
3
- "version": "0.30.0",
3
+ "version": "0.30.2",
4
4
  "description": "Local-first CLI that catches launch blockers in AI-built Next.js/Supabase/Stripe SaaS apps.",
5
5
  "readmeFilename": "README.md",
6
6
  "type": "module",