@agjs/tsforge 0.1.18 → 0.2.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/package.json +4 -1
- package/scripts/build-rules-md.ts +78 -21
- package/scripts/sweep.ts +25 -20
- package/scripts/web-sweep.ts +292 -0
- package/src/browser/oracle.ts +29 -1
- package/src/cli.ts +9 -3
- package/src/config/index.ts +8 -0
- package/src/config/profiles.ts +150 -0
- package/src/config/tsforge-config.ts +64 -5
- package/src/detect-gate.ts +34 -1
- package/src/inference/inference.types.ts +8 -0
- package/src/inference/request.ts +5 -1
- package/src/inference/stream.ts +21 -2
- package/src/inference/wire.ts +0 -0
- package/src/loop/feedback/meta-rule-docs.ts +48 -0
- package/src/loop/feedback/rule-docs.ts +150 -0
- package/src/loop/rule-docs.generated.json +131 -1
- package/src/loop/run.ts +3 -0
- package/src/loop/session.ts +12 -5
- package/src/loop/ttsr-defaults.ts +175 -4
- package/src/meta-rules/registry.ts +32 -0
- package/src/meta-rules/rules/ci/no-github-context-in-shell.ts +40 -0
- package/src/meta-rules/rules/ci/no-pull-request-target-untrusted-checkout.ts +42 -0
- package/src/meta-rules/rules/ci/workflow-permissions-explicit.ts +49 -0
- package/src/meta-rules/rules/ci/workflow-permissions-least-privilege.ts +44 -0
- package/src/meta-rules/rules/config/next-image-remote-patterns-no-wildcards.ts +77 -0
- package/src/meta-rules/rules/config/next-instrumentation-present.ts +66 -0
- package/src/meta-rules/rules/config/next-proxy-over-middleware.ts +64 -0
- package/src/meta-rules/rules/config/tsconfig-recommended-flags.ts +75 -0
- package/src/meta-rules/rules/supply-chain/dependency-overrides-require-comment.ts +61 -0
- package/src/meta-rules/rules/supply-chain/fastify-security-plugins.ts +54 -0
- package/src/meta-rules/rules/supply-chain/lockfile-required.ts +51 -0
- package/src/meta-rules/rules/supply-chain/migrations-must-be-checked-in.ts +49 -0
- package/src/meta-rules/rules/supply-chain/no-git-or-tarball-dependencies.ts +70 -0
- package/src/meta-rules/rules/supply-chain/package-manager-field-required.ts +31 -0
- package/src/meta-rules/rules/supply-chain/production-must-not-use-drizzle-push.ts +75 -0
- package/src/meta-rules/rules/supply-chain/single-package-manager.ts +30 -0
- package/src/meta-rules/utils/lockfiles.ts +105 -0
- package/src/meta-rules/utils/workflow-yaml.ts +86 -0
- package/src/rule-packs/authorization/index.ts +26 -0
- package/src/rule-packs/authorization/rules/id-param-requires-object-authz.ts +87 -0
- package/src/rule-packs/authorization/rules/mutating-route-requires-authz.ts +116 -0
- package/src/rule-packs/authorization/rules/server-action-requires-authz.ts +101 -0
- package/src/rule-packs/authorization/utils.ts +285 -0
- package/src/rule-packs/boundary-utils.ts +13 -0
- package/src/rule-packs/code-flow/index.ts +4 -1
- package/src/rule-packs/code-flow/rules/no-throw-literal.ts +67 -0
- package/src/rule-packs/drizzle/index.ts +7 -0
- package/src/rule-packs/drizzle/rules/update-delete-account-scoped-must-filter-scope.ts +106 -0
- package/src/rule-packs/drizzle/rules/update-delete-must-have-where.ts +73 -0
- package/src/rule-packs/drizzle/utils.ts +133 -1
- package/src/rule-packs/fastify/index.ts +38 -0
- package/src/rule-packs/fastify/rules/error-handler-must-set-status.ts +78 -0
- package/src/rule-packs/fastify/rules/prefer-return-over-reply-send.ts +104 -0
- package/src/rule-packs/fastify/rules/require-fp-for-shared-plugins.ts +106 -0
- package/src/rule-packs/fastify/rules/require-plugin-name.ts +54 -0
- package/src/rule-packs/fastify/rules/require-response-schema.ts +62 -0
- package/src/rule-packs/fastify/rules/require-route-schema.ts +104 -0
- package/src/rule-packs/fastify/rules/test-inject-must-close-app.ts +44 -0
- package/src/rule-packs/fastify/utils/fastifyChain.ts +231 -0
- package/src/rule-packs/index.ts +10 -0
- package/src/rule-packs/jwt-cookies/index.ts +10 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-set-maxage-or-expires.ts +132 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-set-samesite.ts +151 -0
- package/src/rule-packs/jwt-cookies/rules/jwt-must-verify-not-decode.ts +124 -0
- package/src/rule-packs/module-boundaries/index.ts +3 -0
- package/src/rule-packs/module-boundaries/rules/no-react-in-services.ts +111 -0
- package/src/rule-packs/nextjs/index.ts +32 -0
- package/src/rule-packs/nextjs/rules/await-dynamic-request-apis.ts +65 -0
- package/src/rule-packs/nextjs/rules/error-boundary-require-use-client.ts +38 -0
- package/src/rule-packs/nextjs/rules/mutation-should-revalidate-cache.ts +152 -0
- package/src/rule-packs/nextjs/rules/no-html-img-element.ts +45 -0
- package/src/rule-packs/nextjs/rules/no-internal-api-fetch.ts +126 -0
- package/src/rule-packs/nextjs/rules/no-secret-props-to-client.ts +118 -0
- package/src/rule-packs/nextjs/rules/no-sensitive-next-public-env.ts +72 -0
- package/src/rule-packs/nextjs/rules/prefer-lazy-use-state-init.ts +85 -0
- package/src/rule-packs/nextjs/rules/server-action-requires-authz-and-validation.ts +178 -0
- package/src/rule-packs/nextjs/rules/server-only-modules-import-server-only.ts +87 -0
- package/src/rule-packs/nextjs/utils.ts +18 -0
- package/src/rule-packs/react-component-architecture/index.ts +18 -0
- package/src/rule-packs/react-component-architecture/rules/dangerous-html-requires-sanitize.ts +83 -0
- package/src/rule-packs/react-component-architecture/rules/no-anonymous-useEffect.ts +61 -0
- package/src/rule-packs/react-component-architecture/rules/no-component-invocation.ts +55 -0
- package/src/rule-packs/react-component-architecture/rules/no-derived-state-in-effect.ts +204 -0
- package/src/rule-packs/react-component-architecture/rules/no-nested-component.ts +152 -0
- package/src/rule-packs/react-component-architecture/rules/no-react-fc.ts +57 -0
- package/src/rule-packs/rule-catalog.types.ts +21 -0
- package/src/rule-packs/rule-metadata.ts +163 -0
- package/src/rule-packs/runtime-boundaries/index.ts +33 -0
- package/src/rule-packs/runtime-boundaries/rules/no-prototype-polluting-merge.ts +113 -0
- package/src/rule-packs/runtime-boundaries/rules/no-user-controlled-fetch-url.ts +69 -0
- package/src/rule-packs/runtime-boundaries/rules/no-user-controlled-redirect.ts +79 -0
- package/src/rule-packs/runtime-boundaries/rules/upload-must-set-limits.ts +126 -0
- package/src/rule-packs/runtime-boundaries/rules/webhook-must-verify-signature-before-parse.ts +87 -0
- package/src/rule-packs/security/index.ts +35 -0
- package/src/rule-packs/security/rules/catch-must-handle.ts +126 -0
- package/src/rule-packs/security/rules/no-auth-token-in-storage.ts +107 -0
- package/src/rule-packs/security/rules/no-child-process-exec.ts +72 -0
- package/src/rule-packs/security/rules/no-dynamic-regexp.ts +56 -0
- package/src/rule-packs/security/rules/no-inner-html-assignment.ts +42 -0
- package/src/rule-packs/security/rules/no-spawn-with-shell.ts +106 -0
- package/src/rule-packs/structured-logging/index.ts +6 -0
- package/src/rule-packs/structured-logging/rules/caught-error-log-requires-cause.ts +234 -0
- package/src/rule-packs/structured-logging/rules/logger-not-console.ts +146 -0
- package/src/rule-packs/test-conventions/index.ts +9 -0
- package/src/rule-packs/test-conventions/rules/fake-timers-must-be-restored.ts +143 -0
- package/src/rule-packs/test-conventions/rules/no-conditional-expect.ts +77 -0
- package/src/rule-packs/test-conventions/rules/no-real-network-in-unit-tests.ts +174 -0
- package/src/rule-packs/typescript-core/index.ts +30 -0
- package/src/rule-packs/typescript-core/rules/exported-functions-require-return-type.ts +74 -0
- package/src/rule-packs/typescript-core/rules/fetch-must-check-ok.ts +106 -0
- package/src/rule-packs/typescript-core/rules/json-parse-must-validate.ts +97 -0
- package/src/rule-packs/typescript-core/rules/no-unsafe-boundary-cast.ts +70 -0
- package/src/stack-detection/packs.ts +57 -0
- package/strict.web.eslint.config.mjs +32 -1
|
@@ -154,6 +154,11 @@
|
|
|
154
154
|
"bad": "// Example that violates the rule",
|
|
155
155
|
"good": "// Corrected version"
|
|
156
156
|
},
|
|
157
|
+
"tsforge/no-throw-literal": {
|
|
158
|
+
"what": "Disallow throwing primitive literals (strings, numbers) — throw Error instances so error handlers can propagate status and stack traces correctly.",
|
|
159
|
+
"bad": "// Example that violates the rule",
|
|
160
|
+
"good": "// Corrected version"
|
|
161
|
+
},
|
|
157
162
|
"tsforge/prefer-early-return": {
|
|
158
163
|
"what": "Prefer guard clauses (early return) over wrapping the function body in a multi-statement `if` without an `else`.",
|
|
159
164
|
"bad": "// Example that violates the rule",
|
|
@@ -255,7 +260,37 @@
|
|
|
255
260
|
"good": "// Corrected version"
|
|
256
261
|
},
|
|
257
262
|
"tsforge/require-plugin-name": {
|
|
258
|
-
"what": "
|
|
263
|
+
"what": "fastify-plugin (fp) wrappers must include a `name` option so Fastify can deduplicate plugin registration.",
|
|
264
|
+
"bad": "// Example that violates the rule",
|
|
265
|
+
"good": "// Corrected version"
|
|
266
|
+
},
|
|
267
|
+
"tsforge/error-handler-must-set-status": {
|
|
268
|
+
"what": "Custom Fastify setErrorHandler callbacks must call reply.code() or reply.status() — automatic status mapping is disabled when a custom handler is registered.",
|
|
269
|
+
"bad": "// Example that violates the rule",
|
|
270
|
+
"good": "// Corrected version"
|
|
271
|
+
},
|
|
272
|
+
"tsforge/prefer-return-over-reply-send": {
|
|
273
|
+
"what": "Inside Fastify route handlers, prefer `return data` over `return reply.send(data)` so fast-json-stringify can serialize responses.",
|
|
274
|
+
"bad": "// Example that violates the rule",
|
|
275
|
+
"good": "// Corrected version"
|
|
276
|
+
},
|
|
277
|
+
"tsforge/require-fp-for-shared-plugins": {
|
|
278
|
+
"what": "Fastify plugins that call fastify.decorate, fastify.addHook, or fastify.register must be wrapped in fastify-plugin (fp) to break encapsulation and share state.",
|
|
279
|
+
"bad": "// Example that violates the rule",
|
|
280
|
+
"good": "// Corrected version"
|
|
281
|
+
},
|
|
282
|
+
"tsforge/require-response-schema": {
|
|
283
|
+
"what": "Fastify routes should declare schema.response for compiled fast-json-stringify serialization.",
|
|
284
|
+
"bad": "// Example that violates the rule",
|
|
285
|
+
"good": "// Corrected version"
|
|
286
|
+
},
|
|
287
|
+
"tsforge/require-route-schema": {
|
|
288
|
+
"what": "Fastify POST/PUT/PATCH routes must declare schema.body; GET/DELETE routes must declare schema.querystring or schema.params.",
|
|
289
|
+
"bad": "// Example that violates the rule",
|
|
290
|
+
"good": "// Corrected version"
|
|
291
|
+
},
|
|
292
|
+
"tsforge/test-inject-must-close-app": {
|
|
293
|
+
"what": "Test files using fastify.inject must register teardown that calls app.close() to drain connections.",
|
|
259
294
|
"bad": "// Example that violates the rule",
|
|
260
295
|
"good": "// Corrected version"
|
|
261
296
|
},
|
|
@@ -299,11 +334,36 @@
|
|
|
299
334
|
"bad": "// Example that violates the rule",
|
|
300
335
|
"good": "// Corrected version"
|
|
301
336
|
},
|
|
337
|
+
"tsforge/no-react-in-services": {
|
|
338
|
+
"what": "Service and data-fetch modules must not import React — keep business logic decoupled from the view layer.",
|
|
339
|
+
"bad": "// Example that violates the rule",
|
|
340
|
+
"good": "// Corrected version"
|
|
341
|
+
},
|
|
342
|
+
"tsforge/await-dynamic-request-apis": {
|
|
343
|
+
"what": "Require awaiting Next.js dynamic request APIs (cookies, headers, draftMode) in app-router Server Components.",
|
|
344
|
+
"bad": "// Example that violates the rule",
|
|
345
|
+
"good": "// Corrected version"
|
|
346
|
+
},
|
|
302
347
|
"tsforge/client-hooks-require-use-client": {
|
|
303
348
|
"what": "Require the 'use client' directive in app-router page/layout/template files that call client-only hooks. Server Components cannot use state/effect/navigation hooks — doing so crashes at runtime.",
|
|
304
349
|
"bad": "// Example that violates the rule",
|
|
305
350
|
"good": "// Corrected version"
|
|
306
351
|
},
|
|
352
|
+
"tsforge/error-boundary-require-use-client": {
|
|
353
|
+
"what": "Require 'use client' in app-router error.tsx and global-error.tsx — Next.js error boundaries must be Client Components.",
|
|
354
|
+
"bad": "// Example that violates the rule",
|
|
355
|
+
"good": "// Corrected version"
|
|
356
|
+
},
|
|
357
|
+
"tsforge/no-html-img-element": {
|
|
358
|
+
"what": "Prefer next/image over raw <img> elements for optimized responsive images and Core Web Vitals.",
|
|
359
|
+
"bad": "// Example that violates the rule",
|
|
360
|
+
"good": "// Corrected version"
|
|
361
|
+
},
|
|
362
|
+
"tsforge/no-internal-api-fetch": {
|
|
363
|
+
"what": "Disallow Server Components from fetching the app's own /api routes — import services or ORM modules directly to avoid loopback HTTP overhead.",
|
|
364
|
+
"bad": "// Example that violates the rule",
|
|
365
|
+
"good": "// Corrected version"
|
|
366
|
+
},
|
|
307
367
|
"tsforge/no-next-head-in-app": {
|
|
308
368
|
"what": "Disallow importing 'next/head' in app-router files. The <Head> component is a no-op under app/ — use the Metadata API (export const metadata / generateMetadata) instead.",
|
|
309
369
|
"bad": "// Example that violates the rule",
|
|
@@ -314,6 +374,16 @@
|
|
|
314
374
|
"bad": "// Example that violates the rule",
|
|
315
375
|
"good": "// Corrected version"
|
|
316
376
|
},
|
|
377
|
+
"tsforge/no-sensitive-next-public-env": {
|
|
378
|
+
"what": "Disallow NEXT_PUBLIC_* env vars whose names suggest secrets — public build-time vars are visible in the client bundle.",
|
|
379
|
+
"bad": "// Example that violates the rule",
|
|
380
|
+
"good": "// Corrected version"
|
|
381
|
+
},
|
|
382
|
+
"tsforge/prefer-lazy-use-state-init": {
|
|
383
|
+
"what": "Prefer lazy useState initializers when parsing localStorage/sessionStorage — avoids re-parsing on every render.",
|
|
384
|
+
"bad": "// Example that violates the rule",
|
|
385
|
+
"good": "// Corrected version"
|
|
386
|
+
},
|
|
317
387
|
"tsforge/pkce-required-for-oidc": {
|
|
318
388
|
"what": "OIDC providers must use PKCE: `buildAuthorizationURL` must call `generateCodeVerifier()` and pass it to `createAuthorizationURL`.",
|
|
319
389
|
"bad": "// Example that violates the rule",
|
|
@@ -334,6 +404,11 @@
|
|
|
334
404
|
"bad": "// Example that violates the rule",
|
|
335
405
|
"good": "// Corrected version"
|
|
336
406
|
},
|
|
407
|
+
"tsforge/dangerous-html-requires-sanitize": {
|
|
408
|
+
"what": "dangerouslySetInnerHTML requires a sanitization library (DOMPurify or equivalent) imported in the same file.",
|
|
409
|
+
"bad": "// Example that violates the rule",
|
|
410
|
+
"good": "// Corrected version"
|
|
411
|
+
},
|
|
337
412
|
"tsforge/forwardref-display-name": {
|
|
338
413
|
"what": "forwardRef components must have displayName set",
|
|
339
414
|
"bad": "// Example that violates the rule",
|
|
@@ -349,11 +424,26 @@
|
|
|
349
424
|
"bad": "// Example that violates the rule",
|
|
350
425
|
"good": "// Corrected version"
|
|
351
426
|
},
|
|
427
|
+
"tsforge/no-anonymous-useEffect": {
|
|
428
|
+
"what": "Disallow anonymous arrow functions passed to useEffect — use a named function for debuggable stack traces.",
|
|
429
|
+
"bad": "// Example that violates the rule",
|
|
430
|
+
"good": "// Corrected version"
|
|
431
|
+
},
|
|
432
|
+
"tsforge/no-component-invocation": {
|
|
433
|
+
"what": "Disallow invoking React components as plain functions — use JSX (`<Header />`) instead of `{Header()}`.",
|
|
434
|
+
"bad": "// Example that violates the rule",
|
|
435
|
+
"good": "// Corrected version"
|
|
436
|
+
},
|
|
352
437
|
"tsforge/no-cross-feature-imports": {
|
|
353
438
|
"what": "Prevent imports across different features",
|
|
354
439
|
"bad": "// Example that violates the rule",
|
|
355
440
|
"good": "// Corrected version"
|
|
356
441
|
},
|
|
442
|
+
"tsforge/no-derived-state-in-effect": {
|
|
443
|
+
"what": "Disallow setting local state inside useEffect when the value can be derived during render (or memoized with useMemo).",
|
|
444
|
+
"bad": "// Example that violates the rule",
|
|
445
|
+
"good": "// Corrected version"
|
|
446
|
+
},
|
|
357
447
|
"tsforge/no-inline-jsx-functions": {
|
|
358
448
|
"what": "Disallow inline function expressions in JSX attributes",
|
|
359
449
|
"bad": "// Example that violates the rule",
|
|
@@ -364,11 +454,51 @@
|
|
|
364
454
|
"bad": "// Example that violates the rule",
|
|
365
455
|
"good": "// Corrected version"
|
|
366
456
|
},
|
|
457
|
+
"tsforge/no-nested-component": {
|
|
458
|
+
"what": "Disallow declaring React components inside another component body — nested components reset state on every parent render.",
|
|
459
|
+
"bad": "// Example that violates the rule",
|
|
460
|
+
"good": "// Corrected version"
|
|
461
|
+
},
|
|
462
|
+
"tsforge/no-react-fc": {
|
|
463
|
+
"what": "Disallow React.FC / FunctionComponent — type props explicitly on the function parameter instead.",
|
|
464
|
+
"bad": "// Example that violates the rule",
|
|
465
|
+
"good": "// Corrected version"
|
|
466
|
+
},
|
|
367
467
|
"tsforge/no-state-in-component-body": {
|
|
368
468
|
"what": "State hooks must be in .hooks.ts files, not directly in components",
|
|
369
469
|
"bad": "// Example that violates the rule",
|
|
370
470
|
"good": "// Corrected version"
|
|
371
471
|
},
|
|
472
|
+
"tsforge/catch-must-handle": {
|
|
473
|
+
"what": "Catch blocks must log, rethrow, or propagate errors — not silently return empty defaults on failure.",
|
|
474
|
+
"bad": "// Example that violates the rule",
|
|
475
|
+
"good": "// Corrected version"
|
|
476
|
+
},
|
|
477
|
+
"tsforge/no-auth-token-in-storage": {
|
|
478
|
+
"what": "Disallow storing or reading auth tokens from localStorage/sessionStorage — use httpOnly cookies instead.",
|
|
479
|
+
"bad": "// Example that violates the rule",
|
|
480
|
+
"good": "// Corrected version"
|
|
481
|
+
},
|
|
482
|
+
"tsforge/no-child-process-exec": {
|
|
483
|
+
"what": "Disallow child_process.exec/execSync — they run commands in a shell. Use execFile or spawn without shell instead.",
|
|
484
|
+
"bad": "// Example that violates the rule",
|
|
485
|
+
"good": "// Corrected version"
|
|
486
|
+
},
|
|
487
|
+
"tsforge/no-dynamic-regexp": {
|
|
488
|
+
"what": "Disallow new RegExp(non-literal) — dynamic patterns enable ReDoS. Use string-literal regexes or a safe engine like re2.",
|
|
489
|
+
"bad": "// Example that violates the rule",
|
|
490
|
+
"good": "// Corrected version"
|
|
491
|
+
},
|
|
492
|
+
"tsforge/no-inner-html-assignment": {
|
|
493
|
+
"what": "Disallow assigning to innerHTML — use textContent/innerText or sanitize with DOMPurify before injecting HTML.",
|
|
494
|
+
"bad": "// Example that violates the rule",
|
|
495
|
+
"good": "// Corrected version"
|
|
496
|
+
},
|
|
497
|
+
"tsforge/no-spawn-with-shell": {
|
|
498
|
+
"what": "Disallow child_process.spawn/spawnSync with shell: true — shell execution enables command injection.",
|
|
499
|
+
"bad": "// Example that violates the rule",
|
|
500
|
+
"good": "// Corrected version"
|
|
501
|
+
},
|
|
372
502
|
"tsforge/mask-pii-fields": {
|
|
373
503
|
"what": "Disallow unmasked PII (email, phone, password, token, ...) in structured-logger payloads — the #1 way data leaks quietly.",
|
|
374
504
|
"bad": "// Example that violates the rule",
|
package/src/loop/run.ts
CHANGED
|
@@ -366,6 +366,9 @@ export async function runTask(
|
|
|
366
366
|
role: "assistant",
|
|
367
367
|
content: res.content,
|
|
368
368
|
toolCalls: res.toolCalls,
|
|
369
|
+
...(res.reasoning === undefined
|
|
370
|
+
? {}
|
|
371
|
+
: { reasoningContent: res.reasoning }),
|
|
369
372
|
});
|
|
370
373
|
|
|
371
374
|
// Every model call advances cooldown accounting — including interrupted
|
package/src/loop/session.ts
CHANGED
|
@@ -144,6 +144,17 @@ export interface ISendOptions {
|
|
|
144
144
|
|
|
145
145
|
const SESSION_ID = "session";
|
|
146
146
|
|
|
147
|
+
/** Build the assistant history message, carrying `reasoningContent` when the
|
|
148
|
+
* model produced it (DeepSeek's thinking mode requires it replayed). */
|
|
149
|
+
function assistantMessage(res: IModelResponse): IChatMessage {
|
|
150
|
+
return {
|
|
151
|
+
role: "assistant",
|
|
152
|
+
content: res.content,
|
|
153
|
+
toolCalls: res.toolCalls,
|
|
154
|
+
...(res.reasoning === undefined ? {} : { reasoningContent: res.reasoning }),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
147
158
|
/** Default share of the context window that triggers auto-compaction. */
|
|
148
159
|
const AUTO_COMPACT_AT = 0.8;
|
|
149
160
|
|
|
@@ -1054,11 +1065,7 @@ export class Session {
|
|
|
1054
1065
|
});
|
|
1055
1066
|
}
|
|
1056
1067
|
|
|
1057
|
-
ctx.messages.push(
|
|
1058
|
-
role: "assistant",
|
|
1059
|
-
content: res.content,
|
|
1060
|
-
toolCalls: res.toolCalls,
|
|
1061
|
-
});
|
|
1068
|
+
ctx.messages.push(assistantMessage(res));
|
|
1062
1069
|
|
|
1063
1070
|
if (res.salvaged !== undefined && res.salvaged > 0) {
|
|
1064
1071
|
report({
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ITtsrRule } from "./ttsr";
|
|
2
2
|
|
|
3
|
+
const SRC_TS_GLOBS = ["src/**/*.ts", "src/**/*.tsx"] as const;
|
|
4
|
+
const SRC_TSX_GLOBS = ["src/**/*.tsx"] as const;
|
|
5
|
+
|
|
3
6
|
/**
|
|
4
7
|
* Built-in TTSR rules: code quality patterns to abort and correct.
|
|
5
8
|
* All scope tool-args (source of the problem), fileGlobs target src/**\/*.ts(x).
|
|
@@ -10,7 +13,7 @@ export const DEFAULT_TTSR_RULES: readonly ITtsrRule[] = [
|
|
|
10
13
|
name: "no-as-any",
|
|
11
14
|
condition: [/\bas\s+any\b/],
|
|
12
15
|
scope: "tool-args",
|
|
13
|
-
fileGlobs: [
|
|
16
|
+
fileGlobs: [...SRC_TS_GLOBS],
|
|
14
17
|
guidance:
|
|
15
18
|
"Never use 'as any'. If the type is unknown, use 'unknown' or a proper type. " +
|
|
16
19
|
"If the API is untyped, consider a declaration file.",
|
|
@@ -21,7 +24,7 @@ export const DEFAULT_TTSR_RULES: readonly ITtsrRule[] = [
|
|
|
21
24
|
name: "no-ts-suppression",
|
|
22
25
|
condition: [/@ts-(?:ignore|nocheck)/],
|
|
23
26
|
scope: "tool-args",
|
|
24
|
-
fileGlobs: [
|
|
27
|
+
fileGlobs: [...SRC_TS_GLOBS],
|
|
25
28
|
guidance:
|
|
26
29
|
"Never suppress TypeScript with @ts-ignore/@ts-nocheck. Fix the real error; " +
|
|
27
30
|
"if the library is untyped, add a declaration file instead.",
|
|
@@ -32,7 +35,7 @@ export const DEFAULT_TTSR_RULES: readonly ITtsrRule[] = [
|
|
|
32
35
|
name: "no-empty-catch",
|
|
33
36
|
condition: [/catch\s*(?:\([^)]*\))?\s*\{\s*\}/],
|
|
34
37
|
scope: "tool-args",
|
|
35
|
-
fileGlobs: [
|
|
38
|
+
fileGlobs: [...SRC_TS_GLOBS],
|
|
36
39
|
guidance:
|
|
37
40
|
"Empty catch blocks hide errors. Log them or handle them: " +
|
|
38
41
|
"catch (e) { console.error(e); } at minimum.",
|
|
@@ -43,11 +46,179 @@ export const DEFAULT_TTSR_RULES: readonly ITtsrRule[] = [
|
|
|
43
46
|
name: "no-console-log",
|
|
44
47
|
condition: [/\bconsole\.(?:log|debug)\s*\(/],
|
|
45
48
|
scope: "tool-args",
|
|
46
|
-
fileGlobs: [
|
|
49
|
+
fileGlobs: [...SRC_TS_GLOBS],
|
|
47
50
|
guidance:
|
|
48
51
|
"Remove console.log/debug before shipping. Use a logger or remove the line. " +
|
|
49
52
|
"Tests can call console.log; production code must not.",
|
|
50
53
|
repeatMode: "cooldown",
|
|
51
54
|
repeatGap: 5,
|
|
52
55
|
},
|
|
56
|
+
{
|
|
57
|
+
name: "no-react-fc",
|
|
58
|
+
condition: [
|
|
59
|
+
/React\.(?:FC|FunctionComponent|VFC)\b/,
|
|
60
|
+
/\bFunctionComponent\s*</,
|
|
61
|
+
],
|
|
62
|
+
scope: "tool-args",
|
|
63
|
+
fileGlobs: [...SRC_TSX_GLOBS],
|
|
64
|
+
guidance:
|
|
65
|
+
"Do not use React.FC — type props explicitly on the function parameter instead.",
|
|
66
|
+
repeatMode: "cooldown",
|
|
67
|
+
repeatGap: 5,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "no-throw-literal",
|
|
71
|
+
condition: [/throw\s+['"`]/, /throw\s+\d+/],
|
|
72
|
+
scope: "tool-args",
|
|
73
|
+
fileGlobs: [...SRC_TS_GLOBS],
|
|
74
|
+
guidance:
|
|
75
|
+
"Do not throw string/number literals — throw `new Error('...')` so error handlers propagate correctly.",
|
|
76
|
+
repeatMode: "cooldown",
|
|
77
|
+
repeatGap: 5,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "dangerous-html-unsanitized",
|
|
81
|
+
condition: [/dangerouslySetInnerHTML/],
|
|
82
|
+
scope: "tool-args",
|
|
83
|
+
fileGlobs: [...SRC_TSX_GLOBS],
|
|
84
|
+
guidance:
|
|
85
|
+
"dangerouslySetInnerHTML requires sanitizing with DOMPurify (or isomorphic-dompurify) before rendering.",
|
|
86
|
+
repeatMode: "cooldown",
|
|
87
|
+
repeatGap: 5,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "no-child-process-exec",
|
|
91
|
+
condition: [/\bchild_process\.exec\b/, /\bexecSync\s*\(/],
|
|
92
|
+
scope: "tool-args",
|
|
93
|
+
fileGlobs: [...SRC_TS_GLOBS],
|
|
94
|
+
guidance:
|
|
95
|
+
"Do not use child_process.exec/execSync — use execFile or spawn without shell to avoid command injection.",
|
|
96
|
+
repeatMode: "cooldown",
|
|
97
|
+
repeatGap: 5,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "no-inner-html-assignment",
|
|
101
|
+
condition: [/\.innerHTML\s*=/],
|
|
102
|
+
scope: "tool-args",
|
|
103
|
+
fileGlobs: [...SRC_TS_GLOBS],
|
|
104
|
+
guidance:
|
|
105
|
+
"Do not assign to innerHTML — use textContent for plain text or sanitize with DOMPurify first.",
|
|
106
|
+
repeatMode: "cooldown",
|
|
107
|
+
repeatGap: 5,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "no-dynamic-regexp",
|
|
111
|
+
condition: [/new RegExp\s*\(\s*(?!['"`])/],
|
|
112
|
+
scope: "tool-args",
|
|
113
|
+
fileGlobs: [...SRC_TS_GLOBS],
|
|
114
|
+
guidance:
|
|
115
|
+
"Do not construct RegExp from runtime values — use a string-literal pattern or a safe regex library to avoid ReDoS.",
|
|
116
|
+
repeatMode: "cooldown",
|
|
117
|
+
repeatGap: 5,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "no-internal-api-fetch",
|
|
121
|
+
condition: [/fetch\s*\(\s*['"`]\/api/],
|
|
122
|
+
scope: "tool-args",
|
|
123
|
+
fileGlobs: [...SRC_TS_GLOBS],
|
|
124
|
+
guidance:
|
|
125
|
+
"Server Components must not fetch /api routes — import the service or ORM module directly.",
|
|
126
|
+
repeatMode: "cooldown",
|
|
127
|
+
repeatGap: 5,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "no-unawaited-cookies",
|
|
131
|
+
condition: [/(?<![\w.])cookies\s*\(\s*\)/],
|
|
132
|
+
scope: "tool-args",
|
|
133
|
+
fileGlobs: [...SRC_TS_GLOBS],
|
|
134
|
+
guidance:
|
|
135
|
+
"Await dynamic request APIs: `const jar = await cookies()` in Server Components.",
|
|
136
|
+
repeatMode: "cooldown",
|
|
137
|
+
repeatGap: 5,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "no-html-img",
|
|
141
|
+
condition: [/<img[\s/>]/],
|
|
142
|
+
scope: "tool-args",
|
|
143
|
+
fileGlobs: [...SRC_TSX_GLOBS],
|
|
144
|
+
guidance:
|
|
145
|
+
"Use next/image `<Image />` instead of raw `<img>` for optimized responsive images.",
|
|
146
|
+
repeatMode: "cooldown",
|
|
147
|
+
repeatGap: 5,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "no-sensitive-next-public-env",
|
|
151
|
+
condition: [
|
|
152
|
+
/NEXT_PUBLIC_(?:.*(?:SECRET|PRIVATE|PASSWORD|TOKEN|DATABASE|STRIPE|KEY))/i,
|
|
153
|
+
],
|
|
154
|
+
scope: "tool-args",
|
|
155
|
+
fileGlobs: [...SRC_TS_GLOBS],
|
|
156
|
+
guidance:
|
|
157
|
+
"Never prefix secret env vars with NEXT_PUBLIC_ — they are embedded in the client bundle.",
|
|
158
|
+
repeatMode: "cooldown",
|
|
159
|
+
repeatGap: 5,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "no-auth-token-in-storage",
|
|
163
|
+
condition: [
|
|
164
|
+
/localStorage\.(?:setItem|getItem).*(?:token|session|auth|jwt)/i,
|
|
165
|
+
/sessionStorage\.(?:setItem|getItem).*(?:token|session|auth|jwt)/i,
|
|
166
|
+
],
|
|
167
|
+
scope: "tool-args",
|
|
168
|
+
fileGlobs: [...SRC_TS_GLOBS],
|
|
169
|
+
guidance:
|
|
170
|
+
"Do not store auth tokens in localStorage — use httpOnly secure cookies instead.",
|
|
171
|
+
repeatMode: "cooldown",
|
|
172
|
+
repeatGap: 5,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "no-user-controlled-redirect",
|
|
176
|
+
condition: [/redirect\s*\(\s*(?!['"`])/],
|
|
177
|
+
scope: "tool-args",
|
|
178
|
+
fileGlobs: [...SRC_TS_GLOBS],
|
|
179
|
+
guidance:
|
|
180
|
+
"Do not redirect to a runtime URL — use a string literal path or an allowlisted helper.",
|
|
181
|
+
repeatMode: "cooldown",
|
|
182
|
+
repeatGap: 5,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "fetch-without-ok-check",
|
|
186
|
+
condition: [/\.json\s*\(\s*\)/],
|
|
187
|
+
scope: "tool-args",
|
|
188
|
+
fileGlobs: [...SRC_TS_GLOBS],
|
|
189
|
+
guidance:
|
|
190
|
+
"Check response.ok (or status) before calling .json() on a fetch response.",
|
|
191
|
+
repeatMode: "cooldown",
|
|
192
|
+
repeatGap: 5,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: "server-only-missing",
|
|
196
|
+
condition: [/from\s+['"]@\/lib\/(?:db|database|auth)/],
|
|
197
|
+
scope: "tool-args",
|
|
198
|
+
fileGlobs: [...SRC_TS_GLOBS],
|
|
199
|
+
guidance:
|
|
200
|
+
"Add `import 'server-only';` at the top of modules that import DB or auth internals.",
|
|
201
|
+
repeatMode: "cooldown",
|
|
202
|
+
repeatGap: 5,
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "server-action-without-parse",
|
|
206
|
+
condition: [/"use server"[\s\S]{0,200}(?:update|insert|delete)\s*\(/],
|
|
207
|
+
scope: "tool-args",
|
|
208
|
+
fileGlobs: [...SRC_TS_GLOBS],
|
|
209
|
+
guidance:
|
|
210
|
+
"Server actions must call `.parse(` or `.safeParse(` on input before database writes.",
|
|
211
|
+
repeatMode: "cooldown",
|
|
212
|
+
repeatGap: 5,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: "no-secret-props-to-client",
|
|
216
|
+
condition: [/(?:token|session|password|secret)=\{/i],
|
|
217
|
+
scope: "tool-args",
|
|
218
|
+
fileGlobs: [...SRC_TSX_GLOBS],
|
|
219
|
+
guidance:
|
|
220
|
+
"Do not pass session/token/password props from Server Components to Client Components.",
|
|
221
|
+
repeatMode: "cooldown",
|
|
222
|
+
repeatGap: 5,
|
|
223
|
+
},
|
|
53
224
|
];
|
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
import type { IMetaRule } from "./meta-rules.types";
|
|
2
2
|
import { packageExactDepsRule } from "./rules/supply-chain/package-exact-deps";
|
|
3
|
+
import { fastifySecurityPluginsRule } from "./rules/supply-chain/fastify-security-plugins";
|
|
3
4
|
import { noOverlappingLibsRule } from "./rules/supply-chain/no-overlapping-libs";
|
|
5
|
+
import { lockfileRequiredRule } from "./rules/supply-chain/lockfile-required";
|
|
6
|
+
import { singlePackageManagerRule } from "./rules/supply-chain/single-package-manager";
|
|
7
|
+
import { packageManagerFieldRequiredRule } from "./rules/supply-chain/package-manager-field-required";
|
|
8
|
+
import { noGitOrTarballDependenciesRule } from "./rules/supply-chain/no-git-or-tarball-dependencies";
|
|
9
|
+
import { dependencyOverridesRequireCommentRule } from "./rules/supply-chain/dependency-overrides-require-comment";
|
|
10
|
+
import { productionMustNotUseDrizzlePushRule } from "./rules/supply-chain/production-must-not-use-drizzle-push";
|
|
11
|
+
import { migrationsMustBeCheckedInRule } from "./rules/supply-chain/migrations-must-be-checked-in";
|
|
4
12
|
import { noEslintDisableCommentsRule } from "./rules/source-text/no-eslint-disable-comments";
|
|
5
13
|
import { noTsSuppressionRule } from "./rules/source-text/no-ts-suppressions";
|
|
14
|
+
import { nextImageRemotePatternsNoWildcardsRule } from "./rules/config/next-image-remote-patterns-no-wildcards";
|
|
15
|
+
import { nextInstrumentationPresentRule } from "./rules/config/next-instrumentation-present";
|
|
16
|
+
import { nextProxyOverMiddlewareRule } from "./rules/config/next-proxy-over-middleware";
|
|
6
17
|
import { tsconfigPathsExistRule } from "./rules/config/tsconfig-paths-exist";
|
|
18
|
+
import { tsconfigRecommendedFlagsRule } from "./rules/config/tsconfig-recommended-flags";
|
|
7
19
|
import { tsconfigStrictRule } from "./rules/config/tsconfig-strict";
|
|
8
20
|
import { testSiblingRequiredRule } from "./rules/testing/test-sibling-required";
|
|
9
21
|
import { workflowActionsPinnedRule } from "./rules/ci/workflow-actions-pinned";
|
|
10
22
|
import { workflowRunnerPinnedRule } from "./rules/ci/workflow-runner-pinned";
|
|
11
23
|
import { workflowTimeoutRequiredRule } from "./rules/ci/workflow-timeout-required";
|
|
24
|
+
import { workflowPermissionsExplicitRule } from "./rules/ci/workflow-permissions-explicit";
|
|
25
|
+
import { workflowPermissionsLeastPrivilegeRule } from "./rules/ci/workflow-permissions-least-privilege";
|
|
26
|
+
import { noPullRequestTargetUntrustedCheckoutRule } from "./rules/ci/no-pull-request-target-untrusted-checkout";
|
|
27
|
+
import { noGithubContextInShellRule } from "./rules/ci/no-github-context-in-shell";
|
|
12
28
|
|
|
13
29
|
/**
|
|
14
30
|
* All available meta-rules, ordered by category for readability.
|
|
@@ -17,7 +33,15 @@ import { workflowTimeoutRequiredRule } from "./rules/ci/workflow-timeout-require
|
|
|
17
33
|
export const META_RULES: readonly IMetaRule[] = [
|
|
18
34
|
// Supply chain
|
|
19
35
|
packageExactDepsRule,
|
|
36
|
+
fastifySecurityPluginsRule,
|
|
20
37
|
noOverlappingLibsRule,
|
|
38
|
+
lockfileRequiredRule,
|
|
39
|
+
singlePackageManagerRule,
|
|
40
|
+
packageManagerFieldRequiredRule,
|
|
41
|
+
noGitOrTarballDependenciesRule,
|
|
42
|
+
dependencyOverridesRequireCommentRule,
|
|
43
|
+
productionMustNotUseDrizzlePushRule,
|
|
44
|
+
migrationsMustBeCheckedInRule,
|
|
21
45
|
|
|
22
46
|
// Source text
|
|
23
47
|
noEslintDisableCommentsRule,
|
|
@@ -25,7 +49,11 @@ export const META_RULES: readonly IMetaRule[] = [
|
|
|
25
49
|
|
|
26
50
|
// Config
|
|
27
51
|
tsconfigPathsExistRule,
|
|
52
|
+
tsconfigRecommendedFlagsRule,
|
|
28
53
|
tsconfigStrictRule,
|
|
54
|
+
nextProxyOverMiddlewareRule,
|
|
55
|
+
nextInstrumentationPresentRule,
|
|
56
|
+
nextImageRemotePatternsNoWildcardsRule,
|
|
29
57
|
|
|
30
58
|
// Testing
|
|
31
59
|
testSiblingRequiredRule,
|
|
@@ -34,4 +62,8 @@ export const META_RULES: readonly IMetaRule[] = [
|
|
|
34
62
|
workflowActionsPinnedRule,
|
|
35
63
|
workflowRunnerPinnedRule,
|
|
36
64
|
workflowTimeoutRequiredRule,
|
|
65
|
+
workflowPermissionsExplicitRule,
|
|
66
|
+
workflowPermissionsLeastPrivilegeRule,
|
|
67
|
+
noPullRequestTargetUntrustedCheckoutRule,
|
|
68
|
+
noGithubContextInShellRule,
|
|
37
69
|
];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { IMetaRule, IMetaRuleViolation } from "../../meta-rules.types";
|
|
2
|
+
|
|
3
|
+
const RUN_WITH_GITHUB_EVENT = /^ {4,}-?\s*run:\s.*\$\{\{\s*github\.event/u;
|
|
4
|
+
|
|
5
|
+
export const noGithubContextInShellRule: IMetaRule = {
|
|
6
|
+
id: "no-github-context-in-shell",
|
|
7
|
+
category: "ci",
|
|
8
|
+
description:
|
|
9
|
+
"Do not interpolate github.event context directly in run: shell steps — pass values through env: first.",
|
|
10
|
+
severity: "warn",
|
|
11
|
+
run({ workflowFiles, readFile }) {
|
|
12
|
+
const violations: IMetaRuleViolation[] = [];
|
|
13
|
+
|
|
14
|
+
for (const file of workflowFiles) {
|
|
15
|
+
const text = readFile(file);
|
|
16
|
+
|
|
17
|
+
if (text === null) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const lines = text.split("\n");
|
|
22
|
+
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
if (!RUN_WITH_GITHUB_EVENT.test(line)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
violations.push({
|
|
29
|
+
file,
|
|
30
|
+
ruleId: "no-github-context-in-shell",
|
|
31
|
+
severity: "warn",
|
|
32
|
+
message:
|
|
33
|
+
"Shell `run:` step interpolates `${{ github.event... }}` directly — assign the value to an `env:` entry and reference the env var in the script to avoid injection.",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return violations;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { IMetaRule, IMetaRuleViolation } from "../../meta-rules.types";
|
|
2
|
+
|
|
3
|
+
const PULL_REQUEST_TARGET_PATTERN = /(?:^|\s)pull_request_target(?:\s|:|$)/u;
|
|
4
|
+
const UNTRUSTED_CHECKOUT_PATTERN =
|
|
5
|
+
/github\.event\.pull_request\.head\.(?:sha|ref)/u;
|
|
6
|
+
|
|
7
|
+
export const noPullRequestTargetUntrustedCheckoutRule: IMetaRule = {
|
|
8
|
+
id: "no-pull-request-target-untrusted-checkout",
|
|
9
|
+
category: "ci",
|
|
10
|
+
description:
|
|
11
|
+
"Disallow pull_request_target workflows that checkout the PR head ref (untrusted code with write token).",
|
|
12
|
+
severity: "warn",
|
|
13
|
+
run({ workflowFiles, readFile }) {
|
|
14
|
+
const violations: IMetaRuleViolation[] = [];
|
|
15
|
+
|
|
16
|
+
for (const file of workflowFiles) {
|
|
17
|
+
const text = readFile(file);
|
|
18
|
+
|
|
19
|
+
if (text === null) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!PULL_REQUEST_TARGET_PATTERN.test(text)) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!UNTRUSTED_CHECKOUT_PATTERN.test(text)) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
violations.push({
|
|
32
|
+
file,
|
|
33
|
+
ruleId: "no-pull-request-target-untrusted-checkout",
|
|
34
|
+
severity: "warn",
|
|
35
|
+
message:
|
|
36
|
+
"`pull_request_target` combined with checkout of `github.event.pull_request.head.*` runs untrusted PR code with elevated workflow permissions — use `pull_request` or checkout the base ref instead.",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return violations;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { IMetaRule, IMetaRuleViolation } from "../../meta-rules.types";
|
|
2
|
+
import {
|
|
3
|
+
collectJobBlocks,
|
|
4
|
+
hasWorkflowLevelPermissions,
|
|
5
|
+
} from "../../utils/workflow-yaml";
|
|
6
|
+
|
|
7
|
+
export const workflowPermissionsExplicitRule: IMetaRule = {
|
|
8
|
+
id: "workflow-permissions-explicit",
|
|
9
|
+
category: "ci",
|
|
10
|
+
description:
|
|
11
|
+
"GitHub Actions workflows must declare permissions at the workflow or job level.",
|
|
12
|
+
severity: "warn",
|
|
13
|
+
run({ workflowFiles, readFile }) {
|
|
14
|
+
const violations: IMetaRuleViolation[] = [];
|
|
15
|
+
|
|
16
|
+
for (const file of workflowFiles) {
|
|
17
|
+
const text = readFile(file);
|
|
18
|
+
|
|
19
|
+
if (text === null) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (hasWorkflowLevelPermissions(text)) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const jobs = collectJobBlocks(text);
|
|
28
|
+
const jobsMissingPermissions = jobs.filter(
|
|
29
|
+
(job) =>
|
|
30
|
+
!job.lines.some((line) => /^ {4}permissions:\s*(?:#.*)?$/u.test(line))
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (jobsMissingPermissions.length === 0) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const jobNames = jobsMissingPermissions.map((job) => job.name).join(", ");
|
|
38
|
+
|
|
39
|
+
violations.push({
|
|
40
|
+
file,
|
|
41
|
+
ruleId: "workflow-permissions-explicit",
|
|
42
|
+
severity: "warn",
|
|
43
|
+
message: `Workflow is missing top-level \`permissions:\` and jobs without job-level permissions: ${jobNames}. Declare least-privilege permissions at workflow or job scope.`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return violations;
|
|
48
|
+
},
|
|
49
|
+
};
|