ai-saas-guard 0.1.2 → 0.2.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 +33 -13
- package/action.yml +4 -2
- package/dist/cli.js +11 -3
- package/dist/report/markdown.d.ts +2 -0
- package/dist/report/markdown.js +83 -0
- package/dist/rules/catalog.js +7 -0
- package/dist/scanners/gitDiff.js +67 -9
- package/docs/github-action.md +69 -0
- package/docs/npm-publishing.md +11 -13
- package/docs/project-handoff.md +16 -12
- package/docs/rules.md +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ It is intentionally evidence-first. Findings include a rule ID, severity, file e
|
|
|
41
41
|
|
|
42
42
|
This repository is public on GitHub.
|
|
43
43
|
|
|
44
|
-
The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is available through versioned release tags.
|
|
44
|
+
The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is available through versioned release tags. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag for controlled upgrades, or a reviewed commit SHA for stricter supply-chain pinning.
|
|
45
45
|
|
|
46
46
|
| Area | Status |
|
|
47
47
|
| --- | --- |
|
|
@@ -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.
|
|
54
|
-
| npm package | `ai-saas-guard@0.
|
|
53
|
+
| Versioned Action tags | `v0.2.0`, `v0` |
|
|
54
|
+
| npm package | `ai-saas-guard@0.2.0` |
|
|
55
|
+
| npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
|
|
55
56
|
|
|
56
57
|
## Quick Start
|
|
57
58
|
|
|
@@ -75,6 +76,7 @@ Machine-readable output:
|
|
|
75
76
|
```bash
|
|
76
77
|
npx ai-saas-guard@latest scan --root /path/to/your-saas --json
|
|
77
78
|
npx ai-saas-guard@latest scan --root /path/to/your-saas --sarif > ai-saas-guard.sarif
|
|
79
|
+
npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --markdown > ai-saas-guard-pr.md
|
|
78
80
|
npx ai-saas-guard@latest scan --root /path/to/your-saas --fail-on high
|
|
79
81
|
```
|
|
80
82
|
|
|
@@ -146,24 +148,29 @@ AI-generated PRs often combine unrelated work:
|
|
|
146
148
|
- review-first checklist
|
|
147
149
|
- suggested PR split
|
|
148
150
|
- required tests or manual verification
|
|
151
|
+
- explicit git-diff diagnostics when a base ref or shallow checkout prevents PR classification
|
|
152
|
+
- PR-focused markdown for GitHub step summaries or PR comments
|
|
149
153
|
|
|
150
154
|
```bash
|
|
151
155
|
node dist/cli.js pr-risk --root /path/to/your-saas --base origin/main --json
|
|
156
|
+
node dist/cli.js pr-risk --root /path/to/your-saas --base origin/main --markdown
|
|
152
157
|
```
|
|
153
158
|
|
|
159
|
+
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`.
|
|
160
|
+
|
|
154
161
|
## Commands
|
|
155
162
|
|
|
156
163
|
| Command | Purpose |
|
|
157
164
|
| --- | --- |
|
|
158
165
|
| `scan` | Broad local launch preflight across secrets, Stripe, Supabase, MCP, API routes, and deploy config |
|
|
159
|
-
| `pr-risk` | Classify the current git diff or a base branch diff for review priority |
|
|
166
|
+
| `pr-risk` | Classify the current git diff or a base branch diff for review priority; supports JSON, SARIF, and PR-focused markdown |
|
|
160
167
|
| `check-supabase` | Inspect migrations and policy files for RLS and ownership risks |
|
|
161
168
|
| `check-stripe` | Inspect webhook handlers and billing lifecycle coverage |
|
|
162
169
|
| `check-mcp` | Inventory MCP configs and classify side effects |
|
|
163
170
|
|
|
164
171
|
## GitHub Action
|
|
165
172
|
|
|
166
|
-
The repo includes a composite Action. Use the latest release tag or pin a reviewed commit SHA for stricter supply-chain control:
|
|
173
|
+
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.2.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
|
|
167
174
|
|
|
168
175
|
```yaml
|
|
169
176
|
name: ai-saas-guard
|
|
@@ -181,7 +188,7 @@ jobs:
|
|
|
181
188
|
- uses: actions/checkout@v6.0.2
|
|
182
189
|
with:
|
|
183
190
|
fetch-depth: 0
|
|
184
|
-
- uses: zr9959/ai-saas-guard@v0
|
|
191
|
+
- uses: zr9959/ai-saas-guard@v0
|
|
185
192
|
with:
|
|
186
193
|
command: pr-risk
|
|
187
194
|
root: ${{ github.workspace }}
|
|
@@ -192,7 +199,7 @@ jobs:
|
|
|
192
199
|
For SARIF upload:
|
|
193
200
|
|
|
194
201
|
```yaml
|
|
195
|
-
- uses: zr9959/ai-saas-guard@v0
|
|
202
|
+
- uses: zr9959/ai-saas-guard@v0
|
|
196
203
|
with:
|
|
197
204
|
command: scan
|
|
198
205
|
format: sarif
|
|
@@ -202,7 +209,22 @@ For SARIF upload:
|
|
|
202
209
|
sarif_file: ai-saas-guard.sarif
|
|
203
210
|
```
|
|
204
211
|
|
|
205
|
-
For
|
|
212
|
+
For PR-readable markdown in the Actions run:
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
- uses: zr9959/ai-saas-guard@v0
|
|
216
|
+
with:
|
|
217
|
+
command: pr-risk
|
|
218
|
+
root: ${{ github.workspace }}
|
|
219
|
+
base: origin/main
|
|
220
|
+
format: markdown
|
|
221
|
+
output: ai-saas-guard-pr.md
|
|
222
|
+
- run: cat ai-saas-guard-pr.md >> "$GITHUB_STEP_SUMMARY"
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Use markdown for reviewer-facing PR triage and SARIF for GitHub code scanning alerts. See [docs/github-action.md](docs/github-action.md) for copy-paste workflows and trade-offs.
|
|
226
|
+
|
|
227
|
+
For maximum reproducibility, replace `v0` with the full commit SHA from the release notes.
|
|
206
228
|
|
|
207
229
|
## Ignore File
|
|
208
230
|
|
|
@@ -270,18 +292,16 @@ Open-source core:
|
|
|
270
292
|
- local CLI
|
|
271
293
|
- deterministic scanner rules
|
|
272
294
|
- vulnerable and safe fixtures
|
|
273
|
-
- JSON and
|
|
295
|
+
- JSON, SARIF, and PR-focused markdown output
|
|
274
296
|
- GitHub Action wrapper
|
|
275
297
|
- rule documentation
|
|
276
298
|
|
|
277
299
|
Near-term priorities:
|
|
278
300
|
|
|
279
|
-
- npm trusted publishing and provenance
|
|
280
|
-
- PR comment summary mode
|
|
281
301
|
- configurable severity and rule toggles
|
|
282
302
|
- expanded Supabase RLS fixtures
|
|
283
303
|
- Stripe webhook replay cookbook
|
|
284
|
-
-
|
|
304
|
+
- launch-readiness checklist content
|
|
285
305
|
|
|
286
306
|
Potential paid layer later:
|
|
287
307
|
|
|
@@ -301,4 +321,4 @@ Please read [SECURITY.md](SECURITY.md) before reporting vulnerabilities. Do not
|
|
|
301
321
|
|
|
302
322
|
## npm Publishing
|
|
303
323
|
|
|
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
|
|
324
|
+
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/action.yml
CHANGED
|
@@ -12,7 +12,7 @@ inputs:
|
|
|
12
12
|
required: false
|
|
13
13
|
default: ${{ github.workspace }}
|
|
14
14
|
format:
|
|
15
|
-
description: "Output format: terminal, json, or
|
|
15
|
+
description: "Output format: terminal, json, sarif, or markdown."
|
|
16
16
|
required: false
|
|
17
17
|
default: terminal
|
|
18
18
|
fail-on:
|
|
@@ -62,7 +62,7 @@ runs:
|
|
|
62
62
|
esac
|
|
63
63
|
|
|
64
64
|
case "${INPUT_FORMAT}" in
|
|
65
|
-
terminal|json|sarif) ;;
|
|
65
|
+
terminal|json|sarif|markdown) ;;
|
|
66
66
|
*)
|
|
67
67
|
echo "Invalid format input: ${INPUT_FORMAT}" >&2
|
|
68
68
|
exit 2
|
|
@@ -83,6 +83,8 @@ runs:
|
|
|
83
83
|
args+=("--json")
|
|
84
84
|
elif [ "${INPUT_FORMAT}" = "sarif" ]; then
|
|
85
85
|
args+=("--sarif")
|
|
86
|
+
elif [ "${INPUT_FORMAT}" = "markdown" ]; then
|
|
87
|
+
args+=("--markdown")
|
|
86
88
|
fi
|
|
87
89
|
|
|
88
90
|
if [ "${INPUT_FAIL_ON}" != "none" ]; then
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { checkMcp, checkStripe, checkSupabase, classifyPrRisk, scanRepository } from "./index.js";
|
|
4
4
|
import { formatJsonReport } from "./report/json.js";
|
|
5
|
+
import { formatMarkdownReport } from "./report/markdown.js";
|
|
5
6
|
import { formatSarifReport } from "./report/sarif.js";
|
|
6
7
|
import { formatTerminalReport } from "./report/terminal.js";
|
|
7
8
|
async function main(argv) {
|
|
@@ -57,10 +58,14 @@ function parseArgs(argv) {
|
|
|
57
58
|
result.format = "sarif";
|
|
58
59
|
continue;
|
|
59
60
|
}
|
|
61
|
+
if (arg === "--markdown") {
|
|
62
|
+
result.format = "markdown";
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
60
65
|
if (arg === "--format") {
|
|
61
66
|
const value = argv[index + 1];
|
|
62
|
-
if (value !== "terminal" && value !== "json" && value !== "sarif") {
|
|
63
|
-
throw new Error("--format requires terminal, json, or
|
|
67
|
+
if (value !== "terminal" && value !== "json" && value !== "sarif" && value !== "markdown") {
|
|
68
|
+
throw new Error("--format requires terminal, json, sarif, or markdown");
|
|
64
69
|
}
|
|
65
70
|
result.format = value;
|
|
66
71
|
index += 1;
|
|
@@ -108,6 +113,8 @@ function formatReport(report, format) {
|
|
|
108
113
|
return formatJsonReport(report);
|
|
109
114
|
if (format === "sarif")
|
|
110
115
|
return formatSarifReport(report);
|
|
116
|
+
if (format === "markdown")
|
|
117
|
+
return formatMarkdownReport(report);
|
|
111
118
|
return `${formatTerminalReport(report)}\n`;
|
|
112
119
|
}
|
|
113
120
|
function shouldFail(report, failOn) {
|
|
@@ -138,7 +145,7 @@ Usage:
|
|
|
138
145
|
ai-saas-guard check-supabase [--root <repo>] [--json|--sarif] [--fail-on <severity>]
|
|
139
146
|
ai-saas-guard check-stripe [--root <repo>] [--json|--sarif] [--fail-on <severity>]
|
|
140
147
|
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>]
|
|
148
|
+
ai-saas-guard pr-risk [--root <repo>] [--base <branch>] [--json|--sarif|--markdown] [--fail-on <severity>]
|
|
142
149
|
|
|
143
150
|
Defaults:
|
|
144
151
|
- read-only
|
|
@@ -146,6 +153,7 @@ Defaults:
|
|
|
146
153
|
- no account or login required
|
|
147
154
|
- terminal output by default, JSON with --json
|
|
148
155
|
- SARIF output for GitHub code scanning with --sarif
|
|
156
|
+
- PR-focused markdown summary with --markdown
|
|
149
157
|
`;
|
|
150
158
|
}
|
|
151
159
|
main(process.argv.slice(2)).then((code) => {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export function formatMarkdownReport(report) {
|
|
2
|
+
if (report.command === "pr-risk")
|
|
3
|
+
return `${formatPrRiskMarkdown(report)}\n`;
|
|
4
|
+
return `${formatGenericMarkdown(report)}\n`;
|
|
5
|
+
}
|
|
6
|
+
function formatPrRiskMarkdown(report) {
|
|
7
|
+
const lines = [];
|
|
8
|
+
lines.push("## ai-saas-guard PR risk summary");
|
|
9
|
+
lines.push("");
|
|
10
|
+
lines.push(summaryLine(report));
|
|
11
|
+
if (report.categories.length > 0) {
|
|
12
|
+
lines.push("");
|
|
13
|
+
lines.push(`**Risk categories:** ${report.categories.map((category) => `\`${category}\``).join(", ")}`);
|
|
14
|
+
}
|
|
15
|
+
lines.push("");
|
|
16
|
+
lines.push("### Review first");
|
|
17
|
+
if (report.topRiskyFiles.length === 0) {
|
|
18
|
+
lines.push("");
|
|
19
|
+
lines.push("No changed trust-boundary files were classified by `pr-risk`.");
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
lines.push("");
|
|
23
|
+
lines.push("| File | Score | Categories | Diff |");
|
|
24
|
+
lines.push("| --- | ---: | --- | ---: |");
|
|
25
|
+
for (const file of report.topRiskyFiles.slice(0, 10)) {
|
|
26
|
+
lines.push(`| \`${escapeMarkdownTableCell(file.path)}\` | ${file.score} | ${file.categories.map((category) => `\`${escapeMarkdownTableCell(category)}\``).join("<br>")} | +${file.added} / -${file.removed} |`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
lines.push("");
|
|
30
|
+
lines.push("### Required verification");
|
|
31
|
+
appendList(lines, report.requiredTests.length > 0 ? report.requiredTests : report.reviewChecklist);
|
|
32
|
+
lines.push("");
|
|
33
|
+
lines.push("### Suggested PR split");
|
|
34
|
+
appendList(lines, report.suggestedSplit.length > 0 ? report.suggestedSplit : ["No split suggestion from the current diff."]);
|
|
35
|
+
lines.push("");
|
|
36
|
+
lines.push("### Findings");
|
|
37
|
+
appendFindings(lines, report.findings);
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
|
40
|
+
function formatGenericMarkdown(report) {
|
|
41
|
+
const lines = [];
|
|
42
|
+
lines.push(`## ai-saas-guard ${report.command}`);
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push(summaryLine(report));
|
|
45
|
+
lines.push("");
|
|
46
|
+
lines.push("### Findings");
|
|
47
|
+
appendFindings(lines, report.findings);
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
50
|
+
function summaryLine(report) {
|
|
51
|
+
return `**Findings:** ${report.summary.total} total | critical ${report.summary.critical} | high ${report.summary.high} | medium ${report.summary.medium} | low ${report.summary.low} | info ${report.summary.info}`;
|
|
52
|
+
}
|
|
53
|
+
function appendList(lines, items) {
|
|
54
|
+
for (const item of items) {
|
|
55
|
+
lines.push(`- ${item}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function appendFindings(lines, findings) {
|
|
59
|
+
if (findings.length === 0) {
|
|
60
|
+
lines.push("");
|
|
61
|
+
lines.push("No heuristic launch-readiness risks found by this command.");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
for (const [index, finding] of findings.entries()) {
|
|
65
|
+
lines.push("");
|
|
66
|
+
lines.push(`${index + 1}. **[${finding.severity.toUpperCase()}] ${finding.title}**`);
|
|
67
|
+
lines.push(` - Rule: \`${finding.ruleId}\``);
|
|
68
|
+
lines.push(` - Evidence: ${formatEvidence(finding.evidence[0])}`);
|
|
69
|
+
lines.push(` - Why: ${finding.why}`);
|
|
70
|
+
lines.push(` - Verify: ${finding.suggestedVerification}`);
|
|
71
|
+
lines.push(` - Fix direction: ${finding.suggestedFix}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function formatEvidence(evidence) {
|
|
75
|
+
if (!evidence)
|
|
76
|
+
return "`none`";
|
|
77
|
+
const location = evidence.line ? `${evidence.file}:${evidence.line}` : evidence.file;
|
|
78
|
+
const detail = evidence.snippet ?? evidence.match;
|
|
79
|
+
return detail ? `\`${location}\` - ${detail}` : `\`${location}\``;
|
|
80
|
+
}
|
|
81
|
+
function escapeMarkdownTableCell(value) {
|
|
82
|
+
return value.replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
83
|
+
}
|
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 = [];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# GitHub Action Usage
|
|
2
|
+
|
|
3
|
+
`ai-saas-guard` ships as a composite GitHub Action for pull request and code scanning workflows.
|
|
4
|
+
|
|
5
|
+
Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.2.0` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
|
|
6
|
+
|
|
7
|
+
## PR Summary
|
|
8
|
+
|
|
9
|
+
Use markdown when reviewers need a short, evidence-first summary of risky files, required verification, and suggested PR split.
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
name: ai-saas-guard-pr-summary
|
|
13
|
+
|
|
14
|
+
on:
|
|
15
|
+
pull_request:
|
|
16
|
+
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
pr-summary:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v6.0.2
|
|
25
|
+
with:
|
|
26
|
+
fetch-depth: 0
|
|
27
|
+
- uses: zr9959/ai-saas-guard@v0
|
|
28
|
+
with:
|
|
29
|
+
command: pr-risk
|
|
30
|
+
root: ${{ github.workspace }}
|
|
31
|
+
base: origin/main
|
|
32
|
+
format: markdown
|
|
33
|
+
output: ai-saas-guard-pr.md
|
|
34
|
+
- run: cat ai-saas-guard-pr.md >> "$GITHUB_STEP_SUMMARY"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Use markdown for PR review triage. It is intentionally short enough for a GitHub step summary or a PR comment created by your own workflow. It does not require a hosted service.
|
|
38
|
+
|
|
39
|
+
## SARIF Upload
|
|
40
|
+
|
|
41
|
+
Use SARIF when you want findings to appear in GitHub code scanning alerts.
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
name: ai-saas-guard-sarif
|
|
45
|
+
|
|
46
|
+
on:
|
|
47
|
+
pull_request:
|
|
48
|
+
|
|
49
|
+
permissions:
|
|
50
|
+
contents: read
|
|
51
|
+
security-events: write
|
|
52
|
+
|
|
53
|
+
jobs:
|
|
54
|
+
code-scanning:
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/checkout@v6.0.2
|
|
58
|
+
- uses: zr9959/ai-saas-guard@v0
|
|
59
|
+
with:
|
|
60
|
+
command: scan
|
|
61
|
+
root: ${{ github.workspace }}
|
|
62
|
+
format: sarif
|
|
63
|
+
output: ai-saas-guard.sarif
|
|
64
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
65
|
+
with:
|
|
66
|
+
sarif_file: ai-saas-guard.sarif
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Use SARIF for tracking alerts over time. Use markdown for reviewer guidance on a specific PR. Many teams should run both: markdown for quick review order, SARIF for code scanning visibility.
|
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.
|
|
8
|
+
- Current version: `0.2.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.
|
|
11
|
+
- GitHub Release: `v0.2.0`
|
|
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.
|
|
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.2.0`.
|
|
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,12 @@ 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
|
|
49
|
+
- PR-focused markdown summary output for GitHub step summaries or PR comments
|
|
48
50
|
- JSON output
|
|
49
51
|
- SARIF output
|
|
50
52
|
- composite GitHub Action wrapper
|
|
53
|
+
- npm publishing through GitHub Actions Trusted Publisher/OIDC
|
|
51
54
|
|
|
52
55
|
Existing commands:
|
|
53
56
|
|
|
@@ -96,13 +99,9 @@ GitHub Project:
|
|
|
96
99
|
Current issue set:
|
|
97
100
|
|
|
98
101
|
- #1 Add launch-readiness checklist content
|
|
99
|
-
- #2 Add GitHub Action release packaging
|
|
100
102
|
- #3 Add configurable rule severity and rule toggles
|
|
101
|
-
- #4 Add PR comment summary mode
|
|
102
103
|
- #5 Write Stripe webhook replay cookbook
|
|
103
|
-
- #6 Add SARIF upload workflow example
|
|
104
104
|
- #7 Expand Supabase RLS fixtures and ownership patterns
|
|
105
|
-
- #8 Publish ai-saas-guard to npm
|
|
106
105
|
|
|
107
106
|
CI:
|
|
108
107
|
|
|
@@ -111,6 +110,14 @@ CI:
|
|
|
111
110
|
- Uses `permissions: contents: read`
|
|
112
111
|
- Latest verified run after setup succeeded
|
|
113
112
|
|
|
113
|
+
Publishing:
|
|
114
|
+
|
|
115
|
+
- npm package: `ai-saas-guard`
|
|
116
|
+
- Current release line: `v0.2.0`
|
|
117
|
+
- Publish workflow: `.github/workflows/npm-publish.yml`
|
|
118
|
+
- Trusted Publisher: GitHub Actions for `zr9959/ai-saas-guard`, workflow `npm-publish.yml`
|
|
119
|
+
- Long-lived npm publish tokens should not be required.
|
|
120
|
+
|
|
114
121
|
## Repository Boundaries
|
|
115
122
|
|
|
116
123
|
Allowed in this public repository:
|
|
@@ -133,14 +140,11 @@ Not allowed:
|
|
|
133
140
|
|
|
134
141
|
Recommended order:
|
|
135
142
|
|
|
136
|
-
1.
|
|
137
|
-
2.
|
|
138
|
-
3.
|
|
139
|
-
4.
|
|
140
|
-
5.
|
|
141
|
-
6. Write Stripe webhook replay cookbook.
|
|
142
|
-
7. Add SARIF upload workflow example.
|
|
143
|
-
8. Improve false-positive suppression and rule stability labels.
|
|
143
|
+
1. Add configurable severity and rule toggles.
|
|
144
|
+
2. Expand Supabase RLS fixtures and ownership patterns.
|
|
145
|
+
3. Write Stripe webhook replay cookbook.
|
|
146
|
+
4. Improve false-positive suppression and rule stability labels.
|
|
147
|
+
5. Add a GitHub App design note for the potential hosted layer.
|
|
144
148
|
|
|
145
149
|
For every feature, keep the scanner evidence-first:
|
|
146
150
|
|
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
|