ai-saas-guard 0.5.0 → 0.7.0

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
@@ -51,8 +51,8 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
51
51
  | JSON and SARIF output | Available |
52
52
  | Composite GitHub Action | Available |
53
53
  | Project config | `.ai-saas-guard.json` rule toggles, severity overrides, and fail thresholds |
54
- | Versioned Action tags | `v0.5.0`, `v0` |
55
- | npm package | `ai-saas-guard@0.5.0` |
54
+ | Versioned Action tags | `v0.7.0`, `v0` |
55
+ | npm package | `ai-saas-guard@0.7.0` |
56
56
  | npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
57
57
 
58
58
  ## Quick Start
@@ -170,6 +170,10 @@ If `--base` cannot be resolved, `pr-risk` emits `pr-risk.diff-unavailable` inste
170
170
  | `check-stripe` | Inspect webhook handlers and billing lifecycle coverage |
171
171
  | `check-mcp` | Inventory MCP configs and classify side effects |
172
172
 
173
+ ## Launch Readiness Checklist
174
+
175
+ Use [docs/launch-readiness-checklist.md](docs/launch-readiness-checklist.md) when an app is close to inviting real users. It explains how to combine `ai-saas-guard` output with manual two-account authorization testing, Stripe webhook verification, MCP config review, Supabase policy review, deploy checks, rollback planning, and a clear reminder that this is not a full security audit.
176
+
173
177
  ## Stripe Webhook Replay
174
178
 
175
179
  Use [docs/stripe-webhook-replay.md](docs/stripe-webhook-replay.md) after `check-stripe` flags missing signature verification, idempotency, lifecycle handlers, or entitlement updates. The cookbook maps findings to concrete `stripe listen` and `stripe trigger` commands for checkout success, failed renewal, subscription update, cancellation, refund, duplicate delivery, and out-of-order event review.
@@ -185,17 +189,26 @@ Add `.ai-saas-guard.json` at the repository root to tune findings without changi
185
189
  "stripe.webhook.missing-signature": "off",
186
190
  "stripe.webhook.missing-idempotency": "critical",
187
191
  "deploy.env.example-missing": "info"
188
- }
192
+ },
193
+ "suppressions": [
194
+ {
195
+ "ruleId": "stripe.webhook.missing-idempotency",
196
+ "paths": ["app/api/stripe/webhook/route.ts"],
197
+ "reason": "Temporary launch exception with duplicate-event coverage in integration tests."
198
+ }
199
+ ]
189
200
  }
190
201
  ```
191
202
 
192
203
  `rules` is keyed by published rule ID from [docs/rules.md](docs/rules.md). Set a rule to `off` to remove matching findings from terminal, JSON, SARIF, and markdown output. Set a rule to `critical`, `high`, `medium`, `low`, or `info` to override severity before summaries and `--fail-on` are evaluated.
193
204
 
205
+ Use `suppressions` for narrower false-positive handling when one rule is noisy only for specific generated files, fixtures, or reviewed exceptions. Each suppression must name a known `ruleId` and one or more relative `paths` globs, such as `generated/**` or `app/api/stripe/webhook/route.ts`.
206
+
194
207
  `failOn` sets the default CI failure threshold for the project. A CLI `--fail-on` value takes precedence, so local runs can still use `--fail-on none` or a stricter threshold.
195
208
 
196
209
  ## GitHub Action
197
210
 
198
- The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.5.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
211
+ The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.7.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
199
212
 
200
213
  ```yaml
201
214
  name: ai-saas-guard
@@ -324,8 +337,6 @@ Open-source core:
324
337
 
325
338
  Near-term priorities:
326
339
 
327
- - launch-readiness checklist content
328
- - false-positive suppression and rule stability labels
329
340
  - GitHub App design note for the potential hosted layer
330
341
 
331
342
  Potential paid layer later:
package/dist/config.d.ts CHANGED
@@ -1,10 +1,16 @@
1
1
  import type { BaseReport, Severity } from "./types.js";
2
2
  export declare const defaultConfigFileName = ".ai-saas-guard.json";
3
3
  export type RuleConfigValue = "off" | Severity;
4
+ export interface FindingSuppression {
5
+ ruleId: string;
6
+ paths: string[];
7
+ reason?: string;
8
+ }
4
9
  export interface GuardConfig {
5
10
  sourcePath?: string;
6
11
  failOn?: Severity | "none";
7
12
  rules: Record<string, RuleConfigValue>;
13
+ suppressions?: FindingSuppression[];
8
14
  }
9
15
  export declare function loadGuardConfig(rootDir: string, explicitPath?: string): Promise<GuardConfig>;
10
16
  export declare function applyGuardConfig<T extends BaseReport>(report: T, config: GuardConfig): T;
package/dist/config.js CHANGED
@@ -11,22 +11,22 @@ export async function loadGuardConfig(rootDir, explicitPath) {
11
11
  }
12
12
  catch (error) {
13
13
  if (!explicitPath && isNotFoundError(error))
14
- return { rules: {} };
14
+ return { rules: {}, suppressions: [] };
15
15
  throw new Error(`Could not read config file ${sourcePath}: ${errorMessage(error)}`);
16
16
  }
17
17
  return parseGuardConfig(content, sourcePath);
18
18
  }
19
19
  export function applyGuardConfig(report, config) {
20
20
  const configuredRuleIds = Object.keys(config.rules);
21
- if (configuredRuleIds.length === 0)
21
+ const suppressions = config.suppressions ?? [];
22
+ if (configuredRuleIds.length === 0 && suppressions.length === 0)
22
23
  return report;
23
24
  const findings = sortFindings(report.findings.flatMap((finding) => {
24
25
  const ruleConfig = config.rules[finding.ruleId];
25
- if (!ruleConfig)
26
- return [finding];
27
26
  if (ruleConfig === "off")
28
27
  return [];
29
- return [{ ...finding, severity: ruleConfig }];
28
+ const configuredFinding = ruleConfig ? { ...finding, severity: ruleConfig } : finding;
29
+ return isSuppressed(configuredFinding, suppressions) ? [] : [configuredFinding];
30
30
  }));
31
31
  return {
32
32
  ...report,
@@ -63,12 +63,84 @@ function parseGuardConfig(content, sourcePath) {
63
63
  }
64
64
  rules[ruleId] = value;
65
65
  }
66
+ const rawSuppressions = parsed.suppressions ?? [];
67
+ if (!Array.isArray(rawSuppressions)) {
68
+ throw new Error("Invalid config suppressions: expected an array");
69
+ }
70
+ const suppressions = rawSuppressions.map((value, index) => parseSuppression(value, index));
66
71
  return {
67
72
  sourcePath,
68
73
  failOn,
69
- rules
74
+ rules,
75
+ suppressions
76
+ };
77
+ }
78
+ function parseSuppression(value, index) {
79
+ if (!isPlainObject(value)) {
80
+ throw new Error(`Invalid config suppressions[${index}]: expected an object`);
81
+ }
82
+ const ruleId = value.ruleId;
83
+ if (typeof ruleId !== "string" || ruleId.length === 0) {
84
+ throw new Error(`Invalid config suppressions[${index}].ruleId: expected a rule ID`);
85
+ }
86
+ if (!getRuleMetadata(ruleId)) {
87
+ throw new Error(`Unknown rule ID in suppression: ${ruleId}`);
88
+ }
89
+ const paths = value.paths;
90
+ if (!Array.isArray(paths) || paths.length === 0 || !paths.every((path) => typeof path === "string" && path.length > 0)) {
91
+ throw new Error(`Invalid config suppressions[${index}].paths: expected a non-empty array of path globs`);
92
+ }
93
+ const reason = value.reason;
94
+ if (reason !== undefined && typeof reason !== "string") {
95
+ throw new Error(`Invalid config suppressions[${index}].reason: expected a string`);
96
+ }
97
+ return {
98
+ ruleId,
99
+ paths,
100
+ reason
70
101
  };
71
102
  }
103
+ function isSuppressed(finding, suppressions) {
104
+ return suppressions.some((suppression) => suppression.ruleId === finding.ruleId &&
105
+ finding.evidence.some((evidence) => suppression.paths.some((pattern) => pathMatches(pattern, evidence.file))));
106
+ }
107
+ function pathMatches(pattern, filePath) {
108
+ return globToRegExp(normalizeConfigPath(pattern)).test(normalizeConfigPath(filePath));
109
+ }
110
+ function normalizeConfigPath(path) {
111
+ return path.replace(/\\/g, "/").replace(/^\.\//, "");
112
+ }
113
+ function globToRegExp(pattern) {
114
+ let expression = "^";
115
+ for (let index = 0; index < pattern.length; index += 1) {
116
+ const char = pattern[index];
117
+ if (char === "*") {
118
+ const next = pattern[index + 1];
119
+ const afterNext = pattern[index + 2];
120
+ if (next === "*" && afterNext === "/") {
121
+ expression += "(?:.*/)?";
122
+ index += 2;
123
+ }
124
+ else if (next === "*") {
125
+ expression += ".*";
126
+ index += 1;
127
+ }
128
+ else {
129
+ expression += "[^/]*";
130
+ }
131
+ }
132
+ else if (char === "?") {
133
+ expression += "[^/]";
134
+ }
135
+ else {
136
+ expression += escapeRegExp(char);
137
+ }
138
+ }
139
+ return new RegExp(`${expression}$`);
140
+ }
141
+ function escapeRegExp(value) {
142
+ return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
143
+ }
72
144
  function isRuleConfigValue(value) {
73
145
  return value === "off" || isSeverity(value);
74
146
  }
package/dist/index.d.ts CHANGED
@@ -8,5 +8,5 @@ export { createScanContext } from "./context.js";
8
8
  export { getRuleMetadata, RULE_CATALOG } from "./rules/catalog.js";
9
9
  export type { BaseReport, CommandName, Evidence, Finding, McpReport, McpServerInventory, PrRiskFile, PrRiskReport, ScanOptions, StripeReport, SupabaseReport } from "./types.js";
10
10
  export type { ScanContext, ScanInput } from "./context.js";
11
- export type { GuardConfig, RuleConfigValue } from "./config.js";
11
+ export type { FindingSuppression, GuardConfig, RuleConfigValue } from "./config.js";
12
12
  export type { RuleMetadata, RuleStability } from "./rules/catalog.js";
@@ -5,13 +5,18 @@ export function formatSarifReport(report) {
5
5
  if (rules.has(finding.ruleId))
6
6
  continue;
7
7
  const metadata = getRuleMetadata(finding.ruleId);
8
+ const stability = metadata?.stability ?? "default";
8
9
  rules.set(finding.ruleId, {
9
10
  id: finding.ruleId,
10
11
  name: finding.ruleId,
11
12
  shortDescription: { text: metadata?.title ?? finding.title },
12
13
  fullDescription: { text: metadata?.why ?? finding.why },
13
14
  help: { text: `${finding.suggestedVerification}\n\nFix direction: ${finding.suggestedFix}` },
14
- defaultConfiguration: { level: sarifLevel(finding) }
15
+ defaultConfiguration: { level: sarifLevel(finding) },
16
+ properties: {
17
+ "ai-saas-guard/stability": stability,
18
+ tags: [`stability:${stability}`]
19
+ }
15
20
  });
16
21
  }
17
22
  return `${JSON.stringify({
@@ -4,14 +4,14 @@ export const RULE_CATALOG = {
4
4
  severity: "high",
5
5
  title: "Secret-like value detected",
6
6
  why: "Credentials committed to source, config, or examples can be exposed before launch.",
7
- stability: "default"
7
+ stability: "strict"
8
8
  },
9
9
  "next.env.public-secret": {
10
10
  ruleId: "next.env.public-secret",
11
11
  severity: "high",
12
12
  title: "Risky NEXT_PUBLIC environment variable",
13
13
  why: "Next.js exposes NEXT_PUBLIC variables to browser code, so secret-like values can leak to users.",
14
- stability: "default"
14
+ stability: "strict"
15
15
  },
16
16
  "stripe.webhook.missing-route": {
17
17
  ruleId: "stripe.webhook.missing-route",
@@ -25,7 +25,7 @@ export const RULE_CATALOG = {
25
25
  severity: "critical",
26
26
  title: "Stripe webhook does not verify the Stripe signature",
27
27
  why: "Unsigned webhook handlers can accept forged billing events.",
28
- stability: "default"
28
+ stability: "strict"
29
29
  },
30
30
  "stripe.webhook.raw-body-risk": {
31
31
  ruleId: "stripe.webhook.raw-body-risk",
@@ -39,7 +39,7 @@ export const RULE_CATALOG = {
39
39
  severity: "critical",
40
40
  title: "Stripe signing secret appears public",
41
41
  why: "Public Stripe secrets can be bundled into client code and abused.",
42
- stability: "default"
42
+ stability: "strict"
43
43
  },
44
44
  "stripe.webhook.missing-idempotency": {
45
45
  ruleId: "stripe.webhook.missing-idempotency",
@@ -53,7 +53,7 @@ export const RULE_CATALOG = {
53
53
  severity: "medium",
54
54
  title: "Stripe webhook does not show an entitlement update path",
55
55
  why: "Returning HTTP 200 is not the same as changing application access state.",
56
- stability: "default"
56
+ stability: "experimental"
57
57
  },
58
58
  "stripe.webhook.missing-critical-event": {
59
59
  ruleId: "stripe.webhook.missing-critical-event",
@@ -67,7 +67,7 @@ export const RULE_CATALOG = {
67
67
  severity: "critical",
68
68
  title: "Broad Supabase RLS policy",
69
69
  why: "`USING (true)` or `WITH CHECK (true)` can turn login into broad data access or writes.",
70
- stability: "default"
70
+ stability: "strict"
71
71
  },
72
72
  "supabase.rls.missing-ownership-filter": {
73
73
  ruleId: "supabase.rls.missing-ownership-filter",
@@ -95,7 +95,7 @@ export const RULE_CATALOG = {
95
95
  severity: "critical",
96
96
  title: "Sensitive table does not enable row level security",
97
97
  why: "User-data tables should enable row level security.",
98
- stability: "default"
98
+ stability: "strict"
99
99
  },
100
100
  "supabase.storage.public-bucket": {
101
101
  ruleId: "supabase.storage.public-bucket",
@@ -109,14 +109,14 @@ export const RULE_CATALOG = {
109
109
  severity: "medium",
110
110
  title: "Sensitive API route lacks obvious rate limiting",
111
111
  why: "Login, checkout, upload, AI, and webhook routes are common abuse targets.",
112
- stability: "default"
112
+ stability: "experimental"
113
113
  },
114
114
  "api.route.auth-without-ownership": {
115
115
  ruleId: "api.route.auth-without-ownership",
116
116
  severity: "high",
117
117
  title: "API route checks auth but lacks an ownership guard",
118
118
  why: "Login checks do not prove resource ownership checks.",
119
- stability: "default"
119
+ stability: "experimental"
120
120
  },
121
121
  "deploy.next.static-export-api-risk": {
122
122
  ruleId: "deploy.next.static-export-api-risk",
@@ -137,14 +137,14 @@ export const RULE_CATALOG = {
137
137
  severity: "low",
138
138
  title: "Important runtime env var is not documented",
139
139
  why: "Missing env docs cause local-success, production-failure deploys.",
140
- stability: "default"
140
+ stability: "experimental"
141
141
  },
142
142
  "mcp.config.invalid-json": {
143
143
  ruleId: "mcp.config.invalid-json",
144
144
  severity: "medium",
145
145
  title: "MCP config is not valid JSON",
146
146
  why: "Broken MCP configs hide the actual tool inventory.",
147
- stability: "default"
147
+ stability: "strict"
148
148
  },
149
149
  "mcp.config.plaintext-secret": {
150
150
  ruleId: "mcp.config.plaintext-secret",
@@ -200,7 +200,7 @@ export const RULE_CATALOG = {
200
200
  severity: "medium",
201
201
  title: "Review first sensitive PR surface",
202
202
  why: "AI-generated PRs often bury trust-boundary changes inside larger diffs.",
203
- stability: "default"
203
+ stability: "experimental"
204
204
  },
205
205
  "pr-risk.diff-unavailable": {
206
206
  ruleId: "pr-risk.diff-unavailable",
@@ -2,7 +2,7 @@
2
2
 
3
3
  `ai-saas-guard` ships as a composite GitHub Action for pull request and code scanning workflows.
4
4
 
5
- Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.5.0` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
5
+ Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.7.0` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
6
6
 
7
7
  ## PR Summary
8
8
 
@@ -50,7 +50,7 @@ The Action auto-loads `.ai-saas-guard.json` from `root` when the file exists. Us
50
50
  fail-on: none
51
51
  ```
52
52
 
53
- Project config can disable noisy rules, override severity by rule ID, and set a default `failOn` threshold. A workflow `fail-on` input overrides the config threshold for that run.
53
+ Project config can disable noisy rules, override severity by rule ID, apply path-specific `suppressions`, and set a default `failOn` threshold. A workflow `fail-on` input overrides the config threshold for that run.
54
54
 
55
55
  ## SARIF Upload
56
56
 
@@ -0,0 +1,191 @@
1
+ # Launch Readiness Checklist
2
+
3
+ Use this checklist when an AI-built SaaS app is close to launch, a founder is about to invite real users, or a reviewer needs a practical pre-merge review path.
4
+
5
+ This is not a full security audit, penetration test, compliance review, or proof that the app is secure. It is a founder-readable launch preflight that combines `ai-saas-guard` findings with manual verification for the most common SaaS launch blockers.
6
+
7
+ ## Start With The Local Preflight
8
+
9
+ Run from the app repository:
10
+
11
+ ```bash
12
+ npx ai-saas-guard@latest scan --root .
13
+ npx ai-saas-guard@latest pr-risk --root . --base origin/main
14
+ npx ai-saas-guard@latest check-supabase --root .
15
+ npx ai-saas-guard@latest check-stripe --root .
16
+ npx ai-saas-guard@latest check-mcp --root .
17
+ ```
18
+
19
+ Treat every finding as a review queue item. The tool is read-only and local-first, so it can show where to inspect, but it cannot confirm production settings, account ownership, live Stripe dashboard state, or every possible authorization path.
20
+
21
+ ## Launch Blocker Rules
22
+
23
+ Use these labels while reviewing:
24
+
25
+ | Level | Meaning | Examples |
26
+ | --- | --- | --- |
27
+ | Launch blocker | Do not ship until fixed or explicitly disabled for the product. | Any user can read another tenant's data, unsigned Stripe webhooks grant access, real secrets are committed. |
28
+ | Must verify | Shipping may be reasonable only after a named manual test passes. | Rate limits, billing lifecycle handling, storage object scope, rollback path. |
29
+ | Follow-up | Track after launch when the current product risk is bounded. | Extra docs, sharper false-positive suppression, non-critical workflow polish. |
30
+
31
+ If a finding affects auth, billing, user data, secrets, file storage, production deploy config, or MCP tools with side effects, assume it is at least "must verify" until a human has tested the exact path.
32
+
33
+ ## Two-Account Authorization Testing
34
+
35
+ Two-account authorization testing is the fastest manual check for broken SaaS isolation.
36
+
37
+ Prepare two ordinary test accounts:
38
+
39
+ - User A in organization or workspace A.
40
+ - User B in organization or workspace B.
41
+ - At least one private object owned by each account or workspace: project, document, invoice, file, message, API key, or team member.
42
+
43
+ Verify read isolation:
44
+
45
+ - Log in as User A and open User A's private objects.
46
+ - Try to load User B's object by changing route parameters, object IDs, slugs, search filters, API URLs, and browser history entries.
47
+ - Repeat the same checks through direct API calls if the app exposes JSON routes.
48
+ - Confirm User A receives a 403 or 404 and no object metadata leaks in the response.
49
+
50
+ Verify write isolation:
51
+
52
+ - As User A, try to update, delete, invite users to, export, or share User B's objects by tampering with IDs.
53
+ - Confirm the server rejects the request, not only the UI.
54
+ - Confirm no partial write, audit entry, billing change, storage object, notification, or background job was created.
55
+
56
+ Verify tenant switching:
57
+
58
+ - If a user can belong to multiple workspaces, switch active workspaces and repeat the same read/write checks.
59
+ - Confirm server-side ownership checks use the selected tenant membership, not only a client-side workspace ID.
60
+
61
+ For Supabase apps, compare these manual checks with row-level security policy evidence from `check-supabase`. RLS should protect sensitive tables even when a route, query builder, or generated client code is wrong.
62
+
63
+ ## Stripe Webhook Verification
64
+
65
+ Stripe webhook verification is required for subscription or credit products because checkout redirects are not billing truth.
66
+
67
+ Minimum checks:
68
+
69
+ - The webhook route reads the raw request body.
70
+ - The handler verifies the `Stripe-Signature` header with the server-only webhook secret before any database write.
71
+ - The signing secret is never exposed through `NEXT_PUBLIC_*`, client bundles, public logs, issue comments, or screenshots.
72
+ - The handler stores or dedupes `event.id`.
73
+ - Entitlement state is updated from Stripe webhook reconciliation, not only from checkout success pages.
74
+
75
+ Replay the critical billing paths with the companion cookbook:
76
+
77
+ ```bash
78
+ stripe listen --forward-to localhost:3000/api/stripe/webhook
79
+ stripe trigger checkout.session.completed
80
+ stripe trigger invoice.payment_failed
81
+ stripe trigger customer.subscription.updated
82
+ stripe trigger customer.subscription.deleted
83
+ stripe trigger charge.refunded
84
+ ```
85
+
86
+ Expected evidence:
87
+
88
+ - Checkout success grants the right user or tenant access only after signature verification.
89
+ - Failed invoice creates the intended past-due or grace state.
90
+ - Subscription updates reconcile plan, quantity, status, cancellation, and period fields.
91
+ - Cancellation revokes or downgrades paid access.
92
+ - Refund handling is explicit, even if the product requires manual review.
93
+ - Duplicate delivery of the same event ID is a no-op.
94
+ - Out-of-order events reconcile to one durable entitlement state.
95
+
96
+ See [stripe-webhook-replay.md](stripe-webhook-replay.md) for the full command cookbook.
97
+
98
+ ## MCP Config Review
99
+
100
+ MCP config review matters because local tools can turn prompt injection into filesystem, shell, database, or network side effects.
101
+
102
+ Inventory every MCP server used by the app, agent, or development workflow:
103
+
104
+ - Config files such as `.mcp.json`, `.cursor/mcp.json`, `claude_desktop_config.json`, or tool-specific equivalents.
105
+ - Tool descriptions that expose shell commands, raw SQL, browser automation, filesystem writes, or deployment actions.
106
+ - Environment variables and credentials passed to MCP servers.
107
+ - Bind addresses and transport URLs.
108
+
109
+ Review questions:
110
+
111
+ - Does any MCP server bind to `0.0.0.0` or a non-localhost host?
112
+ - Does any tool allow broad filesystem read/write access outside the intended repository?
113
+ - Does any tool run arbitrary shell commands or raw SQL against production-like data?
114
+ - Are credentials stored in plaintext config files?
115
+ - Are secret-bearing config files group-readable or world-readable?
116
+ - Can the tool mutate billing, user access, customer data, or deploy state without a human approval step?
117
+
118
+ Launch blocker examples:
119
+
120
+ - A production database URL is stored in an MCP config.
121
+ - A broad filesystem tool can write outside the app repository.
122
+ - A shell or SQL tool is exposed to untrusted prompts without environment separation.
123
+
124
+ Use `check-mcp` findings as the starting inventory, then manually inspect any tool that can read secrets or change state.
125
+
126
+ ## Supabase And Storage
127
+
128
+ For Supabase apps, launch only after the data model has an ownership story.
129
+
130
+ Check:
131
+
132
+ - Sensitive tables have RLS enabled.
133
+ - Policies use `auth.uid()` or tenant membership checks tied to the row.
134
+ - Write policies use `WITH CHECK` so users cannot insert or move rows into another tenant.
135
+ - Storage buckets and `storage.objects` policies are scoped by owner, tenant, or object path.
136
+ - Service-role keys stay server-only and are not used in browser code.
137
+
138
+ Manual verification should still use the two-account flow above. Scanner findings can point to weak policies, but the actual product workflow determines whether access is correctly isolated.
139
+
140
+ ## Secrets, Env, And Deploy
141
+
142
+ Before launch:
143
+
144
+ - Rotate any real secret that was committed, pasted into a public issue, or printed in a public log.
145
+ - Confirm examples use inert placeholders, not real-looking provider tokens.
146
+ - Confirm `NEXT_PUBLIC_*` variables contain only values that are safe for browsers.
147
+ - Check `.env.example` or deploy docs include required production variables without revealing secret values.
148
+ - Confirm Vercel, Netlify, or other deploy settings match runtime expectations: API routes are not accidentally static, and Node-only libraries are not deployed to incompatible Edge runtimes.
149
+
150
+ ## CI And PR Review
151
+
152
+ Minimum repository workflow:
153
+
154
+ - CI runs tests on pull requests and pushes to the default branch.
155
+ - `ai-saas-guard pr-risk` runs with enough git history to compare against the base branch.
156
+ - SARIF or markdown output is used when reviewers need scan results in GitHub.
157
+ - Large AI-generated pull requests are split when they combine UI, auth, database, billing, and deploy changes.
158
+
159
+ Review the first files named by `pr-risk` before reviewing cosmetic changes. If a base ref is missing, fix checkout depth or fetch history before trusting the PR risk result.
160
+
161
+ ## Rollback And Incident Readiness
162
+
163
+ Before inviting real users, define:
164
+
165
+ - How to disable paid access changes without deleting customer data.
166
+ - How to revoke or rotate leaked keys.
167
+ - How to roll back a failed deployment.
168
+ - How to pause new signups or billing flows.
169
+ - Where webhook delivery failures, auth errors, and database policy denials are logged.
170
+ - Who decides whether a finding is a launch blocker or an accepted risk.
171
+
172
+ The goal is not a perfect process. The goal is that the founder can answer what happens when auth, billing, deploy, or data isolation fails on launch day.
173
+
174
+ ## Founder Sign-Off
175
+
176
+ A launch-ready review should leave behind short evidence, not just a feeling.
177
+
178
+ Use this minimal sign-off:
179
+
180
+ | Area | Evidence to keep |
181
+ | --- | --- |
182
+ | CLI preflight | Command output or CI link for `scan` and focused checks |
183
+ | Two-account authorization | User A/User B notes with tested object types and rejected actions |
184
+ | Stripe | Webhook replay notes for success, failure, update, cancellation, refund, duplicate, and out-of-order paths |
185
+ | Supabase | RLS and storage policy review notes |
186
+ | MCP | Reviewed MCP config files and disabled or constrained risky tools |
187
+ | Secrets | Rotation notes or confirmation that only placeholders were found |
188
+ | Deploy | Production env and runtime checks |
189
+ | Rollback | Known rollback command or deploy platform rollback path |
190
+
191
+ If any row is blank for a product surface the app actually uses, treat it as a must-verify item before launch.
@@ -5,10 +5,10 @@
5
5
  ## Current State
6
6
 
7
7
  - Package name: `ai-saas-guard`
8
- - Current version: `0.5.0`
8
+ - Current version: `0.7.0`
9
9
  - npm registry state: published at <https://www.npmjs.com/package/ai-saas-guard>
10
10
  - First npm-published version: `0.1.1`
11
- - GitHub Release: `v0.5.0`
11
+ - GitHub Release: `v0.7.0`
12
12
  - Publish workflow: `.github/workflows/npm-publish.yml`
13
13
  - Trusted Publisher: GitHub Actions, `zr9959/ai-saas-guard`, workflow `npm-publish.yml`, allowed action `npm publish`
14
14
  - Long-lived npm publish token: not required
@@ -17,7 +17,7 @@
17
17
 
18
18
  Use GitHub Actions with npm Trusted Publisher/OIDC:
19
19
 
20
- 1. Create and review a release tag such as `v0.5.0`.
20
+ 1. Create and review a release tag such as `v0.7.0`.
21
21
  2. Publish from the GitHub Release or run the `Publish npm` workflow manually with `ref` set to that tag.
22
22
  3. Keep `permissions.id-token: write` in the workflow so npm can exchange the GitHub Actions OIDC identity for a short-lived publish credential.
23
23
  4. Run `npm publish --access public` from the workflow. Trusted publishing automatically generates provenance for this public package from this public repository.
@@ -39,6 +39,7 @@ Do not market it as a full pentest, full SAST platform, or proof that an app is
39
39
  Implemented surfaces:
40
40
 
41
41
  - secret-like values and risky public env exposure
42
+ - founder-readable launch-readiness checklist for two-account authorization, Stripe webhook verification, MCP config review, Supabase, deploy, CI, and rollback checks
42
43
  - Stripe webhook signature, raw body, idempotency, and lifecycle handler heuristics
43
44
  - Stripe webhook replay cookbook for checkout, renewal failure, updates, cancellation, refunds, duplicate delivery, and out-of-order review
44
45
  - Supabase RLS, tenant membership, ownership filter, weak `WITH CHECK`, and storage object policy heuristics
@@ -48,7 +49,8 @@ Implemented surfaces:
48
49
  - PR diff risk triage for auth, billing, RLS, env, tests removed, and large mixed diffs
49
50
  - PR diff diagnostics when a base ref or shallow checkout prevents comparison
50
51
  - PR-focused markdown summary output for GitHub step summaries or PR comments
51
- - project-local `.ai-saas-guard.json` config for rule toggles, severity overrides, and default fail thresholds
52
+ - project-local `.ai-saas-guard.json` config for rule toggles, severity overrides, path-specific suppressions, and default fail thresholds
53
+ - rule stability labels in catalog metadata, public rule docs, and SARIF rule properties
52
54
  - JSON output
53
55
  - SARIF output
54
56
  - composite GitHub Action wrapper
@@ -100,7 +102,7 @@ GitHub Project:
100
102
 
101
103
  Current issue set:
102
104
 
103
- - #1 Add launch-readiness checklist content
105
+ - No open roadmap issues after the `v0.7.0` suppression and stability release.
104
106
 
105
107
  CI:
106
108
 
@@ -112,7 +114,7 @@ CI:
112
114
  Publishing:
113
115
 
114
116
  - npm package: `ai-saas-guard`
115
- - Current release line: `v0.5.0`
117
+ - Current release line: `v0.7.0`
116
118
  - Publish workflow: `.github/workflows/npm-publish.yml`
117
119
  - Trusted Publisher: GitHub Actions for `zr9959/ai-saas-guard`, workflow `npm-publish.yml`
118
120
  - Long-lived npm publish tokens should not be required.
@@ -139,9 +141,7 @@ Not allowed:
139
141
 
140
142
  Recommended order:
141
143
 
142
- 1. Add launch-readiness checklist content.
143
- 2. Improve false-positive suppression and rule stability labels.
144
- 3. Add a GitHub App design note for the potential hosted layer.
144
+ 1. Add a GitHub App design note for the potential hosted layer.
145
145
 
146
146
  For every feature, keep the scanner evidence-first:
147
147
 
package/docs/rules.md CHANGED
@@ -2,7 +2,37 @@
2
2
 
3
3
  `ai-saas-guard` rules are deterministic heuristics. They are designed to produce a focused verification queue, not a complete vulnerability list.
4
4
 
5
- Rule metadata is centralized in `src/rules/catalog.ts` and covered by tests so SARIF output, public docs, and future config work can share stable rule IDs. Current published rules use the `default` stability level.
5
+ Rule metadata is centralized in `src/rules/catalog.ts` and covered by tests so SARIF output, public docs, and config work can share stable rule IDs, default severities, and stability labels.
6
+
7
+ ## Stability Labels
8
+
9
+ Stability labels describe how much confidence reviewers should place in a finding before manual verification. They are not severity levels.
10
+
11
+ | Stability | Meaning | Examples |
12
+ | --- | --- | --- |
13
+ | Strict | High-confidence evidence that should rarely be suppressed without a written reason. | Committed secret-like values, public Stripe secrets, unsigned Stripe webhooks, broad Supabase RLS policies. |
14
+ | Default | Routine launch-readiness heuristic with useful evidence and an expected manual verification step. | Missing Stripe lifecycle events, weak Supabase ownership checks, MCP side-effect inventory. |
15
+ | Experimental | Higher-noise heuristic meant to prioritize review, not prove a defect. | API ownership hints, rate-limit hints, missing env docs, PR risk triage. |
16
+
17
+ SARIF output includes the rule stability in `properties["ai-saas-guard/stability"]` and a `stability:<level>` tag for code scanning consumers.
18
+
19
+ ## Suppressing False Positives
20
+
21
+ Prefer fixing risky code over suppressing findings. When a finding is a reviewed false positive for a specific generated file, fixture, or documented launch exception, use path-specific `suppressions` in `.ai-saas-guard.json` instead of disabling the whole rule:
22
+
23
+ ```json
24
+ {
25
+ "suppressions": [
26
+ {
27
+ "ruleId": "stripe.webhook.missing-idempotency",
28
+ "paths": ["app/api/stripe/webhook/route.ts"],
29
+ "reason": "Temporary exception; duplicate-event behavior is covered by integration tests."
30
+ }
31
+ ]
32
+ }
33
+ ```
34
+
35
+ `paths` are relative globs. Examples: `generated/**`, `tests/fixtures/**`, and `app/api/stripe/webhook/route.ts`.
6
36
 
7
37
  ## Secrets And Public Env
8
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-saas-guard",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Repo-local launch-readiness scanner for AI-built SaaS apps.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/zr9959/ai-saas-guard#readme",