ai-saas-guard 0.1.2 → 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 +10 -7
- package/dist/rules/catalog.js +7 -0
- package/dist/scanners/gitDiff.js +67 -9
- package/docs/npm-publishing.md +11 -13
- package/docs/project-handoff.md +17 -8
- package/docs/rules.md +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,8 +50,9 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
|
|
|
50
50
|
| Local CLI from source | Available for development |
|
|
51
51
|
| JSON and SARIF output | Available |
|
|
52
52
|
| Composite GitHub Action | Available |
|
|
53
|
-
| Versioned Action tags | `v0.1.
|
|
54
|
-
| npm package | `ai-saas-guard@0.1.
|
|
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 |
|
|
55
56
|
|
|
56
57
|
## Quick Start
|
|
57
58
|
|
|
@@ -146,11 +147,14 @@ AI-generated PRs often combine unrelated work:
|
|
|
146
147
|
- review-first checklist
|
|
147
148
|
- suggested PR split
|
|
148
149
|
- required tests or manual verification
|
|
150
|
+
- explicit git-diff diagnostics when a base ref or shallow checkout prevents PR classification
|
|
149
151
|
|
|
150
152
|
```bash
|
|
151
153
|
node dist/cli.js pr-risk --root /path/to/your-saas --base origin/main --json
|
|
152
154
|
```
|
|
153
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
|
+
|
|
154
158
|
## Commands
|
|
155
159
|
|
|
156
160
|
| Command | Purpose |
|
|
@@ -181,7 +185,7 @@ jobs:
|
|
|
181
185
|
- uses: actions/checkout@v6.0.2
|
|
182
186
|
with:
|
|
183
187
|
fetch-depth: 0
|
|
184
|
-
- uses: zr9959/ai-saas-guard@v0.1.
|
|
188
|
+
- uses: zr9959/ai-saas-guard@v0.1.3
|
|
185
189
|
with:
|
|
186
190
|
command: pr-risk
|
|
187
191
|
root: ${{ github.workspace }}
|
|
@@ -192,7 +196,7 @@ jobs:
|
|
|
192
196
|
For SARIF upload:
|
|
193
197
|
|
|
194
198
|
```yaml
|
|
195
|
-
- uses: zr9959/ai-saas-guard@v0.1.
|
|
199
|
+
- uses: zr9959/ai-saas-guard@v0.1.3
|
|
196
200
|
with:
|
|
197
201
|
command: scan
|
|
198
202
|
format: sarif
|
|
@@ -202,7 +206,7 @@ For SARIF upload:
|
|
|
202
206
|
sarif_file: ai-saas-guard.sarif
|
|
203
207
|
```
|
|
204
208
|
|
|
205
|
-
For maximum reproducibility, replace `v0.1.
|
|
209
|
+
For maximum reproducibility, replace `v0.1.3` with the full commit SHA from the release notes.
|
|
206
210
|
|
|
207
211
|
## Ignore File
|
|
208
212
|
|
|
@@ -276,7 +280,6 @@ Open-source core:
|
|
|
276
280
|
|
|
277
281
|
Near-term priorities:
|
|
278
282
|
|
|
279
|
-
- npm trusted publishing and provenance
|
|
280
283
|
- PR comment summary mode
|
|
281
284
|
- configurable severity and rule toggles
|
|
282
285
|
- expanded Supabase RLS fixtures
|
|
@@ -301,4 +304,4 @@ Please read [SECURITY.md](SECURITY.md) before reporting vulnerabilities. Do not
|
|
|
301
304
|
|
|
302
305
|
## npm Publishing
|
|
303
306
|
|
|
304
|
-
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
|
|
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
|
@@ -5,27 +5,25 @@
|
|
|
5
5
|
## Current State
|
|
6
6
|
|
|
7
7
|
- Package name: `ai-saas-guard`
|
|
8
|
-
- Current version: `0.1.
|
|
8
|
+
- Current version: `0.1.3`
|
|
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.1.
|
|
11
|
+
- GitHub Release: `v0.1.3`
|
|
12
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
|
|
13
15
|
|
|
14
16
|
## Preferred Path
|
|
15
17
|
|
|
16
|
-
Use GitHub Actions with npm
|
|
18
|
+
Use GitHub Actions with npm Trusted Publisher/OIDC:
|
|
17
19
|
|
|
18
|
-
1. Create and review a release tag such as `v0.1.
|
|
19
|
-
2.
|
|
20
|
-
3.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- Repository: `ai-saas-guard`
|
|
24
|
-
- Workflow filename: `npm-publish.yml`
|
|
25
|
-
- Allowed action: `npm publish`
|
|
26
|
-
4. 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 first npm publish used a temporary granular access token because npm requires a 2FA-bypass token until trusted publishing is configured.
|
|
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
|