technical-debt-radar 1.2.2 → 1.3.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 (2) hide show
  1. package/dist/index.js +171 -26
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -87,7 +87,9 @@ var require_constants = __commonJS({
87
87
  EMPTY_CATCH_BLOCK: "empty-catch-block",
88
88
  MISSING_ERROR_LOGGING: "missing-error-logging",
89
89
  TRANSACTION_NO_TIMEOUT: "transaction-no-timeout",
90
- MISSING_NULL_GUARD: "missing-null-guard"
90
+ MISSING_NULL_GUARD: "missing-null-guard",
91
+ MISSING_DTO_VALIDATION: "missing-dto-validation",
92
+ UNGUARDED_ROUTE_TODO: "unguarded-route-todo"
91
93
  };
92
94
  exports2.PERFORMANCE_RULES = {
93
95
  UNBOUNDED_FIND_MANY: "unbounded-find-many",
@@ -98,7 +100,8 @@ var require_constants = __commonJS({
98
100
  MISSING_PAGINATION: "missing-pagination-endpoint",
99
101
  UNFILTERED_COUNT: "unfiltered-count-large-table",
100
102
  RAW_SQL_NO_LIMIT: "raw-sql-no-limit",
101
- RAW_SQL_UNSAFE: "raw-sql-unsafe"
103
+ RAW_SQL_UNSAFE: "raw-sql-unsafe",
104
+ DANGEROUS_DELETE_ALL: "dangerous-delete-all"
102
105
  };
103
106
  exports2.MAINTAINABILITY_RULES = {
104
107
  HIGH_COMPLEXITY: "high-complexity",
@@ -14030,6 +14033,26 @@ var require_runtime_risk_detector = __commonJS({
14030
14033
  }
14031
14034
  }
14032
14035
  });
14036
+ sourceFile.forEachDescendant((node) => {
14037
+ if (!ts_morph_1.Node.isCallExpression(node))
14038
+ return;
14039
+ const expr = node.getExpression();
14040
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
14041
+ return;
14042
+ if (expr.getName() !== "map")
14043
+ return;
14044
+ const args = node.getArguments();
14045
+ if (args.length === 0)
14046
+ return;
14047
+ const callbackText = args[0].getText();
14048
+ if (!/\(\?[=!]|\.\*|\[.*?\]|\\[bBdDwWsS]/.test(callbackText))
14049
+ return;
14050
+ const fileText = sourceFile.getText();
14051
+ if (!/\$regex/.test(fileText))
14052
+ return;
14053
+ const fn = getEnclosingFunction(node, allFunctions);
14054
+ violations.push(makeViolation(shared_1.RUNTIME_RISK_RULES.REDOS_VULNERABLE_REGEX, filePath, node.getStartLineNumber(), "Dynamic regex construction from user input used with $regex \u2014 ReDoS vulnerability", policy, fn?.name, "Use a safe text search method (MongoDB $text index) instead of $regex with user input. Escape special regex characters."));
14055
+ });
14033
14056
  }
14034
14057
  function isRedosVulnerable(pattern) {
14035
14058
  const nestedQuantifier = /([+*])\)?[+*{]/;
@@ -14464,6 +14487,7 @@ var require_perf_pattern_detector = __commonJS({
14464
14487
  detectMissingPagination(sourceFile, file.path, fns, policy, violations);
14465
14488
  detectUnfilteredCount(sourceFile, file.path, fns, policy, violations);
14466
14489
  detectRawSqlNoLimit(sourceFile, file.path, fns, policy, violations);
14490
+ detectDangerousDeleteAll(sourceFile, file.path, fns, policy, violations);
14467
14491
  project.removeSourceFile(sourceFile);
14468
14492
  }
14469
14493
  return applyExceptions(violations, policy);
@@ -14709,14 +14733,26 @@ var require_perf_pattern_detector = __commonJS({
14709
14733
  if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
14710
14734
  return void 0;
14711
14735
  const obj = expr.getExpression();
14712
- if (!ts_morph_1.Node.isIdentifier(obj))
14713
- return void 0;
14714
- const name = obj.getText();
14715
- if (!/^[A-Z]/.test(name))
14716
- return void 0;
14717
- if (/^(Promise|Array|Object|String|Number|Boolean|Buffer|JSON|Math|Date|Map|Set|Error|RegExp|console|process|Sequelize|DataTypes)$/.test(name))
14718
- return void 0;
14719
- return name;
14736
+ if (ts_morph_1.Node.isIdentifier(obj)) {
14737
+ const name = obj.getText();
14738
+ if (!/^[A-Z]/.test(name))
14739
+ return void 0;
14740
+ if (/^(Promise|Array|Object|String|Number|Boolean|Buffer|JSON|Math|Date|Map|Set|Error|RegExp|console|process|Sequelize|DataTypes)$/.test(name))
14741
+ return void 0;
14742
+ return name;
14743
+ }
14744
+ if (ts_morph_1.Node.isPropertyAccessExpression(obj)) {
14745
+ const propName = obj.getName();
14746
+ const thisExpr = obj.getExpression();
14747
+ if (thisExpr.getKind() === ts_morph_1.SyntaxKind.ThisKeyword) {
14748
+ const match = propName.match(/^(\w+?)(Model|Repository|Repo|Service|Collection)$/i);
14749
+ if (match)
14750
+ return match[1].charAt(0).toUpperCase() + match[1].slice(1);
14751
+ if (/^[a-z]/.test(propName))
14752
+ return propName.charAt(0).toUpperCase() + propName.slice(1);
14753
+ }
14754
+ }
14755
+ return void 0;
14720
14756
  }
14721
14757
  var SEQUELIZE_FIND_METHODS = /* @__PURE__ */ new Set([
14722
14758
  "findAll",
@@ -16082,6 +16118,25 @@ var require_perf_pattern_detector = __commonJS({
16082
16118
  function escapeRegex(str) {
16083
16119
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16084
16120
  }
16121
+ function detectDangerousDeleteAll(sourceFile, filePath, fns, policy, violations) {
16122
+ const DANGEROUS_DELETE_METHODS = /* @__PURE__ */ new Set(["deleteMany", "remove", "clear", "destroyAll"]);
16123
+ sourceFile.forEachDescendant((node) => {
16124
+ if (!ts_morph_1.Node.isCallExpression(node))
16125
+ return;
16126
+ const expr = node.getExpression();
16127
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
16128
+ return;
16129
+ const methodName = expr.getName();
16130
+ if (!DANGEROUS_DELETE_METHODS.has(methodName))
16131
+ return;
16132
+ const args = node.getArguments();
16133
+ const hasFilter = args.length > 0 && !(ts_morph_1.Node.isObjectLiteralExpression(args[0]) && args[0].getProperties().length === 0);
16134
+ if (hasFilter)
16135
+ return;
16136
+ const fn = getEnclosingFn(node, fns);
16137
+ pushIfNotNull(violations, makeViolation(shared_1.PERFORMANCE_RULES.DANGEROUS_DELETE_ALL, filePath, node.getStartLineNumber(), `${methodName}() with empty or no filter \u2014 may delete all records`, policy, void 0, fn?.name, "Add a filter to deleteMany() or use deleteOne() \u2014 deleting all records is rarely intentional"));
16138
+ });
16139
+ }
16085
16140
  function applyExceptions(violations, policy) {
16086
16141
  return violations.filter((v) => !policy.exceptions.some((ex) => ex.isActive && ex.rule === v.ruleId && v.file.includes(ex.file)));
16087
16142
  }
@@ -16115,6 +16170,8 @@ var require_reliability_detector = __commonJS({
16115
16170
  detectMissingErrorLogging(sourceFile, file.path, fns, policy, violations);
16116
16171
  detectTransactionNoTimeout(sourceFile, file.path, fns, policy, violations);
16117
16172
  detectMissingNullGuard(sourceFile, file.path, fns, policy, violations);
16173
+ detectMissingDtoValidation(sourceFile, file.path, fns, policy, violations);
16174
+ detectUnguardedRouteTodo(sourceFile, file.path, fns, policy, violations);
16118
16175
  project.removeSourceFile(sourceFile);
16119
16176
  }
16120
16177
  return applyExceptions(violations, policy);
@@ -16571,25 +16628,53 @@ var require_reliability_detector = __commonJS({
16571
16628
  }
16572
16629
  return false;
16573
16630
  }
16631
+ var SDK_CLIENTS_NO_TIMEOUT = /* @__PURE__ */ new Set([
16632
+ "Replicate",
16633
+ "OpenAI",
16634
+ "Anthropic",
16635
+ "Stripe",
16636
+ "Twilio",
16637
+ "SendGrid"
16638
+ ]);
16574
16639
  function detectExternalCallNoTimeout(sourceFile, filePath, fns, policy, violations) {
16575
16640
  sourceFile.forEachDescendant((node) => {
16576
- if (!ts_morph_1.Node.isCallExpression(node))
16577
- return;
16578
- let callType = null;
16579
- for (const [name, matcher] of Object.entries(EXTERNAL_CALL_PATTERNS)) {
16580
- if (matcher(node)) {
16581
- callType = name;
16582
- break;
16641
+ if (ts_morph_1.Node.isCallExpression(node)) {
16642
+ let callType = null;
16643
+ for (const [name, matcher] of Object.entries(EXTERNAL_CALL_PATTERNS)) {
16644
+ if (matcher(node)) {
16645
+ callType = name;
16646
+ break;
16647
+ }
16583
16648
  }
16584
- }
16585
- if (!callType)
16586
- return;
16587
- if (hasTimeoutOption(node))
16588
- return;
16589
- if (isInsideRetryLoop(node))
16649
+ if (!callType)
16650
+ return;
16651
+ if (hasTimeoutOption(node))
16652
+ return;
16653
+ if (isInsideRetryLoop(node))
16654
+ return;
16655
+ const fn = getEnclosingFn(node, fns);
16656
+ violations.push(makeViolation(shared_1.RELIABILITY_RULES.EXTERNAL_CALL_NO_TIMEOUT, filePath, node.getStartLineNumber(), `${callType}() call without timeout \u2014 may hang indefinitely`, policy, fn?.name, `Add a timeout option or use AbortController with setTimeout`));
16590
16657
  return;
16591
- const fn = getEnclosingFn(node, fns);
16592
- violations.push(makeViolation(shared_1.RELIABILITY_RULES.EXTERNAL_CALL_NO_TIMEOUT, filePath, node.getStartLineNumber(), `${callType}() call without timeout \u2014 may hang indefinitely`, policy, fn?.name, `Add a timeout option or use AbortController with setTimeout`));
16658
+ }
16659
+ if (ts_morph_1.Node.isNewExpression(node)) {
16660
+ const expr = node.getExpression();
16661
+ if (!ts_morph_1.Node.isIdentifier(expr))
16662
+ return;
16663
+ const className = expr.getText();
16664
+ if (!SDK_CLIENTS_NO_TIMEOUT.has(className))
16665
+ return;
16666
+ const args = node.getArguments();
16667
+ const hasTimeout = args.some((arg) => {
16668
+ if (ts_morph_1.Node.isObjectLiteralExpression(arg)) {
16669
+ return /timeout/i.test(arg.getText());
16670
+ }
16671
+ return false;
16672
+ });
16673
+ if (hasTimeout)
16674
+ return;
16675
+ const fn = getEnclosingFn(node, fns);
16676
+ violations.push(makeViolation(shared_1.RELIABILITY_RULES.EXTERNAL_CALL_NO_TIMEOUT, filePath, node.getStartLineNumber(), `new ${className}() without timeout option \u2014 API calls may hang indefinitely`, policy, fn?.name, `Add a timeout option: new ${className}({ timeout: 30000, ... })`));
16677
+ }
16593
16678
  });
16594
16679
  }
16595
16680
  function isInsideRetryLoop(node) {
@@ -16839,6 +16924,58 @@ var require_reliability_detector = __commonJS({
16839
16924
  violations.push(makeViolation(shared_1.RELIABILITY_RULES.MISSING_NULL_GUARD, filePath, node.getStartLineNumber(), `Array index access '${varName}[${indexArg.getText()}]' without length guard \u2014 may throw on empty results`, policy, fn?.name, `Add a guard: if (${varName}.length === 0) throw new NotFoundException();`));
16840
16925
  });
16841
16926
  }
16927
+ function detectMissingDtoValidation(sourceFile, filePath, fns, policy, violations) {
16928
+ if (!filePath.includes("controller"))
16929
+ return;
16930
+ sourceFile.forEachDescendant((node) => {
16931
+ if (!ts_morph_1.Node.isMethodDeclaration(node))
16932
+ return;
16933
+ for (const param of node.getParameters()) {
16934
+ const decorators = param.getDecorators();
16935
+ const hasBody = decorators.some((d) => d.getName() === "Body");
16936
+ const hasQuery = decorators.some((d) => d.getName() === "Query");
16937
+ if (!hasBody && !hasQuery)
16938
+ continue;
16939
+ const typeNode = param.getTypeNode();
16940
+ if (!typeNode)
16941
+ continue;
16942
+ const typeText = typeNode.getText().trim();
16943
+ if (typeText !== "any")
16944
+ continue;
16945
+ const decName = hasBody ? "@Body()" : "@Query()";
16946
+ const fn = getEnclosingFn(node, fns);
16947
+ violations.push(makeViolation(shared_1.RELIABILITY_RULES.MISSING_DTO_VALIDATION, filePath, node.getStartLineNumber(), `${decName} parameter typed as 'any' \u2014 bypasses class-validator validation`, policy, fn?.name, "Create a DTO class with class-validator decorators and use it instead of any"));
16948
+ }
16949
+ });
16950
+ }
16951
+ var ROUTE_DECORATORS = /* @__PURE__ */ new Set(["Get", "Post", "Put", "Delete", "Patch", "All", "Head", "Options"]);
16952
+ var GUARD_TODO_PATTERN = /TODO|FIXME|HACK|guard|auth|security|protected/i;
16953
+ function detectUnguardedRouteTodo(sourceFile, filePath, fns, policy, violations) {
16954
+ if (!filePath.includes("controller"))
16955
+ return;
16956
+ sourceFile.forEachDescendant((node) => {
16957
+ if (!ts_morph_1.Node.isMethodDeclaration(node))
16958
+ return;
16959
+ const decorators = node.getDecorators();
16960
+ const isRoute = decorators.some((d) => ROUTE_DECORATORS.has(d.getName()));
16961
+ if (!isRoute)
16962
+ return;
16963
+ const fullText = node.getFullText();
16964
+ if (!GUARD_TODO_PATTERN.test(fullText))
16965
+ return;
16966
+ const hasGuard = decorators.some((d) => d.getName() === "UseGuards");
16967
+ if (hasGuard)
16968
+ return;
16969
+ const classNode = node.getParent();
16970
+ if (classNode && ts_morph_1.Node.isClassDeclaration(classNode)) {
16971
+ const classGuard = classNode.getDecorators().some((d) => d.getName() === "UseGuards");
16972
+ if (classGuard)
16973
+ return;
16974
+ }
16975
+ const fn = getEnclosingFn(node, fns);
16976
+ violations.push(makeViolation(shared_1.RELIABILITY_RULES.UNGUARDED_ROUTE_TODO, filePath, node.getStartLineNumber(), `Route handler '${node.getName?.() ?? "anonymous"}' has TODO/FIXME comment and no @UseGuards() \u2014 unprotected endpoint`, policy, fn?.name, "Add @UseGuards(JwtAuthGuard) or appropriate guard \u2014 this route is currently unprotected"));
16977
+ });
16978
+ }
16842
16979
  function escapeRegex(str) {
16843
16980
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16844
16981
  }
@@ -19583,7 +19720,15 @@ async function scanCommand(targetPath, options) {
19583
19720
  commit: getGitCommit(),
19584
19721
  duration: Date.now() - scanStart,
19585
19722
  usedAi: shouldRunAi,
19586
- aiCreditsUsed: 0
19723
+ aiCreditsUsed: 0,
19724
+ // PR metadata from CI environment (set by GitHub Action or GitLab CI)
19725
+ ...process.env.RADAR_PR_NUMBER ? {
19726
+ prNumber: parseInt(process.env.RADAR_PR_NUMBER, 10),
19727
+ prTitle: process.env.RADAR_PR_TITLE,
19728
+ prUrl: process.env.RADAR_PR_URL,
19729
+ platform: process.env.RADAR_PLATFORM ?? "github",
19730
+ mode
19731
+ } : {}
19587
19732
  };
19588
19733
  try {
19589
19734
  await client.reportScan(reportData);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "technical-debt-radar",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
4
4
  "description": "Stop Node.js production crashes before merge. 47 detection patterns across 5 categories.",
5
5
  "bin": {
6
6
  "radar": "dist/index.js",