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.
- package/LICENSE +21 -0
- package/README.md +285 -0
- package/action.yml +100 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +156 -0
- package/dist/commands/checkMcp.d.ts +2 -0
- package/dist/commands/checkMcp.js +4 -0
- package/dist/commands/checkStripe.d.ts +2 -0
- package/dist/commands/checkStripe.js +4 -0
- package/dist/commands/checkSupabase.d.ts +2 -0
- package/dist/commands/checkSupabase.js +4 -0
- package/dist/commands/prRisk.d.ts +2 -0
- package/dist/commands/prRisk.js +4 -0
- package/dist/commands/scan.d.ts +2 -0
- package/dist/commands/scan.js +29 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.js +16 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +7 -0
- package/dist/report/findings.d.ts +6 -0
- package/dist/report/findings.js +49 -0
- package/dist/report/json.d.ts +2 -0
- package/dist/report/json.js +3 -0
- package/dist/report/sarif.d.ts +2 -0
- package/dist/report/sarif.js +62 -0
- package/dist/report/terminal.d.ts +3 -0
- package/dist/report/terminal.js +31 -0
- package/dist/rules/catalog.d.ts +11 -0
- package/dist/rules/catalog.js +208 -0
- package/dist/scanners/apiRoutes.d.ts +3 -0
- package/dist/scanners/apiRoutes.js +52 -0
- package/dist/scanners/deploy.d.ts +3 -0
- package/dist/scanners/deploy.js +56 -0
- package/dist/scanners/gitDiff.d.ts +2 -0
- package/dist/scanners/gitDiff.js +246 -0
- package/dist/scanners/mcp.d.ts +3 -0
- package/dist/scanners/mcp.js +180 -0
- package/dist/scanners/secrets.d.ts +5 -0
- package/dist/scanners/secrets.js +122 -0
- package/dist/scanners/stripe.d.ts +3 -0
- package/dist/scanners/stripe.js +166 -0
- package/dist/scanners/supabase.d.ts +3 -0
- package/dist/scanners/supabase.js +126 -0
- package/dist/types.d.ts +92 -0
- package/dist/types.js +1 -0
- package/dist/utils/files.d.ts +11 -0
- package/dist/utils/files.js +116 -0
- package/docs/npm-publishing.md +59 -0
- package/docs/positioning.md +66 -0
- package/docs/project-handoff.md +208 -0
- package/docs/release-quality-knowledge-base.md +482 -0
- package/docs/rules.md +75 -0
- package/examples/sample-report.md +41 -0
- 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
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,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>;
|
package/dist/context.js
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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";
|