ai-saas-guard 0.42.0 → 0.43.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 +7 -5
- package/action.yml +4 -2
- package/dist/commands/scan.js +16 -2
- package/dist/hosted/beta.d.ts +97 -0
- package/dist/hosted/beta.js +132 -0
- package/dist/hosted/contracts.js +1 -1
- package/dist/hosted/production-adapters.js +61 -6
- package/dist/hosted/staging-harness.js +0 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/rules/catalog.js +7 -0
- package/dist/scanners/apiRoutes.js +51 -2
- package/dist/scanners/mcp.js +1 -1
- package/dist/scanners/secrets.js +11 -0
- package/dist/scanners/supabase.js +19 -1
- package/dist/stackInventory.d.ts +22 -0
- package/dist/stackInventory.js +250 -0
- package/dist/types.d.ts +1 -0
- package/dist/utils/files.js +1 -1
- package/docs/CODEX_HANDOFF.md +218 -0
- package/docs/CODEX_RECENT_CHANGES.md +408 -0
- package/docs/CODEX_STATE.md +247 -0
- package/docs/CODEX_TODO.md +224 -0
- package/docs/README.zh-CN.md +7 -5
- package/docs/design-partner-outreach-kit.md +136 -0
- package/docs/hosted-next-proof-plan.md +141 -0
- package/docs/hosted-node-container-app.md +7 -2
- package/docs/hosted-operational-release-gate.md +30 -2
- package/docs/hosted-operations-evidence.md +331 -0
- package/docs/hosted-operator-runbook.md +263 -0
- package/docs/hosted-preimplementation-contracts.md +1 -1
- package/docs/hosted-support-incident-ownership.md +88 -0
- package/docs/npm-publishing.md +3 -3
- package/docs/project-handoff.md +139 -4
- package/docs/public-beta-evidence-feedback.md +318 -0
- package/docs/rules.md +29 -0
- package/hosted/cloudflare-worker/README.md +7 -1
- package/hosted/cloudflare-worker/src/index.js +229 -4
- package/hosted/cloudflare-worker/wrangler.jsonc +5 -2
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -235,13 +235,13 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
|
|
|
235
235
|
| Area | Status |
|
|
236
236
|
| --- | --- |
|
|
237
237
|
| Public GitHub repository | Available |
|
|
238
|
-
| npm CLI | `ai-saas-guard@0.
|
|
239
|
-
| GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.
|
|
238
|
+
| npm CLI | `ai-saas-guard@0.43.1` |
|
|
239
|
+
| GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.43.1` |
|
|
240
240
|
| Outputs | Launch decision queue, short summary, terminal, JSON, SARIF, and PR-focused markdown |
|
|
241
241
|
| Project config | `.ai-saas-guard.json` rule toggles, severity overrides, suppressions, and fail thresholds |
|
|
242
242
|
| Privacy model | Local-first, read-only scan commands, no LLM calls, no code upload |
|
|
243
|
-
| Versioned Action tags | `v0.
|
|
244
|
-
| Current release | `0.
|
|
243
|
+
| Versioned Action tags | `v0.43.1`, `v0` |
|
|
244
|
+
| Current release | `0.43.1` hardens hosted rate-limit failure handling, GitHub API URL validation, Check Run reproduction commands, MCP policy templates, and Action summary output while keeping billing disabled |
|
|
245
245
|
| npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
|
|
246
246
|
| Repository trust hardening | Strict branch protection, Dependabot, CodeQL, fast-check fuzzing, signed release provenance assets, private vulnerability reporting, secret scanning, and push protection |
|
|
247
247
|
| Cloudflare hosted ingress | Deployed at `https://ai-saas-guard-hosted.zr9959.workers.dev`; public install/privacy notes are in [docs/hosted-install-privacy.md](docs/hosted-install-privacy.md); signed GitHub App webhook delivery and compact Check Run smoke now pass in staging |
|
|
@@ -369,6 +369,8 @@ The first live hosted ingress is deployed on Cloudflare Workers at `https://ai-s
|
|
|
369
369
|
|
|
370
370
|
The next hosted source-checkout step is intentionally narrow: deploy the existing read-only checkout worker behind the same selected-repository identity, keep the fixed `pr-risk --json` command, write only compact findings to the Check Run, and require deployed cleanup/log-boundary/rollback evidence before broader trial use. The hosted worker export includes `createHostedSourceCheckoutTrialPlan`, `createHostedSourceCheckoutEvidence`, and `evaluateHostedSourceCheckoutTrialGate` so Phase 3 has one machine-checkable gate for checkout start/end, token removal, CLI start/end, compact report write, Check Run write, cleanup status, live smoke, rollback, monitoring, and incident-owner proof before Phase 4 beta.
|
|
371
371
|
|
|
372
|
+
The `ai-saas-guard/hosted/beta` export adds `evaluateHostedBetaReadinessGate` and `evaluateTeamLaunchGateReadiness`. These pre-commercial gates block hosted beta unless selected-repository install limits, abuse controls, safe telemetry, uninstall deletion proof, rollback, support ownership, beta smoke, and no-audit-claim wording are ready; they also block team use unless org policy config, required status-check docs, suppression audit, reviewer checklist, release evidence export, retention docs, and billing-disabled proof are in place.
|
|
373
|
+
|
|
372
374
|
Hosted install and privacy details are summarized in [docs/hosted-install-privacy.md](docs/hosted-install-privacy.md): selected-repository permissions, supported events, Check Run data boundaries, uninstall cleanup, and why the local CLI remains the private/offline path.
|
|
373
375
|
|
|
374
376
|
The hosted operational release gate is documented in [docs/hosted-operational-release-gate.md](docs/hosted-operational-release-gate.md). It defines the hosted-specific CI, replay, queue, worker cleanup, privacy, monitoring, rollback, and incident-response evidence required before any hosted environment is exposed to users. The pure gate evaluator exported from `ai-saas-guard/hosted/contracts` blocks hosted exposure unless every P0 evidence item is fresh, a container digest is recorded, and release notes avoid pentest, certification, and full-audit claims.
|
|
@@ -423,7 +425,7 @@ Use `suppressions` for narrower false-positive handling when one rule is noisy o
|
|
|
423
425
|
|
|
424
426
|
## GitHub Action
|
|
425
427
|
|
|
426
|
-
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.
|
|
428
|
+
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.43.1` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
|
|
427
429
|
|
|
428
430
|
```yaml
|
|
429
431
|
name: ai-saas-guard
|
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, sarif, or
|
|
15
|
+
description: "Output format: terminal, json, sarif, markdown, or summary. Use summary for first-run CLI output, markdown for PR summaries, and sarif for code scanning."
|
|
16
16
|
required: false
|
|
17
17
|
default: terminal
|
|
18
18
|
fail-on:
|
|
@@ -67,7 +67,7 @@ runs:
|
|
|
67
67
|
esac
|
|
68
68
|
|
|
69
69
|
case "${INPUT_FORMAT}" in
|
|
70
|
-
terminal|json|sarif|markdown) ;;
|
|
70
|
+
terminal|json|sarif|markdown|summary) ;;
|
|
71
71
|
*)
|
|
72
72
|
echo "Invalid format input: ${INPUT_FORMAT}" >&2
|
|
73
73
|
exit 2
|
|
@@ -100,6 +100,8 @@ runs:
|
|
|
100
100
|
args+=("--sarif")
|
|
101
101
|
elif [ "${INPUT_FORMAT}" = "markdown" ]; then
|
|
102
102
|
args+=("--markdown")
|
|
103
|
+
elif [ "${INPUT_FORMAT}" = "summary" ]; then
|
|
104
|
+
args+=("--summary")
|
|
103
105
|
fi
|
|
104
106
|
|
|
105
107
|
if [ "${INPUT_FAIL_ON}" != "none" ]; then
|
package/dist/commands/scan.js
CHANGED
|
@@ -8,13 +8,15 @@ import { scanNextPublicEnv, scanSecrets } from "../scanners/secrets.js";
|
|
|
8
8
|
import { scanSilentSuccess } from "../scanners/silentSuccess.js";
|
|
9
9
|
import { checkStripe } from "../scanners/stripe.js";
|
|
10
10
|
import { checkSupabase } from "../scanners/supabase.js";
|
|
11
|
+
import { detectStackInventory } from "../stackInventory.js";
|
|
11
12
|
export async function scanRepository(options) {
|
|
12
13
|
const context = await createScanContext(options.rootDir);
|
|
14
|
+
const stackInventory = await detectStackInventory(context);
|
|
13
15
|
const [secretFindings, nextPublicFindings, stripeReport, supabaseReport, mcpReport, apiFindings, deployFindings, silentSuccessFindings, actionsReport] = await Promise.all([
|
|
14
16
|
scanSecrets(context),
|
|
15
17
|
scanNextPublicEnv(context),
|
|
16
18
|
checkStripe(context),
|
|
17
|
-
checkSupabase(context),
|
|
19
|
+
stackInventory.databases.includes("supabase") ? checkSupabase(context) : createSkippedSupabaseReport(context.rootDir),
|
|
18
20
|
checkMcp(context),
|
|
19
21
|
scanApiRoutes(context),
|
|
20
22
|
scanDeployConfig(context),
|
|
@@ -31,5 +33,17 @@ export async function scanRepository(options) {
|
|
|
31
33
|
...deployFindings,
|
|
32
34
|
...silentSuccessFindings,
|
|
33
35
|
...actionsReport.findings
|
|
34
|
-
]), {});
|
|
36
|
+
]), { stackInventory });
|
|
37
|
+
}
|
|
38
|
+
function createSkippedSupabaseReport(rootDir) {
|
|
39
|
+
return createReport("check-supabase", rootDir, [], {
|
|
40
|
+
riskyTables: [],
|
|
41
|
+
riskyPolicies: [],
|
|
42
|
+
manualAuthorizationTest: [],
|
|
43
|
+
doctor: {
|
|
44
|
+
staticChecks: [],
|
|
45
|
+
twoAccountVerificationSteps: [],
|
|
46
|
+
sqlCookbook: []
|
|
47
|
+
}
|
|
48
|
+
});
|
|
35
49
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export interface HostedBetaReadinessGateInput {
|
|
2
|
+
requestedAt: string;
|
|
3
|
+
phase3GatePassed: boolean;
|
|
4
|
+
selectedRepositoryInstallOnly: boolean;
|
|
5
|
+
publicInstallDocsReady: boolean;
|
|
6
|
+
rateLimitEnabled: boolean;
|
|
7
|
+
abuseKillSwitchReady: boolean;
|
|
8
|
+
telemetrySafe: boolean;
|
|
9
|
+
uninstallDeletionTested: boolean;
|
|
10
|
+
rollbackTested: boolean;
|
|
11
|
+
incidentOwnerRecorded: boolean;
|
|
12
|
+
supportPathReady: boolean;
|
|
13
|
+
betaSmokePassed: boolean;
|
|
14
|
+
avoidsAuditClaims: boolean;
|
|
15
|
+
noRawSourceStorage: boolean;
|
|
16
|
+
noRawDiffStorage: boolean;
|
|
17
|
+
noPrTextStorage: boolean;
|
|
18
|
+
maxReposPerInstallation: number;
|
|
19
|
+
maxConcurrentScans: number;
|
|
20
|
+
rawSource?: string;
|
|
21
|
+
rawDiff?: string;
|
|
22
|
+
prText?: string;
|
|
23
|
+
installationToken?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface HostedBetaReadinessGate {
|
|
26
|
+
phase: "phase_4_hosted_beta_readiness";
|
|
27
|
+
readyForPublicBeta: boolean;
|
|
28
|
+
blockedReasons: string[];
|
|
29
|
+
requestedAt: string;
|
|
30
|
+
installBoundary: {
|
|
31
|
+
selectedRepositoryOnly: true;
|
|
32
|
+
maxReposPerInstallation: number;
|
|
33
|
+
maxConcurrentScans: number;
|
|
34
|
+
};
|
|
35
|
+
operations: {
|
|
36
|
+
rateLimitEnabled: boolean;
|
|
37
|
+
abuseKillSwitchReady: boolean;
|
|
38
|
+
telemetrySafe: boolean;
|
|
39
|
+
uninstallDeletionTested: boolean;
|
|
40
|
+
rollbackTested: boolean;
|
|
41
|
+
incidentOwnerRecorded: boolean;
|
|
42
|
+
supportPathReady: boolean;
|
|
43
|
+
betaSmokePassed: boolean;
|
|
44
|
+
};
|
|
45
|
+
nextAction: string;
|
|
46
|
+
privacy: {
|
|
47
|
+
includesRawSource: false;
|
|
48
|
+
includesRawDiffs: false;
|
|
49
|
+
includesUntrustedPrText: false;
|
|
50
|
+
includesInstallationToken: false;
|
|
51
|
+
claimsPentest: false;
|
|
52
|
+
claimsFullAudit: false;
|
|
53
|
+
claimsCertification: false;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export interface TeamLaunchGateReadinessInput {
|
|
57
|
+
requestedAt: string;
|
|
58
|
+
hostedBetaGatePassed: boolean;
|
|
59
|
+
orgPolicyConfigReady: boolean;
|
|
60
|
+
requiredStatusCheckDocumented: boolean;
|
|
61
|
+
suppressionAuditReady: boolean;
|
|
62
|
+
reviewerChecklistReady: boolean;
|
|
63
|
+
releaseEvidenceExportReady: boolean;
|
|
64
|
+
teamDocsReady: boolean;
|
|
65
|
+
adminBypassDocumented: boolean;
|
|
66
|
+
retentionPolicyDocumented: boolean;
|
|
67
|
+
noCommercialBillingEnabled: boolean;
|
|
68
|
+
rawSource?: string;
|
|
69
|
+
customerPayload?: unknown;
|
|
70
|
+
}
|
|
71
|
+
export interface TeamLaunchGateReadiness {
|
|
72
|
+
phase: "phase_5_team_launch_gate";
|
|
73
|
+
readyForTeamUse: boolean;
|
|
74
|
+
blockedReasons: string[];
|
|
75
|
+
requestedAt: string;
|
|
76
|
+
teamControls: {
|
|
77
|
+
orgPolicyConfigReady: boolean;
|
|
78
|
+
requiredStatusCheckDocumented: boolean;
|
|
79
|
+
suppressionAuditReady: boolean;
|
|
80
|
+
reviewerChecklistReady: boolean;
|
|
81
|
+
releaseEvidenceExportReady: boolean;
|
|
82
|
+
teamDocsReady: boolean;
|
|
83
|
+
adminBypassDocumented: boolean;
|
|
84
|
+
retentionPolicyDocumented: boolean;
|
|
85
|
+
};
|
|
86
|
+
commercialization: {
|
|
87
|
+
enabled: false;
|
|
88
|
+
reason: "pre_commercial_feedback_stage";
|
|
89
|
+
};
|
|
90
|
+
nextAction: string;
|
|
91
|
+
privacy: {
|
|
92
|
+
includesRawSource: false;
|
|
93
|
+
includesCustomerPayloads: false;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export declare function evaluateHostedBetaReadinessGate(input: HostedBetaReadinessGateInput): HostedBetaReadinessGate;
|
|
97
|
+
export declare function evaluateTeamLaunchGateReadiness(input: TeamLaunchGateReadinessInput): TeamLaunchGateReadiness;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const MAX_BETA_REPOS_PER_INSTALLATION = 10;
|
|
2
|
+
const MAX_BETA_CONCURRENT_SCANS = 5;
|
|
3
|
+
export function evaluateHostedBetaReadinessGate(input) {
|
|
4
|
+
const blockedReasons = hostedBetaBlockedReasons(input);
|
|
5
|
+
return {
|
|
6
|
+
phase: "phase_4_hosted_beta_readiness",
|
|
7
|
+
readyForPublicBeta: blockedReasons.length === 0,
|
|
8
|
+
blockedReasons,
|
|
9
|
+
requestedAt: input.requestedAt,
|
|
10
|
+
installBoundary: {
|
|
11
|
+
selectedRepositoryOnly: true,
|
|
12
|
+
maxReposPerInstallation: Math.max(0, Math.floor(input.maxReposPerInstallation)),
|
|
13
|
+
maxConcurrentScans: Math.max(0, Math.floor(input.maxConcurrentScans))
|
|
14
|
+
},
|
|
15
|
+
operations: {
|
|
16
|
+
rateLimitEnabled: input.rateLimitEnabled,
|
|
17
|
+
abuseKillSwitchReady: input.abuseKillSwitchReady,
|
|
18
|
+
telemetrySafe: input.telemetrySafe,
|
|
19
|
+
uninstallDeletionTested: input.uninstallDeletionTested,
|
|
20
|
+
rollbackTested: input.rollbackTested,
|
|
21
|
+
incidentOwnerRecorded: input.incidentOwnerRecorded,
|
|
22
|
+
supportPathReady: input.supportPathReady,
|
|
23
|
+
betaSmokePassed: input.betaSmokePassed
|
|
24
|
+
},
|
|
25
|
+
nextAction: blockedReasons.length === 0
|
|
26
|
+
? "Open a limited public beta only for selected repositories and keep collecting operational evidence before commercialization."
|
|
27
|
+
: "Do not open hosted beta. Resolve the blocked reasons and rerun the beta readiness gate.",
|
|
28
|
+
privacy: {
|
|
29
|
+
includesRawSource: false,
|
|
30
|
+
includesRawDiffs: false,
|
|
31
|
+
includesUntrustedPrText: false,
|
|
32
|
+
includesInstallationToken: false,
|
|
33
|
+
claimsPentest: false,
|
|
34
|
+
claimsFullAudit: false,
|
|
35
|
+
claimsCertification: false
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function evaluateTeamLaunchGateReadiness(input) {
|
|
40
|
+
const blockedReasons = teamLaunchBlockedReasons(input);
|
|
41
|
+
return {
|
|
42
|
+
phase: "phase_5_team_launch_gate",
|
|
43
|
+
readyForTeamUse: blockedReasons.length === 0,
|
|
44
|
+
blockedReasons,
|
|
45
|
+
requestedAt: input.requestedAt,
|
|
46
|
+
teamControls: {
|
|
47
|
+
orgPolicyConfigReady: input.orgPolicyConfigReady,
|
|
48
|
+
requiredStatusCheckDocumented: input.requiredStatusCheckDocumented,
|
|
49
|
+
suppressionAuditReady: input.suppressionAuditReady,
|
|
50
|
+
reviewerChecklistReady: input.reviewerChecklistReady,
|
|
51
|
+
releaseEvidenceExportReady: input.releaseEvidenceExportReady,
|
|
52
|
+
teamDocsReady: input.teamDocsReady,
|
|
53
|
+
adminBypassDocumented: input.adminBypassDocumented,
|
|
54
|
+
retentionPolicyDocumented: input.retentionPolicyDocumented
|
|
55
|
+
},
|
|
56
|
+
commercialization: {
|
|
57
|
+
enabled: false,
|
|
58
|
+
reason: "pre_commercial_feedback_stage"
|
|
59
|
+
},
|
|
60
|
+
nextAction: blockedReasons.length === 0
|
|
61
|
+
? "Use the team launch gate with design partners, collect user feedback, and delay commercialization until usage evidence exists."
|
|
62
|
+
: "Do not sell or commercialize. Resolve the team launch gate blockers before inviting teams beyond beta.",
|
|
63
|
+
privacy: {
|
|
64
|
+
includesRawSource: false,
|
|
65
|
+
includesCustomerPayloads: false
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function hostedBetaBlockedReasons(input) {
|
|
70
|
+
const reasons = [];
|
|
71
|
+
if (!input.phase3GatePassed)
|
|
72
|
+
reasons.push("phase3_gate_missing");
|
|
73
|
+
if (!input.selectedRepositoryInstallOnly)
|
|
74
|
+
reasons.push("selected_repository_install_required");
|
|
75
|
+
if (!input.publicInstallDocsReady)
|
|
76
|
+
reasons.push("public_install_docs_missing");
|
|
77
|
+
if (!input.rateLimitEnabled)
|
|
78
|
+
reasons.push("rate_limit_missing");
|
|
79
|
+
if (!input.abuseKillSwitchReady)
|
|
80
|
+
reasons.push("abuse_kill_switch_missing");
|
|
81
|
+
if (!input.telemetrySafe)
|
|
82
|
+
reasons.push("safe_telemetry_missing");
|
|
83
|
+
if (!input.uninstallDeletionTested)
|
|
84
|
+
reasons.push("uninstall_deletion_proof_missing");
|
|
85
|
+
if (!input.rollbackTested)
|
|
86
|
+
reasons.push("rollback_test_missing");
|
|
87
|
+
if (!input.incidentOwnerRecorded)
|
|
88
|
+
reasons.push("incident_owner_missing");
|
|
89
|
+
if (!input.supportPathReady)
|
|
90
|
+
reasons.push("support_path_missing");
|
|
91
|
+
if (!input.betaSmokePassed)
|
|
92
|
+
reasons.push("beta_smoke_missing");
|
|
93
|
+
if (!input.avoidsAuditClaims)
|
|
94
|
+
reasons.push("audit_claims_not_blocked");
|
|
95
|
+
if (!input.noRawSourceStorage)
|
|
96
|
+
reasons.push("raw_source_storage_blocked");
|
|
97
|
+
if (!input.noRawDiffStorage)
|
|
98
|
+
reasons.push("raw_diff_storage_blocked");
|
|
99
|
+
if (!input.noPrTextStorage)
|
|
100
|
+
reasons.push("pr_text_storage_blocked");
|
|
101
|
+
if (!Number.isFinite(input.maxReposPerInstallation) || input.maxReposPerInstallation > MAX_BETA_REPOS_PER_INSTALLATION) {
|
|
102
|
+
reasons.push("repo_limit_too_high");
|
|
103
|
+
}
|
|
104
|
+
if (!Number.isFinite(input.maxConcurrentScans) || input.maxConcurrentScans > MAX_BETA_CONCURRENT_SCANS) {
|
|
105
|
+
reasons.push("concurrency_limit_too_high");
|
|
106
|
+
}
|
|
107
|
+
return reasons;
|
|
108
|
+
}
|
|
109
|
+
function teamLaunchBlockedReasons(input) {
|
|
110
|
+
const reasons = [];
|
|
111
|
+
if (!input.hostedBetaGatePassed)
|
|
112
|
+
reasons.push("hosted_beta_gate_missing");
|
|
113
|
+
if (!input.orgPolicyConfigReady)
|
|
114
|
+
reasons.push("org_policy_config_missing");
|
|
115
|
+
if (!input.requiredStatusCheckDocumented)
|
|
116
|
+
reasons.push("required_status_check_docs_missing");
|
|
117
|
+
if (!input.suppressionAuditReady)
|
|
118
|
+
reasons.push("suppression_audit_missing");
|
|
119
|
+
if (!input.reviewerChecklistReady)
|
|
120
|
+
reasons.push("reviewer_checklist_missing");
|
|
121
|
+
if (!input.releaseEvidenceExportReady)
|
|
122
|
+
reasons.push("release_evidence_export_missing");
|
|
123
|
+
if (!input.teamDocsReady)
|
|
124
|
+
reasons.push("team_docs_missing");
|
|
125
|
+
if (!input.adminBypassDocumented)
|
|
126
|
+
reasons.push("admin_bypass_docs_missing");
|
|
127
|
+
if (!input.retentionPolicyDocumented)
|
|
128
|
+
reasons.push("retention_policy_docs_missing");
|
|
129
|
+
if (!input.noCommercialBillingEnabled)
|
|
130
|
+
reasons.push("commercial_billing_enabled_too_early");
|
|
131
|
+
return reasons;
|
|
132
|
+
}
|
package/dist/hosted/contracts.js
CHANGED
|
@@ -477,7 +477,7 @@ export function createCompactHostedReport(input) {
|
|
|
477
477
|
export function createHostedCheckRunSummary(input) {
|
|
478
478
|
const { report } = input;
|
|
479
479
|
const totalFindings = getHostedReportFindingTotal(report);
|
|
480
|
-
const localCliCommand = `npx ai-saas-guard@${report.scannerVersion} pr-risk --root
|
|
480
|
+
const localCliCommand = `npx ai-saas-guard@${report.scannerVersion} pr-risk --root . --base ${report.baseSha} --json`;
|
|
481
481
|
const conclusion = resolveCheckRunConclusion(report, input.failOnSeverity);
|
|
482
482
|
const launchGate = hostedLaunchGateVerdict(report);
|
|
483
483
|
return {
|
|
@@ -6,6 +6,7 @@ export const HOSTED_GITHUB_APP_JWT_CLOCK_SKEW_SECONDS = 60;
|
|
|
6
6
|
export const HOSTED_WORKER_MAX_TIMEOUT_MS = 600_000;
|
|
7
7
|
export const HOSTED_WORKER_DEFAULT_TIMEOUT_MS = 300_000;
|
|
8
8
|
export const HOSTED_WORKER_MAX_OUTPUT_BYTES = 1_048_576;
|
|
9
|
+
const HOSTED_GITHUB_API_BASE_URL = "https://api.github.com";
|
|
9
10
|
export function createHostedGitHubAppJwt(input) {
|
|
10
11
|
const nowSeconds = normalizeUnixSeconds(input.nowSeconds, Math.floor(Date.now() / 1000));
|
|
11
12
|
const ttlSeconds = clampPositiveInteger(input.ttlSeconds, HOSTED_GITHUB_APP_JWT_MAX_TTL_SECONDS, HOSTED_GITHUB_APP_JWT_MAX_TTL_SECONDS);
|
|
@@ -180,17 +181,71 @@ function safeApiUrlBlockedReasons(apiBaseUrl) {
|
|
|
180
181
|
if (!apiBaseUrl) {
|
|
181
182
|
return [];
|
|
182
183
|
}
|
|
184
|
+
return safeGitHubApiBaseUrl(apiBaseUrl) ? [] : ["invalid_github_api_url"];
|
|
185
|
+
}
|
|
186
|
+
function normalizeApiBaseUrl(apiBaseUrl) {
|
|
187
|
+
return safeGitHubApiBaseUrl(apiBaseUrl) ?? HOSTED_GITHUB_API_BASE_URL;
|
|
188
|
+
}
|
|
189
|
+
function safeGitHubApiBaseUrl(apiBaseUrl) {
|
|
190
|
+
if (!apiBaseUrl) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
const trimmed = trimTrailingSlashes(apiBaseUrl.trim());
|
|
183
194
|
try {
|
|
184
|
-
const url = new URL(
|
|
185
|
-
|
|
195
|
+
const url = new URL(trimmed);
|
|
196
|
+
if (url.protocol !== "https:" ||
|
|
197
|
+
url.username ||
|
|
198
|
+
url.password ||
|
|
199
|
+
url.search ||
|
|
200
|
+
url.hash ||
|
|
201
|
+
!isRootPath(url.pathname) ||
|
|
202
|
+
isUnsafeHostedHostname(url.hostname)) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
return trimmed;
|
|
186
206
|
}
|
|
187
207
|
catch {
|
|
188
|
-
return
|
|
208
|
+
return undefined;
|
|
189
209
|
}
|
|
190
210
|
}
|
|
191
|
-
function
|
|
192
|
-
|
|
193
|
-
|
|
211
|
+
function isRootPath(pathname) {
|
|
212
|
+
return pathname === "" || pathname === "/";
|
|
213
|
+
}
|
|
214
|
+
function isUnsafeHostedHostname(hostname) {
|
|
215
|
+
const normalized = normalizeHostname(hostname);
|
|
216
|
+
return (normalized === "localhost" ||
|
|
217
|
+
normalized.endsWith(".localhost") ||
|
|
218
|
+
isUnsafeIpv4Hostname(normalized) ||
|
|
219
|
+
isUnsafeIpv6Hostname(normalized));
|
|
220
|
+
}
|
|
221
|
+
function normalizeHostname(hostname) {
|
|
222
|
+
const lower = hostname.toLowerCase().replace(/\.$/, "");
|
|
223
|
+
return lower.startsWith("[") && lower.endsWith("]") ? lower.slice(1, -1) : lower;
|
|
224
|
+
}
|
|
225
|
+
function isUnsafeIpv4Hostname(hostname) {
|
|
226
|
+
const parts = hostname.split(".");
|
|
227
|
+
if (parts.length !== 4 || !parts.every((part) => /^\d+$/.test(part)))
|
|
228
|
+
return false;
|
|
229
|
+
const octets = parts.map((part) => Number(part));
|
|
230
|
+
if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
const [first, second] = octets;
|
|
234
|
+
return (first === 0 ||
|
|
235
|
+
first === 10 ||
|
|
236
|
+
first === 127 ||
|
|
237
|
+
(first === 169 && second === 254) ||
|
|
238
|
+
(first === 172 && second >= 16 && second <= 31) ||
|
|
239
|
+
(first === 192 && second === 168) ||
|
|
240
|
+
(first === 100 && second >= 64 && second <= 127) ||
|
|
241
|
+
first >= 224);
|
|
242
|
+
}
|
|
243
|
+
function isUnsafeIpv6Hostname(hostname) {
|
|
244
|
+
return (hostname === "::" ||
|
|
245
|
+
hostname === "::1" ||
|
|
246
|
+
hostname.startsWith("fc") ||
|
|
247
|
+
hostname.startsWith("fd") ||
|
|
248
|
+
hostname.startsWith("fe80:"));
|
|
194
249
|
}
|
|
195
250
|
function trimTrailingSlashes(value) {
|
|
196
251
|
let end = value.length;
|
|
@@ -24,7 +24,6 @@ export function createFileBackedHostedStagingHarness(options) {
|
|
|
24
24
|
const scanResult = typeof options.scanResult === "function"
|
|
25
25
|
? await options.scanResult(input)
|
|
26
26
|
: options.scanResult;
|
|
27
|
-
await writeFile(join(sandboxPath, "source.ts"), scanResult.rawSource ?? "", "utf8");
|
|
28
27
|
return scanResult;
|
|
29
28
|
},
|
|
30
29
|
now: options.now
|
package/dist/index.d.ts
CHANGED
|
@@ -7,11 +7,13 @@ export { checkActions } from "./commands/checkActions.js";
|
|
|
7
7
|
export { classifyPrRisk } from "./commands/prRisk.js";
|
|
8
8
|
export { applyGuardConfig, defaultConfigFileName, loadGuardConfig } from "./config.js";
|
|
9
9
|
export { createScanContext } from "./context.js";
|
|
10
|
+
export { detectStackInventory } from "./stackInventory.js";
|
|
10
11
|
export { getRuleMetadata, RULE_CATALOG } from "./rules/catalog.js";
|
|
11
12
|
export { formatSummaryReport } from "./report/summary.js";
|
|
12
13
|
export { createLocalScanResourceBudget } from "./performance.js";
|
|
13
14
|
export type { BaseReport, CommandName, Evidence, Finding, ActionsReport, McpOptions, McpPolicyTemplate, McpReport, McpServerInventory, McpSideEffect, PrRiskFile, PrRiskReport, ScanOptions, ShowcaseReport, StripeReport, SupabaseOptions, SupabaseDoctorReport, SupabaseReport } from "./types.js";
|
|
14
15
|
export type { ScanContext, ScanInput } from "./context.js";
|
|
16
|
+
export type { StackCategory, StackEvidence, StackInventory, StackInventoryInput } from "./stackInventory.js";
|
|
15
17
|
export type { FindingSuppression, GuardConfig, RuleConfigValue } from "./config.js";
|
|
16
18
|
export type { RuleMetadata, RuleStability } from "./rules/catalog.js";
|
|
17
19
|
export type { LocalScanResourceBudget, LocalScanResourceBudgetInput } from "./performance.js";
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export { checkActions } from "./commands/checkActions.js";
|
|
|
7
7
|
export { classifyPrRisk } from "./commands/prRisk.js";
|
|
8
8
|
export { applyGuardConfig, defaultConfigFileName, loadGuardConfig } from "./config.js";
|
|
9
9
|
export { createScanContext } from "./context.js";
|
|
10
|
+
export { detectStackInventory } from "./stackInventory.js";
|
|
10
11
|
export { getRuleMetadata, RULE_CATALOG } from "./rules/catalog.js";
|
|
11
12
|
export { formatSummaryReport } from "./report/summary.js";
|
|
12
13
|
export { createLocalScanResourceBudget } from "./performance.js";
|
package/dist/rules/catalog.js
CHANGED
|
@@ -188,6 +188,13 @@ export const RULE_CATALOG = {
|
|
|
188
188
|
why: "Login, checkout, upload, AI, and webhook routes are common abuse targets.",
|
|
189
189
|
stability: "experimental"
|
|
190
190
|
},
|
|
191
|
+
"api.route.provider-debug-exposed": {
|
|
192
|
+
ruleId: "api.route.provider-debug-exposed",
|
|
193
|
+
severity: "high",
|
|
194
|
+
title: "Provider debug endpoint exposes server-side credential probe",
|
|
195
|
+
why: "Public provider probe endpoints can spend quota, reveal integration state, or exercise server credentials without returning the token.",
|
|
196
|
+
stability: "default"
|
|
197
|
+
},
|
|
191
198
|
"api.route.auth-without-ownership": {
|
|
192
199
|
ruleId: "api.route.auth-without-ownership",
|
|
193
200
|
severity: "high",
|
|
@@ -4,7 +4,9 @@ import { lineAt, lineNumberForIndex } from "../utils/files.js";
|
|
|
4
4
|
const sensitiveRoutePattern = /(login|register|auth|checkout|stripe|webhook|upload|ai|generate|admin|password|reset|token)/i;
|
|
5
5
|
const rateLimitPattern = /(rateLimit|ratelimit|rate-limit|throttle|limiter|upstash|slowDown)/i;
|
|
6
6
|
const authPattern = /(auth|session|currentUser|getUser|jwt|cookies|authorization)/i;
|
|
7
|
-
const ownershipPattern = /(user_id|owner_id|tenant_id|organization_id|workspace_id|resource\.user|resource\.owner|where\s*:\s*{[\s\S]{0,
|
|
7
|
+
const ownershipPattern = /(user_id|userId|owner_id|ownerId|tenant_id|tenantId|organization_id|organizationId|workspace_id|workspaceId|resource\.user|resource\.owner|req\.user(?:Id|\.id)|where\s*:\s*{[\s\S]{0,140}(user|owner|tenant|organization|workspace))/i;
|
|
8
|
+
const providerDebugPathPattern = /(paypal|stripe|github|oauth|openai|anthropic|resend|sendgrid).*(token|config|status|test|probe|debug)|(token|config|status|test|probe|debug).*(paypal|stripe|github|oauth|openai|anthropic|resend|sendgrid)/i;
|
|
9
|
+
const providerCredentialProbePattern = /(client_credentials|oauth2\/token|access_token|PAYPAL_SECRET|PAYPAL_CLIENT_SECRET|STRIPE_SECRET|GITHUB_APP_PRIVATE_KEY|OPENAI_API_KEY|ANTHROPIC_API_KEY|SENDGRID_API_KEY|RESEND_API_KEY)/i;
|
|
8
10
|
export async function scanApiRoutes(input) {
|
|
9
11
|
const files = (await resolveScanContext(input)).getFiles((file) => isApiRoute(file.path));
|
|
10
12
|
const findings = [];
|
|
@@ -13,6 +15,7 @@ export async function scanApiRoutes(input) {
|
|
|
13
15
|
const isSensitive = sensitiveRoutePattern.test(file.path) || sensitiveRoutePattern.test(file.content);
|
|
14
16
|
findings.push(...scanClerkUnsafeMetadata(file.path, file.content));
|
|
15
17
|
findings.push(...scanPrismaTenantScope(file.path, file.content));
|
|
18
|
+
findings.push(...scanProviderDebugEndpoint(file.path, file.content));
|
|
16
19
|
if (isSensitive && hasPostOrMutation && !rateLimitPattern.test(file.content)) {
|
|
17
20
|
findings.push(finding({
|
|
18
21
|
ruleId: "api.route.missing-rate-limit",
|
|
@@ -24,7 +27,11 @@ export async function scanApiRoutes(input) {
|
|
|
24
27
|
suggestedFix: "Add IP/user keyed rate limiting close to the route entry point, with stricter limits for auth, checkout, upload, AI, and webhook paths."
|
|
25
28
|
}));
|
|
26
29
|
}
|
|
27
|
-
if (authPattern.test(file.content) &&
|
|
30
|
+
if (authPattern.test(file.content) &&
|
|
31
|
+
/params\.|searchParams|get\(|findUnique|findFirst|update|delete/i.test(file.content) &&
|
|
32
|
+
!ownershipPattern.test(file.content) &&
|
|
33
|
+
!hasExplicitAdminGuard(file.content) &&
|
|
34
|
+
!hasBenignRouteClassification(file.path, file.content)) {
|
|
28
35
|
findings.push(finding({
|
|
29
36
|
ruleId: "api.route.auth-without-ownership",
|
|
30
37
|
title: `API route checks auth but lacks an obvious ownership guard: ${file.path}`,
|
|
@@ -38,6 +45,48 @@ export async function scanApiRoutes(input) {
|
|
|
38
45
|
}
|
|
39
46
|
return uniqueFindings(findings);
|
|
40
47
|
}
|
|
48
|
+
function scanProviderDebugEndpoint(filePath, content) {
|
|
49
|
+
if (!/\bGET\b|export\s+async\s+function\s+GET/i.test(content))
|
|
50
|
+
return [];
|
|
51
|
+
if (!providerDebugPathPattern.test(filePath) && !providerDebugPathPattern.test(content))
|
|
52
|
+
return [];
|
|
53
|
+
if (!providerCredentialProbePattern.test(content))
|
|
54
|
+
return [];
|
|
55
|
+
if (hasExplicitAdminGuard(content))
|
|
56
|
+
return [];
|
|
57
|
+
return [
|
|
58
|
+
finding({
|
|
59
|
+
ruleId: "api.route.provider-debug-exposed",
|
|
60
|
+
title: `Public provider token or configuration probe endpoint: ${filePath}`,
|
|
61
|
+
severity: "high",
|
|
62
|
+
evidence: [{ file: filePath, line: firstLine(content, providerCredentialProbePattern), snippet: firstSnippet(content, providerCredentialProbePattern) }],
|
|
63
|
+
why: "A public debug endpoint can spend provider quota, reveal integration mode or configuration state, and exercise server-side credentials even when it does not return the token.",
|
|
64
|
+
suggestedVerification: "Call the route without a session in staging and confirm it cannot trigger provider authentication or reveal provider configuration state.",
|
|
65
|
+
suggestedFix: "Remove the endpoint before launch, or require admin authentication, add a dedicated rate limit, emit an audit log, and disable it in production."
|
|
66
|
+
})
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
function hasExplicitAdminGuard(content) {
|
|
70
|
+
return /\b(requireAdmin|adminMiddleware|isAdmin|requireRole\s*\(\s*["']admin|role\s*[:=]\s*["']admin|router\.use\s*\([\s\S]{0,160}requireAdmin)/i.test(content);
|
|
71
|
+
}
|
|
72
|
+
function hasBenignRouteClassification(filePath, content) {
|
|
73
|
+
return isPublicReadOnlyContentRoute(filePath, content) || isInternalProxyRoute(filePath, content) || isScopedTokenRoute(content);
|
|
74
|
+
}
|
|
75
|
+
function isPublicReadOnlyContentRoute(filePath, content) {
|
|
76
|
+
if (/\b(POST|PUT|PATCH|DELETE)\b|export\s+async\s+function\s+(POST|PUT|PATCH|DELETE)/.test(content))
|
|
77
|
+
return false;
|
|
78
|
+
const publicContentSurface = /(\/|^)(content|seo|blog|article|articles|sitemap|robots|public)(\/|\.|-|$)/i.test(filePath);
|
|
79
|
+
const publicPredicate = /\b(published|public|isPublished|visibility)\s*:\s*(true|["']public["'])/i.test(content);
|
|
80
|
+
return publicContentSurface && publicPredicate;
|
|
81
|
+
}
|
|
82
|
+
function isInternalProxyRoute(filePath, content) {
|
|
83
|
+
const internalPath = /(\/|^)(internal|proxy|bff)(\/|\.|-|$)/i.test(filePath);
|
|
84
|
+
const hasServiceTokenGuard = /\b(INTERNAL_[A-Z0-9_]*TOKEN|SERVICE_[A-Z0-9_]*TOKEN|PROXY_[A-Z0-9_]*TOKEN|authorization\s*!==\s*`?Bearer)/i.test(content);
|
|
85
|
+
return internalPath && hasServiceTokenGuard;
|
|
86
|
+
}
|
|
87
|
+
function isScopedTokenRoute(content) {
|
|
88
|
+
return /\b(scopeToken|scopedToken|verifyAgentScopeToken|agentScope|scopes\s*:\s*\[|content-agent:[a-z-]+)/i.test(content);
|
|
89
|
+
}
|
|
41
90
|
function scanClerkUnsafeMetadata(filePath, content) {
|
|
42
91
|
if (!/\b(@clerk\/|clerkClient|currentUser|auth\s*\()/i.test(content))
|
|
43
92
|
return [];
|
package/dist/scanners/mcp.js
CHANGED
|
@@ -248,7 +248,7 @@ function buildPolicyTemplate(servers) {
|
|
|
248
248
|
" decision: allow",
|
|
249
249
|
" reason: repo-local read scope",
|
|
250
250
|
" - match: { sideEffectClass: shell }",
|
|
251
|
-
"decision: deny",
|
|
251
|
+
" decision: deny",
|
|
252
252
|
" reason: shell tools require explicit human approval before launch work"
|
|
253
253
|
],
|
|
254
254
|
receiptFormat: [
|
package/dist/scanners/secrets.js
CHANGED
|
@@ -65,6 +65,8 @@ export async function scanSecrets(input) {
|
|
|
65
65
|
secretPattern.pattern.lastIndex = 0;
|
|
66
66
|
for (const match of file.content.matchAll(secretPattern.pattern)) {
|
|
67
67
|
const matchedText = match[0] ?? "";
|
|
68
|
+
if (isObviousPlaceholderSecret(file.path, matchedText))
|
|
69
|
+
continue;
|
|
68
70
|
const line = lineNumberForIndex(file.content, match.index ?? 0);
|
|
69
71
|
findings.push(finding({
|
|
70
72
|
ruleId: "secrets.detected",
|
|
@@ -87,6 +89,15 @@ export async function scanSecrets(input) {
|
|
|
87
89
|
}
|
|
88
90
|
return findings;
|
|
89
91
|
}
|
|
92
|
+
function isObviousPlaceholderSecret(filePath, matchedText) {
|
|
93
|
+
const lowerPath = filePath.toLowerCase();
|
|
94
|
+
const lower = matchedText.toLowerCase();
|
|
95
|
+
const isExampleFile = /(^|\/)(\.env\.example|\.env\.sample|example\.env|sample\.env)$/.test(lowerPath) || lowerPath.includes("/examples/");
|
|
96
|
+
const hasPlaceholderValue = /\b(your|replace[-_]?me|placeholder|example|sample|dummy|fake|test[-_]?token|do[-_]?not[-_]?use|changeme)\b/i.test(lower) ||
|
|
97
|
+
/<[^>\n]*(key|secret|token|password|client)[^>\n]*>/i.test(matchedText) ||
|
|
98
|
+
/\b(?:1x|2x)0{10,}[A-Za-z0-9_-]*\b/.test(matchedText);
|
|
99
|
+
return hasPlaceholderValue && (isExampleFile || !/sk_(?:live|test)_|gh[pousr]_|-----BEGIN|SUPABASE_SERVICE_ROLE_KEY\s*=\s*eyJ/i.test(matchedText));
|
|
100
|
+
}
|
|
90
101
|
export async function scanNextPublicEnv(input) {
|
|
91
102
|
const files = (await resolveScanContext(input)).files;
|
|
92
103
|
const findings = [];
|
|
@@ -5,6 +5,15 @@ const sensitiveTablePattern = /\b(user|account|profile|team|tenant|project|order
|
|
|
5
5
|
const ownershipColumnPattern = /\b(user_id|owner_id|tenant_id|account_id|organization_id|workspace_id|created_by)\b/i;
|
|
6
6
|
export async function checkSupabase(input, options = {}) {
|
|
7
7
|
const context = await resolveScanContext(input);
|
|
8
|
+
const doctor = buildDoctorReport(options.doctor ?? true);
|
|
9
|
+
if (!hasSupabaseContext(context.files)) {
|
|
10
|
+
return createReport("check-supabase", context.rootDir, [], {
|
|
11
|
+
riskyTables: [],
|
|
12
|
+
riskyPolicies: [],
|
|
13
|
+
manualAuthorizationTest: [],
|
|
14
|
+
doctor
|
|
15
|
+
});
|
|
16
|
+
}
|
|
8
17
|
const files = context.getFiles((file) => {
|
|
9
18
|
const path = file.path.toLowerCase();
|
|
10
19
|
return path.includes("supabase") || path.includes("migration") || path.endsWith(".sql") || path.endsWith(".prisma");
|
|
@@ -129,7 +138,6 @@ export async function checkSupabase(input, options = {}) {
|
|
|
129
138
|
}
|
|
130
139
|
}
|
|
131
140
|
findings.push(...buildDoctorFindings(files, tables, rlsEnabledTables, policies));
|
|
132
|
-
const doctor = buildDoctorReport(options.doctor ?? true);
|
|
133
141
|
return createReport("check-supabase", context.rootDir, uniqueFindings(findings), {
|
|
134
142
|
riskyTables: [...new Set(tables.filter((table) => table.sensitive && !rlsEnabledTables.has(table.name)).map((table) => table.name))],
|
|
135
143
|
riskyPolicies,
|
|
@@ -143,6 +151,16 @@ export async function checkSupabase(input, options = {}) {
|
|
|
143
151
|
doctor
|
|
144
152
|
});
|
|
145
153
|
}
|
|
154
|
+
function hasSupabaseContext(files) {
|
|
155
|
+
return files.some((file) => {
|
|
156
|
+
const path = file.path.toLowerCase();
|
|
157
|
+
if (path.includes("supabase"))
|
|
158
|
+
return true;
|
|
159
|
+
if (path === "package.json" && /@supabase\/supabase-js|supabase/i.test(file.content))
|
|
160
|
+
return true;
|
|
161
|
+
return /\bfrom\s+["']@supabase\/|create\s+policy\b|enable\s+row\s+level\s+security|auth\.uid\s*\(|storage\.objects/i.test(file.content);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
146
164
|
function buildDoctorFindings(files, tables, rlsEnabledTables, policies) {
|
|
147
165
|
const findings = [];
|
|
148
166
|
const policiesByTable = new Map();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ScanInput } from "./context.js";
|
|
2
|
+
export type StackCategory = "frameworks" | "databases" | "orms" | "auth" | "payments" | "storage" | "deploy";
|
|
3
|
+
export interface StackEvidence {
|
|
4
|
+
category: StackCategory;
|
|
5
|
+
tool: string;
|
|
6
|
+
file: string;
|
|
7
|
+
reason: string;
|
|
8
|
+
}
|
|
9
|
+
export interface StackInventory {
|
|
10
|
+
frameworks: string[];
|
|
11
|
+
databases: string[];
|
|
12
|
+
orms: string[];
|
|
13
|
+
auth: string[];
|
|
14
|
+
payments: string[];
|
|
15
|
+
storage: string[];
|
|
16
|
+
deploy: string[];
|
|
17
|
+
evidence: StackEvidence[];
|
|
18
|
+
}
|
|
19
|
+
export type StackInventoryInput = ScanInput | {
|
|
20
|
+
rootDir: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function detectStackInventory(input: StackInventoryInput): Promise<StackInventory>;
|