ai-saas-guard 0.1.1 → 0.1.3
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 +40 -18
- package/dist/rules/catalog.js +7 -0
- package/dist/scanners/gitDiff.js +67 -9
- package/docs/npm-publishing.md +14 -16
- package/docs/project-handoff.md +17 -8
- package/docs/rules.md +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
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="https://www.npmjs.com/package/ai-saas-guard"><img alt="npm" src="https://img.shields.io/npm/v/ai-saas-guard.svg"></a>
|
|
13
14
|
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
|
|
14
15
|
<a href="package.json"><img alt="Node.js >=20" src="https://img.shields.io/badge/node-%3E%3D20-339933.svg"></a>
|
|
15
16
|
<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>
|
|
@@ -40,18 +41,45 @@ It is intentionally evidence-first. Findings include a rule ID, severity, file e
|
|
|
40
41
|
|
|
41
42
|
This repository is public on GitHub.
|
|
42
43
|
|
|
43
|
-
The
|
|
44
|
+
The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is available through versioned release tags. 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
|
|
|
45
46
|
| Area | Status |
|
|
46
47
|
| --- | --- |
|
|
47
48
|
| Public GitHub repository | Available |
|
|
48
|
-
|
|
|
49
|
+
| npm CLI | Published as `ai-saas-guard` |
|
|
50
|
+
| Local CLI from source | Available for development |
|
|
49
51
|
| JSON and SARIF output | Available |
|
|
50
52
|
| Composite GitHub Action | Available |
|
|
51
|
-
| Versioned Action tags | `v0.1.
|
|
52
|
-
| npm package |
|
|
53
|
+
| Versioned Action tags | `v0.1.3` |
|
|
54
|
+
| npm package | `ai-saas-guard@0.1.3` |
|
|
55
|
+
| npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
|
|
53
56
|
|
|
54
|
-
## Quick Start
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
Run the published CLI without installing it globally:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Run focused checks:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main
|
|
69
|
+
npx ai-saas-guard@latest check-supabase --root /path/to/your-saas
|
|
70
|
+
npx ai-saas-guard@latest check-stripe --root /path/to/your-saas
|
|
71
|
+
npx ai-saas-guard@latest check-mcp --root /path/to/your-saas
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Machine-readable output:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas --json
|
|
78
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas --sarif > ai-saas-guard.sarif
|
|
79
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas --fail-on high
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
For local development:
|
|
55
83
|
|
|
56
84
|
```bash
|
|
57
85
|
git clone https://github.com/zr9959/ai-saas-guard.git
|
|
@@ -70,14 +98,6 @@ node dist/cli.js check-stripe --root /path/to/your-saas
|
|
|
70
98
|
node dist/cli.js check-mcp --root /path/to/your-saas
|
|
71
99
|
```
|
|
72
100
|
|
|
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
101
|
## Example Finding
|
|
82
102
|
|
|
83
103
|
Terminal output is designed to be useful to a reviewer, not just a scanner dashboard.
|
|
@@ -127,11 +147,14 @@ AI-generated PRs often combine unrelated work:
|
|
|
127
147
|
- review-first checklist
|
|
128
148
|
- suggested PR split
|
|
129
149
|
- required tests or manual verification
|
|
150
|
+
- explicit git-diff diagnostics when a base ref or shallow checkout prevents PR classification
|
|
130
151
|
|
|
131
152
|
```bash
|
|
132
153
|
node dist/cli.js pr-risk --root /path/to/your-saas --base origin/main --json
|
|
133
154
|
```
|
|
134
155
|
|
|
156
|
+
If `--base` cannot be resolved, `pr-risk` emits `pr-risk.diff-unavailable` instead of silently reporting a clean or empty diff. In GitHub Actions, use `actions/checkout` with `fetch-depth: 0` when you need merge-base comparison against `origin/main`.
|
|
157
|
+
|
|
135
158
|
## Commands
|
|
136
159
|
|
|
137
160
|
| Command | Purpose |
|
|
@@ -162,7 +185,7 @@ jobs:
|
|
|
162
185
|
- uses: actions/checkout@v6.0.2
|
|
163
186
|
with:
|
|
164
187
|
fetch-depth: 0
|
|
165
|
-
- uses: zr9959/ai-saas-guard@v0.1.
|
|
188
|
+
- uses: zr9959/ai-saas-guard@v0.1.3
|
|
166
189
|
with:
|
|
167
190
|
command: pr-risk
|
|
168
191
|
root: ${{ github.workspace }}
|
|
@@ -173,7 +196,7 @@ jobs:
|
|
|
173
196
|
For SARIF upload:
|
|
174
197
|
|
|
175
198
|
```yaml
|
|
176
|
-
- uses: zr9959/ai-saas-guard@v0.1.
|
|
199
|
+
- uses: zr9959/ai-saas-guard@v0.1.3
|
|
177
200
|
with:
|
|
178
201
|
command: scan
|
|
179
202
|
format: sarif
|
|
@@ -183,7 +206,7 @@ For SARIF upload:
|
|
|
183
206
|
sarif_file: ai-saas-guard.sarif
|
|
184
207
|
```
|
|
185
208
|
|
|
186
|
-
For maximum reproducibility, replace `v0.1.
|
|
209
|
+
For maximum reproducibility, replace `v0.1.3` with the full commit SHA from the release notes.
|
|
187
210
|
|
|
188
211
|
## Ignore File
|
|
189
212
|
|
|
@@ -257,7 +280,6 @@ Open-source core:
|
|
|
257
280
|
|
|
258
281
|
Near-term priorities:
|
|
259
282
|
|
|
260
|
-
- npm trusted publishing and provenance
|
|
261
283
|
- PR comment summary mode
|
|
262
284
|
- configurable severity and rule toggles
|
|
263
285
|
- expanded Supabase RLS fixtures
|
|
@@ -282,4 +304,4 @@ Please read [SECURITY.md](SECURITY.md) before reporting vulnerabilities. Do not
|
|
|
282
304
|
|
|
283
305
|
## npm Publishing
|
|
284
306
|
|
|
285
|
-
The package
|
|
307
|
+
The package is published as [`ai-saas-guard`](https://www.npmjs.com/package/ai-saas-guard). See [docs/npm-publishing.md](docs/npm-publishing.md) for the GitHub Actions Trusted Publisher workflow, provenance notes, and first-publish token history.
|
package/dist/rules/catalog.js
CHANGED
|
@@ -195,6 +195,13 @@ export const RULE_CATALOG = {
|
|
|
195
195
|
why: "AI-generated PRs often bury trust-boundary changes inside larger diffs.",
|
|
196
196
|
stability: "default"
|
|
197
197
|
},
|
|
198
|
+
"pr-risk.diff-unavailable": {
|
|
199
|
+
ruleId: "pr-risk.diff-unavailable",
|
|
200
|
+
severity: "info",
|
|
201
|
+
title: "Git diff could not be read",
|
|
202
|
+
why: "PR classification can be misleading when the requested base ref or Git history is unavailable.",
|
|
203
|
+
stability: "default"
|
|
204
|
+
},
|
|
198
205
|
"pr-risk.no-diff": {
|
|
199
206
|
ruleId: "pr-risk.no-diff",
|
|
200
207
|
severity: "info",
|
package/dist/scanners/gitDiff.js
CHANGED
|
@@ -14,10 +14,13 @@ const categoryWeights = {
|
|
|
14
14
|
"large AI-generated/refactor-like diff": 18
|
|
15
15
|
};
|
|
16
16
|
export async function classifyPrRisk(options) {
|
|
17
|
-
const
|
|
17
|
+
const diffResult = options.diffText === undefined
|
|
18
|
+
? await readGitDiff(options.rootDir, options.base)
|
|
19
|
+
: { diffText: options.diffText, diagnostics: [] };
|
|
20
|
+
const { diffText } = diffResult;
|
|
18
21
|
const files = parseDiffFiles(diffText);
|
|
19
22
|
const categories = new Set();
|
|
20
|
-
const findings = [];
|
|
23
|
+
const findings = [...diffResult.diagnostics];
|
|
21
24
|
for (const file of files) {
|
|
22
25
|
for (const category of file.categories) {
|
|
23
26
|
categories.add(category);
|
|
@@ -44,7 +47,7 @@ export async function classifyPrRisk(options) {
|
|
|
44
47
|
suggestedFix: "Split unrelated UI/refactor work away from trust-boundary changes and add focused tests before merge."
|
|
45
48
|
}));
|
|
46
49
|
}
|
|
47
|
-
if (diffText.trim().length === 0) {
|
|
50
|
+
if (diffText.trim().length === 0 && diffResult.diagnostics.length === 0) {
|
|
48
51
|
findings.push(finding({
|
|
49
52
|
ruleId: "pr-risk.no-diff",
|
|
50
53
|
title: "No git diff found",
|
|
@@ -68,13 +71,17 @@ async function readGitDiff(rootDir, base) {
|
|
|
68
71
|
if (base) {
|
|
69
72
|
try {
|
|
70
73
|
const { stdout } = await execFileAsync("git", ["diff", `${base}...HEAD`], { cwd: rootDir, maxBuffer: 20 * 1024 * 1024 });
|
|
71
|
-
return stdout;
|
|
74
|
+
return { diffText: stdout, diagnostics: [] };
|
|
72
75
|
}
|
|
73
|
-
catch {
|
|
74
|
-
return
|
|
76
|
+
catch (error) {
|
|
77
|
+
return {
|
|
78
|
+
diffText: "",
|
|
79
|
+
diagnostics: [buildGitDiffFailureFinding(rootDir, ["git", "diff", `${base}...HEAD`], error, base)]
|
|
80
|
+
};
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
83
|
const parts = [];
|
|
84
|
+
const failures = [];
|
|
78
85
|
for (const args of [
|
|
79
86
|
["diff", "--cached"],
|
|
80
87
|
["diff"]
|
|
@@ -83,11 +90,62 @@ async function readGitDiff(rootDir, base) {
|
|
|
83
90
|
const { stdout } = await execFileAsync("git", args, { cwd: rootDir, maxBuffer: 20 * 1024 * 1024 });
|
|
84
91
|
parts.push(stdout);
|
|
85
92
|
}
|
|
86
|
-
catch {
|
|
87
|
-
|
|
93
|
+
catch (error) {
|
|
94
|
+
failures.push({ args: ["git", ...args], error });
|
|
88
95
|
}
|
|
89
96
|
}
|
|
90
|
-
|
|
97
|
+
if (parts.length === 0 && failures.length > 0) {
|
|
98
|
+
return {
|
|
99
|
+
diffText: "",
|
|
100
|
+
diagnostics: [buildGitDiffFailureFinding(rootDir, failures[0].args, failures[0].error)]
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return { diffText: parts.join("\n"), diagnostics: [] };
|
|
104
|
+
}
|
|
105
|
+
function buildGitDiffFailureFinding(rootDir, command, error, base) {
|
|
106
|
+
const errorText = redactRootPath(getGitErrorText(error), rootDir);
|
|
107
|
+
const lowerError = errorText.toLowerCase();
|
|
108
|
+
let suggestedVerification = "Run `git status` and confirm the target path is inside a Git repository.";
|
|
109
|
+
let suggestedFix = "Run `pr-risk` from a Git checkout, or pass explicit diff text through the API.";
|
|
110
|
+
if (base) {
|
|
111
|
+
suggestedVerification = `Run \`${buildFetchCommand(base)}\`, then \`git rev-parse --verify ${base}\` to confirm the base ref exists locally.`;
|
|
112
|
+
suggestedFix = "Fetch the branch or pass an existing local base ref, for example `--base origin/main`.";
|
|
113
|
+
}
|
|
114
|
+
if (base && (lowerError.includes("no merge base") || lowerError.includes("shallow"))) {
|
|
115
|
+
suggestedVerification = "Run `git rev-parse --is-shallow-repository` and confirm CI checks out full history before `pr-risk`.";
|
|
116
|
+
suggestedFix = "Use `fetch-depth: 0` in `actions/checkout`, or run `git fetch --unshallow` before invoking `pr-risk`.";
|
|
117
|
+
}
|
|
118
|
+
return finding({
|
|
119
|
+
ruleId: "pr-risk.diff-unavailable",
|
|
120
|
+
title: base ? `Could not read git diff for base ${base}` : "Could not read git diff",
|
|
121
|
+
severity: "info",
|
|
122
|
+
evidence: [
|
|
123
|
+
{
|
|
124
|
+
file: ".",
|
|
125
|
+
match: command.join(" "),
|
|
126
|
+
snippet: errorText.slice(0, 500)
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
why: "PR risk classification needs a readable git diff, but the git command failed for the target repository.",
|
|
130
|
+
suggestedVerification,
|
|
131
|
+
suggestedFix
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function buildFetchCommand(base) {
|
|
135
|
+
const remoteRef = /^([^/\s]+)\/(.+)$/.exec(base);
|
|
136
|
+
if (remoteRef)
|
|
137
|
+
return `git fetch ${remoteRef[1]} ${remoteRef[2]}`;
|
|
138
|
+
return `git fetch origin ${base}`;
|
|
139
|
+
}
|
|
140
|
+
function redactRootPath(text, rootDir) {
|
|
141
|
+
return rootDir ? text.replaceAll(rootDir, ".") : text;
|
|
142
|
+
}
|
|
143
|
+
function getGitErrorText(error) {
|
|
144
|
+
const candidate = error;
|
|
145
|
+
return [candidate.stderr, candidate.stdout, candidate.message]
|
|
146
|
+
.filter((value) => Boolean(value?.trim()))
|
|
147
|
+
.join("\n")
|
|
148
|
+
.trim() || "git diff exited with a non-zero status.";
|
|
91
149
|
}
|
|
92
150
|
function parseDiffFiles(diffText) {
|
|
93
151
|
const files = [];
|
package/docs/npm-publishing.md
CHANGED
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
# npm Publishing
|
|
2
2
|
|
|
3
|
-
`ai-saas-guard` is
|
|
3
|
+
`ai-saas-guard` is published on npm and should be released only from reviewed GitHub tags.
|
|
4
4
|
|
|
5
5
|
## Current State
|
|
6
6
|
|
|
7
7
|
- Package name: `ai-saas-guard`
|
|
8
|
-
- Current version: `0.1.
|
|
9
|
-
- npm registry state:
|
|
10
|
-
-
|
|
8
|
+
- Current version: `0.1.3`
|
|
9
|
+
- npm registry state: published at <https://www.npmjs.com/package/ai-saas-guard>
|
|
10
|
+
- First npm-published version: `0.1.1`
|
|
11
|
+
- GitHub Release: `v0.1.3`
|
|
11
12
|
- Publish workflow: `.github/workflows/npm-publish.yml`
|
|
13
|
+
- Trusted Publisher: GitHub Actions, `zr9959/ai-saas-guard`, workflow `npm-publish.yml`, allowed action `npm publish`
|
|
14
|
+
- Long-lived npm publish token: not required
|
|
12
15
|
|
|
13
16
|
## Preferred Path
|
|
14
17
|
|
|
15
|
-
Use GitHub Actions with npm
|
|
18
|
+
Use GitHub Actions with npm Trusted Publisher/OIDC:
|
|
16
19
|
|
|
17
|
-
1. Create
|
|
18
|
-
2.
|
|
19
|
-
3.
|
|
20
|
-
4.
|
|
21
|
-
|
|
22
|
-
- Organization or user: `zr9959`
|
|
23
|
-
- Repository: `ai-saas-guard`
|
|
24
|
-
- Workflow filename: `npm-publish.yml`
|
|
25
|
-
- Allowed action: `npm publish`
|
|
26
|
-
5. Once trusted publishing is verified, remove or rotate any long-lived npm publish token.
|
|
20
|
+
1. Create and review a release tag such as `v0.1.3`.
|
|
21
|
+
2. Publish from the GitHub Release or run the `Publish npm` workflow manually with `ref` set to that tag.
|
|
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
|
+
4. Run `npm publish --access public` from the workflow. Trusted publishing automatically generates provenance for this public package from this public repository.
|
|
24
|
+
5. Keep npm package publishing access set to require 2FA and disallow traditional tokens. Trusted publishers continue to work because they use OIDC instead of npm auth tokens.
|
|
27
25
|
|
|
28
|
-
The
|
|
26
|
+
The first npm publish used a temporary granular access token because npm requires a 2FA-bypass token until trusted publishing is configured. That temporary automation token and the GitHub `NPM_TOKEN` secret were removed after the Trusted Publisher migration.
|
|
29
27
|
|
|
30
28
|
## Release Gate
|
|
31
29
|
|
package/docs/project-handoff.md
CHANGED
|
@@ -45,9 +45,11 @@ Implemented surfaces:
|
|
|
45
45
|
- MCP config side-effect and secret-bearing risk inventory
|
|
46
46
|
- Next/Vercel deploy and runtime footguns
|
|
47
47
|
- PR diff risk triage for auth, billing, RLS, env, tests removed, and large mixed diffs
|
|
48
|
+
- PR diff diagnostics when a base ref or shallow checkout prevents comparison
|
|
48
49
|
- JSON output
|
|
49
50
|
- SARIF output
|
|
50
51
|
- composite GitHub Action wrapper
|
|
52
|
+
- npm publishing through GitHub Actions Trusted Publisher/OIDC
|
|
51
53
|
|
|
52
54
|
Existing commands:
|
|
53
55
|
|
|
@@ -111,6 +113,14 @@ CI:
|
|
|
111
113
|
- Uses `permissions: contents: read`
|
|
112
114
|
- Latest verified run after setup succeeded
|
|
113
115
|
|
|
116
|
+
Publishing:
|
|
117
|
+
|
|
118
|
+
- npm package: `ai-saas-guard`
|
|
119
|
+
- Current release line: `v0.1.3`
|
|
120
|
+
- Publish workflow: `.github/workflows/npm-publish.yml`
|
|
121
|
+
- Trusted Publisher: GitHub Actions for `zr9959/ai-saas-guard`, workflow `npm-publish.yml`
|
|
122
|
+
- Long-lived npm publish tokens should not be required.
|
|
123
|
+
|
|
114
124
|
## Repository Boundaries
|
|
115
125
|
|
|
116
126
|
Allowed in this public repository:
|
|
@@ -133,14 +143,13 @@ Not allowed:
|
|
|
133
143
|
|
|
134
144
|
Recommended order:
|
|
135
145
|
|
|
136
|
-
1.
|
|
137
|
-
2. Add
|
|
138
|
-
3.
|
|
139
|
-
4.
|
|
140
|
-
5.
|
|
141
|
-
6.
|
|
142
|
-
7. Add
|
|
143
|
-
8. Improve false-positive suppression and rule stability labels.
|
|
146
|
+
1. Add PR comment summary mode.
|
|
147
|
+
2. Add configurable severity and rule toggles.
|
|
148
|
+
3. Expand Supabase RLS fixtures and ownership patterns.
|
|
149
|
+
4. Write Stripe webhook replay cookbook.
|
|
150
|
+
5. Add SARIF upload workflow example.
|
|
151
|
+
6. Improve false-positive suppression and rule stability labels.
|
|
152
|
+
7. Add a GitHub App design note for the potential hosted layer.
|
|
144
153
|
|
|
145
154
|
For every feature, keep the scanner evidence-first:
|
|
146
155
|
|
package/docs/rules.md
CHANGED
|
@@ -61,6 +61,7 @@ Rule metadata is centralized in `src/rules/catalog.ts` and covered by tests so S
|
|
|
61
61
|
| Rule ID | Severity | Why it exists |
|
|
62
62
|
| --- | --- | --- |
|
|
63
63
|
| `pr-risk.sensitive-surface` | medium/high | Highlights files reviewers should inspect before cosmetic or refactor files. |
|
|
64
|
+
| `pr-risk.diff-unavailable` | info | Explains when the requested base ref or Git history prevents PR diff classification. |
|
|
64
65
|
| `pr-risk.no-diff` | info | Explains that PR classification needs a diff. |
|
|
65
66
|
|
|
66
67
|
## Adding Rules
|