ai-saas-guard 0.11.0 → 0.13.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 +5 -5
- package/README.zh-CN.md +5 -3
- package/dist/hosted/contracts.d.ts +128 -0
- package/dist/hosted/contracts.js +186 -0
- package/docs/github-action.md +1 -1
- package/docs/hosted-preimplementation-contracts.md +51 -0
- package/docs/npm-publishing.md +3 -3
- package/docs/project-handoff.md +7 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,8 +73,8 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
|
|
|
73
73
|
| JSON and SARIF output | Available |
|
|
74
74
|
| Composite GitHub Action | Available |
|
|
75
75
|
| Project config | `.ai-saas-guard.json` rule toggles, severity overrides, and fail thresholds |
|
|
76
|
-
| Versioned Action tags | `v0.
|
|
77
|
-
| npm package | `ai-saas-guard@0.
|
|
76
|
+
| Versioned Action tags | `v0.13.0`, `v0` |
|
|
77
|
+
| npm package | `ai-saas-guard@0.13.0` |
|
|
78
78
|
| npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
|
|
79
79
|
|
|
80
80
|
## Quick Start
|
|
@@ -214,13 +214,13 @@ Hosted uninstall and data deletion behavior is documented in [docs/hosted-uninst
|
|
|
214
214
|
|
|
215
215
|
Hosted pricing and packaging boundaries are documented in [docs/hosted-pricing-packaging.md](docs/hosted-pricing-packaging.md). Core local scanning stays useful without an account; hosted plans may add workflow convenience, saved reports, team policy, and optional human review, but they do not gate local CLI scanning.
|
|
216
216
|
|
|
217
|
-
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,
|
|
217
|
+
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, and a worker read-only scan planner that fixes the CLI command, requires repository `contents: read`, and ignores PR-authored repo names, token scopes, and commands. They also cover queue-safe webhook event parsing, bounded check-run summary rendering, idempotent queue cleanup planning, worker checkout cleanup planning, and other service-free helpers exported from `ai-saas-guard/hosted/contracts`.
|
|
218
218
|
|
|
219
219
|
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.
|
|
220
220
|
|
|
221
221
|
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.
|
|
222
222
|
|
|
223
|
-
The repository also includes pure pre-implementation hosted contract helpers and tests for webhook intake order, webhook verification, installation token scoping, queue idempotency, compact reports, and retention limits. These helpers do not implement or deploy a hosted service.
|
|
223
|
+
The repository also includes pure pre-implementation hosted contract helpers and tests for webhook intake order, webhook verification, installation token scoping, durable queue idempotency, compact reports, and retention limits. These helpers do not implement or deploy a hosted service.
|
|
224
224
|
|
|
225
225
|
Users should prefer the local CLI for private repositories, offline review, or no-account workflows where hosted code processing is not acceptable.
|
|
226
226
|
|
|
@@ -254,7 +254,7 @@ Use `suppressions` for narrower false-positive handling when one rule is noisy o
|
|
|
254
254
|
|
|
255
255
|
## GitHub Action
|
|
256
256
|
|
|
257
|
-
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.
|
|
257
|
+
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.13.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
|
|
258
258
|
|
|
259
259
|
```yaml
|
|
260
260
|
name: ai-saas-guard
|
package/README.zh-CN.md
CHANGED
|
@@ -55,7 +55,7 @@ AI 能很快把一个 SaaS 从想法做成可运行的产品。真正难的是
|
|
|
55
55
|
|
|
56
56
|
这个仓库是公开 GitHub 仓库。
|
|
57
57
|
|
|
58
|
-
CLI 已发布到 npm:`ai-saas-guard@0.
|
|
58
|
+
CLI 已发布到 npm:`ai-saas-guard@0.13.0`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.13.0`。
|
|
59
59
|
|
|
60
60
|
| 模块 | 状态 |
|
|
61
61
|
| --- | --- |
|
|
@@ -66,8 +66,8 @@ CLI 已发布到 npm:`ai-saas-guard@0.11.0`。GitHub Action 支持 `v0` 浮动
|
|
|
66
66
|
| Markdown PR summary | 已可用 |
|
|
67
67
|
| GitHub Action | 已可用 |
|
|
68
68
|
| 项目配置 | `.ai-saas-guard.json` 支持规则开关、severity 覆盖和 fail threshold |
|
|
69
|
-
| 当前版本 | `0.
|
|
70
|
-
| Action 标签 | `v0.
|
|
69
|
+
| 当前版本 | `0.13.0` |
|
|
70
|
+
| Action 标签 | `v0.13.0`、`v0` |
|
|
71
71
|
| npm 发布 | GitHub Actions Trusted Publisher/OIDC,无需长期 npm token |
|
|
72
72
|
|
|
73
73
|
## 快速开始
|
|
@@ -253,6 +253,8 @@ jobs:
|
|
|
253
253
|
已经实现的 hosted 预实现纯契约包括:
|
|
254
254
|
|
|
255
255
|
- pull request webhook intake planner:先验签,再解析 payload、生成可信 identity、校验 selected-repository scope,并默认只走 check-run-only 输出
|
|
256
|
+
- durable scan queue planner:同一个 trusted scan key 的 queued/running/completed job 会复用,不重复排 worker,也不会把源码、diff、secret 或 PR 正文放进队列 payload
|
|
257
|
+
- worker read-only scan planner:只用 trusted identity 规划临时 worker checkout,要求 repository `contents: read`,固定运行 `ai-saas-guard pr-risk --json`,并忽略 PR 正文里的 repo 名、token scope 或命令
|
|
256
258
|
- webhook event parser
|
|
257
259
|
- check-run summary renderer
|
|
258
260
|
- queue cleanup planner
|
|
@@ -114,6 +114,57 @@ export interface HostedScanJobDecision {
|
|
|
114
114
|
shouldCreatePrComment: boolean;
|
|
115
115
|
}
|
|
116
116
|
export type HostedQueueJobStatus = "queued" | "running" | "completed" | "failed" | "cancelled";
|
|
117
|
+
export interface HostedScanQueueRecord {
|
|
118
|
+
key: string;
|
|
119
|
+
identity: HostedScanIdentity;
|
|
120
|
+
status: HostedQueueJobStatus;
|
|
121
|
+
attempt: number;
|
|
122
|
+
deliveryIds: string[];
|
|
123
|
+
createdAt: string;
|
|
124
|
+
updatedAt: string;
|
|
125
|
+
reportId?: string;
|
|
126
|
+
}
|
|
127
|
+
export interface HostedScanQueuePayload {
|
|
128
|
+
key: string;
|
|
129
|
+
identity: HostedScanIdentity;
|
|
130
|
+
deliveryId: string;
|
|
131
|
+
attempt: number;
|
|
132
|
+
requestedAt: string;
|
|
133
|
+
source: "github_pull_request";
|
|
134
|
+
}
|
|
135
|
+
export interface HostedScanQueueUpsertInput {
|
|
136
|
+
identity: HostedScanIdentity;
|
|
137
|
+
deliveryId: string;
|
|
138
|
+
requestedAt: string;
|
|
139
|
+
queue: Map<string, HostedScanQueueRecord>;
|
|
140
|
+
manualRerun?: boolean;
|
|
141
|
+
rawSource?: string;
|
|
142
|
+
rawDiff?: string;
|
|
143
|
+
secretValues?: string[];
|
|
144
|
+
untrustedPrText?: string;
|
|
145
|
+
customerPayload?: unknown;
|
|
146
|
+
}
|
|
147
|
+
export interface HostedScanQueueUpsertDecision {
|
|
148
|
+
key: string;
|
|
149
|
+
idempotent: true;
|
|
150
|
+
created: boolean;
|
|
151
|
+
reusedExistingJob: boolean;
|
|
152
|
+
existingStatus?: HostedQueueJobStatus;
|
|
153
|
+
attempt: number;
|
|
154
|
+
queueRecord: HostedScanQueueRecord;
|
|
155
|
+
queuePayload: HostedScanQueuePayload;
|
|
156
|
+
shouldEnqueueWorker: boolean;
|
|
157
|
+
shouldReuseCompletedReport: boolean;
|
|
158
|
+
shouldCreateCheckRun: boolean;
|
|
159
|
+
shouldCreatePrComment: false;
|
|
160
|
+
privacy: {
|
|
161
|
+
includesRawSource: false;
|
|
162
|
+
includesRawDiffs: false;
|
|
163
|
+
includesSecrets: false;
|
|
164
|
+
includesUntrustedPrText: false;
|
|
165
|
+
includesCustomerPayloads: false;
|
|
166
|
+
};
|
|
167
|
+
}
|
|
117
168
|
export type HostedQueueCleanupTrigger = "repository_removed" | "installation_deleted" | "repeated_cleanup";
|
|
118
169
|
export interface HostedQueueCleanupJobState {
|
|
119
170
|
key: string;
|
|
@@ -150,6 +201,81 @@ export interface HostedQueueCleanupPlan {
|
|
|
150
201
|
deleteCustomerPayloads: false;
|
|
151
202
|
}
|
|
152
203
|
export type HostedWorkerCheckoutTerminalState = "success" | "failure" | "timeout" | "cancellation" | "cleanup_failure";
|
|
204
|
+
export type HostedWorkerReadOnlyScanRejectReason = InstallationScopeRejectReason | "contents_read_permission_required";
|
|
205
|
+
export interface HostedWorkerReadOnlyScanInput {
|
|
206
|
+
identity: HostedScanIdentity;
|
|
207
|
+
jobKey: string;
|
|
208
|
+
requestedAt: string;
|
|
209
|
+
installationId: number;
|
|
210
|
+
selectedRepositoryIds: number[];
|
|
211
|
+
removedRepositoryIds?: number[];
|
|
212
|
+
installationTokenPermissions: {
|
|
213
|
+
contents?: string;
|
|
214
|
+
};
|
|
215
|
+
checkoutRoot?: string;
|
|
216
|
+
untrustedRepositoryFullName?: string;
|
|
217
|
+
untrustedTokenPermissions?: unknown;
|
|
218
|
+
untrustedCommand?: string;
|
|
219
|
+
untrustedPrText?: string;
|
|
220
|
+
rawSource?: string;
|
|
221
|
+
rawDiff?: string;
|
|
222
|
+
secretValues?: string[];
|
|
223
|
+
customerPayload?: unknown;
|
|
224
|
+
}
|
|
225
|
+
export interface HostedWorkerReadOnlyScanPlan {
|
|
226
|
+
accepted: boolean;
|
|
227
|
+
reason?: HostedWorkerReadOnlyScanRejectReason;
|
|
228
|
+
jobKey: string;
|
|
229
|
+
requestedAt: string;
|
|
230
|
+
readOnly: true;
|
|
231
|
+
shouldFetchSource: boolean;
|
|
232
|
+
shouldRunCli: boolean;
|
|
233
|
+
shouldPersistRawSource: false;
|
|
234
|
+
shouldPersistRawDiffs: false;
|
|
235
|
+
shouldCreatePrComment: false;
|
|
236
|
+
installationTokenScope?: {
|
|
237
|
+
installationId: number;
|
|
238
|
+
repositoryId: number;
|
|
239
|
+
permissions: {
|
|
240
|
+
contents: "read";
|
|
241
|
+
};
|
|
242
|
+
selectedRepositoryOnly: true;
|
|
243
|
+
};
|
|
244
|
+
checkout?: {
|
|
245
|
+
repositoryId: number;
|
|
246
|
+
repositoryFullName: string;
|
|
247
|
+
pullRequestNumber: number;
|
|
248
|
+
baseSha: string;
|
|
249
|
+
targetCommitSha: string;
|
|
250
|
+
directoryScope: "temporary_worker_directory";
|
|
251
|
+
cleanupRequired: true;
|
|
252
|
+
returnsCheckoutPath: false;
|
|
253
|
+
};
|
|
254
|
+
cli?: {
|
|
255
|
+
command: "ai-saas-guard";
|
|
256
|
+
args: string[];
|
|
257
|
+
workingDirectory: "<worker-checkout>";
|
|
258
|
+
networkAccess: "disabled";
|
|
259
|
+
writeMode: "read_only";
|
|
260
|
+
};
|
|
261
|
+
output?: {
|
|
262
|
+
compactJsonOnly: true;
|
|
263
|
+
persistRawSource: false;
|
|
264
|
+
persistRawDiffs: false;
|
|
265
|
+
persistSecrets: false;
|
|
266
|
+
persistCustomerPayloads: false;
|
|
267
|
+
};
|
|
268
|
+
privacy: {
|
|
269
|
+
returnsCheckoutPath: false;
|
|
270
|
+
includesRawSource: false;
|
|
271
|
+
includesRawDiffs: false;
|
|
272
|
+
includesSecrets: false;
|
|
273
|
+
includesCustomerPayloads: false;
|
|
274
|
+
acceptsRepositoryIdentityFromPrText: false;
|
|
275
|
+
acceptsTokenScopeFromPrText: false;
|
|
276
|
+
acceptsCommandFromPrText: false;
|
|
277
|
+
};
|
|
278
|
+
}
|
|
153
279
|
export type HostedWorkerCheckoutCleanupAction = "delete_checkout" | "record_cleanup_failure";
|
|
154
280
|
export interface HostedWorkerCheckoutCleanupInput {
|
|
155
281
|
identity: HostedScanIdentity;
|
|
@@ -303,12 +429,14 @@ export declare function parseHostedPullRequestEvent(input: HostedPullRequestEven
|
|
|
303
429
|
export declare function authorizeInstallationTokenScope(input: InstallationScopeInput): InstallationScopeDecision;
|
|
304
430
|
export declare function getHostedScanIdempotencyKey(identity: HostedScanIdentity): string;
|
|
305
431
|
export declare function upsertHostedScanJob(queue: Map<string, HostedScanJobState>, input: HostedScanJobInput): HostedScanJobDecision;
|
|
432
|
+
export declare function planHostedScanQueueUpsert(input: HostedScanQueueUpsertInput): HostedScanQueueUpsertDecision;
|
|
306
433
|
export declare function getHostedQueueCleanupIdempotencyKey(input: {
|
|
307
434
|
trigger: HostedQueueCleanupTrigger;
|
|
308
435
|
installationId: number;
|
|
309
436
|
repositoryId?: number;
|
|
310
437
|
}): string;
|
|
311
438
|
export declare function createHostedQueueCleanupPlan(input: HostedQueueCleanupPlanInput): HostedQueueCleanupPlan;
|
|
439
|
+
export declare function planHostedWorkerReadOnlyScan(input: HostedWorkerReadOnlyScanInput): HostedWorkerReadOnlyScanPlan;
|
|
312
440
|
export declare function createHostedWorkerCheckoutCleanupPlan(input: HostedWorkerCheckoutCleanupInput): HostedWorkerCheckoutCleanupPlan;
|
|
313
441
|
export declare function resolveHostedRetentionDays(input?: {
|
|
314
442
|
teamRequestedDays?: number;
|
package/dist/hosted/contracts.js
CHANGED
|
@@ -222,6 +222,76 @@ export function upsertHostedScanJob(queue, input) {
|
|
|
222
222
|
shouldCreatePrComment: false
|
|
223
223
|
};
|
|
224
224
|
}
|
|
225
|
+
export function planHostedScanQueueUpsert(input) {
|
|
226
|
+
const key = getHostedScanIdempotencyKey(input.identity);
|
|
227
|
+
const existing = input.queue.get(key);
|
|
228
|
+
if (!existing) {
|
|
229
|
+
const record = {
|
|
230
|
+
key,
|
|
231
|
+
identity: input.identity,
|
|
232
|
+
status: "queued",
|
|
233
|
+
attempt: 1,
|
|
234
|
+
deliveryIds: [input.deliveryId],
|
|
235
|
+
createdAt: input.requestedAt,
|
|
236
|
+
updatedAt: input.requestedAt
|
|
237
|
+
};
|
|
238
|
+
input.queue.set(key, record);
|
|
239
|
+
return {
|
|
240
|
+
key,
|
|
241
|
+
idempotent: true,
|
|
242
|
+
created: true,
|
|
243
|
+
reusedExistingJob: false,
|
|
244
|
+
attempt: record.attempt,
|
|
245
|
+
queueRecord: { ...record, deliveryIds: [...record.deliveryIds] },
|
|
246
|
+
queuePayload: createHostedScanQueuePayload(record, input.deliveryId, input.requestedAt),
|
|
247
|
+
shouldEnqueueWorker: true,
|
|
248
|
+
shouldReuseCompletedReport: false,
|
|
249
|
+
shouldCreateCheckRun: true,
|
|
250
|
+
shouldCreatePrComment: false,
|
|
251
|
+
privacy: hostedScanQueuePrivacy()
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (!existing.deliveryIds.includes(input.deliveryId)) {
|
|
255
|
+
existing.deliveryIds.push(input.deliveryId);
|
|
256
|
+
}
|
|
257
|
+
const existingStatus = existing.status;
|
|
258
|
+
if (input.manualRerun) {
|
|
259
|
+
existing.attempt += 1;
|
|
260
|
+
existing.status = "queued";
|
|
261
|
+
existing.updatedAt = input.requestedAt;
|
|
262
|
+
return {
|
|
263
|
+
key,
|
|
264
|
+
idempotent: true,
|
|
265
|
+
created: false,
|
|
266
|
+
reusedExistingJob: false,
|
|
267
|
+
existingStatus,
|
|
268
|
+
attempt: existing.attempt,
|
|
269
|
+
queueRecord: cloneHostedScanQueueRecord(existing),
|
|
270
|
+
queuePayload: createHostedScanQueuePayload(existing, input.deliveryId, input.requestedAt),
|
|
271
|
+
shouldEnqueueWorker: true,
|
|
272
|
+
shouldReuseCompletedReport: false,
|
|
273
|
+
shouldCreateCheckRun: true,
|
|
274
|
+
shouldCreatePrComment: false,
|
|
275
|
+
privacy: hostedScanQueuePrivacy()
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
existing.updatedAt = input.requestedAt;
|
|
279
|
+
return {
|
|
280
|
+
key,
|
|
281
|
+
idempotent: true,
|
|
282
|
+
created: false,
|
|
283
|
+
reusedExistingJob: true,
|
|
284
|
+
existingStatus,
|
|
285
|
+
attempt: existing.attempt,
|
|
286
|
+
queueRecord: cloneHostedScanQueueRecord(existing),
|
|
287
|
+
queuePayload: createHostedScanQueuePayload(existing, input.deliveryId, input.requestedAt),
|
|
288
|
+
shouldEnqueueWorker: false,
|
|
289
|
+
shouldReuseCompletedReport: existingStatus === "completed",
|
|
290
|
+
shouldCreateCheckRun: false,
|
|
291
|
+
shouldCreatePrComment: false,
|
|
292
|
+
privacy: hostedScanQueuePrivacy()
|
|
293
|
+
};
|
|
294
|
+
}
|
|
225
295
|
export function getHostedQueueCleanupIdempotencyKey(input) {
|
|
226
296
|
return ["queue-cleanup", input.trigger, input.installationId, input.repositoryId ?? "all"].join(":");
|
|
227
297
|
}
|
|
@@ -262,6 +332,69 @@ export function createHostedQueueCleanupPlan(input) {
|
|
|
262
332
|
deleteCustomerPayloads: false
|
|
263
333
|
};
|
|
264
334
|
}
|
|
335
|
+
export function planHostedWorkerReadOnlyScan(input) {
|
|
336
|
+
const scopeDecision = authorizeInstallationTokenScope({
|
|
337
|
+
identity: input.identity,
|
|
338
|
+
installationId: input.installationId,
|
|
339
|
+
selectedRepositoryIds: input.selectedRepositoryIds,
|
|
340
|
+
removedRepositoryIds: input.removedRepositoryIds
|
|
341
|
+
});
|
|
342
|
+
if (!scopeDecision.authorized) {
|
|
343
|
+
return rejectHostedWorkerReadOnlyScan(input, scopeDecision.reason ?? "repository_not_installed");
|
|
344
|
+
}
|
|
345
|
+
if (input.installationTokenPermissions.contents !== "read") {
|
|
346
|
+
return rejectHostedWorkerReadOnlyScan(input, "contents_read_permission_required");
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
accepted: true,
|
|
350
|
+
jobKey: input.jobKey,
|
|
351
|
+
requestedAt: input.requestedAt,
|
|
352
|
+
readOnly: true,
|
|
353
|
+
shouldFetchSource: true,
|
|
354
|
+
shouldRunCli: true,
|
|
355
|
+
shouldPersistRawSource: false,
|
|
356
|
+
shouldPersistRawDiffs: false,
|
|
357
|
+
shouldCreatePrComment: false,
|
|
358
|
+
installationTokenScope: {
|
|
359
|
+
installationId: input.identity.installationId,
|
|
360
|
+
repositoryId: input.identity.repositoryId,
|
|
361
|
+
permissions: { contents: "read" },
|
|
362
|
+
selectedRepositoryOnly: true
|
|
363
|
+
},
|
|
364
|
+
checkout: {
|
|
365
|
+
repositoryId: input.identity.repositoryId,
|
|
366
|
+
repositoryFullName: input.identity.repositoryFullName,
|
|
367
|
+
pullRequestNumber: input.identity.pullRequestNumber,
|
|
368
|
+
baseSha: input.identity.baseSha,
|
|
369
|
+
targetCommitSha: input.identity.headSha,
|
|
370
|
+
directoryScope: "temporary_worker_directory",
|
|
371
|
+
cleanupRequired: true,
|
|
372
|
+
returnsCheckoutPath: false
|
|
373
|
+
},
|
|
374
|
+
cli: {
|
|
375
|
+
command: "ai-saas-guard",
|
|
376
|
+
args: [
|
|
377
|
+
"pr-risk",
|
|
378
|
+
"--root",
|
|
379
|
+
"<worker-checkout>",
|
|
380
|
+
"--base",
|
|
381
|
+
input.identity.baseSha,
|
|
382
|
+
"--json"
|
|
383
|
+
],
|
|
384
|
+
workingDirectory: "<worker-checkout>",
|
|
385
|
+
networkAccess: "disabled",
|
|
386
|
+
writeMode: "read_only"
|
|
387
|
+
},
|
|
388
|
+
output: {
|
|
389
|
+
compactJsonOnly: true,
|
|
390
|
+
persistRawSource: false,
|
|
391
|
+
persistRawDiffs: false,
|
|
392
|
+
persistSecrets: false,
|
|
393
|
+
persistCustomerPayloads: false
|
|
394
|
+
},
|
|
395
|
+
privacy: hostedWorkerReadOnlyScanPrivacy()
|
|
396
|
+
};
|
|
397
|
+
}
|
|
265
398
|
export function createHostedWorkerCheckoutCleanupPlan(input) {
|
|
266
399
|
const cleanupFailed = input.terminalState === "cleanup_failure";
|
|
267
400
|
return {
|
|
@@ -441,6 +574,59 @@ function hostedWebhookIntakePrivacy() {
|
|
|
441
574
|
includesCustomerPayloads: false
|
|
442
575
|
};
|
|
443
576
|
}
|
|
577
|
+
function createHostedScanQueuePayload(record, deliveryId, requestedAt) {
|
|
578
|
+
return {
|
|
579
|
+
key: record.key,
|
|
580
|
+
identity: record.identity,
|
|
581
|
+
deliveryId,
|
|
582
|
+
attempt: record.attempt,
|
|
583
|
+
requestedAt,
|
|
584
|
+
source: "github_pull_request"
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function cloneHostedScanQueueRecord(record) {
|
|
588
|
+
return {
|
|
589
|
+
...record,
|
|
590
|
+
identity: { ...record.identity },
|
|
591
|
+
deliveryIds: [...record.deliveryIds]
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function hostedScanQueuePrivacy() {
|
|
595
|
+
return {
|
|
596
|
+
includesRawSource: false,
|
|
597
|
+
includesRawDiffs: false,
|
|
598
|
+
includesSecrets: false,
|
|
599
|
+
includesUntrustedPrText: false,
|
|
600
|
+
includesCustomerPayloads: false
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function rejectHostedWorkerReadOnlyScan(input, reason) {
|
|
604
|
+
return {
|
|
605
|
+
accepted: false,
|
|
606
|
+
reason,
|
|
607
|
+
jobKey: input.jobKey,
|
|
608
|
+
requestedAt: input.requestedAt,
|
|
609
|
+
readOnly: true,
|
|
610
|
+
shouldFetchSource: false,
|
|
611
|
+
shouldRunCli: false,
|
|
612
|
+
shouldPersistRawSource: false,
|
|
613
|
+
shouldPersistRawDiffs: false,
|
|
614
|
+
shouldCreatePrComment: false,
|
|
615
|
+
privacy: hostedWorkerReadOnlyScanPrivacy()
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function hostedWorkerReadOnlyScanPrivacy() {
|
|
619
|
+
return {
|
|
620
|
+
returnsCheckoutPath: false,
|
|
621
|
+
includesRawSource: false,
|
|
622
|
+
includesRawDiffs: false,
|
|
623
|
+
includesSecrets: false,
|
|
624
|
+
includesCustomerPayloads: false,
|
|
625
|
+
acceptsRepositoryIdentityFromPrText: false,
|
|
626
|
+
acceptsTokenScopeFromPrText: false,
|
|
627
|
+
acceptsCommandFromPrText: false
|
|
628
|
+
};
|
|
629
|
+
}
|
|
444
630
|
function parseJsonPayload(payload) {
|
|
445
631
|
try {
|
|
446
632
|
return JSON.parse(Buffer.isBuffer(payload) ? payload.toString("utf8") : payload);
|
package/docs/github-action.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`ai-saas-guard` ships as a composite GitHub Action for pull request and code scanning workflows.
|
|
4
4
|
|
|
5
|
-
Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.
|
|
5
|
+
Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.13.0` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
|
|
6
6
|
|
|
7
7
|
## PR Summary
|
|
8
8
|
|
|
@@ -26,6 +26,50 @@ Privacy boundaries:
|
|
|
26
26
|
|
|
27
27
|
The exported helper is `planHostedPullRequestWebhookIntake`. It is intentionally service-free: callers still need a real webhook server, queue provider, installation token lookup, worker checkout, scanner execution, compact report storage, and GitHub Checks API writer before any hosted environment exists.
|
|
28
28
|
|
|
29
|
+
## Durable Scan Queue Planner
|
|
30
|
+
|
|
31
|
+
The durable scan queue planner defines how the future hosted service should create or reuse scan jobs once a signed pull request webhook has produced trusted scan identity. It is a pure planner only: it does not connect to a queue provider, run a worker, fetch source, write reports, or call GitHub APIs.
|
|
32
|
+
|
|
33
|
+
Default behavior:
|
|
34
|
+
|
|
35
|
+
- compute the same logical scan key from installation ID, repository ID, pull request number, head SHA, and scanner version
|
|
36
|
+
- create one queued job when no matching job exists
|
|
37
|
+
- reuse existing queued, running, or completed jobs for duplicate deliveries
|
|
38
|
+
- reuse completed compact reports instead of enqueueing duplicate worker work
|
|
39
|
+
- allow manual reruns to increment `attempt` while keeping the same logical scan key
|
|
40
|
+
- keep the first hosted slice check-run-only; PR comments remain disabled
|
|
41
|
+
|
|
42
|
+
Queue payload boundaries:
|
|
43
|
+
|
|
44
|
+
- include only scan identity, job key, delivery ID, attempt, requested time, and source
|
|
45
|
+
- do not include raw source, raw diffs, secret values, untrusted PR text, webhook payload bodies, customer payloads, private URLs, or worker checkout paths
|
|
46
|
+
- return safe queue metadata that can be stored by a durable queue without leaking source code
|
|
47
|
+
|
|
48
|
+
The exported helper is `planHostedScanQueueUpsert`. It is intended to be the queue-provider-independent contract for the first real hosted queue implementation.
|
|
49
|
+
|
|
50
|
+
## Worker Read-Only Scan Planner
|
|
51
|
+
|
|
52
|
+
The worker read-only scan planner defines how a future hosted worker should prepare a scan after the durable queue hands it a trusted job. It is a pure planner only: it does not request installation tokens, create directories, checkout repositories, run the CLI, persist reports, delete files, or call GitHub APIs.
|
|
53
|
+
|
|
54
|
+
Default behavior:
|
|
55
|
+
|
|
56
|
+
- authorize the same installation and selected-repository scope before any checkout is planned
|
|
57
|
+
- require installation token permissions to be repository `contents: read`
|
|
58
|
+
- derive repository ID, repository full name, pull request number, base SHA, head SHA, and scanner version only from trusted scan identity
|
|
59
|
+
- plan checkout of the trusted head commit into a temporary worker directory
|
|
60
|
+
- plan a fixed read-only CLI invocation: `ai-saas-guard pr-risk --root <worker-checkout> --base <baseSha> --json`
|
|
61
|
+
- collect compact JSON output only
|
|
62
|
+
- require checkout cleanup after every terminal worker state
|
|
63
|
+
- keep PR comments disabled for the first hosted slice
|
|
64
|
+
|
|
65
|
+
Trust boundaries:
|
|
66
|
+
|
|
67
|
+
- ignore PR-authored repository names, token scopes, and commands
|
|
68
|
+
- do not accept worker command, checkout target, installation ID, repository ID, repository name, or token permissions from PR title, body, comments, branch names, README, or code
|
|
69
|
+
- do not return checkout paths, raw source, raw diffs, secret values, customer payloads, private URLs, or installation token values
|
|
70
|
+
|
|
71
|
+
The exported helper is `planHostedWorkerReadOnlyScan`. It is intended to be the worker-provider-independent contract for the first real hosted worker implementation.
|
|
72
|
+
|
|
29
73
|
## Webhook Event Parser
|
|
30
74
|
|
|
31
75
|
The webhook event parser runs after webhook signature verification. It converts a reduced GitHub `pull_request` webhook payload into a queue-safe scan request identity.
|
|
@@ -164,6 +208,13 @@ Automated tests must cover:
|
|
|
164
208
|
- signed pull request webhook intake verifies signatures before JSON parsing or queueing
|
|
165
209
|
- accepted pull request webhook intake queues one check-run-only scan request from trusted fields
|
|
166
210
|
- rejected installation scope stops before repository fetch planning
|
|
211
|
+
- durable scan queue planning creates one queued job for a new trusted scan key
|
|
212
|
+
- duplicate deliveries reuse queued, running, and completed jobs without enqueueing duplicate worker work
|
|
213
|
+
- completed duplicate jobs reuse compact reports
|
|
214
|
+
- manual reruns increment attempt without changing the logical scan key
|
|
215
|
+
- worker read-only scan planning requires repository `contents: read` permissions
|
|
216
|
+
- worker read-only scan planning uses trusted identity for checkout target and fixed CLI command
|
|
217
|
+
- worker read-only scan planning does not persist raw source, raw diffs, secrets, customer payloads, checkout paths, PR-authored commands, or PR-authored token scopes
|
|
167
218
|
- accepted pull request events build the expected trusted scan identity
|
|
168
219
|
- unsupported actions are rejected
|
|
169
220
|
- draft pull requests are rejected by default
|
package/docs/npm-publishing.md
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
## Current State
|
|
6
6
|
|
|
7
7
|
- Package name: `ai-saas-guard`
|
|
8
|
-
- Current version: `0.
|
|
8
|
+
- Current version: `0.13.0`
|
|
9
9
|
- npm registry state: published at <https://www.npmjs.com/package/ai-saas-guard>
|
|
10
10
|
- First npm-published version: `0.1.1`
|
|
11
|
-
- GitHub Release: `v0.
|
|
11
|
+
- GitHub Release: `v0.13.0`
|
|
12
12
|
- Publish workflow: `.github/workflows/npm-publish.yml`
|
|
13
13
|
- Trusted Publisher: GitHub Actions, `zr9959/ai-saas-guard`, workflow `npm-publish.yml`, allowed action `npm publish`
|
|
14
14
|
- Long-lived npm publish token: not required
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
Use GitHub Actions with npm Trusted Publisher/OIDC:
|
|
19
19
|
|
|
20
|
-
1. Create and review a release tag such as `v0.
|
|
20
|
+
1. Create and review a release tag such as `v0.13.0`.
|
|
21
21
|
2. Publish from the GitHub Release or run the `Publish npm` workflow manually with `ref` set to that tag.
|
|
22
22
|
3. Keep `permissions.id-token: write` in the workflow so npm can exchange the GitHub Actions OIDC identity for a short-lived publish credential.
|
|
23
23
|
4. Run `npm publish --access public` from the workflow. Trusted publishing automatically generates provenance for this public package from this public repository.
|
package/docs/project-handoff.md
CHANGED
|
@@ -57,9 +57,9 @@ Implemented surfaces:
|
|
|
57
57
|
- hosted operational release gate document requiring hosted CI, webhook replay, dependency and container scanning, privacy and retention verification, worker cleanup, monitoring and alerting, manual rollback, and incident response evidence before exposure
|
|
58
58
|
- hosted uninstall and data deletion document defining repository removal, full app uninstall, compact report deletion, queue cancellation, audit record retention, repeated cleanup idempotency, and user-facing deletion wording
|
|
59
59
|
- hosted pricing and packaging document defining open-source CLI boundaries, free/public repo hosted behavior, private repo hosted behavior, PR comments, saved reports, team policy, optional Launch Review, and no pentest/certification/full-audit claims
|
|
60
|
-
- hosted pre-implementation contracts document, hosted compact report fixture, and pure helpers for pull request webhook intake planning, queue-safe pull request event parsing from trusted GitHub event fields, bounded check-run summary rendering, idempotent queue cleanup planning, and worker checkout cleanup planning
|
|
60
|
+
- hosted pre-implementation contracts document, hosted compact report fixture, and pure helpers for pull request webhook intake planning, durable scan queue upsert planning, worker read-only scan planning, queue-safe pull request event parsing from trusted GitHub event fields, bounded check-run summary rendering, idempotent queue cleanup planning, and worker checkout cleanup planning
|
|
61
61
|
- implementation-ready hosted GitHub App permission contract for required permissions, optional PR comment permissions, selected repository installation, and out-of-scope broad permissions
|
|
62
|
-
- pure hosted GitHub App contract helpers and tests for webhook intake order, webhook verification, installation token scoping, scan queue idempotency, compact reports, and retention limits
|
|
62
|
+
- pure hosted GitHub App contract helpers and tests for webhook intake order, webhook verification, installation token scoping, durable scan queue idempotency, compact reports, and retention limits
|
|
63
63
|
- GitHub issue templates for bug reports, false positives, false negatives, rule requests, and public-safe security reports
|
|
64
64
|
- CODEOWNERS for source, tests, docs, workflows, Action, and package metadata
|
|
65
65
|
- JSON output
|
|
@@ -113,19 +113,21 @@ GitHub Project:
|
|
|
113
113
|
|
|
114
114
|
Current issue set:
|
|
115
115
|
|
|
116
|
-
-
|
|
116
|
+
- Closed hosted MVP issue: #24 webhook intake.
|
|
117
|
+
- Closed hosted MVP issue: #25 idempotent queue contract.
|
|
118
|
+
- Open hosted MVP roadmap issues: #26 read-only worker checkout, #27 Check summaries, #28 retention/uninstall cleanup, and #29 hosted operational release gate.
|
|
117
119
|
|
|
118
120
|
CI:
|
|
119
121
|
|
|
120
122
|
- Workflow: `.github/workflows/ci.yml`
|
|
121
123
|
- Runs on pull requests and pushes to `main`
|
|
122
124
|
- Uses `permissions: contents: read`
|
|
123
|
-
- Latest verified run for the hosted
|
|
125
|
+
- Latest verified run for the hosted durable queue contract release succeeded
|
|
124
126
|
|
|
125
127
|
Publishing:
|
|
126
128
|
|
|
127
129
|
- npm package: `ai-saas-guard`
|
|
128
|
-
- Current release line: `v0.
|
|
130
|
+
- Current release line: `v0.13.0`
|
|
129
131
|
- Publish workflow: `.github/workflows/npm-publish.yml`
|
|
130
132
|
- Trusted Publisher: GitHub Actions for `zr9959/ai-saas-guard`, workflow `npm-publish.yml`
|
|
131
133
|
- Long-lived npm publish tokens should not be required.
|