@agjs/tsforge 0.1.19 → 0.2.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/package.json +6 -2
- package/scripts/browser-check.ts +41 -5
- package/scripts/build-rules-md.ts +78 -21
- package/scripts/cli-metrics.ts +10 -0
- package/scripts/sweep.ts +53 -23
- package/scripts/web-sweep.ts +292 -0
- package/src/browser/index.ts +3 -0
- package/src/browser/oracle.ts +215 -8
- package/src/cli.ts +22 -4
- 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 +144 -13
- package/src/eval/eval.types.ts +9 -0
- package/src/eval/failure-class.ts +263 -0
- package/src/eval/index.ts +8 -0
- package/src/eval/metrics.ts +7 -0
- package/src/eval/parse-log.ts +105 -0
- package/src/eval/report.ts +19 -0
- package/src/eval/score.ts +10 -0
- package/src/loop/feedback/meta-rule-docs.ts +48 -0
- package/src/loop/feedback/rule-docs.ts +150 -0
- package/src/loop/loop.types.ts +4 -0
- package/src/loop/rule-docs.generated.json +131 -1
- package/src/loop/ttsr-defaults.ts +175 -4
- package/src/loop/turn.ts +3 -0
- 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.type-aware.eslint.config.mjs +33 -0
- package/strict.web.eslint.config.mjs +32 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { createRule } from "../../create-rule";
|
|
4
|
+
import { isTestFile } from "../../react-component-architecture/utils";
|
|
5
|
+
import { nodeContainsMemberCall } from "../utils/fastifyChain";
|
|
6
|
+
|
|
7
|
+
export const RULE_NAME = "test-inject-must-close-app";
|
|
8
|
+
|
|
9
|
+
type MessageIds = "missingAppClose";
|
|
10
|
+
|
|
11
|
+
export const testInjectMustCloseAppRule = createRule<[], MessageIds>({
|
|
12
|
+
name: RULE_NAME,
|
|
13
|
+
meta: {
|
|
14
|
+
type: "problem",
|
|
15
|
+
docs: {
|
|
16
|
+
description:
|
|
17
|
+
"Test files using fastify.inject must register teardown that calls app.close() to drain connections.",
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
messages: {
|
|
21
|
+
missingAppClose:
|
|
22
|
+
"Tests using `.inject(...)` must call `app.close()` in an `after`/`t.after` hook to drain connection pools.",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultOptions: [],
|
|
26
|
+
create(context) {
|
|
27
|
+
const filename = context.filename;
|
|
28
|
+
|
|
29
|
+
if (!isTestFile(filename)) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
"Program:exit"(program: TSESTree.Program) {
|
|
35
|
+
const usesInject = nodeContainsMemberCall(program, "inject");
|
|
36
|
+
const closesApp = nodeContainsMemberCall(program, "close");
|
|
37
|
+
|
|
38
|
+
if (usesInject && !closesApp) {
|
|
39
|
+
context.report({ node: program, messageId: "missingAppClose" });
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { walkSome } from "../../utils";
|
|
4
|
+
|
|
5
|
+
export const ROUTE_METHODS = new Set([
|
|
6
|
+
"get",
|
|
7
|
+
"post",
|
|
8
|
+
"put",
|
|
9
|
+
"patch",
|
|
10
|
+
"delete",
|
|
11
|
+
"head",
|
|
12
|
+
"options",
|
|
13
|
+
"all",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export const MUTATING_METHODS = new Set(["post", "put", "patch"]);
|
|
17
|
+
|
|
18
|
+
/** Collect identifiers bound to a Fastify instance (`Fastify()` / `require('fastify')()`). */
|
|
19
|
+
export function collectFastifyVariables(
|
|
20
|
+
program: TSESTree.Program
|
|
21
|
+
): Set<string> {
|
|
22
|
+
const vars = new Set<string>();
|
|
23
|
+
|
|
24
|
+
for (const statement of program.body) {
|
|
25
|
+
if (statement.type !== AST_NODE_TYPES.VariableDeclaration) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const decl of statement.declarations) {
|
|
30
|
+
if (decl.id.type !== AST_NODE_TYPES.Identifier || decl.init === null) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (isFastifyFactoryCall(decl.init)) {
|
|
35
|
+
vars.add(decl.id.name);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return vars;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isFastifyFactoryCall(node: TSESTree.Expression): boolean {
|
|
44
|
+
if (node.type !== AST_NODE_TYPES.CallExpression) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const callee = node.callee;
|
|
49
|
+
|
|
50
|
+
if (callee.type === AST_NODE_TYPES.CallExpression) {
|
|
51
|
+
return isFastifyFactoryCall(callee);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (callee.type === AST_NODE_TYPES.Identifier && callee.name === "Fastify") {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
60
|
+
!callee.computed &&
|
|
61
|
+
callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
62
|
+
callee.property.name === "default" &&
|
|
63
|
+
callee.object.type === AST_NODE_TYPES.Identifier &&
|
|
64
|
+
callee.object.name === "Fastify"
|
|
65
|
+
) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getRouteMethodName(
|
|
73
|
+
node: TSESTree.CallExpression,
|
|
74
|
+
fastifyVars: ReadonlySet<string>
|
|
75
|
+
): string | null {
|
|
76
|
+
const callee = node.callee;
|
|
77
|
+
|
|
78
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (
|
|
83
|
+
callee.object.type !== AST_NODE_TYPES.Identifier ||
|
|
84
|
+
!fastifyVars.has(callee.object.name)
|
|
85
|
+
) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (callee.property.type !== AST_NODE_TYPES.Identifier) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const method = callee.property.name;
|
|
94
|
+
|
|
95
|
+
return ROUTE_METHODS.has(method) ? method : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function findRouteOptionsArg(
|
|
99
|
+
node: TSESTree.CallExpression
|
|
100
|
+
): TSESTree.ObjectExpression | null {
|
|
101
|
+
const firstArg = node.arguments[0];
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
firstArg?.type === AST_NODE_TYPES.ObjectExpression &&
|
|
105
|
+
node.arguments.length >= 2
|
|
106
|
+
) {
|
|
107
|
+
return firstArg;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const secondArg = node.arguments[1];
|
|
111
|
+
|
|
112
|
+
if (secondArg?.type === AST_NODE_TYPES.ObjectExpression) {
|
|
113
|
+
return secondArg;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function findObjectProperty(
|
|
120
|
+
object: TSESTree.ObjectExpression,
|
|
121
|
+
keyName: string
|
|
122
|
+
): TSESTree.Property | null {
|
|
123
|
+
for (const prop of object.properties) {
|
|
124
|
+
if (prop.type !== AST_NODE_TYPES.Property || prop.computed) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const key = prop.key;
|
|
129
|
+
|
|
130
|
+
if (key.type === AST_NODE_TYPES.Identifier && key.name === keyName) {
|
|
131
|
+
return prop;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (key.type === AST_NODE_TYPES.Literal && key.value === keyName) {
|
|
135
|
+
return prop;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function findNestedProperty(
|
|
143
|
+
object: TSESTree.ObjectExpression,
|
|
144
|
+
...keys: string[]
|
|
145
|
+
): TSESTree.Property | null {
|
|
146
|
+
let current: TSESTree.ObjectExpression | null = object;
|
|
147
|
+
|
|
148
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
149
|
+
const keyName = keys[index];
|
|
150
|
+
|
|
151
|
+
if (current === null || keyName === undefined) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const prop = findObjectProperty(current, keyName);
|
|
156
|
+
|
|
157
|
+
if (prop === null) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (index === keys.length - 1) {
|
|
162
|
+
return prop;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (prop.value.type === AST_NODE_TYPES.ObjectExpression) {
|
|
166
|
+
current = prop.value;
|
|
167
|
+
} else {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function getRouteHandler(
|
|
176
|
+
node: TSESTree.CallExpression
|
|
177
|
+
): TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | null {
|
|
178
|
+
for (const arg of node.arguments) {
|
|
179
|
+
if (
|
|
180
|
+
arg.type === AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
181
|
+
arg.type === AST_NODE_TYPES.FunctionExpression
|
|
182
|
+
) {
|
|
183
|
+
return arg;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function nodeContainsCallNamed(
|
|
191
|
+
root: TSESTree.Node,
|
|
192
|
+
objectName: string,
|
|
193
|
+
methodName: string
|
|
194
|
+
): boolean {
|
|
195
|
+
return walkSome(root, (node) => {
|
|
196
|
+
if (node.type !== AST_NODE_TYPES.CallExpression) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const callee = node.callee;
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
204
|
+
!callee.computed &&
|
|
205
|
+
callee.object.type === AST_NODE_TYPES.Identifier &&
|
|
206
|
+
callee.object.name === objectName &&
|
|
207
|
+
callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
208
|
+
callee.property.name === methodName
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function nodeContainsMemberCall(
|
|
214
|
+
root: TSESTree.Node,
|
|
215
|
+
methodName: string
|
|
216
|
+
): boolean {
|
|
217
|
+
return walkSome(root, (node) => {
|
|
218
|
+
if (node.type !== AST_NODE_TYPES.CallExpression) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const callee = node.callee;
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
226
|
+
!callee.computed &&
|
|
227
|
+
callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
228
|
+
callee.property.name === methodName
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
}
|
package/src/rule-packs/index.ts
CHANGED
|
@@ -1,30 +1,37 @@
|
|
|
1
1
|
import type { TSESLint } from "@typescript-eslint/utils";
|
|
2
2
|
|
|
3
3
|
import type { IRulePack } from "./rule-packs.types";
|
|
4
|
+
import { authorizationPack } from "./authorization";
|
|
4
5
|
import { bullmqPack } from "./bullmq";
|
|
5
6
|
import { commentHygienePack } from "./comment-hygiene";
|
|
6
7
|
import { codeFlowPack } from "./code-flow";
|
|
7
8
|
import { drizzlePack } from "./drizzle";
|
|
8
9
|
import { elysiaPack } from "./elysia";
|
|
9
10
|
import { envAccessPack } from "./env-access";
|
|
11
|
+
import { fastifyPack } from "./fastify";
|
|
10
12
|
import { i18nKeysPack } from "./i18n-keys";
|
|
11
13
|
import { jwtCookiesPack } from "./jwt-cookies";
|
|
12
14
|
import { moduleBoundariesPack } from "./module-boundaries";
|
|
13
15
|
import { nextjsPack } from "./nextjs";
|
|
14
16
|
import { oauthSecurityPack } from "./oauth-security";
|
|
15
17
|
import { reactComponentArchitecturePack } from "./react-component-architecture";
|
|
18
|
+
import { runtimeBoundariesPack } from "./runtime-boundaries";
|
|
19
|
+
import { securityPack } from "./security";
|
|
16
20
|
import { structuredLoggingPack } from "./structured-logging";
|
|
17
21
|
import { tanstackQueryPack } from "./tanstack-query";
|
|
18
22
|
import { testConventionsPack } from "./test-conventions";
|
|
23
|
+
import { typescriptCorePack } from "./typescript-core";
|
|
19
24
|
import { PACK_REGISTRY } from "../stack-detection";
|
|
20
25
|
|
|
21
26
|
/** Registry of all available rule packs, keyed by pack ID. */
|
|
22
27
|
export const RULE_PACKS = {
|
|
28
|
+
authorization: authorizationPack,
|
|
23
29
|
bullmq: bullmqPack,
|
|
24
30
|
"code-flow": codeFlowPack,
|
|
25
31
|
"comment-hygiene": commentHygienePack,
|
|
26
32
|
drizzle: drizzlePack,
|
|
27
33
|
elysia: elysiaPack,
|
|
34
|
+
fastify: fastifyPack,
|
|
28
35
|
"env-access": envAccessPack,
|
|
29
36
|
"i18n-keys": i18nKeysPack,
|
|
30
37
|
"jwt-cookies": jwtCookiesPack,
|
|
@@ -32,9 +39,12 @@ export const RULE_PACKS = {
|
|
|
32
39
|
nextjs: nextjsPack,
|
|
33
40
|
"oauth-security": oauthSecurityPack,
|
|
34
41
|
"react-component-architecture": reactComponentArchitecturePack,
|
|
42
|
+
"runtime-boundaries": runtimeBoundariesPack,
|
|
43
|
+
security: securityPack,
|
|
35
44
|
"structured-logging": structuredLoggingPack,
|
|
36
45
|
"tanstack-query": tanstackQueryPack,
|
|
37
46
|
"test-conventions": testConventionsPack,
|
|
47
|
+
"typescript-core": typescriptCorePack,
|
|
38
48
|
} as const;
|
|
39
49
|
|
|
40
50
|
export type IRulePackId = keyof typeof RULE_PACKS;
|
|
@@ -2,13 +2,20 @@ import type { TSESLint } from "@typescript-eslint/utils";
|
|
|
2
2
|
|
|
3
3
|
import { authCookieMustBeHttpOnlyRule } from "./rules/auth-cookie-must-be-httponly";
|
|
4
4
|
import { authCookieMustBeSecureInProdRule } from "./rules/auth-cookie-must-be-secure-in-prod";
|
|
5
|
+
import { authCookieMustSetMaxAgeOrExpiresRule } from "./rules/auth-cookie-must-set-maxage-or-expires";
|
|
6
|
+
import { authCookieMustSetSameSiteRule } from "./rules/auth-cookie-must-set-samesite";
|
|
5
7
|
import { bcryptRoundsMinRule } from "./rules/bcrypt-rounds-min";
|
|
8
|
+
import { jwtMustVerifyNotDecodeRule } from "./rules/jwt-must-verify-not-decode";
|
|
6
9
|
import type { IRulePack } from "../rule-packs.types";
|
|
7
10
|
|
|
8
11
|
const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
|
|
9
12
|
"auth-cookie-must-be-httponly": authCookieMustBeHttpOnlyRule,
|
|
10
13
|
"auth-cookie-must-be-secure-in-prod": authCookieMustBeSecureInProdRule,
|
|
14
|
+
"auth-cookie-must-set-maxage-or-expires":
|
|
15
|
+
authCookieMustSetMaxAgeOrExpiresRule,
|
|
16
|
+
"auth-cookie-must-set-samesite": authCookieMustSetSameSiteRule,
|
|
11
17
|
"bcrypt-rounds-min": bcryptRoundsMinRule,
|
|
18
|
+
"jwt-must-verify-not-decode": jwtMustVerifyNotDecodeRule,
|
|
12
19
|
};
|
|
13
20
|
|
|
14
21
|
export const jwtCookiesPack: IRulePack = {
|
|
@@ -18,7 +25,10 @@ export const jwtCookiesPack: IRulePack = {
|
|
|
18
25
|
rulesConfig: {
|
|
19
26
|
"auth-cookie-must-be-httponly": "error",
|
|
20
27
|
"auth-cookie-must-be-secure-in-prod": "error",
|
|
28
|
+
"auth-cookie-must-set-maxage-or-expires": "warn",
|
|
29
|
+
"auth-cookie-must-set-samesite": "error",
|
|
21
30
|
"bcrypt-rounds-min": "error",
|
|
31
|
+
"jwt-must-verify-not-decode": "error",
|
|
22
32
|
},
|
|
23
33
|
};
|
|
24
34
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
2
|
+
|
|
3
|
+
import { createRule } from "../../create-rule";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_AUTH_COOKIE_NAMES,
|
|
6
|
+
DEFAULT_SET_COOKIE_FUNCTIONS,
|
|
7
|
+
DEFAULT_TRUSTED_CONFIG_NAMES,
|
|
8
|
+
lookupCookieOption,
|
|
9
|
+
matchAuthCookieSet,
|
|
10
|
+
} from "../utils";
|
|
11
|
+
|
|
12
|
+
export const RULE_NAME = "auth-cookie-must-set-maxage-or-expires";
|
|
13
|
+
|
|
14
|
+
export interface IAuthCookieMustSetMaxAgeOrExpiresOptions {
|
|
15
|
+
readonly authCookieNames?: readonly string[];
|
|
16
|
+
readonly trustedConfigNames?: readonly string[];
|
|
17
|
+
readonly setCookieFunctions?: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type RuleOptions = [IAuthCookieMustSetMaxAgeOrExpiresOptions];
|
|
21
|
+
type MessageIds = "missingLifetime";
|
|
22
|
+
|
|
23
|
+
const optionSchema: JSONSchema4 = {
|
|
24
|
+
type: "object",
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
properties: {
|
|
27
|
+
authCookieNames: {
|
|
28
|
+
type: "array",
|
|
29
|
+
items: { type: "string" },
|
|
30
|
+
uniqueItems: true,
|
|
31
|
+
minItems: 1,
|
|
32
|
+
},
|
|
33
|
+
trustedConfigNames: {
|
|
34
|
+
type: "array",
|
|
35
|
+
items: { type: "string" },
|
|
36
|
+
uniqueItems: true,
|
|
37
|
+
},
|
|
38
|
+
setCookieFunctions: {
|
|
39
|
+
type: "array",
|
|
40
|
+
items: { type: "string" },
|
|
41
|
+
uniqueItems: true,
|
|
42
|
+
minItems: 1,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const authCookieMustSetMaxAgeOrExpiresRule = createRule<
|
|
48
|
+
RuleOptions,
|
|
49
|
+
MessageIds
|
|
50
|
+
>({
|
|
51
|
+
name: RULE_NAME,
|
|
52
|
+
meta: {
|
|
53
|
+
type: "suggestion",
|
|
54
|
+
docs: {
|
|
55
|
+
description:
|
|
56
|
+
"Auth-cookie writes should set `maxAge` or `expires` so session cookies do not live forever by default.",
|
|
57
|
+
},
|
|
58
|
+
schema: [optionSchema],
|
|
59
|
+
messages: {
|
|
60
|
+
missingLifetime:
|
|
61
|
+
"Auth cookie '{{name}}' missing `maxAge` or `expires` — session cookies without a lifetime persist until cleared.",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
defaultOptions: [
|
|
65
|
+
{
|
|
66
|
+
authCookieNames: [...DEFAULT_AUTH_COOKIE_NAMES],
|
|
67
|
+
trustedConfigNames: [...DEFAULT_TRUSTED_CONFIG_NAMES],
|
|
68
|
+
setCookieFunctions: [...DEFAULT_SET_COOKIE_FUNCTIONS],
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
create(context, [options]) {
|
|
72
|
+
const authCookieNames = new Set(
|
|
73
|
+
options.authCookieNames ?? DEFAULT_AUTH_COOKIE_NAMES
|
|
74
|
+
);
|
|
75
|
+
const trustedConfigNames = new Set(
|
|
76
|
+
options.trustedConfigNames ?? DEFAULT_TRUSTED_CONFIG_NAMES
|
|
77
|
+
);
|
|
78
|
+
const setCookieFunctions = new Set(
|
|
79
|
+
options.setCookieFunctions ?? DEFAULT_SET_COOKIE_FUNCTIONS
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
CallExpression(node) {
|
|
84
|
+
const match = matchAuthCookieSet(
|
|
85
|
+
node,
|
|
86
|
+
authCookieNames,
|
|
87
|
+
setCookieFunctions
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (match === null) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (match.optionsNode === null) {
|
|
95
|
+
context.report({
|
|
96
|
+
node,
|
|
97
|
+
messageId: "missingLifetime",
|
|
98
|
+
data: { name: match.cookieName },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const maxAge = lookupCookieOption(
|
|
105
|
+
match.optionsNode,
|
|
106
|
+
"maxAge",
|
|
107
|
+
trustedConfigNames
|
|
108
|
+
);
|
|
109
|
+
const expires = lookupCookieOption(
|
|
110
|
+
match.optionsNode,
|
|
111
|
+
"expires",
|
|
112
|
+
trustedConfigNames
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
maxAge.hasTrustedSpread ||
|
|
117
|
+
expires.hasTrustedSpread ||
|
|
118
|
+
maxAge.value !== null ||
|
|
119
|
+
expires.value !== null
|
|
120
|
+
) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
context.report({
|
|
125
|
+
node,
|
|
126
|
+
messageId: "missingLifetime",
|
|
127
|
+
data: { name: match.cookieName },
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
2
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
3
|
+
|
|
4
|
+
import { createRule } from "../../create-rule";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_AUTH_COOKIE_NAMES,
|
|
7
|
+
DEFAULT_SET_COOKIE_FUNCTIONS,
|
|
8
|
+
DEFAULT_TRUSTED_CONFIG_NAMES,
|
|
9
|
+
lookupCookieOption,
|
|
10
|
+
matchAuthCookieSet,
|
|
11
|
+
} from "../utils";
|
|
12
|
+
|
|
13
|
+
export const RULE_NAME = "auth-cookie-must-set-samesite";
|
|
14
|
+
|
|
15
|
+
export interface IAuthCookieMustSetSameSiteOptions {
|
|
16
|
+
readonly authCookieNames?: readonly string[];
|
|
17
|
+
readonly trustedConfigNames?: readonly string[];
|
|
18
|
+
readonly setCookieFunctions?: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type RuleOptions = [IAuthCookieMustSetSameSiteOptions];
|
|
22
|
+
type MessageIds = "missingSameSite";
|
|
23
|
+
|
|
24
|
+
const optionSchema: JSONSchema4 = {
|
|
25
|
+
type: "object",
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
properties: {
|
|
28
|
+
authCookieNames: {
|
|
29
|
+
type: "array",
|
|
30
|
+
items: { type: "string" },
|
|
31
|
+
uniqueItems: true,
|
|
32
|
+
minItems: 1,
|
|
33
|
+
},
|
|
34
|
+
trustedConfigNames: {
|
|
35
|
+
type: "array",
|
|
36
|
+
items: { type: "string" },
|
|
37
|
+
uniqueItems: true,
|
|
38
|
+
},
|
|
39
|
+
setCookieFunctions: {
|
|
40
|
+
type: "array",
|
|
41
|
+
items: { type: "string" },
|
|
42
|
+
uniqueItems: true,
|
|
43
|
+
minItems: 1,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const authCookieMustSetSameSiteRule = createRule<
|
|
49
|
+
RuleOptions,
|
|
50
|
+
MessageIds
|
|
51
|
+
>({
|
|
52
|
+
name: RULE_NAME,
|
|
53
|
+
meta: {
|
|
54
|
+
type: "problem",
|
|
55
|
+
docs: {
|
|
56
|
+
description:
|
|
57
|
+
"Auth-cookie writes must set `sameSite` (`strict` or `lax`) — missing SameSite allows cross-site cookie delivery.",
|
|
58
|
+
},
|
|
59
|
+
schema: [optionSchema],
|
|
60
|
+
messages: {
|
|
61
|
+
missingSameSite:
|
|
62
|
+
"Auth cookie '{{name}}' missing `sameSite` — set `sameSite: 'strict'` or `'lax'` to limit cross-site delivery.",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
defaultOptions: [
|
|
66
|
+
{
|
|
67
|
+
authCookieNames: [...DEFAULT_AUTH_COOKIE_NAMES],
|
|
68
|
+
trustedConfigNames: [...DEFAULT_TRUSTED_CONFIG_NAMES],
|
|
69
|
+
setCookieFunctions: [...DEFAULT_SET_COOKIE_FUNCTIONS],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
create(context, [options]) {
|
|
73
|
+
const authCookieNames = new Set(
|
|
74
|
+
options.authCookieNames ?? DEFAULT_AUTH_COOKIE_NAMES
|
|
75
|
+
);
|
|
76
|
+
const trustedConfigNames = new Set(
|
|
77
|
+
options.trustedConfigNames ?? DEFAULT_TRUSTED_CONFIG_NAMES
|
|
78
|
+
);
|
|
79
|
+
const setCookieFunctions = new Set(
|
|
80
|
+
options.setCookieFunctions ?? DEFAULT_SET_COOKIE_FUNCTIONS
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
CallExpression(node) {
|
|
85
|
+
const match = matchAuthCookieSet(
|
|
86
|
+
node,
|
|
87
|
+
authCookieNames,
|
|
88
|
+
setCookieFunctions
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (match === null) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (match.optionsNode === null) {
|
|
96
|
+
context.report({
|
|
97
|
+
node,
|
|
98
|
+
messageId: "missingSameSite",
|
|
99
|
+
data: { name: match.cookieName },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { value, hasTrustedSpread } = lookupCookieOption(
|
|
106
|
+
match.optionsNode,
|
|
107
|
+
"sameSite",
|
|
108
|
+
trustedConfigNames
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (hasTrustedSpread) {
|
|
112
|
+
if (
|
|
113
|
+
value !== null &&
|
|
114
|
+
value.type === AST_NODE_TYPES.Literal &&
|
|
115
|
+
value.value === "none"
|
|
116
|
+
) {
|
|
117
|
+
context.report({
|
|
118
|
+
node: value,
|
|
119
|
+
messageId: "missingSameSite",
|
|
120
|
+
data: { name: match.cookieName },
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (value === null) {
|
|
128
|
+
context.report({
|
|
129
|
+
node,
|
|
130
|
+
messageId: "missingSameSite",
|
|
131
|
+
data: { name: match.cookieName },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
value.type === AST_NODE_TYPES.Literal &&
|
|
139
|
+
value.value !== "strict" &&
|
|
140
|
+
value.value !== "lax"
|
|
141
|
+
) {
|
|
142
|
+
context.report({
|
|
143
|
+
node: value,
|
|
144
|
+
messageId: "missingSameSite",
|
|
145
|
+
data: { name: match.cookieName },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
});
|