ai-saas-guard 0.1.3 → 0.3.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 +51 -12
- package/action.yml +13 -2
- package/dist/cli.js +30 -9
- package/dist/config.d.ts +10 -0
- package/dist/config.js +89 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/report/markdown.d.ts +2 -0
- package/dist/report/markdown.js +83 -0
- package/docs/github-action.md +85 -0
- package/docs/npm-publishing.md +3 -3
- package/docs/project-handoff.md +8 -13
- 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
|
-
|
|
|
54
|
-
|
|
|
53
|
+
| Project config | `.ai-saas-guard.json` rule toggles, severity overrides, and fail thresholds |
|
|
54
|
+
| Versioned Action tags | `v0.3.0`, `v0` |
|
|
55
|
+
| npm package | `ai-saas-guard@0.3.0` |
|
|
55
56
|
| npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
|
|
56
57
|
|
|
57
58
|
## Quick Start
|
|
@@ -76,6 +77,8 @@ Machine-readable output:
|
|
|
76
77
|
```bash
|
|
77
78
|
npx ai-saas-guard@latest scan --root /path/to/your-saas --json
|
|
78
79
|
npx ai-saas-guard@latest scan --root /path/to/your-saas --sarif > ai-saas-guard.sarif
|
|
80
|
+
npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --markdown > ai-saas-guard-pr.md
|
|
81
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas --config <file> --json
|
|
79
82
|
npx ai-saas-guard@latest scan --root /path/to/your-saas --fail-on high
|
|
80
83
|
```
|
|
81
84
|
|
|
@@ -148,9 +151,11 @@ AI-generated PRs often combine unrelated work:
|
|
|
148
151
|
- suggested PR split
|
|
149
152
|
- required tests or manual verification
|
|
150
153
|
- explicit git-diff diagnostics when a base ref or shallow checkout prevents PR classification
|
|
154
|
+
- PR-focused markdown for GitHub step summaries or PR comments
|
|
151
155
|
|
|
152
156
|
```bash
|
|
153
157
|
node dist/cli.js pr-risk --root /path/to/your-saas --base origin/main --json
|
|
158
|
+
node dist/cli.js pr-risk --root /path/to/your-saas --base origin/main --markdown
|
|
154
159
|
```
|
|
155
160
|
|
|
156
161
|
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,14 +165,33 @@ If `--base` cannot be resolved, `pr-risk` emits `pr-risk.diff-unavailable` inste
|
|
|
160
165
|
| Command | Purpose |
|
|
161
166
|
| --- | --- |
|
|
162
167
|
| `scan` | Broad local launch preflight across secrets, Stripe, Supabase, MCP, API routes, and deploy config |
|
|
163
|
-
| `pr-risk` | Classify the current git diff or a base branch diff for review priority |
|
|
168
|
+
| `pr-risk` | Classify the current git diff or a base branch diff for review priority; supports JSON, SARIF, and PR-focused markdown |
|
|
164
169
|
| `check-supabase` | Inspect migrations and policy files for RLS and ownership risks |
|
|
165
170
|
| `check-stripe` | Inspect webhook handlers and billing lifecycle coverage |
|
|
166
171
|
| `check-mcp` | Inventory MCP configs and classify side effects |
|
|
167
172
|
|
|
173
|
+
## Project Configuration
|
|
174
|
+
|
|
175
|
+
Add `.ai-saas-guard.json` at the repository root to tune findings without changing scanner code. The CLI auto-loads this file from `--root` when it exists. Use `--config <file>` to point to a different JSON file.
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"failOn": "high",
|
|
180
|
+
"rules": {
|
|
181
|
+
"stripe.webhook.missing-signature": "off",
|
|
182
|
+
"stripe.webhook.missing-idempotency": "critical",
|
|
183
|
+
"deploy.env.example-missing": "info"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`rules` is keyed by published rule ID from [docs/rules.md](docs/rules.md). Set a rule to `off` to remove matching findings from terminal, JSON, SARIF, and markdown output. Set a rule to `critical`, `high`, `medium`, `low`, or `info` to override severity before summaries and `--fail-on` are evaluated.
|
|
189
|
+
|
|
190
|
+
`failOn` sets the default CI failure threshold for the project. A CLI `--fail-on` value takes precedence, so local runs can still use `--fail-on none` or a stricter threshold.
|
|
191
|
+
|
|
168
192
|
## GitHub Action
|
|
169
193
|
|
|
170
|
-
The repo includes a composite Action. Use the latest release tag or pin a reviewed commit SHA for stricter supply-chain control:
|
|
194
|
+
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.3.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
|
|
171
195
|
|
|
172
196
|
```yaml
|
|
173
197
|
name: ai-saas-guard
|
|
@@ -185,18 +209,19 @@ jobs:
|
|
|
185
209
|
- uses: actions/checkout@v6.0.2
|
|
186
210
|
with:
|
|
187
211
|
fetch-depth: 0
|
|
188
|
-
- uses: zr9959/ai-saas-guard@v0
|
|
212
|
+
- uses: zr9959/ai-saas-guard@v0
|
|
189
213
|
with:
|
|
190
214
|
command: pr-risk
|
|
191
215
|
root: ${{ github.workspace }}
|
|
192
216
|
base: origin/main
|
|
193
217
|
fail-on: high
|
|
218
|
+
config: .ai-saas-guard.json
|
|
194
219
|
```
|
|
195
220
|
|
|
196
221
|
For SARIF upload:
|
|
197
222
|
|
|
198
223
|
```yaml
|
|
199
|
-
- uses: zr9959/ai-saas-guard@v0
|
|
224
|
+
- uses: zr9959/ai-saas-guard@v0
|
|
200
225
|
with:
|
|
201
226
|
command: scan
|
|
202
227
|
format: sarif
|
|
@@ -206,7 +231,22 @@ For SARIF upload:
|
|
|
206
231
|
sarif_file: ai-saas-guard.sarif
|
|
207
232
|
```
|
|
208
233
|
|
|
209
|
-
For
|
|
234
|
+
For PR-readable markdown in the Actions run:
|
|
235
|
+
|
|
236
|
+
```yaml
|
|
237
|
+
- uses: zr9959/ai-saas-guard@v0
|
|
238
|
+
with:
|
|
239
|
+
command: pr-risk
|
|
240
|
+
root: ${{ github.workspace }}
|
|
241
|
+
base: origin/main
|
|
242
|
+
format: markdown
|
|
243
|
+
output: ai-saas-guard-pr.md
|
|
244
|
+
- run: cat ai-saas-guard-pr.md >> "$GITHUB_STEP_SUMMARY"
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
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.
|
|
248
|
+
|
|
249
|
+
For maximum reproducibility, replace `v0` with the full commit SHA from the release notes.
|
|
210
250
|
|
|
211
251
|
## Ignore File
|
|
212
252
|
|
|
@@ -274,17 +314,16 @@ Open-source core:
|
|
|
274
314
|
- local CLI
|
|
275
315
|
- deterministic scanner rules
|
|
276
316
|
- vulnerable and safe fixtures
|
|
277
|
-
- JSON and
|
|
317
|
+
- JSON, SARIF, and PR-focused markdown output
|
|
278
318
|
- GitHub Action wrapper
|
|
279
319
|
- rule documentation
|
|
280
320
|
|
|
281
321
|
Near-term priorities:
|
|
282
322
|
|
|
283
|
-
- PR comment summary mode
|
|
284
|
-
- configurable severity and rule toggles
|
|
285
323
|
- expanded Supabase RLS fixtures
|
|
286
324
|
- Stripe webhook replay cookbook
|
|
287
|
-
-
|
|
325
|
+
- launch-readiness checklist content
|
|
326
|
+
- false-positive suppression and rule stability labels
|
|
288
327
|
|
|
289
328
|
Potential paid layer later:
|
|
290
329
|
|
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:
|
|
@@ -23,6 +23,10 @@ inputs:
|
|
|
23
23
|
description: Base ref for pr-risk.
|
|
24
24
|
required: false
|
|
25
25
|
default: ""
|
|
26
|
+
config:
|
|
27
|
+
description: Optional ai-saas-guard JSON config path.
|
|
28
|
+
required: false
|
|
29
|
+
default: ""
|
|
26
30
|
output:
|
|
27
31
|
description: Optional path to write output while also keeping the command exit code.
|
|
28
32
|
required: false
|
|
@@ -49,6 +53,7 @@ runs:
|
|
|
49
53
|
INPUT_FORMAT: ${{ inputs.format }}
|
|
50
54
|
INPUT_FAIL_ON: ${{ inputs.fail-on }}
|
|
51
55
|
INPUT_BASE: ${{ inputs.base }}
|
|
56
|
+
INPUT_CONFIG: ${{ inputs.config }}
|
|
52
57
|
INPUT_OUTPUT: ${{ inputs.output }}
|
|
53
58
|
run: |
|
|
54
59
|
set -o pipefail
|
|
@@ -62,7 +67,7 @@ runs:
|
|
|
62
67
|
esac
|
|
63
68
|
|
|
64
69
|
case "${INPUT_FORMAT}" in
|
|
65
|
-
terminal|json|sarif) ;;
|
|
70
|
+
terminal|json|sarif|markdown) ;;
|
|
66
71
|
*)
|
|
67
72
|
echo "Invalid format input: ${INPUT_FORMAT}" >&2
|
|
68
73
|
exit 2
|
|
@@ -83,6 +88,8 @@ runs:
|
|
|
83
88
|
args+=("--json")
|
|
84
89
|
elif [ "${INPUT_FORMAT}" = "sarif" ]; then
|
|
85
90
|
args+=("--sarif")
|
|
91
|
+
elif [ "${INPUT_FORMAT}" = "markdown" ]; then
|
|
92
|
+
args+=("--markdown")
|
|
86
93
|
fi
|
|
87
94
|
|
|
88
95
|
if [ "${INPUT_FAIL_ON}" != "none" ]; then
|
|
@@ -93,6 +100,10 @@ runs:
|
|
|
93
100
|
args+=("--base" "${INPUT_BASE}")
|
|
94
101
|
fi
|
|
95
102
|
|
|
103
|
+
if [ -n "${INPUT_CONFIG}" ]; then
|
|
104
|
+
args+=("--config" "${INPUT_CONFIG}")
|
|
105
|
+
fi
|
|
106
|
+
|
|
96
107
|
if [ -n "${INPUT_OUTPUT}" ]; then
|
|
97
108
|
node "${GITHUB_ACTION_PATH}/dist/cli.js" "${args[@]}" | tee -- "${INPUT_OUTPUT}"
|
|
98
109
|
else
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
+
import { applyGuardConfig, loadGuardConfig } from "./config.js";
|
|
3
4
|
import { checkMcp, checkStripe, checkSupabase, classifyPrRisk, scanRepository } from "./index.js";
|
|
4
5
|
import { formatJsonReport } from "./report/json.js";
|
|
6
|
+
import { formatMarkdownReport } from "./report/markdown.js";
|
|
5
7
|
import { formatSarifReport } from "./report/sarif.js";
|
|
6
8
|
import { formatTerminalReport } from "./report/terminal.js";
|
|
7
9
|
async function main(argv) {
|
|
@@ -10,6 +12,7 @@ async function main(argv) {
|
|
|
10
12
|
process.stdout.write(helpText());
|
|
11
13
|
return 0;
|
|
12
14
|
}
|
|
15
|
+
const config = await loadGuardConfig(args.rootDir, args.configPath);
|
|
13
16
|
let report;
|
|
14
17
|
switch (args.command) {
|
|
15
18
|
case "scan":
|
|
@@ -31,9 +34,11 @@ async function main(argv) {
|
|
|
31
34
|
process.stderr.write(`Unknown command: ${String(args.command)}\n\n${helpText()}`);
|
|
32
35
|
return 2;
|
|
33
36
|
}
|
|
37
|
+
report = applyGuardConfig(report, config);
|
|
34
38
|
process.stdout.write(formatReport(report, args.format));
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
const failOn = args.failOn ?? config.failOn;
|
|
40
|
+
if (shouldFail(report, failOn)) {
|
|
41
|
+
process.stderr.write(`Failing because findings met --fail-on ${failOn}\n`);
|
|
37
42
|
return 1;
|
|
38
43
|
}
|
|
39
44
|
return 0;
|
|
@@ -57,10 +62,14 @@ function parseArgs(argv) {
|
|
|
57
62
|
result.format = "sarif";
|
|
58
63
|
continue;
|
|
59
64
|
}
|
|
65
|
+
if (arg === "--markdown") {
|
|
66
|
+
result.format = "markdown";
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
60
69
|
if (arg === "--format") {
|
|
61
70
|
const value = argv[index + 1];
|
|
62
|
-
if (value !== "terminal" && value !== "json" && value !== "sarif") {
|
|
63
|
-
throw new Error("--format requires terminal, json, or
|
|
71
|
+
if (value !== "terminal" && value !== "json" && value !== "sarif" && value !== "markdown") {
|
|
72
|
+
throw new Error("--format requires terminal, json, sarif, or markdown");
|
|
64
73
|
}
|
|
65
74
|
result.format = value;
|
|
66
75
|
index += 1;
|
|
@@ -82,6 +91,14 @@ function parseArgs(argv) {
|
|
|
82
91
|
index += 1;
|
|
83
92
|
continue;
|
|
84
93
|
}
|
|
94
|
+
if (arg === "--config") {
|
|
95
|
+
const value = argv[index + 1];
|
|
96
|
+
if (!value)
|
|
97
|
+
throw new Error("--config requires a path");
|
|
98
|
+
result.configPath = resolve(value);
|
|
99
|
+
index += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
85
102
|
if (arg === "--base") {
|
|
86
103
|
const value = argv[index + 1];
|
|
87
104
|
if (!value)
|
|
@@ -108,6 +125,8 @@ function formatReport(report, format) {
|
|
|
108
125
|
return formatJsonReport(report);
|
|
109
126
|
if (format === "sarif")
|
|
110
127
|
return formatSarifReport(report);
|
|
128
|
+
if (format === "markdown")
|
|
129
|
+
return formatMarkdownReport(report);
|
|
111
130
|
return `${formatTerminalReport(report)}\n`;
|
|
112
131
|
}
|
|
113
132
|
function shouldFail(report, failOn) {
|
|
@@ -134,11 +153,11 @@ function helpText() {
|
|
|
134
153
|
Repo-local launch-readiness scanner for AI-built SaaS apps.
|
|
135
154
|
|
|
136
155
|
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>]
|
|
156
|
+
ai-saas-guard scan [--root <repo>] [--config <file>] [--json|--sarif] [--fail-on <severity>]
|
|
157
|
+
ai-saas-guard check-supabase [--root <repo>] [--config <file>] [--json|--sarif] [--fail-on <severity>]
|
|
158
|
+
ai-saas-guard check-stripe [--root <repo>] [--config <file>] [--json|--sarif] [--fail-on <severity>]
|
|
159
|
+
ai-saas-guard check-mcp [--root <repo>] [--config <file>] [--json|--sarif] [--fail-on <severity>]
|
|
160
|
+
ai-saas-guard pr-risk [--root <repo>] [--config <file>] [--base <branch>] [--json|--sarif|--markdown] [--fail-on <severity>]
|
|
142
161
|
|
|
143
162
|
Defaults:
|
|
144
163
|
- read-only
|
|
@@ -146,6 +165,8 @@ Defaults:
|
|
|
146
165
|
- no account or login required
|
|
147
166
|
- terminal output by default, JSON with --json
|
|
148
167
|
- SARIF output for GitHub code scanning with --sarif
|
|
168
|
+
- PR-focused markdown summary with --markdown
|
|
169
|
+
- project config auto-loaded from .ai-saas-guard.json when present
|
|
149
170
|
`;
|
|
150
171
|
}
|
|
151
172
|
main(process.argv.slice(2)).then((code) => {
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BaseReport, Severity } from "./types.js";
|
|
2
|
+
export declare const defaultConfigFileName = ".ai-saas-guard.json";
|
|
3
|
+
export type RuleConfigValue = "off" | Severity;
|
|
4
|
+
export interface GuardConfig {
|
|
5
|
+
sourcePath?: string;
|
|
6
|
+
failOn?: Severity | "none";
|
|
7
|
+
rules: Record<string, RuleConfigValue>;
|
|
8
|
+
}
|
|
9
|
+
export declare function loadGuardConfig(rootDir: string, explicitPath?: string): Promise<GuardConfig>;
|
|
10
|
+
export declare function applyGuardConfig<T extends BaseReport>(report: T, config: GuardConfig): T;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { summarizeFindings, sortFindings } from "./report/findings.js";
|
|
4
|
+
import { getRuleMetadata } from "./rules/catalog.js";
|
|
5
|
+
export const defaultConfigFileName = ".ai-saas-guard.json";
|
|
6
|
+
export async function loadGuardConfig(rootDir, explicitPath) {
|
|
7
|
+
const sourcePath = explicitPath ? resolve(explicitPath) : resolve(rootDir, defaultConfigFileName);
|
|
8
|
+
let content;
|
|
9
|
+
try {
|
|
10
|
+
content = await readFile(sourcePath, "utf8");
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (!explicitPath && isNotFoundError(error))
|
|
14
|
+
return { rules: {} };
|
|
15
|
+
throw new Error(`Could not read config file ${sourcePath}: ${errorMessage(error)}`);
|
|
16
|
+
}
|
|
17
|
+
return parseGuardConfig(content, sourcePath);
|
|
18
|
+
}
|
|
19
|
+
export function applyGuardConfig(report, config) {
|
|
20
|
+
const configuredRuleIds = Object.keys(config.rules);
|
|
21
|
+
if (configuredRuleIds.length === 0)
|
|
22
|
+
return report;
|
|
23
|
+
const findings = sortFindings(report.findings.flatMap((finding) => {
|
|
24
|
+
const ruleConfig = config.rules[finding.ruleId];
|
|
25
|
+
if (!ruleConfig)
|
|
26
|
+
return [finding];
|
|
27
|
+
if (ruleConfig === "off")
|
|
28
|
+
return [];
|
|
29
|
+
return [{ ...finding, severity: ruleConfig }];
|
|
30
|
+
}));
|
|
31
|
+
return {
|
|
32
|
+
...report,
|
|
33
|
+
findings,
|
|
34
|
+
summary: summarizeFindings(findings)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function parseGuardConfig(content, sourcePath) {
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(content);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw new Error(`Invalid JSON in config file ${sourcePath}: ${errorMessage(error)}`);
|
|
44
|
+
}
|
|
45
|
+
if (!isPlainObject(parsed)) {
|
|
46
|
+
throw new Error(`Invalid config file ${sourcePath}: expected a JSON object`);
|
|
47
|
+
}
|
|
48
|
+
const failOn = parsed.failOn;
|
|
49
|
+
if (failOn !== undefined && !isFailOnValue(failOn)) {
|
|
50
|
+
throw new Error("Invalid config failOn: expected critical, high, medium, low, info, or none");
|
|
51
|
+
}
|
|
52
|
+
const rawRules = parsed.rules ?? {};
|
|
53
|
+
if (!isPlainObject(rawRules)) {
|
|
54
|
+
throw new Error("Invalid config rules: expected an object keyed by rule ID");
|
|
55
|
+
}
|
|
56
|
+
const rules = {};
|
|
57
|
+
for (const [ruleId, value] of Object.entries(rawRules)) {
|
|
58
|
+
if (!getRuleMetadata(ruleId)) {
|
|
59
|
+
throw new Error(`Unknown rule ID in config: ${ruleId}`);
|
|
60
|
+
}
|
|
61
|
+
if (!isRuleConfigValue(value)) {
|
|
62
|
+
throw new Error(`Invalid config for rule ${ruleId}: expected off, critical, high, medium, low, or info`);
|
|
63
|
+
}
|
|
64
|
+
rules[ruleId] = value;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
sourcePath,
|
|
68
|
+
failOn,
|
|
69
|
+
rules
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function isRuleConfigValue(value) {
|
|
73
|
+
return value === "off" || isSeverity(value);
|
|
74
|
+
}
|
|
75
|
+
function isFailOnValue(value) {
|
|
76
|
+
return value === "none" || isSeverity(value);
|
|
77
|
+
}
|
|
78
|
+
function isSeverity(value) {
|
|
79
|
+
return value === "critical" || value === "high" || value === "medium" || value === "low" || value === "info";
|
|
80
|
+
}
|
|
81
|
+
function isPlainObject(value) {
|
|
82
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
83
|
+
}
|
|
84
|
+
function isNotFoundError(error) {
|
|
85
|
+
return isPlainObject(error) && error.code === "ENOENT";
|
|
86
|
+
}
|
|
87
|
+
function errorMessage(error) {
|
|
88
|
+
return error instanceof Error ? error.message : String(error);
|
|
89
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,10 @@ export { checkStripe } from "./commands/checkStripe.js";
|
|
|
3
3
|
export { checkSupabase } from "./commands/checkSupabase.js";
|
|
4
4
|
export { checkMcp } from "./commands/checkMcp.js";
|
|
5
5
|
export { classifyPrRisk } from "./commands/prRisk.js";
|
|
6
|
+
export { applyGuardConfig, defaultConfigFileName, loadGuardConfig } from "./config.js";
|
|
6
7
|
export { createScanContext } from "./context.js";
|
|
7
8
|
export { getRuleMetadata, RULE_CATALOG } from "./rules/catalog.js";
|
|
8
9
|
export type { BaseReport, CommandName, Evidence, Finding, McpReport, McpServerInventory, PrRiskFile, PrRiskReport, ScanOptions, StripeReport, SupabaseReport } from "./types.js";
|
|
9
10
|
export type { ScanContext, ScanInput } from "./context.js";
|
|
11
|
+
export type { GuardConfig, RuleConfigValue } from "./config.js";
|
|
10
12
|
export type { RuleMetadata, RuleStability } from "./rules/catalog.js";
|
package/dist/index.js
CHANGED
|
@@ -3,5 +3,6 @@ export { checkStripe } from "./commands/checkStripe.js";
|
|
|
3
3
|
export { checkSupabase } from "./commands/checkSupabase.js";
|
|
4
4
|
export { checkMcp } from "./commands/checkMcp.js";
|
|
5
5
|
export { classifyPrRisk } from "./commands/prRisk.js";
|
|
6
|
+
export { applyGuardConfig, defaultConfigFileName, loadGuardConfig } from "./config.js";
|
|
6
7
|
export { createScanContext } from "./context.js";
|
|
7
8
|
export { getRuleMetadata, RULE_CATALOG } from "./rules/catalog.js";
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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.3.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
|
+
config: .ai-saas-guard.json
|
|
33
|
+
format: markdown
|
|
34
|
+
output: ai-saas-guard-pr.md
|
|
35
|
+
- run: cat ai-saas-guard-pr.md >> "$GITHUB_STEP_SUMMARY"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
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.
|
|
39
|
+
|
|
40
|
+
## Project Config
|
|
41
|
+
|
|
42
|
+
The Action auto-loads `.ai-saas-guard.json` from `root` when the file exists. Use the `config` input when the policy file lives somewhere else or when you want the workflow to be explicit:
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
- uses: zr9959/ai-saas-guard@v0
|
|
46
|
+
with:
|
|
47
|
+
command: scan
|
|
48
|
+
root: ${{ github.workspace }}
|
|
49
|
+
config: .ai-saas-guard.json
|
|
50
|
+
fail-on: none
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Project config can disable noisy rules, override severity by rule ID, and set a default `failOn` threshold. A workflow `fail-on` input overrides the config threshold for that run.
|
|
54
|
+
|
|
55
|
+
## SARIF Upload
|
|
56
|
+
|
|
57
|
+
Use SARIF when you want findings to appear in GitHub code scanning alerts.
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
name: ai-saas-guard-sarif
|
|
61
|
+
|
|
62
|
+
on:
|
|
63
|
+
pull_request:
|
|
64
|
+
|
|
65
|
+
permissions:
|
|
66
|
+
contents: read
|
|
67
|
+
security-events: write
|
|
68
|
+
|
|
69
|
+
jobs:
|
|
70
|
+
code-scanning:
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
steps:
|
|
73
|
+
- uses: actions/checkout@v6.0.2
|
|
74
|
+
- uses: zr9959/ai-saas-guard@v0
|
|
75
|
+
with:
|
|
76
|
+
command: scan
|
|
77
|
+
root: ${{ github.workspace }}
|
|
78
|
+
format: sarif
|
|
79
|
+
output: ai-saas-guard.sarif
|
|
80
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
81
|
+
with:
|
|
82
|
+
sarif_file: ai-saas-guard.sarif
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
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,10 +5,10 @@
|
|
|
5
5
|
## Current State
|
|
6
6
|
|
|
7
7
|
- Package name: `ai-saas-guard`
|
|
8
|
-
- Current version: `0.
|
|
8
|
+
- Current version: `0.3.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.3.0`
|
|
12
12
|
- Publish workflow: `.github/workflows/npm-publish.yml`
|
|
13
13
|
- Trusted Publisher: GitHub Actions, `zr9959/ai-saas-guard`, workflow `npm-publish.yml`, allowed action `npm publish`
|
|
14
14
|
- Long-lived npm publish token: not required
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
Use GitHub Actions with npm Trusted Publisher/OIDC:
|
|
19
19
|
|
|
20
|
-
1. Create and review a release tag such as `v0.
|
|
20
|
+
1. Create and review a release tag such as `v0.3.0`.
|
|
21
21
|
2. Publish from the GitHub Release or run the `Publish npm` workflow manually with `ref` set to that tag.
|
|
22
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
23
|
4. Run `npm publish --access public` from the workflow. Trusted publishing automatically generates provenance for this public package from this public repository.
|
package/docs/project-handoff.md
CHANGED
|
@@ -46,6 +46,8 @@ Implemented surfaces:
|
|
|
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
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
|
|
50
|
+
- project-local `.ai-saas-guard.json` config for rule toggles, severity overrides, and default fail thresholds
|
|
49
51
|
- JSON output
|
|
50
52
|
- SARIF output
|
|
51
53
|
- composite GitHub Action wrapper
|
|
@@ -98,13 +100,8 @@ GitHub Project:
|
|
|
98
100
|
Current issue set:
|
|
99
101
|
|
|
100
102
|
- #1 Add launch-readiness checklist content
|
|
101
|
-
- #2 Add GitHub Action release packaging
|
|
102
|
-
- #3 Add configurable rule severity and rule toggles
|
|
103
|
-
- #4 Add PR comment summary mode
|
|
104
103
|
- #5 Write Stripe webhook replay cookbook
|
|
105
|
-
- #6 Add SARIF upload workflow example
|
|
106
104
|
- #7 Expand Supabase RLS fixtures and ownership patterns
|
|
107
|
-
- #8 Publish ai-saas-guard to npm
|
|
108
105
|
|
|
109
106
|
CI:
|
|
110
107
|
|
|
@@ -116,7 +113,7 @@ CI:
|
|
|
116
113
|
Publishing:
|
|
117
114
|
|
|
118
115
|
- npm package: `ai-saas-guard`
|
|
119
|
-
- Current release line: `v0.
|
|
116
|
+
- Current release line: `v0.3.0`
|
|
120
117
|
- Publish workflow: `.github/workflows/npm-publish.yml`
|
|
121
118
|
- Trusted Publisher: GitHub Actions for `zr9959/ai-saas-guard`, workflow `npm-publish.yml`
|
|
122
119
|
- Long-lived npm publish tokens should not be required.
|
|
@@ -143,13 +140,11 @@ Not allowed:
|
|
|
143
140
|
|
|
144
141
|
Recommended order:
|
|
145
142
|
|
|
146
|
-
1.
|
|
147
|
-
2.
|
|
148
|
-
3.
|
|
149
|
-
4.
|
|
150
|
-
5. Add
|
|
151
|
-
6. Improve false-positive suppression and rule stability labels.
|
|
152
|
-
7. Add a GitHub App design note for the potential hosted layer.
|
|
143
|
+
1. Expand Supabase RLS fixtures and ownership patterns.
|
|
144
|
+
2. Write Stripe webhook replay cookbook.
|
|
145
|
+
3. Add launch-readiness checklist content.
|
|
146
|
+
4. Improve false-positive suppression and rule stability labels.
|
|
147
|
+
5. Add a GitHub App design note for the potential hosted layer.
|
|
153
148
|
|
|
154
149
|
For every feature, keep the scanner evidence-first:
|
|
155
150
|
|