@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.
Files changed (122) hide show
  1. package/package.json +6 -2
  2. package/scripts/browser-check.ts +41 -5
  3. package/scripts/build-rules-md.ts +78 -21
  4. package/scripts/cli-metrics.ts +10 -0
  5. package/scripts/sweep.ts +53 -23
  6. package/scripts/web-sweep.ts +292 -0
  7. package/src/browser/index.ts +3 -0
  8. package/src/browser/oracle.ts +215 -8
  9. package/src/cli.ts +22 -4
  10. package/src/config/index.ts +8 -0
  11. package/src/config/profiles.ts +150 -0
  12. package/src/config/tsforge-config.ts +64 -5
  13. package/src/detect-gate.ts +144 -13
  14. package/src/eval/eval.types.ts +9 -0
  15. package/src/eval/failure-class.ts +263 -0
  16. package/src/eval/index.ts +8 -0
  17. package/src/eval/metrics.ts +7 -0
  18. package/src/eval/parse-log.ts +105 -0
  19. package/src/eval/report.ts +19 -0
  20. package/src/eval/score.ts +10 -0
  21. package/src/loop/feedback/meta-rule-docs.ts +48 -0
  22. package/src/loop/feedback/rule-docs.ts +150 -0
  23. package/src/loop/loop.types.ts +4 -0
  24. package/src/loop/rule-docs.generated.json +131 -1
  25. package/src/loop/ttsr-defaults.ts +175 -4
  26. package/src/loop/turn.ts +3 -0
  27. package/src/meta-rules/registry.ts +32 -0
  28. package/src/meta-rules/rules/ci/no-github-context-in-shell.ts +40 -0
  29. package/src/meta-rules/rules/ci/no-pull-request-target-untrusted-checkout.ts +42 -0
  30. package/src/meta-rules/rules/ci/workflow-permissions-explicit.ts +49 -0
  31. package/src/meta-rules/rules/ci/workflow-permissions-least-privilege.ts +44 -0
  32. package/src/meta-rules/rules/config/next-image-remote-patterns-no-wildcards.ts +77 -0
  33. package/src/meta-rules/rules/config/next-instrumentation-present.ts +66 -0
  34. package/src/meta-rules/rules/config/next-proxy-over-middleware.ts +64 -0
  35. package/src/meta-rules/rules/config/tsconfig-recommended-flags.ts +75 -0
  36. package/src/meta-rules/rules/supply-chain/dependency-overrides-require-comment.ts +61 -0
  37. package/src/meta-rules/rules/supply-chain/fastify-security-plugins.ts +54 -0
  38. package/src/meta-rules/rules/supply-chain/lockfile-required.ts +51 -0
  39. package/src/meta-rules/rules/supply-chain/migrations-must-be-checked-in.ts +49 -0
  40. package/src/meta-rules/rules/supply-chain/no-git-or-tarball-dependencies.ts +70 -0
  41. package/src/meta-rules/rules/supply-chain/package-manager-field-required.ts +31 -0
  42. package/src/meta-rules/rules/supply-chain/production-must-not-use-drizzle-push.ts +75 -0
  43. package/src/meta-rules/rules/supply-chain/single-package-manager.ts +30 -0
  44. package/src/meta-rules/utils/lockfiles.ts +105 -0
  45. package/src/meta-rules/utils/workflow-yaml.ts +86 -0
  46. package/src/rule-packs/authorization/index.ts +26 -0
  47. package/src/rule-packs/authorization/rules/id-param-requires-object-authz.ts +87 -0
  48. package/src/rule-packs/authorization/rules/mutating-route-requires-authz.ts +116 -0
  49. package/src/rule-packs/authorization/rules/server-action-requires-authz.ts +101 -0
  50. package/src/rule-packs/authorization/utils.ts +285 -0
  51. package/src/rule-packs/boundary-utils.ts +13 -0
  52. package/src/rule-packs/code-flow/index.ts +4 -1
  53. package/src/rule-packs/code-flow/rules/no-throw-literal.ts +67 -0
  54. package/src/rule-packs/drizzle/index.ts +7 -0
  55. package/src/rule-packs/drizzle/rules/update-delete-account-scoped-must-filter-scope.ts +106 -0
  56. package/src/rule-packs/drizzle/rules/update-delete-must-have-where.ts +73 -0
  57. package/src/rule-packs/drizzle/utils.ts +133 -1
  58. package/src/rule-packs/fastify/index.ts +38 -0
  59. package/src/rule-packs/fastify/rules/error-handler-must-set-status.ts +78 -0
  60. package/src/rule-packs/fastify/rules/prefer-return-over-reply-send.ts +104 -0
  61. package/src/rule-packs/fastify/rules/require-fp-for-shared-plugins.ts +106 -0
  62. package/src/rule-packs/fastify/rules/require-plugin-name.ts +54 -0
  63. package/src/rule-packs/fastify/rules/require-response-schema.ts +62 -0
  64. package/src/rule-packs/fastify/rules/require-route-schema.ts +104 -0
  65. package/src/rule-packs/fastify/rules/test-inject-must-close-app.ts +44 -0
  66. package/src/rule-packs/fastify/utils/fastifyChain.ts +231 -0
  67. package/src/rule-packs/index.ts +10 -0
  68. package/src/rule-packs/jwt-cookies/index.ts +10 -0
  69. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-set-maxage-or-expires.ts +132 -0
  70. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-set-samesite.ts +151 -0
  71. package/src/rule-packs/jwt-cookies/rules/jwt-must-verify-not-decode.ts +124 -0
  72. package/src/rule-packs/module-boundaries/index.ts +3 -0
  73. package/src/rule-packs/module-boundaries/rules/no-react-in-services.ts +111 -0
  74. package/src/rule-packs/nextjs/index.ts +32 -0
  75. package/src/rule-packs/nextjs/rules/await-dynamic-request-apis.ts +65 -0
  76. package/src/rule-packs/nextjs/rules/error-boundary-require-use-client.ts +38 -0
  77. package/src/rule-packs/nextjs/rules/mutation-should-revalidate-cache.ts +152 -0
  78. package/src/rule-packs/nextjs/rules/no-html-img-element.ts +45 -0
  79. package/src/rule-packs/nextjs/rules/no-internal-api-fetch.ts +126 -0
  80. package/src/rule-packs/nextjs/rules/no-secret-props-to-client.ts +118 -0
  81. package/src/rule-packs/nextjs/rules/no-sensitive-next-public-env.ts +72 -0
  82. package/src/rule-packs/nextjs/rules/prefer-lazy-use-state-init.ts +85 -0
  83. package/src/rule-packs/nextjs/rules/server-action-requires-authz-and-validation.ts +178 -0
  84. package/src/rule-packs/nextjs/rules/server-only-modules-import-server-only.ts +87 -0
  85. package/src/rule-packs/nextjs/utils.ts +18 -0
  86. package/src/rule-packs/react-component-architecture/index.ts +18 -0
  87. package/src/rule-packs/react-component-architecture/rules/dangerous-html-requires-sanitize.ts +83 -0
  88. package/src/rule-packs/react-component-architecture/rules/no-anonymous-useEffect.ts +61 -0
  89. package/src/rule-packs/react-component-architecture/rules/no-component-invocation.ts +55 -0
  90. package/src/rule-packs/react-component-architecture/rules/no-derived-state-in-effect.ts +204 -0
  91. package/src/rule-packs/react-component-architecture/rules/no-nested-component.ts +152 -0
  92. package/src/rule-packs/react-component-architecture/rules/no-react-fc.ts +57 -0
  93. package/src/rule-packs/rule-catalog.types.ts +21 -0
  94. package/src/rule-packs/rule-metadata.ts +163 -0
  95. package/src/rule-packs/runtime-boundaries/index.ts +33 -0
  96. package/src/rule-packs/runtime-boundaries/rules/no-prototype-polluting-merge.ts +113 -0
  97. package/src/rule-packs/runtime-boundaries/rules/no-user-controlled-fetch-url.ts +69 -0
  98. package/src/rule-packs/runtime-boundaries/rules/no-user-controlled-redirect.ts +79 -0
  99. package/src/rule-packs/runtime-boundaries/rules/upload-must-set-limits.ts +126 -0
  100. package/src/rule-packs/runtime-boundaries/rules/webhook-must-verify-signature-before-parse.ts +87 -0
  101. package/src/rule-packs/security/index.ts +35 -0
  102. package/src/rule-packs/security/rules/catch-must-handle.ts +126 -0
  103. package/src/rule-packs/security/rules/no-auth-token-in-storage.ts +107 -0
  104. package/src/rule-packs/security/rules/no-child-process-exec.ts +72 -0
  105. package/src/rule-packs/security/rules/no-dynamic-regexp.ts +56 -0
  106. package/src/rule-packs/security/rules/no-inner-html-assignment.ts +42 -0
  107. package/src/rule-packs/security/rules/no-spawn-with-shell.ts +106 -0
  108. package/src/rule-packs/structured-logging/index.ts +6 -0
  109. package/src/rule-packs/structured-logging/rules/caught-error-log-requires-cause.ts +234 -0
  110. package/src/rule-packs/structured-logging/rules/logger-not-console.ts +146 -0
  111. package/src/rule-packs/test-conventions/index.ts +9 -0
  112. package/src/rule-packs/test-conventions/rules/fake-timers-must-be-restored.ts +143 -0
  113. package/src/rule-packs/test-conventions/rules/no-conditional-expect.ts +77 -0
  114. package/src/rule-packs/test-conventions/rules/no-real-network-in-unit-tests.ts +174 -0
  115. package/src/rule-packs/typescript-core/index.ts +30 -0
  116. package/src/rule-packs/typescript-core/rules/exported-functions-require-return-type.ts +74 -0
  117. package/src/rule-packs/typescript-core/rules/fetch-must-check-ok.ts +106 -0
  118. package/src/rule-packs/typescript-core/rules/json-parse-must-validate.ts +97 -0
  119. package/src/rule-packs/typescript-core/rules/no-unsafe-boundary-cast.ts +70 -0
  120. package/src/stack-detection/packs.ts +57 -0
  121. package/strict.type-aware.eslint.config.mjs +33 -0
  122. package/strict.web.eslint.config.mjs +32 -1
@@ -0,0 +1,35 @@
1
+ import type { TSESLint } from "@typescript-eslint/utils";
2
+
3
+ import { catchMustHandleRule } from "./rules/catch-must-handle";
4
+ import { noAuthTokenInStorageRule } from "./rules/no-auth-token-in-storage";
5
+ import { noChildProcessExecRule } from "./rules/no-child-process-exec";
6
+ import { noDynamicRegexpRule } from "./rules/no-dynamic-regexp";
7
+ import { noInnerHtmlAssignmentRule } from "./rules/no-inner-html-assignment";
8
+ import { noSpawnWithShellRule } from "./rules/no-spawn-with-shell";
9
+ import type { IRulePack } from "../rule-packs.types";
10
+
11
+ const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
12
+ "catch-must-handle": catchMustHandleRule,
13
+ "no-auth-token-in-storage": noAuthTokenInStorageRule,
14
+ "no-child-process-exec": noChildProcessExecRule,
15
+ "no-dynamic-regexp": noDynamicRegexpRule,
16
+ "no-inner-html-assignment": noInnerHtmlAssignmentRule,
17
+ "no-spawn-with-shell": noSpawnWithShellRule,
18
+ };
19
+
20
+ export const securityPack: IRulePack = {
21
+ id: "security",
22
+ description:
23
+ "Application security guardrails: command injection, ReDoS, DOM XSS, and silent error masking",
24
+ rules,
25
+ rulesConfig: {
26
+ "catch-must-handle": "error",
27
+ "no-auth-token-in-storage": "error",
28
+ "no-child-process-exec": "error",
29
+ "no-dynamic-regexp": "error",
30
+ "no-inner-html-assignment": "error",
31
+ "no-spawn-with-shell": "error",
32
+ },
33
+ };
34
+
35
+ export default securityPack;
@@ -0,0 +1,126 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+
3
+ import { createRule } from "../../create-rule";
4
+ import { walkSome } from "../../utils";
5
+
6
+ export const RULE_NAME = "catch-must-handle";
7
+
8
+ type MessageIds = "silentCatch";
9
+
10
+ function isSilentDefaultReturn(node: TSESTree.Node): boolean {
11
+ if (node.type === AST_NODE_TYPES.ReturnStatement) {
12
+ const arg = node.argument;
13
+
14
+ if (arg === null) {
15
+ return true;
16
+ }
17
+
18
+ if (arg.type === AST_NODE_TYPES.Identifier && arg.name === "undefined") {
19
+ return true;
20
+ }
21
+
22
+ if (arg.type === AST_NODE_TYPES.Literal && arg.value === null) {
23
+ return true;
24
+ }
25
+
26
+ if (
27
+ arg.type === AST_NODE_TYPES.ArrayExpression &&
28
+ arg.elements.length === 0
29
+ ) {
30
+ return true;
31
+ }
32
+
33
+ if (
34
+ arg.type === AST_NODE_TYPES.ObjectExpression &&
35
+ arg.properties.length === 0
36
+ ) {
37
+ return true;
38
+ }
39
+ }
40
+
41
+ return false;
42
+ }
43
+
44
+ function catchBodyHandlesError(body: TSESTree.BlockStatement): boolean {
45
+ if (body.body.length === 0) {
46
+ return false;
47
+ }
48
+
49
+ if (
50
+ walkSome(body, (node) => {
51
+ if (node.type === AST_NODE_TYPES.ThrowStatement) {
52
+ return true;
53
+ }
54
+
55
+ if (node.type !== AST_NODE_TYPES.CallExpression) {
56
+ return false;
57
+ }
58
+
59
+ const callee = node.callee;
60
+
61
+ if (callee.type === AST_NODE_TYPES.MemberExpression && !callee.computed) {
62
+ const object = callee.object;
63
+ const property = callee.property;
64
+
65
+ if (
66
+ object.type === AST_NODE_TYPES.Identifier &&
67
+ object.name === "console" &&
68
+ property.type === AST_NODE_TYPES.Identifier &&
69
+ (property.name === "error" || property.name === "warn")
70
+ ) {
71
+ return true;
72
+ }
73
+
74
+ if (
75
+ object.type === AST_NODE_TYPES.Identifier &&
76
+ object.name === "logger" &&
77
+ property.type === AST_NODE_TYPES.Identifier
78
+ ) {
79
+ return true;
80
+ }
81
+ }
82
+
83
+ return false;
84
+ })
85
+ ) {
86
+ return true;
87
+ }
88
+
89
+ const onlySilentReturns = body.body.every((stmt) => {
90
+ if (stmt.type === AST_NODE_TYPES.ReturnStatement) {
91
+ return isSilentDefaultReturn(stmt);
92
+ }
93
+
94
+ return false;
95
+ });
96
+
97
+ return !onlySilentReturns;
98
+ }
99
+
100
+ export const catchMustHandleRule = createRule<[], MessageIds>({
101
+ name: RULE_NAME,
102
+ meta: {
103
+ type: "problem",
104
+ docs: {
105
+ description:
106
+ "Catch blocks must log, rethrow, or propagate errors — not silently return empty defaults on failure.",
107
+ },
108
+ schema: [],
109
+ messages: {
110
+ silentCatch:
111
+ "Catch block silently masks failure — log with `logger.error`/`console.warn`, rethrow, or return a typed error result instead of an empty default.",
112
+ },
113
+ },
114
+ defaultOptions: [],
115
+ create(context) {
116
+ return {
117
+ CatchClause(node: TSESTree.CatchClause) {
118
+ const body = node.body;
119
+
120
+ if (!catchBodyHandlesError(body)) {
121
+ context.report({ node, messageId: "silentCatch" });
122
+ }
123
+ },
124
+ };
125
+ },
126
+ });
@@ -0,0 +1,107 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+
3
+ import { createRule } from "../../create-rule";
4
+
5
+ export const RULE_NAME = "no-auth-token-in-storage";
6
+
7
+ type MessageIds = "authTokenInStorage";
8
+
9
+ const SENSITIVE_KEY = /(?:token|session|auth|jwt)/i;
10
+
11
+ function isBrowserStorageObject(
12
+ node: TSESTree.Node
13
+ ): node is TSESTree.Identifier {
14
+ return (
15
+ node.type === AST_NODE_TYPES.Identifier &&
16
+ (node.name === "localStorage" || node.name === "sessionStorage")
17
+ );
18
+ }
19
+
20
+ function storageMethodName(node: TSESTree.CallExpression): string | null {
21
+ const callee = node.callee;
22
+
23
+ if (
24
+ callee.type !== AST_NODE_TYPES.MemberExpression ||
25
+ callee.computed ||
26
+ !isBrowserStorageObject(callee.object) ||
27
+ callee.property.type !== AST_NODE_TYPES.Identifier
28
+ ) {
29
+ return null;
30
+ }
31
+
32
+ const method = callee.property.name;
33
+
34
+ if (method !== "setItem" && method !== "getItem") {
35
+ return null;
36
+ }
37
+
38
+ return method;
39
+ }
40
+
41
+ function keyLooksSensitive(arg: TSESTree.Expression): boolean {
42
+ if (arg.type === AST_NODE_TYPES.Literal && typeof arg.value === "string") {
43
+ return SENSITIVE_KEY.test(arg.value);
44
+ }
45
+
46
+ if (arg.type === AST_NODE_TYPES.Identifier) {
47
+ return SENSITIVE_KEY.test(arg.name);
48
+ }
49
+
50
+ return false;
51
+ }
52
+
53
+ export const noAuthTokenInStorageRule = createRule<[], MessageIds>({
54
+ name: RULE_NAME,
55
+ meta: {
56
+ type: "problem",
57
+ docs: {
58
+ description:
59
+ "Disallow storing or reading auth tokens from localStorage/sessionStorage — use httpOnly cookies instead.",
60
+ },
61
+ schema: [],
62
+ messages: {
63
+ authTokenInStorage:
64
+ "Do not store auth tokens in `{{storage}}` — use httpOnly, secure cookies so XSS cannot exfiltrate sessions.",
65
+ },
66
+ },
67
+ defaultOptions: [],
68
+ create(context) {
69
+ return {
70
+ CallExpression(node: TSESTree.CallExpression) {
71
+ const method = storageMethodName(node);
72
+
73
+ if (method === null) {
74
+ return;
75
+ }
76
+
77
+ const keyArg = node.arguments[0];
78
+
79
+ if (
80
+ keyArg === undefined ||
81
+ keyArg.type === AST_NODE_TYPES.SpreadElement ||
82
+ !keyLooksSensitive(keyArg)
83
+ ) {
84
+ return;
85
+ }
86
+
87
+ const callee = node.callee;
88
+
89
+ if (callee.type !== AST_NODE_TYPES.MemberExpression) {
90
+ return;
91
+ }
92
+
93
+ const storageObject = callee.object;
94
+
95
+ if (!isBrowserStorageObject(storageObject)) {
96
+ return;
97
+ }
98
+
99
+ context.report({
100
+ node,
101
+ messageId: "authTokenInStorage",
102
+ data: { storage: storageObject.name },
103
+ });
104
+ },
105
+ };
106
+ },
107
+ });
@@ -0,0 +1,72 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+
3
+ import { createRule } from "../../create-rule";
4
+
5
+ export const RULE_NAME = "no-child-process-exec";
6
+
7
+ type MessageIds = "noExec";
8
+
9
+ const EXEC_METHODS = new Set(["exec", "execSync"]);
10
+
11
+ function isChildProcessExecCall(node: TSESTree.CallExpression): boolean {
12
+ const callee = node.callee;
13
+
14
+ if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed) {
15
+ return false;
16
+ }
17
+
18
+ if (callee.property.type !== AST_NODE_TYPES.Identifier) {
19
+ return false;
20
+ }
21
+
22
+ if (!EXEC_METHODS.has(callee.property.name)) {
23
+ return false;
24
+ }
25
+
26
+ const object = callee.object;
27
+
28
+ if (
29
+ object.type === AST_NODE_TYPES.Identifier &&
30
+ (object.name === "child_process" || object.name === "cp")
31
+ ) {
32
+ return true;
33
+ }
34
+
35
+ if (
36
+ object.type === AST_NODE_TYPES.MemberExpression &&
37
+ !object.computed &&
38
+ object.object.type === AST_NODE_TYPES.Identifier &&
39
+ object.object.name === "child_process" &&
40
+ object.property.type === AST_NODE_TYPES.Identifier
41
+ ) {
42
+ return EXEC_METHODS.has(object.property.name);
43
+ }
44
+
45
+ return false;
46
+ }
47
+
48
+ export const noChildProcessExecRule = createRule<[], MessageIds>({
49
+ name: RULE_NAME,
50
+ meta: {
51
+ type: "problem",
52
+ docs: {
53
+ description:
54
+ "Disallow child_process.exec/execSync — they run commands in a shell. Use execFile or spawn without shell instead.",
55
+ },
56
+ schema: [],
57
+ messages: {
58
+ noExec:
59
+ "Do not use `child_process.exec` or `execSync` — use `execFile`/`spawn` without a shell to avoid command injection.",
60
+ },
61
+ },
62
+ defaultOptions: [],
63
+ create(context) {
64
+ return {
65
+ CallExpression(node: TSESTree.CallExpression) {
66
+ if (isChildProcessExecCall(node)) {
67
+ context.report({ node, messageId: "noExec" });
68
+ }
69
+ },
70
+ };
71
+ },
72
+ });
@@ -0,0 +1,56 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+
3
+ import { createRule } from "../../create-rule";
4
+
5
+ export const RULE_NAME = "no-dynamic-regexp";
6
+
7
+ type MessageIds = "dynamicRegexp";
8
+
9
+ function isExpression(node: TSESTree.Node): node is TSESTree.Expression {
10
+ return node.type !== AST_NODE_TYPES.SpreadElement;
11
+ }
12
+
13
+ function isStringLiteral(node: TSESTree.Expression): boolean {
14
+ return node.type === AST_NODE_TYPES.Literal && typeof node.value === "string";
15
+ }
16
+
17
+ export const noDynamicRegexpRule = createRule<[], MessageIds>({
18
+ name: RULE_NAME,
19
+ meta: {
20
+ type: "problem",
21
+ docs: {
22
+ description:
23
+ "Disallow new RegExp(non-literal) — dynamic patterns enable ReDoS. Use string-literal regexes or a safe engine like re2.",
24
+ },
25
+ schema: [],
26
+ messages: {
27
+ dynamicRegexp:
28
+ "Do not construct `RegExp` from a runtime value — use a string-literal pattern or a safe regex library to avoid ReDoS.",
29
+ },
30
+ },
31
+ defaultOptions: [],
32
+ create(context) {
33
+ return {
34
+ NewExpression(node: TSESTree.NewExpression) {
35
+ const callee = node.callee;
36
+
37
+ if (
38
+ callee.type !== AST_NODE_TYPES.Identifier ||
39
+ callee.name !== "RegExp"
40
+ ) {
41
+ return;
42
+ }
43
+
44
+ const patternArg = node.arguments[0];
45
+
46
+ if (patternArg === undefined || !isExpression(patternArg)) {
47
+ return;
48
+ }
49
+
50
+ if (!isStringLiteral(patternArg)) {
51
+ context.report({ node, messageId: "dynamicRegexp" });
52
+ }
53
+ },
54
+ };
55
+ },
56
+ });
@@ -0,0 +1,42 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+
3
+ import { createRule } from "../../create-rule";
4
+
5
+ export const RULE_NAME = "no-inner-html-assignment";
6
+
7
+ type MessageIds = "innerHtmlAssignment";
8
+
9
+ export const noInnerHtmlAssignmentRule = createRule<[], MessageIds>({
10
+ name: RULE_NAME,
11
+ meta: {
12
+ type: "problem",
13
+ docs: {
14
+ description:
15
+ "Disallow assigning to innerHTML — use textContent/innerText or sanitize with DOMPurify before injecting HTML.",
16
+ },
17
+ schema: [],
18
+ messages: {
19
+ innerHtmlAssignment:
20
+ "Do not assign to `.innerHTML` — use `textContent` for plain text or sanitize HTML with DOMPurify first.",
21
+ },
22
+ },
23
+ defaultOptions: [],
24
+ create(context) {
25
+ return {
26
+ AssignmentExpression(node: TSESTree.AssignmentExpression) {
27
+ const left = node.left;
28
+
29
+ if (
30
+ left.type !== AST_NODE_TYPES.MemberExpression ||
31
+ left.computed ||
32
+ left.property.type !== AST_NODE_TYPES.Identifier ||
33
+ left.property.name !== "innerHTML"
34
+ ) {
35
+ return;
36
+ }
37
+
38
+ context.report({ node, messageId: "innerHtmlAssignment" });
39
+ },
40
+ };
41
+ },
42
+ });
@@ -0,0 +1,106 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+
3
+ import { createRule } from "../../create-rule";
4
+
5
+ export const RULE_NAME = "no-spawn-with-shell";
6
+
7
+ type MessageIds = "spawnWithShell";
8
+
9
+ function isExpression(node: TSESTree.Node): node is TSESTree.Expression {
10
+ return node.type !== AST_NODE_TYPES.SpreadElement;
11
+ }
12
+
13
+ function isShellTrue(node: TSESTree.Expression): boolean {
14
+ if (node.type === AST_NODE_TYPES.Literal && node.value === true) {
15
+ return true;
16
+ }
17
+
18
+ return false;
19
+ }
20
+
21
+ function optionsEnableShell(optionsArg: TSESTree.Expression): boolean {
22
+ if (optionsArg.type !== AST_NODE_TYPES.ObjectExpression) {
23
+ return false;
24
+ }
25
+
26
+ for (const prop of optionsArg.properties) {
27
+ if (prop.type !== AST_NODE_TYPES.Property || prop.computed) {
28
+ continue;
29
+ }
30
+
31
+ const key = prop.key;
32
+
33
+ if (
34
+ key.type === AST_NODE_TYPES.Identifier &&
35
+ key.name === "shell" &&
36
+ isExpression(prop.value) &&
37
+ isShellTrue(prop.value)
38
+ ) {
39
+ return true;
40
+ }
41
+
42
+ if (
43
+ key.type === AST_NODE_TYPES.Literal &&
44
+ key.value === "shell" &&
45
+ isExpression(prop.value) &&
46
+ isShellTrue(prop.value)
47
+ ) {
48
+ return true;
49
+ }
50
+ }
51
+
52
+ return false;
53
+ }
54
+
55
+ function isSpawnCall(node: TSESTree.CallExpression): boolean {
56
+ const callee = node.callee;
57
+
58
+ if (callee.type === AST_NODE_TYPES.Identifier) {
59
+ return callee.name === "spawn" || callee.name === "spawnSync";
60
+ }
61
+
62
+ if (
63
+ callee.type === AST_NODE_TYPES.MemberExpression &&
64
+ !callee.computed &&
65
+ callee.property.type === AST_NODE_TYPES.Identifier &&
66
+ (callee.property.name === "spawn" || callee.property.name === "spawnSync")
67
+ ) {
68
+ return true;
69
+ }
70
+
71
+ return false;
72
+ }
73
+
74
+ export const noSpawnWithShellRule = createRule<[], MessageIds>({
75
+ name: RULE_NAME,
76
+ meta: {
77
+ type: "problem",
78
+ docs: {
79
+ description:
80
+ "Disallow child_process.spawn/spawnSync with shell: true — shell execution enables command injection.",
81
+ },
82
+ schema: [],
83
+ messages: {
84
+ spawnWithShell:
85
+ "Do not pass `{ shell: true }` to `spawn`/`spawnSync` — execute the binary directly with argument arrays instead.",
86
+ },
87
+ },
88
+ defaultOptions: [],
89
+ create(context) {
90
+ return {
91
+ CallExpression(node: TSESTree.CallExpression) {
92
+ if (!isSpawnCall(node)) {
93
+ return;
94
+ }
95
+
96
+ for (const arg of node.arguments) {
97
+ if (isExpression(arg) && optionsEnableShell(arg)) {
98
+ context.report({ node, messageId: "spawnWithShell" });
99
+
100
+ return;
101
+ }
102
+ }
103
+ },
104
+ };
105
+ },
106
+ });
@@ -1,11 +1,15 @@
1
1
  import type { TSESLint } from "@typescript-eslint/utils";
2
2
 
3
+ import { caughtErrorLogRequiresCauseRule } from "./rules/caught-error-log-requires-cause";
4
+ import { loggerNotConsoleRule } from "./rules/logger-not-console";
3
5
  import { maskPiiFieldsRule } from "./rules/mask-pii-fields";
4
6
  import { noErrorStringifyRule } from "./rules/no-error-stringify";
5
7
  import { requireEventFieldRule } from "./rules/require-event-field";
6
8
  import type { IRulePack } from "../rule-packs.types";
7
9
 
8
10
  const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
11
+ "caught-error-log-requires-cause": caughtErrorLogRequiresCauseRule,
12
+ "logger-not-console": loggerNotConsoleRule,
9
13
  "mask-pii-fields": maskPiiFieldsRule,
10
14
  "no-error-stringify": noErrorStringifyRule,
11
15
  "require-event-field": requireEventFieldRule,
@@ -17,6 +21,8 @@ export const structuredLoggingPack: IRulePack = {
17
21
  "Structured logging best practices: PII masking, error handling, and event field requirements",
18
22
  rules,
19
23
  rulesConfig: {
24
+ "caught-error-log-requires-cause": "error",
25
+ "logger-not-console": "warn",
20
26
  "mask-pii-fields": "error",
21
27
  "no-error-stringify": "error",
22
28
  "require-event-field": "error",