ai-saas-guard 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +285 -0
  3. package/action.yml +100 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +156 -0
  6. package/dist/commands/checkMcp.d.ts +2 -0
  7. package/dist/commands/checkMcp.js +4 -0
  8. package/dist/commands/checkStripe.d.ts +2 -0
  9. package/dist/commands/checkStripe.js +4 -0
  10. package/dist/commands/checkSupabase.d.ts +2 -0
  11. package/dist/commands/checkSupabase.js +4 -0
  12. package/dist/commands/prRisk.d.ts +2 -0
  13. package/dist/commands/prRisk.js +4 -0
  14. package/dist/commands/scan.d.ts +2 -0
  15. package/dist/commands/scan.js +29 -0
  16. package/dist/context.d.ts +10 -0
  17. package/dist/context.js +16 -0
  18. package/dist/index.d.ts +10 -0
  19. package/dist/index.js +7 -0
  20. package/dist/report/findings.d.ts +6 -0
  21. package/dist/report/findings.js +49 -0
  22. package/dist/report/json.d.ts +2 -0
  23. package/dist/report/json.js +3 -0
  24. package/dist/report/sarif.d.ts +2 -0
  25. package/dist/report/sarif.js +62 -0
  26. package/dist/report/terminal.d.ts +3 -0
  27. package/dist/report/terminal.js +31 -0
  28. package/dist/rules/catalog.d.ts +11 -0
  29. package/dist/rules/catalog.js +208 -0
  30. package/dist/scanners/apiRoutes.d.ts +3 -0
  31. package/dist/scanners/apiRoutes.js +52 -0
  32. package/dist/scanners/deploy.d.ts +3 -0
  33. package/dist/scanners/deploy.js +56 -0
  34. package/dist/scanners/gitDiff.d.ts +2 -0
  35. package/dist/scanners/gitDiff.js +246 -0
  36. package/dist/scanners/mcp.d.ts +3 -0
  37. package/dist/scanners/mcp.js +180 -0
  38. package/dist/scanners/secrets.d.ts +5 -0
  39. package/dist/scanners/secrets.js +122 -0
  40. package/dist/scanners/stripe.d.ts +3 -0
  41. package/dist/scanners/stripe.js +166 -0
  42. package/dist/scanners/supabase.d.ts +3 -0
  43. package/dist/scanners/supabase.js +126 -0
  44. package/dist/types.d.ts +92 -0
  45. package/dist/types.js +1 -0
  46. package/dist/utils/files.d.ts +11 -0
  47. package/dist/utils/files.js +116 -0
  48. package/docs/npm-publishing.md +59 -0
  49. package/docs/positioning.md +66 -0
  50. package/docs/project-handoff.md +208 -0
  51. package/docs/release-quality-knowledge-base.md +482 -0
  52. package/docs/rules.md +75 -0
  53. package/examples/sample-report.md +41 -0
  54. package/package.json +58 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ai-saas-guard contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,285 @@
1
+ <h1 align="center">ai-saas-guard</h1>
2
+
3
+ <p align="center">
4
+ <strong>Local-first launch preflight for AI-built SaaS apps.</strong>
5
+ </p>
6
+
7
+ <p align="center">
8
+ Find the auth, billing, data-access, secret, MCP, and deploy surfaces a human should review before launch or merge.
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://github.com/zr9959/ai-saas-guard/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/zr9959/ai-saas-guard/actions/workflows/ci.yml/badge.svg"></a>
13
+ <a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
14
+ <a href="package.json"><img alt="Node.js >=20" src="https://img.shields.io/badge/node-%3E%3D20-339933.svg"></a>
15
+ <a href="docs/release-quality-knowledge-base.md"><img alt="Release gate documented" src="https://img.shields.io/badge/release%20gate-documented-0f766e.svg"></a>
16
+ </p>
17
+
18
+ ---
19
+
20
+ ## What It Does
21
+
22
+ `ai-saas-guard` is a command-line launch preflight for founders, solo builders, and reviewers shipping SaaS apps with AI coding tools.
23
+
24
+ It answers one narrow question:
25
+
26
+ > What changed in auth, billing, data access, secrets, MCP tools, or deploy config that deserves human review first?
27
+
28
+ It is built for common AI-SaaS stacks:
29
+
30
+ - Next.js and Vercel
31
+ - Supabase row-level security and storage policies
32
+ - Stripe checkout, subscriptions, and webhooks
33
+ - Prisma or SQL migrations
34
+ - MCP server configuration
35
+ - AI-generated pull requests with large mixed diffs
36
+
37
+ It is intentionally evidence-first. Findings include a rule ID, severity, file evidence, why it matters, how to verify it, and a fix direction.
38
+
39
+ ## Current Status
40
+
41
+ This repository is public on GitHub.
42
+
43
+ The first GitHub release and Action tag are `v0.1.0`; the npm-ready patch release is `v0.1.1`. The npm package is not published yet, so run the CLI from source for now. If you need stricter supply-chain pinning in CI, pin the GitHub Action to a reviewed commit SHA instead of a mutable tag.
44
+
45
+ | Area | Status |
46
+ | --- | --- |
47
+ | Public GitHub repository | Available |
48
+ | Local CLI from source | Available |
49
+ | JSON and SARIF output | Available |
50
+ | Composite GitHub Action | Available |
51
+ | Versioned Action tags | `v0.1.1` |
52
+ | npm package | Not published yet |
53
+
54
+ ## Quick Start From Source
55
+
56
+ ```bash
57
+ git clone https://github.com/zr9959/ai-saas-guard.git
58
+ cd ai-saas-guard
59
+ npm ci
60
+ npm run build
61
+ node dist/cli.js scan --root /path/to/your-saas
62
+ ```
63
+
64
+ Run focused checks:
65
+
66
+ ```bash
67
+ node dist/cli.js pr-risk --root /path/to/your-saas --base origin/main
68
+ node dist/cli.js check-supabase --root /path/to/your-saas
69
+ node dist/cli.js check-stripe --root /path/to/your-saas
70
+ node dist/cli.js check-mcp --root /path/to/your-saas
71
+ ```
72
+
73
+ Machine-readable output:
74
+
75
+ ```bash
76
+ node dist/cli.js scan --root /path/to/your-saas --json
77
+ node dist/cli.js scan --root /path/to/your-saas --sarif > ai-saas-guard.sarif
78
+ node dist/cli.js scan --root /path/to/your-saas --fail-on high
79
+ ```
80
+
81
+ ## Example Finding
82
+
83
+ Terminal output is designed to be useful to a reviewer, not just a scanner dashboard.
84
+
85
+ ```text
86
+ [HIGH] Stripe webhook lacks obvious duplicate event idempotency
87
+ Rule: stripe.webhook.missing-idempotency
88
+ Why: Stripe can retry and deliver duplicate events; without storing processed event IDs, access grants and revocations can drift.
89
+ Verify: Replay the same Stripe event ID twice and confirm the second delivery does not create duplicate fulfillment or inconsistent state.
90
+ Fix direction: Persist processed Stripe event IDs and make entitlement updates idempotent around event ID and subscription/customer IDs.
91
+ Evidence:
92
+ - app/api/stripe/webhook/route.ts:41 -> switch (event.type) {
93
+ ```
94
+
95
+ ## What It Checks
96
+
97
+ | Surface | Examples of risks it flags |
98
+ | --- | --- |
99
+ | Secrets and env | Secret-like values, risky `NEXT_PUBLIC_*` exposure |
100
+ | Stripe | Missing webhook route, unsigned webhook handling, parsed-body signature risk, missing idempotency, missing failure/cancel/update/refund paths |
101
+ | Supabase | RLS disabled on sensitive tables, `USING (true)`, missing ownership filters, public storage hints |
102
+ | API routes | Auth checks without obvious ownership guards, missing rate-limit hints on sensitive mutation routes |
103
+ | MCP | Plaintext secrets, non-localhost binds, broad filesystem/write access, shell tools, raw SQL tools |
104
+ | Deploy config | Next static export/runtime mismatches, Edge runtime with Node-only APIs, missing important env documentation |
105
+ | PR risk | Auth, billing, RLS, env, deploy, API, storage, test-removal, and large mixed-diff classification |
106
+
107
+ See [docs/rules.md](docs/rules.md) for the full rule map.
108
+
109
+ ## The Main Bet: PR Risk Triage
110
+
111
+ Most scanners start with "scan the whole repository." `ai-saas-guard` can do that, but its sharper wedge is pull request review.
112
+
113
+ AI-generated PRs often combine unrelated work:
114
+
115
+ - UI polish
116
+ - auth/session changes
117
+ - database migrations
118
+ - Stripe checkout edits
119
+ - Supabase policies
120
+ - Vercel config
121
+ - removed or weakened tests
122
+
123
+ `pr-risk` classifies the current diff and returns:
124
+
125
+ - top risky files to review first
126
+ - sensitive categories touched by the PR
127
+ - review-first checklist
128
+ - suggested PR split
129
+ - required tests or manual verification
130
+
131
+ ```bash
132
+ node dist/cli.js pr-risk --root /path/to/your-saas --base origin/main --json
133
+ ```
134
+
135
+ ## Commands
136
+
137
+ | Command | Purpose |
138
+ | --- | --- |
139
+ | `scan` | Broad local launch preflight across secrets, Stripe, Supabase, MCP, API routes, and deploy config |
140
+ | `pr-risk` | Classify the current git diff or a base branch diff for review priority |
141
+ | `check-supabase` | Inspect migrations and policy files for RLS and ownership risks |
142
+ | `check-stripe` | Inspect webhook handlers and billing lifecycle coverage |
143
+ | `check-mcp` | Inventory MCP configs and classify side effects |
144
+
145
+ ## GitHub Action
146
+
147
+ The repo includes a composite Action. Use the latest release tag or pin a reviewed commit SHA for stricter supply-chain control:
148
+
149
+ ```yaml
150
+ name: ai-saas-guard
151
+
152
+ on:
153
+ pull_request:
154
+
155
+ permissions:
156
+ contents: read
157
+
158
+ jobs:
159
+ preflight:
160
+ runs-on: ubuntu-latest
161
+ steps:
162
+ - uses: actions/checkout@v6.0.2
163
+ with:
164
+ fetch-depth: 0
165
+ - uses: zr9959/ai-saas-guard@v0.1.1
166
+ with:
167
+ command: pr-risk
168
+ root: ${{ github.workspace }}
169
+ base: origin/main
170
+ fail-on: high
171
+ ```
172
+
173
+ For SARIF upload:
174
+
175
+ ```yaml
176
+ - uses: zr9959/ai-saas-guard@v0.1.1
177
+ with:
178
+ command: scan
179
+ format: sarif
180
+ output: ai-saas-guard.sarif
181
+ - uses: github/codeql-action/upload-sarif@v3
182
+ with:
183
+ sarif_file: ai-saas-guard.sarif
184
+ ```
185
+
186
+ For maximum reproducibility, replace `v0.1.1` with the full commit SHA from the release notes.
187
+
188
+ ## Ignore File
189
+
190
+ Add `.ai-saas-guardignore` at the repository root to suppress generated fixtures, snapshots, vendored output, or known noisy paths:
191
+
192
+ ```gitignore
193
+ fixtures/**
194
+ snapshots/**
195
+ vendor/generated/**
196
+ ```
197
+
198
+ Use this sparingly. The goal is not to hide launch blockers; it is to keep reports focused enough that reviewers act on them.
199
+
200
+ ## Privacy Model
201
+
202
+ `ai-saas-guard` is designed to be safe to run against private local repositories.
203
+
204
+ - Runs locally.
205
+ - Reads repository files and git diffs.
206
+ - Makes no network calls during scan commands.
207
+ - Does not upload code.
208
+ - Requires no account or login.
209
+ - Does not modify scanned repositories.
210
+ - Redacts matched secret-like evidence.
211
+
212
+ ## What This Is Not
213
+
214
+ This project deliberately avoids broad security claims.
215
+
216
+ - It is not a pentest.
217
+ - It is not a full SAST platform.
218
+ - It does not prove your app is secure.
219
+ - It does not replace manual two-account authorization testing.
220
+ - It does not execute Stripe, Supabase, Vercel, or browser flows.
221
+ - It does not inspect production settings unless they are represented locally.
222
+ - It does not try to replace Semgrep, Gitleaks, TruffleHog, Bearer, CodeQL, or human review.
223
+
224
+ ## When To Use It
225
+
226
+ Use `ai-saas-guard` when:
227
+
228
+ - you are about to launch an AI-built SaaS MVP
229
+ - you are reviewing a large AI-generated pull request
230
+ - you added checkout, subscriptions, RLS, MCP tools, or deploy config
231
+ - you want a local, readable checklist before asking a human to review
232
+ - you need JSON or SARIF output for automation
233
+
234
+ Do not use it as the only launch approval signal. Treat it as a preflight that helps you decide where to spend review time.
235
+
236
+ ## Development
237
+
238
+ ```bash
239
+ npm ci
240
+ npm test
241
+ npm run build
242
+ node dist/cli.js scan --root .
243
+ ```
244
+
245
+ Before publishing a CLI update, GitHub Action update, npm package, plugin, or public repository change, follow [docs/release-quality-knowledge-base.md](docs/release-quality-knowledge-base.md).
246
+
247
+ ## Roadmap
248
+
249
+ Open-source core:
250
+
251
+ - local CLI
252
+ - deterministic scanner rules
253
+ - vulnerable and safe fixtures
254
+ - JSON and SARIF output
255
+ - GitHub Action wrapper
256
+ - rule documentation
257
+
258
+ Near-term priorities:
259
+
260
+ - npm trusted publishing and provenance
261
+ - PR comment summary mode
262
+ - configurable severity and rule toggles
263
+ - expanded Supabase RLS fixtures
264
+ - Stripe webhook replay cookbook
265
+ - SARIF upload workflow example
266
+
267
+ Potential paid layer later:
268
+
269
+ - hosted GitHub App
270
+ - saved and shareable reports
271
+ - PR comments and review-first annotations
272
+ - scan history
273
+ - team policy settings
274
+ - deeper Stripe, Supabase, Vercel, and MCP integrations
275
+ - optional human launch-readiness review
276
+
277
+ The open-source CLI should remain useful on its own. Paid features should save time, preserve history, and integrate with team workflows.
278
+
279
+ ## Security
280
+
281
+ Please read [SECURITY.md](SECURITY.md) before reporting vulnerabilities. Do not post real API keys, customer data, private source code, or production URLs in public issues.
282
+
283
+ ## npm Publishing
284
+
285
+ The package name is prepared but not published yet. See [docs/npm-publishing.md](docs/npm-publishing.md) for the GitHub Actions provenance workflow and the required `NPM_TOKEN` or trusted-publisher setup.
package/action.yml ADDED
@@ -0,0 +1,100 @@
1
+ name: AI SaaS Guard
2
+ description: Run repo-local launch-readiness checks for AI-built SaaS apps.
3
+ author: ai-saas-guard
4
+
5
+ inputs:
6
+ command:
7
+ description: "Command to run: scan, check-supabase, check-stripe, check-mcp, or pr-risk."
8
+ required: false
9
+ default: scan
10
+ root:
11
+ description: Repository path to scan.
12
+ required: false
13
+ default: ${{ github.workspace }}
14
+ format:
15
+ description: "Output format: terminal, json, or sarif."
16
+ required: false
17
+ default: terminal
18
+ fail-on:
19
+ description: "Fail the workflow when findings meet this severity: critical, high, medium, low, info, or none."
20
+ required: false
21
+ default: none
22
+ base:
23
+ description: Base ref for pr-risk.
24
+ required: false
25
+ default: ""
26
+ output:
27
+ description: Optional path to write output while also keeping the command exit code.
28
+ required: false
29
+ default: ""
30
+
31
+ runs:
32
+ using: composite
33
+ steps:
34
+ - name: Install ai-saas-guard action dependencies
35
+ shell: bash
36
+ run: npm ci
37
+ working-directory: ${{ github.action_path }}
38
+
39
+ - name: Build ai-saas-guard
40
+ shell: bash
41
+ run: npm run build
42
+ working-directory: ${{ github.action_path }}
43
+
44
+ - name: Run ai-saas-guard
45
+ shell: bash
46
+ env:
47
+ INPUT_COMMAND: ${{ inputs.command }}
48
+ INPUT_ROOT: ${{ inputs.root }}
49
+ INPUT_FORMAT: ${{ inputs.format }}
50
+ INPUT_FAIL_ON: ${{ inputs.fail-on }}
51
+ INPUT_BASE: ${{ inputs.base }}
52
+ INPUT_OUTPUT: ${{ inputs.output }}
53
+ run: |
54
+ set -o pipefail
55
+
56
+ case "${INPUT_COMMAND}" in
57
+ scan|check-supabase|check-stripe|check-mcp|pr-risk) ;;
58
+ *)
59
+ echo "Invalid command input: ${INPUT_COMMAND}" >&2
60
+ exit 2
61
+ ;;
62
+ esac
63
+
64
+ case "${INPUT_FORMAT}" in
65
+ terminal|json|sarif) ;;
66
+ *)
67
+ echo "Invalid format input: ${INPUT_FORMAT}" >&2
68
+ exit 2
69
+ ;;
70
+ esac
71
+
72
+ case "${INPUT_FAIL_ON}" in
73
+ none|critical|high|medium|low|info) ;;
74
+ *)
75
+ echo "Invalid fail-on input: ${INPUT_FAIL_ON}" >&2
76
+ exit 2
77
+ ;;
78
+ esac
79
+
80
+ args=("${INPUT_COMMAND}" "--root" "${INPUT_ROOT}")
81
+
82
+ if [ "${INPUT_FORMAT}" = "json" ]; then
83
+ args+=("--json")
84
+ elif [ "${INPUT_FORMAT}" = "sarif" ]; then
85
+ args+=("--sarif")
86
+ fi
87
+
88
+ if [ "${INPUT_FAIL_ON}" != "none" ]; then
89
+ args+=("--fail-on" "${INPUT_FAIL_ON}")
90
+ fi
91
+
92
+ if [ -n "${INPUT_BASE}" ]; then
93
+ args+=("--base" "${INPUT_BASE}")
94
+ fi
95
+
96
+ if [ -n "${INPUT_OUTPUT}" ]; then
97
+ node "${GITHUB_ACTION_PATH}/dist/cli.js" "${args[@]}" | tee -- "${INPUT_OUTPUT}"
98
+ else
99
+ node "${GITHUB_ACTION_PATH}/dist/cli.js" "${args[@]}"
100
+ fi
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { checkMcp, checkStripe, checkSupabase, classifyPrRisk, scanRepository } from "./index.js";
4
+ import { formatJsonReport } from "./report/json.js";
5
+ import { formatSarifReport } from "./report/sarif.js";
6
+ import { formatTerminalReport } from "./report/terminal.js";
7
+ async function main(argv) {
8
+ const args = parseArgs(argv);
9
+ if (!args.command || args.command === "help") {
10
+ process.stdout.write(helpText());
11
+ return 0;
12
+ }
13
+ let report;
14
+ switch (args.command) {
15
+ case "scan":
16
+ report = await scanRepository({ rootDir: args.rootDir });
17
+ break;
18
+ case "check-supabase":
19
+ report = await checkSupabase({ rootDir: args.rootDir });
20
+ break;
21
+ case "check-stripe":
22
+ report = await checkStripe({ rootDir: args.rootDir });
23
+ break;
24
+ case "check-mcp":
25
+ report = await checkMcp({ rootDir: args.rootDir });
26
+ break;
27
+ case "pr-risk":
28
+ report = await classifyPrRisk({ rootDir: args.rootDir, base: args.base });
29
+ break;
30
+ default:
31
+ process.stderr.write(`Unknown command: ${String(args.command)}\n\n${helpText()}`);
32
+ return 2;
33
+ }
34
+ process.stdout.write(formatReport(report, args.format));
35
+ if (shouldFail(report, args.failOn)) {
36
+ process.stderr.write(`Failing because findings met --fail-on ${args.failOn}\n`);
37
+ return 1;
38
+ }
39
+ return 0;
40
+ }
41
+ function parseArgs(argv) {
42
+ const result = {
43
+ rootDir: process.cwd(),
44
+ format: "terminal"
45
+ };
46
+ for (let index = 0; index < argv.length; index += 1) {
47
+ const arg = argv[index];
48
+ if (index === 0 && !arg.startsWith("-")) {
49
+ result.command = arg;
50
+ continue;
51
+ }
52
+ if (arg === "--json") {
53
+ result.format = "json";
54
+ continue;
55
+ }
56
+ if (arg === "--sarif") {
57
+ result.format = "sarif";
58
+ continue;
59
+ }
60
+ if (arg === "--format") {
61
+ const value = argv[index + 1];
62
+ if (value !== "terminal" && value !== "json" && value !== "sarif") {
63
+ throw new Error("--format requires terminal, json, or sarif");
64
+ }
65
+ result.format = value;
66
+ index += 1;
67
+ continue;
68
+ }
69
+ if (arg === "--root" || arg === "--path") {
70
+ const value = argv[index + 1];
71
+ if (!value)
72
+ throw new Error(`${arg} requires a path`);
73
+ result.rootDir = resolve(value);
74
+ index += 1;
75
+ continue;
76
+ }
77
+ if (arg === "--fail-on") {
78
+ const value = argv[index + 1];
79
+ if (!isFailOnValue(value))
80
+ throw new Error("--fail-on requires critical, high, medium, low, info, or none");
81
+ result.failOn = value;
82
+ index += 1;
83
+ continue;
84
+ }
85
+ if (arg === "--base") {
86
+ const value = argv[index + 1];
87
+ if (!value)
88
+ throw new Error("--base requires a branch or ref");
89
+ result.base = value;
90
+ index += 1;
91
+ continue;
92
+ }
93
+ if (arg === "-h" || arg === "--help") {
94
+ result.command = "help";
95
+ continue;
96
+ }
97
+ if (!result.command) {
98
+ result.command = arg;
99
+ continue;
100
+ }
101
+ throw new Error(`Unknown argument: ${arg}`);
102
+ }
103
+ result.rootDir = resolve(result.rootDir);
104
+ return result;
105
+ }
106
+ function formatReport(report, format) {
107
+ if (format === "json")
108
+ return formatJsonReport(report);
109
+ if (format === "sarif")
110
+ return formatSarifReport(report);
111
+ return `${formatTerminalReport(report)}\n`;
112
+ }
113
+ function shouldFail(report, failOn) {
114
+ if (!failOn || failOn === "none")
115
+ return false;
116
+ const threshold = severityRank(failOn);
117
+ return report.findings.some((finding) => severityRank(finding.severity) <= threshold);
118
+ }
119
+ function severityRank(severity) {
120
+ return {
121
+ critical: 0,
122
+ high: 1,
123
+ medium: 2,
124
+ low: 3,
125
+ info: 4
126
+ }[severity];
127
+ }
128
+ function isFailOnValue(value) {
129
+ return value === "critical" || value === "high" || value === "medium" || value === "low" || value === "info" || value === "none";
130
+ }
131
+ function helpText() {
132
+ return `ai-saas-guard
133
+
134
+ Repo-local launch-readiness scanner for AI-built SaaS apps.
135
+
136
+ Usage:
137
+ ai-saas-guard scan [--root <repo>] [--json|--sarif] [--fail-on <severity>]
138
+ ai-saas-guard check-supabase [--root <repo>] [--json|--sarif] [--fail-on <severity>]
139
+ ai-saas-guard check-stripe [--root <repo>] [--json|--sarif] [--fail-on <severity>]
140
+ ai-saas-guard check-mcp [--root <repo>] [--json|--sarif] [--fail-on <severity>]
141
+ ai-saas-guard pr-risk [--root <repo>] [--base <branch>] [--json|--sarif] [--fail-on <severity>]
142
+
143
+ Defaults:
144
+ - read-only
145
+ - no network calls
146
+ - no account or login required
147
+ - terminal output by default, JSON with --json
148
+ - SARIF output for GitHub code scanning with --sarif
149
+ `;
150
+ }
151
+ main(process.argv.slice(2)).then((code) => {
152
+ process.exitCode = code;
153
+ }, (error) => {
154
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
155
+ process.exitCode = 1;
156
+ });
@@ -0,0 +1,2 @@
1
+ import type { McpReport, ScanOptions } from "../types.js";
2
+ export declare function checkMcp(options: ScanOptions): Promise<McpReport>;
@@ -0,0 +1,4 @@
1
+ import { checkMcp as runMcpScanner } from "../scanners/mcp.js";
2
+ export function checkMcp(options) {
3
+ return runMcpScanner(options.rootDir);
4
+ }
@@ -0,0 +1,2 @@
1
+ import type { ScanOptions, StripeReport } from "../types.js";
2
+ export declare function checkStripe(options: ScanOptions): Promise<StripeReport>;
@@ -0,0 +1,4 @@
1
+ import { checkStripe as runStripeScanner } from "../scanners/stripe.js";
2
+ export function checkStripe(options) {
3
+ return runStripeScanner(options.rootDir);
4
+ }
@@ -0,0 +1,2 @@
1
+ import type { ScanOptions, SupabaseReport } from "../types.js";
2
+ export declare function checkSupabase(options: ScanOptions): Promise<SupabaseReport>;
@@ -0,0 +1,4 @@
1
+ import { checkSupabase as runSupabaseScanner } from "../scanners/supabase.js";
2
+ export function checkSupabase(options) {
3
+ return runSupabaseScanner(options.rootDir);
4
+ }
@@ -0,0 +1,2 @@
1
+ import type { PrRiskOptions, PrRiskReport } from "../types.js";
2
+ export declare function classifyPrRisk(options: PrRiskOptions): Promise<PrRiskReport>;
@@ -0,0 +1,4 @@
1
+ import { classifyPrRisk as runPrRiskScanner } from "../scanners/gitDiff.js";
2
+ export function classifyPrRisk(options) {
3
+ return runPrRiskScanner(options);
4
+ }
@@ -0,0 +1,2 @@
1
+ import type { BaseReport, ScanOptions } from "../types.js";
2
+ export declare function scanRepository(options: ScanOptions): Promise<BaseReport>;
@@ -0,0 +1,29 @@
1
+ import { createScanContext } from "../context.js";
2
+ import { createReport, uniqueFindings } from "../report/findings.js";
3
+ import { scanApiRoutes } from "../scanners/apiRoutes.js";
4
+ import { scanDeployConfig } from "../scanners/deploy.js";
5
+ import { checkMcp } from "../scanners/mcp.js";
6
+ import { scanNextPublicEnv, scanSecrets } from "../scanners/secrets.js";
7
+ import { checkStripe } from "../scanners/stripe.js";
8
+ import { checkSupabase } from "../scanners/supabase.js";
9
+ export async function scanRepository(options) {
10
+ const context = await createScanContext(options.rootDir);
11
+ const [secretFindings, nextPublicFindings, stripeReport, supabaseReport, mcpReport, apiFindings, deployFindings] = await Promise.all([
12
+ scanSecrets(context),
13
+ scanNextPublicEnv(context),
14
+ checkStripe(context),
15
+ checkSupabase(context),
16
+ checkMcp(context),
17
+ scanApiRoutes(context),
18
+ scanDeployConfig(context)
19
+ ]);
20
+ return createReport("scan", options.rootDir, uniqueFindings([
21
+ ...secretFindings,
22
+ ...nextPublicFindings,
23
+ ...stripeReport.findings,
24
+ ...supabaseReport.findings,
25
+ ...mcpReport.findings,
26
+ ...apiFindings,
27
+ ...deployFindings
28
+ ]), {});
29
+ }
@@ -0,0 +1,10 @@
1
+ import { type TextFile } from "./utils/files.js";
2
+ export interface ScanContext {
3
+ rootDir: string;
4
+ files: readonly TextFile[];
5
+ filesByPath: ReadonlyMap<string, TextFile>;
6
+ getFiles: (predicate?: (file: TextFile) => boolean) => TextFile[];
7
+ }
8
+ export type ScanInput = string | ScanContext;
9
+ export declare function createScanContext(rootDir: string): Promise<ScanContext>;
10
+ export declare function resolveScanContext(input: ScanInput): Promise<ScanContext>;
@@ -0,0 +1,16 @@
1
+ import { collectTextFiles } from "./utils/files.js";
2
+ export async function createScanContext(rootDir) {
3
+ const files = await collectTextFiles(rootDir);
4
+ const filesByPath = new Map(files.map((file) => [file.path, file]));
5
+ return {
6
+ rootDir,
7
+ files,
8
+ filesByPath,
9
+ getFiles(predicate) {
10
+ return predicate ? files.filter(predicate) : [...files];
11
+ }
12
+ };
13
+ }
14
+ export async function resolveScanContext(input) {
15
+ return typeof input === "string" ? createScanContext(input) : input;
16
+ }
@@ -0,0 +1,10 @@
1
+ export { scanRepository } from "./commands/scan.js";
2
+ export { checkStripe } from "./commands/checkStripe.js";
3
+ export { checkSupabase } from "./commands/checkSupabase.js";
4
+ export { checkMcp } from "./commands/checkMcp.js";
5
+ export { classifyPrRisk } from "./commands/prRisk.js";
6
+ export { createScanContext } from "./context.js";
7
+ export { getRuleMetadata, RULE_CATALOG } from "./rules/catalog.js";
8
+ export type { BaseReport, CommandName, Evidence, Finding, McpReport, McpServerInventory, PrRiskFile, PrRiskReport, ScanOptions, StripeReport, SupabaseReport } from "./types.js";
9
+ export type { ScanContext, ScanInput } from "./context.js";
10
+ export type { RuleMetadata, RuleStability } from "./rules/catalog.js";