ai-saas-guard 0.6.0 → 0.7.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 +13 -5
- package/dist/config.d.ts +6 -0
- package/dist/config.js +78 -6
- package/dist/index.d.ts +1 -1
- package/dist/report/sarif.js +6 -1
- package/dist/rules/catalog.js +12 -12
- package/docs/github-action.md +2 -2
- package/docs/npm-publishing.md +3 -3
- package/docs/project-handoff.md +5 -5
- package/docs/rules.md +31 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,8 +51,8 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
|
|
|
51
51
|
| JSON and SARIF output | Available |
|
|
52
52
|
| Composite GitHub Action | Available |
|
|
53
53
|
| Project config | `.ai-saas-guard.json` rule toggles, severity overrides, and fail thresholds |
|
|
54
|
-
| Versioned Action tags | `v0.
|
|
55
|
-
| npm package | `ai-saas-guard@0.
|
|
54
|
+
| Versioned Action tags | `v0.7.0`, `v0` |
|
|
55
|
+
| npm package | `ai-saas-guard@0.7.0` |
|
|
56
56
|
| npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
|
|
57
57
|
|
|
58
58
|
## Quick Start
|
|
@@ -189,17 +189,26 @@ Add `.ai-saas-guard.json` at the repository root to tune findings without changi
|
|
|
189
189
|
"stripe.webhook.missing-signature": "off",
|
|
190
190
|
"stripe.webhook.missing-idempotency": "critical",
|
|
191
191
|
"deploy.env.example-missing": "info"
|
|
192
|
-
}
|
|
192
|
+
},
|
|
193
|
+
"suppressions": [
|
|
194
|
+
{
|
|
195
|
+
"ruleId": "stripe.webhook.missing-idempotency",
|
|
196
|
+
"paths": ["app/api/stripe/webhook/route.ts"],
|
|
197
|
+
"reason": "Temporary launch exception with duplicate-event coverage in integration tests."
|
|
198
|
+
}
|
|
199
|
+
]
|
|
193
200
|
}
|
|
194
201
|
```
|
|
195
202
|
|
|
196
203
|
`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.
|
|
197
204
|
|
|
205
|
+
Use `suppressions` for narrower false-positive handling when one rule is noisy only for specific generated files, fixtures, or reviewed exceptions. Each suppression must name a known `ruleId` and one or more relative `paths` globs, such as `generated/**` or `app/api/stripe/webhook/route.ts`.
|
|
206
|
+
|
|
198
207
|
`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.
|
|
199
208
|
|
|
200
209
|
## GitHub Action
|
|
201
210
|
|
|
202
|
-
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.
|
|
211
|
+
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.7.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
|
|
203
212
|
|
|
204
213
|
```yaml
|
|
205
214
|
name: ai-saas-guard
|
|
@@ -328,7 +337,6 @@ Open-source core:
|
|
|
328
337
|
|
|
329
338
|
Near-term priorities:
|
|
330
339
|
|
|
331
|
-
- false-positive suppression and rule stability labels
|
|
332
340
|
- GitHub App design note for the potential hosted layer
|
|
333
341
|
|
|
334
342
|
Potential paid layer later:
|
package/dist/config.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { BaseReport, Severity } from "./types.js";
|
|
2
2
|
export declare const defaultConfigFileName = ".ai-saas-guard.json";
|
|
3
3
|
export type RuleConfigValue = "off" | Severity;
|
|
4
|
+
export interface FindingSuppression {
|
|
5
|
+
ruleId: string;
|
|
6
|
+
paths: string[];
|
|
7
|
+
reason?: string;
|
|
8
|
+
}
|
|
4
9
|
export interface GuardConfig {
|
|
5
10
|
sourcePath?: string;
|
|
6
11
|
failOn?: Severity | "none";
|
|
7
12
|
rules: Record<string, RuleConfigValue>;
|
|
13
|
+
suppressions?: FindingSuppression[];
|
|
8
14
|
}
|
|
9
15
|
export declare function loadGuardConfig(rootDir: string, explicitPath?: string): Promise<GuardConfig>;
|
|
10
16
|
export declare function applyGuardConfig<T extends BaseReport>(report: T, config: GuardConfig): T;
|
package/dist/config.js
CHANGED
|
@@ -11,22 +11,22 @@ export async function loadGuardConfig(rootDir, explicitPath) {
|
|
|
11
11
|
}
|
|
12
12
|
catch (error) {
|
|
13
13
|
if (!explicitPath && isNotFoundError(error))
|
|
14
|
-
return { rules: {} };
|
|
14
|
+
return { rules: {}, suppressions: [] };
|
|
15
15
|
throw new Error(`Could not read config file ${sourcePath}: ${errorMessage(error)}`);
|
|
16
16
|
}
|
|
17
17
|
return parseGuardConfig(content, sourcePath);
|
|
18
18
|
}
|
|
19
19
|
export function applyGuardConfig(report, config) {
|
|
20
20
|
const configuredRuleIds = Object.keys(config.rules);
|
|
21
|
-
|
|
21
|
+
const suppressions = config.suppressions ?? [];
|
|
22
|
+
if (configuredRuleIds.length === 0 && suppressions.length === 0)
|
|
22
23
|
return report;
|
|
23
24
|
const findings = sortFindings(report.findings.flatMap((finding) => {
|
|
24
25
|
const ruleConfig = config.rules[finding.ruleId];
|
|
25
|
-
if (!ruleConfig)
|
|
26
|
-
return [finding];
|
|
27
26
|
if (ruleConfig === "off")
|
|
28
27
|
return [];
|
|
29
|
-
|
|
28
|
+
const configuredFinding = ruleConfig ? { ...finding, severity: ruleConfig } : finding;
|
|
29
|
+
return isSuppressed(configuredFinding, suppressions) ? [] : [configuredFinding];
|
|
30
30
|
}));
|
|
31
31
|
return {
|
|
32
32
|
...report,
|
|
@@ -63,12 +63,84 @@ function parseGuardConfig(content, sourcePath) {
|
|
|
63
63
|
}
|
|
64
64
|
rules[ruleId] = value;
|
|
65
65
|
}
|
|
66
|
+
const rawSuppressions = parsed.suppressions ?? [];
|
|
67
|
+
if (!Array.isArray(rawSuppressions)) {
|
|
68
|
+
throw new Error("Invalid config suppressions: expected an array");
|
|
69
|
+
}
|
|
70
|
+
const suppressions = rawSuppressions.map((value, index) => parseSuppression(value, index));
|
|
66
71
|
return {
|
|
67
72
|
sourcePath,
|
|
68
73
|
failOn,
|
|
69
|
-
rules
|
|
74
|
+
rules,
|
|
75
|
+
suppressions
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function parseSuppression(value, index) {
|
|
79
|
+
if (!isPlainObject(value)) {
|
|
80
|
+
throw new Error(`Invalid config suppressions[${index}]: expected an object`);
|
|
81
|
+
}
|
|
82
|
+
const ruleId = value.ruleId;
|
|
83
|
+
if (typeof ruleId !== "string" || ruleId.length === 0) {
|
|
84
|
+
throw new Error(`Invalid config suppressions[${index}].ruleId: expected a rule ID`);
|
|
85
|
+
}
|
|
86
|
+
if (!getRuleMetadata(ruleId)) {
|
|
87
|
+
throw new Error(`Unknown rule ID in suppression: ${ruleId}`);
|
|
88
|
+
}
|
|
89
|
+
const paths = value.paths;
|
|
90
|
+
if (!Array.isArray(paths) || paths.length === 0 || !paths.every((path) => typeof path === "string" && path.length > 0)) {
|
|
91
|
+
throw new Error(`Invalid config suppressions[${index}].paths: expected a non-empty array of path globs`);
|
|
92
|
+
}
|
|
93
|
+
const reason = value.reason;
|
|
94
|
+
if (reason !== undefined && typeof reason !== "string") {
|
|
95
|
+
throw new Error(`Invalid config suppressions[${index}].reason: expected a string`);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
ruleId,
|
|
99
|
+
paths,
|
|
100
|
+
reason
|
|
70
101
|
};
|
|
71
102
|
}
|
|
103
|
+
function isSuppressed(finding, suppressions) {
|
|
104
|
+
return suppressions.some((suppression) => suppression.ruleId === finding.ruleId &&
|
|
105
|
+
finding.evidence.some((evidence) => suppression.paths.some((pattern) => pathMatches(pattern, evidence.file))));
|
|
106
|
+
}
|
|
107
|
+
function pathMatches(pattern, filePath) {
|
|
108
|
+
return globToRegExp(normalizeConfigPath(pattern)).test(normalizeConfigPath(filePath));
|
|
109
|
+
}
|
|
110
|
+
function normalizeConfigPath(path) {
|
|
111
|
+
return path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
112
|
+
}
|
|
113
|
+
function globToRegExp(pattern) {
|
|
114
|
+
let expression = "^";
|
|
115
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
116
|
+
const char = pattern[index];
|
|
117
|
+
if (char === "*") {
|
|
118
|
+
const next = pattern[index + 1];
|
|
119
|
+
const afterNext = pattern[index + 2];
|
|
120
|
+
if (next === "*" && afterNext === "/") {
|
|
121
|
+
expression += "(?:.*/)?";
|
|
122
|
+
index += 2;
|
|
123
|
+
}
|
|
124
|
+
else if (next === "*") {
|
|
125
|
+
expression += ".*";
|
|
126
|
+
index += 1;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
expression += "[^/]*";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (char === "?") {
|
|
133
|
+
expression += "[^/]";
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
expression += escapeRegExp(char);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return new RegExp(`${expression}$`);
|
|
140
|
+
}
|
|
141
|
+
function escapeRegExp(value) {
|
|
142
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
143
|
+
}
|
|
72
144
|
function isRuleConfigValue(value) {
|
|
73
145
|
return value === "off" || isSeverity(value);
|
|
74
146
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,5 +8,5 @@ export { createScanContext } from "./context.js";
|
|
|
8
8
|
export { getRuleMetadata, RULE_CATALOG } from "./rules/catalog.js";
|
|
9
9
|
export type { BaseReport, CommandName, Evidence, Finding, McpReport, McpServerInventory, PrRiskFile, PrRiskReport, ScanOptions, StripeReport, SupabaseReport } from "./types.js";
|
|
10
10
|
export type { ScanContext, ScanInput } from "./context.js";
|
|
11
|
-
export type { GuardConfig, RuleConfigValue } from "./config.js";
|
|
11
|
+
export type { FindingSuppression, GuardConfig, RuleConfigValue } from "./config.js";
|
|
12
12
|
export type { RuleMetadata, RuleStability } from "./rules/catalog.js";
|
package/dist/report/sarif.js
CHANGED
|
@@ -5,13 +5,18 @@ export function formatSarifReport(report) {
|
|
|
5
5
|
if (rules.has(finding.ruleId))
|
|
6
6
|
continue;
|
|
7
7
|
const metadata = getRuleMetadata(finding.ruleId);
|
|
8
|
+
const stability = metadata?.stability ?? "default";
|
|
8
9
|
rules.set(finding.ruleId, {
|
|
9
10
|
id: finding.ruleId,
|
|
10
11
|
name: finding.ruleId,
|
|
11
12
|
shortDescription: { text: metadata?.title ?? finding.title },
|
|
12
13
|
fullDescription: { text: metadata?.why ?? finding.why },
|
|
13
14
|
help: { text: `${finding.suggestedVerification}\n\nFix direction: ${finding.suggestedFix}` },
|
|
14
|
-
defaultConfiguration: { level: sarifLevel(finding) }
|
|
15
|
+
defaultConfiguration: { level: sarifLevel(finding) },
|
|
16
|
+
properties: {
|
|
17
|
+
"ai-saas-guard/stability": stability,
|
|
18
|
+
tags: [`stability:${stability}`]
|
|
19
|
+
}
|
|
15
20
|
});
|
|
16
21
|
}
|
|
17
22
|
return `${JSON.stringify({
|
package/dist/rules/catalog.js
CHANGED
|
@@ -4,14 +4,14 @@ export const RULE_CATALOG = {
|
|
|
4
4
|
severity: "high",
|
|
5
5
|
title: "Secret-like value detected",
|
|
6
6
|
why: "Credentials committed to source, config, or examples can be exposed before launch.",
|
|
7
|
-
stability: "
|
|
7
|
+
stability: "strict"
|
|
8
8
|
},
|
|
9
9
|
"next.env.public-secret": {
|
|
10
10
|
ruleId: "next.env.public-secret",
|
|
11
11
|
severity: "high",
|
|
12
12
|
title: "Risky NEXT_PUBLIC environment variable",
|
|
13
13
|
why: "Next.js exposes NEXT_PUBLIC variables to browser code, so secret-like values can leak to users.",
|
|
14
|
-
stability: "
|
|
14
|
+
stability: "strict"
|
|
15
15
|
},
|
|
16
16
|
"stripe.webhook.missing-route": {
|
|
17
17
|
ruleId: "stripe.webhook.missing-route",
|
|
@@ -25,7 +25,7 @@ export const RULE_CATALOG = {
|
|
|
25
25
|
severity: "critical",
|
|
26
26
|
title: "Stripe webhook does not verify the Stripe signature",
|
|
27
27
|
why: "Unsigned webhook handlers can accept forged billing events.",
|
|
28
|
-
stability: "
|
|
28
|
+
stability: "strict"
|
|
29
29
|
},
|
|
30
30
|
"stripe.webhook.raw-body-risk": {
|
|
31
31
|
ruleId: "stripe.webhook.raw-body-risk",
|
|
@@ -39,7 +39,7 @@ export const RULE_CATALOG = {
|
|
|
39
39
|
severity: "critical",
|
|
40
40
|
title: "Stripe signing secret appears public",
|
|
41
41
|
why: "Public Stripe secrets can be bundled into client code and abused.",
|
|
42
|
-
stability: "
|
|
42
|
+
stability: "strict"
|
|
43
43
|
},
|
|
44
44
|
"stripe.webhook.missing-idempotency": {
|
|
45
45
|
ruleId: "stripe.webhook.missing-idempotency",
|
|
@@ -53,7 +53,7 @@ export const RULE_CATALOG = {
|
|
|
53
53
|
severity: "medium",
|
|
54
54
|
title: "Stripe webhook does not show an entitlement update path",
|
|
55
55
|
why: "Returning HTTP 200 is not the same as changing application access state.",
|
|
56
|
-
stability: "
|
|
56
|
+
stability: "experimental"
|
|
57
57
|
},
|
|
58
58
|
"stripe.webhook.missing-critical-event": {
|
|
59
59
|
ruleId: "stripe.webhook.missing-critical-event",
|
|
@@ -67,7 +67,7 @@ export const RULE_CATALOG = {
|
|
|
67
67
|
severity: "critical",
|
|
68
68
|
title: "Broad Supabase RLS policy",
|
|
69
69
|
why: "`USING (true)` or `WITH CHECK (true)` can turn login into broad data access or writes.",
|
|
70
|
-
stability: "
|
|
70
|
+
stability: "strict"
|
|
71
71
|
},
|
|
72
72
|
"supabase.rls.missing-ownership-filter": {
|
|
73
73
|
ruleId: "supabase.rls.missing-ownership-filter",
|
|
@@ -95,7 +95,7 @@ export const RULE_CATALOG = {
|
|
|
95
95
|
severity: "critical",
|
|
96
96
|
title: "Sensitive table does not enable row level security",
|
|
97
97
|
why: "User-data tables should enable row level security.",
|
|
98
|
-
stability: "
|
|
98
|
+
stability: "strict"
|
|
99
99
|
},
|
|
100
100
|
"supabase.storage.public-bucket": {
|
|
101
101
|
ruleId: "supabase.storage.public-bucket",
|
|
@@ -109,14 +109,14 @@ export const RULE_CATALOG = {
|
|
|
109
109
|
severity: "medium",
|
|
110
110
|
title: "Sensitive API route lacks obvious rate limiting",
|
|
111
111
|
why: "Login, checkout, upload, AI, and webhook routes are common abuse targets.",
|
|
112
|
-
stability: "
|
|
112
|
+
stability: "experimental"
|
|
113
113
|
},
|
|
114
114
|
"api.route.auth-without-ownership": {
|
|
115
115
|
ruleId: "api.route.auth-without-ownership",
|
|
116
116
|
severity: "high",
|
|
117
117
|
title: "API route checks auth but lacks an ownership guard",
|
|
118
118
|
why: "Login checks do not prove resource ownership checks.",
|
|
119
|
-
stability: "
|
|
119
|
+
stability: "experimental"
|
|
120
120
|
},
|
|
121
121
|
"deploy.next.static-export-api-risk": {
|
|
122
122
|
ruleId: "deploy.next.static-export-api-risk",
|
|
@@ -137,14 +137,14 @@ export const RULE_CATALOG = {
|
|
|
137
137
|
severity: "low",
|
|
138
138
|
title: "Important runtime env var is not documented",
|
|
139
139
|
why: "Missing env docs cause local-success, production-failure deploys.",
|
|
140
|
-
stability: "
|
|
140
|
+
stability: "experimental"
|
|
141
141
|
},
|
|
142
142
|
"mcp.config.invalid-json": {
|
|
143
143
|
ruleId: "mcp.config.invalid-json",
|
|
144
144
|
severity: "medium",
|
|
145
145
|
title: "MCP config is not valid JSON",
|
|
146
146
|
why: "Broken MCP configs hide the actual tool inventory.",
|
|
147
|
-
stability: "
|
|
147
|
+
stability: "strict"
|
|
148
148
|
},
|
|
149
149
|
"mcp.config.plaintext-secret": {
|
|
150
150
|
ruleId: "mcp.config.plaintext-secret",
|
|
@@ -200,7 +200,7 @@ export const RULE_CATALOG = {
|
|
|
200
200
|
severity: "medium",
|
|
201
201
|
title: "Review first sensitive PR surface",
|
|
202
202
|
why: "AI-generated PRs often bury trust-boundary changes inside larger diffs.",
|
|
203
|
-
stability: "
|
|
203
|
+
stability: "experimental"
|
|
204
204
|
},
|
|
205
205
|
"pr-risk.diff-unavailable": {
|
|
206
206
|
ruleId: "pr-risk.diff-unavailable",
|
package/docs/github-action.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`ai-saas-guard` ships as a composite GitHub Action for pull request and code scanning workflows.
|
|
4
4
|
|
|
5
|
-
Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.
|
|
5
|
+
Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.7.0` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
|
|
6
6
|
|
|
7
7
|
## PR Summary
|
|
8
8
|
|
|
@@ -50,7 +50,7 @@ The Action auto-loads `.ai-saas-guard.json` from `root` when the file exists. Us
|
|
|
50
50
|
fail-on: none
|
|
51
51
|
```
|
|
52
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.
|
|
53
|
+
Project config can disable noisy rules, override severity by rule ID, apply path-specific `suppressions`, and set a default `failOn` threshold. A workflow `fail-on` input overrides the config threshold for that run.
|
|
54
54
|
|
|
55
55
|
## SARIF Upload
|
|
56
56
|
|
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.7.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.7.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.7.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
|
@@ -49,7 +49,8 @@ Implemented surfaces:
|
|
|
49
49
|
- PR diff risk triage for auth, billing, RLS, env, tests removed, and large mixed diffs
|
|
50
50
|
- PR diff diagnostics when a base ref or shallow checkout prevents comparison
|
|
51
51
|
- PR-focused markdown summary output for GitHub step summaries or PR comments
|
|
52
|
-
- project-local `.ai-saas-guard.json` config for rule toggles, severity overrides, and default fail thresholds
|
|
52
|
+
- project-local `.ai-saas-guard.json` config for rule toggles, severity overrides, path-specific suppressions, and default fail thresholds
|
|
53
|
+
- rule stability labels in catalog metadata, public rule docs, and SARIF rule properties
|
|
53
54
|
- JSON output
|
|
54
55
|
- SARIF output
|
|
55
56
|
- composite GitHub Action wrapper
|
|
@@ -101,7 +102,7 @@ GitHub Project:
|
|
|
101
102
|
|
|
102
103
|
Current issue set:
|
|
103
104
|
|
|
104
|
-
- No open roadmap issues after the `v0.
|
|
105
|
+
- No open roadmap issues after the `v0.7.0` suppression and stability release.
|
|
105
106
|
|
|
106
107
|
CI:
|
|
107
108
|
|
|
@@ -113,7 +114,7 @@ CI:
|
|
|
113
114
|
Publishing:
|
|
114
115
|
|
|
115
116
|
- npm package: `ai-saas-guard`
|
|
116
|
-
- Current release line: `v0.
|
|
117
|
+
- Current release line: `v0.7.0`
|
|
117
118
|
- Publish workflow: `.github/workflows/npm-publish.yml`
|
|
118
119
|
- Trusted Publisher: GitHub Actions for `zr9959/ai-saas-guard`, workflow `npm-publish.yml`
|
|
119
120
|
- Long-lived npm publish tokens should not be required.
|
|
@@ -140,8 +141,7 @@ Not allowed:
|
|
|
140
141
|
|
|
141
142
|
Recommended order:
|
|
142
143
|
|
|
143
|
-
1.
|
|
144
|
-
2. Add a GitHub App design note for the potential hosted layer.
|
|
144
|
+
1. Add a GitHub App design note for the potential hosted layer.
|
|
145
145
|
|
|
146
146
|
For every feature, keep the scanner evidence-first:
|
|
147
147
|
|
package/docs/rules.md
CHANGED
|
@@ -2,7 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
`ai-saas-guard` rules are deterministic heuristics. They are designed to produce a focused verification queue, not a complete vulnerability list.
|
|
4
4
|
|
|
5
|
-
Rule metadata is centralized in `src/rules/catalog.ts` and covered by tests so SARIF output, public docs, and
|
|
5
|
+
Rule metadata is centralized in `src/rules/catalog.ts` and covered by tests so SARIF output, public docs, and config work can share stable rule IDs, default severities, and stability labels.
|
|
6
|
+
|
|
7
|
+
## Stability Labels
|
|
8
|
+
|
|
9
|
+
Stability labels describe how much confidence reviewers should place in a finding before manual verification. They are not severity levels.
|
|
10
|
+
|
|
11
|
+
| Stability | Meaning | Examples |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| Strict | High-confidence evidence that should rarely be suppressed without a written reason. | Committed secret-like values, public Stripe secrets, unsigned Stripe webhooks, broad Supabase RLS policies. |
|
|
14
|
+
| Default | Routine launch-readiness heuristic with useful evidence and an expected manual verification step. | Missing Stripe lifecycle events, weak Supabase ownership checks, MCP side-effect inventory. |
|
|
15
|
+
| Experimental | Higher-noise heuristic meant to prioritize review, not prove a defect. | API ownership hints, rate-limit hints, missing env docs, PR risk triage. |
|
|
16
|
+
|
|
17
|
+
SARIF output includes the rule stability in `properties["ai-saas-guard/stability"]` and a `stability:<level>` tag for code scanning consumers.
|
|
18
|
+
|
|
19
|
+
## Suppressing False Positives
|
|
20
|
+
|
|
21
|
+
Prefer fixing risky code over suppressing findings. When a finding is a reviewed false positive for a specific generated file, fixture, or documented launch exception, use path-specific `suppressions` in `.ai-saas-guard.json` instead of disabling the whole rule:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"suppressions": [
|
|
26
|
+
{
|
|
27
|
+
"ruleId": "stripe.webhook.missing-idempotency",
|
|
28
|
+
"paths": ["app/api/stripe/webhook/route.ts"],
|
|
29
|
+
"reason": "Temporary exception; duplicate-event behavior is covered by integration tests."
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`paths` are relative globs. Examples: `generated/**`, `tests/fixtures/**`, and `app/api/stripe/webhook/route.ts`.
|
|
6
36
|
|
|
7
37
|
## Secrets And Public Env
|
|
8
38
|
|