ai-saas-guard 0.2.0 → 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 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.2.0`, `v0` |
54
- | npm package | `ai-saas-guard@0.2.0` |
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
@@ -77,6 +78,7 @@ Machine-readable output:
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
79
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
80
82
  npx ai-saas-guard@latest scan --root /path/to/your-saas --fail-on high
81
83
  ```
82
84
 
@@ -168,9 +170,28 @@ If `--base` cannot be resolved, `pr-risk` emits `pr-risk.diff-unavailable` inste
168
170
  | `check-stripe` | Inspect webhook handlers and billing lifecycle coverage |
169
171
  | `check-mcp` | Inventory MCP configs and classify side effects |
170
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
+
171
192
  ## GitHub Action
172
193
 
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:
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:
174
195
 
175
196
  ```yaml
176
197
  name: ai-saas-guard
@@ -194,6 +215,7 @@ jobs:
194
215
  root: ${{ github.workspace }}
195
216
  base: origin/main
196
217
  fail-on: high
218
+ config: .ai-saas-guard.json
197
219
  ```
198
220
 
199
221
  For SARIF upload:
@@ -298,10 +320,10 @@ Open-source core:
298
320
 
299
321
  Near-term priorities:
300
322
 
301
- - configurable severity and rule toggles
302
323
  - expanded Supabase RLS fixtures
303
324
  - Stripe webhook replay cookbook
304
325
  - launch-readiness checklist content
326
+ - false-positive suppression and rule stability labels
305
327
 
306
328
  Potential paid layer later:
307
329
 
package/action.yml CHANGED
@@ -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
@@ -95,6 +100,10 @@ runs:
95
100
  args+=("--base" "${INPUT_BASE}")
96
101
  fi
97
102
 
103
+ if [ -n "${INPUT_CONFIG}" ]; then
104
+ args+=("--config" "${INPUT_CONFIG}")
105
+ fi
106
+
98
107
  if [ -n "${INPUT_OUTPUT}" ]; then
99
108
  node "${GITHUB_ACTION_PATH}/dist/cli.js" "${args[@]}" | tee -- "${INPUT_OUTPUT}"
100
109
  else
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
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";
5
6
  import { formatMarkdownReport } from "./report/markdown.js";
@@ -11,6 +12,7 @@ async function main(argv) {
11
12
  process.stdout.write(helpText());
12
13
  return 0;
13
14
  }
15
+ const config = await loadGuardConfig(args.rootDir, args.configPath);
14
16
  let report;
15
17
  switch (args.command) {
16
18
  case "scan":
@@ -32,9 +34,11 @@ async function main(argv) {
32
34
  process.stderr.write(`Unknown command: ${String(args.command)}\n\n${helpText()}`);
33
35
  return 2;
34
36
  }
37
+ report = applyGuardConfig(report, config);
35
38
  process.stdout.write(formatReport(report, args.format));
36
- if (shouldFail(report, args.failOn)) {
37
- process.stderr.write(`Failing because findings met --fail-on ${args.failOn}\n`);
39
+ const failOn = args.failOn ?? config.failOn;
40
+ if (shouldFail(report, failOn)) {
41
+ process.stderr.write(`Failing because findings met --fail-on ${failOn}\n`);
38
42
  return 1;
39
43
  }
40
44
  return 0;
@@ -87,6 +91,14 @@ function parseArgs(argv) {
87
91
  index += 1;
88
92
  continue;
89
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
+ }
90
102
  if (arg === "--base") {
91
103
  const value = argv[index + 1];
92
104
  if (!value)
@@ -141,11 +153,11 @@ function helpText() {
141
153
  Repo-local launch-readiness scanner for AI-built SaaS apps.
142
154
 
143
155
  Usage:
144
- ai-saas-guard scan [--root <repo>] [--json|--sarif] [--fail-on <severity>]
145
- ai-saas-guard check-supabase [--root <repo>] [--json|--sarif] [--fail-on <severity>]
146
- ai-saas-guard check-stripe [--root <repo>] [--json|--sarif] [--fail-on <severity>]
147
- ai-saas-guard check-mcp [--root <repo>] [--json|--sarif] [--fail-on <severity>]
148
- ai-saas-guard pr-risk [--root <repo>] [--base <branch>] [--json|--sarif|--markdown] [--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>]
149
161
 
150
162
  Defaults:
151
163
  - read-only
@@ -154,6 +166,7 @@ Defaults:
154
166
  - terminal output by default, JSON with --json
155
167
  - SARIF output for GitHub code scanning with --sarif
156
168
  - PR-focused markdown summary with --markdown
169
+ - project config auto-loaded from .ai-saas-guard.json when present
157
170
  `;
158
171
  }
159
172
  main(process.argv.slice(2)).then((code) => {
@@ -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";
@@ -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.2.0` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
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
6
 
7
7
  ## PR Summary
8
8
 
@@ -29,6 +29,7 @@ jobs:
29
29
  command: pr-risk
30
30
  root: ${{ github.workspace }}
31
31
  base: origin/main
32
+ config: .ai-saas-guard.json
32
33
  format: markdown
33
34
  output: ai-saas-guard-pr.md
34
35
  - run: cat ai-saas-guard-pr.md >> "$GITHUB_STEP_SUMMARY"
@@ -36,6 +37,21 @@ jobs:
36
37
 
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.
38
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
+
39
55
  ## SARIF Upload
40
56
 
41
57
  Use SARIF when you want findings to appear in GitHub code scanning alerts.
@@ -5,10 +5,10 @@
5
5
  ## Current State
6
6
 
7
7
  - Package name: `ai-saas-guard`
8
- - Current version: `0.2.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.2.0`
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.2.0`.
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.
@@ -47,6 +47,7 @@ Implemented surfaces:
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
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
50
51
  - JSON output
51
52
  - SARIF output
52
53
  - composite GitHub Action wrapper
@@ -99,7 +100,6 @@ GitHub Project:
99
100
  Current issue set:
100
101
 
101
102
  - #1 Add launch-readiness checklist content
102
- - #3 Add configurable rule severity and rule toggles
103
103
  - #5 Write Stripe webhook replay cookbook
104
104
  - #7 Expand Supabase RLS fixtures and ownership patterns
105
105
 
@@ -113,7 +113,7 @@ CI:
113
113
  Publishing:
114
114
 
115
115
  - npm package: `ai-saas-guard`
116
- - Current release line: `v0.2.0`
116
+ - Current release line: `v0.3.0`
117
117
  - Publish workflow: `.github/workflows/npm-publish.yml`
118
118
  - Trusted Publisher: GitHub Actions for `zr9959/ai-saas-guard`, workflow `npm-publish.yml`
119
119
  - Long-lived npm publish tokens should not be required.
@@ -140,9 +140,9 @@ Not allowed:
140
140
 
141
141
  Recommended order:
142
142
 
143
- 1. Add configurable severity and rule toggles.
144
- 2. Expand Supabase RLS fixtures and ownership patterns.
145
- 3. Write Stripe webhook replay cookbook.
143
+ 1. Expand Supabase RLS fixtures and ownership patterns.
144
+ 2. Write Stripe webhook replay cookbook.
145
+ 3. Add launch-readiness checklist content.
146
146
  4. Improve false-positive suppression and rule stability labels.
147
147
  5. Add a GitHub App design note for the potential hosted layer.
148
148
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-saas-guard",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Repo-local launch-readiness scanner for AI-built SaaS apps.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/zr9959/ai-saas-guard#readme",