nestjs-doctor 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  </p>
14
14
 
15
15
  <p align="center">
16
- 47 built-in rules across <b>security</b>, <b>performance</b>, <b>correctness</b>, and <b>architecture</b>. Outputs a <b>0-100 score</b> with actionable diagnostics. Zero config. Monorepo support. Built to catch the anti-patterns that AI-generated code loves to introduce.
16
+ 45 built-in rules across <b>security</b>, <b>performance</b>, <b>correctness</b>, and <b>architecture</b>. Outputs a <b>0-100 score</b> with actionable diagnostics. Zero config. Monorepo support. Built to catch the anti-patterns that AI-generated code loves to introduce.
17
17
  </p>
18
18
 
19
19
  ---
@@ -166,7 +166,7 @@ Or use a `"nestjs-doctor"` key in `package.json`.
166
166
  | Key | Type | Description |
167
167
  |-----|------|-------------|
168
168
  | `include` | `string[]` | Glob patterns to scan (default: `["**/*.ts"]`) |
169
- | `exclude` | `string[]` | Glob patterns to skip (default includes `node_modules`, `dist`, test files) |
169
+ | `exclude` | `string[]` | Glob patterns to skip (default includes `node_modules`, `dist`, `build`, `coverage`, `*.spec.ts`, `*.test.ts`, `*.e2e-spec.ts`, `*.e2e-test.ts`, `*.d.ts`, `test/`, `tests/`, `__tests__/`, `__mocks__/`, `__fixtures__/`) |
170
170
  | `minScore` | `number` | Minimum passing score (0-100). Exits with code 1 if below threshold |
171
171
  | `ignore.rules` | `string[]` | Rule IDs to suppress |
172
172
  | `ignore.files` | `string[]` | Glob patterns for files whose diagnostics are hidden |
@@ -234,15 +234,13 @@ mono.combined; // Merged DiagnoseResult
234
234
 
235
235
  ---
236
236
 
237
- ## Rules (47)
237
+ ## Rules (45)
238
238
 
239
- ### Security (11)
239
+ ### Security (9)
240
240
 
241
241
  | Rule | Severity | What it catches |
242
242
  |------|----------|-----------------|
243
243
  | `no-hardcoded-secrets` | error | API keys, tokens, passwords in source code |
244
- | `no-wildcard-cors` | error | CORS with `origin: '*'` or `origin: true` |
245
- | `no-unsafe-raw-query` | error | Template literal interpolation in raw SQL |
246
244
  | `no-eval` | error | `eval()` or `new Function()` usage |
247
245
  | `no-csrf-disabled` | error | Explicitly disabling CSRF protection |
248
246
  | `no-dangerous-redirects` | error | Redirects with user-controlled input |
@@ -257,7 +255,7 @@ mono.combined; // Merged DiagnoseResult
257
255
  | Rule | Severity | What it catches |
258
256
  |------|----------|-----------------|
259
257
  | `no-missing-injectable` | error | Provider in module missing `@Injectable()` |
260
- | `no-duplicate-routes` | error | Same method + path twice in a controller |
258
+ | `no-duplicate-routes` | error | Same method + path + version twice in a controller |
261
259
  | `no-missing-guard-method` | error | Guard class missing `canActivate()` |
262
260
  | `no-missing-pipe-method` | error | Pipe class missing `transform()` |
263
261
  | `no-missing-filter-catch` | error | `@Catch()` class missing `catch()` |
@@ -467,6 +467,18 @@ const noGodService = {
467
467
 
468
468
  //#endregion
469
469
  //#region src/rules/architecture/no-manual-instantiation.ts
470
+ const DI_ONLY_SUFFIXES = [
471
+ "Service",
472
+ "Repository",
473
+ "Gateway",
474
+ "Resolver"
475
+ ];
476
+ const CONTEXT_AWARE_SUFFIXES = [
477
+ "Guard",
478
+ "Interceptor",
479
+ "Pipe",
480
+ "Filter"
481
+ ];
470
482
  const noManualInstantiation = {
471
483
  meta: {
472
484
  id: "architecture/no-manual-instantiation",
@@ -479,7 +491,16 @@ const noManualInstantiation = {
479
491
  const newExpressions = context.sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.NewExpression);
480
492
  for (const expr of newExpressions) {
481
493
  const exprText = expr.getExpression().getText();
482
- if (exprText.endsWith("Service") || exprText.endsWith("Repository") || exprText.endsWith("Guard") || exprText.endsWith("Interceptor") || exprText.endsWith("Pipe") || exprText.endsWith("Filter") || exprText.endsWith("Gateway") || exprText.endsWith("Resolver")) context.report({
494
+ const isDiOnly = DI_ONLY_SUFFIXES.some((s) => exprText.endsWith(s));
495
+ const isContextAware = CONTEXT_AWARE_SUFFIXES.some((s) => exprText.endsWith(s));
496
+ if (!(isDiOnly || isContextAware)) continue;
497
+ if (isContextAware) {
498
+ if (expr.getFirstAncestorByKind(ts_morph.SyntaxKind.Decorator)) continue;
499
+ const inMethod = expr.getFirstAncestorByKind(ts_morph.SyntaxKind.MethodDeclaration);
500
+ const inConstructor = expr.getFirstAncestorByKind(ts_morph.SyntaxKind.Constructor);
501
+ if (!(inMethod || inConstructor)) continue;
502
+ }
503
+ context.report({
483
504
  filePath: context.filePath,
484
505
  message: `Manual instantiation of '${exprText}' detected. Use dependency injection instead.`,
485
506
  help: this.meta.help,
@@ -905,7 +926,7 @@ const noDuplicateRoutes = {
905
926
  id: "correctness/no-duplicate-routes",
906
927
  category: "correctness",
907
928
  severity: "error",
908
- description: "Same HTTP method + route path should not appear twice in a single controller",
929
+ description: "Same HTTP method + route path + version should not appear twice in a single controller",
909
930
  help: "Remove or rename one of the duplicate route handlers."
910
931
  },
911
932
  check(context) {
@@ -917,7 +938,8 @@ const noDuplicateRoutes = {
917
938
  if (!HTTP_METHOD_DECORATORS.has(decoratorName)) continue;
918
939
  const args = decorator.getArguments();
919
940
  const path = args.length > 0 ? args[0].getText() : "\"\"";
920
- const routeKey = `${decoratorName}:${path}`;
941
+ const versionDecorator = method.getDecorator("Version");
942
+ const routeKey = `${decoratorName}:${path}:${versionDecorator ? versionDecorator.getArguments()[0]?.getText() ?? "" : ""}`;
921
943
  const existing = routeMap.get(routeKey);
922
944
  if (existing) context.report({
923
945
  filePath: context.filePath,
@@ -984,6 +1006,7 @@ const noMissingFilterCatch = {
984
1006
  check(context) {
985
1007
  for (const cls of context.sourceFile.getClasses()) {
986
1008
  if (!hasDecorator(cls, "Catch")) continue;
1009
+ if (cls.getExtends()) continue;
987
1010
  if (!cls.getMethods().some((m) => m.getName() === "catch")) context.report({
988
1011
  filePath: context.filePath,
989
1012
  message: `Exception filter '${cls.getName()}' has @Catch() but is missing the 'catch()' method.`,
@@ -1010,6 +1033,7 @@ const noMissingGuardMethod = {
1010
1033
  const name = cls.getName() ?? "";
1011
1034
  if (!name.endsWith("Guard")) continue;
1012
1035
  if (!hasDecorator(cls, "Injectable")) continue;
1036
+ if (cls.getExtends()) continue;
1013
1037
  if (!cls.getMethods().some((m) => m.getName() === "canActivate")) context.report({
1014
1038
  filePath: context.filePath,
1015
1039
  message: `Guard '${name}' is missing the 'canActivate()' method.`,
@@ -1080,6 +1104,7 @@ const noMissingInterceptorMethod = {
1080
1104
  const name = cls.getName() ?? "";
1081
1105
  if (!name.endsWith("Interceptor")) continue;
1082
1106
  if (!hasDecorator(cls, "Injectable")) continue;
1107
+ if (cls.getExtends()) continue;
1083
1108
  if (!cls.getMethods().some((m) => m.getName() === "intercept")) context.report({
1084
1109
  filePath: context.filePath,
1085
1110
  message: `Interceptor '${name}' is missing the 'intercept()' method.`,
@@ -1132,6 +1157,7 @@ const noMissingPipeMethod = {
1132
1157
  const name = cls.getName() ?? "";
1133
1158
  if (!name.endsWith("Pipe")) continue;
1134
1159
  if (!hasDecorator(cls, "Injectable")) continue;
1160
+ if (cls.getExtends()) continue;
1135
1161
  if (!cls.getMethods().some((m) => m.getName() === "transform")) context.report({
1136
1162
  filePath: context.filePath,
1137
1163
  message: `Pipe '${name}' is missing the 'transform()' method.`,
@@ -1825,7 +1851,7 @@ const noExposedStackTrace = {
1825
1851
  //#region src/rules/security/no-hardcoded-secrets.ts
1826
1852
  const SECRET_PATTERNS = [
1827
1853
  {
1828
- pattern: /^[A-Za-z0-9+/]{40,}={0,2}$/,
1854
+ pattern: /^(?=.*\d)[A-Za-z0-9+/]{40,}={0,2}$/,
1829
1855
  name: "Base64 key"
1830
1856
  },
1831
1857
  {
@@ -1880,8 +1906,15 @@ const PLACEHOLDER_VALUES = new Set([
1880
1906
  "changeme",
1881
1907
  "password"
1882
1908
  ]);
1909
+ const DOT_SEPARATED_CONSTANT = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/;
1883
1910
  function isSuspiciousValue(value) {
1884
- return value.length >= 8 && !value.includes("${") && !value.startsWith("process.env") && !PLACEHOLDER_VALUES.has(value);
1911
+ if (value.length < 8) return false;
1912
+ if (value.includes("${")) return false;
1913
+ if (value.startsWith("process.env")) return false;
1914
+ if (PLACEHOLDER_VALUES.has(value)) return false;
1915
+ if (value.includes(" ")) return false;
1916
+ if (DOT_SEPARATED_CONSTANT.test(value)) return false;
1917
+ return true;
1885
1918
  }
1886
1919
  function hasSuspiciousName(name) {
1887
1920
  return VARIABLE_NAME_PATTERNS.some((p) => p.test(name));
@@ -1942,41 +1975,6 @@ const noHardcodedSecrets = {
1942
1975
  }
1943
1976
  };
1944
1977
 
1945
- //#endregion
1946
- //#region src/rules/security/no-unsafe-raw-query.ts
1947
- const RAW_QUERY_METHODS = new Set([
1948
- "$executeRaw",
1949
- "$queryRaw",
1950
- "$executeRawUnsafe",
1951
- "$queryRawUnsafe",
1952
- "query"
1953
- ]);
1954
- const noUnsafeRawQuery = {
1955
- meta: {
1956
- id: "security/no-unsafe-raw-query",
1957
- category: "security",
1958
- severity: "error",
1959
- description: "Raw SQL queries with template literal interpolation are a SQL injection risk",
1960
- help: "Use parameterized queries or Prisma's tagged template (Prisma.sql`...`) instead of string interpolation."
1961
- },
1962
- check(context) {
1963
- const callExpressions = context.sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.CallExpression);
1964
- for (const call of callExpressions) {
1965
- const methodName = call.getExpression().getText().split(".").pop() ?? "";
1966
- if (!RAW_QUERY_METHODS.has(methodName)) continue;
1967
- const args = call.getArguments();
1968
- if (args.length === 0) continue;
1969
- if (args[0].getKind() === ts_morph.SyntaxKind.TemplateExpression) context.report({
1970
- filePath: context.filePath,
1971
- message: `Raw SQL query '${methodName}()' uses template literal interpolation — SQL injection risk.`,
1972
- help: this.meta.help,
1973
- line: call.getStartLineNumber(),
1974
- column: 1
1975
- });
1976
- }
1977
- }
1978
- };
1979
-
1980
1978
  //#endregion
1981
1979
  //#region src/rules/security/no-weak-crypto.ts
1982
1980
  const WEAK_ALGORITHMS = new Set(["md5", "sha1"]);
@@ -2008,34 +2006,6 @@ const noWeakCrypto = {
2008
2006
  }
2009
2007
  };
2010
2008
 
2011
- //#endregion
2012
- //#region src/rules/security/no-wildcard-cors.ts
2013
- const noWildcardCors = {
2014
- meta: {
2015
- id: "security/no-wildcard-cors",
2016
- category: "security",
2017
- severity: "error",
2018
- description: "CORS should not be configured with origin: '*' or origin: true — allows any domain",
2019
- help: "Specify allowed origins explicitly instead of using wildcard CORS."
2020
- },
2021
- check(context) {
2022
- const propertyAssignments = context.sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.PropertyAssignment);
2023
- for (const prop of propertyAssignments) {
2024
- if (prop.getName() !== "origin") continue;
2025
- const initializer = prop.getInitializer();
2026
- if (!initializer) continue;
2027
- const text = initializer.getText();
2028
- if (text === "'*'" || text === "\"*\"" || text === "true") context.report({
2029
- filePath: context.filePath,
2030
- message: `CORS configured with origin: ${text} — allows requests from any domain.`,
2031
- help: this.meta.help,
2032
- line: prop.getStartLineNumber(),
2033
- column: 1
2034
- });
2035
- }
2036
- }
2037
- };
2038
-
2039
2009
  //#endregion
2040
2010
  //#region src/rules/security/require-auth-guard.ts
2041
2011
  const requireAuthGuard = {
@@ -2122,8 +2092,6 @@ const allRules = [
2122
2092
  requireInjectDecorator,
2123
2093
  noMissingInjectable,
2124
2094
  noHardcodedSecrets,
2125
- noWildcardCors,
2126
- noUnsafeRawQuery,
2127
2095
  requireAuthGuard,
2128
2096
  noEval,
2129
2097
  noWeakCrypto,
@@ -2224,7 +2192,13 @@ const DEFAULT_CONFIG = {
2224
2192
  "**/*.spec.ts",
2225
2193
  "**/*.test.ts",
2226
2194
  "**/*.e2e-spec.ts",
2227
- "**/*.d.ts"
2195
+ "**/*.e2e-test.ts",
2196
+ "**/*.d.ts",
2197
+ "**/test/**",
2198
+ "**/tests/**",
2199
+ "**/__tests__/**",
2200
+ "**/__mocks__/**",
2201
+ "**/__fixtures__/**"
2228
2202
  ]
2229
2203
  };
2230
2204
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/engine/module-graph.ts","../../src/engine/type-resolver.ts","../../src/types/diagnostic.ts","../../src/types/config.ts","../../src/rules/types.ts","../../src/rules/index.ts","../../src/types/errors.ts","../../src/types/result.ts","../../src/api/index.ts"],"mappings":";;;UASiB,UAAA;EAChB,gBAAA,EAAkB,gBAAA;EAClB,WAAA;EACA,OAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,SAAA;AAAA;AAAA,UAGgB,WAAA;EAChB,KAAA,EAAO,GAAA,SAAY,GAAA;EACnB,OAAA,EAAS,GAAA,SAAY,UAAA;EACrB,gBAAA,EAAkB,GAAA,SAAY,UAAA;AAAA;;;UCjBd,YAAA;EAChB,gBAAA,EAAkB,gBAAA;EAClB,YAAA;EACA,QAAA;EACA,IAAA;EACA,iBAAA;AAAA;;;KCVW,QAAA;AAAA,KACA,QAAA;AAAA,UAMK,UAAA;EAChB,QAAA,EAAU,QAAA;EACV,MAAA;EACA,QAAA;EACA,IAAA;EACA,IAAA;EACA,OAAA;EACA,IAAA;EACA,QAAA,EAAU,QAAA;AAAA;;;UCbM,YAAA;EAChB,OAAA;EACA,QAAA,GAAW,QAAA;AAAA;AAAA,UAGK,wBAAA;EAChB,KAAA;EACA,KAAA;AAAA;AAAA,UAGgB,kBAAA;EAChB,UAAA,GAAa,OAAA,CAAQ,MAAA,CAAO,QAAA;EAC5B,OAAA;EACA,MAAA,GAAS,wBAAA;EACT,OAAA;EACA,QAAA;EACA,KAAA,GAAQ,MAAA,SAAe,YAAA;EACvB,UAAA;IACC,kBAAA;IACA,gBAAA;IACA,iBAAA;IACA,cAAA;EAAA;AAAA;;;KCjBU,SAAA;AAAA,UAEK,QAAA;EAChB,QAAA,EAAU,QAAA;EACV,WAAA;EACA,IAAA;EACA,EAAA;EACA,KAAA,GAAQ,SAAA;EACR,QAAA,EAAU,QAAA;AAAA;AAAA,UAGM,WAAA;EAChB,QAAA;EACA,MAAA,CAAO,UAAA,EAAY,IAAA,CAAK,UAAA;EACxB,UAAA,EAAY,UAAA;AAAA;AAAA,UAGI,kBAAA;EAChB,MAAA,EAAQ,kBAAA;EACR,KAAA;EACA,WAAA,EAAa,WAAA;EACb,OAAA,EAAS,OAAA;EACT,SAAA,EAAW,GAAA,SAAY,YAAA;EACvB,MAAA,CAAO,UAAA,EAAY,IAAA,CAAK,UAAA;AAAA;AAAA,UAGR,IAAA;EAChB,KAAA,CAAM,OAAA,EAAS,WAAA;EACf,IAAA,EAAM,QAAA;AAAA;AAAA,UAGU,WAAA;EAChB,KAAA,CAAM,OAAA,EAAS,kBAAA;EACf,IAAA,EAAM,QAAA;AAAA;AAAA,KAGK,OAAA,GAAU,IAAA,GAAO,WAAA;;;iBCsEb,QAAA,CAAA,GAAY,OAAA;;;cChHf,iBAAA,SAA0B,KAAA;cAC1B,OAAA;AAAA;AAAA,cAMA,kBAAA,SAA2B,iBAAA;cAC3B,OAAA;AAAA;AAAA,cAMA,SAAA,SAAkB,iBAAA;cAClB,OAAA;AAAA;AAAA,cAMA,eAAA,SAAwB,iBAAA;cACxB,OAAA;AAAA;;;UCpBI,KAAA;EAChB,KAAA;EACA,KAAA;AAAA;AAAA,UAGgB,WAAA;EAChB,SAAA;EACA,SAAA;EACA,WAAA;EACA,IAAA;EACA,WAAA;EACA,GAAA;AAAA;AAAA,UAGgB,eAAA;EAChB,UAAA,EAAY,MAAA,CAAO,QAAA;EACnB,MAAA;EACA,IAAA;EACA,KAAA;EACA,QAAA;AAAA;AAAA,UAGgB,aAAA;EAChB,KAAA;EACA,MAAA;AAAA;AAAA,UAGgB,cAAA;EAChB,WAAA,EAAa,UAAA;EACb,SAAA;EACA,OAAA,EAAS,WAAA;EACT,UAAA,EAAY,aAAA;EACZ,KAAA,EAAO,KAAA;EACP,OAAA,EAAS,eAAA;AAAA;AAAA,UAGO,gBAAA;EAChB,IAAA;EACA,MAAA,EAAQ,cAAA;AAAA;AAAA,UAGQ,cAAA;EAChB,QAAA,EAAU,cAAA;EACV,SAAA;EACA,UAAA;EACA,WAAA,EAAa,gBAAA;AAAA;;;;;;;;;;;iBCiBQ,QAAA,CACrB,IAAA,UACA,OAAA;EAAW,MAAA;AAAA,IAAsB,OAAA,CAFJ,cAAA;AR7C9B;;;;;;;;;;;AAAA,iBQgEsB,gBAAA,CACrB,IAAA,UACA,OAAA;EAAW,MAAA;AAAA,IAAsB,OAAA,CAFI,cAAA"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/engine/module-graph.ts","../../src/engine/type-resolver.ts","../../src/types/diagnostic.ts","../../src/types/config.ts","../../src/rules/types.ts","../../src/rules/index.ts","../../src/types/errors.ts","../../src/types/result.ts","../../src/api/index.ts"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/engine/module-graph.ts","../../src/engine/type-resolver.ts","../../src/types/diagnostic.ts","../../src/types/config.ts","../../src/rules/types.ts","../../src/rules/index.ts","../../src/types/errors.ts","../../src/types/result.ts","../../src/api/index.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/engine/module-graph.ts","../../src/engine/type-resolver.ts","../../src/types/diagnostic.ts","../../src/types/config.ts","../../src/rules/types.ts","../../src/rules/index.ts","../../src/types/errors.ts","../../src/types/result.ts","../../src/api/index.ts"],"mappings":"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"}
@@ -438,6 +438,18 @@ const noGodService = {
438
438
 
439
439
  //#endregion
440
440
  //#region src/rules/architecture/no-manual-instantiation.ts
441
+ const DI_ONLY_SUFFIXES = [
442
+ "Service",
443
+ "Repository",
444
+ "Gateway",
445
+ "Resolver"
446
+ ];
447
+ const CONTEXT_AWARE_SUFFIXES = [
448
+ "Guard",
449
+ "Interceptor",
450
+ "Pipe",
451
+ "Filter"
452
+ ];
441
453
  const noManualInstantiation = {
442
454
  meta: {
443
455
  id: "architecture/no-manual-instantiation",
@@ -450,7 +462,16 @@ const noManualInstantiation = {
450
462
  const newExpressions = context.sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression);
451
463
  for (const expr of newExpressions) {
452
464
  const exprText = expr.getExpression().getText();
453
- if (exprText.endsWith("Service") || exprText.endsWith("Repository") || exprText.endsWith("Guard") || exprText.endsWith("Interceptor") || exprText.endsWith("Pipe") || exprText.endsWith("Filter") || exprText.endsWith("Gateway") || exprText.endsWith("Resolver")) context.report({
465
+ const isDiOnly = DI_ONLY_SUFFIXES.some((s) => exprText.endsWith(s));
466
+ const isContextAware = CONTEXT_AWARE_SUFFIXES.some((s) => exprText.endsWith(s));
467
+ if (!(isDiOnly || isContextAware)) continue;
468
+ if (isContextAware) {
469
+ if (expr.getFirstAncestorByKind(SyntaxKind.Decorator)) continue;
470
+ const inMethod = expr.getFirstAncestorByKind(SyntaxKind.MethodDeclaration);
471
+ const inConstructor = expr.getFirstAncestorByKind(SyntaxKind.Constructor);
472
+ if (!(inMethod || inConstructor)) continue;
473
+ }
474
+ context.report({
454
475
  filePath: context.filePath,
455
476
  message: `Manual instantiation of '${exprText}' detected. Use dependency injection instead.`,
456
477
  help: this.meta.help,
@@ -876,7 +897,7 @@ const noDuplicateRoutes = {
876
897
  id: "correctness/no-duplicate-routes",
877
898
  category: "correctness",
878
899
  severity: "error",
879
- description: "Same HTTP method + route path should not appear twice in a single controller",
900
+ description: "Same HTTP method + route path + version should not appear twice in a single controller",
880
901
  help: "Remove or rename one of the duplicate route handlers."
881
902
  },
882
903
  check(context) {
@@ -888,7 +909,8 @@ const noDuplicateRoutes = {
888
909
  if (!HTTP_METHOD_DECORATORS.has(decoratorName)) continue;
889
910
  const args = decorator.getArguments();
890
911
  const path = args.length > 0 ? args[0].getText() : "\"\"";
891
- const routeKey = `${decoratorName}:${path}`;
912
+ const versionDecorator = method.getDecorator("Version");
913
+ const routeKey = `${decoratorName}:${path}:${versionDecorator ? versionDecorator.getArguments()[0]?.getText() ?? "" : ""}`;
892
914
  const existing = routeMap.get(routeKey);
893
915
  if (existing) context.report({
894
916
  filePath: context.filePath,
@@ -955,6 +977,7 @@ const noMissingFilterCatch = {
955
977
  check(context) {
956
978
  for (const cls of context.sourceFile.getClasses()) {
957
979
  if (!hasDecorator(cls, "Catch")) continue;
980
+ if (cls.getExtends()) continue;
958
981
  if (!cls.getMethods().some((m) => m.getName() === "catch")) context.report({
959
982
  filePath: context.filePath,
960
983
  message: `Exception filter '${cls.getName()}' has @Catch() but is missing the 'catch()' method.`,
@@ -981,6 +1004,7 @@ const noMissingGuardMethod = {
981
1004
  const name = cls.getName() ?? "";
982
1005
  if (!name.endsWith("Guard")) continue;
983
1006
  if (!hasDecorator(cls, "Injectable")) continue;
1007
+ if (cls.getExtends()) continue;
984
1008
  if (!cls.getMethods().some((m) => m.getName() === "canActivate")) context.report({
985
1009
  filePath: context.filePath,
986
1010
  message: `Guard '${name}' is missing the 'canActivate()' method.`,
@@ -1051,6 +1075,7 @@ const noMissingInterceptorMethod = {
1051
1075
  const name = cls.getName() ?? "";
1052
1076
  if (!name.endsWith("Interceptor")) continue;
1053
1077
  if (!hasDecorator(cls, "Injectable")) continue;
1078
+ if (cls.getExtends()) continue;
1054
1079
  if (!cls.getMethods().some((m) => m.getName() === "intercept")) context.report({
1055
1080
  filePath: context.filePath,
1056
1081
  message: `Interceptor '${name}' is missing the 'intercept()' method.`,
@@ -1103,6 +1128,7 @@ const noMissingPipeMethod = {
1103
1128
  const name = cls.getName() ?? "";
1104
1129
  if (!name.endsWith("Pipe")) continue;
1105
1130
  if (!hasDecorator(cls, "Injectable")) continue;
1131
+ if (cls.getExtends()) continue;
1106
1132
  if (!cls.getMethods().some((m) => m.getName() === "transform")) context.report({
1107
1133
  filePath: context.filePath,
1108
1134
  message: `Pipe '${name}' is missing the 'transform()' method.`,
@@ -1796,7 +1822,7 @@ const noExposedStackTrace = {
1796
1822
  //#region src/rules/security/no-hardcoded-secrets.ts
1797
1823
  const SECRET_PATTERNS = [
1798
1824
  {
1799
- pattern: /^[A-Za-z0-9+/]{40,}={0,2}$/,
1825
+ pattern: /^(?=.*\d)[A-Za-z0-9+/]{40,}={0,2}$/,
1800
1826
  name: "Base64 key"
1801
1827
  },
1802
1828
  {
@@ -1851,8 +1877,15 @@ const PLACEHOLDER_VALUES = new Set([
1851
1877
  "changeme",
1852
1878
  "password"
1853
1879
  ]);
1880
+ const DOT_SEPARATED_CONSTANT = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/;
1854
1881
  function isSuspiciousValue(value) {
1855
- return value.length >= 8 && !value.includes("${") && !value.startsWith("process.env") && !PLACEHOLDER_VALUES.has(value);
1882
+ if (value.length < 8) return false;
1883
+ if (value.includes("${")) return false;
1884
+ if (value.startsWith("process.env")) return false;
1885
+ if (PLACEHOLDER_VALUES.has(value)) return false;
1886
+ if (value.includes(" ")) return false;
1887
+ if (DOT_SEPARATED_CONSTANT.test(value)) return false;
1888
+ return true;
1856
1889
  }
1857
1890
  function hasSuspiciousName(name) {
1858
1891
  return VARIABLE_NAME_PATTERNS.some((p) => p.test(name));
@@ -1913,41 +1946,6 @@ const noHardcodedSecrets = {
1913
1946
  }
1914
1947
  };
1915
1948
 
1916
- //#endregion
1917
- //#region src/rules/security/no-unsafe-raw-query.ts
1918
- const RAW_QUERY_METHODS = new Set([
1919
- "$executeRaw",
1920
- "$queryRaw",
1921
- "$executeRawUnsafe",
1922
- "$queryRawUnsafe",
1923
- "query"
1924
- ]);
1925
- const noUnsafeRawQuery = {
1926
- meta: {
1927
- id: "security/no-unsafe-raw-query",
1928
- category: "security",
1929
- severity: "error",
1930
- description: "Raw SQL queries with template literal interpolation are a SQL injection risk",
1931
- help: "Use parameterized queries or Prisma's tagged template (Prisma.sql`...`) instead of string interpolation."
1932
- },
1933
- check(context) {
1934
- const callExpressions = context.sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
1935
- for (const call of callExpressions) {
1936
- const methodName = call.getExpression().getText().split(".").pop() ?? "";
1937
- if (!RAW_QUERY_METHODS.has(methodName)) continue;
1938
- const args = call.getArguments();
1939
- if (args.length === 0) continue;
1940
- if (args[0].getKind() === SyntaxKind.TemplateExpression) context.report({
1941
- filePath: context.filePath,
1942
- message: `Raw SQL query '${methodName}()' uses template literal interpolation — SQL injection risk.`,
1943
- help: this.meta.help,
1944
- line: call.getStartLineNumber(),
1945
- column: 1
1946
- });
1947
- }
1948
- }
1949
- };
1950
-
1951
1949
  //#endregion
1952
1950
  //#region src/rules/security/no-weak-crypto.ts
1953
1951
  const WEAK_ALGORITHMS = new Set(["md5", "sha1"]);
@@ -1979,34 +1977,6 @@ const noWeakCrypto = {
1979
1977
  }
1980
1978
  };
1981
1979
 
1982
- //#endregion
1983
- //#region src/rules/security/no-wildcard-cors.ts
1984
- const noWildcardCors = {
1985
- meta: {
1986
- id: "security/no-wildcard-cors",
1987
- category: "security",
1988
- severity: "error",
1989
- description: "CORS should not be configured with origin: '*' or origin: true — allows any domain",
1990
- help: "Specify allowed origins explicitly instead of using wildcard CORS."
1991
- },
1992
- check(context) {
1993
- const propertyAssignments = context.sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAssignment);
1994
- for (const prop of propertyAssignments) {
1995
- if (prop.getName() !== "origin") continue;
1996
- const initializer = prop.getInitializer();
1997
- if (!initializer) continue;
1998
- const text = initializer.getText();
1999
- if (text === "'*'" || text === "\"*\"" || text === "true") context.report({
2000
- filePath: context.filePath,
2001
- message: `CORS configured with origin: ${text} — allows requests from any domain.`,
2002
- help: this.meta.help,
2003
- line: prop.getStartLineNumber(),
2004
- column: 1
2005
- });
2006
- }
2007
- }
2008
- };
2009
-
2010
1980
  //#endregion
2011
1981
  //#region src/rules/security/require-auth-guard.ts
2012
1982
  const requireAuthGuard = {
@@ -2093,8 +2063,6 @@ const allRules = [
2093
2063
  requireInjectDecorator,
2094
2064
  noMissingInjectable,
2095
2065
  noHardcodedSecrets,
2096
- noWildcardCors,
2097
- noUnsafeRawQuery,
2098
2066
  requireAuthGuard,
2099
2067
  noEval,
2100
2068
  noWeakCrypto,
@@ -2195,7 +2163,13 @@ const DEFAULT_CONFIG = {
2195
2163
  "**/*.spec.ts",
2196
2164
  "**/*.test.ts",
2197
2165
  "**/*.e2e-spec.ts",
2198
- "**/*.d.ts"
2166
+ "**/*.e2e-test.ts",
2167
+ "**/*.d.ts",
2168
+ "**/test/**",
2169
+ "**/tests/**",
2170
+ "**/__tests__/**",
2171
+ "**/__mocks__/**",
2172
+ "**/__fixtures__/**"
2199
2173
  ]
2200
2174
  };
2201
2175