technical-debt-radar 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +171 -26
- 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 (
|
|
14713
|
-
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
14718
|
-
return
|
|
14719
|
-
|
|
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 (ts_morph_1.Node.isThisExpression(thisExpr) || ts_morph_1.Node.isIdentifier(thisExpr) && thisExpr.getText() === "this") {
|
|
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 (
|
|
16577
|
-
|
|
16578
|
-
|
|
16579
|
-
|
|
16580
|
-
|
|
16581
|
-
|
|
16582
|
-
|
|
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
|
-
|
|
16586
|
-
|
|
16587
|
-
|
|
16588
|
-
|
|
16589
|
-
|
|
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
|
-
|
|
16592
|
-
|
|
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);
|