ai-saas-guard 0.34.0 → 0.35.1
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 +47 -6
- package/dist/hosted/contracts.d.ts +49 -0
- package/dist/hosted/contracts.js +53 -0
- package/dist/hosted/worker.d.ts +35 -0
- package/dist/hosted/worker.js +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/performance.d.ts +21 -0
- package/dist/performance.js +23 -0
- package/docs/README.zh-CN.md +42 -5
- package/docs/case-study-ai-saas.md +21 -0
- package/docs/hosted-preimplementation-contracts.md +28 -0
- package/docs/npm-publishing.md +3 -3
- package/docs/v0.36-roadmap.md +38 -0
- package/examples/case-study-ai-saas/.github/workflows/ci.yml +19 -0
- package/examples/case-study-ai-saas/README.md +13 -0
- package/examples/case-study-ai-saas/app/api/billing/checkout/route.ts +11 -0
- package/examples/case-study-ai-saas/app/api/projects/[projectId]/route.ts +8 -0
- package/examples/case-study-ai-saas/app/api/stripe/webhook/route.ts +13 -0
- package/examples/case-study-ai-saas/next.config.js +10 -0
- package/examples/case-study-ai-saas/package.json +12 -0
- package/examples/case-study-ai-saas/supabase/migrations/001_projects.sql +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,19 @@
|
|
|
31
31
|
|
|
32
32
|
AI-built SaaS can look ready before it is ready: login works, checkout opens, the dashboard loads, and tests are green. The launch risk is usually hidden in trust-boundary code that decides who gets access, who pays, what data they can see, and whether failures are visible.
|
|
33
33
|
|
|
34
|
-
Start with the 30-second copy-paste demo: `npx ai-saas-guard@latest demo --summary`. No signup, no code upload, no LLM call. See the
|
|
34
|
+
Start with the 30-second copy-paste demo: `npx ai-saas-guard@latest demo --summary`. No signup, no code upload, no LLM call. See the saved output and [compare with alternatives](docs/launch-gate-positioning.md). Then run the same launch gate against your repo in about three minutes:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas --summary
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The output is meant to answer three practical questions before you invite users:
|
|
41
|
+
|
|
42
|
+
- **Can a real user get access they should not have?** Check auth, tenant ownership, Supabase RLS, and Stripe entitlement paths first.
|
|
43
|
+
- **Can the app claim success when something failed?** Check swallowed errors, fake success responses, hardcoded fallback data, and skipped tests.
|
|
44
|
+
- **Can launch infrastructure do too much damage?** Check exposed env vars, overpowered workflows, MCP tools, deploy gaps, and missing request evidence.
|
|
45
|
+
|
|
46
|
+
See the [terminal screenshot](docs/demo-terminal-screenshot.svg), [saved output](docs/demo-terminal-output.txt), and the [30-second cold-start review](docs/cold-start-review.md).
|
|
35
47
|
|
|
36
48
|
These are the failures that hurt after real users arrive:
|
|
37
49
|
|
|
@@ -46,6 +58,18 @@ These are the failures that hurt after real users arrive:
|
|
|
46
58
|
|
|
47
59
|
`ai-saas-guard` gives you a short local review queue for those risks. It does not prove the app is secure, certify a release, or replace human review. It tells founders, solo builders, small teams, and reviewers what deserves attention first.
|
|
48
60
|
|
|
61
|
+
## What To Do With The Result
|
|
62
|
+
|
|
63
|
+
Treat the report as a launch review queue, not a scorecard. Fix or manually prove the highest trust-boundary findings before spending time on low-severity hygiene.
|
|
64
|
+
|
|
65
|
+
| If you see | Do this first |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| Critical/high auth, billing, RLS, tenant, webhook, or silent-success findings | Reproduce the manual proof step in staging and confirm the path fails closed |
|
|
68
|
+
| Medium deploy, env, request ID, MCP, or Actions hygiene findings | Decide whether the launch path needs the control now or can be tracked after critical paths are closed |
|
|
69
|
+
| Low/info hints | Clean them up after the user-access, payment, and data-access paths are understood |
|
|
70
|
+
|
|
71
|
+
For a realistic risky app, scan [examples/case-study-ai-saas](examples/case-study-ai-saas) or read [docs/case-study-ai-saas.md](docs/case-study-ai-saas.md).
|
|
72
|
+
|
|
49
73
|
## 30-Second Copy-Paste Demo
|
|
50
74
|
|
|
51
75
|
No signup, no code upload, no LLM call:
|
|
@@ -138,6 +162,14 @@ One command returns a launch-readiness report with:
|
|
|
138
162
|
|
|
139
163
|
For a concise comparison with Semgrep, zizmor, OpenSSF Scorecard, Snyk, and GitHub code scanning, see [docs/launch-gate-positioning.md](docs/launch-gate-positioning.md).
|
|
140
164
|
|
|
165
|
+
## Three Ways To Use It
|
|
166
|
+
|
|
167
|
+
| Path | Best for | Status |
|
|
168
|
+
| --- | --- | --- |
|
|
169
|
+
| Local CLI | Private code, first local launch review, founder or reviewer workflow | Published on npm; local-first, read-only, no code upload, no LLM calls |
|
|
170
|
+
| GitHub Action | CI review queue, SARIF upload, PR summary artifacts, controlled fail thresholds | Available through `zr9959/ai-saas-guard@v0` and fixed version tags |
|
|
171
|
+
| Hosted GitHub App | Future hosted Check Run experience for selected repositories | Limited trial gate only; not the complete hosted SaaS, not a public hosted scanner |
|
|
172
|
+
|
|
141
173
|
## Quick Start
|
|
142
174
|
|
|
143
175
|
Run the published CLI without installing it globally:
|
|
@@ -199,18 +231,19 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
|
|
|
199
231
|
| Area | Status |
|
|
200
232
|
| --- | --- |
|
|
201
233
|
| Public GitHub repository | Available |
|
|
202
|
-
| npm CLI | `ai-saas-guard@0.
|
|
203
|
-
| GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.
|
|
234
|
+
| npm CLI | `ai-saas-guard@0.35.1` |
|
|
235
|
+
| GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.35.1` |
|
|
204
236
|
| Outputs | Short summary, terminal, JSON, SARIF, and PR-focused markdown |
|
|
205
237
|
| Project config | `.ai-saas-guard.json` rule toggles, severity overrides, suppressions, and fail thresholds |
|
|
206
238
|
| Privacy model | Local-first, read-only scan commands, no LLM calls, no code upload |
|
|
207
|
-
| Versioned Action tags | `v0.
|
|
208
|
-
| Current release | `0.
|
|
239
|
+
| Versioned Action tags | `v0.35.1`, `v0` |
|
|
240
|
+
| Current release | `0.35.1` updates the case-study fixture to a patched Next.js release so GitHub Dependabot no longer reports known Next.js advisories for the packaged example |
|
|
209
241
|
| npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
|
|
210
242
|
| Repository trust hardening | Strict branch protection, Dependabot, CodeQL, fast-check fuzzing, signed release provenance assets, private vulnerability reporting, secret scanning, and push protection |
|
|
211
243
|
| Cloudflare hosted ingress | Deployed at `https://ai-saas-guard-hosted.zr9959.workers.dev`; signed GitHub App webhook delivery and compact Check Run smoke now pass in staging |
|
|
212
244
|
| Hosted GitHub App staging | Private App `ai-saas-guard-hosted` (`3834787`) installed on `zr9959/ai-saas-guard`; hosted operations evidence is in [docs/hosted-operations-evidence.md](docs/hosted-operations-evidence.md) |
|
|
213
245
|
| OpenSSF Best Practices | Passing badge, project `12955`; `.bestpractices.json` remains the conservative evidence record |
|
|
246
|
+
| Next roadmap | v0.36.0 plan is tracked in [docs/v0.36-roadmap.md](docs/v0.36-roadmap.md) |
|
|
214
247
|
|
|
215
248
|
## Example Finding
|
|
216
249
|
|
|
@@ -318,6 +351,8 @@ The hosted production adapter layer is documented in [docs/hosted-production-ada
|
|
|
318
351
|
|
|
319
352
|
The hosted read-only checkout worker is exported from `ai-saas-guard/hosted/worker`. It creates a temporary checkout from trusted GitHub App identity, uses a runtime installation token only through git askpass, removes askpass material before the CLI phase, rejects mutated command/checkout/token-scope plans, runs the fixed `ai-saas-guard pr-risk --json` command with bounded timeout/output, converts CLI JSON into compact findings, and deletes the checkout after success or failure. It does not return source, diffs, secrets, checkout paths, PR-authored commands, or installation tokens.
|
|
320
353
|
|
|
354
|
+
The read-only checkout scan gate is exported as `evaluateHostedReadOnlyCheckoutScanGate` from `ai-saas-guard/hosted/worker`. It records whether the real worker path observed trusted git stages, CLI scan, compact report storage, Check Run publication, checkout cleanup, token removal before CLI, and bounded timeout/output settings before a hosted trial can proceed.
|
|
355
|
+
|
|
321
356
|
The hosted Node/container app skeleton is documented in [docs/hosted-node-container-app.md](docs/hosted-node-container-app.md). It exports `createHostedHttpApp`, `createInMemoryHostedAppPlatform`, `createHostedNodeCheckoutAppPlatform`, and `planHostedNodeContainerDeployment` from `ai-saas-guard/hosted/app`. It adds a safe `/healthz` route, signed `/github/webhook` ingress, one-job worker tick, in-memory provider adapters for tests, a concrete read-only checkout worker composition with visible timeout/output safety budgets, and deployment-plan validation for secret manager, queue, compact report store, worker sandbox, and GitHub Checks publisher references. It still does not deploy or expose a public hosted service by itself.
|
|
322
357
|
|
|
323
358
|
The hosted staging deployment planner is documented in [docs/hosted-staging-deployment.md](docs/hosted-staging-deployment.md). It exports `planHostedProviderBinding`, `planHostedStagingDeployment`, and `planHostedGitHubAppPromotion` from `ai-saas-guard/hosted/staging`. It composes real provider references, the Node/container deployment plan, hosted operational release-gate evidence, and GitHub App deployment planning so staging and production promotion stay blocked until the required queue, store, worker sandbox, Check Run publisher, logs, metrics, rollback, and incident-response references are present. It still does not call a cloud provider, create a GitHub App, or expose a public hosted service by itself.
|
|
@@ -336,8 +371,14 @@ Hosted pricing and packaging boundaries are documented in [docs/hosted-pricing-p
|
|
|
336
371
|
|
|
337
372
|
Hosted pre-implementation pure contracts are documented in [docs/hosted-preimplementation-contracts.md](docs/hosted-preimplementation-contracts.md). They now include a pull request webhook intake planner that verifies signatures before parsing or queueing, a durable scan queue planner that reuses queued, running, and completed jobs for the same trusted scan key, a worker read-only scan planner that fixes the CLI command and requires repository `contents: read`, a concrete Node read-only checkout scan runner, and a Check Run publication planner that requires repository `checks: write` and builds bounded check-only payloads from compact reports. They also cover queue-safe webhook event parsing, bounded check-run summary rendering, idempotent queue cleanup planning, worker checkout cleanup planning, a retention/deletion cleanup planner, an operational release gate evaluator, the production adapter plans needed for GitHub App auth and bounded worker execution, the Node/container app skeleton needed for real provider wiring, the staging deployment planner needed before production GitHub App promotion, the local staging harness needed to rehearse webhook replay, persistence, publication, and cleanup without cloud calls, and the deployed worker staging evidence helper needed to evaluate public HTTPS health, deployed cleanup, and log-boundary evidence without storing raw hosted data. The service runtime composes these contracts behind replaceable adapters. PR comments remain a later workflow or paid hosted feature, not part of the hosted MVP contract.
|
|
338
373
|
|
|
374
|
+
The limited hosted GitHub App trial gate is exported as `createHostedGitHubAppTrialGate` from `ai-saas-guard/hosted/contracts`. It keeps trial use scoped to selected repositories and requires Check Run publication, compact report storage, worker cleanup, and safe log-boundary evidence before treating a small trial as ready. It does not claim the complete hosted SaaS is available.
|
|
375
|
+
|
|
339
376
|
A public hosted compact report schema fixture is available at [examples/hosted-compact-report.json](examples/hosted-compact-report.json). It is synthetic and public-safe: compact evidence only, no raw source, raw diffs, secrets, webhook payload bodies, customer payloads, private URLs, or worker checkout paths.
|
|
340
377
|
|
|
378
|
+
The case-study fixture in [examples/case-study-ai-saas](examples/case-study-ai-saas) and [docs/case-study-ai-saas.md](docs/case-study-ai-saas.md) shows a more realistic AI-built SaaS shape across auth-adjacent routes, billing, Stripe, Supabase, Next/Vercel, and GitHub Actions.
|
|
379
|
+
|
|
380
|
+
For large repositories, `createLocalScanResourceBudget` exposes the conservative local scan budget: bounded text files, per-file bytes, total bytes, ignored build/dependency directories, no code upload, and no LLM calls.
|
|
381
|
+
|
|
341
382
|
The proposed hosted app permission boundary is intentionally narrow: repository contents read, pull requests read, checks write, and metadata read for the first version. Optional PR comments require repository policy opt-in, and broad permissions such as administration, deployments, Actions write, and repository secrets are out of scope.
|
|
342
383
|
|
|
343
384
|
The repository also includes hosted contract helpers and runtime tests for webhook intake order, webhook verification, installation token scoping, durable queue idempotency, compact reports, retention limits, uninstall cleanup, repeated cleanup idempotency, scoped deletion planning, operational release gate blocking, provider-independent hosted service orchestration, and GitHub App deployment planning. These helpers do not deploy a public hosted service.
|
|
@@ -374,7 +415,7 @@ Use `suppressions` for narrower false-positive handling when one rule is noisy o
|
|
|
374
415
|
|
|
375
416
|
## GitHub Action
|
|
376
417
|
|
|
377
|
-
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.
|
|
418
|
+
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.35.1` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
|
|
378
419
|
|
|
379
420
|
```yaml
|
|
380
421
|
name: ai-saas-guard
|
|
@@ -453,6 +453,54 @@ export interface HostedCheckRunPublicationPlan {
|
|
|
453
453
|
modelTraining: "disabled";
|
|
454
454
|
};
|
|
455
455
|
}
|
|
456
|
+
export type HostedGitHubAppTrialGateBlockedReason = "trial_repository_limit_exceeded" | "trial_repository_not_selected" | "check_run_publication_missing" | "compact_report_missing" | "worker_cleanup_missing" | "safe_log_boundary_rejected";
|
|
457
|
+
export interface HostedGitHubAppTrialGateInput {
|
|
458
|
+
requestedAt: string;
|
|
459
|
+
appName: string;
|
|
460
|
+
installationId: number;
|
|
461
|
+
selectedRepositoryIds: number[];
|
|
462
|
+
trialRepositoryIds: number[];
|
|
463
|
+
completedCheckRuns: Array<{
|
|
464
|
+
repositoryId: number;
|
|
465
|
+
pullRequestNumber: number;
|
|
466
|
+
headSha: string;
|
|
467
|
+
conclusion: HostedCheckRunConclusion;
|
|
468
|
+
compactReportStored: boolean;
|
|
469
|
+
checkRunPublished: boolean;
|
|
470
|
+
workerCleanupVerified: boolean;
|
|
471
|
+
}>;
|
|
472
|
+
safeLogBoundary: {
|
|
473
|
+
accepted: boolean;
|
|
474
|
+
sampleCount: number;
|
|
475
|
+
blockedReasons: string[];
|
|
476
|
+
};
|
|
477
|
+
maxTrialRepositories?: number;
|
|
478
|
+
rawSource?: string;
|
|
479
|
+
rawDiff?: string;
|
|
480
|
+
secretValues?: string[];
|
|
481
|
+
customerPayload?: unknown;
|
|
482
|
+
}
|
|
483
|
+
export interface HostedGitHubAppTrialGate {
|
|
484
|
+
readyForTrial: boolean;
|
|
485
|
+
blockedReasons: HostedGitHubAppTrialGateBlockedReason[];
|
|
486
|
+
requestedAt: string;
|
|
487
|
+
scope: {
|
|
488
|
+
appName: string;
|
|
489
|
+
installationId: number;
|
|
490
|
+
trialRepositoryIds: number[];
|
|
491
|
+
maxTrialRepositories: number;
|
|
492
|
+
};
|
|
493
|
+
checkRunsReady: boolean;
|
|
494
|
+
safeLogBoundaryReady: boolean;
|
|
495
|
+
requiredNextProof: string[];
|
|
496
|
+
privacy: {
|
|
497
|
+
includesRawSource: false;
|
|
498
|
+
includesRawDiffs: false;
|
|
499
|
+
includesSecrets: false;
|
|
500
|
+
includesCustomerPayloads: false;
|
|
501
|
+
claimsCompleteHostedSaas: false;
|
|
502
|
+
};
|
|
503
|
+
}
|
|
456
504
|
export type HostedDeletionTrigger = "repository_removed" | "installation_deleted" | "repeated_cleanup";
|
|
457
505
|
export interface HostedDeletionPlanInput {
|
|
458
506
|
trigger: HostedDeletionTrigger;
|
|
@@ -690,6 +738,7 @@ export declare function resolveHostedRetentionDays(input?: {
|
|
|
690
738
|
export declare function createCompactHostedReport(input: CompactHostedReportInput): CompactHostedReport;
|
|
691
739
|
export declare function createHostedCheckRunSummary(input: HostedCheckRunSummaryInput): HostedCheckRunSummary;
|
|
692
740
|
export declare function planHostedCheckRunPublication(input: HostedCheckRunPublicationInput): HostedCheckRunPublicationPlan;
|
|
741
|
+
export declare function createHostedGitHubAppTrialGate(input: HostedGitHubAppTrialGateInput): HostedGitHubAppTrialGate;
|
|
693
742
|
export declare function getHostedDeletionIdempotencyKey(input: {
|
|
694
743
|
trigger: HostedDeletionTrigger;
|
|
695
744
|
installationId: number;
|
package/dist/hosted/contracts.js
CHANGED
|
@@ -559,6 +559,59 @@ export function planHostedCheckRunPublication(input) {
|
|
|
559
559
|
privacy: hostedCheckRunPublicationPrivacy()
|
|
560
560
|
};
|
|
561
561
|
}
|
|
562
|
+
export function createHostedGitHubAppTrialGate(input) {
|
|
563
|
+
const maxTrialRepositories = input.maxTrialRepositories ?? 3;
|
|
564
|
+
const selectedRepositoryIds = new Set(input.selectedRepositoryIds);
|
|
565
|
+
const blockedReasons = [];
|
|
566
|
+
const trialRepositoryLimitExceeded = input.trialRepositoryIds.length > maxTrialRepositories;
|
|
567
|
+
const trialRepositoryNotSelected = input.trialRepositoryIds.some((repositoryId) => !selectedRepositoryIds.has(repositoryId));
|
|
568
|
+
const matchingRuns = input.completedCheckRuns.filter((run) => input.trialRepositoryIds.includes(run.repositoryId));
|
|
569
|
+
const checkRunPublicationMissing = matchingRuns.length === 0 || matchingRuns.some((run) => !run.checkRunPublished);
|
|
570
|
+
const compactReportMissing = matchingRuns.length === 0 || matchingRuns.some((run) => !run.compactReportStored);
|
|
571
|
+
const workerCleanupMissing = matchingRuns.length === 0 || matchingRuns.some((run) => !run.workerCleanupVerified);
|
|
572
|
+
const safeLogBoundaryRejected = !input.safeLogBoundary.accepted || input.safeLogBoundary.sampleCount <= 0;
|
|
573
|
+
if (trialRepositoryLimitExceeded)
|
|
574
|
+
blockedReasons.push("trial_repository_limit_exceeded");
|
|
575
|
+
if (trialRepositoryNotSelected)
|
|
576
|
+
blockedReasons.push("trial_repository_not_selected");
|
|
577
|
+
if (checkRunPublicationMissing)
|
|
578
|
+
blockedReasons.push("check_run_publication_missing");
|
|
579
|
+
if (compactReportMissing)
|
|
580
|
+
blockedReasons.push("compact_report_missing");
|
|
581
|
+
if (workerCleanupMissing)
|
|
582
|
+
blockedReasons.push("worker_cleanup_missing");
|
|
583
|
+
if (safeLogBoundaryRejected)
|
|
584
|
+
blockedReasons.push("safe_log_boundary_rejected");
|
|
585
|
+
return {
|
|
586
|
+
readyForTrial: blockedReasons.length === 0,
|
|
587
|
+
blockedReasons,
|
|
588
|
+
requestedAt: input.requestedAt,
|
|
589
|
+
scope: {
|
|
590
|
+
appName: input.appName,
|
|
591
|
+
installationId: input.installationId,
|
|
592
|
+
trialRepositoryIds: [...input.trialRepositoryIds].sort((a, b) => a - b),
|
|
593
|
+
maxTrialRepositories
|
|
594
|
+
},
|
|
595
|
+
checkRunsReady: matchingRuns.length > 0 &&
|
|
596
|
+
!checkRunPublicationMissing &&
|
|
597
|
+
!compactReportMissing &&
|
|
598
|
+
!workerCleanupMissing,
|
|
599
|
+
safeLogBoundaryReady: !safeLogBoundaryRejected,
|
|
600
|
+
requiredNextProof: [
|
|
601
|
+
"Install only on selected trial repositories.",
|
|
602
|
+
"Publish one bounded Check Run from compact report data only.",
|
|
603
|
+
"Verify worker checkout cleanup after success and failure.",
|
|
604
|
+
"Sample logs and confirm raw source, raw diffs, secrets, customer payloads, and tokens are absent."
|
|
605
|
+
],
|
|
606
|
+
privacy: {
|
|
607
|
+
includesRawSource: false,
|
|
608
|
+
includesRawDiffs: false,
|
|
609
|
+
includesSecrets: false,
|
|
610
|
+
includesCustomerPayloads: false,
|
|
611
|
+
claimsCompleteHostedSaas: false
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
}
|
|
562
615
|
export function getHostedDeletionIdempotencyKey(input) {
|
|
563
616
|
return [input.trigger, input.installationId, input.repositoryId ?? "all"].join(":");
|
|
564
617
|
}
|
package/dist/hosted/worker.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HostedServiceScanRunner, HostedServiceScanRunnerInput, HostedServiceScanRunnerResult } from "./service.js";
|
|
2
2
|
export type HostedReadOnlyCheckoutCommandStage = "git_init" | "git_remote_add" | "git_fetch_head" | "git_fetch_base" | "git_checkout" | "cli_scan";
|
|
3
|
+
export type HostedReadOnlyCheckoutScanGateBlockedReason = `missing_command_stage_${HostedReadOnlyCheckoutCommandStage}` | "compact_report_missing" | "check_run_missing" | "checkout_cleanup_missing" | "token_boundary_missing" | "output_budget_exceeded" | "timeout_budget_exceeded";
|
|
3
4
|
export interface HostedReadOnlyCheckoutCommand {
|
|
4
5
|
stage: HostedReadOnlyCheckoutCommandStage;
|
|
5
6
|
command: string;
|
|
@@ -26,6 +27,39 @@ export interface HostedReadOnlyCheckoutScanRunnerOptions {
|
|
|
26
27
|
installationTokenProvider: HostedInstallationTokenProvider;
|
|
27
28
|
commandRunner?: HostedReadOnlyCheckoutCommandRunner;
|
|
28
29
|
}
|
|
30
|
+
export interface HostedReadOnlyCheckoutScanGateInput {
|
|
31
|
+
requestedAt: string;
|
|
32
|
+
jobKey: string;
|
|
33
|
+
commandStages: HostedReadOnlyCheckoutCommandStage[];
|
|
34
|
+
summaryCounts: Record<string, number>;
|
|
35
|
+
compactFindingCount: number;
|
|
36
|
+
compactReportStored: boolean;
|
|
37
|
+
checkRunPublished: boolean;
|
|
38
|
+
checkoutDeleted: boolean;
|
|
39
|
+
tokenRemovedBeforeCli: boolean;
|
|
40
|
+
maxOutputBytes: number;
|
|
41
|
+
timeoutMs: number;
|
|
42
|
+
rawSource?: string;
|
|
43
|
+
rawDiff?: string;
|
|
44
|
+
checkoutPath?: string;
|
|
45
|
+
installationToken?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface HostedReadOnlyCheckoutScanGate {
|
|
48
|
+
readyForHostedTrial: boolean;
|
|
49
|
+
blockedReasons: HostedReadOnlyCheckoutScanGateBlockedReason[];
|
|
50
|
+
requestedAt: string;
|
|
51
|
+
jobKey: string;
|
|
52
|
+
summaryCounts: Record<string, number>;
|
|
53
|
+
compactFindingCount: number;
|
|
54
|
+
commandStagesObserved: HostedReadOnlyCheckoutCommandStage[];
|
|
55
|
+
privacy: {
|
|
56
|
+
includesRawSource: false;
|
|
57
|
+
includesRawDiffs: false;
|
|
58
|
+
includesPrivateCheckoutPath: false;
|
|
59
|
+
includesInstallationToken: false;
|
|
60
|
+
claimsCompleteHostedSaas: false;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
29
63
|
export type HostedReadOnlyCheckoutScanSafeReason = "invalid_worker_plan" | "invalid_repository_full_name" | "invalid_clone_base_url" | "missing_installation_token" | "git_init_failed" | "git_remote_add_failed" | "git_fetch_head_failed" | "git_fetch_base_failed" | "git_checkout_failed" | "cli_scan_failed" | "invalid_cli_output" | "cleanup_failed";
|
|
30
64
|
export declare class HostedReadOnlyCheckoutScanError extends Error {
|
|
31
65
|
readonly safeReason: HostedReadOnlyCheckoutScanSafeReason;
|
|
@@ -40,4 +74,5 @@ export declare class HostedReadOnlyCheckoutScanError extends Error {
|
|
|
40
74
|
constructor(safeReason: HostedReadOnlyCheckoutScanSafeReason);
|
|
41
75
|
}
|
|
42
76
|
export declare function createHostedReadOnlyCheckoutScanRunner(options: HostedReadOnlyCheckoutScanRunnerOptions): HostedServiceScanRunner;
|
|
77
|
+
export declare function evaluateHostedReadOnlyCheckoutScanGate(input: HostedReadOnlyCheckoutScanGateInput): HostedReadOnlyCheckoutScanGate;
|
|
43
78
|
export declare function runHostedReadOnlyCheckoutScan(input: HostedServiceScanRunnerInput, options: HostedReadOnlyCheckoutScanRunnerOptions): Promise<HostedServiceScanRunnerResult>;
|
package/dist/hosted/worker.js
CHANGED
|
@@ -28,6 +28,52 @@ export class HostedReadOnlyCheckoutScanError extends Error {
|
|
|
28
28
|
export function createHostedReadOnlyCheckoutScanRunner(options) {
|
|
29
29
|
return (input) => runHostedReadOnlyCheckoutScan(input, options);
|
|
30
30
|
}
|
|
31
|
+
export function evaluateHostedReadOnlyCheckoutScanGate(input) {
|
|
32
|
+
const requiredStages = [
|
|
33
|
+
"git_init",
|
|
34
|
+
"git_remote_add",
|
|
35
|
+
"git_fetch_head",
|
|
36
|
+
"git_fetch_base",
|
|
37
|
+
"git_checkout",
|
|
38
|
+
"cli_scan"
|
|
39
|
+
];
|
|
40
|
+
const observedStages = new Set(input.commandStages);
|
|
41
|
+
const blockedReasons = [];
|
|
42
|
+
for (const stage of requiredStages) {
|
|
43
|
+
if (!observedStages.has(stage))
|
|
44
|
+
blockedReasons.push(`missing_command_stage_${stage}`);
|
|
45
|
+
}
|
|
46
|
+
if (!input.compactReportStored)
|
|
47
|
+
blockedReasons.push("compact_report_missing");
|
|
48
|
+
if (!input.checkRunPublished)
|
|
49
|
+
blockedReasons.push("check_run_missing");
|
|
50
|
+
if (!input.checkoutDeleted)
|
|
51
|
+
blockedReasons.push("checkout_cleanup_missing");
|
|
52
|
+
if (!input.tokenRemovedBeforeCli)
|
|
53
|
+
blockedReasons.push("token_boundary_missing");
|
|
54
|
+
if (input.maxOutputBytes > HOSTED_WORKER_MAX_OUTPUT_BYTES) {
|
|
55
|
+
blockedReasons.push("output_budget_exceeded");
|
|
56
|
+
}
|
|
57
|
+
if (input.timeoutMs > HOSTED_WORKER_MAX_TIMEOUT_MS) {
|
|
58
|
+
blockedReasons.push("timeout_budget_exceeded");
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
readyForHostedTrial: blockedReasons.length === 0,
|
|
62
|
+
blockedReasons,
|
|
63
|
+
requestedAt: input.requestedAt,
|
|
64
|
+
jobKey: input.jobKey,
|
|
65
|
+
summaryCounts: { ...input.summaryCounts },
|
|
66
|
+
compactFindingCount: input.compactFindingCount,
|
|
67
|
+
commandStagesObserved: input.commandStages.filter((stage, index, stages) => stages.indexOf(stage) === index),
|
|
68
|
+
privacy: {
|
|
69
|
+
includesRawSource: false,
|
|
70
|
+
includesRawDiffs: false,
|
|
71
|
+
includesPrivateCheckoutPath: false,
|
|
72
|
+
includesInstallationToken: false,
|
|
73
|
+
claimsCompleteHostedSaas: false
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
31
77
|
export async function runHostedReadOnlyCheckoutScan(input, options) {
|
|
32
78
|
const { plan } = input;
|
|
33
79
|
const { checkout, cli } = plan;
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,9 @@ export { applyGuardConfig, defaultConfigFileName, loadGuardConfig } from "./conf
|
|
|
9
9
|
export { createScanContext } from "./context.js";
|
|
10
10
|
export { getRuleMetadata, RULE_CATALOG } from "./rules/catalog.js";
|
|
11
11
|
export { formatSummaryReport } from "./report/summary.js";
|
|
12
|
+
export { createLocalScanResourceBudget } from "./performance.js";
|
|
12
13
|
export type { BaseReport, CommandName, Evidence, Finding, ActionsReport, McpOptions, McpPolicyTemplate, McpReport, McpServerInventory, McpSideEffect, PrRiskFile, PrRiskReport, ScanOptions, ShowcaseReport, StripeReport, SupabaseOptions, SupabaseDoctorReport, SupabaseReport } from "./types.js";
|
|
13
14
|
export type { ScanContext, ScanInput } from "./context.js";
|
|
14
15
|
export type { FindingSuppression, GuardConfig, RuleConfigValue } from "./config.js";
|
|
15
16
|
export type { RuleMetadata, RuleStability } from "./rules/catalog.js";
|
|
17
|
+
export type { LocalScanResourceBudget, LocalScanResourceBudgetInput } from "./performance.js";
|
package/dist/index.js
CHANGED
|
@@ -9,3 +9,4 @@ export { applyGuardConfig, defaultConfigFileName, loadGuardConfig } from "./conf
|
|
|
9
9
|
export { createScanContext } from "./context.js";
|
|
10
10
|
export { getRuleMetadata, RULE_CATALOG } from "./rules/catalog.js";
|
|
11
11
|
export { formatSummaryReport } from "./report/summary.js";
|
|
12
|
+
export { createLocalScanResourceBudget } from "./performance.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface LocalScanResourceBudgetInput {
|
|
2
|
+
repositoryKind?: string;
|
|
3
|
+
maxFiles?: number;
|
|
4
|
+
maxTotalBytes?: number;
|
|
5
|
+
maxFileBytes?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface LocalScanResourceBudget {
|
|
8
|
+
repositoryKind: string;
|
|
9
|
+
localFirst: true;
|
|
10
|
+
deterministic: true;
|
|
11
|
+
uploadsCode: false;
|
|
12
|
+
callsLlm: false;
|
|
13
|
+
limits: {
|
|
14
|
+
maxFiles: number;
|
|
15
|
+
maxTotalBytes: number;
|
|
16
|
+
maxFileBytes: number;
|
|
17
|
+
};
|
|
18
|
+
ignoredDirectories: string[];
|
|
19
|
+
operatorNote: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function createLocalScanResourceBudget(input?: LocalScanResourceBudgetInput): LocalScanResourceBudget;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { DEFAULT_MAX_TEXT_FILE_BYTES, DEFAULT_MAX_TEXT_FILES, DEFAULT_MAX_TOTAL_TEXT_BYTES } from "./utils/files.js";
|
|
2
|
+
export function createLocalScanResourceBudget(input = {}) {
|
|
3
|
+
return {
|
|
4
|
+
repositoryKind: input.repositoryKind ?? "ai-built-saas",
|
|
5
|
+
localFirst: true,
|
|
6
|
+
deterministic: true,
|
|
7
|
+
uploadsCode: false,
|
|
8
|
+
callsLlm: false,
|
|
9
|
+
limits: {
|
|
10
|
+
maxFiles: positiveIntegerOrDefault(input.maxFiles, DEFAULT_MAX_TEXT_FILES),
|
|
11
|
+
maxTotalBytes: positiveIntegerOrDefault(input.maxTotalBytes, DEFAULT_MAX_TOTAL_TEXT_BYTES),
|
|
12
|
+
maxFileBytes: positiveIntegerOrDefault(input.maxFileBytes, DEFAULT_MAX_TEXT_FILE_BYTES)
|
|
13
|
+
},
|
|
14
|
+
ignoredDirectories: [".git", ".next", ".turbo", "coverage", "dist", "build", "node_modules", "out"],
|
|
15
|
+
operatorNote: "This keeps local scans bounded for large AI SaaS repositories without uploading code or calling an LLM."
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function positiveIntegerOrDefault(value, fallback) {
|
|
19
|
+
if (value === undefined)
|
|
20
|
+
return fallback;
|
|
21
|
+
const normalized = Math.floor(value);
|
|
22
|
+
return Number.isFinite(normalized) && normalized > 0 ? normalized : fallback;
|
|
23
|
+
}
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -41,8 +41,32 @@ AI 构建的 SaaS 很容易“看起来已经能上线”:能登录、能打
|
|
|
41
41
|
- Next/Vercel 生产环境缺 env 文档、security headers、request ID 或成本风险提示
|
|
42
42
|
- AI 生成的大 PR 把 auth、billing、data、deploy 或测试改动藏在“普通改动”里
|
|
43
43
|
|
|
44
|
+
先用 30 秒 demo 看输出:`npx ai-saas-guard@latest demo --summary`。不用注册、不上传代码、不调用 LLM。然后用大约 3 分钟扫自己的仓库:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx ai-saas-guard@latest scan --root /path/to/your-saas --summary
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
输出会先回答三个上线前最实际的问题:
|
|
51
|
+
|
|
52
|
+
- **真实用户会不会拿到不该有的权限?** 先看 auth、tenant ownership、Supabase RLS 和 Stripe entitlement。
|
|
53
|
+
- **服务失败时会不会假装成功?** 先看 swallowed error、fake success、hardcoded fallback data 和跳过的测试。
|
|
54
|
+
- **上线基础设施是不是权限太大?** 再看 env 暴露、过宽 workflow、MCP tool、deploy gap 和 request evidence。
|
|
55
|
+
|
|
44
56
|
`ai-saas-guard` 是面向这个时刻的本地优先、review-first 上线预检工具。它不会证明你的应用绝对安全,也不是渗透测试、认证或完整安全审计。它的目标是给 founder、独立开发者、小团队和 reviewer 一份短而有证据的清单,告诉你上线或合并 PR 前最该先看哪里。
|
|
45
57
|
|
|
58
|
+
## 看到结果后该怎么处理
|
|
59
|
+
|
|
60
|
+
把报告当成上线 review 队列,而不是分数。先处理或人工验证最高风险的信任边界 finding,再处理低级别 hygiene。
|
|
61
|
+
|
|
62
|
+
| 如果看到 | 先做什么 |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| Critical/high 的 auth、billing、RLS、tenant、webhook 或 silent-success finding | 在 staging 复现 manual proof,确认风险路径会 fail closed |
|
|
65
|
+
| Medium 的 deploy、env、request ID、MCP 或 Actions hygiene finding | 判断这个上线路径现在是否必须补控制,还是可以在 critical 路径关闭后跟进 |
|
|
66
|
+
| Low/info 提示 | 等 user access、payment、data access 路径都清楚后再清理 |
|
|
67
|
+
|
|
68
|
+
想看一个更像真实项目的风险样例,可以扫描 [examples/case-study-ai-saas](../examples/case-study-ai-saas) 或阅读 [case-study-ai-saas.md](case-study-ai-saas.md)。
|
|
69
|
+
|
|
46
70
|
## 30 秒复制粘贴 demo
|
|
47
71
|
|
|
48
72
|
不需要注册、不上传代码、不调用 LLM:
|
|
@@ -135,6 +159,14 @@ Next steps
|
|
|
135
159
|
|
|
136
160
|
如果想看它和 Semgrep、zizmor、OpenSSF Scorecard、Snyk、GitHub code scanning 的边界区别,见 [launch-gate-positioning.md](launch-gate-positioning.md)。
|
|
137
161
|
|
|
162
|
+
## 三种使用路径
|
|
163
|
+
|
|
164
|
+
| 路径 | 适合什么场景 | 当前状态 |
|
|
165
|
+
| --- | --- | --- |
|
|
166
|
+
| 本地 CLI | 私有代码、本机首次上线 review、founder 或 reviewer 自查 | 已发布到 npm;本地优先、只读、不上传代码、不调用 LLM |
|
|
167
|
+
| GitHub Action | CI 里的 review queue、SARIF、PR summary artifact、可控 fail threshold | 可通过 `zr9959/ai-saas-guard@v0` 或固定版本标签使用 |
|
|
168
|
+
| Hosted GitHub App | 未来面向 selected repositories 的 hosted Check Run 体验 | 目前只是 limited trial gate,不是完整 hosted SaaS,也不是公开 hosted scanner |
|
|
169
|
+
|
|
138
170
|
## 快速开始
|
|
139
171
|
|
|
140
172
|
无需全局安装,直接运行:
|
|
@@ -179,23 +211,24 @@ node dist/cli.js scan --root /path/to/your-saas
|
|
|
179
211
|
|
|
180
212
|
这个仓库是公开 GitHub 仓库。
|
|
181
213
|
|
|
182
|
-
CLI 已发布到 npm:`ai-saas-guard@0.
|
|
214
|
+
CLI 已发布到 npm:`ai-saas-guard@0.35.1`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.35.1`。
|
|
183
215
|
|
|
184
216
|
| 模块 | 状态 |
|
|
185
217
|
| --- | --- |
|
|
186
218
|
| 公开 GitHub 仓库 | 已可用 |
|
|
187
|
-
| npm CLI | `ai-saas-guard@0.
|
|
188
|
-
| GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.
|
|
219
|
+
| npm CLI | `ai-saas-guard@0.35.1` |
|
|
220
|
+
| GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.35.1` |
|
|
189
221
|
| 输出格式 | 短 summary、Terminal、JSON、SARIF 和 PR markdown |
|
|
190
222
|
| 项目配置 | `.ai-saas-guard.json` 支持规则开关、severity 覆盖、suppressions 和 fail threshold |
|
|
191
223
|
| 隐私模型 | 本地优先、只读扫描、不调用 LLM、不上传代码 |
|
|
192
|
-
| 当前版本 | `0.
|
|
193
|
-
| Action 标签 | `v0.
|
|
224
|
+
| 当前版本 | `0.35.1` 将 case-study fixture 升级到已修补的 Next.js 版本,避免 GitHub Dependabot 对包内示例继续报告已知 Next.js advisories |
|
|
225
|
+
| Action 标签 | `v0.35.1`、`v0` |
|
|
194
226
|
| npm 发布 | GitHub Actions Trusted Publisher/OIDC,无需长期 npm token |
|
|
195
227
|
| 仓库可信度加固 | 严格 branch protection、Dependabot、CodeQL、fast-check fuzzing、signed release provenance assets、private vulnerability reporting、secret scanning 和 push protection |
|
|
196
228
|
| Cloudflare hosted ingress | 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`;签名 GitHub App webhook delivery 和 compact Check Run staging smoke 已通过 |
|
|
197
229
|
| Hosted GitHub App staging | 私有 App `ai-saas-guard-hosted`(`3834787`)已安装到 `zr9959/ai-saas-guard`;hosted operations evidence 见 [docs/hosted-operations-evidence.md](hosted-operations-evidence.md) |
|
|
198
230
|
| OpenSSF Best Practices | 已获得 passing badge,项目 `12955`;`.bestpractices.json` 继续作为保守证据记录 |
|
|
231
|
+
| 下一版路线 | v0.36.0 计划见 [v0.36-roadmap.md](v0.36-roadmap.md) |
|
|
199
232
|
|
|
200
233
|
## 主要命令
|
|
201
234
|
|
|
@@ -375,10 +408,14 @@ GitHub Marketplace wrapper 决策见 [docs/github-marketplace-wrapper-decision.m
|
|
|
375
408
|
- GitHub App deployment planner:`ai-saas-guard/hosted/github-app` 导出 `planHostedGitHubAppDeployment`,生成 first slice 最小权限 manifest,并在 release gate、公开 HTTPS URL、container digest、secret 引用、原始 secret 输入、permission 或 event 不安全时阻止创建
|
|
376
409
|
- Hosted production adapter layer:`ai-saas-guard/hosted/production-adapters` 导出 `createHostedGitHubAppJwt`、`planHostedGitHubInstallationTokenRequest` 和 `planHostedProductionWorkerExecution`,用于 GitHub App RS256 JWT、selected-repository installation token 请求规划、worker/check-run 分离 token scope、固定只读 worker 命令、timeout/output 预算、compact JSON-only 输出,以及 success/failure/timeout/cancellation 的 cleanup 规划;它本身仍然不部署公开 hosted 服务
|
|
377
410
|
- Hosted Node/container app skeleton:`ai-saas-guard/hosted/app` 导出 `createHostedHttpApp`、`createInMemoryHostedAppPlatform`、`createHostedNodeCheckoutAppPlatform` 和 `planHostedNodeContainerDeployment`,提供安全 `/healthz`、签名 `/github/webhook` ingress、单 job worker tick、测试用 in-memory provider adapters、真实 read-only checkout worker 组合入口、可见 timeout/output 安全预算,以及 secret manager、queue、compact report store、worker sandbox、GitHub Checks publisher 的部署引用校验;它本身仍然不部署或暴露公开 hosted 服务
|
|
411
|
+
- Read-only checkout scan gate:`ai-saas-guard/hosted/worker` 导出 `evaluateHostedReadOnlyCheckoutScanGate`,要求真实 worker 路径证明 trusted git stages、CLI scan、compact report storage、Check Run publication、checkout cleanup、CLI 前 token removal 以及 timeout/output budgets 都满足后,才能进入 hosted trial
|
|
378
412
|
- Hosted staging deployment planner:`ai-saas-guard/hosted/staging` 导出 `planHostedProviderBinding`、`planHostedStagingDeployment` 和 `planHostedGitHubAppPromotion`,把真实 provider 引用、Node/container deployment plan、hosted operational release-gate evidence 和 GitHub App deployment planning 组合起来;缺少 queue、store、worker sandbox、Check Run publisher、logs、metrics、rollback 或 incident-response 引用时,会阻止 staging exposure 和 production promotion;它本身仍然不会调用云平台、创建 GitHub App 或暴露公开 hosted 服务
|
|
379
413
|
- Hosted staging harness:`ai-saas-guard/hosted/staging-harness` 导出 `createFileBackedHostedStagingHarness`、`createHostedStagingHarnessEvidence`、`createHostedStagingReleaseEvidenceBundle`、`evaluateHostedStagingReleaseEvidenceBundle` 和 `validateHostedLogBoundary`,可以在本地用 file-backed queue、compact report、Check Run request 和 worker sandbox 跑通签名 webhook replay、worker tick 和 cleanup 校验,把 success/failure cleanup probes 与 log-boundary samples 转成 release-gate evidence,并直接执行 hosted release gate 判断;它只是 staging 演练工具,不会调用云平台、创建 GitHub App、写真实 Check Run 或暴露公开 hosted 服务
|
|
380
414
|
- Deployed worker staging evidence:`ai-saas-guard/hosted/deployed-staging` 导出 `createHostedDeployedWorkerStagingEvidenceAutomation`、`createHostedDeployedWorkerStagingEvidenceBundle` 和 `evaluateHostedDeployedWorkerStagingReleaseGate`,先验证 safe log samples,再把 public HTTPS health、signed webhook replay、deployed worker cleanup 以及外部 CI/scan/rollback evidence 转成 hosted release gate evidence;它不会部署云资源,也不会宣称 production hosted exposure
|
|
381
415
|
- Cloudflare hosted ingress:`hosted/cloudflare-worker` 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`,提供 `/healthz`、`/github/app/manifest-callback` 和签名 `/github/webhook` intake;Worker 已具备 compact pull request identity、file/category risk signal 和 Check Run metadata 路径;staging GitHub App ID 为 `3834787`,installation ID 为 `135085075`;真实 GitHub App webhook delivery 和 Check Run smoke 已通过;完整 source checkout worker deployment、monitoring、rollback 和 incident-response evidence 仍需要通过 hosted operational release gate
|
|
416
|
+
- Hosted GitHub App limited trial gate:`ai-saas-guard/hosted/contracts` 导出 `createHostedGitHubAppTrialGate`,确保 trial 只作用于 selected repositories,并要求 Check Run publication、compact report、worker cleanup 和 safe log-boundary evidence;它不宣称完整 hosted SaaS 已可用
|
|
417
|
+
- Case-study fixture:`examples/case-study-ai-saas` 和 [case-study-ai-saas.md](case-study-ai-saas.md) 展示一个更接近真实 AI SaaS 的 auth、billing、Supabase、Next/Vercel 和 GitHub Actions 风险组合
|
|
418
|
+
- Resource budget:`createLocalScanResourceBudget` 暴露大仓库本地扫描的保守预算:文件数、单文件字节、总字节、忽略 build/dependency 目录、不上传代码、不调用 LLM
|
|
382
419
|
- webhook event parser
|
|
383
420
|
- check-run summary renderer
|
|
384
421
|
- Check Run publication planner:要求 repository `checks: write`,只从 compact report 生成有长度上限的 Check Run payload,包含 review categories、优先 review 文件、verification steps 和本地 CLI 复现命令;MVP 不发 PR comment
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# AI SaaS Case Study Fixture
|
|
2
|
+
|
|
3
|
+
`examples/case-study-ai-saas` is a synthetic case study for a realistic AI-built SaaS shape: auth-adjacent API routes, billing checkout, Stripe webhook, Supabase RLS, Next/Vercel config, and GitHub Actions.
|
|
4
|
+
|
|
5
|
+
The fixture is intentionally not safe to launch. It helps readers see why a local-first launch gate is useful even when an app appears to have the expected product surfaces.
|
|
6
|
+
|
|
7
|
+
Expected findings include:
|
|
8
|
+
|
|
9
|
+
- missing Stripe webhook signature verification
|
|
10
|
+
- silent-success billing fallback
|
|
11
|
+
- broad Supabase RLS policy
|
|
12
|
+
- missing Next/Vercel security headers
|
|
13
|
+
- broad GitHub Actions permissions
|
|
14
|
+
|
|
15
|
+
Use it locally:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx ai-saas-guard@latest scan --root examples/case-study-ai-saas --summary
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This fixture is public-safe and synthetic. It is not a SaaS starter template, pentest target, certification artifact, or full audit.
|
|
@@ -91,6 +91,19 @@ Privacy boundaries:
|
|
|
91
91
|
|
|
92
92
|
- do not return temporary checkout paths, raw source, raw diffs, evidence snippets, secrets, customer payloads, PR-authored commands, PR-authored repository names, or installation tokens
|
|
93
93
|
- do not persist source checkout contents beyond the worker run
|
|
94
|
+
|
|
95
|
+
The read-only checkout scan gate turns a real worker observation into a small release/trial decision. The exported helper is `evaluateHostedReadOnlyCheckoutScanGate`.
|
|
96
|
+
|
|
97
|
+
It requires:
|
|
98
|
+
|
|
99
|
+
- trusted git setup, fetch, checkout, and CLI scan stages
|
|
100
|
+
- compact report storage
|
|
101
|
+
- bounded Check Run publication
|
|
102
|
+
- checkout cleanup
|
|
103
|
+
- installation token removal before the CLI phase
|
|
104
|
+
- timeout and output budgets within the hosted worker limits
|
|
105
|
+
|
|
106
|
+
It returns only compact counts, observed stages, blocked reasons, and privacy flags. It does not return raw source, raw diffs, checkout paths, or installation tokens.
|
|
94
107
|
- rely on the deployment sandbox for network egress restrictions around the CLI phase; the runner itself removes GitHub credentials before invoking the CLI
|
|
95
108
|
|
|
96
109
|
The exported helper is `createHostedReadOnlyCheckoutScanRunner`.
|
|
@@ -266,6 +279,21 @@ Privacy boundaries:
|
|
|
266
279
|
|
|
267
280
|
The exported helper is `planHostedCheckRunPublication`. It is intended to be the GitHub-API-independent contract for the first real Check Run writer. PR comments remain an explicit later workflow or paid hosted feature, not part of this MVP contract.
|
|
268
281
|
|
|
282
|
+
## Hosted GitHub App Limited Trial Gate
|
|
283
|
+
|
|
284
|
+
The limited trial gate decides whether the hosted GitHub App can be tried on a small selected-repository set. The exported helper is `createHostedGitHubAppTrialGate`.
|
|
285
|
+
|
|
286
|
+
It requires:
|
|
287
|
+
|
|
288
|
+
- trial repositories are a subset of selected GitHub App repositories
|
|
289
|
+
- the trial repository count stays under the configured cap
|
|
290
|
+
- compact Check Runs were published
|
|
291
|
+
- compact reports were stored
|
|
292
|
+
- worker cleanup was verified
|
|
293
|
+
- safe log-boundary samples were accepted
|
|
294
|
+
|
|
295
|
+
It does not install a GitHub App, call GitHub, run workers, or claim the complete hosted SaaS is ready. It is a deterministic gate for small controlled trials.
|
|
296
|
+
|
|
269
297
|
## Queue Cleanup Planner
|
|
270
298
|
|
|
271
299
|
The queue cleanup planner turns repository removal, installation deletion, and repeated cleanup events into a safe cancellation plan for hosted scan jobs. It is a pure planner only: it does not connect to a queue provider, mutate jobs, delete worker files, call GitHub, or retry work.
|
package/docs/npm-publishing.md
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
## Current State
|
|
6
6
|
|
|
7
7
|
- Package name: `ai-saas-guard`
|
|
8
|
-
- Current published version: `0.
|
|
8
|
+
- Current published version: `0.35.1`
|
|
9
9
|
- Next source candidate: none
|
|
10
10
|
- npm registry state: published at <https://www.npmjs.com/package/ai-saas-guard>
|
|
11
11
|
- First npm-published version: `0.1.1`
|
|
12
|
-
- GitHub Release: `v0.
|
|
12
|
+
- GitHub Release: `v0.35.1`
|
|
13
13
|
- Publish workflow: `.github/workflows/npm-publish.yml`
|
|
14
14
|
- Trusted Publisher: GitHub Actions, `zr9959/ai-saas-guard`, workflow `npm-publish.yml`, allowed action `npm publish`
|
|
15
15
|
- Long-lived npm publish token: not required
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
Use GitHub Actions with npm Trusted Publisher/OIDC:
|
|
20
20
|
|
|
21
|
-
1. Create and review a release tag such as `v0.
|
|
21
|
+
1. Create and review a release tag such as `v0.35.1`.
|
|
22
22
|
2. Publish from the GitHub Release or run the `Publish npm` workflow manually with `ref` set to that tag.
|
|
23
23
|
3. Keep `permissions.id-token: write` in the workflow so npm can exchange the GitHub Actions OIDC identity for a short-lived publish credential.
|
|
24
24
|
4. Run `npm publish --access public` from the workflow. Trusted publishing automatically generates provenance for this public package from this public repository.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# v0.36.0 Roadmap
|
|
2
|
+
|
|
3
|
+
This is the next practical release plan after v0.35.0. It keeps the product focused on a local-first launch gate for AI-built SaaS apps.
|
|
4
|
+
|
|
5
|
+
## Goals
|
|
6
|
+
|
|
7
|
+
- Make the first real scan easier to understand for a founder or reviewer.
|
|
8
|
+
- Improve report usefulness without turning the tool into a general pentest, certification, or CI analytics platform.
|
|
9
|
+
- Keep the CLI deterministic, local-first, read-only, and free of LLM calls.
|
|
10
|
+
|
|
11
|
+
## Planned Work
|
|
12
|
+
|
|
13
|
+
1. **Markdown report polish**
|
|
14
|
+
- Tighten the top summary, manual proof steps, and fix-direction wording.
|
|
15
|
+
- Make CI artifacts easier to read without opening JSON or SARIF.
|
|
16
|
+
|
|
17
|
+
2. **PR comment readiness**
|
|
18
|
+
- Prepare a compact PR summary format suitable for GitHub Action artifacts and future Check Runs.
|
|
19
|
+
- Keep it deterministic and file-evidence based.
|
|
20
|
+
|
|
21
|
+
3. **Risk sorting clarity**
|
|
22
|
+
- Explain why auth, billing, tenant data, webhook, RLS, and silent-success findings are ranked above cosmetic changes.
|
|
23
|
+
- Keep the ranking simple and inspectable.
|
|
24
|
+
|
|
25
|
+
4. **Fixture coverage**
|
|
26
|
+
- Expand the case-study fixture only where it demonstrates real launch-risk patterns.
|
|
27
|
+
- Keep positive and negative examples small enough to review quickly.
|
|
28
|
+
|
|
29
|
+
5. **Resource guardrails**
|
|
30
|
+
- Continue measuring large-repo scan limits, ignored directories, output size, and cleanup behavior.
|
|
31
|
+
- Avoid background services or persistent local processes for normal CLI use.
|
|
32
|
+
|
|
33
|
+
## Non-Goals
|
|
34
|
+
|
|
35
|
+
- No code upload.
|
|
36
|
+
- No LLM review.
|
|
37
|
+
- No hosted scanner claims until the hosted GitHub App path is fully gated and documented.
|
|
38
|
+
- No pentest, full audit, or certification positioning.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
pull-requests: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: 24
|
|
19
|
+
- run: npm test
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Case Study AI SaaS Fixture
|
|
2
|
+
|
|
3
|
+
This synthetic fixture looks like a small AI-built SaaS with auth, billing, Supabase, Vercel/Next.js deploy config, and GitHub Actions.
|
|
4
|
+
|
|
5
|
+
It is intentionally risky:
|
|
6
|
+
|
|
7
|
+
- billing checkout swallows provider failure and returns fake success
|
|
8
|
+
- Stripe webhook does not verify signatures
|
|
9
|
+
- Supabase RLS policy is broad
|
|
10
|
+
- Next.js config lacks production security headers
|
|
11
|
+
- GitHub Actions grants broad permissions
|
|
12
|
+
|
|
13
|
+
It is not a complete SaaS template and does not contain real secrets.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export async function POST() {
|
|
2
|
+
try {
|
|
3
|
+
return Response.json({ checkoutUrl: await createCheckoutSession() });
|
|
4
|
+
} catch {
|
|
5
|
+
return Response.json({ success: true, checkoutUrl: "/billing/demo-success" });
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function createCheckoutSession() {
|
|
10
|
+
throw new Error("provider unavailable");
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export async function POST(request: Request) {
|
|
2
|
+
const event = await request.json();
|
|
3
|
+
|
|
4
|
+
if (event.type === "checkout.session.completed") {
|
|
5
|
+
await grantEntitlement(event.data.object.customer);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return Response.json({ received: true });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function grantEntitlement(customerId: string) {
|
|
12
|
+
console.log("grant", customerId);
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
create table public.projects (
|
|
2
|
+
id uuid primary key,
|
|
3
|
+
tenant_id uuid not null,
|
|
4
|
+
name text not null
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
alter table public.projects enable row level security;
|
|
8
|
+
|
|
9
|
+
create policy "ai generated broad select"
|
|
10
|
+
on public.projects
|
|
11
|
+
for select
|
|
12
|
+
using (true);
|
package/package.json
CHANGED